@sage-protocol/cli 0.8.0 → 0.8.3

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.
Files changed (34) hide show
  1. package/README.md +12 -11
  2. package/dist/cli/commands/boost.js +339 -62
  3. package/dist/cli/commands/bounty.js +28 -4
  4. package/dist/cli/commands/config.js +10 -1
  5. package/dist/cli/commands/contributor.js +16 -6
  6. package/dist/cli/commands/dao.js +1 -1
  7. package/dist/cli/commands/discover.js +3 -3
  8. package/dist/cli/commands/governance.js +141 -58
  9. package/dist/cli/commands/install.js +178 -36
  10. package/dist/cli/commands/ipfs.js +12 -2
  11. package/dist/cli/commands/library.js +277 -268
  12. package/dist/cli/commands/members.js +132 -18
  13. package/dist/cli/commands/multiplier.js +101 -13
  14. package/dist/cli/commands/nft.js +16 -3
  15. package/dist/cli/commands/personal.js +69 -2
  16. package/dist/cli/commands/prompt.js +1 -1
  17. package/dist/cli/commands/proposals.js +153 -3
  18. package/dist/cli/commands/stake-status.js +130 -56
  19. package/dist/cli/commands/sxxx.js +37 -4
  20. package/dist/cli/commands/wallet.js +5 -10
  21. package/dist/cli/contracts/index.js +2 -1
  22. package/dist/cli/index.js +5 -0
  23. package/dist/cli/privy-auth-wallet-manager.js +3 -2
  24. package/dist/cli/services/config/chain-defaults.js +1 -1
  25. package/dist/cli/services/config/manager.js +3 -0
  26. package/dist/cli/services/config/schema.js +1 -0
  27. package/dist/cli/services/ipfs/onboarding.js +11 -0
  28. package/dist/cli/utils/aliases.js +62 -3
  29. package/dist/cli/utils/cli-ui.js +1 -1
  30. package/dist/cli/utils/provider.js +7 -3
  31. package/dist/cli/wallet-manager.js +7 -12
  32. package/dist/prompts/e2e-test-prompt.md +22 -0
  33. package/dist/prompts/skills/build-web3/plugin.json +11 -0
  34. package/package.json +1 -1
@@ -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);
@@ -847,7 +847,7 @@ function register(program) {
847
847
  const favorites = subdaoStore.listFavorites();
848
848
  const recent = subdaoStore.listRecent();
849
849
  if (opts.json) {
850
- ui.json({ favorites, recent }, null, 2, { pretty: false });
850
+ ui.json({ favorites, recent }, { pretty: false });
851
851
  return;
852
852
  }
853
853
  if (!favorites.length && !recent.length) return ui.info('No saved DAOs. Use `sage dao save <address> --alias <name>`.');
@@ -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),
@@ -19,6 +19,56 @@ const logger = require('../utils/logger');
19
19
  const hooks = require('../utils/hooks');
20
20
  const contracts = require('../contracts');
21
21
 
