@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
|
@@ -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
|
|
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
'Account': account
|
|
177
|
-
|
|
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 (!
|
|
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}`,
|
package/dist/cli/commands/nft.js
CHANGED
|
@@ -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
|
|
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('
|
|
288
|
+
const calldata = iface.encodeFunctionData('createTierViaGovernance', [
|
|
276
289
|
daoAddr,
|
|
277
290
|
String(opts.name),
|
|
278
291
|
BigInt(multiplier),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const axios = require('axios');
|
|
4
|
-
const { getAddress, id, parseUnits, formatUnits, Contract, MaxUint256 } = require('ethers');
|
|
4
|
+
const { getAddress, id, parseUnits, formatUnits, Contract, MaxUint256, ZeroAddress } = require('ethers');
|
|
5
5
|
const { decryptAesGcm, fromB64 } = require('../utils/aes');
|
|
6
6
|
const sdk = require('@sage-protocol/sdk');
|
|
7
7
|
const { formatJson } = require('../utils/format');
|
|
@@ -244,6 +244,42 @@ const PERSONAL_REGISTRY_QUERY = `
|
|
|
244
244
|
async function resolveOrCreatePersonalRegistry(wallet, opts = {}) {
|
|
245
245
|
const ownerAddress = getAddress(await wallet.getAddress());
|
|
246
246
|
const subgraphUrl = resolveSubgraphUrl(opts.subgraph);
|
|
247
|
+
const provider = wallet.provider || await getProvider();
|
|
248
|
+
|
|
249
|
+
const { resolveArtifact } = require('../utils/artifacts');
|
|
250
|
+
const PromptRegistryABI = resolveArtifact('contracts/PromptRegistry.sol/PromptRegistry.json').abi;
|
|
251
|
+
|
|
252
|
+
async function validateRegistryCandidate(registryAddr) {
|
|
253
|
+
try {
|
|
254
|
+
const code = await provider.getCode(registryAddr);
|
|
255
|
+
if (!code || code === '0x') {
|
|
256
|
+
return { ok: false, reason: 'no_code' };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const read = new Contract(registryAddr, PromptRegistryABI, provider);
|
|
260
|
+
|
|
261
|
+
// Ensure this registry is actually usable by the current wallet.
|
|
262
|
+
const govRole = await read.GOVERNANCE_ROLE().catch(() => null);
|
|
263
|
+
if (!govRole) return { ok: false, reason: 'missing_governance_role' };
|
|
264
|
+
|
|
265
|
+
const hasGovRole = await read.hasRole(govRole, ownerAddress).catch(() => null);
|
|
266
|
+
if (!hasGovRole) return { ok: false, reason: 'missing_governance_role_for_owner' };
|
|
267
|
+
|
|
268
|
+
// Personal registries should not be linked to a SubDAO.
|
|
269
|
+
const subdao = await read.subDAO().catch(() => null);
|
|
270
|
+
if (subdao && getAddress(subdao) !== getAddress(ZeroAddress)) {
|
|
271
|
+
return { ok: false, reason: 'linked_to_subdao', subdao: getAddress(subdao) };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// If paused, addPrompt will revert.
|
|
275
|
+
const paused = await read.paused().catch(() => false);
|
|
276
|
+
if (paused) return { ok: false, reason: 'paused' };
|
|
277
|
+
|
|
278
|
+
return { ok: true };
|
|
279
|
+
} catch (e) {
|
|
280
|
+
return { ok: false, reason: 'validation_error', error: e };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
247
283
|
|
|
248
284
|
// Try subgraph lookup first
|
|
249
285
|
if (subgraphUrl) {
|
|
@@ -254,7 +290,14 @@ async function resolveOrCreatePersonalRegistry(wallet, opts = {}) {
|
|
|
254
290
|
});
|
|
255
291
|
const registries = resp.data?.data?.personalRegistries || [];
|
|
256
292
|
if (registries.length > 0) {
|
|
257
|
-
|
|
293
|
+
const candidate = getAddress(registries[0].id);
|
|
294
|
+
const validation = await validateRegistryCandidate(candidate);
|
|
295
|
+
if (validation.ok) return candidate;
|
|
296
|
+
|
|
297
|
+
if (!opts.json) {
|
|
298
|
+
const extra = validation.subdao ? ` (subDAO=${validation.subdao})` : '';
|
|
299
|
+
ui.warn(`Resolved personal registry ${candidate} is not usable (${validation.reason}${extra}). Creating a fresh registry...`);
|
|
300
|
+
}
|
|
258
301
|
}
|
|
259
302
|
} catch (_) {
|
|
260
303
|
// Subgraph unavailable, fall through to create
|
|
@@ -309,6 +352,30 @@ async function addPromptToRegistry(wallet, registryAddr, promptData, opts = {})
|
|
|
309
352
|
const PromptRegistryABI = resolveArtifact('contracts/PromptRegistry.sol/PromptRegistry.json').abi;
|
|
310
353
|
const registry = new Contract(registryAddr, PromptRegistryABI, wallet);
|
|
311
354
|
|
|
355
|
+
// Proactive checks to surface actionable errors before spending gas.
|
|
356
|
+
try {
|
|
357
|
+
const ownerAddress = getAddress(await wallet.getAddress());
|
|
358
|
+
const govRole = await registry.GOVERNANCE_ROLE().catch(() => null);
|
|
359
|
+
if (!govRole) {
|
|
360
|
+
throw new Error(`Resolved registry ${registryAddr} does not expose GOVERNANCE_ROLE(); is this a valid PromptRegistry?`);
|
|
361
|
+
}
|
|
362
|
+
const hasGov = await registry.hasRole(govRole, ownerAddress).catch(() => null);
|
|
363
|
+
if (!hasGov) {
|
|
364
|
+
throw new Error(`Wallet ${ownerAddress} lacks GOVERNANCE_ROLE on registry ${registryAddr}. This usually means the registry is not your personal registry (or it was not initialized as personal).`);
|
|
365
|
+
}
|
|
366
|
+
const subdao = await registry.subDAO().catch(() => null);
|
|
367
|
+
if (subdao && getAddress(subdao) !== getAddress(ZeroAddress)) {
|
|
368
|
+
throw new Error(`Registry ${registryAddr} is linked to SubDAO ${getAddress(subdao)}; personal publish requires a personal registry (subDAO=0x0).`);
|
|
369
|
+
}
|
|
370
|
+
const paused = await registry.paused().catch(() => false);
|
|
371
|
+
if (paused) {
|
|
372
|
+
throw new Error(`Registry ${registryAddr} is paused; unpause it (DEFAULT_ADMIN_ROLE) or create a fresh personal registry.`);
|
|
373
|
+
}
|
|
374
|
+
} catch (e) {
|
|
375
|
+
// Re-throw with a cleaner message for JSON/non-JSON callers.
|
|
376
|
+
throw e;
|
|
377
|
+
}
|
|
378
|
+
|
|
312
379
|
const title = promptData.title || promptData.name || promptData.key || 'Untitled';
|
|
313
380
|
const tags = promptData.tags || [];
|
|
314
381
|
const contentCID = promptData.contentCID || '';
|
|
@@ -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(`/
|
|
108
|
+
const { body } = await client._fetchJson(`/prompts/search?${params}`);
|
|
109
109
|
return (body.results || []).map(r => ({
|
|
110
110
|
...r,
|
|
111
111
|
source: 'platform',
|