@sage-protocol/cli 0.8.2 → 0.8.4

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.
@@ -9,6 +9,45 @@ function register(program) {
9
9
  .description('USDC governance boosts (direct or policy-based)')
10
10
  .addHelpText('after', `\nAgent/Automation Support:\n Use -y/--yes flag or SAGE_YES=1 environment variable to skip all interactive prompts.\n\nExamples:\n $ sage boost create --proposal-id 1 --governor 0x... --per-voter 1000000 --max-voters 100 --yes\n $ sage boost fund --proposal-id 1 --amount 10000000 -y\n`);
11
11
 
12
+ const resolveSubgraphUrl = () => (cliConfig.resolveSubgraphUrl ? cliConfig.resolveSubgraphUrl() : (
13
+ process.env.SUBGRAPH_URL || process.env.SAGE_SUBGRAPH_URL || process.env.NEXT_PUBLIC_GRAPH_ENDPOINT || process.env.NEXT_PUBLIC_SUBGRAPH_URL || null
14
+ ));
15
+
16
+ const parseProofInput = (value) => {
17
+ const fs = require('fs');
18
+ if (!value) return null;
19
+ let raw = String(value).trim();
20
+ if (!raw.startsWith('[')) {
21
+ raw = fs.readFileSync(raw, 'utf8');
22
+ }
23
+ return JSON.parse(raw);
24
+ };
25
+
26
+ const detectManagerType = async (provider, address) => {
27
+ const directIface = new ethers.Interface([
28
+ 'function getBoost(uint256) view returns (tuple(address creator, address governor, uint256 proposalId, uint256 snapshot, uint256 perVoter, uint256 maxVoters, uint256 votersPaid, uint96 minVotes, uint8 payoutMode, uint8 support, uint256 startAt, uint256 expiresAt, uint256 totalPool, uint256 totalPaid, uint8 kind, address policy, bool active, bool paused) boost)'
29
+ ]);
30
+ const merkleIface = new ethers.Interface([
31
+ 'function getBoost(uint256) view returns (uint256,uint256,bytes32,bool,bool,address)'
32
+ ]);
33
+ try {
34
+ const callData = directIface.encodeFunctionData('getBoost', [0n]);
35
+ const ret = await provider.call({ to: address, data: callData });
36
+ directIface.decodeFunctionResult('getBoost', ret);
37
+ return 'direct';
38
+ } catch (_) {
39
+ try {
40
+ const callDataM = merkleIface.encodeFunctionData('getBoost', [0n]);
41
+ const retM = await provider.call({ to: address, data: callDataM });
42
+ merkleIface.decodeFunctionResult('getBoost', retM);
43
+ return 'merkle';
44
+ } catch (_) {
45
+ return 'unknown';
46
+ }
47
+ }
48
+ };
49
+
50
+
12
51
  boost
13
52
  .command('create')
14
53
  .description('Create a boost (direct/Merkle/custom) with payout mode and time window')
@@ -49,31 +88,7 @@ function register(program) {
49
88
  if (!usdcAddr) throw new Error('Missing USDC token (env USDC_ADDRESS/MOCK_USDC_ADDRESS) and manager.usdc() not readable');
50
89
 
51
90
  // ===== Manager-type preflight (DIRECT vs MERKLE) =====
52
- const detectManagerType = async (address) => {
53
- const directIface = new ethers.Interface([
54
- 'function getBoost(uint256) view returns (tuple(address creator, address governor, uint256 proposalId, uint256 snapshot, uint256 perVoter, uint256 maxVoters, uint256 votersPaid, uint96 minVotes, uint8 payoutMode, uint8 support, uint256 startAt, uint256 expiresAt, uint256 totalPool, uint256 totalPaid, uint8 kind, address policy, bool active, bool paused) boost)'
55
- ]);
56
- const merkleIface = new ethers.Interface([
57
- 'function getBoost(uint256) view returns (uint256,uint256,bytes32,bool,bool,address)'
58
- ]);
59
- try {
60
- const callData = directIface.encodeFunctionData('getBoost', [0n]);
61
- const ret = await provider.call({ to: address, data: callData });
62
- directIface.decodeFunctionResult('getBoost', ret); // will throw if not direct
63
- return 'direct';
64
- } catch (_) {
65
- try {
66
- const callDataM = merkleIface.encodeFunctionData('getBoost', [0n]);
67
- const retM = await provider.call({ to: address, data: callDataM });
68
- merkleIface.decodeFunctionResult('getBoost', retM);
69
- return 'merkle';
70
- } catch (_) {
71
- return 'unknown';
72
- }
73
- }
74
- };
75
-
76
- const mgrType = await detectManagerType(mgrAddr);
91
+ const mgrType = await detectManagerType(provider, mgrAddr);
77
92
  if (mgrType === 'merkle') {
78
93
  ui.error(`Detected Merkle boost manager at ${mgrAddr}`);
79
94
  ui.output(' This command uses the DIRECT manager ABI (createBoost(address,uint256,...)).');
@@ -288,18 +303,266 @@ function register(program) {
288
303
  } catch (e) { handleCLIError(e, 'set-root'); }
289
304
  });
290
305
 
306
+ boost
307
+ .command('list')
308
+ .description('List boosts (direct/merkle) using subgraph or on-chain logs')
309
+ .option('--type <type>', 'direct|merkle|auto (default: auto)', 'auto')
310
+ .option('--manager <address>', 'Boost manager address (overrides default for the selected type)')
311
+ .option('--governor <address>', 'Filter by governor (direct only)')
312
+ .option('--proposal-id <id>', 'Filter by proposal ID')
313
+ .option('--active', 'Only show active boosts', false)
314
+ .option('--from-block <n>', 'Start block for on-chain log scan (fallback)')
315
+ .option('--limit <n>', 'Maximum number of results (default: 25)', '25')
316
+ .option('--json', 'Output JSON', false)
317
+ .action(async (opts) => {
318
+ try {
319
+ const { enterJsonMode, withJsonVersion } = require('../utils/json-output');
320
+ enterJsonMode(opts);
321
+ ui.configure({ json: opts.json });
322
+ const WM = require('../wallet-manager');
323
+ const wm = new WM(); await wm.connect();
324
+ const provider = wm.getProvider();
325
+
326
+ const limit = Math.max(1, Number(opts.limit || 25));
327
+ let type = String(opts.type || 'auto').toLowerCase();
328
+ const proposalId = opts.proposalId ? BigInt(opts.proposalId) : null;
329
+ const governor = opts.governor ? String(opts.governor).toLowerCase() : null;
330
+
331
+ let directManager = cliConfig.resolveAddress('BOOST_MANAGER_DIRECT_ADDRESS', process.env.BOOST_MANAGER_DIRECT_ADDRESS) || cliConfig.resolveAddress('BOOST_MANAGER_ADDRESS');
332
+ let merkleManager = cliConfig.resolveAddress('BOOST_MANAGER_ADDRESS');
333
+ const loggerGlue = cliConfig.resolveAddress('BOOST_LOGGER_GLUE_ADDRESS', process.env.BOOST_LOGGER_GLUE_ADDRESS);
334
+
335
+ const results = [];
336
+ const sources = [];
337
+
338
+ if (opts.manager) {
339
+ if (type === 'direct') {
340
+ directManager = opts.manager;
341
+ } else if (type === 'merkle') {
342
+ merkleManager = opts.manager;
343
+ } else {
344
+ const detected = await detectManagerType(provider, opts.manager);
345
+ if (detected === 'direct') {
346
+ directManager = opts.manager;
347
+ type = 'direct';
348
+ } else if (detected === 'merkle') {
349
+ merkleManager = opts.manager;
350
+ type = 'merkle';
351
+ } else {
352
+ throw new Error(`Unknown boost manager type at ${opts.manager}. Pass --type direct|merkle.`);
353
+ }
354
+ }
355
+ }
356
+
357
+ const fetchDirectFromSubgraph = async () => {
358
+ const subgraphUrl = resolveSubgraphUrl();
359
+ if (!subgraphUrl) return null;
360
+ const { querySubgraph } = require('../utils/subgraph-client');
361
+ const where = {};
362
+ if (proposalId != null) where.proposalId = proposalId.toString();
363
+ if (governor) where.governor = governor;
364
+ const query = `
365
+ query BoostEvents($first: Int!, $skip: Int!, $orderBy: String!, $orderDirection: String!, $where: BoostEvent_filter) {
366
+ boostEvents(first: $first, skip: $skip, orderBy: $orderBy, orderDirection: $orderDirection, where: $where) {
367
+ id
368
+ proposalId
369
+ governor
370
+ perVoter
371
+ maxVoters
372
+ snapshot
373
+ kind
374
+ policy
375
+ minVotes
376
+ blockTimestamp
377
+ transactionHash
378
+ }
379
+ }
380
+ `;
381
+ const data = await querySubgraph(query, {
382
+ first: limit,
383
+ skip: 0,
384
+ orderBy: 'blockTimestamp',
385
+ orderDirection: 'desc',
386
+ where
387
+ }, { subgraphUrl });
388
+ return data?.boostEvents || [];
389
+ };
390
+
391
+ const fetchDirectFromLogs = async () => {
392
+ if (!loggerGlue) throw new Error('Missing BOOST_LOGGER_GLUE_ADDRESS for log scan fallback');
393
+ const iface = new ethers.Interface([
394
+ 'event BoostCreated(uint256 indexed proposalId, address indexed governor, uint256 perVoter, uint256 maxVoters, uint256 snapshot, uint8 kind, address policy, uint96 minVotes)'
395
+ ]);
396
+ const latest = await provider.getBlockNumber();
397
+ const fromBlock = opts.fromBlock ? Number(opts.fromBlock) : Math.max(0, latest - 200000);
398
+ const topics = [iface.getEvent('BoostCreated').topicHash, null, null];
399
+ if (proposalId != null) topics[1] = ethers.zeroPadValue(ethers.toBeHex(proposalId), 32);
400
+ if (governor) topics[2] = ethers.zeroPadValue(governor, 32);
401
+ const logs = await provider.getLogs({ address: loggerGlue, fromBlock, toBlock: latest, topics });
402
+ return logs.map((log) => {
403
+ const parsed = iface.parseLog(log);
404
+ return {
405
+ id: `${log.transactionHash}-${log.logIndex}`,
406
+ proposalId: parsed.args.proposalId.toString(),
407
+ governor: parsed.args.governor,
408
+ perVoter: parsed.args.perVoter.toString(),
409
+ maxVoters: parsed.args.maxVoters.toString(),
410
+ snapshot: parsed.args.snapshot.toString(),
411
+ kind: Number(parsed.args.kind),
412
+ policy: parsed.args.policy,
413
+ minVotes: parsed.args.minVotes.toString(),
414
+ blockTimestamp: String(log.blockNumber || ''),
415
+ transactionHash: log.transactionHash
416
+ };
417
+ }).slice(0, limit);
418
+ };
419
+
420
+ const fetchMerkleFromLogs = async () => {
421
+ if (!merkleManager) throw new Error('Missing BOOST_MANAGER_ADDRESS for merkle listing');
422
+ const iface = new ethers.Interface([
423
+ 'event BoostCreated(uint256 indexed proposalId, address indexed creator, uint256 totalPool)'
424
+ ]);
425
+ const latest = await provider.getBlockNumber();
426
+ const fromBlock = opts.fromBlock ? Number(opts.fromBlock) : Math.max(0, latest - 200000);
427
+ const topics = [iface.getEvent('BoostCreated').topicHash];
428
+ if (proposalId != null) topics.push(ethers.zeroPadValue(ethers.toBeHex(proposalId), 32));
429
+ const logs = await provider.getLogs({ address: merkleManager, fromBlock, toBlock: latest, topics });
430
+ return logs.map((log) => {
431
+ const parsed = iface.parseLog(log);
432
+ return {
433
+ id: `${log.transactionHash}-${log.logIndex}`,
434
+ proposalId: parsed.args.proposalId.toString(),
435
+ creator: parsed.args.creator,
436
+ totalPool: parsed.args.totalPool.toString(),
437
+ transactionHash: log.transactionHash
438
+ };
439
+ }).slice(0, limit);
440
+ };
441
+
442
+ const enrichDirect = async (items) => {
443
+ if (!directManager) return items;
444
+ const ifDirect = new ethers.Interface([
445
+ 'function getBoost(uint256) view returns (tuple(address creator, address governor, uint256 proposalId, uint256 snapshot, uint256 perVoter, uint256 maxVoters, uint256 votersPaid, uint96 minVotes, uint8 payoutMode, uint8 support, uint256 startAt, uint256 expiresAt, uint256 totalPool, uint256 totalPaid, uint8 kind, address policy, bool active, bool paused) boost)'
446
+ ]);
447
+ const enriched = await Promise.all(items.map(async (row) => {
448
+ try {
449
+ const ret = await provider.call({ to: directManager, data: ifDirect.encodeFunctionData('getBoost', [BigInt(row.proposalId)]) });
450
+ const [b] = ifDirect.decodeFunctionResult('getBoost', ret);
451
+ return {
452
+ ...row,
453
+ managerType: 'direct',
454
+ manager: directManager,
455
+ active: b.active,
456
+ paused: b.paused,
457
+ totalPool: b.totalPool.toString(),
458
+ totalPaid: b.totalPaid.toString(),
459
+ votersPaid: b.votersPaid.toString()
460
+ };
461
+ } catch (_) {
462
+ return { ...row, managerType: 'direct', manager: directManager };
463
+ }
464
+ }));
465
+ return enriched;
466
+ };
467
+
468
+ const enrichMerkle = async (items) => {
469
+ if (!merkleManager) return items;
470
+ const ifMerkle = new ethers.Interface([
471
+ 'function getBoost(uint256) view returns (uint256,uint256,bytes32,bool,bool,address)'
472
+ ]);
473
+ const enriched = await Promise.all(items.map(async (row) => {
474
+ try {
475
+ const ret = await provider.call({ to: merkleManager, data: ifMerkle.encodeFunctionData('getBoost', [BigInt(row.proposalId)]) });
476
+ const [totalPool, totalClaimed, merkleRoot, active, finalized, creator] = ifMerkle.decodeFunctionResult('getBoost', ret);
477
+ return {
478
+ ...row,
479
+ managerType: 'merkle',
480
+ manager: merkleManager,
481
+ totalPool: totalPool.toString(),
482
+ totalClaimed: totalClaimed.toString(),
483
+ merkleRoot,
484
+ active,
485
+ finalized,
486
+ creator
487
+ };
488
+ } catch (_) {
489
+ return { ...row, managerType: 'merkle', manager: merkleManager };
490
+ }
491
+ }));
492
+ return enriched;
493
+ };
494
+
495
+ if (type === 'direct' || type === 'auto') {
496
+ let directItems = null;
497
+ try {
498
+ directItems = await fetchDirectFromSubgraph();
499
+ if (directItems?.length) sources.push('subgraph');
500
+ } catch (e) {
501
+ ui.warn(`Subgraph query failed: ${e.message}. Falling back to logs.`);
502
+ }
503
+ if (!directItems || !directItems.length) {
504
+ directItems = await fetchDirectFromLogs();
505
+ sources.push('logs');
506
+ }
507
+ const enriched = await enrichDirect(directItems || []);
508
+ results.push(...enriched);
509
+ }
510
+
511
+ if (type === 'merkle' || type === 'auto') {
512
+ if (governor && type !== 'direct') {
513
+ ui.warn('Governor filter is only supported for direct boosts; ignoring for merkle listing.');
514
+ }
515
+ const merkleItems = await fetchMerkleFromLogs();
516
+ sources.push('logs');
517
+ const enriched = await enrichMerkle(merkleItems || []);
518
+ results.push(...enriched);
519
+ }
520
+
521
+ const filtered = opts.active ? results.filter(r => r.active) : results;
522
+ if (opts.json) {
523
+ ui.json(withJsonVersion({ count: filtered.length, source: sources, boosts: filtered }));
524
+ return;
525
+ }
526
+
527
+ if (!filtered.length) {
528
+ ui.info('No boosts found matching the criteria.');
529
+ return;
530
+ }
531
+ ui.output(`Found ${filtered.length} boosts (${sources.join(', ')}):\n`);
532
+ for (const b of filtered) {
533
+ if (b.managerType === 'merkle') {
534
+ ui.output(`[${b.proposalId}] merkle boost`);
535
+ ui.output(` Manager: ${b.manager || 'unknown'}`);
536
+ if (b.totalPool) ui.output(` Pool: ${b.totalPool} | Claimed: ${b.totalClaimed ?? '0'}`);
537
+ if (b.active != null) ui.output(` Active: ${b.active} | Finalized: ${b.finalized ?? false}`);
538
+ } else {
539
+ ui.output(`[${b.proposalId}] direct boost`);
540
+ ui.output(` Governor: ${b.governor || 'n/a'} | Manager: ${b.manager || 'unknown'}`);
541
+ ui.output(` Per Voter: ${b.perVoter} | Max Voters: ${b.maxVoters}`);
542
+ if (b.totalPool) ui.output(` Pool: ${b.totalPool} | Paid: ${b.totalPaid ?? '0'} | Voters Paid: ${b.votersPaid ?? '0'}`);
543
+ if (b.active != null) ui.output(` Active: ${b.active} | Paused: ${b.paused ?? false}`);
544
+ }
545
+ ui.output('');
546
+ }
547
+ } catch (e) { handleCLIError(e, 'list'); }
548
+ });
549
+
291
550
  boost
292
551
  .command('status')
293
552
  .description('Show boost status for a proposal')
294
- .requiredOption('--proposal-id <id>', 'Proposal ID')
553
+ .option('--proposal-id <id>', 'Proposal ID')
554
+ .option('--boost-id <id>', 'Alias for proposal ID')
295
555
  .option('--manager <address>', 'Boost manager')
296
556
  .action(async (opts) => {
297
557
  try {
558
+ if (!opts.proposalId && !opts.boostId) {
559
+ throw new Error('Missing --proposal-id (or --boost-id)');
560
+ }
298
561
  const WM = require('../wallet-manager');
299
562
  const wm = new WM(); await wm.connect();
300
563
  const provider = wm.getProvider();
301
564
  const mgrAddr = opts.manager || cliConfig.resolveAddress('BOOST_MANAGER_ADDRESS');
302
- const id = BigInt(opts.proposalId);
565
+ const id = BigInt(opts.proposalId || opts.boostId);
303
566
  const ifMerkle = new ethers.Interface(['function getBoost(uint256) view returns (uint256,uint256,bytes32,bool,bool,address)']);
304
567
  const ifDirect = new ethers.Interface(['function getBoost(uint256) view returns (tuple(address creator, address governor, uint256 proposalId, uint256 snapshot, uint256 perVoter, uint256 maxVoters, uint256 votersPaid, uint96 minVotes, uint8 payoutMode, uint8 support, uint256 startAt, uint256 expiresAt, uint256 totalPool, uint256 totalPaid, uint8 kind, address policy, bool active, bool paused) boost)']);
305
568
  // Try DIRECT first
@@ -307,7 +570,7 @@ function register(program) {
307
570
  const retD = await provider.call({ to: mgrAddr, data: ifDirect.encodeFunctionData('getBoost', [id]) });
308
571
  const [b] = ifDirect.decodeFunctionResult('getBoost', retD);
309
572
  const boostData = {
310
- managerType: 'direct', proposalId: opts.proposalId,
573
+ managerType: 'direct', proposalId: String(opts.proposalId || opts.boostId),
311
574
  creator: b.creator, governor: b.governor, snapshot: b.snapshot.toString(), perVoter: b.perVoter.toString(),
312
575
  maxVoters: b.maxVoters.toString(), votersPaid: b.votersPaid.toString(), minVotes: Number(b.minVotes),
313
576
  kind: Number(b.kind), policy: b.policy, active: b.active, paused: b.paused,
@@ -321,7 +584,7 @@ function register(program) {
321
584
  const retM = await provider.call({ to: mgrAddr, data: ifMerkle.encodeFunctionData('getBoost', [id]) });
322
585
  const [totalPool, totalClaimed, merkleRoot, active, finalized, creator] = ifMerkle.decodeFunctionResult('getBoost', retM);
323
586
  const boostData = {
324
- managerType: 'merkle', proposalId: opts.proposalId,
587
+ managerType: 'merkle', proposalId: String(opts.proposalId || opts.boostId),
325
588
  totalPool: totalPool.toString(), totalClaimed: totalClaimed.toString(), merkleRoot, active, finalized, creator
326
589
  };
327
590
  ui.json(boostData);
@@ -347,27 +610,7 @@ function register(program) {
347
610
  const mgrAddr = opts.manager || cliConfig.resolveAddress('BOOST_MANAGER_ADDRESS');
348
611
 
349
612
  // Manager-type preflight + helpful hint if mismatched
350
- const detectManagerType = async (address) => {
351
- const directIface = new ethers.Interface([
352
- 'function getBoost(uint256) view returns (tuple(address creator, address governor, uint256 proposalId, uint256 snapshot, uint256 perVoter, uint256 maxVoters, uint256 votersPaid, uint96 minVotes, uint8 payoutMode, uint8 support, uint256 startAt, uint256 expiresAt, uint256 totalPool, uint256 totalPaid, uint8 kind, address policy, bool active, bool paused) boost)'
353
- ]);
354
- const merkleIface = new ethers.Interface([
355
- 'function getBoost(uint256) view returns (uint256,uint256,bytes32,bool,bool,address)'
356
- ]);
357
- try {
358
- const retD = await provider.call({ to: address, data: directIface.encodeFunctionData('getBoost', [0n]) });
359
- directIface.decodeFunctionResult('getBoost', retD);
360
- return 'direct';
361
- } catch (_) {
362
- try {
363
- const retM = await provider.call({ to: address, data: merkleIface.encodeFunctionData('getBoost', [0n]) });
364
- merkleIface.decodeFunctionResult('getBoost', retM);
365
- return 'merkle';
366
- } catch (_) { return 'unknown'; }
367
- }
368
- };
369
-
370
- const mgrType = await detectManagerType(mgrAddr);
613
+ const mgrType = await detectManagerType(provider, mgrAddr);
371
614
  const id = BigInt(opts.proposalId);
372
615
  let looksEmpty = false;
373
616
  if (mgrType === 'direct') {
@@ -404,10 +647,12 @@ function register(program) {
404
647
 
405
648
  boost
406
649
  .command('claim')
407
- .description('Claim a boost rebate with Merkle proof')
650
+ .description('Claim a boost rebate (direct or merkle)')
408
651
  .requiredOption('--proposal-id <id>', 'Proposal ID')
409
- .requiredOption('--amount <usdc>', 'USDC amount (6 decimals)')
410
- .requiredOption('--proof <json>', 'JSON array of hex nodes or path to json file')
652
+ .option('--type <type>', 'direct|merkle|auto (default: auto)', 'auto')
653
+ .option('--amount <usdc>', 'USDC amount (required for merkle or variable payouts)')
654
+ .option('--proof <json>', 'JSON array of hex nodes or path to json file')
655
+ .option('--data <hex>', 'ABI-encoded bytes (overrides proof/amount for direct custom eligibility)')
411
656
  .option('--manager <address>', 'Boost manager')
412
657
  .option('--json', 'Output JSON', false)
413
658
  .action(async (opts) => {
@@ -416,19 +661,51 @@ function register(program) {
416
661
  enterJsonMode(opts);
417
662
  ui.configure({ json: opts.json });
418
663
  const WM = require('../wallet-manager');
419
- const fs = require('fs');
420
664
  const wm = new WM(); await wm.connect();
421
665
  const signer = wm.getSigner();
422
- const mgrAddr = opts.manager || cliConfig.resolveAddress('BOOST_MANAGER_ADDRESS');
423
- let proofStr = opts.proof;
424
- if (!proofStr.startsWith('[')) {
425
- proofStr = fs.readFileSync(proofStr, 'utf8');
666
+ const provider = wm.getProvider();
667
+ const mgrAddr = opts.manager || cliConfig.resolveAddress('BOOST_MANAGER_ADDRESS') || cliConfig.resolveAddress('BOOST_MANAGER_DIRECT_ADDRESS');
668
+
669
+ const type = String(opts.type || 'auto').toLowerCase();
670
+ const managerType = type === 'auto' ? await detectManagerType(provider, mgrAddr) : type;
671
+
672
+ if (managerType === 'merkle') {
673
+ if (!opts.amount) throw new Error('--amount is required for merkle claims');
674
+ if (!opts.proof) throw new Error('--proof is required for merkle claims');
675
+ const proof = parseProofInput(opts.proof);
676
+ const iface = new ethers.Interface(['function claim(uint256,address,uint256,bytes32[])']);
677
+ const tx = await signer.sendTransaction({ to: mgrAddr, data: iface.encodeFunctionData('claim', [BigInt(opts.proposalId), await signer.getAddress(), BigInt(String(opts.amount).replace(/_/g, '')), proof]) });
678
+ if (opts.json) {
679
+ printOk({ proposalId: String(opts.proposalId), boostId: String(opts.proposalId), manager: mgrAddr, amount: String(opts.amount), txHash: tx.hash });
680
+ return;
681
+ }
682
+ ui.success(`Transaction sent: ${tx.hash}`);
683
+ return;
684
+ }
685
+
686
+ if (managerType !== 'direct') {
687
+ throw new Error(`Unknown boost manager type at ${mgrAddr}. Pass --type direct|merkle.`);
688
+ }
689
+
690
+ let data = opts.data;
691
+ if (!data) {
692
+ if (opts.proof) {
693
+ const proof = parseProofInput(opts.proof);
694
+ const coder = ethers.AbiCoder.defaultAbiCoder();
695
+ if (opts.amount) {
696
+ data = coder.encode(['bytes32[]', 'uint256'], [proof, BigInt(String(opts.amount).replace(/_/g, ''))]);
697
+ } else {
698
+ data = coder.encode(['bytes32[]'], [proof]);
699
+ }
700
+ } else {
701
+ data = '0x';
702
+ }
426
703
  }
427
- const proof = JSON.parse(proofStr);
428
- const iface = new ethers.Interface(['function claim(uint256,address,uint256,bytes32[])']);
429
- const tx = await signer.sendTransaction({ to: mgrAddr, data: iface.encodeFunctionData('claim', [BigInt(opts.proposalId), await signer.getAddress(), BigInt(opts.amount), proof]) });
704
+
705
+ const iface = new ethers.Interface(['function claim(uint256,bytes)']);
706
+ const tx = await signer.sendTransaction({ to: mgrAddr, data: iface.encodeFunctionData('claim', [BigInt(opts.proposalId), data]) });
430
707
  if (opts.json) {
431
- printOk({ proposalId: String(opts.proposalId), boostId: String(opts.proposalId), manager: mgrAddr, amount: String(opts.amount), txHash: tx.hash });
708
+ printOk({ proposalId: String(opts.proposalId), boostId: String(opts.proposalId), manager: mgrAddr, txHash: tx.hash, managerType: 'direct' });
432
709
  return;
433
710
  }
434
711
  ui.success(`Transaction sent: ${tx.hash}`);
@@ -1709,6 +1709,7 @@ function register(program) {
1709
1709
  .option('--limit <number>', 'Maximum number of bounties to show', '10')
1710
1710
  .option('--dao <address>', 'Filter by DAO/SubDAO/Governor address')
1711
1711
  .option('--subdao <address>', 'Alias for --dao')
1712
+ .option('--json', 'Output JSON', false)
1712
1713
  .action(async (opts) => {
1713
1714
  try {
1714
1715
  const bountySystem = cliConfig.resolveAddress('SIMPLE_BOUNTY_SYSTEM_ADDRESS');
@@ -1718,7 +1719,7 @@ function register(program) {
1718
1719
  return;
1719
1720
  }
1720
1721
 
1721
- const rpcUrl = cliConfig.resolveRpcUrl();
1722
+ const rpcUrl = cliConfig.resolveRpcUrl({ allowEnv: true });
1722
1723
 
1723
1724
  // Resolve dao filter to governor address if provided
1724
1725
  let governorFilter = null;
@@ -1745,14 +1746,17 @@ function register(program) {
1745
1746
  const nextIdBigInt = await contracts.getNextBountyId(bountySystem, rpcUrl);
1746
1747
  const totalBounties = Number(nextIdBigInt);
1747
1748
 
1748
- ui.info(`Found ${totalBounties} total bounties`);
1749
- ui.header('Bounty List');
1749
+ if (!opts.json) {
1750
+ ui.info(`Found ${totalBounties} total bounties`);
1751
+ ui.header('Bounty List');
1752
+ }
1750
1753
 
1751
1754
  const limit = Math.min(parseInt(opts.limit), totalBounties);
1752
1755
  const statusFilter = opts.status?.toUpperCase();
1753
1756
  const statusMap = { 'ACTIVE': 0, 'CLAIMED': 1, 'COMPLETED': 2, 'CANCELLED': 3, 'EXPIRED': 4, 'UNDER_REVIEW': 5 };
1754
1757
 
1755
1758
  let displayed = 0;
1759
+ const results = [];
1756
1760
  for (let i = 0; i < totalBounties && displayed < limit; i++) {
1757
1761
  try {
1758
1762
  const bounty = await contracts.getBounty(bountySystem, i, rpcUrl);
@@ -1773,7 +1777,22 @@ function register(program) {
1773
1777
  const statusNames = ['ACTIVE', 'CLAIMED', 'COMPLETED', 'CANCELLED', 'EXPIRED', 'UNDER_REVIEW'];
1774
1778
  const rewardEther = ethers.formatEther(reward);
1775
1779
 
1776
- ui.output(`ID: ${i} | Status: ${statusNames[status]} | Reward: ${rewardEther} SXXX`);
1780
+ results.push({
1781
+ id: i,
1782
+ status: statusNames[status] || String(status),
1783
+ rewardWei: reward?.toString?.() || String(reward),
1784
+ rewardSxxx: rewardEther,
1785
+ creator: bounty.creator || null,
1786
+ deadline: bounty.deadline?.toString?.() || null,
1787
+ minContributorLevel: bounty.minContributorLevel?.toString?.() || null,
1788
+ minTokenBalance: bounty.minTokenBalance?.toString?.() || null,
1789
+ requiredBadgeId: bounty.requiredBadgeId?.toString?.() || null,
1790
+ libraryAction: bounty.libraryAction?.toString?.() || null
1791
+ });
1792
+
1793
+ if (!opts.json) {
1794
+ ui.output(`ID: ${i} | Status: ${statusNames[status]} | Reward: ${rewardEther} SXXX`);
1795
+ }
1777
1796
  displayed++;
1778
1797
 
1779
1798
  } catch (e) {
@@ -1782,6 +1801,11 @@ function register(program) {
1782
1801
  }
1783
1802
  }
1784
1803
 
1804
+ if (opts.json) {
1805
+ ui.json({ ok: true, total: totalBounties, count: results.length, bounties: results });
1806
+ return;
1807
+ }
1808
+
1785
1809
  if (displayed === 0) {
1786
1810
  ui.info('No bounties found matching the criteria');
1787
1811
  }
@@ -762,7 +762,16 @@ function register(program) {
762
762
  .addCommand(
763
763
  new Command('show')
764
764
  .description('Show current configuration')
765
- .action(() => {
765
+ .option('--json', 'Output JSON', false)
766
+ .action((opts) => {
767
+ if (opts.json) {
768
+ ui.json({
769
+ ok: true,
770
+ projectDir: config.getProjectDir(),
771
+ envPath: path.join(config.getProjectDir(), '.env')
772
+ });
773
+ return;
774
+ }
766
775
  ui.keyValue({
767
776
  'Current project directory': config.getProjectDir(),
768
777
  '.env file location': path.join(config.getProjectDir(), '.env')
@@ -16,10 +16,19 @@ function register(program) {
16
16
  try {
17
17
  const chalk = { green: (s)=>s, yellow: (s)=>s, red: (s)=>s, cyan: (s)=>s, gray: (s)=>s };
18
18
  const { ethers } = require('ethers');
19
- const sxxxTokenAddress = cliConfig.resolveAddress('SXXX_TOKEN_ADDRESS');
20
19
  const system = cliConfig.resolveAddress('SIMPLE_CONTRIBUTOR_SYSTEM_ADDRESS');
21
20
  if (!system) throw new Error('SimpleContributorSystem address not configured');
21
+ const rpcUrl = cliConfig.resolveRpcUrl({ allowEnv: true });
22
+ const provider = new ethers.JsonRpcProvider(rpcUrl);
23
+ const sysAbi = resolveArtifact('contracts/SimpleContributorSystem.sol/SimpleContributorSystem.json').abi;
24
+ const sysRead = new ethers.Contract(system, sysAbi, provider);
25
+ const systemToken = await sysRead.sxxxToken().catch(() => null);
26
+ const cfgToken = cliConfig.resolveAddress('SXXX_TOKEN_ADDRESS');
27
+ const sxxxTokenAddress = systemToken || cfgToken;
22
28
  if (!sxxxTokenAddress) throw new Error('SXXX token address not configured');
29
+ if (systemToken && cfgToken && systemToken.toLowerCase() !== cfgToken.toLowerCase()) {
30
+ ui.warn(`SXXX token mismatch: contributor system uses ${systemToken}, config has ${cfgToken}. Using system token.`);
31
+ }
23
32
  const WM = require('../wallet-manager');
24
33
  const wm = new WM();
25
34
  await wm.connect();
@@ -36,13 +45,12 @@ function register(program) {
36
45
  const ERC20 = [ 'function approve(address,uint256) returns (bool)' ];
37
46
  const tok = new ethers.Contract(sxxxTokenAddress, ERC20, signer);
38
47
  const txa = await tok.approve(system, amountWei);
39
- { const { waitForReceipt } = require('../utils/tx-wait'); const pr = new (require('ethers')).JsonRpcProvider(process.env.RPC_URL || process.env.BASE_SEPOLIA_RPC || process.env.BASE_RPC_URL || 'https://base-sepolia.publicnode.com'); await waitForReceipt(pr, txa, Number(process.env.SAGE_TX_WAIT_MS || 60000)); }
48
+ { const { waitForReceipt } = require('../utils/tx-wait'); await waitForReceipt(provider, txa, Number(process.env.SAGE_TX_WAIT_MS || 60000)); }
40
49
  ui.success('SXXX approval confirmed');
41
50
  ui.info('Staking tokens...');
42
- const sysAbi = resolveArtifact('contracts/SimpleContributorSystem.sol/SimpleContributorSystem.json').abi;
43
51
  const c = new ethers.Contract(system, sysAbi, signer);
44
52
  const txs = await c.stake(amountWei);
45
- { const { waitForReceipt } = require('../utils/tx-wait'); const pr = new (require('ethers')).JsonRpcProvider(process.env.RPC_URL || process.env.BASE_SEPOLIA_RPC || process.env.BASE_RPC_URL || 'https://base-sepolia.publicnode.com'); await waitForReceipt(pr, txs, Number(process.env.SAGE_TX_WAIT_MS || 60000)); }
53
+ { const { waitForReceipt } = require('../utils/tx-wait'); await waitForReceipt(provider, txs, Number(process.env.SAGE_TX_WAIT_MS || 60000)); }
46
54
  if (options.json) ui.json({ success: true, amount: amountWei.toString() });
47
55
  else ui.success(`Successfully staked ${ethers.formatEther(amountWei)} SXXX`);
48
56
  } catch (error) {
@@ -61,6 +69,8 @@ function register(program) {
61
69
  const { ethers } = require('ethers');
62
70
  const system = cliConfig.resolveAddress('SIMPLE_CONTRIBUTOR_SYSTEM_ADDRESS');
63
71
  if (!system) throw new Error('SimpleContributorSystem address not configured');
72
+ const rpcUrl = cliConfig.resolveRpcUrl({ allowEnv: true });
73
+ const provider = new ethers.JsonRpcProvider(rpcUrl);
64
74
  const WM = require('../wallet-manager');
65
75
  const wm = new WM();
66
76
  await wm.connect();
@@ -72,7 +82,7 @@ function register(program) {
72
82
  const sysAbi = resolveArtifact('contracts/SimpleContributorSystem.sol/SimpleContributorSystem.json').abi;
73
83
  const c = new ethers.Contract(system, sysAbi, signer);
74
84
  const tx = await c.withdrawStake(amountWei);
75
- { const { waitForReceipt } = require('../utils/tx-wait'); const pr = new (require('ethers')).JsonRpcProvider(process.env.RPC_URL || process.env.BASE_SEPOLIA_RPC || process.env.BASE_RPC_URL || 'https://base-sepolia.publicnode.com'); await waitForReceipt(pr, tx, Number(process.env.SAGE_TX_WAIT_MS || 60000)); }
85
+ { const { waitForReceipt } = require('../utils/tx-wait'); await waitForReceipt(provider, tx, Number(process.env.SAGE_TX_WAIT_MS || 60000)); }
76
86
  if (options.json) ui.json({ success: true, amount: amountWei.toString() });
77
87
  else ui.success(`Successfully unstaked ${ethers.formatEther(amountWei)} SXXX`);
78
88
  } catch (error) {
@@ -91,7 +101,7 @@ function register(program) {
91
101
  const { ethers } = require('ethers');
92
102
  const system = cliConfig.resolveAddress('SIMPLE_CONTRIBUTOR_SYSTEM_ADDRESS');
93
103
  if (!system) throw new Error('SimpleContributorSystem address not configured');
94
- const rpcUrl = process.env.RPC_URL || 'https://base-sepolia.publicnode.com';
104
+ const rpcUrl = cliConfig.resolveRpcUrl({ allowEnv: true });
95
105
  const provider = new ethers.JsonRpcProvider(rpcUrl);
96
106
  const sysAbi = resolveArtifact('contracts/SimpleContributorSystem.sol/SimpleContributorSystem.json').abi;
97
107
  const c = new ethers.Contract(system, sysAbi, provider);
@@ -41,7 +41,7 @@ function register(program) {
41
41
  if (options.tags) params.set('tags', options.tags);
42
42
  if (options.vector === false) params.set('useVector', 'false');
43
43
 
44
- const { body } = await client._fetchJson(`/discover/search?${params}`);
44
+ const { body } = await client._fetchJson(`/prompts/search?${params}`);
45
45
 
46
46
  if (options.json) {
47
47
  ui.json(body);
@@ -167,7 +167,7 @@ function register(program) {
167
167
  method: options.method,
168
168
  });
169
169
 
170
- const { body } = await client._fetchJson(`/discover/similar/${cid}?${params}`);
170
+ const { body } = await client._fetchJson(`/prompts/similar/${cid}?${params}`);
171
171
 
172
172
  if (options.json) {
173
173
  ui.json(body);
@@ -246,7 +246,7 @@ function register(program) {
246
246
  payload.diversify = true;
247
247
  }
248
248
 
249
- const { body } = await client._fetchJson('/discover/match', {
249
+ const { body } = await client._fetchJson('/prompts/match', {
250
250
  method: 'POST',
251
251
  headers: client.headers(),
252
252
  body: JSON.stringify(payload),