@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.
- package/dist/cli/commands/boost.js +339 -62
- package/dist/cli/commands/bounty.js +28 -4
- package/dist/cli/commands/config.js +10 -1
- package/dist/cli/commands/contributor.js +16 -6
- package/dist/cli/commands/discover.js +3 -3
- package/dist/cli/commands/doctor.js +27 -20
- package/dist/cli/commands/governance.js +141 -58
- package/dist/cli/commands/ipfs.js +12 -2
- package/dist/cli/commands/library.js +2 -1
- package/dist/cli/commands/members.js +132 -18
- package/dist/cli/commands/multiplier.js +101 -13
- package/dist/cli/commands/nft.js +16 -3
- package/dist/cli/commands/prompt.js +1 -1
- package/dist/cli/commands/proposals.js +153 -3
- package/dist/cli/commands/stake-status.js +130 -56
- package/dist/cli/commands/sxxx.js +37 -4
- package/dist/cli/contracts/index.js +2 -1
- package/dist/cli/utils/aliases.js +61 -3
- package/package.json +1 -1
|
@@ -48,9 +48,9 @@ async function runDoctor(opts = {}) {
|
|
|
48
48
|
const rpcUrl = process.env.RPC_URL || 'http://127.0.0.1:8545';
|
|
49
49
|
const chainId = Number(process.env.CHAIN_ID || 84532);
|
|
50
50
|
const addresses = {
|
|
51
|
-
SXXX_TOKEN_ADDRESS: config.resolveAddress('SXXX_TOKEN_ADDRESS')
|
|
52
|
-
LIBRARY_REGISTRY_ADDRESS: config.resolveAddress('LIBRARY_REGISTRY_ADDRESS')
|
|
53
|
-
SUBDAO_FACTORY_ADDRESS: config.resolveAddress('SUBDAO_FACTORY_ADDRESS')
|
|
51
|
+
SXXX_TOKEN_ADDRESS: config.resolveAddress('SXXX_TOKEN_ADDRESS'),
|
|
52
|
+
LIBRARY_REGISTRY_ADDRESS: config.resolveAddress('LIBRARY_REGISTRY_ADDRESS'),
|
|
53
|
+
SUBDAO_FACTORY_ADDRESS: config.resolveAddress('SUBDAO_FACTORY_ADDRESS'),
|
|
54
54
|
};
|
|
55
55
|
ui.info('Sage Doctor Report (test mode)');
|
|
56
56
|
ui.keyValue({ 'RPC': rpcUrl, 'chainId': chainId });
|
|
@@ -257,6 +257,7 @@ async function runDoctor(opts = {}) {
|
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
// 4) Required addresses (config prerequisites)
|
|
260
|
+
// Uses config.resolveAddress() to check profile, env, AND chain defaults
|
|
260
261
|
out.push(c.cyan('⚙️ Config prerequisites'));
|
|
261
262
|
const requiredAddrs = [
|
|
262
263
|
{ key: 'SXXX_TOKEN_ADDRESS', label: 'SXXX token', cliFlag: '--sxxx' },
|
|
@@ -265,9 +266,11 @@ async function runDoctor(opts = {}) {
|
|
|
265
266
|
];
|
|
266
267
|
for (const entry of requiredAddrs) {
|
|
267
268
|
const k = entry.key;
|
|
268
|
-
const v =
|
|
269
|
+
const v = config.resolveAddress(k);
|
|
270
|
+
const isDefault = v && !process.env[k];
|
|
269
271
|
if (v) {
|
|
270
|
-
|
|
272
|
+
const suffix = isDefault ? ' (default)' : '';
|
|
273
|
+
out.push(`✅ ${k}: ${v}${suffix}`);
|
|
271
274
|
} else {
|
|
272
275
|
out.push(`❌ ${k} (missing)`);
|
|
273
276
|
const cliHint = entry.cliFlag ? ` or use "sage config addresses set ${entry.cliFlag} <0x...>"` : '';
|
|
@@ -283,15 +286,18 @@ async function runDoctor(opts = {}) {
|
|
|
283
286
|
}
|
|
284
287
|
|
|
285
288
|
// Optional feature addresses (warn-level): Bounty system, Boost managers, etc.
|
|
289
|
+
// Uses config.resolveAddress() to check profile, env, AND chain defaults
|
|
286
290
|
const featureAddrs = [
|
|
287
291
|
{ key: 'SIMPLE_BOUNTY_SYSTEM_ADDRESS', feature: 'bounty', hint: 'Required for `sage bounty *` commands.' },
|
|
288
292
|
{ key: 'BOOST_MANAGER_ADDRESS', feature: 'boost (merkle)', hint: 'Required for `sage boost fund/status/claim` (Merkle manager).' },
|
|
289
293
|
{ key: 'BOOST_MANAGER_DIRECT_ADDRESS', feature: 'boost (direct)', hint: 'Required for `sage boost create/status/finalize` (direct manager).' }
|
|
290
294
|
];
|
|
291
295
|
for (const entry of featureAddrs) {
|
|
292
|
-
const v =
|
|
296
|
+
const v = config.resolveAddress(entry.key);
|
|
297
|
+
const isDefault = v && !process.env[entry.key];
|
|
293
298
|
if (v) {
|
|
294
|
-
|
|
299
|
+
const suffix = isDefault ? ' (default)' : '';
|
|
300
|
+
out.push(`✅ ${entry.key}: ${v}${suffix}`);
|
|
295
301
|
} else {
|
|
296
302
|
out.push(`ℹ️ ${entry.key} (missing; ${entry.feature} commands will be unavailable)`);
|
|
297
303
|
diagnostics.push({
|
|
@@ -861,24 +867,25 @@ function register(program) {
|
|
|
861
867
|
|
|
862
868
|
if (opts.json) {
|
|
863
869
|
// Build JSON object with key facts
|
|
864
|
-
|
|
865
|
-
|
|
870
|
+
// Use config.resolve* to include chain defaults, not just env vars
|
|
871
|
+
const rpcUrl = config.resolveRpcUrl();
|
|
872
|
+
const chainId = config.resolveChainId();
|
|
866
873
|
const sageHome = _resolveSageHome();
|
|
867
874
|
const out = {
|
|
868
875
|
rpcUrl,
|
|
869
876
|
chainId,
|
|
870
877
|
addresses: {
|
|
871
|
-
SXXX_TOKEN_ADDRESS:
|
|
872
|
-
LIBRARY_REGISTRY_ADDRESS:
|
|
873
|
-
SUBDAO_FACTORY_ADDRESS:
|
|
874
|
-
SIMPLE_BOUNTY_SYSTEM_ADDRESS:
|
|
875
|
-
BOOST_MANAGER_ADDRESS:
|
|
876
|
-
BOOST_MANAGER_DIRECT_ADDRESS:
|
|
877
|
-
PERSONAL_MARKETPLACE_ADDRESS:
|
|
878
|
-
PERSONAL_LICENSE_RECEIPT_ADDRESS:
|
|
879
|
-
SUBDAO:
|
|
880
|
-
GOV:
|
|
881
|
-
TIMELOCK:
|
|
878
|
+
SXXX_TOKEN_ADDRESS: config.resolveAddress('SXXX_TOKEN_ADDRESS'),
|
|
879
|
+
LIBRARY_REGISTRY_ADDRESS: config.resolveAddress('LIBRARY_REGISTRY_ADDRESS'),
|
|
880
|
+
SUBDAO_FACTORY_ADDRESS: config.resolveAddress('SUBDAO_FACTORY_ADDRESS'),
|
|
881
|
+
SIMPLE_BOUNTY_SYSTEM_ADDRESS: config.resolveAddress('SIMPLE_BOUNTY_SYSTEM_ADDRESS'),
|
|
882
|
+
BOOST_MANAGER_ADDRESS: config.resolveAddress('BOOST_MANAGER_ADDRESS'),
|
|
883
|
+
BOOST_MANAGER_DIRECT_ADDRESS: config.resolveAddress('BOOST_MANAGER_DIRECT_ADDRESS'),
|
|
884
|
+
PERSONAL_MARKETPLACE_ADDRESS: config.resolveAddress('PERSONAL_MARKETPLACE_ADDRESS'),
|
|
885
|
+
PERSONAL_LICENSE_RECEIPT_ADDRESS: config.resolveAddress('PERSONAL_LICENSE_RECEIPT_ADDRESS'),
|
|
886
|
+
SUBDAO: config.resolveAddress('SUBDAO'),
|
|
887
|
+
GOV: config.resolveAddress('GOV'),
|
|
888
|
+
TIMELOCK: config.resolveAddress('TIMELOCK'),
|
|
882
889
|
SAFE_TX_SERVICE_URL: process.env.SAFE_TX_SERVICE_URL || null,
|
|
883
890
|
SAGE_USE_SAFE_TX_SERVICE: process.env.SAGE_USE_SAFE_TX_SERVICE || null
|
|
884
891
|
},
|
|
@@ -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(
|
|
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
|
|
87
|
-
|
|
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(
|
|
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
|
|
249
|
-
|
|
250
|
-
|
|
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(
|
|
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(
|
|
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
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
let
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
-
//
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
610
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|