@sage-protocol/cli 0.3.7 → 0.3.10

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.
@@ -1,675 +1,245 @@
1
1
  const { Command } = require('commander');
2
- const { ethers } = require('ethers');
3
- const { handleCLIError } = require('../utils/error-handler');
4
- let normalizeCidV1Base32;
5
- try { ({ normalizeCidV1Base32 } = require('-protocol/shared')); }
6
- catch (_) { ({ normalizeCidV1Base32 } = require('../utils/cid')); }
2
+
3
+ /**
4
+ * Premium Prompts CLI
5
+ *
6
+ * SubDAO‑based premium prompts are no longer the recommended user path.
7
+ * Personal premium is the default for creators; the remaining premium commands
8
+ * are focused on diagnostics and governance wiring for the PremiumPrompts/
9
+ * SimpleKeyStore system.
10
+ *
11
+ * Personal premium (recommended):
12
+ *
13
+ * sage personal sell <key> <price> --encrypt --file <path>
14
+ * sage personal buy <creator> <key>
15
+ * sage personal access <creator> <key>
16
+ *
17
+ * See docs/specs/premium-endorsement-model.md for the new architecture.
18
+ */
19
+
20
+ async function runPremiumDoctor(opts) {
21
+ const { ethers } = require('ethers');
22
+ const cfg = require('../config');
23
+ const { resolveGovContext } = require('../utils/gov-context');
24
+ const colors = require('../utils/chalk-adapter');
25
+
26
+ const rpcUrl = (cfg.resolveRpcUrl && cfg.resolveRpcUrl()) || process.env.RPC_URL || 'https://base-sepolia.publicnode.com';
27
+ const provider = new ethers.JsonRpcProvider(rpcUrl);
28
+
29
+ let signerAddress = null;
30
+ try {
31
+ const WM = require('@sage-protocol/wallet-manager');
32
+ const wm = new WM();
33
+ await wm.connect();
34
+ signerAddress = wm.getAccount();
35
+ } catch (_) {
36
+ // Best-effort; signerAddress may remain null for read-only checks
37
+ }
38
+
39
+ let ctx;
40
+ try {
41
+ ctx = await resolveGovContext({
42
+ govOpt: opts.gov || null,
43
+ subdaoOpt: opts.dao || opts.subdao || null,
44
+ provider
45
+ });
46
+ } catch (e) {
47
+ const msg = `Failed to resolve governance context: ${e.message}`;
48
+ if (opts.json) {
49
+ const out = {
50
+ ok: false,
51
+ error: msg,
52
+ context: { dao: opts.dao || opts.subdao || null }
53
+ };
54
+ process.stdout.write(JSON.stringify(out, null, 2) + '\n');
55
+ } else {
56
+ console.error(colors.red(`❌ ${msg}`));
57
+ console.error('Pass --dao <address> or configure context via `sage context use`.');
58
+ }
59
+ process.exit(1);
60
+ }
61
+
62
+ const subdao = ctx.subdao || process.env.WORKING_SUBDAO_ADDRESS || process.env.SUBDAO || null;
63
+ const governor = ctx.governor || process.env.GOV || process.env.GOVERNOR_ADDRESS || null;
64
+ const timelock = ctx.timelock || process.env.TIMELOCK || process.env.TIMELOCK_ADDRESS || null;
65
+
66
+ if (!timelock) {
67
+ const msg = 'Timelock not resolved. Pass --dao <address> or configure context via `sage context use`.';
68
+ if (opts.json) {
69
+ process.stdout.write(JSON.stringify({ ok: false, error: msg, context: { subdao, governor } }, null, 2) + '\n');
70
+ } else {
71
+ console.error(colors.red(`❌ ${msg}`));
72
+ }
73
+ process.exit(1);
74
+ }
75
+
76
+ const promptsAddr = process.env.PREMIUM_PROMPTS_ADDRESS || null;
77
+ const receiptAddr = process.env.PREMIUM_RECEIPT_ADDRESS || null;
78
+ const keystoreAddr = process.env.SIMPLE_KEY_STORE_ADDRESS || null;
79
+
80
+ const result = {
81
+ ok: true,
82
+ rpcUrl,
83
+ subdao,
84
+ governor,
85
+ timelock,
86
+ signer: signerAddress,
87
+ prompts: { address: promptsAddr || null },
88
+ receipt: { address: receiptAddr || null },
89
+ keystore: { address: keystoreAddr || null }
90
+ };
91
+
92
+ if (!opts.json) {
93
+ console.log(colors.cyan('💎 Premium Doctor'));
94
+ console.log(` RPC : ${rpcUrl}`);
95
+ console.log(` DAO : ${subdao || '-'}${subdao ? '' : ' (not resolved)'}`);
96
+ console.log(` Governor : ${governor || '-'}${governor ? '' : ' (not resolved)'}`);
97
+ console.log(` Timelock : ${timelock}`);
98
+ console.log(` Signer : ${signerAddress || '-'}${signerAddress ? '' : ' (no default signer detected)'}`);
99
+ console.log('');
100
+ }
101
+
102
+ // PremiumReceipt → PremiumPrompts (MINTER_ROLE)
103
+ if (receiptAddr && promptsAddr) {
104
+ try {
105
+ const rcptAbi = ['function MINTER_ROLE() view returns (bytes32)', 'function hasRole(bytes32,address) view returns (bool)'];
106
+ const receipt = new ethers.Contract(receiptAddr, rcptAbi, provider);
107
+ const MINTER = await receipt.MINTER_ROLE();
108
+ const has = await receipt.hasRole(MINTER, promptsAddr).catch(() => false);
109
+ result.receipt.minterRoleGrantedToPrompts = !!has;
110
+ if (!opts.json) {
111
+ console.log(colors.cyan('📜 PremiumReceipt → PremiumPrompts'));
112
+ console.log(` MINTER_ROLE(PremiumPrompts): ${has ? '✅' : '❌'}`);
113
+ }
114
+ } catch (e) {
115
+ result.receipt.error = e.message;
116
+ if (!opts.json) {
117
+ console.log(colors.red(`❌ Failed to inspect PremiumReceipt roles: ${e.message}`));
118
+ }
119
+ }
120
+ }
121
+
122
+ // PremiumPrompts Timelock/admin checks
123
+ if (promptsAddr) {
124
+ try {
125
+ const ppAbi = [
126
+ 'function MANAGER_ROLE() view returns (bytes32)',
127
+ 'function DEFAULT_ADMIN_ROLE() view returns (bytes32)',
128
+ 'function hasRole(bytes32,address) view returns (bool)'
129
+ ];
130
+ const pp = new ethers.Contract(promptsAddr, ppAbi, provider);
131
+ const MANAGER = await pp.MANAGER_ROLE();
132
+ const ADMIN = await pp.DEFAULT_ADMIN_ROLE();
133
+
134
+ let signerIsAdmin = null;
135
+ if (signerAddress) {
136
+ signerIsAdmin = await pp.hasRole(ADMIN, signerAddress).catch(() => null);
137
+ }
138
+ const timelockIsManager = await pp.hasRole(MANAGER, timelock).catch(() => null);
139
+ const timelockIsAdmin = await pp.hasRole(ADMIN, timelock).catch(() => null);
140
+
141
+ result.prompts.signerIsAdmin = signerIsAdmin;
142
+ result.prompts.timelockIsManager = timelockIsManager;
143
+ result.prompts.timelockIsAdmin = timelockIsAdmin;
144
+
145
+ if (!opts.json) {
146
+ console.log(colors.cyan('🏛️ PremiumPrompts Roles'));
147
+ console.log(` Address : ${promptsAddr}`);
148
+ if (signerAddress != null && signerIsAdmin != null) {
149
+ console.log(` DEFAULT_ADMIN_ROLE(signer) : ${signerIsAdmin ? '✅' : '❌'}`);
150
+ }
151
+ if (timelockIsManager != null) {
152
+ console.log(` MANAGER_ROLE(timelock) : ${timelockIsManager ? '✅' : '❌'}`);
153
+ }
154
+ if (timelockIsAdmin != null) {
155
+ console.log(` DEFAULT_ADMIN_ROLE(timelock): ${timelockIsAdmin ? '✅' : '❌'}`);
156
+ }
157
+ }
158
+ } catch (e) {
159
+ result.prompts.error = e.message;
160
+ if (!opts.json) {
161
+ console.log(colors.red(`❌ Failed to inspect PremiumPrompts roles: ${e.message}`));
162
+ }
163
+ }
164
+ }
165
+
166
+ // SimpleKeyStore Timelock/admin checks
167
+ if (keystoreAddr) {
168
+ try {
169
+ const ksAbi = [
170
+ 'function DEFAULT_ADMIN_ROLE() view returns (bytes32)',
171
+ 'function MANAGER_ROLE() view returns (bytes32)',
172
+ 'function hasRole(bytes32,address) view returns (bool)'
173
+ ];
174
+ const ks = new ethers.Contract(keystoreAddr, ksAbi, provider);
175
+ const ADMIN = await ks.DEFAULT_ADMIN_ROLE();
176
+ const MANAGER = await ks.MANAGER_ROLE();
177
+
178
+ const tlIsAdmin = await ks.hasRole(ADMIN, timelock).catch(() => null);
179
+ const tlIsManager = await ks.hasRole(MANAGER, timelock).catch(() => null);
180
+
181
+ result.keystore.timelockIsAdmin = tlIsAdmin;
182
+ result.keystore.timelockIsManager = tlIsManager;
183
+
184
+ if (!opts.json) {
185
+ console.log(colors.cyan('🔑 SimpleKeyStore Roles'));
186
+ console.log(` Address : ${keystoreAddr}`);
187
+ if (tlIsAdmin != null) {
188
+ console.log(` DEFAULT_ADMIN_ROLE(timelock) : ${tlIsAdmin ? '✅' : '❌'}`);
189
+ }
190
+ if (tlIsManager != null) {
191
+ console.log(` MANAGER_ROLE(timelock) : ${tlIsManager ? '✅' : '❌'}`);
192
+ }
193
+ }
194
+ } catch (e) {
195
+ result.keystore.error = e.message;
196
+ if (!opts.json) {
197
+ console.log(colors.red(`❌ Failed to inspect SimpleKeyStore roles: ${e.message}`));
198
+ }
199
+ }
200
+ }
201
+
202
+ if (!opts.json) {
203
+ console.log('');
204
+ console.log('Next steps:');
205
+ console.log(' - Use `sage timelock doctor --dao <dao>` to inspect broader governance wiring.');
206
+ console.log(' - Use `sage timelock fix-roles --dao <dao>` to repair Timelock roles (admin only).');
207
+ console.log(' - Use `sage governance enable-proposals --dao <dao>` to grant PROPOSER_ROLE via governance.');
208
+ } else {
209
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
210
+ }
211
+ }
7
212
 