22
+ async function getLogsChunked(provider, filter, maxRange = 50000) {
23
+ // Default to 50k blocks to stay under most RPC limits (Base Sepolia limits to 100k)
24
+ const latest = filter.toBlock === 'latest' || typeof filter.toBlock === 'undefined'
25
+ ? await provider.getBlockNumber()
26
+ : Number(filter.toBlock);
27
+ const from = Number(filter.fromBlock || 0);
28
+ const results = [];
29
+ for (let start = from; start <= latest; start += maxRange) {
30
+ const end = Math.min(latest, start + maxRange - 1);
31
+ try {
32
+ const batch = await provider.getLogs({ ...filter, fromBlock: start, toBlock: end });
33
+ if (batch && batch.length) results.push(...batch);
34
+ } catch (e) {
35
+ if (process.env.SAGE_VERBOSE === '1') {
36
+ console.warn(`Log query failed for blocks ${start}-${end}: ${e.message}`);
37
+ }
38
+ }
39
+ }
40
+ return results;
41
+ }
42
+
43
+ async function findProposalCreatedLog(provider, govAddr, gov, pid, lookbacks) {
44
+ const eventSig = gov.interface.getEvent('ProposalCreated').topicHash;
45
+ const currentBlock = await provider.getBlockNumber();
46
+ const pidHex = ethers.hexlify(ethers.toBeHex(pid));
47
+ for (const span of lookbacks) {
48
+ const from = currentBlock > span ? currentBlock - span : 0;
49
+ let logs = await getLogsChunked(provider, {
50
+ topics: [eventSig, ethers.zeroPadValue(pidHex, 32)],
51
+ address: govAddr,
52
+ fromBlock: from,
53
+ toBlock: 'latest'
54
+ });
55
+ if (!logs.length) {
56
+ const all = await getLogsChunked(provider, { topics: [eventSig], address: govAddr, fromBlock: from, toBlock: 'latest' });
57
+ for (const l of all) {
58
+ try {
59
+ const p = gov.interface.parseLog(l);
60
+ if (p && p.name === 'ProposalCreated' && BigInt(p.args.proposalId.toString()) === pid) {
61
+ logs = [l];
62
+ break;
63
+ }
64
+ } catch { }
65
+ }
66
+ }
67
+ if (logs.length) return logs[0];
68
+ }
69
+ return null;
70
+ }
71
+
22
72
  function register(program) {
23
73
  const governanceCommand = new Command('governance')
24
74
  .description('Governance management commands')
@@ -69,7 +119,7 @@ function register(program) {
69
119
  try {
70
120
  const { resolveGovContext } = require('../utils/gov-context');
71
121
  const { ethers } = require('ethers');
72
- const provider = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
122
+ const provider = new ethers.JsonRpcProvider(cliConfig.resolveRpcUrl({ allowEnv: true }));
73
123
  const ctx = await resolveGovContext({ govOpt: opts.gov, subdaoOpt: opts.subdao, provider, useGovernorOnly: process.env.SAGE_USE_GOVERNOR === '1' });
74
124
  const govAddr = ctx.governor || process.env.GOV;
75
125
  if (!govAddr) throw new Error('Governor address not resolved. Pass --gov or --subdao.');
@@ -79,22 +129,10 @@ function register(program) {
79
129
  const pid = (typeof id === 'string' && id.startsWith('0x')) ? BigInt(id) : BigInt(String(id));
80
130
 
81
131
  // Locate ProposalCreated event
82
- const eventSig = gov.interface.getEvent('ProposalCreated').topicHash;
83
- const currentBlock = await provider.getBlockNumber();
84
132
  let parsed = null; let logTxHash = null;
85
133
  const lookbacks = [20000, 100000, 500000];
86
- const pidHex = ethers.hexlify(ethers.toBeHex(pid));
87
- for (const span of lookbacks) {
88
- const from = currentBlock > span ? currentBlock - span : 0;
89
- let logs = await provider.getLogs({ topics: [eventSig, ethers.zeroPadValue(pidHex, 32)], address: govAddr, fromBlock: from, toBlock: 'latest' });
90
- if (!logs.length) {
91
- const all = await provider.getLogs({ topics: [eventSig], address: govAddr, fromBlock: from, toBlock: 'latest' });
92
- for (const l of all) {
93
- try { const p = gov.interface.parseLog(l); if (p && BigInt(p.args.proposalId.toString()) === pid) { logs = [l]; break; } } catch {}
94
- }
95
- }
96
- if (logs.length) { parsed = gov.interface.parseLog(logs[0]); logTxHash = logs[0].transactionHash; break; }
97
- }
134
+ const hit = await findProposalCreatedLog(provider, govAddr, gov, pid, lookbacks);
135
+ if (hit) { parsed = gov.interface.parseLog(hit); logTxHash = hit.transactionHash; }
98
136
  if (!parsed) throw new Error('ProposalCreated event not found; verify Governor/ID');
99
137
 
100
138
  const targets = Array.from(parsed.args.targets, (a)=>String(a));
@@ -218,7 +256,7 @@ function register(program) {
218
256
  try {
219
257
  const { resolveGovContext } = require('../utils/gov-context');
220
258
  const { ethers } = require('ethers');
221
- const provider = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
259
+ const provider = new ethers.JsonRpcProvider(cliConfig.resolveRpcUrl({ allowEnv: true }));
222
260
  const ctx = await resolveGovContext({ govOpt: opts.gov, subdaoOpt: opts.subdao, provider, useGovernorOnly: process.env.SAGE_USE_GOVERNOR === '1' });
223
261
  const govAddr = ctx.governor || process.env.GOV;
224
262
  if (!govAddr) throw new Error('Governor address not resolved. Pass --gov or --subdao.');
@@ -241,14 +279,10 @@ function register(program) {
241
279
  } catch(_) {}
242
280
 
243
281
  // --- Decode effects (library/prompt) with previous ⇒ new
244
- const ProposalCreatedABI = ['event ProposalCreated(uint256 id,address proposer,address[] targets,uint256[] values,string[] signatures,bytes[] calldatas,uint256 startBlock,uint256 endBlock,string description)'];
245
- const evIface = new ethers.Interface(ProposalCreatedABI);
246
- const topic = evIface.getEvent('ProposalCreated').topicHash;
247
282
  let parsed = null; let logTxHash = null;
248
- const logs = await provider.getLogs({ address: govAddr, fromBlock: 0, toBlock: 'latest', topics: [topic] });
249
- for (const log of logs) {
250
- try { const p = evIface.parseLog(log); if (BigInt(p.args.id.toString()) === pid) { parsed = p; logTxHash = log.transactionHash; break; } } catch(_) {}
251
- }
283
+ const lookbacks = [20000, 100000, 500000];
284
+ const hit = await findProposalCreatedLog(provider, govAddr, gov, pid, lookbacks);
285
+ if (hit) { parsed = gov.interface.parseLog(hit); logTxHash = hit.transactionHash; }
252
286
  if (!parsed) throw new Error('ProposalCreated event not found');
253
287
  const targets = parsed.args.targets.map(String);
254
288
  const calldatas = parsed.args.calldatas.map((b)=> ethers.hexlify(b));
@@ -498,7 +532,7 @@ function register(program) {
498
532
  .option('--json', 'Output JSON', false)
499
533
  .action(async (opts) => {
500
534
  try {
501
- const provider = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
535
+ const provider = new ethers.JsonRpcProvider(cliConfig.resolveRpcUrl({ allowEnv: true }));
502
536
  const { resolveGovContext } = require('../utils/gov-context');
503
537
  const ctx = await resolveGovContext({ govOpt: opts.gov, subdaoOpt: opts.subdao, provider });
504
538
  if (opts.subdao) {
@@ -563,7 +597,7 @@ function register(program) {
563
597
  try {
564
598
  const { resolveGovContext } = require('../utils/gov-context');
565
599
  const { ethers } = require('ethers');
566
- const provider = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
600
+ const provider = new ethers.JsonRpcProvider(cliConfig.resolveRpcUrl({ allowEnv: true }));
567
601
  const ctx = await resolveGovContext({ govOpt: options.gov, subdaoOpt: options.subdao, provider, useGovernorOnly: process.env.SAGE_USE_GOVERNOR === '1' });
568
602
  const govAddr = ctx.governor || process.env.GOV;
569
603
  if (!govAddr) throw new Error('Governor address not resolved. Pass --gov or --subdao.');
@@ -572,43 +606,91 @@ function register(program) {
572
606
  const gov = new ethers.Contract(govAddr, GovernorABI, provider);
573
607
  const pid = (typeof id === 'bigint') ? id : (typeof id === 'string' && id.startsWith('0x') ? BigInt(id) : BigInt(String(id)));
574
608
 
575
- // --- Load ProposalCreated event to recover tuple and description ---
576
- const eventSig = gov.interface.getEvent('ProposalCreated').topicHash;
577
- const currentBlock = await provider.getBlockNumber();
578
- const lookbacks = [20000, 100000, 500000];
579
- let parsed = null; let logTxHash = null;
580
- const pidHex = (typeof id === 'string' && id.startsWith('0x')) ? id : ethers.hexlify(ethers.toBeHex(pid));
581
- for (const span of lookbacks) {
582
- const from = currentBlock > span ? currentBlock - span : 0;
583
- let logs = await provider.getLogs({ topics: [eventSig, ethers.zeroPadValue(pidHex, 32)], address: govAddr, fromBlock: from, toBlock: 'latest' });
584
- if (!logs.length) {
585
- // broad scan and filter
586
- const all = await provider.getLogs({ topics: [eventSig], address: govAddr, fromBlock: from, toBlock: 'latest' });
587
- for (const l of all) {
588
- try {
589
- const p = gov.interface.parseLog(l);
590
- if (p && p.name === 'ProposalCreated' && BigInt(p.args.proposalId.toString()) === pid) { logs = [l]; break; }
591
- } catch { }
609
+ // --- Load proposal data: prefer subgraph (no block range limits), fallback to RPC ---
610
+ const subgraphClient = require('../utils/subgraph-client');
611
+ let targets = [];
612
+ let values = [];
613
+ let calldatas = [];
614
+ let description = '';
615
+ let logTxHash = null;
616
+ let usedSubgraph = false;
617
+
618
+ // Try subgraph first (preferred - no block range limits)
619
+ try {
620
+ const status = await subgraphClient.checkSubgraphStatus(provider);
621
+ if (status.available) {
622
+ const proposals = await subgraphClient.getProposals(govAddr, { first: 200 });
623
+ const pidDec = pid.toString();
624
+ const match = proposals.find((p) => {
625
+ const parts = String(p.id || '').split('-');
626
+ const last = parts.length > 1 ? parts[parts.length - 1] : String(p.id || '');
627
+ if (!last) return false;
628
+ if (last === pidDec) return true;
629
+ try { if (String(last).startsWith('0x') && BigInt(String(last)) === pid) return true; } catch (_) { }
630
+ return false;
631
+ });
632
+ if (match && match.targets?.length) {
633
+ targets = Array.from(match.targets || [], (a) => String(a));
634
+ values = Array.from(match.values || [], (v) => BigInt(String(v || 0)));
635
+ calldatas = Array.from(match.calldatas || [], (b) => String(b));
636
+ description = String(match.description || '');
637
+ usedSubgraph = true;
638
+ if (process.env.SAGE_VERBOSE === '1') {
639
+ ui.info(`Using subgraph (indexed to block ${status.indexedBlock})`);
640
+ }
592
641
  }
593
642
  }
594
- if (logs.length) {
595
- parsed = gov.interface.parseLog(logs[0]);
596
- logTxHash = logs[0].transactionHash;
597
- break;
643
+ } catch (sgErr) {
644
+ if (process.env.SAGE_VERBOSE === '1') {
645
+ ui.warn(`Subgraph unavailable: ${sgErr.message}. Falling back to RPC.`);
598
646
  }
599
647
  }
600
- if (!parsed) throw new Error('ProposalCreated event not found; try increasing lookback or verify Governor/ID');
601
648
 
602
- // Extract tuple
603
- const targets = Array.from(parsed.args.targets, (a) => String(a));
604
- let values = Array.from(parsed.args.values, (v) => BigInt(v.toString()));
605
- const calldatas = Array.from(parsed.args.calldatas, (b) => ethers.hexlify(b));
606
- const description = String(parsed.args.description ?? '');
607
- if (values.length === 0 && targets.length > 0) values = targets.map(() => 0n);
649
+ // Fallback to RPC log scan (chunked to stay under block range limits)
650
+ if (!usedSubgraph) {
651
+ const eventSig = gov.interface.getEvent('ProposalCreated').topicHash;
652
+ const currentBlock = await provider.getBlockNumber();
653
+ const MAX_CHUNK = 50000;
654
+ const lookbacks = [20000, 100000, 500000];
655
+ let parsed = null;
656
+ const pidHex = (typeof id === 'string' && id.startsWith('0x')) ? id : ethers.hexlify(ethers.toBeHex(pid));
657
+
658
+ for (const span of lookbacks) {
659
+ if (parsed) break;
660
+ const from = currentBlock > span ? currentBlock - span : 0;
661
+ for (let start = from; start <= currentBlock && !parsed; start += MAX_CHUNK) {
662
+ const chunkEnd = Math.min(start + MAX_CHUNK - 1, currentBlock);
663
+ try {
664
+ let logs = await provider.getLogs({ topics: [eventSig, ethers.zeroPadValue(pidHex, 32)], address: govAddr, fromBlock: start, toBlock: chunkEnd });
665
+ if (!logs.length) {
666
+ const all = await provider.getLogs({ topics: [eventSig], address: govAddr, fromBlock: start, toBlock: chunkEnd });
667
+ for (const l of all) {
668
+ try {
669
+ const p = gov.interface.parseLog(l);
670
+ if (p && p.name === 'ProposalCreated' && BigInt(p.args.proposalId.toString()) === pid) { logs = [l]; break; }
671
+ } catch { }
672
+ }
673
+ }
674
+ if (logs.length) {
675
+ parsed = gov.interface.parseLog(logs[0]);
676
+ logTxHash = logs[0].transactionHash;
677
+ }
678
+ } catch (e) {
679
+ if (process.env.SAGE_VERBOSE === '1') {
680
+ console.warn(`Log query failed for blocks ${start}-${chunkEnd}: ${e.message}`);
681
+ }
682
+ }
683
+ }
684
+ }
685
+ if (!parsed) throw new Error('ProposalCreated event not found; configure SUBGRAPH_URL or verify Governor/ID');
686
+ targets = Array.from(parsed.args.targets, (a) => String(a));
687
+ values = Array.from(parsed.args.values, (v) => BigInt(v.toString()));
688
+ calldatas = Array.from(parsed.args.calldatas, (b) => ethers.hexlify(b));
689
+ description = String(parsed.args.description ?? '');
690
+ }
608
691
 
609
- const descriptionHash = (parsed.args.descriptionHash && String(parsed.args.descriptionHash).length === 66)
610
- ? String(parsed.args.descriptionHash)
611
- : ethers.keccak256(ethers.toUtf8Bytes(description));
692
+ if (values.length === 0 && targets.length > 0) values = targets.map(() => 0n);
693
+ const descriptionHash = ethers.keccak256(ethers.toUtf8Bytes(description));
612
694
 
613
695
  // --- State, ETA, Timelock ---
614
696
  const stateNum = Number(await gov.state(pid).catch(() => 255));
@@ -1935,11 +2017,12 @@ function register(program) {
1935
2017
  .action(async (options) => {
1936
2018
  try {
1937
2019
  if (options.json) process.env.SAGE_QUIET_JSON = '1';
1938
- if (options.rpc) process.env.RPC_URL = options.rpc;
1939
2020
  // Unified context resolution
1940
2021
  const { resolveGovContext } = require('../utils/gov-context');
1941
2022
  const { ethers } = require('ethers');
1942
- const provider = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
2023
+ const rpcUrl = options.rpc || cliConfig.resolveRpcUrl({ allowEnv: true });
2024
+ process.env.SAGE_RPC_URL = rpcUrl;
2025
+ const provider = new ethers.JsonRpcProvider(rpcUrl);
1943
2026
  const ctx = await resolveGovContext({ govOpt: options.gov, subdaoOpt: options.subdao, provider });
1944
2027
  if (ctx.governor) process.env.GOV = ctx.governor;
1945
2028
  if (ctx.subdao) process.env.WORKING_SUBDAO_ADDRESS = ctx.subdao, process.env.SUBDAO = ctx.subdao;