@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.
Potentially problematic release.
This version of @sage-protocol/cli might be problematic. Click here for more details.
- package/README.md +12 -11
- 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/dao.js +1 -1
- package/dist/cli/commands/discover.js +3 -3
- package/dist/cli/commands/governance.js +141 -58
- package/dist/cli/commands/install.js +178 -36
- package/dist/cli/commands/ipfs.js +12 -2
- package/dist/cli/commands/library.js +277 -268
- 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/personal.js +69 -2
- 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/commands/wallet.js +5 -10
- package/dist/cli/contracts/index.js +2 -1
- package/dist/cli/index.js +5 -0
- package/dist/cli/privy-auth-wallet-manager.js +3 -2
- package/dist/cli/services/config/chain-defaults.js +1 -1
- package/dist/cli/services/config/manager.js +3 -0
- package/dist/cli/services/config/schema.js +1 -0
- package/dist/cli/services/ipfs/onboarding.js +11 -0
- package/dist/cli/utils/aliases.js +62 -3
- package/dist/cli/utils/cli-ui.js +1 -1
- package/dist/cli/utils/provider.js +7 -3
- package/dist/cli/wallet-manager.js +7 -12
- package/dist/prompts/e2e-test-prompt.md +22 -0
- package/dist/prompts/skills/build-web3/plugin.json +11 -0
- package/package.json +1 -1
|
@@ -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
|
-
//
|
|
931
|
+
// Queue only when needed; skip queue if already queued
|
|
932
|
+
let stateNum = null;
|
|
933
|
+
let stateName = 'unknown';
|
|
799
934
|
try {
|
|
800
|
-
await gm.
|
|
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) {
|
|
@@ -11,19 +11,21 @@ function register(program) {
|
|
|
11
11
|
.option('--governor <address>', 'Governor address to check')
|
|
12
12
|
.option('--address <address>', 'Address to check (defaults to connected wallet)')
|
|
13
13
|
.option('-v, --verbose', 'Show detailed output')
|
|
14
|
+
.option('--json', 'Output JSON', false)
|
|
14
15
|
.action(async (opts) => {
|
|
15
16
|
try {
|
|
16
|
-
ui.configure({ verbose: opts.verbose });
|
|
17
|
+
ui.configure({ verbose: opts.verbose, json: opts.json });
|
|
18
|
+
const show = !opts.json;
|
|
17
19
|
|
|
18
20
|
const WalletManager = require('../wallet-manager');
|
|
19
21
|
const { resolveGovContext } = require('../utils/gov-context');
|
|
20
22
|
|
|
21
23
|
const invokedAsStakeStatus = process.argv.includes('stake-status');
|
|
22
|
-
if (invokedAsStakeStatus) {
|
|
24
|
+
if (invokedAsStakeStatus && show) {
|
|
23
25
|
ui.warn('`sage stake-status` is deprecated. Use `sage voting-status` (governance uses delegation, not staking).');
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
const rpcUrl = process.env.RPC_URL || process.env.BASE_SEPOLIA_RPC || 'https://base-sepolia.publicnode.com';
|
|
28
|
+
const rpcUrl = process.env.SAGE_RPC_URL || process.env.RPC_URL || process.env.BASE_SEPOLIA_RPC || 'https://base-sepolia.publicnode.com';
|
|
27
29
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
28
30
|
|
|
29
31
|
// Get wallet manager and connect
|
|
@@ -32,11 +34,13 @@ function register(program) {
|
|
|
32
34
|
const signer = walletManager.getSigner();
|
|
33
35
|
const address = opts.address || await signer.getAddress();
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
[
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
if (show) {
|
|
38
|
+
ui.header('Voting Power Status');
|
|
39
|
+
ui.keyValue([
|
|
40
|
+
['Address', address],
|
|
41
|
+
['RPC', rpcUrl]
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
40
44
|
|
|
41
45
|
// Resolve governance context
|
|
42
46
|
const ctx = await resolveGovContext({
|
|
@@ -48,14 +52,17 @@ function register(program) {
|
|
|
48
52
|
});
|
|
49
53
|
|
|
50
54
|
if (!ctx.governor) {
|
|
51
|
-
ui.error('No Governor found');
|
|
55
|
+
if (show) ui.error('No Governor found');
|
|
56
|
+
else ui.json({ ok: false, error: 'No Governor found' });
|
|
52
57
|
return;
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
[
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
if (show) {
|
|
61
|
+
ui.keyValue([
|
|
62
|
+
['Governor', ctx.governor],
|
|
63
|
+
['SubDAO', ctx.subdao || 'N/A']
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
59
66
|
|
|
60
67
|
// Get Governor's voting token and multiplier status
|
|
61
68
|
const govABI = [
|
|
@@ -78,19 +85,21 @@ function register(program) {
|
|
|
78
85
|
const baseVotesTokenAddr = await governor.baseVotesToken().catch(() => votingToken);
|
|
79
86
|
const isWrapperMode = isMultiplierEnabled || (baseVotesTokenAddr && baseVotesTokenAddr !== votingToken);
|
|
80
87
|
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
[
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
[
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
if (show) {
|
|
89
|
+
if (isWrapperMode) {
|
|
90
|
+
ui.keyValue([
|
|
91
|
+
['Voting Token (wrapper)', votingToken],
|
|
92
|
+
['Base Token (SXXX)', baseVotesTokenAddr],
|
|
93
|
+
['NFT Multipliers', isMultiplierEnabled ? 'Enabled' : 'Unknown']
|
|
94
|
+
]);
|
|
95
|
+
} else {
|
|
96
|
+
ui.keyValue([
|
|
97
|
+
['Voting Token', votingToken],
|
|
98
|
+
['NFT Multipliers', 'Disabled']
|
|
99
|
+
]);
|
|
100
|
+
}
|
|
101
|
+
ui.info(`Proposal Threshold: ${ethers.formatEther(threshold)} tokens`);
|
|
92
102
|
}
|
|
93
|
-
ui.info(`Proposal Threshold: ${ethers.formatEther(threshold)} tokens`);
|
|
94
103
|
|
|
95
104
|
// In multiplier mode, governor.token() is an IVotes wrapper (MultipliedVotes) which is NOT an ERC20,
|
|
96
105
|
// so ERC20 calls (name/symbol/decimals/balanceOf) can revert. Always read ERC20 metadata from the base token.
|
|
@@ -117,35 +126,39 @@ function register(program) {
|
|
|
117
126
|
provider.getBlockNumber()
|
|
118
127
|
]);
|
|
119
128
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
[
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
+
if (show) {
|
|
130
|
+
ui.newline();
|
|
131
|
+
ui.header('Token Details');
|
|
132
|
+
ui.keyValue([
|
|
133
|
+
['Name', name],
|
|
134
|
+
['Symbol', symbol],
|
|
135
|
+
['Decimals', decimals],
|
|
136
|
+
['Balance', `${ethers.formatUnits(balance, decimals)} ${symbol}`],
|
|
137
|
+
['Delegates to', delegates]
|
|
138
|
+
]);
|
|
139
|
+
}
|
|
129
140
|
|
|
130
141
|
// Check voting power at different blocks
|
|
131
142
|
const blocksToCheck = [currentBlock - 1, currentBlock - 2, currentBlock - 5];
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
if (show) {
|
|
144
|
+
ui.newline();
|
|
145
|
+
ui.header('Voting Power History');
|
|
146
|
+
}
|
|
134
147
|
|
|
135
148
|
for (const block of blocksToCheck) {
|
|
136
149
|
try {
|
|
137
150
|
const votes = await votesToken.getPastVotes(address, block);
|
|
138
151
|
const formattedVotes = ethers.formatUnits(votes, decimals);
|
|
139
152
|
const status = votes >= threshold ? ui.symbols.check : ui.symbols.cross;
|
|
140
|
-
ui.output(` Block ${block}: ${formattedVotes} ${symbol} ${status}`);
|
|
153
|
+
if (show) ui.output(` Block ${block}: ${formattedVotes} ${symbol} ${status}`);
|
|
141
154
|
} catch (e) {
|
|
142
|
-
ui.output(` Block ${block}: Error - ${e.message}`);
|
|
155
|
+
if (show) ui.output(` Block ${block}: Error - ${e.message}`);
|
|
143
156
|
}
|
|
144
157
|
}
|
|
145
158
|
|
|
146
159
|
// Note: stakeToken() removed from SubDAO contracts (threshold-based governance)
|
|
147
160
|
// Voting power now comes from SXXX token delegation, not staking
|
|
148
|
-
if (ctx.subdao) {
|
|
161
|
+
if (ctx.subdao && show) {
|
|
149
162
|
ui.newline();
|
|
150
163
|
ui.header('Governance Model');
|
|
151
164
|
ui.output(' This SubDAO uses threshold-based governance.');
|
|
@@ -154,8 +167,10 @@ function register(program) {
|
|
|
154
167
|
|
|
155
168
|
// Summary
|
|
156
169
|
const currentVotes = await votesToken.getPastVotes(address, currentBlock - 1).catch(() => 0n);
|
|
157
|
-
|
|
158
|
-
|
|
170
|
+
if (show) {
|
|
171
|
+
ui.newline();
|
|
172
|
+
ui.header('Summary');
|
|
173
|
+
}
|
|
159
174
|
|
|
160
175
|
// If multipliers are enabled, show both base and effective voting power
|
|
161
176
|
if (isWrapperMode) {
|
|
@@ -163,32 +178,91 @@ function register(program) {
|
|
|
163
178
|
const [baseVotes, multiplier, effectiveVotes] = await governor.getVotingBreakdown(address);
|
|
164
179
|
const multiplierDisplay = (Number(multiplier) / 10000).toFixed(2);
|
|
165
180
|
const canPropose = effectiveVotes >= threshold;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
181
|
+
if (show) {
|
|
182
|
+
ui.keyValue([
|
|
183
|
+
['Base Voting Power', `${ethers.formatUnits(baseVotes, decimals)} ${symbol}`],
|
|
184
|
+
['NFT Multiplier', `${multiplierDisplay}x`],
|
|
185
|
+
['Effective Voting Power', `${ethers.formatUnits(effectiveVotes, decimals)} votes`],
|
|
186
|
+
['Proposal Threshold', `${ethers.formatEther(threshold)} tokens`],
|
|
187
|
+
['Can Propose', canPropose ? 'YES' : 'NO']
|
|
188
|
+
]);
|
|
189
|
+
} else {
|
|
190
|
+
ui.json({
|
|
191
|
+
ok: true,
|
|
192
|
+
address,
|
|
193
|
+
rpcUrl,
|
|
194
|
+
governor: ctx.governor,
|
|
195
|
+
subdao: ctx.subdao || null,
|
|
196
|
+
votingToken,
|
|
197
|
+
baseToken: baseVotesTokenAddr,
|
|
198
|
+
isMultiplierEnabled: !!isMultiplierEnabled,
|
|
199
|
+
threshold: threshold.toString(),
|
|
200
|
+
balance: balance.toString(),
|
|
201
|
+
delegates,
|
|
202
|
+
baseVotes: baseVotes.toString(),
|
|
203
|
+
multiplier: multiplier.toString(),
|
|
204
|
+
effectiveVotes: effectiveVotes.toString(),
|
|
205
|
+
canPropose
|
|
206
|
+
});
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
173
209
|
} catch (e) {
|
|
174
210
|
// Fallback if getVotingBreakdown fails
|
|
175
211
|
const canPropose = currentVotes >= threshold;
|
|
212
|
+
if (show) {
|
|
213
|
+
ui.keyValue([
|
|
214
|
+
['Current Voting Power', `${ethers.formatUnits(currentVotes, decimals)} ${symbol}`],
|
|
215
|
+
['Proposal Threshold', `${ethers.formatEther(threshold)} tokens`],
|
|
216
|
+
['Can Propose', canPropose ? 'YES' : 'NO']
|
|
217
|
+
]);
|
|
218
|
+
} else {
|
|
219
|
+
ui.json({
|
|
220
|
+
ok: true,
|
|
221
|
+
address,
|
|
222
|
+
rpcUrl,
|
|
223
|
+
governor: ctx.governor,
|
|
224
|
+
subdao: ctx.subdao || null,
|
|
225
|
+
votingToken,
|
|
226
|
+
baseToken: baseVotesTokenAddr,
|
|
227
|
+
isMultiplierEnabled: !!isMultiplierEnabled,
|
|
228
|
+
threshold: threshold.toString(),
|
|
229
|
+
balance: balance.toString(),
|
|
230
|
+
delegates,
|
|
231
|
+
currentVotes: currentVotes.toString(),
|
|
232
|
+
canPropose
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
const canPropose = currentVotes >= threshold;
|
|
239
|
+
if (show) {
|
|
176
240
|
ui.keyValue([
|
|
177
241
|
['Current Voting Power', `${ethers.formatUnits(currentVotes, decimals)} ${symbol}`],
|
|
178
242
|
['Proposal Threshold', `${ethers.formatEther(threshold)} tokens`],
|
|
179
243
|
['Can Propose', canPropose ? 'YES' : 'NO']
|
|
180
244
|
]);
|
|
245
|
+
} else {
|
|
246
|
+
ui.json({
|
|
247
|
+
ok: true,
|
|
248
|
+
address,
|
|
249
|
+
rpcUrl,
|
|
250
|
+
governor: ctx.governor,
|
|
251
|
+
subdao: ctx.subdao || null,
|
|
252
|
+
votingToken,
|
|
253
|
+
baseToken: baseVotesTokenAddr,
|
|
254
|
+
isMultiplierEnabled: !!isMultiplierEnabled,
|
|
255
|
+
threshold: threshold.toString(),
|
|
256
|
+
balance: balance.toString(),
|
|
257
|
+
delegates,
|
|
258
|
+
currentVotes: currentVotes.toString(),
|
|
259
|
+
canPropose
|
|
260
|
+
});
|
|
261
|
+
return;
|
|
181
262
|
}
|
|
182
|
-
} else {
|
|
183
|
-
const canPropose = currentVotes >= threshold;
|
|
184
|
-
ui.keyValue([
|
|
185
|
-
['Current Voting Power', `${ethers.formatUnits(currentVotes, decimals)} ${symbol}`],
|
|
186
|
-
['Proposal Threshold', `${ethers.formatEther(threshold)} tokens`],
|
|
187
|
-
['Can Propose', canPropose ? 'YES' : 'NO']
|
|
188
|
-
]);
|
|
189
263
|
}
|
|
190
264
|
|
|
191
|
-
if (currentVotes < threshold) {
|
|
265
|
+
if (show && currentVotes < threshold) {
|
|
192
266
|
ui.newline();
|
|
193
267
|
ui.info('To gain voting power:');
|
|
194
268
|
if (balance === 0n) {
|
|
@@ -22,10 +22,14 @@ function register(program) {
|
|
|
22
22
|
try {
|
|
23
23
|
ui.configure({ verbose: opts.verbose });
|
|
24
24
|
const { ethers } = require('ethers');
|
|
25
|
-
const rpcUrl = cliConfig.resolveRpcUrl();
|
|
25
|
+
const rpcUrl = cliConfig.resolveRpcUrl({ allowEnv: true });
|
|
26
26
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
27
27
|
const tokenAddress = cliConfig.resolveAddress('SXXX_TOKEN_ADDRESS');
|
|
28
28
|
if (!tokenAddress) throw new Error('SXXX_TOKEN_ADDRESS not set');
|
|
29
|
+
const code = await provider.getCode(tokenAddress);
|
|
30
|
+
if (!code || code === '0x' || code === '0x0') {
|
|
31
|
+
throw new Error(`SXXX contract not found at ${tokenAddress}. Check SAGE_RPC_URL/CHAIN_ID or import addresses.`);
|
|
32
|
+
}
|
|
29
33
|
let targetAddress = address;
|
|
30
34
|
// 1) Prefer configured default account from profile
|
|
31
35
|
if (!targetAddress) {
|
|
@@ -46,9 +50,38 @@ function register(program) {
|
|
|
46
50
|
} catch (_) {}
|
|
47
51
|
}
|
|
48
52
|
if (!targetAddress) throw new Error('No address provided and could not resolve wallet account');
|
|
49
|
-
const abi = ['function balanceOf(address) view returns (uint256)'];
|
|
53
|
+
const abi = ['function balanceOf(address) view returns (uint256)', 'function symbol() view returns (string)'];
|
|
50
54
|
const token = new ethers.Contract(tokenAddress, abi, provider);
|
|
51
|
-
|
|
55
|
+
|
|
56
|
+
// Verify contract exists and is an ERC20
|
|
57
|
+
try {
|
|
58
|
+
const code = await provider.getCode(tokenAddress);
|
|
59
|
+
if (code === '0x') {
|
|
60
|
+
ui.error(`SXXX contract not deployed at ${tokenAddress}`);
|
|
61
|
+
ui.info(' This may mean the address is incorrect or the network is wrong.');
|
|
62
|
+
ui.info(' Check: RPC_URL, SXXX_TOKEN_ADDRESS, or run `sage doctor` for diagnostics.');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
} catch (codeErr) {
|
|
66
|
+
// Ignore code check failure, proceed with balance check
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let bal;
|
|
70
|
+
try {
|
|
71
|
+
bal = await token.balanceOf(targetAddress);
|
|
72
|
+
} catch (balErr) {
|
|
73
|
+
const errMsg = balErr.message || String(balErr);
|
|
74
|
+
if (errMsg.includes('BAD_DATA') || errMsg.includes('could not decode result')) {
|
|
75
|
+
ui.error(`Contract at ${tokenAddress} returned invalid data for balanceOf()`);
|
|
76
|
+
ui.info(' This usually means the address is not an ERC20 token contract.');
|
|
77
|
+
ui.info(' Possible fixes:');
|
|
78
|
+
ui.info(' - Check SXXX_TOKEN_ADDRESS is correct for your network');
|
|
79
|
+
ui.info(' - Verify RPC_URL points to the correct chain (Base Sepolia)');
|
|
80
|
+
ui.info(' - Run `sage doctor` for full diagnostics');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
throw balErr;
|
|
84
|
+
}
|
|
52
85
|
ui.success(`SXXX Balance for ${ui.formatAddress(targetAddress)}: ${ui.formatToken(bal, { symbol: 'SXXX' })}`);
|
|
53
86
|
} catch (error) {
|
|
54
87
|
handleCLIError('sxxx:balance', error, { exit: true });
|
|
@@ -62,7 +95,7 @@ function register(program) {
|
|
|
62
95
|
.action(async (opts) => {
|
|
63
96
|
try {
|
|
64
97
|
ui.configure({ verbose: opts.verbose });
|
|
65
|
-
const rpcUrl = cliConfig.resolveRpcUrl();
|
|
98
|
+
const rpcUrl = cliConfig.resolveRpcUrl({ allowEnv: true });
|
|
66
99
|
const tokenAddress = cliConfig.resolveAddress('SXXX_TOKEN_ADDRESS');
|
|
67
100
|
if (!tokenAddress) throw new Error('SXXX_TOKEN_ADDRESS not set (configure via .env or .sage profile)');
|
|
68
101
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
@@ -289,9 +289,10 @@ const Web3AuthWalletManager = require('../web3auth-wallet-manager');
|
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
// Device code flow via relay works with just appId (no secret needed)
|
|
292
|
-
//
|
|
292
|
+
// Always prefer relay unless --local is explicitly set
|
|
293
293
|
if (appId) {
|
|
294
294
|
if (options?.local) {
|
|
295
|
+
process.env.SAGE_PRIVY_LOGIN_MODE = 'local';
|
|
295
296
|
// Local OAuth requires appSecret for direct Privy SDK authentication
|
|
296
297
|
if (!appSecret) {
|
|
297
298
|
ui.error('Local OAuth mode requires PRIVY_APP_SECRET.');
|
|
@@ -300,15 +301,9 @@ const Web3AuthWalletManager = require('../web3auth-wallet-manager');
|
|
|
300
301
|
}
|
|
301
302
|
ui.info('Using local OAuth authentication...');
|
|
302
303
|
} else {
|
|
303
|
-
// Web
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
process.env.SAGE_PRIVY_LOGIN_MODE = 'device';
|
|
307
|
-
ui.info('Using device code authentication via web app relay...');
|
|
308
|
-
} else {
|
|
309
|
-
process.env.SAGE_PRIVY_LOGIN_MODE = 'web';
|
|
310
|
-
ui.info('Using web-based authentication...');
|
|
311
|
-
}
|
|
304
|
+
// Web relay flow - uses web app, no localhost callback
|
|
305
|
+
process.env.SAGE_PRIVY_LOGIN_MODE = 'web';
|
|
306
|
+
ui.info('Using web-based authentication via relay...');
|
|
312
307
|
}
|
|
313
308
|
|
|
314
309
|
const walletManager = new PrivyAuthWalletManager();
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
const { ethers } = require('ethers');
|
|
9
9
|
|
|
10
10
|
// Default RPC URL
|
|
11
|
-
const DEFAULT_RPC = process.env.
|
|
11
|
+
const DEFAULT_RPC = process.env.SAGE_RPC_URL
|
|
12
|
+
|| process.env.RPC_URL
|
|
12
13
|
|| process.env.BASE_SEPOLIA_RPC
|
|
13
14
|
|| 'https://base-sepolia.publicnode.com';
|
|
14
15
|
|
package/dist/cli/index.js
CHANGED
|
@@ -19,6 +19,11 @@ if (!process.env.CONTRACT_ARTIFACTS_ROOT) {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// Always trust SAGE_RPC_URL over RPC_URL when provided
|
|
23
|
+
if (process.env.SAGE_RPC_URL) {
|
|
24
|
+
process.env.RPC_URL = process.env.SAGE_RPC_URL;
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
// Filter out deprecated Lit SDK warning immediately
|
|
23
28
|
const realStderrWrite = process.stderr.write.bind(process.stderr);
|
|
24
29
|
process.stderr.write = (chunk, encoding, callback) => {
|
|
@@ -82,7 +82,9 @@ class PrivyAuthWalletManager {
|
|
|
82
82
|
try {
|
|
83
83
|
const relayEnabled = process.env.SAGE_DISABLE_PRIVY_RELAY !== '1';
|
|
84
84
|
const hasAppSecret = !!this.appSecret;
|
|
85
|
-
const
|
|
85
|
+
const mode = String(process.env.SAGE_PRIVY_LOGIN_MODE || '').trim().toLowerCase();
|
|
86
|
+
const forceRelay = relayEnabled && mode !== 'local' && mode !== 'oauth';
|
|
87
|
+
const preferSessionFlow = forceRelay;
|
|
86
88
|
|
|
87
89
|
// Try cached credentials first
|
|
88
90
|
const cached = readPrivyCredentials();
|
|
@@ -140,7 +142,6 @@ class PrivyAuthWalletManager {
|
|
|
140
142
|
|
|
141
143
|
// No valid cached credentials - need OAuth login
|
|
142
144
|
console.log('\nNo cached Privy credentials found.');
|
|
143
|
-
const mode = String(process.env.SAGE_PRIVY_LOGIN_MODE || '').trim().toLowerCase();
|
|
144
145
|
const useSessionFlow = preferSessionFlow || mode === 'web' || mode === 'session';
|
|
145
146
|
const useDeviceCodeFlow = !preferSessionFlow && (mode === 'device' || mode === 'device_code');
|
|
146
147
|
|
|
@@ -58,7 +58,7 @@ const CHAIN_CONFIGS = {
|
|
|
58
58
|
* Used for resolution precedence.
|
|
59
59
|
*/
|
|
60
60
|
const ENV_VARS = {
|
|
61
|
-
rpcUrl: ['RPC_URL', 'BASE_SEPOLIA_RPC', 'BASE_RPC_URL'],
|
|
61
|
+
rpcUrl: ['SAGE_RPC_URL', 'RPC_URL', 'BASE_SEPOLIA_RPC', 'BASE_RPC_URL'],
|
|
62
62
|
chainId: ['CHAIN_ID', 'BASE_CHAIN_ID'],
|
|
63
63
|
subgraphUrl: ['SUBGRAPH_URL', 'SAGE_SUBGRAPH_URL', 'NEXT_PUBLIC_GRAPH_ENDPOINT', 'NEXT_PUBLIC_SUBGRAPH_URL']
|
|
64
64
|
};
|
|
@@ -121,6 +121,9 @@ class ConfigManager {
|
|
|
121
121
|
if (fs.existsSync(sageEnv)) {
|
|
122
122
|
dotenv.config({ path: sageEnv });
|
|
123
123
|
}
|
|
124
|
+
if (process.env.SAGE_RPC_URL) {
|
|
125
|
+
process.env.RPC_URL = process.env.SAGE_RPC_URL;
|
|
126
|
+
}
|
|
124
127
|
} catch (_) {
|
|
125
128
|
// dotenv not available, skip
|
|
126
129
|
}
|
|
@@ -105,6 +105,7 @@ const addressMap = z.record(
|
|
|
105
105
|
*/
|
|
106
106
|
const ipfsConfig = z.object({
|
|
107
107
|
gateway: z.string().url().optional(),
|
|
108
|
+
gateways: z.array(z.string()).optional(),
|
|
108
109
|
pinataApiKey: z.string().optional(),
|
|
109
110
|
pinataSecretKey: z.string().optional(),
|
|
110
111
|
pinataJwt: z.string().optional(),
|
|
@@ -2,6 +2,12 @@ const path = require('path');
|
|
|
2
2
|
const ui = require('../../utils/cli-ui');
|
|
3
3
|
|
|
4
4
|
const DEFAULT_GATEWAY = 'https://ipfs.dev.sageprotocol.io/ipfs';
|
|
5
|
+
const FALLBACK_GATEWAYS = [
|
|
6
|
+
'https://dweb.link/ipfs',
|
|
7
|
+
'https://nftstorage.link/ipfs',
|
|
8
|
+
'https://ipfs.io/ipfs',
|
|
9
|
+
'https://gateway.pinata.cloud/ipfs'
|
|
10
|
+
];
|
|
5
11
|
const DEFAULT_WORKER_BASE = 'https://api.sageprotocol.io';
|
|
6
12
|
const DEFAULT_WORKER_CHALLENGE_PATH = '/ipfs/auth/challenge';
|
|
7
13
|
const DEFAULT_WORKER_UPLOAD_PATH = '/ipfs/upload';
|
|
@@ -87,6 +93,7 @@ function createIpfsOnboarding({
|
|
|
87
93
|
|
|
88
94
|
if (!quiet) {
|
|
89
95
|
ui.header('Sage IPFS pinning setup', 'Configure your gateway and pinning providers so uploads work out of the box.');
|
|
96
|
+
ui.output(`Gateway fallbacks: ${FALLBACK_GATEWAYS.join(', ')}`);
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
let scope = initialScope && ['project', 'global'].includes(initialScope) ? initialScope : null;
|
|
@@ -332,6 +339,10 @@ function createIpfsOnboarding({
|
|
|
332
339
|
const payload = {};
|
|
333
340
|
if (gateway !== undefined) {
|
|
334
341
|
payload.gateway = ensureString(gateway) || null;
|
|
342
|
+
if (payload.gateway) {
|
|
343
|
+
const fallback = FALLBACK_GATEWAYS.filter((g) => g !== payload.gateway);
|
|
344
|
+
payload.gateways = fallback;
|
|
345
|
+
}
|
|
335
346
|
}
|
|
336
347
|
payload.warmGateway = !!warmGateway;
|
|
337
348
|
if (provider) payload.provider = provider;
|