8
213
  function register(program) {
9
214
  const premium = new Command('premium')
10
- .description('Premium prompts (ERC1155 receipts)')
11
- .addCommand(
12
- new Command('buy')
13
- .description('Approve USDC if needed and purchase access for a manifest CID')
14
- .requiredOption('--cid <manifestCID>', 'Manifest CID of the premium prompt')
15
- .option('--prompts <address>', 'PremiumPrompts address (defaults to PREMIUM_PROMPTS_ADDRESS)')
16
- .action(async (opts) => {
17
- try {
18
- const { ethers } = require('ethers');
19
- const { normalizeCidV1Base32 } = require('../utils/cid');
20
- const IPFSManager = require('../ipfs-manager');
21
- const WM = require('@sage-protocol/wallet-manager');
22
- const wm = new WM(); await wm.connect();
23
- const signer = wm.getSigner(); const provider = wm.getProvider();
24
- const me = await signer.getAddress();
25
- const prAddr = ethers.getAddress(opts.prompts || process.env.PREMIUM_PROMPTS_ADDRESS);
26
- if (!prAddr) throw new Error('PREMIUM_PROMPTS_ADDRESS not set');
27
- // Read usdc() and price from prompts mapping
28
- const iface = new ethers.Interface([
29
- 'function usdc() view returns (address)',
30
- 'function prompts(bytes32) view returns (address subdao, uint256 price, bool exists)',
31
- 'function purchaseAccess(bytes32)'
32
- ]);
33
- const pr = new ethers.Contract(prAddr, iface, provider);
34
- // Derive tokenId from encryptedCID inside manifest (stable id used for ERC-1155)
35
- const ipfs = new IPFSManager(); await ipfs.initialize();
36
- const manifest = await ipfs.downloadJson(opts.cid);
37
- if (!manifest?.encryptedCID) throw new Error('Manifest missing encryptedCID');
38
- const cidHash = ethers.keccak256(ethers.toUtf8Bytes(normalizeCidV1Base32(manifest.encryptedCID)));
39
- const [usdcAddr, tuple] = await Promise.all([
40
- pr.usdc().catch(()=>{ throw new Error('Failed to read usdc() from PremiumPrompts'); }),
41
- pr.prompts(cidHash).catch(()=>{ throw new Error('Prompt not registered (prompts[cidHash].exists=false)'); })
42
- ]);
43
- const price = BigInt(tuple.price?.toString?.() || tuple[1]?.toString?.() || '0');
44
- const exists = Boolean(tuple.exists ?? tuple[2]);
45
- if (!exists) throw new Error('Prompt not registered on PremiumPrompts');
46
- const usdc = new ethers.Contract(usdcAddr, ['function allowance(address,address) view returns (uint256)','function approve(address,uint256) returns (bool)'], provider);
47
- const currentAllowance = await usdc.allowance(me, prAddr).catch(()=>0n);
48
- if (BigInt(currentAllowance.toString()) < price) {
49
- console.log('🪙 Approving USDC...', `needed=${price.toString()}`);
50
- const usdcW = new ethers.Contract(usdcAddr, ['function approve(address,uint256) returns (bool)'], signer);
51
- const txa = await usdcW.approve(prAddr, price);
52
- console.log('⏳ approve tx:', txa.hash);
53
- { const { waitForReceipt } = require('../utils/tx-wait'); const { ethers } = require('ethers'); const pr = new ethers.JsonRpcProvider(process.env.RPC_URL || process.env.BASE_SEPOLIA_RPC || process.env.BASE_RPC_URL || 'https://base-sepolia.publicnode.com'); await waitForReceipt(pr, txa, Number(process.env.SAGE_TX_WAIT_MS || 60000)); }
54
- console.log('✅ Approved');
55
- } else {
56
- console.log('✅ Allowance sufficient');
57
- }
58
- // Purchase
59
- const prW = new ethers.Contract(prAddr, ['function purchaseAccess(bytes32)'], signer);
60
- const txp = await prW.purchaseAccess(cidHash);
61
- console.log('🧾 purchaseAccess tx:', txp.hash);
62
- { const { waitForReceipt } = require('../utils/tx-wait'); const { ethers } = require('ethers'); const pr = new ethers.JsonRpcProvider(process.env.RPC_URL || process.env.BASE_SEPOLIA_RPC || process.env.BASE_RPC_URL || 'https://base-sepolia.publicnode.com'); await waitForReceipt(pr, txp, Number(process.env.SAGE_TX_WAIT_MS || 60000)); }
63
- console.log('✅ Purchase complete');
64
- console.log('Next: generate an AuthSig for this wallet and decrypt:');
65
- console.log(' sage premium-pre authsig --account', me, '--out ./authSig.buyer.json');
66
- console.log(' sage premium-pre view --cid', opts.cid, '--auth-sig ./authSig.buyer.json');
67
- } catch (e) {
68
- handleCLIError('premium:buy', e);
69
- }
70
- })
71
- )
72
- .addCommand(
73
- new Command('setup-roles')
74
- .description('One-shot: wire PremiumPrompts + SimpleKeyStore roles to the SubDAO Timelock (auto: bootstrap | timelock | governor)')
75
- .requiredOption('--subdao <address>', 'SubDAO address to resolve Governor/Timelock')
76
- .option('--prompts <address>', 'PremiumPrompts address (defaults to PREMIUM_PROMPTS_ADDRESS)')
77
- .option('--keystore <address>', 'SimpleKeyStore address (defaults to SIMPLE_KEY_STORE_ADDRESS)')
78
- .option('--bootstrap', 'Force direct admin grants from current signer (deployer/Safe) without timelock/governor)', false)
79
- .action(async (opts) => {
80
- try {
81
- const { ethers } = require('ethers');
82
- const WM = require('@sage-protocol/wallet-manager');
83
- const wm = new WM(); await wm.connect();
84
- const signer = wm.getSigner(); const provider = wm.getProvider();
85
- const { resolveGovContext } = require('../utils/gov-context');
86
- const ctx = await resolveGovContext({ govOpt: null, subdaoOpt: opts.subdao, provider });
87
- const tl = ctx.timelock; const gov = ctx.governor;
88
- if (!tl || !gov) throw new Error('Failed to resolve Governor/Timelock from --subdao');
89
- const prAddr = ethers.getAddress(opts.prompts || process.env.PREMIUM_PROMPTS_ADDRESS);
90
- const ksAddr = ethers.getAddress(opts.keystore || process.env.SIMPLE_KEY_STORE_ADDRESS);
91
- if (!prAddr) throw new Error('PREMIUM_PROMPTS_ADDRESS not set');
92
- if (!ksAddr) throw new Error('SIMPLE_KEY_STORE_ADDRESS not set');
93
-
94
- // Read role ids
95
- const pr = new ethers.Contract(prAddr, ['function DEFAULT_ADMIN_ROLE() view returns (bytes32)','function MANAGER_ROLE() view returns (bytes32)','function hasRole(bytes32,address) view returns (bool)','function getRoleAdmin(bytes32) view returns (bytes32)'], provider);
96
- const ks = new ethers.Contract(ksAddr, ['function DEFAULT_ADMIN_ROLE() view returns (bytes32)','function MANAGER_ROLE() view returns (bytes32)','function hasRole(bytes32,address) view returns (bool)','function getRoleAdmin(bytes32) view returns (bytes32)'], provider);
97
- const [PR_ADMIN, PR_MANAGER] = await Promise.all([pr.DEFAULT_ADMIN_ROLE(), pr.MANAGER_ROLE()]);
98
- const [KS_ADMIN, KS_MANAGER] = await Promise.all([ks.DEFAULT_ADMIN_ROLE(), ks.MANAGER_ROLE()]);
99
- const need = [];
100
- const hasPRadm = await pr.hasRole(PR_ADMIN, tl).catch(()=>false);
101
- const hasPRman = await pr.hasRole(PR_MANAGER, tl).catch(()=>false);
102
- const hasKSadm = await ks.hasRole(KS_ADMIN, tl).catch(()=>false);
103
- const hasKSman = await ks.hasRole(KS_MANAGER, tl).catch(()=>false);
104
- if (!hasPRadm) need.push({ target: prAddr, role: PR_ADMIN, label: 'PremiumPrompts.DEFAULT_ADMIN_ROLE', where: 'PremiumPrompts' });
105
- if (!hasPRman) need.push({ target: prAddr, role: PR_MANAGER, label: 'PremiumPrompts.MANAGER_ROLE', where: 'PremiumPrompts' });
106
- if (!hasKSadm) need.push({ target: ksAddr, role: KS_ADMIN, label: 'SimpleKeyStore.DEFAULT_ADMIN_ROLE', where: 'SimpleKeyStore' });
107
- if (!hasKSman) need.push({ target: ksAddr, role: KS_MANAGER, label: 'SimpleKeyStore.MANAGER_ROLE', where: 'SimpleKeyStore' });
108
- if (!need.length) { console.log('✅ No wiring needed.'); return; }
109
-
110
- // Try bootstrap (admin direct grant) for both contracts
111
- const tryBootstrap = async () => {
112
- let okAny = false; let okAll = true;
113
- for (const n of need) {
114
- try {
115
- const admin = new ethers.Contract(n.target, ['function hasRole(bytes32,address) view returns (bool)','function grantRole(bytes32,address)','function DEFAULT_ADMIN_ROLE() view returns (bytes32)','function getRoleAdmin(bytes32) view returns (bytes32)'], signer);
116
- const signerAddr = await signer.getAddress();
117
- const roleAdmin = await admin.getRoleAdmin(n.role).catch(async()=> admin.DEFAULT_ADMIN_ROLE());
118
- const isAdmin = await admin.hasRole(roleAdmin, signerAddr).catch(()=>false);
119
- if (!isAdmin) { okAll = false; continue; }
120
- const tx = await admin.grantRole(n.role, tl); console.log('⚙️ grantRole bootstrap', n.label, '->', tl, 'tx:', tx.hash); await tx.wait(); okAny = true;
121
- } catch (_) { okAll = false; }
122
- }
123
- return { okAny, okAll };
124
- };
125
-
126
- if (opts.bootstrap) {
127
- const boot = await tryBootstrap();
128
- if (!boot.okAll) {
129
- console.log('❌ Bootstrap incomplete. Some roles could not be granted.');
130
- console.log('Reason: current signer likely lacks admin rights for one or more roles.');
131
- console.log('Tip: run: sage premium roles-wizard --subdao', opts.subdao, '--format safe (generate Safe TX Builder JSON)');
132
- process.exit(1);
133
- }
134
- console.log('✅ Wired via bootstrap');
135
- return;
136
- }
137
-
138
- const boot = await tryBootstrap();
139
- if (boot.okAll) { console.log('✅ Wired via bootstrap'); return; }
140
-
141
- // Try Timelock schedule (requires PROPOSER_ROLE on signer)
142
- const tlc = new ethers.Contract(tl, ['function PROPOSER_ROLE() view returns (bytes32)','function hasRole(bytes32,address) view returns (bool)','function getMinDelay() view returns (uint256)','function schedule(address,uint256,bytes,bytes32,bytes32,uint256)','function execute(address,uint256,bytes,bytes32,bytes32)'], signer);
143
- let canSchedule = false; try { const PR = await tlc.PROPOSER_ROLE(); const signerAddr = await signer.getAddress(); canSchedule = await tlc.hasRole(PR, signerAddr); } catch(_) {}
144
- const iface = new ethers.Interface(['function grantRole(bytes32,address)']);
145
- if (canSchedule) {
146
- const delay = await tlc.getMinDelay().catch(()=>0n);
147
- const pred = ethers.ZeroHash;
148
- for (const n of need) {
149
- const salt = ethers.hexlify(ethers.randomBytes(32));
150
- const data = iface.encodeFunctionData('grantRole', [n.role, tl]);
151
- const stx = await tlc.schedule(n.target, 0, data, pred, salt, delay); console.log('⏳ schedule', n.label, 'tx:', stx.hash);
152
- if (delay === 0n) { const etx = await tlc.execute(n.target, 0, data, pred, salt); console.log('⚡ execute', n.label, 'tx:', etx.hash); }
153
- else console.log(`ℹ️ Execute later: cast send ${tl} "execute(address,uint256,bytes,bytes32,bytes32)" ${n.target} 0 ${data} ${pred} ${salt}`);
154
- }
155
- console.log('✅ Wired via timelock schedule'); return;
156
- }
157
-
158
- // Fallback: Governor proposal batching all grantRole calls
159
- const govCtr = new ethers.Contract(gov, ['function propose(address[],uint256[],bytes[],string) returns (uint256)'], signer);
160
- const targets = []; const values = []; const calldatas = [];
161
- for (const n of need) { targets.push(n.target); values.push(0); calldatas.push(iface.encodeFunctionData('grantRole', [n.role, tl])); }
162
- const desc = 'Wire PremiumPrompts + SimpleKeyStore roles to SubDAO Timelock';
163
- const { makeProposalDescription } = require('../utils/format');
164
- const salted = makeProposalDescription(desc || '');
165
- if (process.env.SAGE_USE_PRIVATE_TX === '1' && process.env.SAGE_PRIVATE_RPC) {
166
- const { loadSdk } = require('../utils/sdk-resolver'); const sdk = loadSdk();
167
- const calldata = govCtr.interface.encodeFunctionData('propose', [targets, values, calldatas, salted]);
168
- const { hash } = await sdk.utils.privateTx.sendTransaction({ signer, tx: { to: govAddr, data: calldata, value: 0n } });
169
- console.log('🗳️ propose (private) tx:', hash);
170
- } else {
171
- const tx = await govCtr.propose(targets, values, calldatas, salted); console.log('🗳️ propose tx:', tx.hash);
172
- }
173
- console.log('➡️ Next: vote/queue/execute or run:');
174
- console.log(` sage governance watch <proposalId> --subdao ${opts.subdao}`);
175
- } catch (e) {
176
- handleCLIError('premium:setup-roles', e, {
177
- context: { subdao: opts.subdao }
178
- });
179
- }
180
- })
181
- )
182
- .addCommand(
183
- new Command('bootstrap-proposal')
184
- .description('Create a governance proposal to finalize team-only wiring: grant Timelock roles and revoke deployer/admin roles')
185
- .requiredOption('--subdao <address>', 'SubDAO to derive Governor/Timelock')
186
- .option('--prompts <address>', 'PremiumPrompts address (defaults to env)')
187
- .option('--keystore <address>', 'SimpleKeyStore address (defaults to env)')
188
- .option('--deployer <address>', 'Address to revoke roles from (can be comma-separated)')
189
- .option('--ensure-proposer', 'Ensure Governor has PROPOSER_ROLE on Timelock (adds grantRole)', false)
190
- .action(async (opts) => {
191
- try {
192
- const { ethers } = require('ethers');
193
- const WM = require('@sage-protocol/wallet-manager');
194
- const wm = new WM(); await wm.connect();
195
- const signer = wm.getSigner(); const provider = wm.getProvider();
196
- const { resolveGovContext } = require('../utils/gov-context');
197
- const ctx = await resolveGovContext({ govOpt: null, subdaoOpt: opts.subdao, provider });
198
- const govAddr = ctx.governor; const tlAddr = ctx.timelock;
199
- if (!govAddr || !tlAddr) throw new Error('Failed to resolve Governor/Timelock from --subdao');
200
- const prAddr = ethers.getAddress(opts.prompts || process.env.PREMIUM_PROMPTS_ADDRESS);
201
- const ksAddr = ethers.getAddress(opts.keystore || process.env.SIMPLE_KEY_STORE_ADDRESS);
202
- if (!prAddr) throw new Error('PREMIUM_PROMPTS_ADDRESS not set');
203
- if (!ksAddr) throw new Error('SIMPLE_KEY_STORE_ADDRESS not set');
204
-
205
- const pr = new ethers.Contract(prAddr, ['function DEFAULT_ADMIN_ROLE() view returns (bytes32)','function MANAGER_ROLE() view returns (bytes32)','function hasRole(bytes32,address) view returns (bool)'], provider);
206
- const ks = new ethers.Contract(ksAddr, ['function DEFAULT_ADMIN_ROLE() view returns (bytes32)','function MANAGER_ROLE() view returns (bytes32)','function hasRole(bytes32,address) view returns (bool)'], provider);
207
- const [PR_ADMIN, PR_MANAGER, KS_ADMIN, KS_MANAGER] = await Promise.all([pr.DEFAULT_ADMIN_ROLE(), pr.MANAGER_ROLE(), ks.DEFAULT_ADMIN_ROLE(), ks.MANAGER_ROLE()]);
208
-
209
- const ops = [];
210
- const addGrant = (to, role, target) => ops.push({ type:'grant', target, role, to });
211
- const addRevoke = (from, role, target) => ops.push({ type:'revoke', target, role, from });
212
- const needs = await Promise.all([
213
- pr.hasRole(PR_ADMIN, tlAddr).catch(()=>false),
214
- pr.hasRole(PR_MANAGER, tlAddr).catch(()=>false),
215
- ks.hasRole(KS_ADMIN, tlAddr).catch(()=>false),
216
- ks.hasRole(KS_MANAGER, tlAddr).catch(()=>false),
217
- ]);
218
- if (!needs[0]) addGrant(tlAddr, PR_ADMIN, prAddr);
219
- if (!needs[1]) addGrant(tlAddr, PR_MANAGER, prAddr);
220
- if (!needs[2]) addGrant(tlAddr, KS_ADMIN, ksAddr);
221
- if (!needs[3]) addGrant(tlAddr, KS_MANAGER, ksAddr);
222
-
223
- // Optional revoke step(s)
224
- const revokeList = [];
225
- if (opts.deployer) {
226
- for (const a of String(opts.deployer).split(',').map(s=>s.trim())) { if (a) revokeList.push(ethers.getAddress(a)); }
227
- }
228
- for (const addr of revokeList) {
229
- if (addr.toLowerCase() !== tlAddr.toLowerCase()) {
230
- const [pra, prm, ksa, ksm] = await Promise.all([
231
- pr.hasRole(PR_ADMIN, addr).catch(()=>false),
232
- pr.hasRole(PR_MANAGER, addr).catch(()=>false),
233
- ks.hasRole(KS_ADMIN, addr).catch(()=>false),
234
- ks.hasRole(KS_MANAGER, addr).catch(()=>false),
235
- ]);
236
- if (pra) addRevoke(addr, PR_ADMIN, prAddr);
237
- if (prm) addRevoke(addr, PR_MANAGER, prAddr);
238
- if (ksa) addRevoke(addr, KS_ADMIN, ksAddr);
239
- if (ksm) addRevoke(addr, KS_MANAGER, ksAddr);
240
- }
241
- }
242
-
243
- // Optional ensure Governor has PROPOSER on Timelock
244
- if (opts.ensureProposer) {
245
- const tl = new ethers.Contract(tlAddr, ['function PROPOSER_ROLE() view returns (bytes32)','function hasRole(bytes32,address) view returns (bool)'], provider);
246
- const PR = await tl.PROPOSER_ROLE();
247
- const has = await tl.hasRole(PR, govAddr).catch(()=>false);
248
- if (!has) ops.push({ type:'timelockGrant', roleName:'PROPOSER_ROLE' });
249
- }
250
-
251
- if (!ops.length) { console.log('✅ Nothing to do; roles already correct'); return; }
252
-
253
- // Build proposal
254
- const tlAbi = resolveArtifact('contracts/cloneable/TimelockControllerCloneable.sol/TimelockControllerCloneable.json').abi;
255
- const tlIface = new ethers.Interface(tlAbi);
256
- const iface = new ethers.Interface(['function grantRole(bytes32,address)','function revokeRole(bytes32,address)']);
257
- const targets = []; const values = []; const calldatas = [];
258
- for (const op of ops) {
259
- if (op.type === 'grant') { targets.push(op.target); values.push(0); calldatas.push(iface.encodeFunctionData('grantRole', [op.role, tlAddr])); }
260
- else if (op.type === 'revoke') { targets.push(op.target); values.push(0); calldatas.push(iface.encodeFunctionData('revokeRole', [op.role, op.from])); }
261
- else if (op.type === 'timelockGrant') {
262
- const tlRO = new ethers.Contract(tlAddr, ['function PROPOSER_ROLE() view returns (bytes32)'], provider);
263
- const PR = await tlRO.PROPOSER_ROLE();
264
- targets.push(tlAddr); values.push(0); calldatas.push(tlIface.encodeFunctionData('grantRole', [PR, govAddr]));
265
- }
266
- }
267
- const gov = new ethers.Contract(govAddr, ['function propose(address[],uint256[],bytes[],string) returns (uint256)'], signer);
268
- const desc = 'Finalize team-only premium wiring (grant Timelock roles, revoke deployer roles)';
269
- const { makeProposalDescription } = require('../utils/format');
270
- const salted2 = makeProposalDescription(desc || '');
271
- if (process.env.SAGE_USE_PRIVATE_TX === '1' && process.env.SAGE_PRIVATE_RPC) {
272
- const { loadSdk } = require('../utils/sdk-resolver'); const sdk = loadSdk();
273
- const calldata2 = gov.interface.encodeFunctionData('propose', [targets, values, calldatas, salted2]);
274
- const { hash } = await sdk.utils.privateTx.sendTransaction({ signer, tx: { to: govAddr, data: calldata2, value: 0n } });
275
- console.log('🗳️ propose (private) tx:', hash);
276
- } else {
277
- const tx = await gov.propose(targets, values, calldatas, salted2);
278
- console.log('🗳️ propose tx:', tx.hash);
279
- }
280
- // Compute id
281
- let id = null; try { const gi = new ethers.Interface(['function hashProposal(address[],uint256[],bytes[],bytes32) view returns (uint256)']); const govRO = new ethers.Contract(govAddr, gi, provider); const descHash = ethers.keccak256(ethers.toUtf8Bytes(desc)); id = (await govRO.hashProposal(targets, values, calldatas, descHash)).toString(); } catch(_) {}
282
- if (id) {
283
- console.log(`➡️ Watching proposal ${id} ...`);
284
- try { const { execSync } = require('child_process'); console.log(execSync(['node','packages/cli/src/index.js','governance','watch',String(id),'--subdao',String(opts.subdao)].join(' '), { encoding:'utf8' })); } catch(e) { console.log('⚠️ watch failed:', e.message); }
285
- }
286
- } catch (e) {
287
- handleCLIError('premium:bootstrap-proposal', e, {
288
- context: { subdao: opts.subdao }
289
- });
290
- }
291
- })
292
- )
293
- .addCommand(
294
- new Command('roles-wizard')
295
- .description('Interactive plan + exporters for PremiumPrompts/SimpleKeyStore role wiring (plan|safe|cast|governor)')
296
- .requiredOption('--subdao <address>', 'SubDAO address to resolve Governor/Timelock')
297
- .option('--prompts <address>', 'PremiumPrompts address (defaults to PREMIUM_PROMPTS_ADDRESS)')
298
- .option('--keystore <address>', 'SimpleKeyStore address (defaults to SIMPLE_KEY_STORE_ADDRESS)')
299
- .option('--format <fmt>', 'plan|safe|cast|governor', 'plan')
300
- .option('--out <path>', 'Write JSON output to file (safe/governor only)')
301
- .action(async (opts) => {
302
- try {
303
- const { ethers } = require('ethers');
304
- let colors; try { colors = (require('chalk').default || require('chalk')); } catch (_) { colors = { cyan:(s)=>s, green:(s)=>s, yellow:(s)=>s, red:(s)=>s, gray:(s)=>s }; }
305
- const WM = require('@sage-protocol/wallet-manager');
306
- const wm = new WM(); await wm.connect();
307
- const provider = wm.getProvider();
308
- const { resolveGovContext } = require('../utils/gov-context');
309
- const ctx = await resolveGovContext({ govOpt: null, subdaoOpt: opts.subdao, provider });
310
- const timelock = ctx.timelock; const governor = ctx.governor;
311
- if (!timelock || !governor) throw new Error('Failed to resolve Governor/Timelock from --subdao');
312
- const prAddr = ethers.getAddress(opts.prompts || process.env.PREMIUM_PROMPTS_ADDRESS);
313
- const ksAddr = ethers.getAddress(opts.keystore || process.env.SIMPLE_KEY_STORE_ADDRESS);
314
- if (!prAddr) throw new Error('PREMIUM_PROMPTS_ADDRESS not set');
315
- if (!ksAddr) throw new Error('SIMPLE_KEY_STORE_ADDRESS not set');
316
-
317
- const pr = new ethers.Contract(prAddr, [
318
- 'function DEFAULT_ADMIN_ROLE() view returns (bytes32)',
319
- 'function MANAGER_ROLE() view returns (bytes32)',
320
- 'function hasRole(bytes32,address) view returns (bool)',
321
- 'function getRoleAdmin(bytes32) view returns (bytes32)'
322
- ], provider);
323
- const ks = new ethers.Contract(ksAddr, [
324
- 'function DEFAULT_ADMIN_ROLE() view returns (bytes32)',
325
- 'function MANAGER_ROLE() view returns (bytes32)',
326
- 'function hasRole(bytes32,address) view returns (bool)',
327
- 'function getRoleAdmin(bytes32) view returns (bytes32)'
328
- ], provider);
329
-
330
- const [PR_ADMIN, PR_MANAGER, KS_ADMIN, KS_MANAGER] = await Promise.all([
331
- pr.DEFAULT_ADMIN_ROLE(), pr.MANAGER_ROLE(), ks.DEFAULT_ADMIN_ROLE(), ks.MANAGER_ROLE()
332
- ]);
333
-
334
- const needed = [];
335
- const checks = [
336
- { where: 'PremiumPrompts', addr: prAddr, role: PR_ADMIN, label: 'DEFAULT_ADMIN_ROLE' },
337
- { where: 'PremiumPrompts', addr: prAddr, role: PR_MANAGER, label: 'MANAGER_ROLE' },
338
- { where: 'SimpleKeyStore', addr: ksAddr, role: KS_ADMIN, label: 'DEFAULT_ADMIN_ROLE' },
339
- { where: 'SimpleKeyStore', addr: ksAddr, role: KS_MANAGER, label: 'MANAGER_ROLE' },
340
- ];
341
- for (const c of checks) {
342
- const cctr = (c.where === 'PremiumPrompts') ? pr : ks;
343
- const has = await cctr.hasRole(c.role, timelock).catch(()=>false);
344
- if (!has) {
345
- const admin = await cctr.getRoleAdmin(c.role).catch(async()=> (c.where==='PremiumPrompts'? PR_ADMIN : KS_ADMIN));
346
- const tlIsAdmin = await cctr.hasRole(admin, timelock).catch(()=>false);
347
- needed.push({ ...c, admin, tlIsAdmin });
348
- }
349
- }
350
-
351
- if (!needed.length) { console.log(colors.green('✅ No wiring needed. Timelock already has required roles.')); return; }
352
-
353
- console.log(colors.cyan('Premium Roles Plan'));
354
- console.log('Timelock:', timelock);
355
- console.log('Governor:', governor);
356
- console.log('— Required Grants —');
357
- for (const n of needed) {
358
- const reason = n.tlIsAdmin ? 'grantable via timelock/governor' : 'cannot be granted by timelock (no admin) — bootstrap required';
359
- console.log(` ${n.where}.${n.label} → Timelock: ❌ (${reason})`);
360
- }
361
-
362
- const iface = new ethers.Interface(['function grantRole(bytes32,address)']);
363
- const actions = needed.map(n => ({ to: n.addr, data: iface.encodeFunctionData('grantRole', [n.role, timelock]) }));
364
-
365
- // Output formatter
366
- const fmt = String(opts.format||'plan').toLowerCase();
367
- if (fmt === 'plan') {
368
- console.log('\nNext steps (choose one path):');
369
- const anyBootstrap = needed.some(n => !n.tlIsAdmin);
370
- if (anyBootstrap) console.log(' - Bootstrap once from an admin signer (or Safe): sage premium setup-roles --subdao', opts.subdao, '--bootstrap');
371
- console.log(' - Operator path (if signer has PROPOSER_ROLE): sage premium setup-roles --subdao', opts.subdao);
372
- console.log(' - Pure governance path:');
373
- console.log(' sage premium setup-roles --subdao', opts.subdao);
374
- console.log(' sage governance watch <proposalId> --subdao', opts.subdao);
375
- return;
376
- }
377
-
378
- if (fmt === 'cast') {
379
- const rpc = process.env.RPC_URL || 'https://base-sepolia.publicnode.com';
380
- console.log('\ncast commands (run from an admin account):');
381
- for (const n of needed) {
382
- console.log(`cast send ${n.addr} "grantRole(bytes32,address)" ${n.role} ${timelock} --rpc-url ${rpc}`);
383
- }
384
- return;
385
- }
386
-
387
- if (fmt === 'safe') {
388
- const net = await provider.getNetwork();
389
- const bundle = {
390
- version: '1.0',
391
- chainId: String(net.chainId),
392
- createdAt: Date.now(),
393
- meta: { name: 'Sage Premium Roles Wiring', description: 'Grant roles to Timelock (PremiumPrompts + SimpleKeyStore)' },
394
- transactions: actions.map(a => ({ to: a.to, value: '0', data: a.data, operation: 0 }))
395
- };
396
- const out = JSON.stringify(bundle, null, 2);
397
- if (opts.out) { require('fs').writeFileSync(require('path').resolve(opts.out), out); console.log('✅ Wrote Safe TX bundle to', opts.out); }
398
- else console.log(out);
399
- return;
400
- }
401
-
402
- if (fmt === 'governor') {
403
- const targets = actions.map(a => a.to);
404
- const values = actions.map(() => 0);
405
- const calldatas = actions.map(a => a.data);
406
- const desc = 'Wire PremiumPrompts + SimpleKeyStore roles to SubDAO Timelock';
407
- const out = JSON.stringify({ governor, targets, values, calldatas, description: desc }, null, 2);
408
- if (opts.out) { require('fs').writeFileSync(require('path').resolve(opts.out), out); console.log('✅ Wrote Governor proposal bundle to', opts.out); }
409
- else console.log(out);
410
- return;
411
- }
412
-
413
- console.log('Unknown --format. Use one of: plan|safe|cast|governor');
414
- } catch (e) {
415
- handleCLIError('premium:roles-wizard', e, {
416
- context: { subdao: opts.subdao }
417
- });
418
- }
419
- })
420
- )
421
- .addCommand(
422
- new Command('create')
423
- .description('Create/register a premium prompt')
424
- .requiredOption('--cid <cid>', 'CID of premium prompt (normalized to CIDv1 base32)')
425
- .requiredOption('--subdao <address>', 'SubDAO address to receive proceeds')
426
- .requiredOption('--price <usdc>', 'Price in USDC (6 decimals, human value e.g., 2.50)')
427
- .action(async (opts) => {
428
- try {
429
- const addr = process.env.PREMIUM_PROMPTS_ADDRESS;
430
- if (!addr) throw new Error('PREMIUM_PROMPTS_ADDRESS not set');
431
- const provider = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
432
- const wm = new (require('@sage-protocol/wallet-manager'))(); await wm.connect();
433
- const signer = wm.getSigner();
434
- const { resolveArtifact } = require('../utils/artifacts');
435
- const abi = resolveArtifact('contracts/premium/PremiumPrompts.sol/PremiumPrompts.json').abi;
436
- const ctr = new ethers.Contract(addr, abi, signer);
437
-
438
- // Preflight: signer or resolved Timelock must have MANAGER_ROLE
439
- try {
440
- const MANAGER = await ctr.MANAGER_ROLE();
441
- const signerAddr = await signer.getAddress();
442
- const signerHas = await ctr.hasRole(MANAGER, signerAddr).catch(()=>false);
443
- if (!signerHas) {
444
- const { resolveGovContext } = require('../utils/gov-context');
445
- const ctx = await resolveGovContext({ govOpt: null, subdaoOpt: opts.subdao, provider });
446
- const tl = ctx?.timelock || process.env.TIMELOCK || process.env.TIMELOCK_ADDRESS;
447
- if (tl) {
448
- const tlHas = await ctr.hasRole(MANAGER, tl).catch(()=>false);
449
- if (!tlHas) {
450
- const rpc = process.env.RPC_URL || 'https://base-sepolia.publicnode.com';
451
- console.log('❌ Preflight: Timelock lacks MANAGER_ROLE on PremiumPrompts.');
452
- console.log('Grant roles (from PremiumPrompts admin):');
453
- console.log(` cast send ${addr} "grantRole(bytes32,address)" $(cast call ${addr} "DEFAULT_ADMIN_ROLE() returns (bytes32)") ${tl} --rpc-url ${rpc}`);
454
- console.log(` cast send ${addr} "grantRole(bytes32,address)" $(cast call ${addr} "MANAGER_ROLE() returns (bytes32)") ${tl} --rpc-url ${rpc}`);
455
- console.log('\nOr schedule via timelock:');
456
- console.log(` sage timelock premium-roles --prompts ${addr} --subdao ${opts.subdao} --fix`);
457
- console.log('\nIf you intend to create directly, the current signer must hold MANAGER_ROLE.');
458
- return process.exit(1);
459
- }
460
- } else {
461
- console.log('❌ Preflight: No Timelock resolved and signer lacks MANAGER_ROLE.');
462
- console.log('Grant the signer MANAGER_ROLE or use timelock premium-roles to set up team mode.');
463
- return process.exit(1);
464
- }
465
- }
466
- } catch (_) { /* non-fatal */ }
467
- const hash = ethers.keccak256(ethers.toUtf8Bytes(normalizeCidV1Base32(opts.cid)));
468
- const price = ethers.parseUnits(String(opts.price), 6);
469
- const tx = await ctr.createPremiumPrompt(hash, ethers.getAddress(opts.subdao), price);
470
- console.log('⏳ createPremiumPrompt tx:', tx.hash); { const { waitForReceipt } = require('../utils/tx-wait'); const { ethers } = require('ethers'); const pr = new ethers.JsonRpcProvider(process.env.RPC_URL || process.env.BASE_SEPOLIA_RPC || process.env.BASE_RPC_URL || 'https://base-sepolia.publicnode.com'); await waitForReceipt(pr, tx, Number(process.env.SAGE_TX_WAIT_MS || 60000)); }
471
- console.log('✅ Premium prompt created');
472
- } catch (e) {
473
- handleCLIError('premium:create', e, {
474
- context: { subdao: opts.subdao }
475
- });
476
- }
477
- })
478
- )
479
- .addCommand(
480
- new Command('check')
481
- .description('Check ERC1155 receipt balance for a premium prompt')
482
- .requiredOption('--cid <cid>', 'CID of premium prompt')
483
- .option('--address <address>', 'Wallet address (defaults to current)')
484
- .action(async (opts) => {
485
- try {
486
- const rcptAddr = process.env.PREMIUM_RECEIPT_ADDRESS;
487
- if (!rcptAddr) throw new Error('PREMIUM_RECEIPT_ADDRESS not set');
488
- const wm = new (require('@sage-protocol/wallet-manager'))(); await wm.connect();
489
- const provider = wm.getProvider();
490
- const target = opts.address || wm.getAccount();
491
- const { resolveArtifact } = require('../utils/artifacts');
492
- const abi = resolveArtifact('contracts/premium/PremiumReceipt.sol/PremiumReceipt.json').abi;
493
- const r = new ethers.Contract(rcptAddr, abi, provider);
494
- const id = BigInt(ethers.keccak256(ethers.toUtf8Bytes(normalizeCidV1Base32(opts.cid))));
495
- const bal = await r.balanceOf(target, id);
496
- console.log(`balanceOf(${target}, tokenId=${id}) = ${bal.toString()}`);
497
- } catch (e) {
498
- handleCLIError('premium:check', e);
499
- }
500
- })
501
- )
502
- .addCommand(
503
- new Command('info')
504
- .description('Show premium prompt configuration')
505
- .requiredOption('--cid <cid>', 'CID of premium prompt')
506
- .action(async (opts) => {
507
- try {
508
- const promptsAddr = process.env.PREMIUM_PROMPTS_ADDRESS; if (!promptsAddr) throw new Error('PREMIUM_PROMPTS_ADDRESS not set');
509
- const factoryAddr = process.env.SUBDAO_FACTORY_ADDRESS; if (!factoryAddr) throw new Error('SUBDAO_FACTORY_ADDRESS not set');
510
- const provider = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
511
- const { resolveArtifact } = require('../utils/artifacts');
512
- const pa = resolveArtifact('contracts/premium/PremiumPrompts.sol/PremiumPrompts.json').abi;
513
- const fa = resolveArtifact('contracts/SubDAOFactoryOptimized.sol/SubDAOFactoryOptimized.json').abi;
514
- const pr = new ethers.Contract(promptsAddr, pa, provider);
515
- const f = new ethers.Contract(factoryAddr, fa, provider);
516
- const hash = ethers.keccak256(ethers.toUtf8Bytes(normalizeCidV1Base32(opts.cid)));
517
- const p = await pr.prompts(hash);
518
- if (!p.exists) { console.log('No premium prompt found'); return; }
519
- const treasury = await f.resolveTreasury(p.subdao);
520
- console.log(JSON.stringify({ cid: opts.cid, cidHash: hash, subdao: p.subdao, price: p.price.toString(), treasury }, null, 2));
521
- } catch (e) {
522
- handleCLIError('premium:info', e);
523
- }
524
- })
525
- )
526
- .addCommand(new Command('encrypt').description('[removed] Use premium-simple encrypt/publish').action(()=>{ console.log('This command has been removed. Use: sage premium-simple encrypt/publish'); process.exit(1); }))
527
- .addCommand(
528
- new Command('publish')
529
- .description('Encrypt, upload, and register a premium prompt (Lit-gated)')
530
- .requiredOption('--in <path>', 'Path to plaintext file')
531
- .requiredOption('--name <name>', 'Name')
532
- .requiredOption('--description <text>', 'Description')
533
- .requiredOption('--subdao <address>', 'SubDAO')
534
- .requiredOption('--price <usdc>', 'Price, e.g., 2.50')
535
- .action(async (opts) => {
536
- try {
537
- const fs = require('fs'); const path = require('path');
538
- const IPFSManager = require('../ipfs-manager'); const ipfs = new IPFSManager(); await ipfs.initialize();
539
- const rcpt = process.env.PREMIUM_RECEIPT_ADDRESS; if (!rcpt) throw new Error('PREMIUM_RECEIPT_ADDRESS not set');
540
- const { encryptStringWithLit, buildNodeAuthSig } = require('../utils/lit');
541
- const content = fs.readFileSync(path.resolve(opts.in), 'utf8');
542
- // reuse signer below; do not redeclare later
543
- const wm = new (require('@sage-protocol/wallet-manager'))(); await wm.connect(); const signer = wm.getSigner();
544
- const authSig = await buildNodeAuthSig(signer, 'base-sepolia');
545
- const { encryptedBlob, encryptedSymmetricKey, accessControlConditions } = await (async () => {
546
- const fakeTokenId = '0';
547
- return await encryptStringWithLit({ content, receiptAddress: rcpt, tokenId: fakeTokenId, authSig });
548
- })();
549
- const encPayload = {
550
- type: 'lit-encrypted-prompt', name: opts.name, description: opts.description,
551
- encryptedSymmetricKey, accessControlConditions, encoding: 'base64',
552
- encryptedData: Buffer.from(await encryptedBlob.arrayBuffer?.() || encryptedBlob).toString('base64')
553
- };
554
- const encryptedCID = await ipfs.uploadJson(encPayload, `${opts.name}.encrypted`);
555
- const manifest = { type: 'sage-premium-manifest', version: '1.0.0', encryptedCID,
556
- lit: { receiptAddress: rcpt, encryptedSymmetricKey, accessControlConditions, chain: 'base-sepolia' } };
557
- const manifestCID = await ipfs.uploadJson(manifest, `${opts.name}.manifest`);
558
- // signer already initialized above
559
- const { resolveArtifact } = require('../utils/artifacts');
560
- const prAddr = process.env.PREMIUM_PROMPTS_ADDRESS; if (!prAddr) throw new Error('PREMIUM_PROMPTS_ADDRESS not set');
561
- // Preflight: signer or Timelock must be authorized (MANAGER_ROLE) to create prompts
562
- try {
563
- const { ethers } = require('ethers');
564
- const abiPre = resolveArtifact('contracts/premium/PremiumPrompts.sol/PremiumPrompts.json').abi;
565
- const ctrPre = new ethers.Contract(prAddr, abiPre, signer);
566
- const MANAGER = await ctrPre.MANAGER_ROLE();
567
- const signerAddr = await signer.getAddress();
568
- const signerHas = await ctrPre.hasRole(MANAGER, signerAddr).catch(()=>false);
569
- if (!signerHas) {
570
- const provider = wm.getProvider();
571
- const { resolveGovContext } = require('../utils/gov-context');
572
- const ctx = await resolveGovContext({ govOpt: null, subdaoOpt: opts.subdao, provider });
573
- const tl = ctx?.timelock || process.env.TIMELOCK || process.env.TIMELOCK_ADDRESS;
574
- if (!tl) {
575
- console.log('❌ Preflight: No Timelock resolved and signer lacks MANAGER_ROLE on PremiumPrompts.');
576
- console.log('Grant MANAGER to signer or set up team mode via timelock premium-roles.');
577
- return process.exit(1);
578
- }
579
- const tlHas = await ctrPre.hasRole(MANAGER, tl).catch(()=>false);
580
- if (!tlHas) {
581
- const rpc = process.env.RPC_URL || 'https://base-sepolia.publicnode.com';
582
- console.log('❌ Preflight: Timelock lacks MANAGER_ROLE on PremiumPrompts.');
583
- console.log(` sage timelock premium-roles --prompts ${prAddr} --subdao ${opts.subdao} --fix`);
584
- console.log('Or bootstrap once from an admin:');
585
- console.log(` sage timelock premium-roles --prompts ${prAddr} --subdao ${opts.subdao} --bootstrap`);
586
- console.log(` cast send ${prAddr} \"grantRole(bytes32,address)\" $(cast call ${prAddr} \"MANAGER_ROLE() returns (bytes32)\") <TIMELOCK> --rpc-url ${rpc}`);
587
- return process.exit(1);
588
- }
589
- }
590
- } catch (_) { /* non-fatal guard */ }
591
- const cidHash = ethers.keccak256(ethers.toUtf8Bytes(manifestCID));
592
- const price = ethers.parseUnits(String(opts.price), 6);
593
- // Prefer V2 if available
594
- const ifaceV2 = new ethers.Interface(['function createPremiumPromptWithManifest(bytes32,address,uint256,string)']);
595
- let tx;
596
- try {
597
- // staticcall to detect function existence
598
- await signer.provider.call({ to: prAddr, data: ifaceV2.encodeFunctionData('createPremiumPromptWithManifest',[cidHash, ethers.getAddress(opts.subdao), price, manifestCID]) });
599
- tx = await signer.sendTransaction({ to: prAddr, data: ifaceV2.encodeFunctionData('createPremiumPromptWithManifest',[cidHash, ethers.getAddress(opts.subdao), price, manifestCID]) });
600
- } catch (_) {
601
- const iface = new ethers.Interface(['function createPremiumPrompt(bytes32,address,uint256)']);
602
- tx = await signer.sendTransaction({ to: prAddr, data: iface.encodeFunctionData('createPremiumPrompt',[cidHash, ethers.getAddress(opts.subdao), price]) });
603
- }
604
- console.log('⏳ createPremiumPrompt tx:', tx.hash); { const { waitForReceipt } = require('../utils/tx-wait'); const { ethers } = require('ethers'); const pr = new ethers.JsonRpcProvider(process.env.RPC_URL || process.env.BASE_SEPOLIA_RPC || process.env.BASE_RPC_URL || 'https://base-sepolia.publicnode.com'); await waitForReceipt(pr, tx, Number(process.env.SAGE_TX_WAIT_MS || 60000)); }
605
- console.log(JSON.stringify({ manifestCID, encryptedCID, tx: tx.hash }, null, 2));
606
- } catch (e) {
607
- console.error('❌ premium publish failed:', e.message);
608
- if (e.code === 'LIT_DEP_MISSING') console.error('ℹ️ npm i @lit-protocol/lit-node-client @lit-protocol/encryption');
609
- process.exit(1);
610
- }
611
- })
612
- )
613
- .addCommand(new Command('reveal').description('[removed] Use premium-simple view').action(()=>{ console.log('This command has been removed. Use: sage premium-simple view'); process.exit(1); }))
215
+ .description('Premium prompts tooling (diagnostics; use "sage personal" for creator flows)')
216
+ .action(() => {
217
+ console.log(`
218
+ Premium Prompts
219
+
220
+ For personal premium prompts (recommended):
221
+
222
+ sage personal sell <key> <price> --encrypt --file <path> # List encrypted content
223
+ sage personal buy <creator> <key> # Purchase license
224
+ sage personal access <creator> <key> # Decrypt purchased content
225
+ sage personal list --mine # List your listings
226
+ sage personal my-licenses # Show owned licenses
227
+
228
+ Enable personal commands:
229
+ export SAGE_FLAGS_ALLOW_ENV=1 SAGE_ENABLE_PERSONAL=1
230
+
231
+ For more information:
232
+ docs/specs/premium-endorsement-model.md
233
+ `);
234
+ })
614
235
  .addCommand(
615
- new Command('open-creators')
616
- .description('Enable/disable open creators for a SubDAO (manager/admin or via Timelock)')
617
- .requiredOption('--subdao <address>', 'SubDAO address')
618
- .requiredOption('--enable <bool>', 'true|false')
619
- .option('--via-timelock', 'Schedule via resolved Timelock instead of sending directly', false)
620
- .action(async (opts) => {
621
- try {
622
- const promptsAddr = process.env.PREMIUM_PROMPTS_ADDRESS; if (!promptsAddr) throw new Error('PREMIUM_PROMPTS_ADDRESS not set');
623
- const provider = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
624
- const wm = new (require('@sage-protocol/wallet-manager'))(); await wm.connect();
625
- const signer = wm.getSigner();
626
- const enable = String(opts.enable).toLowerCase();
627
- const flag = (enable === 'true' || enable === '1' || enable === 'yes');
628
- const iface = new ethers.Interface(['function setOpenCreators(address,bool)']);
629
- const data = iface.encodeFunctionData('setOpenCreators', [ethers.getAddress(opts.subdao), flag]);
630
- if (!opts.viaTimelock) {
631
- const tx = await signer.sendTransaction({ to: promptsAddr, data });
632
- console.log('tx:', tx.hash); return;
633
- }
634
- // via timelock: resolve and schedule
635
- const { resolveGovContext } = require('../utils/gov-context');
636
- const ctx = await resolveGovContext({ govOpt: null, subdaoOpt: opts.subdao, provider });
637
- const tl = ctx?.timelock || process.env.TIMELOCK || process.env.TIMELOCK_ADDRESS; if (!tl) throw new Error('Timelock not resolved');
638
- const tlc = new ethers.Contract(tl, [
639
- 'function schedule(address,uint256,bytes,bytes32,bytes32,uint256)',
640
- 'function getMinDelay() view returns (uint256)',
641
- 'function PROPOSER_ROLE() view returns (bytes32)',
642
- 'function hasRole(bytes32,address) view returns (bool)'
643
- ], signer);
644
- // Preflight: ensure current signer has PROPOSER_ROLE on Timelock; otherwise guide next steps
645
- try {
646
- const signerAddr = await signer.getAddress();
647
- const PROPOSER = await tlc.PROPOSER_ROLE();
648
- const ok = await tlc.hasRole(PROPOSER, signerAddr);
649
- if (!ok) {
650
- console.log('❌ Missing PROPOSER_ROLE on Timelock for current signer.');
651
- console.log('Options:');
652
- console.log(' - Run: sage timelock doctor --subdao', opts.subdao, '--fix (grants PROPOSER to Governor if signer is admin)');
653
- console.log(' - Or schedule from a wallet that holds PROPOSER_ROLE on the Timelock');
654
- console.log(' - Or create a governance proposal that calls setOpenCreators()');
655
- return process.exit(1);
656
- }
657
- } catch (_) {}
658
- const delay = await tlc.getMinDelay().catch(()=>0n);
659
- const salt = ethers.hexlify(ethers.randomBytes(32));
660
- const predecessor = ethers.ZeroHash;
661
- const stx = await tlc.schedule(promptsAddr, 0, data, predecessor, salt, delay);
662
- console.log('schedule tx:', stx.hash);
663
- if (delay === 0n) {
664
- const exe = new ethers.Interface(['function execute(address,uint256,bytes,bytes32,bytes32)']);
665
- const txe = await signer.sendTransaction({ to: tl, data: exe.encodeFunctionData('execute', [promptsAddr, 0, data, predecessor, salt]) });
666
- console.log('execute tx:', txe.hash);
667
- } else {
668
- console.log('Execute later with:');
669
- console.log(`cast send ${tl} "execute(address,uint256,bytes,bytes32,bytes32)" ${promptsAddr} 0 ${data} ${predecessor} ${salt} --rpc-url ${process.env.RPC_URL || 'https://base-sepolia.publicnode.com'}`);
670
- }
671
- } catch (e) { console.error('❌ open-creators failed:', e.message); process.exit(1); }
672
- })
236
+ new Command('doctor')
237
+ .description('Inspect PremiumPrompts / SimpleKeyStore roles for a DAO')
238
+ .option('--dao <address>', 'DAO context for governance wiring')
239
+ .option('--subdao <address>', 'Alias for --dao (deprecated)')
240
+ .option('--gov <address>', 'Explicit Governor address (optional)')
241
+ .option('--json', 'Output machine-readable JSON only', false)
242
+ .action(runPremiumDoctor)
673
243
  );
674
244
 
675
245
  program.addCommand(premium);