@obolos_tech/cli 0.2.3 → 0.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/dist/index.d.ts +11 -0
- package/dist/index.js +1831 -0
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -10,6 +10,17 @@
|
|
|
10
10
|
* npx @obolos_tech/cli categories
|
|
11
11
|
* npx @obolos_tech/cli balance
|
|
12
12
|
* npx @obolos_tech/cli setup-mcp
|
|
13
|
+
* npx @obolos_tech/cli job list --status=open
|
|
14
|
+
* npx @obolos_tech/cli job create --title "..." --evaluator 0x...
|
|
15
|
+
* npx @obolos_tech/cli job info <id>
|
|
16
|
+
* npx @obolos_tech/cli listing list --status=open
|
|
17
|
+
* npx @obolos_tech/cli listing create --title "..." --max-budget 10.00
|
|
18
|
+
* npx @obolos_tech/cli listing bid <id> --price 5.00
|
|
19
|
+
* npx @obolos_tech/cli anp list --status=open
|
|
20
|
+
* npx @obolos_tech/cli anp create --title "..." --min-budget 5 --max-budget 50
|
|
21
|
+
* npx @obolos_tech/cli anp bid <cid> --price 25 --delivery 48h
|
|
22
|
+
* npx @obolos_tech/cli anp accept <cid> --bid <bid_cid>
|
|
23
|
+
* npx @obolos_tech/cli anp verify <cid>
|
|
13
24
|
*/
|
|
14
25
|
import { homedir } from 'os';
|
|
15
26
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
@@ -36,6 +47,103 @@ const config = loadConfig();
|
|
|
36
47
|
const OBOLOS_API_URL = process.env.OBOLOS_API_URL || config.api_url || 'https://obolos.tech';
|
|
37
48
|
const OBOLOS_PRIVATE_KEY = process.env.OBOLOS_PRIVATE_KEY || config.private_key || '';
|
|
38
49
|
// ─── Colors (no deps) ──────────────────────────────────────────────────────
|
|
50
|
+
// ─── ACP Contract ABIs (ERC-8183) ─────────────────────────────────────────
|
|
51
|
+
const ACP_ADDRESS = '0xaF3148696242F7Fb74893DC47690e37950807362';
|
|
52
|
+
const USDC_CONTRACT = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
|
|
53
|
+
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
54
|
+
const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000';
|
|
55
|
+
const ACP_ABI = [
|
|
56
|
+
{
|
|
57
|
+
type: 'function',
|
|
58
|
+
name: 'createJob',
|
|
59
|
+
inputs: [
|
|
60
|
+
{ name: 'provider', type: 'address' },
|
|
61
|
+
{ name: 'evaluator', type: 'address' },
|
|
62
|
+
{ name: 'expiredAt', type: 'uint256' },
|
|
63
|
+
{ name: 'description', type: 'string' },
|
|
64
|
+
{ name: 'hook', type: 'address' },
|
|
65
|
+
],
|
|
66
|
+
outputs: [{ name: 'jobId', type: 'uint256' }],
|
|
67
|
+
stateMutability: 'nonpayable',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
type: 'function',
|
|
71
|
+
name: 'fund',
|
|
72
|
+
inputs: [
|
|
73
|
+
{ name: 'jobId', type: 'uint256' },
|
|
74
|
+
{ name: 'expectedBudget', type: 'uint256' },
|
|
75
|
+
{ name: 'optParams', type: 'bytes' },
|
|
76
|
+
],
|
|
77
|
+
outputs: [],
|
|
78
|
+
stateMutability: 'nonpayable',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: 'function',
|
|
82
|
+
name: 'submit',
|
|
83
|
+
inputs: [
|
|
84
|
+
{ name: 'jobId', type: 'uint256' },
|
|
85
|
+
{ name: 'deliverable', type: 'bytes32' },
|
|
86
|
+
{ name: 'optParams', type: 'bytes' },
|
|
87
|
+
],
|
|
88
|
+
outputs: [],
|
|
89
|
+
stateMutability: 'nonpayable',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: 'function',
|
|
93
|
+
name: 'complete',
|
|
94
|
+
inputs: [
|
|
95
|
+
{ name: 'jobId', type: 'uint256' },
|
|
96
|
+
{ name: 'reason', type: 'bytes32' },
|
|
97
|
+
{ name: 'optParams', type: 'bytes' },
|
|
98
|
+
],
|
|
99
|
+
outputs: [],
|
|
100
|
+
stateMutability: 'nonpayable',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
type: 'function',
|
|
104
|
+
name: 'reject',
|
|
105
|
+
inputs: [
|
|
106
|
+
{ name: 'jobId', type: 'uint256' },
|
|
107
|
+
{ name: 'reason', type: 'bytes32' },
|
|
108
|
+
{ name: 'optParams', type: 'bytes' },
|
|
109
|
+
],
|
|
110
|
+
outputs: [],
|
|
111
|
+
stateMutability: 'nonpayable',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
type: 'event',
|
|
115
|
+
name: 'JobCreated',
|
|
116
|
+
inputs: [
|
|
117
|
+
{ name: 'jobId', type: 'uint256', indexed: true },
|
|
118
|
+
{ name: 'client', type: 'address', indexed: true },
|
|
119
|
+
{ name: 'provider', type: 'address', indexed: false },
|
|
120
|
+
{ name: 'evaluator', type: 'address', indexed: false },
|
|
121
|
+
{ name: 'expiredAt', type: 'uint256', indexed: false },
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
const ERC20_ABI = [
|
|
126
|
+
{
|
|
127
|
+
type: 'function',
|
|
128
|
+
name: 'approve',
|
|
129
|
+
inputs: [
|
|
130
|
+
{ name: 'spender', type: 'address' },
|
|
131
|
+
{ name: 'amount', type: 'uint256' },
|
|
132
|
+
],
|
|
133
|
+
outputs: [{ name: '', type: 'bool' }],
|
|
134
|
+
stateMutability: 'nonpayable',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
type: 'function',
|
|
138
|
+
name: 'allowance',
|
|
139
|
+
inputs: [
|
|
140
|
+
{ name: 'owner', type: 'address' },
|
|
141
|
+
{ name: 'spender', type: 'address' },
|
|
142
|
+
],
|
|
143
|
+
outputs: [{ name: '', type: 'uint256' }],
|
|
144
|
+
stateMutability: 'view',
|
|
145
|
+
},
|
|
146
|
+
];
|
|
39
147
|
const c = {
|
|
40
148
|
reset: '\x1b[0m',
|
|
41
149
|
bold: '\x1b[1m',
|
|
@@ -59,6 +167,258 @@ async function apiGet(path) {
|
|
|
59
167
|
throw new Error(`${res.status} ${res.statusText}`);
|
|
60
168
|
return res.json();
|
|
61
169
|
}
|
|
170
|
+
async function apiPost(path, body, headers) {
|
|
171
|
+
const res = await fetch(`${OBOLOS_API_URL}${path}`, {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
174
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
175
|
+
});
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
let msg = `${res.status} ${res.statusText}`;
|
|
178
|
+
try {
|
|
179
|
+
const err = await res.json();
|
|
180
|
+
if (err.error)
|
|
181
|
+
msg = err.error;
|
|
182
|
+
else if (err.message)
|
|
183
|
+
msg = err.message;
|
|
184
|
+
}
|
|
185
|
+
catch { }
|
|
186
|
+
throw new Error(msg);
|
|
187
|
+
}
|
|
188
|
+
return res.json();
|
|
189
|
+
}
|
|
190
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
191
|
+
function getFlag(args, name) {
|
|
192
|
+
// Supports --name=value and --name value
|
|
193
|
+
for (let i = 0; i < args.length; i++) {
|
|
194
|
+
if (args[i] === `--${name}` && args[i + 1] && !args[i + 1].startsWith('--')) {
|
|
195
|
+
return args[i + 1];
|
|
196
|
+
}
|
|
197
|
+
if (args[i].startsWith(`--${name}=`)) {
|
|
198
|
+
return args[i].slice(`--${name}=`.length);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
function getPositional(args, index) {
|
|
204
|
+
let pos = 0;
|
|
205
|
+
for (const arg of args) {
|
|
206
|
+
if (!arg.startsWith('--')) {
|
|
207
|
+
if (pos === index)
|
|
208
|
+
return arg;
|
|
209
|
+
pos++;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
function shortenAddr(addr) {
|
|
215
|
+
if (!addr)
|
|
216
|
+
return `${c.dim}—${c.reset}`;
|
|
217
|
+
if (addr.length <= 12)
|
|
218
|
+
return addr;
|
|
219
|
+
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
|
|
220
|
+
}
|
|
221
|
+
function shortenId(id) {
|
|
222
|
+
if (id.length <= 12)
|
|
223
|
+
return id;
|
|
224
|
+
return `${id.slice(0, 8)}...`;
|
|
225
|
+
}
|
|
226
|
+
function formatDate(iso) {
|
|
227
|
+
if (!iso)
|
|
228
|
+
return `${c.dim}—${c.reset}`;
|
|
229
|
+
const d = new Date(iso);
|
|
230
|
+
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
231
|
+
}
|
|
232
|
+
function statusColor(status) {
|
|
233
|
+
switch (status) {
|
|
234
|
+
case 'open': return `${c.yellow}${status}${c.reset}`;
|
|
235
|
+
case 'funded': return `${c.blue}${status}${c.reset}`;
|
|
236
|
+
case 'submitted': return `${c.cyan}${status}${c.reset}`;
|
|
237
|
+
case 'completed': return `${c.green}${status}${c.reset}`;
|
|
238
|
+
case 'rejected': return `${c.red}${status}${c.reset}`;
|
|
239
|
+
case 'expired': return `${c.gray}${status}${c.reset}`;
|
|
240
|
+
case 'negotiating': return `${c.magenta}${status}${c.reset}`;
|
|
241
|
+
case 'accepted': return `${c.green}${status}${c.reset}`;
|
|
242
|
+
case 'cancelled': return `${c.gray}${status}${c.reset}`;
|
|
243
|
+
default: return status;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function parseRelativeTime(input) {
|
|
247
|
+
const match = input.match(/^(\d+)\s*(h|hr|hrs|hour|hours|d|day|days|m|min|mins|minute|minutes)$/i);
|
|
248
|
+
if (!match) {
|
|
249
|
+
// Try parsing as ISO date directly
|
|
250
|
+
const d = new Date(input);
|
|
251
|
+
if (!isNaN(d.getTime()))
|
|
252
|
+
return d.toISOString();
|
|
253
|
+
throw new Error(`Cannot parse expiry: "${input}". Use formats like "24h", "7d", "1h", or an ISO date.`);
|
|
254
|
+
}
|
|
255
|
+
const num = parseInt(match[1], 10);
|
|
256
|
+
const unit = match[2].toLowerCase();
|
|
257
|
+
const now = Date.now();
|
|
258
|
+
let ms = 0;
|
|
259
|
+
if (unit.startsWith('h'))
|
|
260
|
+
ms = num * 60 * 60 * 1000;
|
|
261
|
+
else if (unit.startsWith('d'))
|
|
262
|
+
ms = num * 24 * 60 * 60 * 1000;
|
|
263
|
+
else if (unit.startsWith('m'))
|
|
264
|
+
ms = num * 60 * 1000;
|
|
265
|
+
return new Date(now + ms).toISOString();
|
|
266
|
+
}
|
|
267
|
+
function resolveWalletAddress() {
|
|
268
|
+
const cfg = loadConfig();
|
|
269
|
+
const addr = cfg.wallet_address;
|
|
270
|
+
if (addr)
|
|
271
|
+
return addr;
|
|
272
|
+
// Derive from private key if available
|
|
273
|
+
if (!OBOLOS_PRIVATE_KEY) {
|
|
274
|
+
console.error(`${c.red}No wallet configured.${c.reset} Run ${c.cyan}obolos setup${c.reset} first.`);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
// We'll derive it lazily — for now return empty and let callers handle async derivation
|
|
278
|
+
return '';
|
|
279
|
+
}
|
|
280
|
+
async function getWalletAddress() {
|
|
281
|
+
const cfg = loadConfig();
|
|
282
|
+
if (cfg.wallet_address)
|
|
283
|
+
return cfg.wallet_address;
|
|
284
|
+
if (!OBOLOS_PRIVATE_KEY) {
|
|
285
|
+
console.error(`${c.red}No wallet configured.${c.reset} Run ${c.cyan}obolos setup${c.reset} first.`);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
const { privateKeyToAccount } = await import('viem/accounts');
|
|
289
|
+
const key = OBOLOS_PRIVATE_KEY.startsWith('0x') ? OBOLOS_PRIVATE_KEY : `0x${OBOLOS_PRIVATE_KEY}`;
|
|
290
|
+
const account = privateKeyToAccount(key);
|
|
291
|
+
return account.address;
|
|
292
|
+
}
|
|
293
|
+
async function getACPClient() {
|
|
294
|
+
const key = OBOLOS_PRIVATE_KEY;
|
|
295
|
+
if (!key) {
|
|
296
|
+
console.error(`${c.red}No wallet configured.${c.reset} Run ${c.cyan}obolos setup${c.reset} first.`);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
const { createPublicClient, createWalletClient, http: viemHttp, parseUnits, keccak256, toHex, decodeEventLog } = await import('viem');
|
|
300
|
+
const { privateKeyToAccount } = await import('viem/accounts');
|
|
301
|
+
const { base } = await import('viem/chains');
|
|
302
|
+
const normalizedKey = key.startsWith('0x') ? key : `0x${key}`;
|
|
303
|
+
const account = privateKeyToAccount(normalizedKey);
|
|
304
|
+
const publicClient = createPublicClient({ chain: base, transport: viemHttp() });
|
|
305
|
+
const walletClient = createWalletClient({ account, chain: base, transport: viemHttp() });
|
|
306
|
+
return { account, publicClient, walletClient, parseUnits, keccak256, toHex, decodeEventLog };
|
|
307
|
+
}
|
|
308
|
+
function stateVisualization(status) {
|
|
309
|
+
const states = ['open', 'funded', 'submitted', 'completed'];
|
|
310
|
+
const parts = states.map(s => {
|
|
311
|
+
if (s === status)
|
|
312
|
+
return `${c.bold}[${s.charAt(0).toUpperCase() + s.slice(1)}]${c.reset}`;
|
|
313
|
+
return `${c.dim}${s.charAt(0).toUpperCase() + s.slice(1)}${c.reset}`;
|
|
314
|
+
});
|
|
315
|
+
// Handle terminal states that branch off
|
|
316
|
+
if (status === 'rejected') {
|
|
317
|
+
const base = states.slice(0, 3).map(s => `${c.dim}${s.charAt(0).toUpperCase() + s.slice(1)}${c.reset}`);
|
|
318
|
+
return ` ${base.join(` ${c.dim}->${c.reset} `)} ${c.dim}->${c.reset} ${c.bold}${c.red}[Rejected]${c.reset}`;
|
|
319
|
+
}
|
|
320
|
+
if (status === 'expired') {
|
|
321
|
+
return ` ${c.bold}${c.gray}[Expired]${c.reset}`;
|
|
322
|
+
}
|
|
323
|
+
return ` ${parts.join(` ${c.dim}->${c.reset} `)}`;
|
|
324
|
+
}
|
|
325
|
+
// ─── ANP Helpers ─────────────────────────────────────────────────────────────
|
|
326
|
+
function canonicalJSON(obj) {
|
|
327
|
+
if (obj === null || typeof obj !== 'object')
|
|
328
|
+
return JSON.stringify(obj);
|
|
329
|
+
if (Array.isArray(obj))
|
|
330
|
+
return '[' + obj.map(canonicalJSON).join(',') + ']';
|
|
331
|
+
const keys = Object.keys(obj).sort();
|
|
332
|
+
return '{' + keys.map(k => JSON.stringify(k) + ':' + canonicalJSON(obj[k])).join(',') + '}';
|
|
333
|
+
}
|
|
334
|
+
async function computeContentHash(data) {
|
|
335
|
+
const { createHash } = await import('crypto');
|
|
336
|
+
const hash = createHash('sha256').update(canonicalJSON(data)).digest('hex');
|
|
337
|
+
return `0x${hash}`;
|
|
338
|
+
}
|
|
339
|
+
function parseTimeToSeconds(input) {
|
|
340
|
+
const match = input.match(/^(\d+)\s*(s|sec|secs|second|seconds|h|hr|hrs|hour|hours|d|day|days|m|min|mins|minute|minutes)$/i);
|
|
341
|
+
if (!match) {
|
|
342
|
+
throw new Error(`Cannot parse time: "${input}". Use formats like "48h", "7d", "3d".`);
|
|
343
|
+
}
|
|
344
|
+
const num = parseInt(match[1], 10);
|
|
345
|
+
const unit = match[2].toLowerCase();
|
|
346
|
+
if (unit.startsWith('s'))
|
|
347
|
+
return num;
|
|
348
|
+
if (unit.startsWith('m'))
|
|
349
|
+
return num * 60;
|
|
350
|
+
if (unit.startsWith('h'))
|
|
351
|
+
return num * 3600;
|
|
352
|
+
if (unit.startsWith('d'))
|
|
353
|
+
return num * 86400;
|
|
354
|
+
return num;
|
|
355
|
+
}
|
|
356
|
+
const ANP_DOMAIN = {
|
|
357
|
+
name: 'ANP',
|
|
358
|
+
version: '1',
|
|
359
|
+
chainId: 8453,
|
|
360
|
+
verifyingContract: '0xfEa362Bf569e97B20681289fB4D4a64CEBDFa792',
|
|
361
|
+
};
|
|
362
|
+
const ANP_LISTING_TYPES = {
|
|
363
|
+
ListingIntent: [
|
|
364
|
+
{ name: 'contentHash', type: 'bytes32' },
|
|
365
|
+
{ name: 'minBudget', type: 'uint256' },
|
|
366
|
+
{ name: 'maxBudget', type: 'uint256' },
|
|
367
|
+
{ name: 'deadline', type: 'uint256' },
|
|
368
|
+
{ name: 'jobDuration', type: 'uint256' },
|
|
369
|
+
{ name: 'preferredEvaluator', type: 'address' },
|
|
370
|
+
{ name: 'nonce', type: 'uint256' },
|
|
371
|
+
],
|
|
372
|
+
};
|
|
373
|
+
const ANP_BID_TYPES = {
|
|
374
|
+
BidIntent: [
|
|
375
|
+
{ name: 'listingHash', type: 'bytes32' },
|
|
376
|
+
{ name: 'contentHash', type: 'bytes32' },
|
|
377
|
+
{ name: 'price', type: 'uint256' },
|
|
378
|
+
{ name: 'deliveryTime', type: 'uint256' },
|
|
379
|
+
{ name: 'nonce', type: 'uint256' },
|
|
380
|
+
],
|
|
381
|
+
};
|
|
382
|
+
const ANP_ACCEPT_TYPES = {
|
|
383
|
+
AcceptIntent: [
|
|
384
|
+
{ name: 'listingHash', type: 'bytes32' },
|
|
385
|
+
{ name: 'bidHash', type: 'bytes32' },
|
|
386
|
+
{ name: 'nonce', type: 'uint256' },
|
|
387
|
+
],
|
|
388
|
+
};
|
|
389
|
+
async function getANPSigningClient() {
|
|
390
|
+
if (!OBOLOS_PRIVATE_KEY) {
|
|
391
|
+
console.error(`${c.red}No wallet configured.${c.reset} Run ${c.cyan}obolos setup${c.reset} first.`);
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
const { createWalletClient, http: viemHttp, encodeAbiParameters, keccak256 } = await import('viem');
|
|
395
|
+
const { privateKeyToAccount } = await import('viem/accounts');
|
|
396
|
+
const { base } = await import('viem/chains');
|
|
397
|
+
const key = OBOLOS_PRIVATE_KEY.startsWith('0x') ? OBOLOS_PRIVATE_KEY : `0x${OBOLOS_PRIVATE_KEY}`;
|
|
398
|
+
const account = privateKeyToAccount(key);
|
|
399
|
+
const walletClient = createWalletClient({ account, chain: base, transport: viemHttp() });
|
|
400
|
+
const LISTING_TYPEHASH = keccak256(Buffer.from('ListingIntent(bytes32 contentHash,uint256 minBudget,uint256 maxBudget,uint256 deadline,uint256 jobDuration,address preferredEvaluator,uint256 nonce)'));
|
|
401
|
+
const BID_TYPEHASH = keccak256(Buffer.from('BidIntent(bytes32 listingHash,bytes32 contentHash,uint256 price,uint256 deliveryTime,uint256 nonce)'));
|
|
402
|
+
const ACCEPT_TYPEHASH = keccak256(Buffer.from('AcceptIntent(bytes32 listingHash,bytes32 bidHash,uint256 nonce)'));
|
|
403
|
+
function hashListingStruct(listing) {
|
|
404
|
+
return keccak256(encodeAbiParameters([{ type: 'bytes32' }, { type: 'bytes32' }, { type: 'uint256' }, { type: 'uint256' }, { type: 'uint256' }, { type: 'uint256' }, { type: 'address' }, { type: 'uint256' }], [LISTING_TYPEHASH, listing.contentHash, listing.minBudget, listing.maxBudget, listing.deadline, listing.jobDuration, listing.preferredEvaluator, listing.nonce]));
|
|
405
|
+
}
|
|
406
|
+
function hashBidStruct(bid) {
|
|
407
|
+
return keccak256(encodeAbiParameters([{ type: 'bytes32' }, { type: 'bytes32' }, { type: 'bytes32' }, { type: 'uint256' }, { type: 'uint256' }, { type: 'uint256' }], [BID_TYPEHASH, bid.listingHash, bid.contentHash, bid.price, bid.deliveryTime, bid.nonce]));
|
|
408
|
+
}
|
|
409
|
+
function hashAcceptStruct(accept) {
|
|
410
|
+
return keccak256(encodeAbiParameters([{ type: 'bytes32' }, { type: 'bytes32' }, { type: 'bytes32' }, { type: 'uint256' }], [ACCEPT_TYPEHASH, accept.listingHash, accept.bidHash, accept.nonce]));
|
|
411
|
+
}
|
|
412
|
+
return { account, walletClient, hashListingStruct, hashBidStruct, hashAcceptStruct };
|
|
413
|
+
}
|
|
414
|
+
function generateNonce() {
|
|
415
|
+
const bytes = new Uint8Array(8);
|
|
416
|
+
crypto.getRandomValues(bytes);
|
|
417
|
+
let val = 0n;
|
|
418
|
+
for (const b of bytes)
|
|
419
|
+
val = (val << 8n) | BigInt(b);
|
|
420
|
+
return val;
|
|
421
|
+
}
|
|
62
422
|
// ─── Commands ───────────────────────────────────────────────────────────────
|
|
63
423
|
async function cmdSearch(args) {
|
|
64
424
|
const query = args.join(' ');
|
|
@@ -469,6 +829,1428 @@ async function cmdSetupMcp() {
|
|
|
469
829
|
console.log(` }`);
|
|
470
830
|
console.log(` }${c.reset}\n`);
|
|
471
831
|
}
|
|
832
|
+
// ─── Job Commands (ERC-8183 ACP) ────────────────────────────────────────────
|
|
833
|
+
async function cmdJobList(args) {
|
|
834
|
+
const params = new URLSearchParams();
|
|
835
|
+
const status = getFlag(args, 'status');
|
|
836
|
+
const client = getFlag(args, 'client');
|
|
837
|
+
const provider = getFlag(args, 'provider');
|
|
838
|
+
const limit = getFlag(args, 'limit') || '20';
|
|
839
|
+
if (status)
|
|
840
|
+
params.set('status', status);
|
|
841
|
+
if (client)
|
|
842
|
+
params.set('client', client);
|
|
843
|
+
if (provider)
|
|
844
|
+
params.set('provider', provider);
|
|
845
|
+
params.set('limit', limit);
|
|
846
|
+
const data = await apiGet(`/api/jobs?${params}`);
|
|
847
|
+
const jobs = data.jobs || data.data || [];
|
|
848
|
+
if (jobs.length === 0) {
|
|
849
|
+
console.log(`${c.yellow}No jobs found.${c.reset}`);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const total = data.pagination?.total || data.total || jobs.length;
|
|
853
|
+
console.log(`\n${c.bold}${c.cyan}ACP Jobs${c.reset} ${c.dim}— ${total} jobs${c.reset}\n`);
|
|
854
|
+
// Table header
|
|
855
|
+
console.log(` ${c.bold}${'ID'.padEnd(12)} ${'Title'.padEnd(30)} ${'Status'.padEnd(12)} ${'Budget'.padEnd(12)} ${'Client'.padEnd(14)} ${'Provider'.padEnd(14)} Created${c.reset}`);
|
|
856
|
+
console.log(` ${c.dim}${'─'.repeat(110)}${c.reset}`);
|
|
857
|
+
for (const job of jobs) {
|
|
858
|
+
const id = shortenId(job.id || '');
|
|
859
|
+
const title = (job.title || 'Untitled').slice(0, 28).padEnd(30);
|
|
860
|
+
const st = statusColor((job.status || 'open').padEnd(10));
|
|
861
|
+
const budget = job.budget != null ? `$${Number(job.budget).toFixed(2)}`.padEnd(12) : `${c.dim}—${c.reset}`.padEnd(12);
|
|
862
|
+
const cl = shortenAddr(job.client).padEnd(14);
|
|
863
|
+
const prov = job.provider ? shortenAddr(job.provider).padEnd(14) : `${c.dim}Open${c.reset}`.padEnd(14);
|
|
864
|
+
const created = formatDate(job.created_at || job.createdAt);
|
|
865
|
+
console.log(` ${id.padEnd(12)} ${title} ${st} ${budget} ${cl} ${prov} ${created}`);
|
|
866
|
+
}
|
|
867
|
+
console.log(`\n${c.dim}Use: obolos job info <id> for full details${c.reset}\n`);
|
|
868
|
+
}
|
|
869
|
+
async function cmdJobCreate(args) {
|
|
870
|
+
const title = getFlag(args, 'title');
|
|
871
|
+
const description = getFlag(args, 'description');
|
|
872
|
+
const evaluator = getFlag(args, 'evaluator');
|
|
873
|
+
const provider = getFlag(args, 'provider');
|
|
874
|
+
const budget = getFlag(args, 'budget');
|
|
875
|
+
const expires = getFlag(args, 'expires');
|
|
876
|
+
if (!title) {
|
|
877
|
+
console.error(`${c.red}Usage: obolos job create --title "..." --description "..." --evaluator 0x... [--provider 0x...] [--budget 1.00] [--expires 24h]${c.reset}`);
|
|
878
|
+
process.exit(1);
|
|
879
|
+
}
|
|
880
|
+
if (!evaluator) {
|
|
881
|
+
console.error(`${c.red}--evaluator is required. Provide the evaluator address (0x...).${c.reset}`);
|
|
882
|
+
process.exit(1);
|
|
883
|
+
}
|
|
884
|
+
const walletAddress = await getWalletAddress();
|
|
885
|
+
// Create job on-chain first
|
|
886
|
+
let chainJobId = null;
|
|
887
|
+
let chainTxHash = null;
|
|
888
|
+
try {
|
|
889
|
+
const acp = await getACPClient();
|
|
890
|
+
// Parse expiry to unix timestamp (default: 7 days)
|
|
891
|
+
let expiredAt;
|
|
892
|
+
if (expires) {
|
|
893
|
+
const parsed = parseRelativeTime(expires);
|
|
894
|
+
expiredAt = Math.floor(new Date(parsed).getTime() / 1000);
|
|
895
|
+
}
|
|
896
|
+
else {
|
|
897
|
+
expiredAt = Math.floor((Date.now() + 7 * 86400000) / 1000);
|
|
898
|
+
}
|
|
899
|
+
console.log(`\n ${c.dim}Creating job on-chain...${c.reset}`);
|
|
900
|
+
const txHash = await acp.walletClient.writeContract({
|
|
901
|
+
address: ACP_ADDRESS,
|
|
902
|
+
abi: ACP_ABI,
|
|
903
|
+
functionName: 'createJob',
|
|
904
|
+
args: [
|
|
905
|
+
(provider || ZERO_ADDRESS),
|
|
906
|
+
evaluator,
|
|
907
|
+
BigInt(expiredAt),
|
|
908
|
+
description || title,
|
|
909
|
+
ZERO_ADDRESS,
|
|
910
|
+
],
|
|
911
|
+
account: acp.account,
|
|
912
|
+
chain: (await import('viem/chains')).base,
|
|
913
|
+
});
|
|
914
|
+
console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
|
|
915
|
+
const receipt = await acp.publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
916
|
+
// Extract jobId from JobCreated event
|
|
917
|
+
for (const log of receipt.logs) {
|
|
918
|
+
try {
|
|
919
|
+
const decoded = acp.decodeEventLog({
|
|
920
|
+
abi: ACP_ABI,
|
|
921
|
+
data: log.data,
|
|
922
|
+
topics: log.topics,
|
|
923
|
+
});
|
|
924
|
+
if (decoded.eventName === 'JobCreated') {
|
|
925
|
+
chainJobId = (decoded.args.jobId).toString();
|
|
926
|
+
break;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
catch { }
|
|
930
|
+
}
|
|
931
|
+
chainTxHash = txHash;
|
|
932
|
+
console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}`);
|
|
933
|
+
if (chainJobId) {
|
|
934
|
+
console.log(` ${c.green}Chain job ID: ${chainJobId}${c.reset}`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
catch (err) {
|
|
938
|
+
console.error(` ${c.yellow}On-chain creation failed: ${err.message}${c.reset}`);
|
|
939
|
+
console.error(` ${c.dim}Falling back to backend-only...${c.reset}`);
|
|
940
|
+
}
|
|
941
|
+
const payload = {
|
|
942
|
+
title,
|
|
943
|
+
evaluator,
|
|
944
|
+
};
|
|
945
|
+
if (description)
|
|
946
|
+
payload.description = description;
|
|
947
|
+
if (provider)
|
|
948
|
+
payload.provider = provider;
|
|
949
|
+
if (budget)
|
|
950
|
+
payload.budget = parseFloat(budget);
|
|
951
|
+
if (expires)
|
|
952
|
+
payload.expires_at = parseRelativeTime(expires);
|
|
953
|
+
if (chainJobId)
|
|
954
|
+
payload.chain_job_id = chainJobId;
|
|
955
|
+
if (chainTxHash)
|
|
956
|
+
payload.chain_tx_hash = chainTxHash;
|
|
957
|
+
const data = await apiPost('/api/jobs', payload, {
|
|
958
|
+
'x-wallet-address': walletAddress,
|
|
959
|
+
});
|
|
960
|
+
const job = data.job || data;
|
|
961
|
+
console.log(`\n${c.green}Job created successfully!${c.reset}\n`);
|
|
962
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
963
|
+
console.log(` ${c.bold}ID:${c.reset} ${job.id}`);
|
|
964
|
+
if (chainJobId) {
|
|
965
|
+
console.log(` ${c.bold}Chain ID:${c.reset} ${chainJobId}`);
|
|
966
|
+
}
|
|
967
|
+
console.log(` ${c.bold}Title:${c.reset} ${job.title}`);
|
|
968
|
+
if (job.description) {
|
|
969
|
+
console.log(` ${c.bold}Description:${c.reset} ${job.description}`);
|
|
970
|
+
}
|
|
971
|
+
console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'open')}`);
|
|
972
|
+
console.log(` ${c.bold}Client:${c.reset} ${job.client || walletAddress}`);
|
|
973
|
+
console.log(` ${c.bold}Evaluator:${c.reset} ${job.evaluator}`);
|
|
974
|
+
if (job.provider) {
|
|
975
|
+
console.log(` ${c.bold}Provider:${c.reset} ${job.provider}`);
|
|
976
|
+
}
|
|
977
|
+
if (job.budget != null) {
|
|
978
|
+
console.log(` ${c.bold}Budget:${c.reset} ${c.green}$${Number(job.budget).toFixed(2)} USDC${c.reset}`);
|
|
979
|
+
}
|
|
980
|
+
if (job.expires_at) {
|
|
981
|
+
console.log(` ${c.bold}Expires:${c.reset} ${formatDate(job.expires_at)}`);
|
|
982
|
+
}
|
|
983
|
+
if (chainTxHash) {
|
|
984
|
+
console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${chainTxHash}${c.reset}`);
|
|
985
|
+
}
|
|
986
|
+
console.log(`\n${c.dim}Next: obolos job fund ${job.id}${c.reset}\n`);
|
|
987
|
+
}
|
|
988
|
+
async function cmdJobInfo(args) {
|
|
989
|
+
const id = getPositional(args, 0);
|
|
990
|
+
if (!id) {
|
|
991
|
+
console.error(`${c.red}Usage: obolos job info <id>${c.reset}`);
|
|
992
|
+
process.exit(1);
|
|
993
|
+
}
|
|
994
|
+
const data = await apiGet(`/api/jobs/${encodeURIComponent(id)}`);
|
|
995
|
+
const job = data.job || data;
|
|
996
|
+
console.log(`\n${c.bold}${c.cyan}${job.title || 'Untitled Job'}${c.reset}`);
|
|
997
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
998
|
+
console.log(` ${c.bold}ID:${c.reset} ${job.id}`);
|
|
999
|
+
console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'open')}`);
|
|
1000
|
+
// State machine visualization
|
|
1001
|
+
console.log(` ${c.bold}Progress:${c.reset}`);
|
|
1002
|
+
console.log(stateVisualization(job.status || 'open'));
|
|
1003
|
+
console.log(` ${c.bold}Client:${c.reset} ${job.client || `${c.dim}—${c.reset}`}`);
|
|
1004
|
+
console.log(` ${c.bold}Evaluator:${c.reset} ${job.evaluator || `${c.dim}—${c.reset}`}`);
|
|
1005
|
+
console.log(` ${c.bold}Provider:${c.reset} ${job.provider || `${c.dim}Open (anyone can claim)${c.reset}`}`);
|
|
1006
|
+
if (job.budget != null) {
|
|
1007
|
+
console.log(` ${c.bold}Budget:${c.reset} ${c.green}$${Number(job.budget).toFixed(2)} USDC${c.reset}`);
|
|
1008
|
+
}
|
|
1009
|
+
if (job.description) {
|
|
1010
|
+
console.log(`\n ${c.bold}Description:${c.reset}`);
|
|
1011
|
+
const descLines = job.description.split('\n');
|
|
1012
|
+
for (const line of descLines) {
|
|
1013
|
+
console.log(` ${line}`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (job.deliverable) {
|
|
1017
|
+
console.log(`\n ${c.bold}Deliverable:${c.reset} ${c.cyan}${job.deliverable}${c.reset}`);
|
|
1018
|
+
}
|
|
1019
|
+
if (job.reason) {
|
|
1020
|
+
console.log(` ${c.bold}Reason:${c.reset} ${job.reason}`);
|
|
1021
|
+
}
|
|
1022
|
+
if (job.expires_at) {
|
|
1023
|
+
const expiryDate = new Date(job.expires_at);
|
|
1024
|
+
const now = new Date();
|
|
1025
|
+
const expired = expiryDate < now;
|
|
1026
|
+
console.log(` ${c.bold}Expires:${c.reset} ${expired ? c.red : c.dim}${formatDate(job.expires_at)}${expired ? ' (expired)' : ''}${c.reset}`);
|
|
1027
|
+
}
|
|
1028
|
+
console.log(` ${c.bold}Created:${c.reset} ${formatDate(job.created_at || job.createdAt)}`);
|
|
1029
|
+
if (job.updated_at || job.updatedAt) {
|
|
1030
|
+
console.log(` ${c.bold}Updated:${c.reset} ${formatDate(job.updated_at || job.updatedAt)}`);
|
|
1031
|
+
}
|
|
1032
|
+
// Actions hint based on status
|
|
1033
|
+
console.log();
|
|
1034
|
+
const s = job.status || 'open';
|
|
1035
|
+
if (s === 'open') {
|
|
1036
|
+
console.log(` ${c.bold}Actions:${c.reset}`);
|
|
1037
|
+
console.log(` obolos job fund ${job.id} ${c.dim}Fund the escrow${c.reset}`);
|
|
1038
|
+
}
|
|
1039
|
+
else if (s === 'funded') {
|
|
1040
|
+
console.log(` ${c.bold}Actions:${c.reset}`);
|
|
1041
|
+
console.log(` obolos job submit ${job.id} --deliverable <hash> ${c.dim}Submit work${c.reset}`);
|
|
1042
|
+
}
|
|
1043
|
+
else if (s === 'submitted') {
|
|
1044
|
+
console.log(` ${c.bold}Actions:${c.reset}`);
|
|
1045
|
+
console.log(` obolos job complete ${job.id} ${c.dim}Approve and release funds${c.reset}`);
|
|
1046
|
+
console.log(` obolos job reject ${job.id} ${c.dim}Reject the submission${c.reset}`);
|
|
1047
|
+
}
|
|
1048
|
+
console.log();
|
|
1049
|
+
}
|
|
1050
|
+
async function cmdJobFund(args) {
|
|
1051
|
+
const id = getPositional(args, 0);
|
|
1052
|
+
if (!id) {
|
|
1053
|
+
console.error(`${c.red}Usage: obolos job fund <id>${c.reset}`);
|
|
1054
|
+
process.exit(1);
|
|
1055
|
+
}
|
|
1056
|
+
const walletAddress = await getWalletAddress();
|
|
1057
|
+
// First fetch the job to show budget info
|
|
1058
|
+
const jobData = await apiGet(`/api/jobs/${encodeURIComponent(id)}`);
|
|
1059
|
+
const job = jobData.job || jobData;
|
|
1060
|
+
console.log(`\n${c.bold}${c.cyan}Fund Job${c.reset}\n`);
|
|
1061
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
1062
|
+
console.log(` ${c.bold}Job:${c.reset} ${job.title || id}`);
|
|
1063
|
+
if (job.budget != null) {
|
|
1064
|
+
console.log(` ${c.bold}Budget:${c.reset} ${c.green}$${Number(job.budget).toFixed(2)} USDC${c.reset}`);
|
|
1065
|
+
}
|
|
1066
|
+
console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'open')}`);
|
|
1067
|
+
console.log();
|
|
1068
|
+
const chainJobId = job.chain_job_id;
|
|
1069
|
+
let txHash = null;
|
|
1070
|
+
if (chainJobId && job.budget != null) {
|
|
1071
|
+
try {
|
|
1072
|
+
const acp = await getACPClient();
|
|
1073
|
+
const budgetStr = String(job.budget);
|
|
1074
|
+
// Check USDC allowance and approve if needed
|
|
1075
|
+
console.log(` ${c.dim}Checking USDC allowance...${c.reset}`);
|
|
1076
|
+
const amount = acp.parseUnits(budgetStr, 6);
|
|
1077
|
+
const allowance = await acp.publicClient.readContract({
|
|
1078
|
+
address: USDC_CONTRACT,
|
|
1079
|
+
abi: ERC20_ABI,
|
|
1080
|
+
functionName: 'allowance',
|
|
1081
|
+
args: [acp.account.address, ACP_ADDRESS],
|
|
1082
|
+
});
|
|
1083
|
+
if (allowance < amount) {
|
|
1084
|
+
console.log(` ${c.dim}Approving USDC spend...${c.reset}`);
|
|
1085
|
+
const approveTx = await acp.walletClient.writeContract({
|
|
1086
|
+
address: USDC_CONTRACT,
|
|
1087
|
+
abi: ERC20_ABI,
|
|
1088
|
+
functionName: 'approve',
|
|
1089
|
+
args: [ACP_ADDRESS, amount],
|
|
1090
|
+
account: acp.account,
|
|
1091
|
+
chain: (await import('viem/chains')).base,
|
|
1092
|
+
});
|
|
1093
|
+
await acp.publicClient.waitForTransactionReceipt({ hash: approveTx });
|
|
1094
|
+
console.log(` ${c.green}USDC approved${c.reset}`);
|
|
1095
|
+
}
|
|
1096
|
+
console.log(` ${c.dim}Funding escrow on-chain...${c.reset}`);
|
|
1097
|
+
const fundTx = await acp.walletClient.writeContract({
|
|
1098
|
+
address: ACP_ADDRESS,
|
|
1099
|
+
abi: ACP_ABI,
|
|
1100
|
+
functionName: 'fund',
|
|
1101
|
+
args: [BigInt(chainJobId), amount, '0x'],
|
|
1102
|
+
account: acp.account,
|
|
1103
|
+
chain: (await import('viem/chains')).base,
|
|
1104
|
+
});
|
|
1105
|
+
console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
|
|
1106
|
+
await acp.publicClient.waitForTransactionReceipt({ hash: fundTx });
|
|
1107
|
+
txHash = fundTx;
|
|
1108
|
+
console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}\n`);
|
|
1109
|
+
}
|
|
1110
|
+
catch (err) {
|
|
1111
|
+
console.error(` ${c.yellow}On-chain funding failed: ${err.message}${c.reset}`);
|
|
1112
|
+
console.error(` ${c.dim}Recording funding intent in backend...${c.reset}\n`);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
// Update backend
|
|
1116
|
+
const fundPayload = {};
|
|
1117
|
+
if (txHash)
|
|
1118
|
+
fundPayload.tx_hash = txHash;
|
|
1119
|
+
if (chainJobId)
|
|
1120
|
+
fundPayload.chain_job_id = chainJobId;
|
|
1121
|
+
const data = await apiPost(`/api/jobs/${encodeURIComponent(id)}/fund`, fundPayload, {
|
|
1122
|
+
'x-wallet-address': walletAddress,
|
|
1123
|
+
});
|
|
1124
|
+
const updated = data.job || data;
|
|
1125
|
+
console.log(`${c.green}Job funded successfully!${c.reset}`);
|
|
1126
|
+
console.log(` ${c.bold}Status:${c.reset} ${statusColor(updated.status || 'funded')}`);
|
|
1127
|
+
if (txHash) {
|
|
1128
|
+
console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${txHash}${c.reset}`);
|
|
1129
|
+
}
|
|
1130
|
+
console.log(`${c.dim}Next: Provider submits work with: obolos job submit ${id} --deliverable <hash>${c.reset}\n`);
|
|
1131
|
+
}
|
|
1132
|
+
async function cmdJobSubmit(args) {
|
|
1133
|
+
const id = getPositional(args, 0);
|
|
1134
|
+
if (!id) {
|
|
1135
|
+
console.error(`${c.red}Usage: obolos job submit <id> --deliverable <hash/CID/URL>${c.reset}`);
|
|
1136
|
+
process.exit(1);
|
|
1137
|
+
}
|
|
1138
|
+
const deliverable = getFlag(args, 'deliverable');
|
|
1139
|
+
if (!deliverable) {
|
|
1140
|
+
console.error(`${c.red}--deliverable is required. Provide a hash, CID, or URL for the work product.${c.reset}`);
|
|
1141
|
+
process.exit(1);
|
|
1142
|
+
}
|
|
1143
|
+
const walletAddress = await getWalletAddress();
|
|
1144
|
+
// Fetch job to get chain_job_id
|
|
1145
|
+
const jobData = await apiGet(`/api/jobs/${encodeURIComponent(id)}`);
|
|
1146
|
+
const existingJob = jobData.job || jobData;
|
|
1147
|
+
const chainJobId = existingJob.chain_job_id;
|
|
1148
|
+
let txHash = null;
|
|
1149
|
+
if (chainJobId) {
|
|
1150
|
+
try {
|
|
1151
|
+
const acp = await getACPClient();
|
|
1152
|
+
const deliverableHash = acp.keccak256(acp.toHex(deliverable));
|
|
1153
|
+
console.log(`\n ${c.dim}Submitting work on-chain...${c.reset}`);
|
|
1154
|
+
const submitTx = await acp.walletClient.writeContract({
|
|
1155
|
+
address: ACP_ADDRESS,
|
|
1156
|
+
abi: ACP_ABI,
|
|
1157
|
+
functionName: 'submit',
|
|
1158
|
+
args: [BigInt(chainJobId), deliverableHash, '0x'],
|
|
1159
|
+
account: acp.account,
|
|
1160
|
+
chain: (await import('viem/chains')).base,
|
|
1161
|
+
});
|
|
1162
|
+
console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
|
|
1163
|
+
await acp.publicClient.waitForTransactionReceipt({ hash: submitTx });
|
|
1164
|
+
txHash = submitTx;
|
|
1165
|
+
console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}`);
|
|
1166
|
+
}
|
|
1167
|
+
catch (err) {
|
|
1168
|
+
console.error(` ${c.yellow}On-chain submission failed: ${err.message}${c.reset}`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
const submitPayload = { deliverable };
|
|
1172
|
+
if (txHash)
|
|
1173
|
+
submitPayload.tx_hash = txHash;
|
|
1174
|
+
const data = await apiPost(`/api/jobs/${encodeURIComponent(id)}/submit`, submitPayload, {
|
|
1175
|
+
'x-wallet-address': walletAddress,
|
|
1176
|
+
});
|
|
1177
|
+
const job = data.job || data;
|
|
1178
|
+
console.log(`\n${c.green}Work submitted successfully!${c.reset}\n`);
|
|
1179
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
1180
|
+
console.log(` ${c.bold}Job:${c.reset} ${job.title || id}`);
|
|
1181
|
+
console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'submitted')}`);
|
|
1182
|
+
console.log(` ${c.bold}Deliverable:${c.reset} ${c.cyan}${deliverable}${c.reset}`);
|
|
1183
|
+
if (txHash) {
|
|
1184
|
+
console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${txHash}${c.reset}`);
|
|
1185
|
+
}
|
|
1186
|
+
console.log(`\n${c.dim}The evaluator will now review and approve or reject the submission.${c.reset}\n`);
|
|
1187
|
+
}
|
|
1188
|
+
async function cmdJobComplete(args) {
|
|
1189
|
+
const id = getPositional(args, 0);
|
|
1190
|
+
if (!id) {
|
|
1191
|
+
console.error(`${c.red}Usage: obolos job complete <id> [--reason "..."]${c.reset}`);
|
|
1192
|
+
process.exit(1);
|
|
1193
|
+
}
|
|
1194
|
+
const reason = getFlag(args, 'reason');
|
|
1195
|
+
const walletAddress = await getWalletAddress();
|
|
1196
|
+
// Fetch job to get chain_job_id
|
|
1197
|
+
const jobData = await apiGet(`/api/jobs/${encodeURIComponent(id)}`);
|
|
1198
|
+
const existingJob = jobData.job || jobData;
|
|
1199
|
+
const chainJobId = existingJob.chain_job_id;
|
|
1200
|
+
let txHash = null;
|
|
1201
|
+
if (chainJobId) {
|
|
1202
|
+
try {
|
|
1203
|
+
const acp = await getACPClient();
|
|
1204
|
+
const reasonHash = reason
|
|
1205
|
+
? acp.keccak256(acp.toHex(reason))
|
|
1206
|
+
: ZERO_BYTES32;
|
|
1207
|
+
console.log(`\n ${c.dim}Completing job on-chain...${c.reset}`);
|
|
1208
|
+
const completeTx = await acp.walletClient.writeContract({
|
|
1209
|
+
address: ACP_ADDRESS,
|
|
1210
|
+
abi: ACP_ABI,
|
|
1211
|
+
functionName: 'complete',
|
|
1212
|
+
args: [BigInt(chainJobId), reasonHash, '0x'],
|
|
1213
|
+
account: acp.account,
|
|
1214
|
+
chain: (await import('viem/chains')).base,
|
|
1215
|
+
});
|
|
1216
|
+
console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
|
|
1217
|
+
await acp.publicClient.waitForTransactionReceipt({ hash: completeTx });
|
|
1218
|
+
txHash = completeTx;
|
|
1219
|
+
console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}`);
|
|
1220
|
+
}
|
|
1221
|
+
catch (err) {
|
|
1222
|
+
console.error(` ${c.yellow}On-chain completion failed: ${err.message}${c.reset}`);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
const payload = {};
|
|
1226
|
+
if (reason)
|
|
1227
|
+
payload.reason = reason;
|
|
1228
|
+
if (txHash)
|
|
1229
|
+
payload.tx_hash = txHash;
|
|
1230
|
+
const data = await apiPost(`/api/jobs/${encodeURIComponent(id)}/complete`, payload, {
|
|
1231
|
+
'x-wallet-address': walletAddress,
|
|
1232
|
+
});
|
|
1233
|
+
const job = data.job || data;
|
|
1234
|
+
console.log(`\n${c.green}Job completed and approved!${c.reset}\n`);
|
|
1235
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
1236
|
+
console.log(` ${c.bold}Job:${c.reset} ${job.title || id}`);
|
|
1237
|
+
console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'completed')}`);
|
|
1238
|
+
if (reason) {
|
|
1239
|
+
console.log(` ${c.bold}Reason:${c.reset} ${reason}`);
|
|
1240
|
+
}
|
|
1241
|
+
if (txHash) {
|
|
1242
|
+
console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${txHash}${c.reset}`);
|
|
1243
|
+
}
|
|
1244
|
+
if (job.budget != null) {
|
|
1245
|
+
console.log(`\n ${c.dim}Escrow of $${Number(job.budget).toFixed(2)} USDC released to provider.${c.reset}`);
|
|
1246
|
+
}
|
|
1247
|
+
console.log();
|
|
1248
|
+
}
|
|
1249
|
+
async function cmdJobReject(args) {
|
|
1250
|
+
const id = getPositional(args, 0);
|
|
1251
|
+
if (!id) {
|
|
1252
|
+
console.error(`${c.red}Usage: obolos job reject <id> [--reason "..."]${c.reset}`);
|
|
1253
|
+
process.exit(1);
|
|
1254
|
+
}
|
|
1255
|
+
const reason = getFlag(args, 'reason');
|
|
1256
|
+
const walletAddress = await getWalletAddress();
|
|
1257
|
+
// Fetch job to get chain_job_id
|
|
1258
|
+
const jobData = await apiGet(`/api/jobs/${encodeURIComponent(id)}`);
|
|
1259
|
+
const existingJob = jobData.job || jobData;
|
|
1260
|
+
const chainJobId = existingJob.chain_job_id;
|
|
1261
|
+
let txHash = null;
|
|
1262
|
+
if (chainJobId) {
|
|
1263
|
+
try {
|
|
1264
|
+
const acp = await getACPClient();
|
|
1265
|
+
const reasonHash = reason
|
|
1266
|
+
? acp.keccak256(acp.toHex(reason))
|
|
1267
|
+
: ZERO_BYTES32;
|
|
1268
|
+
console.log(`\n ${c.dim}Rejecting job on-chain...${c.reset}`);
|
|
1269
|
+
const rejectTx = await acp.walletClient.writeContract({
|
|
1270
|
+
address: ACP_ADDRESS,
|
|
1271
|
+
abi: ACP_ABI,
|
|
1272
|
+
functionName: 'reject',
|
|
1273
|
+
args: [BigInt(chainJobId), reasonHash, '0x'],
|
|
1274
|
+
account: acp.account,
|
|
1275
|
+
chain: (await import('viem/chains')).base,
|
|
1276
|
+
});
|
|
1277
|
+
console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
|
|
1278
|
+
await acp.publicClient.waitForTransactionReceipt({ hash: rejectTx });
|
|
1279
|
+
txHash = rejectTx;
|
|
1280
|
+
console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}`);
|
|
1281
|
+
}
|
|
1282
|
+
catch (err) {
|
|
1283
|
+
console.error(` ${c.yellow}On-chain rejection failed: ${err.message}${c.reset}`);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
const payload = {};
|
|
1287
|
+
if (reason)
|
|
1288
|
+
payload.reason = reason;
|
|
1289
|
+
if (txHash)
|
|
1290
|
+
payload.tx_hash = txHash;
|
|
1291
|
+
const data = await apiPost(`/api/jobs/${encodeURIComponent(id)}/reject`, payload, {
|
|
1292
|
+
'x-wallet-address': walletAddress,
|
|
1293
|
+
});
|
|
1294
|
+
const job = data.job || data;
|
|
1295
|
+
console.log(`\n${c.red}Job rejected.${c.reset}\n`);
|
|
1296
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
1297
|
+
console.log(` ${c.bold}Job:${c.reset} ${job.title || id}`);
|
|
1298
|
+
console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'rejected')}`);
|
|
1299
|
+
if (reason) {
|
|
1300
|
+
console.log(` ${c.bold}Reason:${c.reset} ${reason}`);
|
|
1301
|
+
}
|
|
1302
|
+
if (txHash) {
|
|
1303
|
+
console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${txHash}${c.reset}`);
|
|
1304
|
+
}
|
|
1305
|
+
console.log();
|
|
1306
|
+
}
|
|
1307
|
+
function showJobHelp() {
|
|
1308
|
+
console.log(`
|
|
1309
|
+
${c.bold}${c.cyan}obolos job${c.reset} — ERC-8183 Agentic Commerce Protocol (ACP) job management
|
|
1310
|
+
|
|
1311
|
+
${c.bold}Usage:${c.reset}
|
|
1312
|
+
obolos job list [options] List jobs with optional filters
|
|
1313
|
+
obolos job create [options] Create a new job
|
|
1314
|
+
obolos job info <id> Get full job details
|
|
1315
|
+
obolos job fund <id> Fund a job's escrow
|
|
1316
|
+
obolos job submit <id> [options] Submit work for a job
|
|
1317
|
+
obolos job complete <id> [options] Approve a job (evaluator)
|
|
1318
|
+
obolos job reject <id> [options] Reject a job submission
|
|
1319
|
+
|
|
1320
|
+
${c.bold}List Options:${c.reset}
|
|
1321
|
+
--status=open|funded|submitted|completed|rejected|expired
|
|
1322
|
+
--client=0x... Filter by client address
|
|
1323
|
+
--provider=0x... Filter by provider address
|
|
1324
|
+
--limit=20 Max results (default: 20)
|
|
1325
|
+
|
|
1326
|
+
${c.bold}Create Options:${c.reset}
|
|
1327
|
+
--title "..." Job title (required)
|
|
1328
|
+
--description "..." Job description
|
|
1329
|
+
--evaluator 0x... Evaluator address (required)
|
|
1330
|
+
--provider 0x... Specific provider (optional, open if omitted)
|
|
1331
|
+
--budget 1.00 Budget in USDC
|
|
1332
|
+
--expires 24h Expiry (e.g., "24h", "7d", "1h")
|
|
1333
|
+
|
|
1334
|
+
${c.bold}Submit Options:${c.reset}
|
|
1335
|
+
--deliverable <hash/CID/URL> Work product reference (required)
|
|
1336
|
+
|
|
1337
|
+
${c.bold}Complete/Reject Options:${c.reset}
|
|
1338
|
+
--reason "..." Optional reason text
|
|
1339
|
+
|
|
1340
|
+
${c.bold}Examples:${c.reset}
|
|
1341
|
+
obolos job list --status=open
|
|
1342
|
+
obolos job create --title "Analyze dataset" --evaluator 0xABC... --budget 5.00 --expires 7d
|
|
1343
|
+
obolos job info abc123
|
|
1344
|
+
obolos job fund abc123
|
|
1345
|
+
obolos job submit abc123 --deliverable ipfs://Qm...
|
|
1346
|
+
obolos job complete abc123 --reason "Looks great"
|
|
1347
|
+
obolos job reject abc123 --reason "Missing section 3"
|
|
1348
|
+
`);
|
|
1349
|
+
}
|
|
1350
|
+
async function cmdJob(args) {
|
|
1351
|
+
const subcommand = args[0];
|
|
1352
|
+
const subArgs = args.slice(1);
|
|
1353
|
+
switch (subcommand) {
|
|
1354
|
+
case 'list':
|
|
1355
|
+
case 'ls':
|
|
1356
|
+
await cmdJobList(subArgs);
|
|
1357
|
+
break;
|
|
1358
|
+
case 'create':
|
|
1359
|
+
case 'new':
|
|
1360
|
+
await cmdJobCreate(subArgs);
|
|
1361
|
+
break;
|
|
1362
|
+
case 'info':
|
|
1363
|
+
case 'show':
|
|
1364
|
+
await cmdJobInfo(subArgs);
|
|
1365
|
+
break;
|
|
1366
|
+
case 'fund':
|
|
1367
|
+
await cmdJobFund(subArgs);
|
|
1368
|
+
break;
|
|
1369
|
+
case 'submit':
|
|
1370
|
+
await cmdJobSubmit(subArgs);
|
|
1371
|
+
break;
|
|
1372
|
+
case 'complete':
|
|
1373
|
+
case 'approve':
|
|
1374
|
+
await cmdJobComplete(subArgs);
|
|
1375
|
+
break;
|
|
1376
|
+
case 'reject':
|
|
1377
|
+
await cmdJobReject(subArgs);
|
|
1378
|
+
break;
|
|
1379
|
+
case 'help':
|
|
1380
|
+
case '--help':
|
|
1381
|
+
case '-h':
|
|
1382
|
+
case undefined:
|
|
1383
|
+
showJobHelp();
|
|
1384
|
+
break;
|
|
1385
|
+
default:
|
|
1386
|
+
console.error(`${c.red}Unknown job subcommand: ${subcommand}${c.reset}`);
|
|
1387
|
+
showJobHelp();
|
|
1388
|
+
process.exit(1);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
// ─── Listing Commands (Negotiation Layer) ────────────────────────────────────
|
|
1392
|
+
async function cmdListingList(args) {
|
|
1393
|
+
const params = new URLSearchParams();
|
|
1394
|
+
const status = getFlag(args, 'status');
|
|
1395
|
+
const client = getFlag(args, 'client');
|
|
1396
|
+
const limit = getFlag(args, 'limit') || '20';
|
|
1397
|
+
if (status)
|
|
1398
|
+
params.set('status', status);
|
|
1399
|
+
if (client)
|
|
1400
|
+
params.set('client', client);
|
|
1401
|
+
params.set('limit', limit);
|
|
1402
|
+
const data = await apiGet(`/api/listings?${params}`);
|
|
1403
|
+
const listings = data.listings || data.data || [];
|
|
1404
|
+
if (listings.length === 0) {
|
|
1405
|
+
console.log(`${c.yellow}No listings found.${c.reset}`);
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
const total = data.pagination?.total || data.total || listings.length;
|
|
1409
|
+
console.log(`\n${c.bold}${c.cyan}Job Listings${c.reset} ${c.dim}— ${total} listings${c.reset}\n`);
|
|
1410
|
+
// Table header
|
|
1411
|
+
console.log(` ${c.bold}${'ID'.padEnd(12)} ${'Title'.padEnd(28)} ${'Status'.padEnd(14)} ${'Budget Range'.padEnd(20)} ${'Bids'.padEnd(6)} ${'Client'.padEnd(14)} Deadline${c.reset}`);
|
|
1412
|
+
console.log(` ${c.dim}${'─'.repeat(110)}${c.reset}`);
|
|
1413
|
+
for (const l of listings) {
|
|
1414
|
+
const id = shortenId(l.id || '');
|
|
1415
|
+
const title = (l.title || 'Untitled').slice(0, 26).padEnd(28);
|
|
1416
|
+
const st = statusColor((l.status || 'open').padEnd(12));
|
|
1417
|
+
const budgetMin = l.min_budget != null ? `$${Number(l.min_budget).toFixed(2)}` : '?';
|
|
1418
|
+
const budgetMax = l.max_budget != null ? `$${Number(l.max_budget).toFixed(2)}` : '?';
|
|
1419
|
+
const budget = `${budgetMin}-${budgetMax}`.padEnd(20);
|
|
1420
|
+
const bids = String(l.bid_count ?? l.bids?.length ?? 0).padEnd(6);
|
|
1421
|
+
const cl = shortenAddr(l.client_address || l.client).padEnd(14);
|
|
1422
|
+
const deadline = l.deadline ? formatDate(l.deadline) : `${c.dim}—${c.reset}`;
|
|
1423
|
+
console.log(` ${id.padEnd(12)} ${title} ${st} ${budget} ${bids} ${cl} ${deadline}`);
|
|
1424
|
+
}
|
|
1425
|
+
console.log(`\n${c.dim}Use: obolos listing info <id> for full details${c.reset}\n`);
|
|
1426
|
+
}
|
|
1427
|
+
async function cmdListingCreate(args) {
|
|
1428
|
+
const title = getFlag(args, 'title');
|
|
1429
|
+
const description = getFlag(args, 'description');
|
|
1430
|
+
const minBudget = getFlag(args, 'min-budget');
|
|
1431
|
+
const maxBudget = getFlag(args, 'max-budget');
|
|
1432
|
+
const deadline = getFlag(args, 'deadline');
|
|
1433
|
+
const duration = getFlag(args, 'duration');
|
|
1434
|
+
const evaluator = getFlag(args, 'evaluator');
|
|
1435
|
+
const hook = getFlag(args, 'hook');
|
|
1436
|
+
if (!title) {
|
|
1437
|
+
console.error(`${c.red}Usage: obolos listing create --title "..." --description "..." [--min-budget 1.00] [--max-budget 10.00] [--deadline 7d] [--duration 24]${c.reset}`);
|
|
1438
|
+
process.exit(1);
|
|
1439
|
+
}
|
|
1440
|
+
const walletAddress = await getWalletAddress();
|
|
1441
|
+
const payload = { title };
|
|
1442
|
+
if (description)
|
|
1443
|
+
payload.description = description;
|
|
1444
|
+
if (minBudget)
|
|
1445
|
+
payload.min_budget = minBudget;
|
|
1446
|
+
if (maxBudget)
|
|
1447
|
+
payload.max_budget = maxBudget;
|
|
1448
|
+
if (deadline)
|
|
1449
|
+
payload.deadline = deadline;
|
|
1450
|
+
if (duration)
|
|
1451
|
+
payload.job_duration = parseInt(duration, 10);
|
|
1452
|
+
if (evaluator)
|
|
1453
|
+
payload.preferred_evaluator = evaluator;
|
|
1454
|
+
if (hook)
|
|
1455
|
+
payload.hook_address = hook;
|
|
1456
|
+
const data = await apiPost('/api/listings', payload, {
|
|
1457
|
+
'x-wallet-address': walletAddress,
|
|
1458
|
+
});
|
|
1459
|
+
const listing = data.listing || data;
|
|
1460
|
+
console.log(`\n${c.green}Listing created successfully!${c.reset}\n`);
|
|
1461
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
1462
|
+
console.log(` ${c.bold}ID:${c.reset} ${listing.id}`);
|
|
1463
|
+
console.log(` ${c.bold}Title:${c.reset} ${listing.title}`);
|
|
1464
|
+
if (listing.description) {
|
|
1465
|
+
console.log(` ${c.bold}Description:${c.reset} ${listing.description}`);
|
|
1466
|
+
}
|
|
1467
|
+
console.log(` ${c.bold}Status:${c.reset} ${statusColor(listing.status || 'open')}`);
|
|
1468
|
+
console.log(` ${c.bold}Client:${c.reset} ${listing.client_address || walletAddress}`);
|
|
1469
|
+
if (listing.min_budget != null || listing.max_budget != null) {
|
|
1470
|
+
const min = listing.min_budget != null ? `$${Number(listing.min_budget).toFixed(2)}` : '?';
|
|
1471
|
+
const max = listing.max_budget != null ? `$${Number(listing.max_budget).toFixed(2)}` : '?';
|
|
1472
|
+
console.log(` ${c.bold}Budget:${c.reset} ${c.green}${min} – ${max} USDC${c.reset}`);
|
|
1473
|
+
}
|
|
1474
|
+
if (listing.deadline) {
|
|
1475
|
+
console.log(` ${c.bold}Deadline:${c.reset} ${formatDate(listing.deadline)}`);
|
|
1476
|
+
}
|
|
1477
|
+
if (listing.job_duration) {
|
|
1478
|
+
console.log(` ${c.bold}Duration:${c.reset} ${listing.job_duration}h`);
|
|
1479
|
+
}
|
|
1480
|
+
console.log(`\n${c.dim}Share this listing with providers. They can bid with: obolos listing bid ${listing.id} --price 5.00${c.reset}\n`);
|
|
1481
|
+
}
|
|
1482
|
+
async function cmdListingInfo(args) {
|
|
1483
|
+
const id = getPositional(args, 0);
|
|
1484
|
+
if (!id) {
|
|
1485
|
+
console.error(`${c.red}Usage: obolos listing info <id>${c.reset}`);
|
|
1486
|
+
process.exit(1);
|
|
1487
|
+
}
|
|
1488
|
+
const data = await apiGet(`/api/listings/${encodeURIComponent(id)}`);
|
|
1489
|
+
const listing = data.listing || data;
|
|
1490
|
+
console.log(`\n${c.bold}${c.cyan}${listing.title || 'Untitled Listing'}${c.reset}`);
|
|
1491
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
1492
|
+
console.log(` ${c.bold}ID:${c.reset} ${listing.id}`);
|
|
1493
|
+
console.log(` ${c.bold}Status:${c.reset} ${statusColor(listing.status || 'open')}`);
|
|
1494
|
+
console.log(` ${c.bold}Client:${c.reset} ${listing.client_address || `${c.dim}—${c.reset}`}`);
|
|
1495
|
+
if (listing.min_budget != null || listing.max_budget != null) {
|
|
1496
|
+
const min = listing.min_budget != null ? `$${Number(listing.min_budget).toFixed(2)}` : '?';
|
|
1497
|
+
const max = listing.max_budget != null ? `$${Number(listing.max_budget).toFixed(2)}` : '?';
|
|
1498
|
+
console.log(` ${c.bold}Budget:${c.reset} ${c.green}${min} – ${max} USDC${c.reset}`);
|
|
1499
|
+
}
|
|
1500
|
+
if (listing.deadline) {
|
|
1501
|
+
const deadlineDate = new Date(listing.deadline);
|
|
1502
|
+
const now = new Date();
|
|
1503
|
+
const expired = deadlineDate < now;
|
|
1504
|
+
console.log(` ${c.bold}Deadline:${c.reset} ${expired ? c.red : c.dim}${formatDate(listing.deadline)}${expired ? ' (passed)' : ''}${c.reset}`);
|
|
1505
|
+
}
|
|
1506
|
+
if (listing.job_duration) {
|
|
1507
|
+
console.log(` ${c.bold}Duration:${c.reset} ${listing.job_duration}h`);
|
|
1508
|
+
}
|
|
1509
|
+
if (listing.preferred_evaluator) {
|
|
1510
|
+
console.log(` ${c.bold}Evaluator:${c.reset} ${listing.preferred_evaluator}`);
|
|
1511
|
+
}
|
|
1512
|
+
if (listing.description) {
|
|
1513
|
+
console.log(`\n ${c.bold}Description:${c.reset}`);
|
|
1514
|
+
const descLines = listing.description.split('\n');
|
|
1515
|
+
for (const line of descLines) {
|
|
1516
|
+
console.log(` ${line}`);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
console.log(` ${c.bold}Created:${c.reset} ${formatDate(listing.created_at || listing.createdAt)}`);
|
|
1520
|
+
// Bids
|
|
1521
|
+
const bids = listing.bids || [];
|
|
1522
|
+
if (bids.length > 0) {
|
|
1523
|
+
console.log(`\n ${c.bold}${c.cyan}Bids (${bids.length})${c.reset}`);
|
|
1524
|
+
console.log(` ${c.dim}${'─'.repeat(56)}${c.reset}`);
|
|
1525
|
+
console.log(` ${c.bold}${'Bid ID'.padEnd(12)} ${'Provider'.padEnd(14)} ${'Price'.padEnd(12)} ${'Delivery'.padEnd(10)} Message${c.reset}`);
|
|
1526
|
+
console.log(` ${c.dim}${'─'.repeat(56)}${c.reset}`);
|
|
1527
|
+
for (const bid of bids) {
|
|
1528
|
+
const bidId = shortenId(bid.id || '');
|
|
1529
|
+
const provider = shortenAddr(bid.provider_address);
|
|
1530
|
+
const price = bid.price != null ? `${c.green}$${Number(bid.price).toFixed(2)}${c.reset}` : `${c.dim}—${c.reset}`;
|
|
1531
|
+
const delivery = bid.delivery_time ? `${bid.delivery_time}h` : `${c.dim}—${c.reset}`;
|
|
1532
|
+
const msg = (bid.message || '').slice(0, 40);
|
|
1533
|
+
console.log(` ${bidId.padEnd(12)} ${provider.padEnd(14)} ${price.padEnd(12)} ${delivery.padEnd(10)} ${c.dim}${msg}${c.reset}`);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
else {
|
|
1537
|
+
console.log(`\n ${c.dim}No bids yet.${c.reset}`);
|
|
1538
|
+
}
|
|
1539
|
+
// Actions
|
|
1540
|
+
console.log();
|
|
1541
|
+
const s = listing.status || 'open';
|
|
1542
|
+
if (s === 'open') {
|
|
1543
|
+
console.log(` ${c.bold}Actions:${c.reset}`);
|
|
1544
|
+
console.log(` obolos listing bid ${listing.id} --price 5.00 ${c.dim}Submit a bid${c.reset}`);
|
|
1545
|
+
if (bids.length > 0) {
|
|
1546
|
+
console.log(` obolos listing accept ${listing.id} --bid <bid_id> ${c.dim}Accept a bid${c.reset}`);
|
|
1547
|
+
}
|
|
1548
|
+
console.log(` obolos listing cancel ${listing.id} ${c.dim}Cancel the listing${c.reset}`);
|
|
1549
|
+
}
|
|
1550
|
+
console.log();
|
|
1551
|
+
}
|
|
1552
|
+
async function cmdListingBid(args) {
|
|
1553
|
+
const listingId = getPositional(args, 0);
|
|
1554
|
+
if (!listingId) {
|
|
1555
|
+
console.error(`${c.red}Usage: obolos listing bid <listing_id> --price 5.00 [--delivery 24] [--message "..."]${c.reset}`);
|
|
1556
|
+
process.exit(1);
|
|
1557
|
+
}
|
|
1558
|
+
const price = getFlag(args, 'price');
|
|
1559
|
+
if (!price) {
|
|
1560
|
+
console.error(`${c.red}--price is required. Provide your bid amount in USDC.${c.reset}`);
|
|
1561
|
+
process.exit(1);
|
|
1562
|
+
}
|
|
1563
|
+
const delivery = getFlag(args, 'delivery');
|
|
1564
|
+
const message = getFlag(args, 'message');
|
|
1565
|
+
const proposalHash = getFlag(args, 'proposal-hash');
|
|
1566
|
+
const walletAddress = await getWalletAddress();
|
|
1567
|
+
const payload = { price };
|
|
1568
|
+
if (delivery)
|
|
1569
|
+
payload.delivery_time = parseInt(delivery, 10);
|
|
1570
|
+
if (message)
|
|
1571
|
+
payload.message = message;
|
|
1572
|
+
if (proposalHash)
|
|
1573
|
+
payload.proposal_hash = proposalHash;
|
|
1574
|
+
const data = await apiPost(`/api/listings/${encodeURIComponent(listingId)}/bid`, payload, {
|
|
1575
|
+
'x-wallet-address': walletAddress,
|
|
1576
|
+
});
|
|
1577
|
+
const bid = data.bid || data;
|
|
1578
|
+
console.log(`\n${c.green}Bid submitted successfully!${c.reset}\n`);
|
|
1579
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
1580
|
+
console.log(` ${c.bold}Bid ID:${c.reset} ${bid.id}`);
|
|
1581
|
+
console.log(` ${c.bold}Listing:${c.reset} ${listingId}`);
|
|
1582
|
+
console.log(` ${c.bold}Price:${c.reset} ${c.green}$${Number(price).toFixed(2)} USDC${c.reset}`);
|
|
1583
|
+
if (delivery) {
|
|
1584
|
+
console.log(` ${c.bold}Delivery:${c.reset} ${delivery}h`);
|
|
1585
|
+
}
|
|
1586
|
+
if (message) {
|
|
1587
|
+
console.log(` ${c.bold}Message:${c.reset} ${message}`);
|
|
1588
|
+
}
|
|
1589
|
+
console.log(`\n${c.dim}The client will review your bid. You'll be notified if accepted.${c.reset}\n`);
|
|
1590
|
+
}
|
|
1591
|
+
async function cmdListingAccept(args) {
|
|
1592
|
+
const listingId = getPositional(args, 0);
|
|
1593
|
+
if (!listingId) {
|
|
1594
|
+
console.error(`${c.red}Usage: obolos listing accept <listing_id> --bid <bid_id>${c.reset}`);
|
|
1595
|
+
process.exit(1);
|
|
1596
|
+
}
|
|
1597
|
+
const bidId = getFlag(args, 'bid');
|
|
1598
|
+
if (!bidId) {
|
|
1599
|
+
console.error(`${c.red}--bid is required. Specify the bid ID to accept.${c.reset}`);
|
|
1600
|
+
process.exit(1);
|
|
1601
|
+
}
|
|
1602
|
+
const walletAddress = await getWalletAddress();
|
|
1603
|
+
// Create on-chain ACP job if wallet is available
|
|
1604
|
+
let chainJobId = null;
|
|
1605
|
+
let chainTxHash = null;
|
|
1606
|
+
try {
|
|
1607
|
+
// Fetch listing details to get terms
|
|
1608
|
+
const listingData = await apiGet(`/api/listings/${encodeURIComponent(listingId)}`);
|
|
1609
|
+
const listing = listingData.listing || listingData;
|
|
1610
|
+
const bids = listing.bids || [];
|
|
1611
|
+
const acceptedBid = bids.find((b) => b.id === bidId);
|
|
1612
|
+
if (acceptedBid && OBOLOS_PRIVATE_KEY) {
|
|
1613
|
+
const acp = await getACPClient();
|
|
1614
|
+
const providerAddress = acceptedBid.provider_address || ZERO_ADDRESS;
|
|
1615
|
+
const evaluatorAddress = listing.preferred_evaluator || walletAddress;
|
|
1616
|
+
// Default expiry: delivery_time hours or job_duration or 7 days
|
|
1617
|
+
const durationHours = acceptedBid.delivery_time || listing.job_duration || 168;
|
|
1618
|
+
const expiredAt = Math.floor((Date.now() + durationHours * 3600000) / 1000);
|
|
1619
|
+
const description = `${listing.title}: ${listing.description || ''}`.slice(0, 500);
|
|
1620
|
+
console.log(`\n ${c.dim}Creating ACP job on-chain...${c.reset}`);
|
|
1621
|
+
const txHash = await acp.walletClient.writeContract({
|
|
1622
|
+
address: ACP_ADDRESS,
|
|
1623
|
+
abi: ACP_ABI,
|
|
1624
|
+
functionName: 'createJob',
|
|
1625
|
+
args: [
|
|
1626
|
+
providerAddress,
|
|
1627
|
+
evaluatorAddress,
|
|
1628
|
+
BigInt(expiredAt),
|
|
1629
|
+
description,
|
|
1630
|
+
(listing.hook_address || ZERO_ADDRESS),
|
|
1631
|
+
],
|
|
1632
|
+
account: acp.account,
|
|
1633
|
+
chain: (await import('viem/chains')).base,
|
|
1634
|
+
});
|
|
1635
|
+
console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
|
|
1636
|
+
const receipt = await acp.publicClient.waitForTransactionReceipt({ hash: txHash });
|
|
1637
|
+
// Extract jobId from JobCreated event
|
|
1638
|
+
for (const log of receipt.logs) {
|
|
1639
|
+
try {
|
|
1640
|
+
const decoded = acp.decodeEventLog({
|
|
1641
|
+
abi: ACP_ABI,
|
|
1642
|
+
data: log.data,
|
|
1643
|
+
topics: log.topics,
|
|
1644
|
+
});
|
|
1645
|
+
if (decoded.eventName === 'JobCreated') {
|
|
1646
|
+
chainJobId = (decoded.args.jobId).toString();
|
|
1647
|
+
break;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
catch { }
|
|
1651
|
+
}
|
|
1652
|
+
chainTxHash = txHash;
|
|
1653
|
+
console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}`);
|
|
1654
|
+
if (chainJobId) {
|
|
1655
|
+
console.log(` ${c.green}Chain job ID: ${chainJobId}${c.reset}`);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
catch (err) {
|
|
1660
|
+
console.error(` ${c.yellow}On-chain job creation failed: ${err.message}${c.reset}`);
|
|
1661
|
+
console.error(` ${c.dim}Proceeding with backend-only acceptance...${c.reset}`);
|
|
1662
|
+
}
|
|
1663
|
+
const payload = { bid_id: bidId };
|
|
1664
|
+
if (chainJobId)
|
|
1665
|
+
payload.acp_job_id = chainJobId;
|
|
1666
|
+
if (chainTxHash)
|
|
1667
|
+
payload.chain_tx_hash = chainTxHash;
|
|
1668
|
+
const data = await apiPost(`/api/listings/${encodeURIComponent(listingId)}/accept`, payload, {
|
|
1669
|
+
'x-wallet-address': walletAddress,
|
|
1670
|
+
});
|
|
1671
|
+
const listing = data.listing || data;
|
|
1672
|
+
console.log(`\n${c.green}Bid accepted! ACP job created.${c.reset}\n`);
|
|
1673
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
1674
|
+
console.log(` ${c.bold}Listing:${c.reset} ${listing.title || listingId}`);
|
|
1675
|
+
console.log(` ${c.bold}Status:${c.reset} ${statusColor(listing.status || 'accepted')}`);
|
|
1676
|
+
console.log(` ${c.bold}Bid:${c.reset} ${bidId}`);
|
|
1677
|
+
if (chainJobId) {
|
|
1678
|
+
console.log(` ${c.bold}Chain ID:${c.reset} ${chainJobId}`);
|
|
1679
|
+
}
|
|
1680
|
+
if (chainTxHash) {
|
|
1681
|
+
console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${chainTxHash}${c.reset}`);
|
|
1682
|
+
}
|
|
1683
|
+
if (listing.job_id || data.job_id) {
|
|
1684
|
+
console.log(` ${c.bold}Job ID:${c.reset} ${listing.job_id || data.job_id}`);
|
|
1685
|
+
}
|
|
1686
|
+
console.log(`\n${c.dim}Next: Fund the escrow with: obolos job fund <job-id>${c.reset}\n`);
|
|
1687
|
+
}
|
|
1688
|
+
async function cmdListingCancel(args) {
|
|
1689
|
+
const listingId = getPositional(args, 0);
|
|
1690
|
+
if (!listingId) {
|
|
1691
|
+
console.error(`${c.red}Usage: obolos listing cancel <listing_id>${c.reset}`);
|
|
1692
|
+
process.exit(1);
|
|
1693
|
+
}
|
|
1694
|
+
const walletAddress = await getWalletAddress();
|
|
1695
|
+
const data = await apiPost(`/api/listings/${encodeURIComponent(listingId)}/cancel`, {}, {
|
|
1696
|
+
'x-wallet-address': walletAddress,
|
|
1697
|
+
});
|
|
1698
|
+
const listing = data.listing || data;
|
|
1699
|
+
console.log(`\n${c.yellow}Listing cancelled.${c.reset}\n`);
|
|
1700
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
1701
|
+
console.log(` ${c.bold}Listing:${c.reset} ${listing.title || listingId}`);
|
|
1702
|
+
console.log(` ${c.bold}Status:${c.reset} ${statusColor(listing.status || 'cancelled')}`);
|
|
1703
|
+
console.log();
|
|
1704
|
+
}
|
|
1705
|
+
function showListingHelp() {
|
|
1706
|
+
console.log(`
|
|
1707
|
+
${c.bold}${c.cyan}obolos listing${c.reset} — Agent-to-agent negotiation layer
|
|
1708
|
+
|
|
1709
|
+
${c.bold}Usage:${c.reset}
|
|
1710
|
+
obolos listing list [options] Browse open job listings
|
|
1711
|
+
obolos listing create [options] Create a new listing for agents to bid on
|
|
1712
|
+
obolos listing info <id> Get listing details with all bids
|
|
1713
|
+
obolos listing bid <id> [options] Submit a bid on a listing
|
|
1714
|
+
obolos listing accept <id> [options] Accept a bid (auto-creates ACP job)
|
|
1715
|
+
obolos listing cancel <id> Cancel a listing
|
|
1716
|
+
|
|
1717
|
+
${c.bold}List Options:${c.reset}
|
|
1718
|
+
--status=open|negotiating|accepted|cancelled
|
|
1719
|
+
--client=0x... Filter by client address
|
|
1720
|
+
--limit=20 Max results (default: 20)
|
|
1721
|
+
|
|
1722
|
+
${c.bold}Create Options:${c.reset}
|
|
1723
|
+
--title "..." Listing title (required)
|
|
1724
|
+
--description "..." Detailed description
|
|
1725
|
+
--min-budget 1.00 Minimum budget in USDC
|
|
1726
|
+
--max-budget 10.00 Maximum budget in USDC
|
|
1727
|
+
--deadline 7d Bidding deadline (e.g., "24h", "7d")
|
|
1728
|
+
--duration 24 Expected job duration in hours
|
|
1729
|
+
--evaluator 0x... Preferred evaluator address
|
|
1730
|
+
--hook 0x... Hook contract address
|
|
1731
|
+
|
|
1732
|
+
${c.bold}Bid Options:${c.reset}
|
|
1733
|
+
--price 5.00 Your proposed price in USDC (required)
|
|
1734
|
+
--delivery 24 Estimated delivery time in hours
|
|
1735
|
+
--message "I can do this" Pitch to the client
|
|
1736
|
+
--proposal-hash <hash> Hash of detailed proposal
|
|
1737
|
+
|
|
1738
|
+
${c.bold}Accept Options:${c.reset}
|
|
1739
|
+
--bid <bid_id> Bid ID to accept (required)
|
|
1740
|
+
|
|
1741
|
+
${c.bold}Examples:${c.reset}
|
|
1742
|
+
obolos listing list --status=open
|
|
1743
|
+
obolos listing create --title "Analyze dataset" --description "Parse and summarize CSV" --max-budget 10.00 --deadline 7d
|
|
1744
|
+
obolos listing info abc123
|
|
1745
|
+
obolos listing bid abc123 --price 5.00 --delivery 24 --message "I can do this in 12h"
|
|
1746
|
+
obolos listing accept abc123 --bid bid456
|
|
1747
|
+
obolos listing cancel abc123
|
|
1748
|
+
`);
|
|
1749
|
+
}
|
|
1750
|
+
async function cmdListing(args) {
|
|
1751
|
+
const sub = args[0];
|
|
1752
|
+
const subArgs = args.slice(1);
|
|
1753
|
+
switch (sub) {
|
|
1754
|
+
case 'list':
|
|
1755
|
+
case 'ls':
|
|
1756
|
+
await cmdListingList(subArgs);
|
|
1757
|
+
break;
|
|
1758
|
+
case 'create':
|
|
1759
|
+
case 'new':
|
|
1760
|
+
await cmdListingCreate(subArgs);
|
|
1761
|
+
break;
|
|
1762
|
+
case 'info':
|
|
1763
|
+
case 'show':
|
|
1764
|
+
await cmdListingInfo(subArgs);
|
|
1765
|
+
break;
|
|
1766
|
+
case 'bid':
|
|
1767
|
+
await cmdListingBid(subArgs);
|
|
1768
|
+
break;
|
|
1769
|
+
case 'accept':
|
|
1770
|
+
await cmdListingAccept(subArgs);
|
|
1771
|
+
break;
|
|
1772
|
+
case 'cancel':
|
|
1773
|
+
await cmdListingCancel(subArgs);
|
|
1774
|
+
break;
|
|
1775
|
+
case 'help':
|
|
1776
|
+
case '--help':
|
|
1777
|
+
case '-h':
|
|
1778
|
+
case undefined:
|
|
1779
|
+
showListingHelp();
|
|
1780
|
+
break;
|
|
1781
|
+
default:
|
|
1782
|
+
console.error(`${c.red}Unknown listing subcommand: ${sub}${c.reset}`);
|
|
1783
|
+
showListingHelp();
|
|
1784
|
+
process.exit(1);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
// ─── ANP Commands (Agent Negotiation Protocol) ──────────────────────────────
|
|
1788
|
+
async function cmdAnpList(args) {
|
|
1789
|
+
const params = new URLSearchParams();
|
|
1790
|
+
const status = getFlag(args, 'status');
|
|
1791
|
+
const limit = getFlag(args, 'limit') || '20';
|
|
1792
|
+
if (status)
|
|
1793
|
+
params.set('status', status);
|
|
1794
|
+
params.set('limit', limit);
|
|
1795
|
+
const data = await apiGet(`/api/anp/listings?${params}`);
|
|
1796
|
+
const listings = data.listings || data.data || [];
|
|
1797
|
+
if (listings.length === 0) {
|
|
1798
|
+
console.log(`${c.yellow}No ANP listings found.${c.reset}`);
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
const total = data.pagination?.total || data.total || listings.length;
|
|
1802
|
+
console.log(`\n${c.bold}${c.cyan}ANP Listings${c.reset} ${c.dim}— ${total} listings${c.reset}\n`);
|
|
1803
|
+
// Table header
|
|
1804
|
+
console.log(` ${c.bold}${'CID'.padEnd(18)} ${'Title'.padEnd(28)} ${'Budget Range'.padEnd(20)} ${'Status'.padEnd(14)} ${'Bids'.padEnd(6)} Client${c.reset}`);
|
|
1805
|
+
console.log(` ${c.dim}${'─'.repeat(100)}${c.reset}`);
|
|
1806
|
+
for (const l of listings) {
|
|
1807
|
+
const cid = (l.cid || l.id || '').slice(0, 16).padEnd(18);
|
|
1808
|
+
const title = (l.title || 'Untitled').slice(0, 26).padEnd(28);
|
|
1809
|
+
const budgetMin = l.min_budget != null ? `$${Number(l.min_budget).toFixed(2)}` : '?';
|
|
1810
|
+
const budgetMax = l.max_budget != null ? `$${Number(l.max_budget).toFixed(2)}` : '?';
|
|
1811
|
+
const budget = `${budgetMin}-${budgetMax}`.padEnd(20);
|
|
1812
|
+
const st = statusColor((l.status || 'open').padEnd(12));
|
|
1813
|
+
const bids = String(l.bid_count ?? l.bids?.length ?? 0).padEnd(6);
|
|
1814
|
+
const cl = shortenAddr(l.client_address || l.client || l.signer);
|
|
1815
|
+
console.log(` ${cid} ${title} ${budget} ${st} ${bids} ${cl}`);
|
|
1816
|
+
}
|
|
1817
|
+
console.log(`\n${c.dim}Use: obolos anp info <cid> for full details${c.reset}\n`);
|
|
1818
|
+
}
|
|
1819
|
+
async function cmdAnpInfo(args) {
|
|
1820
|
+
const cid = getPositional(args, 0);
|
|
1821
|
+
if (!cid) {
|
|
1822
|
+
console.error(`${c.red}Usage: obolos anp info <cid>${c.reset}`);
|
|
1823
|
+
process.exit(1);
|
|
1824
|
+
}
|
|
1825
|
+
const data = await apiGet(`/api/anp/listings/${encodeURIComponent(cid)}`);
|
|
1826
|
+
const listing = data.listing || data;
|
|
1827
|
+
console.log(`\n${c.bold}${c.cyan}${listing.title || 'Untitled ANP Listing'}${c.reset}`);
|
|
1828
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
1829
|
+
console.log(` ${c.bold}CID:${c.reset} ${listing.cid || cid}`);
|
|
1830
|
+
console.log(` ${c.bold}Status:${c.reset} ${statusColor(listing.status || 'open')}`);
|
|
1831
|
+
console.log(` ${c.bold}Signer:${c.reset} ${listing.signer || listing.client_address || `${c.dim}—${c.reset}`}`);
|
|
1832
|
+
if (listing.min_budget != null || listing.max_budget != null) {
|
|
1833
|
+
const min = listing.min_budget != null ? `$${Number(listing.min_budget).toFixed(2)}` : '?';
|
|
1834
|
+
const max = listing.max_budget != null ? `$${Number(listing.max_budget).toFixed(2)}` : '?';
|
|
1835
|
+
console.log(` ${c.bold}Budget:${c.reset} ${c.green}${min} – ${max} USDC${c.reset}`);
|
|
1836
|
+
}
|
|
1837
|
+
if (listing.deadline) {
|
|
1838
|
+
const deadlineDate = new Date(typeof listing.deadline === 'number' ? listing.deadline * 1000 : listing.deadline);
|
|
1839
|
+
const now = new Date();
|
|
1840
|
+
const expired = deadlineDate < now;
|
|
1841
|
+
console.log(` ${c.bold}Deadline:${c.reset} ${expired ? c.red : c.dim}${formatDate(deadlineDate.toISOString())}${expired ? ' (passed)' : ''}${c.reset}`);
|
|
1842
|
+
}
|
|
1843
|
+
if (listing.job_duration || listing.jobDuration) {
|
|
1844
|
+
const dur = listing.job_duration || listing.jobDuration;
|
|
1845
|
+
console.log(` ${c.bold}Duration:${c.reset} ${dur >= 86400 ? `${Math.floor(dur / 86400)}d` : dur >= 3600 ? `${Math.floor(dur / 3600)}h` : `${dur}s`}`);
|
|
1846
|
+
}
|
|
1847
|
+
if (listing.preferred_evaluator || listing.preferredEvaluator) {
|
|
1848
|
+
const ev = listing.preferred_evaluator || listing.preferredEvaluator;
|
|
1849
|
+
if (ev !== ZERO_ADDRESS) {
|
|
1850
|
+
console.log(` ${c.bold}Evaluator:${c.reset} ${ev}`);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
if (listing.description) {
|
|
1854
|
+
console.log(`\n ${c.bold}Description:${c.reset}`);
|
|
1855
|
+
const descLines = listing.description.split('\n');
|
|
1856
|
+
for (const line of descLines) {
|
|
1857
|
+
console.log(` ${line}`);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
if (listing.content_hash || listing.contentHash) {
|
|
1861
|
+
console.log(` ${c.bold}Content Hash:${c.reset} ${c.dim}${listing.content_hash || listing.contentHash}${c.reset}`);
|
|
1862
|
+
}
|
|
1863
|
+
if (listing.nonce != null) {
|
|
1864
|
+
console.log(` ${c.bold}Nonce:${c.reset} ${c.dim}${listing.nonce}${c.reset}`);
|
|
1865
|
+
}
|
|
1866
|
+
if (listing.signature) {
|
|
1867
|
+
console.log(` ${c.bold}Signature:${c.reset} ${c.dim}${listing.signature.slice(0, 20)}...${c.reset}`);
|
|
1868
|
+
}
|
|
1869
|
+
// Bids
|
|
1870
|
+
const bids = listing.bids || [];
|
|
1871
|
+
if (bids.length > 0) {
|
|
1872
|
+
console.log(`\n ${c.bold}${c.cyan}Bids (${bids.length})${c.reset}`);
|
|
1873
|
+
console.log(` ${c.dim}${'─'.repeat(56)}${c.reset}`);
|
|
1874
|
+
console.log(` ${c.bold}${'CID'.padEnd(18)} ${'Bidder'.padEnd(14)} ${'Price'.padEnd(12)} ${'Delivery'.padEnd(10)} Message${c.reset}`);
|
|
1875
|
+
console.log(` ${c.dim}${'─'.repeat(56)}${c.reset}`);
|
|
1876
|
+
for (const bid of bids) {
|
|
1877
|
+
const bidCid = (bid.cid || bid.id || '').slice(0, 16).padEnd(18);
|
|
1878
|
+
const bidder = shortenAddr(bid.signer || bid.provider_address);
|
|
1879
|
+
const price = bid.price != null ? `${c.green}$${Number(bid.price).toFixed(2)}${c.reset}` : `${c.dim}—${c.reset}`;
|
|
1880
|
+
const delivery = bid.delivery_time || bid.deliveryTime;
|
|
1881
|
+
const deliveryStr = delivery ? (delivery >= 86400 ? `${Math.floor(delivery / 86400)}d` : delivery >= 3600 ? `${Math.floor(delivery / 3600)}h` : `${delivery}s`) : `${c.dim}—${c.reset}`;
|
|
1882
|
+
const msg = (bid.message || '').slice(0, 40);
|
|
1883
|
+
console.log(` ${bidCid} ${bidder.padEnd(14)} ${price.padEnd(12)} ${deliveryStr.padEnd(10)} ${c.dim}${msg}${c.reset}`);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
else {
|
|
1887
|
+
console.log(`\n ${c.dim}No bids yet.${c.reset}`);
|
|
1888
|
+
}
|
|
1889
|
+
// Actions
|
|
1890
|
+
console.log();
|
|
1891
|
+
const s = listing.status || 'open';
|
|
1892
|
+
if (s === 'open' || s === 'negotiating') {
|
|
1893
|
+
console.log(` ${c.bold}Actions:${c.reset}`);
|
|
1894
|
+
console.log(` obolos anp bid ${cid} --price 5.00 ${c.dim}Submit a bid${c.reset}`);
|
|
1895
|
+
if (bids.length > 0) {
|
|
1896
|
+
console.log(` obolos anp accept ${cid} --bid <bid_cid> ${c.dim}Accept a bid${c.reset}`);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
console.log(` obolos anp verify ${cid} ${c.dim}Verify document${c.reset}`);
|
|
1900
|
+
console.log();
|
|
1901
|
+
}
|
|
1902
|
+
async function cmdAnpCreate(args) {
|
|
1903
|
+
const title = getFlag(args, 'title');
|
|
1904
|
+
const description = getFlag(args, 'description');
|
|
1905
|
+
const minBudget = getFlag(args, 'min-budget');
|
|
1906
|
+
const maxBudget = getFlag(args, 'max-budget');
|
|
1907
|
+
const deadline = getFlag(args, 'deadline');
|
|
1908
|
+
const duration = getFlag(args, 'duration');
|
|
1909
|
+
const evaluator = getFlag(args, 'evaluator');
|
|
1910
|
+
if (!title) {
|
|
1911
|
+
console.error(`${c.red}Usage: obolos anp create --title "..." --description "..." --min-budget 5 --max-budget 50 --deadline 7d --duration 3d [--evaluator 0x...]${c.reset}`);
|
|
1912
|
+
process.exit(1);
|
|
1913
|
+
}
|
|
1914
|
+
const anp = await getANPSigningClient();
|
|
1915
|
+
// Compute content hash
|
|
1916
|
+
const contentHash = await computeContentHash({ title, description: description || '' });
|
|
1917
|
+
// Generate nonce
|
|
1918
|
+
const nonce = generateNonce();
|
|
1919
|
+
// Parse deadline to unix timestamp (seconds from now)
|
|
1920
|
+
let deadlineTs;
|
|
1921
|
+
if (deadline) {
|
|
1922
|
+
const secs = parseTimeToSeconds(deadline);
|
|
1923
|
+
deadlineTs = BigInt(Math.floor(Date.now() / 1000) + secs);
|
|
1924
|
+
}
|
|
1925
|
+
else {
|
|
1926
|
+
deadlineTs = BigInt(Math.floor(Date.now() / 1000) + 7 * 86400); // default 7d
|
|
1927
|
+
}
|
|
1928
|
+
// Parse duration to seconds
|
|
1929
|
+
let jobDuration;
|
|
1930
|
+
if (duration) {
|
|
1931
|
+
jobDuration = BigInt(parseTimeToSeconds(duration));
|
|
1932
|
+
}
|
|
1933
|
+
else {
|
|
1934
|
+
jobDuration = BigInt(3 * 86400); // default 3d
|
|
1935
|
+
}
|
|
1936
|
+
const minBudgetWei = BigInt(Math.floor((minBudget ? parseFloat(minBudget) : 0) * 1e6));
|
|
1937
|
+
const maxBudgetWei = BigInt(Math.floor((maxBudget ? parseFloat(maxBudget) : 0) * 1e6));
|
|
1938
|
+
const preferredEvaluator = (evaluator || ZERO_ADDRESS);
|
|
1939
|
+
const message = {
|
|
1940
|
+
contentHash,
|
|
1941
|
+
minBudget: minBudgetWei,
|
|
1942
|
+
maxBudget: maxBudgetWei,
|
|
1943
|
+
deadline: deadlineTs,
|
|
1944
|
+
jobDuration: jobDuration,
|
|
1945
|
+
preferredEvaluator,
|
|
1946
|
+
nonce,
|
|
1947
|
+
};
|
|
1948
|
+
console.log(`\n ${c.dim}Signing ListingIntent...${c.reset}`);
|
|
1949
|
+
const signature = await anp.walletClient.signTypedData({
|
|
1950
|
+
account: anp.account,
|
|
1951
|
+
domain: ANP_DOMAIN,
|
|
1952
|
+
types: ANP_LISTING_TYPES,
|
|
1953
|
+
primaryType: 'ListingIntent',
|
|
1954
|
+
message,
|
|
1955
|
+
});
|
|
1956
|
+
console.log(` ${c.green}Signed.${c.reset} Publishing...`);
|
|
1957
|
+
const document = {
|
|
1958
|
+
type: 'ListingIntent',
|
|
1959
|
+
title,
|
|
1960
|
+
description: description || '',
|
|
1961
|
+
contentHash,
|
|
1962
|
+
minBudget: minBudgetWei.toString(),
|
|
1963
|
+
maxBudget: maxBudgetWei.toString(),
|
|
1964
|
+
deadline: deadlineTs.toString(),
|
|
1965
|
+
jobDuration: jobDuration.toString(),
|
|
1966
|
+
preferredEvaluator,
|
|
1967
|
+
nonce: nonce.toString(),
|
|
1968
|
+
signer: anp.account.address,
|
|
1969
|
+
signature,
|
|
1970
|
+
};
|
|
1971
|
+
const data = await apiPost('/api/anp/publish', document);
|
|
1972
|
+
const result = data.listing || data;
|
|
1973
|
+
console.log(`\n${c.green}ANP listing published!${c.reset}\n`);
|
|
1974
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
1975
|
+
console.log(` ${c.bold}CID:${c.reset} ${result.cid || result.id}`);
|
|
1976
|
+
console.log(` ${c.bold}Title:${c.reset} ${title}`);
|
|
1977
|
+
console.log(` ${c.bold}Budget:${c.reset} ${c.green}$${(minBudget || '0')} – $${(maxBudget || '0')} USDC${c.reset}`);
|
|
1978
|
+
console.log(` ${c.bold}Deadline:${c.reset} ${formatDate(new Date(Number(deadlineTs) * 1000).toISOString())}`);
|
|
1979
|
+
console.log(` ${c.bold}Duration:${c.reset} ${duration || '3d'}`);
|
|
1980
|
+
console.log(` ${c.bold}Signer:${c.reset} ${anp.account.address}`);
|
|
1981
|
+
console.log(` ${c.bold}Signature:${c.reset} ${c.dim}${signature.slice(0, 20)}...${c.reset}`);
|
|
1982
|
+
console.log(`\n${c.dim}Agents can bid with: obolos anp bid ${result.cid || result.id} --price 25 --delivery 48h${c.reset}\n`);
|
|
1983
|
+
}
|
|
1984
|
+
async function cmdAnpBid(args) {
|
|
1985
|
+
const listingCid = getPositional(args, 0);
|
|
1986
|
+
if (!listingCid) {
|
|
1987
|
+
console.error(`${c.red}Usage: obolos anp bid <listing_cid> --price 25 --delivery 48h [--message "..."]${c.reset}`);
|
|
1988
|
+
process.exit(1);
|
|
1989
|
+
}
|
|
1990
|
+
const price = getFlag(args, 'price');
|
|
1991
|
+
if (!price) {
|
|
1992
|
+
console.error(`${c.red}--price is required. Provide your bid amount in USDC.${c.reset}`);
|
|
1993
|
+
process.exit(1);
|
|
1994
|
+
}
|
|
1995
|
+
const delivery = getFlag(args, 'delivery');
|
|
1996
|
+
const message = getFlag(args, 'message');
|
|
1997
|
+
const anp = await getANPSigningClient();
|
|
1998
|
+
// Fetch listing document to compute listingHash
|
|
1999
|
+
console.log(`\n ${c.dim}Fetching listing document...${c.reset}`);
|
|
2000
|
+
const listingData = await apiGet(`/api/anp/objects/${encodeURIComponent(listingCid)}`);
|
|
2001
|
+
const listing = listingData.document || listingData.listing || listingData;
|
|
2002
|
+
// Reconstruct listing struct and compute hash
|
|
2003
|
+
const listingHash = anp.hashListingStruct({
|
|
2004
|
+
contentHash: listing.contentHash,
|
|
2005
|
+
minBudget: BigInt(listing.minBudget || listing.min_budget || '0'),
|
|
2006
|
+
maxBudget: BigInt(listing.maxBudget || listing.max_budget || '0'),
|
|
2007
|
+
deadline: BigInt(listing.deadline || '0'),
|
|
2008
|
+
jobDuration: BigInt(listing.jobDuration || listing.job_duration || '0'),
|
|
2009
|
+
preferredEvaluator: (listing.preferredEvaluator || listing.preferred_evaluator || ZERO_ADDRESS),
|
|
2010
|
+
nonce: BigInt(listing.nonce || '0'),
|
|
2011
|
+
});
|
|
2012
|
+
// Compute content hash for bid
|
|
2013
|
+
const contentHash = await computeContentHash({ message: message || '', proposalCid: '' });
|
|
2014
|
+
const nonce = generateNonce();
|
|
2015
|
+
const priceWei = BigInt(Math.floor(parseFloat(price) * 1e6));
|
|
2016
|
+
let deliveryTime;
|
|
2017
|
+
if (delivery) {
|
|
2018
|
+
deliveryTime = BigInt(parseTimeToSeconds(delivery));
|
|
2019
|
+
}
|
|
2020
|
+
else {
|
|
2021
|
+
deliveryTime = BigInt(86400); // default 24h
|
|
2022
|
+
}
|
|
2023
|
+
const bidMessage = {
|
|
2024
|
+
listingHash,
|
|
2025
|
+
contentHash,
|
|
2026
|
+
price: priceWei,
|
|
2027
|
+
deliveryTime,
|
|
2028
|
+
nonce,
|
|
2029
|
+
};
|
|
2030
|
+
console.log(` ${c.dim}Signing BidIntent...${c.reset}`);
|
|
2031
|
+
const signature = await anp.walletClient.signTypedData({
|
|
2032
|
+
account: anp.account,
|
|
2033
|
+
domain: ANP_DOMAIN,
|
|
2034
|
+
types: ANP_BID_TYPES,
|
|
2035
|
+
primaryType: 'BidIntent',
|
|
2036
|
+
message: bidMessage,
|
|
2037
|
+
});
|
|
2038
|
+
console.log(` ${c.green}Signed.${c.reset} Publishing...`);
|
|
2039
|
+
const document = {
|
|
2040
|
+
type: 'BidIntent',
|
|
2041
|
+
listingCid,
|
|
2042
|
+
listingHash,
|
|
2043
|
+
contentHash,
|
|
2044
|
+
message: message || '',
|
|
2045
|
+
price: priceWei.toString(),
|
|
2046
|
+
deliveryTime: deliveryTime.toString(),
|
|
2047
|
+
nonce: nonce.toString(),
|
|
2048
|
+
signer: anp.account.address,
|
|
2049
|
+
signature,
|
|
2050
|
+
};
|
|
2051
|
+
const data = await apiPost('/api/anp/publish', document);
|
|
2052
|
+
const result = data.bid || data;
|
|
2053
|
+
console.log(`\n${c.green}ANP bid published!${c.reset}\n`);
|
|
2054
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
2055
|
+
console.log(` ${c.bold}CID:${c.reset} ${result.cid || result.id}`);
|
|
2056
|
+
console.log(` ${c.bold}Listing:${c.reset} ${listingCid}`);
|
|
2057
|
+
console.log(` ${c.bold}Price:${c.reset} ${c.green}$${parseFloat(price).toFixed(2)} USDC${c.reset}`);
|
|
2058
|
+
console.log(` ${c.bold}Delivery:${c.reset} ${delivery || '24h'}`);
|
|
2059
|
+
if (message) {
|
|
2060
|
+
console.log(` ${c.bold}Message:${c.reset} ${message}`);
|
|
2061
|
+
}
|
|
2062
|
+
console.log(` ${c.bold}Signer:${c.reset} ${anp.account.address}`);
|
|
2063
|
+
console.log(` ${c.bold}Signature:${c.reset} ${c.dim}${signature.slice(0, 20)}...${c.reset}`);
|
|
2064
|
+
console.log(`\n${c.dim}The listing owner can accept with: obolos anp accept ${listingCid} --bid ${result.cid || result.id}${c.reset}\n`);
|
|
2065
|
+
}
|
|
2066
|
+
async function cmdAnpAccept(args) {
|
|
2067
|
+
const listingCid = getPositional(args, 0);
|
|
2068
|
+
if (!listingCid) {
|
|
2069
|
+
console.error(`${c.red}Usage: obolos anp accept <listing_cid> --bid <bid_cid>${c.reset}`);
|
|
2070
|
+
process.exit(1);
|
|
2071
|
+
}
|
|
2072
|
+
const bidCid = getFlag(args, 'bid');
|
|
2073
|
+
if (!bidCid) {
|
|
2074
|
+
console.error(`${c.red}--bid is required. Specify the bid CID to accept.${c.reset}`);
|
|
2075
|
+
process.exit(1);
|
|
2076
|
+
}
|
|
2077
|
+
const anp = await getANPSigningClient();
|
|
2078
|
+
// Fetch listing and bid documents
|
|
2079
|
+
console.log(`\n ${c.dim}Fetching listing and bid documents...${c.reset}`);
|
|
2080
|
+
const [listingData, bidData] = await Promise.all([
|
|
2081
|
+
apiGet(`/api/anp/objects/${encodeURIComponent(listingCid)}`),
|
|
2082
|
+
apiGet(`/api/anp/objects/${encodeURIComponent(bidCid)}`),
|
|
2083
|
+
]);
|
|
2084
|
+
const listing = listingData.document || listingData.listing || listingData;
|
|
2085
|
+
const bid = bidData.document || bidData.bid || bidData;
|
|
2086
|
+
// Compute listing hash
|
|
2087
|
+
const listingHash = anp.hashListingStruct({
|
|
2088
|
+
contentHash: listing.contentHash,
|
|
2089
|
+
minBudget: BigInt(listing.minBudget || listing.min_budget || '0'),
|
|
2090
|
+
maxBudget: BigInt(listing.maxBudget || listing.max_budget || '0'),
|
|
2091
|
+
deadline: BigInt(listing.deadline || '0'),
|
|
2092
|
+
jobDuration: BigInt(listing.jobDuration || listing.job_duration || '0'),
|
|
2093
|
+
preferredEvaluator: (listing.preferredEvaluator || listing.preferred_evaluator || ZERO_ADDRESS),
|
|
2094
|
+
nonce: BigInt(listing.nonce || '0'),
|
|
2095
|
+
});
|
|
2096
|
+
// Compute bid hash
|
|
2097
|
+
const bidHash = anp.hashBidStruct({
|
|
2098
|
+
listingHash: (bid.listingHash || listingHash),
|
|
2099
|
+
contentHash: bid.contentHash,
|
|
2100
|
+
price: BigInt(bid.price || '0'),
|
|
2101
|
+
deliveryTime: BigInt(bid.deliveryTime || bid.delivery_time || '0'),
|
|
2102
|
+
nonce: BigInt(bid.nonce || '0'),
|
|
2103
|
+
});
|
|
2104
|
+
const nonce = generateNonce();
|
|
2105
|
+
const acceptMessage = {
|
|
2106
|
+
listingHash,
|
|
2107
|
+
bidHash,
|
|
2108
|
+
nonce,
|
|
2109
|
+
};
|
|
2110
|
+
console.log(` ${c.dim}Signing AcceptIntent...${c.reset}`);
|
|
2111
|
+
const signature = await anp.walletClient.signTypedData({
|
|
2112
|
+
account: anp.account,
|
|
2113
|
+
domain: ANP_DOMAIN,
|
|
2114
|
+
types: ANP_ACCEPT_TYPES,
|
|
2115
|
+
primaryType: 'AcceptIntent',
|
|
2116
|
+
message: acceptMessage,
|
|
2117
|
+
});
|
|
2118
|
+
console.log(` ${c.green}Signed.${c.reset} Publishing...`);
|
|
2119
|
+
const document = {
|
|
2120
|
+
type: 'AcceptIntent',
|
|
2121
|
+
listingCid,
|
|
2122
|
+
bidCid,
|
|
2123
|
+
listingHash,
|
|
2124
|
+
bidHash,
|
|
2125
|
+
nonce: nonce.toString(),
|
|
2126
|
+
signer: anp.account.address,
|
|
2127
|
+
signature,
|
|
2128
|
+
};
|
|
2129
|
+
const data = await apiPost('/api/anp/publish', document);
|
|
2130
|
+
const result = data.accept || data;
|
|
2131
|
+
console.log(`\n${c.green}Bid accepted! ANP agreement published.${c.reset}\n`);
|
|
2132
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
2133
|
+
console.log(` ${c.bold}CID:${c.reset} ${result.cid || result.id}`);
|
|
2134
|
+
console.log(` ${c.bold}Listing:${c.reset} ${listingCid}`);
|
|
2135
|
+
console.log(` ${c.bold}Bid:${c.reset} ${bidCid}`);
|
|
2136
|
+
console.log(` ${c.bold}Signer:${c.reset} ${anp.account.address}`);
|
|
2137
|
+
console.log(` ${c.bold}Signature:${c.reset} ${c.dim}${signature.slice(0, 20)}...${c.reset}`);
|
|
2138
|
+
console.log(`\n${c.dim}The agreement is now verifiable on-chain.${c.reset}\n`);
|
|
2139
|
+
}
|
|
2140
|
+
async function cmdAnpVerify(args) {
|
|
2141
|
+
const cid = getPositional(args, 0);
|
|
2142
|
+
if (!cid) {
|
|
2143
|
+
console.error(`${c.red}Usage: obolos anp verify <cid>${c.reset}`);
|
|
2144
|
+
process.exit(1);
|
|
2145
|
+
}
|
|
2146
|
+
console.log(`\n ${c.dim}Verifying document...${c.reset}`);
|
|
2147
|
+
const data = await apiGet(`/api/anp/verify/${encodeURIComponent(cid)}`);
|
|
2148
|
+
console.log(`\n${c.bold}${c.cyan}ANP Document Verification${c.reset}`);
|
|
2149
|
+
console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
|
|
2150
|
+
console.log(` ${c.bold}CID:${c.reset} ${cid}`);
|
|
2151
|
+
console.log(` ${c.bold}Type:${c.reset} ${data.type || `${c.dim}—${c.reset}`}`);
|
|
2152
|
+
console.log(` ${c.bold}Signer:${c.reset} ${data.signer || `${c.dim}—${c.reset}`}`);
|
|
2153
|
+
if (data.valid || data.verified) {
|
|
2154
|
+
console.log(` ${c.bold}Signature:${c.reset} ${c.green}Valid${c.reset}`);
|
|
2155
|
+
}
|
|
2156
|
+
else {
|
|
2157
|
+
console.log(` ${c.bold}Signature:${c.reset} ${c.red}Invalid${c.reset}`);
|
|
2158
|
+
}
|
|
2159
|
+
if (data.content_valid != null) {
|
|
2160
|
+
console.log(` ${c.bold}Content Hash:${c.reset} ${data.content_valid ? `${c.green}Matches${c.reset}` : `${c.red}Mismatch${c.reset}`}`);
|
|
2161
|
+
}
|
|
2162
|
+
if (data.chain_refs != null) {
|
|
2163
|
+
console.log(` ${c.bold}Chain Refs:${c.reset} ${data.chain_refs ? `${c.green}Valid${c.reset}` : `${c.red}Invalid${c.reset}`}`);
|
|
2164
|
+
}
|
|
2165
|
+
if (data.details) {
|
|
2166
|
+
console.log(`\n ${c.bold}Details:${c.reset}`);
|
|
2167
|
+
const details = typeof data.details === 'string' ? data.details : JSON.stringify(data.details, null, 2);
|
|
2168
|
+
for (const line of details.split('\n')) {
|
|
2169
|
+
console.log(` ${c.dim}${line}${c.reset}`);
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
console.log();
|
|
2173
|
+
}
|
|
2174
|
+
function showAnpHelp() {
|
|
2175
|
+
console.log(`
|
|
2176
|
+
${c.bold}${c.cyan}obolos anp${c.reset} — Agent Negotiation Protocol (EIP-712 signed documents)
|
|
2177
|
+
|
|
2178
|
+
${c.bold}Usage:${c.reset}
|
|
2179
|
+
obolos anp list [options] Browse ANP listings
|
|
2180
|
+
obolos anp info <cid> Get listing details with bids
|
|
2181
|
+
obolos anp create [options] Sign and publish a listing
|
|
2182
|
+
obolos anp bid <cid> [options] Sign and publish a bid
|
|
2183
|
+
obolos anp accept <cid> [options] Accept a bid (sign AcceptIntent)
|
|
2184
|
+
obolos anp verify <cid> Verify document integrity
|
|
2185
|
+
|
|
2186
|
+
${c.bold}List Options:${c.reset}
|
|
2187
|
+
--status=open|negotiating|accepted Filter by status
|
|
2188
|
+
--limit=20 Max results (default: 20)
|
|
2189
|
+
|
|
2190
|
+
${c.bold}Create Options:${c.reset}
|
|
2191
|
+
--title "..." Listing title (required)
|
|
2192
|
+
--description "..." Detailed description
|
|
2193
|
+
--min-budget 5 Minimum budget in USDC
|
|
2194
|
+
--max-budget 50 Maximum budget in USDC
|
|
2195
|
+
--deadline 7d Bidding deadline (e.g., "24h", "7d")
|
|
2196
|
+
--duration 3d Expected job duration (e.g., "48h", "3d")
|
|
2197
|
+
--evaluator 0x... Preferred evaluator address
|
|
2198
|
+
|
|
2199
|
+
${c.bold}Bid Options:${c.reset}
|
|
2200
|
+
--price 25 Your proposed price in USDC (required)
|
|
2201
|
+
--delivery 48h Estimated delivery time (e.g., "24h", "3d")
|
|
2202
|
+
--message "I can do this" Message to the client
|
|
2203
|
+
|
|
2204
|
+
${c.bold}Accept Options:${c.reset}
|
|
2205
|
+
--bid <bid_cid> Bid CID to accept (required)
|
|
2206
|
+
|
|
2207
|
+
${c.bold}Examples:${c.reset}
|
|
2208
|
+
obolos anp list --status=open
|
|
2209
|
+
obolos anp create --title "Analyze dataset" --description "Parse CSV" --min-budget 5 --max-budget 50 --deadline 7d --duration 3d
|
|
2210
|
+
obolos anp info sha256-abc123...
|
|
2211
|
+
obolos anp bid sha256-abc123... --price 25 --delivery 48h --message "I can do this"
|
|
2212
|
+
obolos anp accept sha256-listing... --bid sha256-bid...
|
|
2213
|
+
obolos anp verify sha256-abc123...
|
|
2214
|
+
`);
|
|
2215
|
+
}
|
|
2216
|
+
async function cmdAnp(args) {
|
|
2217
|
+
const sub = args[0];
|
|
2218
|
+
const subArgs = args.slice(1);
|
|
2219
|
+
switch (sub) {
|
|
2220
|
+
case 'list':
|
|
2221
|
+
case 'ls':
|
|
2222
|
+
await cmdAnpList(subArgs);
|
|
2223
|
+
break;
|
|
2224
|
+
case 'info':
|
|
2225
|
+
case 'show':
|
|
2226
|
+
await cmdAnpInfo(subArgs);
|
|
2227
|
+
break;
|
|
2228
|
+
case 'create':
|
|
2229
|
+
case 'new':
|
|
2230
|
+
await cmdAnpCreate(subArgs);
|
|
2231
|
+
break;
|
|
2232
|
+
case 'bid':
|
|
2233
|
+
await cmdAnpBid(subArgs);
|
|
2234
|
+
break;
|
|
2235
|
+
case 'accept':
|
|
2236
|
+
await cmdAnpAccept(subArgs);
|
|
2237
|
+
break;
|
|
2238
|
+
case 'verify':
|
|
2239
|
+
await cmdAnpVerify(subArgs);
|
|
2240
|
+
break;
|
|
2241
|
+
case 'help':
|
|
2242
|
+
case '--help':
|
|
2243
|
+
case '-h':
|
|
2244
|
+
case undefined:
|
|
2245
|
+
showAnpHelp();
|
|
2246
|
+
break;
|
|
2247
|
+
default:
|
|
2248
|
+
console.error(`${c.red}Unknown anp subcommand: ${sub}${c.reset}`);
|
|
2249
|
+
showAnpHelp();
|
|
2250
|
+
process.exit(1);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
// ─── Help ───────────────────────────────────────────────────────────────────
|
|
472
2254
|
function showHelp() {
|
|
473
2255
|
console.log(`
|
|
474
2256
|
${c.bold}${c.cyan}obolos${c.reset} — CLI for the Obolos x402 API Marketplace
|
|
@@ -484,6 +2266,34 @@ ${c.bold}Usage:${c.reset}
|
|
|
484
2266
|
obolos setup --show Show current wallet config
|
|
485
2267
|
obolos setup-mcp Show MCP server setup instructions
|
|
486
2268
|
|
|
2269
|
+
${c.bold}Job Commands (ERC-8183 ACP):${c.reset}
|
|
2270
|
+
obolos job list [options] List jobs with filters
|
|
2271
|
+
obolos job create [options] Create a new job
|
|
2272
|
+
obolos job info <id> Get full job details
|
|
2273
|
+
obolos job fund <id> Fund a job's escrow
|
|
2274
|
+
obolos job submit <id> [opts] Submit work for a job
|
|
2275
|
+
obolos job complete <id> Approve a job (evaluator)
|
|
2276
|
+
obolos job reject <id> Reject a job submission
|
|
2277
|
+
obolos job help Show job command help
|
|
2278
|
+
|
|
2279
|
+
${c.bold}Listing Commands (Negotiation):${c.reset}
|
|
2280
|
+
obolos listing list [options] Browse open job listings
|
|
2281
|
+
obolos listing create [opts] Create a listing for bids
|
|
2282
|
+
obolos listing info <id> Get listing details + bids
|
|
2283
|
+
obolos listing bid <id> [opts] Submit a bid on a listing
|
|
2284
|
+
obolos listing accept <id> Accept a bid (creates job)
|
|
2285
|
+
obolos listing cancel <id> Cancel a listing
|
|
2286
|
+
obolos listing help Show listing command help
|
|
2287
|
+
|
|
2288
|
+
${c.bold}ANP Commands (Agent Negotiation Protocol):${c.reset}
|
|
2289
|
+
obolos anp list [options] Browse ANP listings
|
|
2290
|
+
obolos anp info <cid> Get listing details + bids
|
|
2291
|
+
obolos anp create [options] Sign and publish a listing
|
|
2292
|
+
obolos anp bid <cid> [opts] Sign and publish a bid
|
|
2293
|
+
obolos anp accept <cid> [opts] Accept a bid (sign AcceptIntent)
|
|
2294
|
+
obolos anp verify <cid> Verify document integrity
|
|
2295
|
+
obolos anp help Show ANP command help
|
|
2296
|
+
|
|
487
2297
|
${c.bold}Call Options:${c.reset}
|
|
488
2298
|
--method POST|GET|PUT HTTP method (default: GET)
|
|
489
2299
|
--body '{"key":"value"}' Request body (JSON)
|
|
@@ -497,6 +2307,16 @@ ${c.bold}Examples:${c.reset}
|
|
|
497
2307
|
obolos search "token price"
|
|
498
2308
|
obolos info a59a0377-d77b-4fee-...
|
|
499
2309
|
obolos call a59a0377-... --body '{"prompt":"a cat in space"}'
|
|
2310
|
+
obolos job list --status=open
|
|
2311
|
+
obolos job create --title "Analyze data" --evaluator 0xABC... --budget 5.00
|
|
2312
|
+
obolos listing list --status=open
|
|
2313
|
+
obolos listing create --title "Parse CSV data" --max-budget 10.00 --deadline 7d
|
|
2314
|
+
obolos listing bid abc123 --price 5.00 --message "I can do this"
|
|
2315
|
+
obolos listing accept abc123 --bid bid456
|
|
2316
|
+
obolos anp list --status=open
|
|
2317
|
+
obolos anp create --title "Analyze data" --min-budget 5 --max-budget 50 --deadline 7d
|
|
2318
|
+
obolos anp bid sha256-abc... --price 25 --delivery 48h --message "I can do this"
|
|
2319
|
+
obolos anp accept sha256-listing... --bid sha256-bid...
|
|
500
2320
|
`);
|
|
501
2321
|
}
|
|
502
2322
|
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
@@ -532,6 +2352,17 @@ async function main() {
|
|
|
532
2352
|
case 'mcp':
|
|
533
2353
|
await cmdSetupMcp();
|
|
534
2354
|
break;
|
|
2355
|
+
case 'job':
|
|
2356
|
+
case 'j':
|
|
2357
|
+
await cmdJob(commandArgs);
|
|
2358
|
+
break;
|
|
2359
|
+
case 'listing':
|
|
2360
|
+
case 'l':
|
|
2361
|
+
await cmdListing(commandArgs);
|
|
2362
|
+
break;
|
|
2363
|
+
case 'anp':
|
|
2364
|
+
await cmdAnp(commandArgs);
|
|
2365
|
+
break;
|
|
535
2366
|
case 'help':
|
|
536
2367
|
case '--help':
|
|
537
2368
|
case '-h':
|