@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.

Potentially problematic release.


This version of @sage-protocol/cli might be problematic. Click here for more details.

@@ -52,6 +52,10 @@ const AUCTION_ABI = [
52
52
  'function settleCurrentAndCreateNewAuction()'
53
53
  ];
54
54
 
55
+ const SUBDAO_ABI = [
56
+ 'function governor() view returns (address)'
57
+ ];
58
+
55
59
  const TOKEN_ABI = [
56
60
  'function name() view returns (string)',
57
61
  'function symbol() view returns (string)',
@@ -61,10 +65,38 @@ const TOKEN_ABI = [
61
65
  ];
62
66
 
63
67
  function getProvider() {
64
- const rpcUrl = process.env.RPC_URL || 'https://base-sepolia.publicnode.com';
68
+ const rpcUrl = cliConfig.resolveRpcUrl({ allowEnv: true });
65
69
  return new ethers.JsonRpcProvider(rpcUrl);
66
70
  }
67
71
 
72
+ /**
73
+ * Resolve a Governor address from a SubDAO or Governor address.
74
+ * If the address is a SubDAO, calls governor() to get the Governor.
75
+ * If the address is already a Governor, returns it directly.
76
+ */
77
+ async function resolveGovernorAddress(address, provider) {
78
+ const normalized = ethers.getAddress(address);
79
+ // Try calling governor() - if it works, the address is a SubDAO
80
+ try {
81
+ const subdao = new ethers.Contract(normalized, SUBDAO_ABI, provider);
82
+ const governorAddr = await subdao.governor();
83
+ if (governorAddr && governorAddr !== ethers.ZeroAddress) {
84
+ return { governor: governorAddr, subdao: normalized, isSubDAO: true };
85
+ }
86
+ } catch (_) {
87
+ // Not a SubDAO or doesn't have governor() - assume it's a Governor
88
+ }
89
+
90
+ // Verify governor interface before returning
91
+ try {
92
+ const gov = new ethers.Contract(normalized, GOVERNOR_ABI, provider);
93
+ await gov.token();
94
+ return { governor: normalized, subdao: null, isSubDAO: false };
95
+ } catch (err) {
96
+ throw new Error(`Address ${normalized} is not a SubDAO or Governor. Use --gov for governor address or --subdao for SubDAO.`);
97
+ }
98
+ }
99
+
68
100
  async function resolveAccountAddress(address) {
69
101
  if (address) return address;
70
102
 
@@ -92,11 +124,21 @@ function register(program) {
92
124
  addDAOAddressOptions(statusCmd);
93
125
  statusCmd.action(async (options) => {
94
126
  try {
95
- const governorAddr = resolveDAOAddress(options, { required: true });
127
+ const inputAddr = resolveDAOAddress(options, { required: true });
96
128
  const provider = getProvider();
97
- const governor = new ethers.Contract(governorAddr, GOVERNOR_ABI, provider);
98
129
 
99
- ui.info(`Checking multiplier status for governor: ${governorAddr}`);
130
+ // Auto-resolve Governor from SubDAO if needed
131
+ const resolved = await resolveGovernorAddress(inputAddr, provider);
132
+ const governorAddr = resolved.governor;
133
+
134
+ if (resolved.isSubDAO) {
135
+ ui.info(`SubDAO detected: ${inputAddr}`);
136
+ ui.info(`Resolved Governor: ${governorAddr}`);
137
+ } else {
138
+ ui.info(`Checking multiplier status for governor: ${governorAddr}`);
139
+ }
140
+
141
+ const governor = new ethers.Contract(governorAddr, GOVERNOR_ABI, provider);
100
142
 
101
143
  // Check if multipliers are enabled
102
144
  const isEnabled = await governor.isMultiplierEnabled().catch(() => false);
@@ -158,30 +200,44 @@ function register(program) {
158
200
  // Build calculate command
159
201
  const calculateCmd = new Command('calculate')
160
202
  .description('Show voting power breakdown for an account')
161
- .argument('[address]', 'Account address (defaults to connected wallet)');
203
+ .argument('[address]', 'Account address (defaults to connected wallet)')
204
+ .option('--json', 'Output JSON', false);
162
205
  addDAOAddressOptions(calculateCmd);
163
206
  calculateCmd.action(async (address, options) => {
164
207
  try {
165
- const governorAddr = resolveDAOAddress(options, { required: true });
208
+ const inputAddr = resolveDAOAddress(options, { required: true });
166
209
  const account = await resolveAccountAddress(address);
167
210
  if (!account) {
168
211
  throw new Error('No address provided. Pass an address or connect a wallet.');
169
212
  }
170
213
 
171
214
  const provider = getProvider();
215
+
216
+ // Auto-resolve Governor from SubDAO if needed
217
+ const resolved = await resolveGovernorAddress(inputAddr, provider);
218
+ const governorAddr = resolved.governor;
219
+
172
220
  const governor = new ethers.Contract(governorAddr, GOVERNOR_ABI, provider);
173
221
 
174
- ui.output('\nVoting Power Breakdown');
175
- ui.keyValue({
176
- 'Account': account,
177
- 'Governor': governorAddr
178
- });
222
+ if (!options.json) {
223
+ ui.output('\nVoting Power Breakdown');
224
+ const displayInfo = { 'Account': account };
225
+ if (resolved.isSubDAO) {
226
+ displayInfo['SubDAO'] = resolved.subdao;
227
+ displayInfo['Governor'] = governorAddr;
228
+ } else {
229
+ displayInfo['Governor'] = governorAddr;
230
+ }
231
+ ui.keyValue(displayInfo);
232
+ }
179
233
 
180
234
  // Check if multipliers are enabled
181
235
  const isEnabled = await governor.isMultiplierEnabled().catch(() => false);
182
236
  const votingToken = await governor.token();
237
+ const baseVotesToken = await governor.baseVotesToken().catch(() => null);
238
+ const isWrapperMode = isEnabled || (baseVotesToken && baseVotesToken.toLowerCase() !== votingToken.toLowerCase());
183
239
 
184
- if (!isEnabled) {
240
+ if (!isWrapperMode) {
185
241
  // Simple ERC20Votes - no multiplier
186
242
  const token = new ethers.Contract(votingToken, TOKEN_ABI, provider);
187
243
  const [name, symbol, decimals, balance, votes] = await Promise.all([
@@ -192,6 +248,22 @@ function register(program) {
192
248
  token.getVotes(account)
193
249
  ]);
194
250
 
251
+ if (options.json) {
252
+ ui.json({
253
+ ok: true,
254
+ account,
255
+ governor: governorAddr,
256
+ token: votingToken,
257
+ name,
258
+ symbol,
259
+ decimals,
260
+ balance: balance.toString(),
261
+ votingPower: votes.toString(),
262
+ multiplier: '1x'
263
+ });
264
+ return;
265
+ }
266
+
195
267
  ui.output(`\n${name} (${symbol})`);
196
268
  ui.keyValue({
197
269
  'Balance': `${ethers.formatUnits(balance, decimals)}`,
@@ -203,7 +275,7 @@ function register(program) {
203
275
 
204
276
  // Multiplied votes - get breakdown
205
277
  const multipliedVotes = new ethers.Contract(votingToken, MULTIPLIED_VOTES_ABI, provider);
206
- const baseTokenAddr = await multipliedVotes.baseToken();
278
+ const baseTokenAddr = baseVotesToken || await multipliedVotes.baseToken();
207
279
  const nftAddr = await multipliedVotes.multiplierNFT();
208
280
  const basis = await multipliedVotes.BASIS();
209
281
 
@@ -221,6 +293,22 @@ function register(program) {
221
293
  const baseDisplay = ethers.formatUnits(baseVotes, decimals);
222
294
  const effectiveDisplay = ethers.formatUnits(effectiveVotes, decimals);
223
295
 
296
+ if (options.json) {
297
+ ui.json({
298
+ ok: true,
299
+ account,
300
+ governor: governorAddr,
301
+ baseToken: baseTokenAddr,
302
+ votingToken,
303
+ baseVotes: baseVotes.toString(),
304
+ multiplier: multiplier.toString(),
305
+ multiplierDisplay: multDisplay,
306
+ effectiveVotes: effectiveVotes.toString(),
307
+ hasBonus: !!hasBonus
308
+ });
309
+ return;
310
+ }
311
+
224
312
  ui.output(`\n${name} (${symbol}) with NFT Multipliers`);
225
313
  ui.keyValue({
226
314
  'Base Votes': `${baseDisplay} ${symbol}`,
@@ -176,9 +176,10 @@ Automation:
176
176
  .option('--dao <address>', 'DAO address (auto-detected from context if not provided)')
177
177
  .option('--account <address>', 'Account to check (default: your wallet)')
178
178
  .option('-v, --verbose', 'Show detailed output')
179
+ .option('--json', 'Output JSON', false)
179
180
  .action(async (opts) => {
180
181
  try {
181
- ui.configure({ verbose: opts.verbose });
182
+ ui.configure({ verbose: opts.verbose, json: opts.json });
182
183
  const rpcUrl = cliConfig.resolveRpcUrl();
183
184
  const provider = new ethers.JsonRpcProvider(rpcUrl);
184
185
  const nftAddr = cliConfig.resolveAddress('VOTING_MULTIPLIER_NFT_ADDRESS');
@@ -208,6 +209,18 @@ Automation:
208
209
  const multiplier = await contract.getMultiplier(account, daoAddr);
209
210
  const multiplierDisplay = (Number(multiplier) / 10000).toFixed(4);
210
211
 
212
+ if (opts.json) {
213
+ ui.json({
214
+ ok: true,
215
+ account,
216
+ dao: daoAddr,
217
+ nft: nftAddr,
218
+ multiplier: multiplier.toString(),
219
+ multiplierDisplay: `${multiplierDisplay}x`
220
+ });
221
+ return;
222
+ }
223
+
211
224
  ui.header(`Voting Multiplier for ${ui.formatAddress(account)}`);
212
225
  ui.keyValue([
213
226
  ['DAO', ui.formatAddress(daoAddr)],
@@ -269,10 +282,10 @@ Automation:
269
282
  throw new Error('Multiplier must be >= 100 (100 = 1x)');
270
283
  }
271
284
 
272
- // Build calldata for createTier
285
+ // Build calldata for governance-safe createTierViaGovernance
273
286
  const abi = resolveArtifact('contracts/VotingMultiplierNFT.sol/VotingMultiplierNFT.json').abi;
274
287
  const iface = new ethers.Interface(abi);
275
- const calldata = iface.encodeFunctionData('createTier', [
288
+ const calldata = iface.encodeFunctionData('createTierViaGovernance', [
276
289
  daoAddr,
277
290
  String(opts.name),
278
291
  BigInt(multiplier),
@@ -105,7 +105,7 @@ async function fetchPlatformPrompts(opts = {}) {
105
105
 
106
106
  if (opts.search) {
107
107
  params.set('q', opts.search);
108
- const { body } = await client._fetchJson(`/discover/search?${params}`);
108
+ const { body } = await client._fetchJson(`/prompts/search?${params}`);
109
109
  return (body.results || []).map(r => ({
110
110
  ...r,
111
111
  source: 'platform',
@@ -7,6 +7,57 @@ const { handleCLIError } = require('../utils/error-handler');
7
7
  const subgraphClient = require('../utils/subgraph-client');
8
8
  const cliConfig = require('../config');
9
9
 
10
+ async function getLogsChunked(provider, filter, maxRange = 50000) {
11
+ // Default to 50k blocks to stay under most RPC limits (Base Sepolia limits to 100k)
12
+ const latest = filter.toBlock === 'latest' || typeof filter.toBlock === 'undefined'
13
+ ? await provider.getBlockNumber()
14
+ : Number(filter.toBlock);
15
+ const from = Number(filter.fromBlock || 0);
16
+ const results = [];
17
+ for (let start = from; start <= latest; start += maxRange) {
18
+ const end = Math.min(latest, start + maxRange - 1);
19
+ try {
20
+ const batch = await provider.getLogs({ ...filter, fromBlock: start, toBlock: end });
21
+ if (batch && batch.length) results.push(...batch);
22
+ } catch (e) {
23
+ if (process.env.SAGE_VERBOSE === '1') {
24
+ console.warn(`Log query failed for blocks ${start}-${end}: ${e.message}`);
25
+ }
26
+ }
27
+ }
28
+ return results;
29
+ }
30
+
31
+ async function findProposalCreatedLog(provider, govAddr, gov, pid, lookbacks) {
32
+ const { ethers } = require('ethers');
33
+ const eventSig = gov.interface.getEvent('ProposalCreated').topicHash;
34
+ const currentBlock = await provider.getBlockNumber();
35
+ const pidHex = ethers.hexlify(ethers.toBeHex(pid));
36
+ for (const span of lookbacks) {
37
+ const from = currentBlock > span ? currentBlock - span : 0;
38
+ let logs = await getLogsChunked(provider, {
39
+ topics: [eventSig, ethers.zeroPadValue(pidHex, 32)],
40
+ address: govAddr,
41
+ fromBlock: from,
42
+ toBlock: 'latest'
43
+ });
44
+ if (!logs.length) {
45
+ const all = await getLogsChunked(provider, { topics: [eventSig], address: govAddr, fromBlock: from, toBlock: 'latest' });
46
+ for (const l of all) {
47
+ try {
48
+ const p = gov.interface.parseLog(l);
49
+ if (p && p.name === 'ProposalCreated' && BigInt(p.args.proposalId.toString()) === pid) {
50
+ logs = [l];
51
+ break;
52
+ }
53
+ } catch { }
54
+ }
55
+ }
56
+ if (logs.length) return logs[0];
57
+ }
58
+ return null;
59
+ }
60
+
10
61
  function register(program) {
11
62
  const cmd = new Command('proposals')
12
63
  .description('Proposals navigation: inbox, preview, vote, execute');
@@ -219,10 +270,92 @@ function register(program) {
219
270
  if (summary.description) ui.output(` Description: ${summary.description.slice(0, 100)}...`);
220
271
  }
221
272
  } catch (e) {
222
- // Final fallback: minimal info
223
273
  if (process.env.SAGE_VERBOSE === '1') {
224
274
  ui.warn(`SDK getProposalDetails failed: ${e.message}`);
225
275
  }
276
+ // Fallback 1: try subgraph directly (no block range limits)
277
+ let foundViaSubgraph = false;
278
+ try {
279
+ const status = await subgraphClient.checkSubgraphStatus(provider);
280
+ if (status.available && ctx.governor) {
281
+ const proposals = await subgraphClient.getProposals(ctx.governor, { first: 200 });
282
+ const pidDec = pid.toString();
283
+ const match = proposals.find((p) => {
284
+ const parts = String(p.id || '').split('-');
285
+ const last = parts.length > 1 ? parts[parts.length - 1] : String(p.id || '');
286
+ if (!last) return false;
287
+ if (last === pidDec) return true;
288
+ try { if (String(last).startsWith('0x') && BigInt(String(last)) === pid) return true; } catch (_) { }
289
+ return false;
290
+ });
291
+ if (match && match.targets?.length) {
292
+ const targets = Array.from(match.targets || [], (a) => String(a));
293
+ const values = Array.from(match.values || [], (v) => BigInt(String(v || 0)));
294
+ const calldatas = Array.from(match.calldatas || [], (b) => String(b));
295
+ const description = String(match.description || '');
296
+ const selectors = calldatas.map((d) => (typeof d === 'string' ? d.slice(0, 10) : '0x'));
297
+ const summary = {
298
+ id: pid.toString(),
299
+ source: 'subgraph',
300
+ actionCount: targets.length,
301
+ targets,
302
+ values: values.map((v) => v.toString()),
303
+ selectors,
304
+ description
305
+ };
306
+ if (opts.json) ui.json(withJsonVersion(summary));
307
+ else {
308
+ ui.output(`Proposal ${summary.id} (subgraph)`);
309
+ ui.output(` Actions: ${summary.actionCount}`);
310
+ if (summary.targets.length) ui.output(` First target: ${summary.targets[0]}`);
311
+ if (summary.selectors.length) ui.output(` First selector: ${summary.selectors[0]}`);
312
+ if (summary.description) ui.output(` Description: ${summary.description.slice(0, 100)}...`);
313
+ }
314
+ foundViaSubgraph = true;
315
+ }
316
+ }
317
+ } catch (sgErr) {
318
+ if (process.env.SAGE_VERBOSE === '1') {
319
+ ui.warn(`Subgraph fallback failed: ${sgErr.message}`);
320
+ }
321
+ }
322
+ if (foundViaSubgraph) return;
323
+
324
+ // Fallback 2: scan ProposalCreated logs with chunked queries
325
+ try {
326
+ if (ctx.governor) {
327
+ const GovABI = require('../utils/artifacts').resolveArtifact('contracts/cloneable/PromptGovernorCloneable.sol/PromptGovernorCloneable.json').abi;
328
+ const gov = new ethers.Contract(ctx.governor, GovABI, provider);
329
+ const hit = await findProposalCreatedLog(provider, ctx.governor, gov, pid, [20000, 100000, 500000]);
330
+ if (hit) {
331
+ const parsed = gov.interface.parseLog(hit);
332
+ const targets = Array.from(parsed.args.targets || [], (a) => String(a));
333
+ const values = Array.from(parsed.args.values || [], (v) => BigInt(v.toString()));
334
+ const calldatas = Array.from(parsed.args.calldatas || [], (b) => ethers.hexlify(b));
335
+ const description = String(parsed.args.description ?? '');
336
+ const selectors = calldatas.map((d) => (typeof d === 'string' ? d.slice(0, 10) : '0x'));
337
+ const summary = {
338
+ id: pid.toString(),
339
+ source: 'logs',
340
+ actionCount: targets.length,
341
+ targets,
342
+ values: values.map((v) => v.toString()),
343
+ selectors,
344
+ description
345
+ };
346
+ if (opts.json) ui.json(withJsonVersion(summary));
347
+ else {
348
+ ui.output(`Proposal ${summary.id} (logs)`);
349
+ ui.output(` Actions: ${summary.actionCount}`);
350
+ if (summary.targets.length) ui.output(` First target: ${summary.targets[0]}`);
351
+ if (summary.selectors.length) ui.output(` First selector: ${summary.selectors[0]}`);
352
+ if (summary.description) ui.output(` Description: ${summary.description.slice(0, 100)}...`);
353
+ }
354
+ return;
355
+ }
356
+ }
357
+ } catch (_) { }
358
+ // Final fallback: minimal info
226
359
  const summary = { id: pid.toString(), description: '(unable to fetch proposal details)' };
227
360
  if (opts.json) ui.json(withJsonVersion(summary));
228
361
  else ui.output(`Proposal ${summary.id} - ${summary.description}`);
@@ -795,10 +928,27 @@ function register(program) {
795
928
  return;
796
929
  }
797
930
 
798
- // Delegate to existing library-manager execute flow if it is a library update; otherwise, try queue/execute via gm
931
+ // Queue only when needed; skip queue if already queued
932
+ let stateNum = null;
933
+ let stateName = 'unknown';
799
934
  try {
800
- await gm.queueProposal(id);
935
+ const state = await gm.governor.state(gm.normalizeProposalId(id));
936
+ stateNum = Number(state);
937
+ stateName = gm.getStateName ? gm.getStateName(stateNum) : String(stateNum);
801
938
  } catch (_) {}
939
+
940
+ if (stateNum === 4) {
941
+ try {
942
+ await gm.queueProposal(id);
943
+ } catch (_) {}
944
+ } else if (stateNum === 5) {
945
+ if (!process.env.SAGE_QUIET_JSON) {
946
+ console.log(`ℹ️ Proposal already queued (state ${stateName}). Skipping queue step.`);
947
+ }
948
+ } else if (stateNum !== null && !process.env.SAGE_QUIET_JSON) {
949
+ console.log(`⚠️ Proposal is in state ${stateName}; queue may fail.`);
950
+ }
951
+
802
952
  await gm.executeProposal(id);
803
953
  ui.success('Execute complete');
804
954
  } catch (e) {