@sage-protocol/cli 0.8.2 → 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.
@@ -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;
@@ -369,9 +369,19 @@ function register(program) {
369
369
  ui.configure({ verbose: opts.verbose, json: opts.json });
370
370
 
371
371
  const baseUrl = opts.workerUrl || (config.readIpfsConfig && (config.readIpfsConfig().workerBaseUrl || '')) || DEFAULT_WORKER_BASE;
372
- const wc = new WorkerClient({ baseUrl, token: opts.workerToken, address: opts.workerAddress });
372
+ let workerAddress = opts.workerAddress;
373
+ if (!workerAddress) {
374
+ try {
375
+ const wallet = await getConnectedWallet();
376
+ workerAddress = wallet?.account || (wallet?.signer && await wallet.signer.getAddress()) || null;
377
+ } catch (_) { /* ignore */ }
378
+ }
379
+ if (!workerAddress) {
380
+ throw new Error('address required (set --worker-address or configure a wallet)');
381
+ }
382
+ const wc = new WorkerClient({ baseUrl, token: opts.workerToken, address: workerAddress });
373
383
  const result = await wc.listPins({
374
- address: opts.workerAddress,
384
+ address: workerAddress,
375
385
  status: opts.status,
376
386
  limit: opts.limit,
377
387
  });
@@ -364,7 +364,8 @@ QUICK START
364
364
  const subdaoStore = require('../utils/subdao-store');
365
365
 
366
366
  // Resolve SubDAO address
367
- let subdao = opts.subdao || subdaoStore.getCurrent() || process.env.SUBDAO;
367
+ const current = subdaoStore.getCurrent();
368
+ let subdao = opts.subdao || (current && current.address) || process.env.SUBDAO;
368
369
  if (!subdao) {
369
370
  throw new Error('No DAO specified. Use --subdao or run `sage dao use <address>`');
370
371
  }
@@ -20,6 +20,7 @@ function register(program) {
20
20
  .option('--all', 'List members for all SubDAOs', false)
21
21
  .option('--limit <n>', 'Max members per SubDAO to print', '100')
22
22
  .option('--include-inactive', 'Include inactive members', false)
23
+ .option('--json', 'Output JSON', false)
23
24
  .action(async (opts) => {
24
25
  try {
25
26
  const { ethers } = require('ethers');
@@ -34,26 +35,52 @@ function register(program) {
34
35
  subdaos = [addr];
35
36
  }
36
37
  const first = Number(opts.limit || '100');
38
+ const results = [];
37
39
  for (const s of subdaos) {
38
40
  const where = opts.includeInactive ? `subdao: "${ethers.getAddress(s)}"` : `subdao: "${ethers.getAddress(s)}", active: true`;
39
- const q = `query($first:Int,$skip:Int){ members(where:{${where}}, first:$first, skip:$skip, orderBy: account, orderDirection: asc){ account currentStake active } }`;
41
+ const qWithStake = `query($first:Int,$skip:Int){ members(where:{${where}}, first:$first, skip:$skip, orderBy: account, orderDirection: asc){ account currentStake active } }`;
42
+ const qNoStake = `query($first:Int,$skip:Int){ members(where:{${where}}, first:$first, skip:$skip, orderBy: account, orderDirection: asc){ account active } }`;
43
+ let stakeSupported = true;
40
44
  let skip = 0; let printed = 0; let batch;
41
- ui.newline();
42
- ui.output(`SubDAO: ${s}`);
43
- ui.output('Account Active CurrentStake');
44
- ui.output('----------------------------------------------------------');
45
+ const members = [];
46
+ if (!opts.json) {
47
+ ui.newline();
48
+ ui.output(`SubDAO: ${s}`);
49
+ ui.output('Account Active CurrentStake');
50
+ ui.output('----------------------------------------------------------');
51
+ }
45
52
  do {
46
- batch = (await fetchGraph(q, { first: Math.min(first - printed, 1000), skip })).members || [];
53
+ try {
54
+ batch = (await fetchGraph(stakeSupported ? qWithStake : qNoStake, { first: Math.min(first - printed, 1000), skip })).members || [];
55
+ } catch (e) {
56
+ const msg = String(e && e.message ? e.message : e);
57
+ if (stakeSupported && /currentStake/i.test(msg)) {
58
+ stakeSupported = false;
59
+ batch = (await fetchGraph(qNoStake, { first: Math.min(first - printed, 1000), skip })).members || [];
60
+ } else {
61
+ throw e;
62
+ }
63
+ }
47
64
  for (const m of batch) {
48
65
  const acct = m.account;
49
- const stake = m.currentStake;
66
+ const stake = stakeSupported ? m.currentStake : '0';
50
67
  const active = m.active ? 'yes ' : 'no ';
51
- ui.output(`${acct} ${active} ${stake}`);
68
+ members.push(opts.json
69
+ ? { account: acct, active: !!m.active, currentStake: stake }
70
+ : { account: acct, active: !!m.active, currentStake: stake }
71
+ );
72
+ if (!opts.json) {
73
+ ui.output(`${acct} ${active} ${stake}`);
74
+ }
52
75
  printed += 1; if (printed >= first) break;
53
76
  }
54
77
  skip += batch.length;
55
78
  } while (batch.length && printed < first);
56
- if (printed === 0) ui.output('(no members found)');
79
+ if (!opts.json && printed === 0) ui.output('(no members found)');
80
+ results.push({ subdao: s, count: members.length, members });
81
+ }
82
+ if (opts.json) {
83
+ ui.json({ ok: true, subdaos: results });
57
84
  }
58
85
  } catch (e) {
59
86
  handleCLIError('members:list', e);
@@ -65,6 +92,7 @@ function register(program) {
65
92
  .option('--subdao <address>', 'SubDAO address')
66
93
  .option('--all', 'List stakes for all SubDAOs', false)
67
94
  .option('--limit <n>', 'Max rows per SubDAO', '100')
95
+ .option('--json', 'Output JSON', false)
68
96
  .action(async (opts) => {
69
97
  try {
70
98
  const { ethers } = require('ethers');
@@ -79,32 +107,118 @@ function register(program) {
79
107
  subdaos = [addr];
80
108
  }
81
109
  const first = Number(opts.limit || '100');
110
+ const results = [];
82
111
  for (const s of subdaos) {
83
112
  const q = `query($first:Int,$skip:Int){ members(where:{subdao: "${ethers.getAddress(s)}", currentStake_gt: "0"}, first:$first, skip:$skip, orderBy: currentStake, orderDirection: desc){ account currentStake active } }`;
84
113
  let skip = 0; let printed = 0; let batch; let total = 0n;
85
- ui.newline();
86
- ui.output(`SubDAO: ${s}`);
87
- ui.output('Account Active CurrentStake');
88
- ui.output('----------------------------------------------------------');
114
+ const members = [];
115
+ if (!opts.json) {
116
+ ui.newline();
117
+ ui.output(`SubDAO: ${s}`);
118
+ ui.output('Account Active CurrentStake');
119
+ ui.output('----------------------------------------------------------');
120
+ }
89
121
  do {
90
- batch = (await fetchGraph(q, { first: Math.min(first - printed, 1000), skip })).members || [];
122
+ try {
123
+ batch = (await fetchGraph(q, { first: Math.min(first - printed, 1000), skip })).members || [];
124
+ } catch (e) {
125
+ const msg = String(e && e.message ? e.message : e);
126
+ if (/currentStake/i.test(msg)) {
127
+ throw new Error('Subgraph schema missing currentStake. Redeploy subgraph or use `sage members list` instead.');
128
+ }
129
+ throw e;
130
+ }
91
131
  for (const m of batch) {
92
132
  const acct = m.account; const stake = BigInt(m.currentStake); const active = m.active ? 'yes ' : 'no ';
93
133
  total += stake;
94
- ui.output(`${acct} ${active} ${m.currentStake}`);
134
+ members.push({ account: acct, active: !!m.active, currentStake: m.currentStake });
135
+ if (!opts.json) {
136
+ ui.output(`${acct} ${active} ${m.currentStake}`);
137
+ }
95
138
  printed += 1; if (printed >= first) break;
96
139
  }
97
140
  skip += batch.length;
98
141
  } while (batch.length && printed < first);
99
- ui.output('----------------------------------------------------------');
100
- ui.output(`Total current stake (visible subset): ${total.toString()}`);
101
- if (printed === 0) ui.output('(no stakers found)');
142
+ if (!opts.json) {
143
+ ui.output('----------------------------------------------------------');
144
+ ui.output(`Total current stake (visible subset): ${total.toString()}`);
145
+ if (printed === 0) ui.output('(no stakers found)');
146
+ }
147
+ results.push({ subdao: s, count: members.length, totalStake: total.toString(), members });
148
+ }
149
+ if (opts.json) {
150
+ ui.json({ ok: true, subdaos: results });
102
151
  }
103
152
  } catch (e) {
104
153
  handleCLIError('members:current-stake', e);
105
154
  }
106
155
  });
107
156
 
157
+ cmd.command('voting-power')
158
+ .description('List delegated SXXX voting power for members (via token getVotes)')
159
+ .option('--subdao <address>', 'SubDAO address; omit to select interactively or use --all')
160
+ .option('--all', 'List members for all SubDAOs', false)
161
+ .option('--limit <n>', 'Max members per SubDAO to print', '100')
162
+ .option('--include-inactive', 'Include inactive members', false)
163
+ .option('--json', 'Output JSON', false)
164
+ .action(async (opts) => {
165
+ try {
166
+ const { ethers } = require('ethers');
167
+ const cliConfig = require('../config');
168
+ const SubDAOManager = require('../subdao-manager');
169
+ const manager = new SubDAOManager(); await manager.initialize();
170
+ const rpcUrl = cliConfig.resolveRpcUrl({ allowEnv: true });
171
+ const provider = new ethers.JsonRpcProvider(rpcUrl);
172
+ const sxxxAddress = cliConfig.resolveAddress('SXXX_TOKEN_ADDRESS');
173
+ if (!sxxxAddress) throw new Error('SXXX_TOKEN_ADDRESS not configured');
174
+ const sxxx = new ethers.Contract(sxxxAddress, ['function getVotes(address) view returns (uint256)'], provider);
175
+
176
+ let subdaos = [];
177
+ if (opts.all) {
178
+ const data = await fetchGraph('{ subDAOs(first: 1000) { id address } }');
179
+ subdaos = (data.subDAOs || []).map(s => s.address || s.id);
180
+ } else {
181
+ const addr = await manager.resolveSubDAOAddress(opts.subdao);
182
+ subdaos = [addr];
183
+ }
184
+ const first = Number(opts.limit || '100');
185
+ const results = [];
186
+ for (const s of subdaos) {
187
+ const where = opts.includeInactive ? `subdao: "${ethers.getAddress(s)}"` : `subdao: "${ethers.getAddress(s)}", active: true`;
188
+ const q = `query($first:Int,$skip:Int){ members(where:{${where}}, first:$first, skip:$skip, orderBy: account, orderDirection: asc){ account active } }`;
189
+ let skip = 0; let printed = 0; let batch;
190
+ const members = [];
191
+ if (!opts.json) {
192
+ ui.newline();
193
+ ui.output(`SubDAO: ${s}`);
194
+ ui.output('Account Active VotingPower(SXXX)');
195
+ ui.output('----------------------------------------------------------------');
196
+ }
197
+ do {
198
+ batch = (await fetchGraph(q, { first: Math.min(first - printed, 1000), skip })).members || [];
199
+ for (const m of batch) {
200
+ const acct = m.account;
201
+ const active = m.active ? 'yes ' : 'no ';
202
+ const votes = await sxxx.getVotes(acct).catch(() => 0n);
203
+ members.push({ account: acct, active: !!m.active, votingPower: votes.toString() });
204
+ if (!opts.json) {
205
+ ui.output(`${acct} ${active} ${ethers.formatEther(votes)}`);
206
+ }
207
+ printed += 1; if (printed >= first) break;
208
+ }
209
+ skip += batch.length;
210
+ } while (batch.length && printed < first);
211
+ if (!opts.json && printed === 0) ui.output('(no members found)');
212
+ results.push({ subdao: s, count: members.length, members });
213
+ }
214
+ if (opts.json) {
215
+ ui.json({ ok: true, subdaos: results });
216
+ }
217
+ } catch (e) {
218
+ handleCLIError('members:voting-power', e);
219
+ }
220
+ });
221
+
108
222
  program.addCommand(cmd);
109
223
  }
110
224