@sage-protocol/cli 0.3.6 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/boost.js +76 -68
- package/dist/cli/commands/bounty.js +123 -85
- package/dist/cli/commands/config.js +2 -0
- package/dist/cli/commands/doctor.js +18 -7
- package/dist/cli/commands/personal.js +64 -78
- package/dist/cli/commands/premium-pre.js +23 -875
- package/dist/cli/commands/premium.js +237 -667
- package/dist/cli/commands/prompts.js +6 -1
- package/dist/cli/commands/sxxx.js +103 -0
- package/dist/cli/commands/timelock.js +22 -192
- package/dist/cli/index.js +1 -5
- package/dist/cli/utils/aliases.js +3 -1
- package/dist/cli/utils/error-handler.js +7 -13
- package/dist/cli/utils/lit.js +103 -0
- package/dist/ops/config/env/load-env.sepolia.sh +4 -4
- package/package.json +1 -1
|
@@ -1,887 +1,35 @@
|
|
|
1
1
|
const { Command } = require('commander');
|
|
2
|
-
const { ethers } = require('ethers');
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const path = require('path');
|
|
13
|
-
const { createRequire } = require('module');
|
|
14
|
-
// Base at cli/ (two levels up from this file: cli/commands → cli)
|
|
15
|
-
const cliBase = path.resolve(__dirname, '..', '..');
|
|
16
|
-
const reqCli = createRequire(path.join(cliBase, 'package.json'));
|
|
17
|
-
const { LitNodeClient } = reqCli('@lit-protocol/lit-node-client');
|
|
18
|
-
const { decryptToUint8Array, encryptUint8Array } = reqCli('@lit-protocol/encryption');
|
|
19
|
-
return { LitNodeClient, decryptToUint8Array, encryptUint8Array };
|
|
20
|
-
} catch (e2) {
|
|
21
|
-
const hint = 'Install Lit SDK at root with Yarn workspaces (yarn add -W @lit-protocol/lit-node-client) or inside ./cli (cd cli && npm i @lit-protocol/lit-node-client).';
|
|
22
|
-
throw new Error(`Lit SDK not found in root or cli package. ${hint}`);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function requireLitAuthHelpers() {
|
|
28
|
-
// Try resolving from normal Node resolution, then fall back to the cli/ directory
|
|
29
|
-
try {
|
|
30
|
-
const { LitAccessControlConditionResource } = require('@lit-protocol/auth-helpers');
|
|
31
|
-
return { LitAccessControlConditionResource };
|
|
32
|
-
} catch (_) {
|
|
33
|
-
try {
|
|
34
|
-
const path = require('path');
|
|
35
|
-
const { createRequire } = require('module');
|
|
36
|
-
const cliBase = path.resolve(__dirname, '..', '..');
|
|
37
|
-
const reqCli = createRequire(path.join(cliBase, 'package.json'));
|
|
38
|
-
const { LitAccessControlConditionResource } = reqCli('@lit-protocol/auth-helpers');
|
|
39
|
-
return { LitAccessControlConditionResource };
|
|
40
|
-
} catch (e2) {
|
|
41
|
-
throw new Error('Lit auth-helpers not found. Install @lit-protocol/auth-helpers in root or cli package.');
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function loadAuthSig(pathOrEnv) {
|
|
47
|
-
const fs = require('fs'); const path = require('path');
|
|
48
|
-
const candidate = pathOrEnv || process.env.LIT_AUTH_SIG;
|
|
49
|
-
if (!candidate) throw new Error('AuthSig not provided. Pass --auth-sig <file> or set LIT_AUTH_SIG=<file>');
|
|
50
|
-
const p = path.resolve(candidate);
|
|
51
|
-
if (!fs.existsSync(p)) throw new Error(`AuthSig file not found: ${p}`);
|
|
52
|
-
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (e) { throw new Error('Invalid AuthSig JSON'); }
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function toB64(buf) { return Buffer.from(buf).toString('base64'); }
|
|
56
|
-
function fromB64(b64) { return Buffer.from(b64, 'base64'); }
|
|
3
|
+
/**
|
|
4
|
+
* Premium PRE (Proxy Re-Encryption) CLI
|
|
5
|
+
*
|
|
6
|
+
* This command was part of the SubDAO-based premium prompts model.
|
|
7
|
+
* Use `sage personal` commands instead for personal premium.
|
|
8
|
+
*
|
|
9
|
+
* See docs/specs/premium-endorsement-model.md for the new architecture.
|
|
10
|
+
*/
|
|
57
11
|
|
|
58
12
|
function register(program) {
|
|
59
|
-
const cmd = new Command('premium-pre')
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
const net = await provider.getNetwork();
|
|
66
|
-
const id = Number(net.chainId);
|
|
67
|
-
// Minimal mapping for common test/prod nets used here
|
|
68
|
-
if (id === 84532) return 'baseSepolia';
|
|
69
|
-
if (id === 8453) return 'base';
|
|
70
|
-
if (id === 1) return 'ethereum';
|
|
71
|
-
if (id === 137) return 'polygon';
|
|
72
|
-
if (id === 80001) return 'mumbai';
|
|
73
|
-
if (id === 11155111) return 'sepolia';
|
|
74
|
-
} catch(_) {}
|
|
75
|
-
return process.env.LIT_CHAIN || 'baseSepolia';
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
cmd
|
|
79
|
-
.command('doctor')
|
|
80
|
-
.description('Diagnose PRE/Lit v7 setup and manifests (chain key, tokenId decimal, conditions)')
|
|
81
|
-
.option('--cid <manifestCID>', 'Manifest CID to inspect (optional)')
|
|
82
|
-
.option('--chain <litChain>', 'Expected Lit chain key (default: infer from RPC or env)')
|
|
83
|
-
.option('--json', 'Output JSON', false)
|
|
84
|
-
.action(async (opts)=>{
|
|
85
|
-
try {
|
|
86
|
-
const out = { ok: true, notes: [], warnings: [], manifest: null };
|
|
87
|
-
const WM = require('../wallet-manager'); const wm = new WM(); await wm.connect();
|
|
88
|
-
const provider = wm.getProvider();
|
|
89
|
-
const litChain = await inferLitChain(provider, opts.chain);
|
|
90
|
-
out.notes.push(`Lit chain: ${litChain}`);
|
|
91
|
-
const net = await provider.getNetwork(); out.notes.push(`RPC chainId: ${Number(net.chainId)}`);
|
|
92
|
-
if (opts.cid) {
|
|
93
|
-
const IPFSManager = require('../ipfs-manager');
|
|
94
|
-
const ipfs = new IPFSManager(); await ipfs.initialize();
|
|
95
|
-
const manifest = await ipfs.downloadJson(opts.cid);
|
|
96
|
-
out.manifest = { cid: opts.cid };
|
|
97
|
-
if (!manifest?.pre || manifest.pre.provider !== 'lit') {
|
|
98
|
-
out.ok = false; out.warnings.push('Manifest missing PRE payload (pre.provider!=lit)');
|
|
99
|
-
} else {
|
|
100
|
-
const { normalizeCidV1Base32 } = require('../utils/cid');
|
|
101
|
-
const { ethers } = require('ethers');
|
|
102
|
-
const encCid = manifest.encryptedCID;
|
|
103
|
-
const acc = manifest.pre.evmContractConditions;
|
|
104
|
-
let chain = manifest.pre.chain || '';
|
|
105
|
-
if (chain === 'base-sepolia') { out.warnings.push('Legacy chain key base-sepolia found; should be baseSepolia'); chain = 'baseSepolia'; }
|
|
106
|
-
if (chain !== litChain) out.warnings.push(`Chain mismatch: manifest=${manifest.pre.chain} vs expected=${litChain}`);
|
|
107
|
-
// tokenId decimal check
|
|
108
|
-
const tok = String(manifest.pre.tokenId || '').trim();
|
|
109
|
-
if (!tok || /^0x[0-9a-fA-F]+$/.test(tok)) {
|
|
110
|
-
out.ok = false; out.warnings.push('pre.tokenId must be decimal for Lit v7. Republish to store decimal tokenId.');
|
|
111
|
-
}
|
|
112
|
-
// expected id derived from encryptedCID (CIDv1 base32 then keccak256 of utf8)
|
|
113
|
-
if (encCid) {
|
|
114
|
-
const tokenIdHex = ethers.keccak256(ethers.toUtf8Bytes(normalizeCidV1Base32(encCid)));
|
|
115
|
-
const tokenIdDecExpected = BigInt(tokenIdHex).toString();
|
|
116
|
-
if (tok && tok !== tokenIdDecExpected) {
|
|
117
|
-
out.warnings.push(`tokenId mismatch: manifest=${tok}, expected=${tokenIdDecExpected} (derived from encryptedCID)`);
|
|
118
|
-
}
|
|
119
|
-
} else {
|
|
120
|
-
out.warnings.push('Manifest missing encryptedCID');
|
|
121
|
-
}
|
|
122
|
-
// ACC param normalization
|
|
123
|
-
if (Array.isArray(acc) && acc[0]?.functionName === 'balanceOf') {
|
|
124
|
-
const p = acc[0].functionParams || [];
|
|
125
|
-
const tokParam = String(p[1] || '');
|
|
126
|
-
if (/^0x[0-9a-fA-F]+$/.test(tokParam)) out.warnings.push('Access condition tokenId should be decimal, not hex');
|
|
127
|
-
if (acc[0].chain === 'base-sepolia') out.warnings.push('Access condition chain should be baseSepolia');
|
|
128
|
-
} else {
|
|
129
|
-
out.warnings.push('evmContractConditions missing or not in expected balanceOf format');
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
if (opts.json) { process.stdout.write(JSON.stringify(out, null, 2) + '\n'); return; }
|
|
134
|
-
console.log('🔍 PRE Doctor');
|
|
135
|
-
out.notes.forEach(n => console.log(' -', n));
|
|
136
|
-
if (out.manifest) console.log(' - Manifest CID:', out.manifest.cid);
|
|
137
|
-
if (out.warnings.length) {
|
|
138
|
-
console.log('\nWarnings:'); out.warnings.forEach(w => console.log(' -', w));
|
|
139
|
-
}
|
|
140
|
-
console.log('\nTips: ensure decimal pre.tokenId, chain=baseSepolia, and session signatures enabled.');
|
|
141
|
-
} catch (e) { console.error('❌ premium-pre doctor failed:', e.message); process.exit(1); }
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
cmd
|
|
145
|
-
.command('publish')
|
|
146
|
-
.description('Encrypt content; save AES key with Lit using ERC-1155 receipt gating; store PRE payload in manifest')
|
|
147
|
-
.requiredOption('--in <path>', 'Path to plaintext file')
|
|
148
|
-
.requiredOption('--name <name>', 'Name')
|
|
149
|
-
.requiredOption('--description <text>', 'Description')
|
|
150
|
-
.requiredOption('--subdao <address>', 'SubDAO to receive proceeds (for price registration via existing premium create flow)')
|
|
151
|
-
.requiredOption('--price <usdc>', 'Price in USDC (6dp)')
|
|
152
|
-
.option('--receipt <address>', 'PremiumReceipt address (defaults to PREMIUM_RECEIPT_ADDRESS)', process.env.PREMIUM_RECEIPT_ADDRESS)
|
|
153
|
-
.option('--chain <name>', 'Lit EVM chain identifier (e.g., base-sepolia). Defaults inferred from connected RPC')
|
|
154
|
-
.requiredOption('--auth-sig <path>', 'Path to Lit AuthSig JSON (SIWE signature)')
|
|
155
|
-
.option('--no-auto-self-delegate', 'Do not attempt auto self-delegation on the voting token when preparing a proposal')
|
|
156
|
-
.option('--lit-network <name>', 'Lit network: serrano (default) | datil-test | datil-dev', process.env.LIT_NETWORK || 'serrano')
|
|
157
|
-
.action(async (opts) => {
|
|
158
|
-
try {
|
|
159
|
-
const fs = require('fs'); const path = require('path');
|
|
160
|
-
const IPFSManager = require('../ipfs-manager');
|
|
161
|
-
const { encryptAesGcm, randomBytes, toB64: b64 } = require('../utils/aes');
|
|
162
|
-
const { normalizeCidV1Base32 } = require('../utils/cid');
|
|
163
|
-
const ipfs = new IPFSManager(); await ipfs.initialize();
|
|
164
|
-
|
|
165
|
-
if (!opts.receipt) throw new Error('PREMIUM_RECEIPT_ADDRESS not set; pass --receipt');
|
|
166
|
-
const receipt = ethers.getAddress(opts.receipt);
|
|
167
|
-
|
|
168
|
-
const content = fs.readFileSync(path.resolve(opts.in), 'utf8');
|
|
169
|
-
const key = randomBytes(32);
|
|
170
|
-
const { ciphertext, iv, tag } = encryptAesGcm(content, key);
|
|
171
|
-
const encPayload = { type: 'sage-premium-encrypted', enc: 'aes-256-gcm', name: opts.name, description: opts.description, iv: b64(iv), tag: b64(tag), ciphertext: b64(ciphertext) };
|
|
172
|
-
const cipherCID = await ipfs.uploadJson(encPayload, `${opts.name}.encrypted`);
|
|
173
|
-
// Derive stable tokenId from encryptedCID to avoid self-referential manifest CID loops
|
|
174
|
-
const tokenIdHex = ethers.keccak256(ethers.toUtf8Bytes(normalizeCidV1Base32(cipherCID)));
|
|
175
|
-
// Convert to decimal for Lit Protocol compatibility
|
|
176
|
-
const tokenId = BigInt(tokenIdHex).toString();
|
|
177
|
-
const manifest = { type: 'sage-premium-manifest-pre', version: '1.0.0', encryptedCID: cipherCID, cipher: 'aes-256-gcm', name: opts.name, description: opts.description };
|
|
178
|
-
|
|
179
|
-
// PRE: save AES key with Lit under ERC-1155 gating
|
|
180
|
-
const { LitNodeClient, decryptToUint8Array } = requireLit();
|
|
181
|
-
let litNetworkPub = opts.litNetwork || process.env.LIT_NETWORK || 'serrano';
|
|
182
|
-
if (litNetworkPub === 'jalapeno') { console.log('⚠️ Lit network "jalapeno" is deprecated; using "serrano".'); litNetworkPub = 'serrano'; }
|
|
183
|
-
const client = new LitNodeClient({ litNetwork: litNetworkPub });
|
|
184
|
-
await client.connect();
|
|
185
|
-
// Back-compat: AuthSig file may still be provided. We'll prefer sessionSigs below.
|
|
186
|
-
const hasAuthSigFile = !!opts.authSig;
|
|
187
|
-
const authSig = hasAuthSigFile ? loadAuthSig(opts.authSig) : null;
|
|
188
|
-
|
|
189
|
-
// Infer Lit chain from connected provider if not provided
|
|
190
|
-
const wm = new (require('../wallet-manager'))(); await wm.connect();
|
|
191
|
-
const provider = wm.getProvider();
|
|
192
|
-
const litChain = await inferLitChain(provider, opts.chain);
|
|
193
|
-
|
|
194
|
-
// Upload a preliminary manifest (not used for id derivation anymore)
|
|
195
|
-
const manifestCID = await ipfs.uploadJson(manifest, `${opts.name}.manifest`);
|
|
196
|
-
|
|
197
|
-
// evmContractConditions for ERC-1155 balanceOf(address,uint256) > 0
|
|
198
|
-
const evmContractConditions = [
|
|
199
|
-
{
|
|
200
|
-
contractAddress: receipt,
|
|
201
|
-
functionName: 'balanceOf',
|
|
202
|
-
functionParams: [':userAddress', tokenId],
|
|
203
|
-
functionAbi: { name: 'balanceOf', type: 'function', stateMutability: 'view', inputs: [{ name: 'account', type: 'address' }, { name: 'id', type: 'uint256' }], outputs: [{ name: '', type: 'uint256' }] },
|
|
204
|
-
chain: litChain,
|
|
205
|
-
returnValueTest: { key: '', comparator: '>', value: '0' }
|
|
206
|
-
}
|
|
207
|
-
];
|
|
208
|
-
|
|
209
|
-
// Build a session for Lit v7 using session signatures (preferred over AuthSig)
|
|
210
|
-
// Resource must reflect both ACC hash and the private data hash; derive via client helper
|
|
211
|
-
const symmetricKey = new Uint8Array(key);
|
|
212
|
-
const resource = await client.getLitResourceForEncryption({ evmContractConditions, chain: litChain, dataToEncrypt: symmetricKey });
|
|
13
|
+
const cmd = new Command('premium-pre')
|
|
14
|
+
.description('Premium PRE encryption - use "sage personal" instead')
|
|
15
|
+
.action(() => {
|
|
16
|
+
console.log(`
|
|
17
|
+
Premium PRE (Proxy Re-Encryption)
|
|
213
18
|
|
|
214
|
-
|
|
215
|
-
const sessionSigs = await client.getSessionSigs({
|
|
216
|
-
chain: 'ethereum',
|
|
217
|
-
resourceAbilityRequests: [
|
|
218
|
-
{
|
|
219
|
-
resource,
|
|
220
|
-
ability: 'access-control-condition-decryption'
|
|
221
|
-
}
|
|
222
|
-
],
|
|
223
|
-
authNeededCallback: async ({ uri, expiration, resources, nonce, domain }) => {
|
|
224
|
-
const signer = wm.getSigner();
|
|
225
|
-
const address = await signer.getAddress();
|
|
226
|
-
const siweDomain = domain || process.env.LIT_SIWE_DOMAIN || 'localhost';
|
|
227
|
-
const siweUri = uri || 'lit:session';
|
|
228
|
-
const issuedAt = new Date().toISOString();
|
|
229
|
-
const chainId = 1; // ethereum for SIWE
|
|
230
|
-
const statement = 'Authorize access to Sage premium content via Lit';
|
|
231
|
-
const lines = [
|
|
232
|
-
`${siweDomain} wants you to sign in with your Ethereum account:`,
|
|
233
|
-
`${address}`,
|
|
234
|
-
'',
|
|
235
|
-
`${statement}`,
|
|
236
|
-
'',
|
|
237
|
-
`URI: ${siweUri}`,
|
|
238
|
-
`Version: 1`,
|
|
239
|
-
`Chain ID: ${chainId}`,
|
|
240
|
-
`Nonce: ${nonce}`,
|
|
241
|
-
`Issued At: ${issuedAt}`,
|
|
242
|
-
`Expiration Time: ${expiration}`
|
|
243
|
-
];
|
|
244
|
-
if (Array.isArray(resources) && resources.length) {
|
|
245
|
-
lines.push('Resources:');
|
|
246
|
-
for (const r of resources) lines.push(`- ${r}`);
|
|
247
|
-
}
|
|
248
|
-
const message = lines.join('\n');
|
|
249
|
-
const sig = await signer.signMessage(message);
|
|
250
|
-
return { sig, derivedVia: 'web3.eth.personal.sign', signedMessage: message, address };
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
const { encryptUint8Array } = requireLit();
|
|
255
|
-
|
|
256
|
-
// v7 API: encrypt the symmetric key with access conditions
|
|
257
|
-
// Note: param name must be `dataToEncrypt` (not `data`)
|
|
258
|
-
const encResp = await encryptUint8Array({
|
|
259
|
-
evmContractConditions,
|
|
260
|
-
chain: litChain,
|
|
261
|
-
// Prefer sessionSigs; fall back to authSig if explicitly provided
|
|
262
|
-
...(sessionSigs ? { sessionSigs } : {}),
|
|
263
|
-
...(sessionSigs ? {} : (authSig ? { authSig } : {})),
|
|
264
|
-
dataToEncrypt: symmetricKey
|
|
265
|
-
}, client);
|
|
266
|
-
// encResp: { ciphertext, dataToEncryptHash }
|
|
267
|
-
const eskB64 = encResp.ciphertext;
|
|
268
|
-
const dataToEncryptHash = encResp.dataToEncryptHash;
|
|
269
|
-
|
|
270
|
-
// Write final manifest with PRE payload reference
|
|
271
|
-
manifest.pre = {
|
|
272
|
-
provider: 'lit',
|
|
273
|
-
version: '1.0.0',
|
|
274
|
-
chain: litChain,
|
|
275
|
-
receipt,
|
|
276
|
-
tokenId,
|
|
277
|
-
evmContractConditions,
|
|
278
|
-
encryptedSymmetricKey: eskB64,
|
|
279
|
-
dataToEncryptHash
|
|
280
|
-
};
|
|
281
|
-
const manifestFinalCID = await ipfs.uploadJson(manifest, `${opts.name}.manifest`);
|
|
282
|
-
|
|
283
|
-
// Register price / prompt via existing premium contract
|
|
284
|
-
const promptsAddr = process.env.PREMIUM_PROMPTS_ADDRESS; if (!promptsAddr) throw new Error('PREMIUM_PROMPTS_ADDRESS not set');
|
|
285
|
-
const signer = wm.getSigner(); const providerRO = wm.getProvider();
|
|
286
|
-
const price = ethers.parseUnits(String(opts.price), 6);
|
|
287
|
-
const ifaceV2 = new ethers.Interface(['function createPremiumPromptWithManifest(bytes32,address,uint256,string)']);
|
|
288
|
-
// Register using cidHash = tokenIdHex (derived from encryptedCID) to match ERC-1155 gating
|
|
289
|
-
const cidHash = tokenIdHex;
|
|
290
|
-
const data = ifaceV2.encodeFunctionData('createPremiumPromptWithManifest', [cidHash, ethers.getAddress(opts.subdao), price, manifestFinalCID]);
|
|
291
|
-
let sent = false;
|
|
292
|
-
try {
|
|
293
|
-
// Preflight: check MANAGER role (robust to contracts that revert on view)
|
|
294
|
-
const prRO = new ethers.Contract(promptsAddr, ['function hasRole(bytes32,address) view returns (bool)'], providerRO);
|
|
295
|
-
const MR = ethers.id('MANAGER_ROLE'); // keccak256("MANAGER_ROLE")
|
|
296
|
-
const me = await signer.getAddress();
|
|
297
|
-
let hasMgr = false;
|
|
298
|
-
try { hasMgr = await prRO.hasRole(MR, me); } catch(_) { hasMgr = false; }
|
|
299
|
-
if (!hasMgr) throw new Error('NO_MANAGER');
|
|
300
|
-
const tx = await signer.sendTransaction({ to: promptsAddr, data });
|
|
301
|
-
console.log('🧾 createPremiumPromptWithManifest tx:', tx.hash);
|
|
302
|
-
sent = true;
|
|
303
|
-
} catch (e) {
|
|
304
|
-
const msg = String(e?.message||'');
|
|
305
|
-
if (msg.includes('closed') || msg.includes('NO_MANAGER') || msg.includes('execution reverted')) {
|
|
306
|
-
console.log('ℹ️ Creator path closed or missing MANAGER_ROLE. Falling back to governance proposal.');
|
|
307
|
-
// Propose via GovernanceManager to ensure tuple is cached for reliable queue/execute
|
|
308
|
-
const GM = require('../governance-manager');
|
|
309
|
-
const gm = new GM();
|
|
310
|
-
await gm.initialize(opts.subdao || null);
|
|
311
|
-
|
|
312
|
-
// Preflight: ensure proposer meets threshold; auto self-delegate on voting token if needed
|
|
313
|
-
try {
|
|
314
|
-
const { ethers } = require('ethers');
|
|
315
|
-
const provider = gm.provider; const signer = gm.signer;
|
|
316
|
-
const me = await signer.getAddress();
|
|
317
|
-
// Resolve governor address for minimal ABI calls
|
|
318
|
-
const govAddr = (gm.governor.getAddress && typeof gm.governor.getAddress === 'function')
|
|
319
|
-
? await gm.governor.getAddress() : gm.governor.target;
|
|
320
|
-
const govView = new ethers.Contract(govAddr, [
|
|
321
|
-
'function proposalThreshold() view returns (uint256)',
|
|
322
|
-
'function token() view returns (address)'
|
|
323
|
-
], provider);
|
|
324
|
-
let votingToken = ethers.ZeroAddress;
|
|
325
|
-
try { votingToken = await govView.token(); } catch(_) { votingToken = ethers.ZeroAddress; }
|
|
326
|
-
if (votingToken === ethers.ZeroAddress) {
|
|
327
|
-
// Fallback to SubDAO stake token (common for GOVERNANCE/HYBRID modes)
|
|
328
|
-
try {
|
|
329
|
-
const sub = new ethers.Contract(ethers.getAddress(opts.subdao), ['function stakeToken() view returns (address)'], provider);
|
|
330
|
-
votingToken = await sub.stakeToken();
|
|
331
|
-
} catch(_) {}
|
|
332
|
-
}
|
|
333
|
-
// Read threshold and current votes
|
|
334
|
-
let threshold = 0n; let votes = 0n;
|
|
335
|
-
try { threshold = await govView.proposalThreshold(); } catch(_) { threshold = 0n; }
|
|
336
|
-
if (votingToken && votingToken !== ethers.ZeroAddress) {
|
|
337
|
-
const vAbi = [
|
|
338
|
-
'function getVotes(address) view returns (uint256)',
|
|
339
|
-
'function delegates(address) view returns (address)',
|
|
340
|
-
'function delegate(address)'
|
|
341
|
-
];
|
|
342
|
-
const vt = new ethers.Contract(votingToken, vAbi, signer);
|
|
343
|
-
try { votes = await vt.getVotes(me); } catch(_) { votes = 0n; }
|
|
344
|
-
if (threshold > 0n && votes < threshold) {
|
|
345
|
-
// Auto self-delegate unless disabled via flag
|
|
346
|
-
if (opts.autoSelfDelegate !== false) {
|
|
347
|
-
try {
|
|
348
|
-
let curDel = ethers.ZeroAddress;
|
|
349
|
-
try { curDel = await vt.delegates(me); } catch(_) { curDel = ethers.ZeroAddress; }
|
|
350
|
-
if (String(curDel).toLowerCase() !== me.toLowerCase()) {
|
|
351
|
-
console.log('🪪 Auto self-delegating voting power on token', votingToken);
|
|
352
|
-
const dtx = await vt.delegate(me);
|
|
353
|
-
console.log('⏳ delegate tx:', dtx.hash);
|
|
354
|
-
await dtx.wait();
|
|
355
|
-
// Re-check votes post-delegation
|
|
356
|
-
try { votes = await vt.getVotes(me); } catch(_) {}
|
|
357
|
-
console.log('🗳️ Votes after delegation:', require('ethers').formatEther(votes));
|
|
358
|
-
}
|
|
359
|
-
} catch (delErr) {
|
|
360
|
-
console.log('⚠️ Auto self-delegation skipped/failed:', String(delErr?.message||delErr));
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
// Final advisory if still under threshold
|
|
364
|
-
if (votes < threshold) {
|
|
365
|
-
const fmt = (x)=>require('ethers').formatEther(x);
|
|
366
|
-
console.log('❗ Proposer preflight: insufficient voting power.');
|
|
367
|
-
console.log(' votes=', fmt(votes), ' threshold=', fmt(threshold));
|
|
368
|
-
console.log(' Hint: stake into the SubDAO and self-delegate, or use timelock schedule-call.');
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
} catch (pfErr) {
|
|
373
|
-
// Non-fatal; continue to attempt proposal (Governor will enforce)
|
|
374
|
-
console.log('⚠️ Preflight check failed (continuing):', String(pfErr?.message||pfErr));
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const targets = [promptsAddr]; const values = [0n]; const calldatas = [data];
|
|
378
|
-
const desc = `Register premium prompt via PRE (Lit): ${opts.name}`;
|
|
379
|
-
|
|
380
|
-
// If after preflight+auto-delegate we still have insufficient votes, offer Timelock route now
|
|
381
|
-
try {
|
|
382
|
-
const { ethers } = require('ethers');
|
|
383
|
-
const provider = gm.provider; const signer = gm.signer; const me = await signer.getAddress();
|
|
384
|
-
const govAddr = (gm.governor.getAddress && typeof gm.governor.getAddress === 'function') ? await gm.governor.getAddress() : gm.governor.target;
|
|
385
|
-
const govView = new ethers.Contract(govAddr, ['function proposalThreshold() view returns (uint256)','function token() view returns (address)'], provider);
|
|
386
|
-
let threshold = 0n; try { threshold = await govView.proposalThreshold(); } catch(_) {}
|
|
387
|
-
if (threshold > 0n) {
|
|
388
|
-
let votingToken = ethers.ZeroAddress;
|
|
389
|
-
try { votingToken = await govView.token(); } catch(_) {}
|
|
390
|
-
if (votingToken === ethers.ZeroAddress) {
|
|
391
|
-
try { const sub = new ethers.Contract(ethers.getAddress(opts.subdao), ['function stakeToken() view returns (address)'], provider); votingToken = await sub.stakeToken(); } catch(_) {}
|
|
392
|
-
}
|
|
393
|
-
let votes = 0n; if (votingToken && votingToken !== ethers.ZeroAddress) { try { votes = await new ethers.Contract(votingToken, ['function getVotes(address) view returns (uint256)'], provider).getVotes(me); } catch(_) {} }
|
|
394
|
-
if (votes < threshold) {
|
|
395
|
-
const inquirer = (require('inquirer').default || require('inquirer'));
|
|
396
|
-
const { route } = await inquirer.prompt([
|
|
397
|
-
{ type:'list', name:'route', message:'Insufficient voting power. Choose how to proceed:', choices:[
|
|
398
|
-
{ name:'Schedule via Timelock now (recommended)', value:'timelock' },
|
|
399
|
-
{ name:'Attempt creating a proposal anyway', value:'proposal' },
|
|
400
|
-
{ name:'Abort (I will stake/delegate first)', value:'abort' }
|
|
401
|
-
]}
|
|
402
|
-
]);
|
|
403
|
-
if (route === 'timelock') {
|
|
404
|
-
try {
|
|
405
|
-
const tl = gm.timelock; if (!tl) throw new Error('Timelock not resolved');
|
|
406
|
-
const tlAddr = (tl.getAddress && typeof tl.getAddress === 'function') ? await tl.getAddress() : tl.target;
|
|
407
|
-
const PROPOSER = await tl.PROPOSER_ROLE();
|
|
408
|
-
const has = await tl.hasRole(PROPOSER, me).catch(()=>false);
|
|
409
|
-
if (!has) throw new Error('Current signer lacks PROPOSER_ROLE on Timelock');
|
|
410
|
-
const delay = await tl.getMinDelay().catch(()=>0n);
|
|
411
|
-
const salt = ethers.hexlify(ethers.randomBytes(32));
|
|
412
|
-
const predecessor = ethers.ZeroHash;
|
|
413
|
-
console.log('⏳ Scheduling via Timelock...', { timelock: tlAddr, target: promptsAddr, delay: String(delay) });
|
|
414
|
-
const stx = await tl.schedule(promptsAddr, 0, data, predecessor, salt, delay);
|
|
415
|
-
console.log('📨 schedule tx:', stx.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, stx, Number(process.env.SAGE_TX_WAIT_MS || 60000)); } console.log('✅ Scheduled');
|
|
416
|
-
if (delay === 0n) {
|
|
417
|
-
try { const etx = await tl.execute(promptsAddr, 0, data, predecessor, salt); console.log('⚡ execute tx:', etx.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, etx, Number(process.env.SAGE_TX_WAIT_MS || 60000)); } console.log('✅ Executed'); }
|
|
418
|
-
catch(exErr) { console.log('⚠️ Execute failed or not ready:', String(exErr?.message||exErr)); }
|
|
419
|
-
} else {
|
|
420
|
-
console.log('ℹ️ Execute after delay:');
|
|
421
|
-
console.log(` sage timelock execute --subdao ${ethers.getAddress(opts.subdao)} --target ${promptsAddr} --value 0 --data ${data} --predecessor 0x${'0'.repeat(64)} --salt ${salt}`);
|
|
422
|
-
}
|
|
423
|
-
// Output identifiers and exit early (timelock route)
|
|
424
|
-
console.log('🔗 Identifiers:');
|
|
425
|
-
console.log(' encryptedCID:', cipherCID);
|
|
426
|
-
console.log(' tokenId :', tokenId);
|
|
427
|
-
console.log(' cidHash :', cidHash);
|
|
428
|
-
console.log(' manifestCID :', manifestFinalCID);
|
|
429
|
-
console.log(JSON.stringify({ manifestCID: manifestFinalCID, encryptedCID: cipherCID, cidHash, route: 'timelock', timelock: tlAddr }, null, 2));
|
|
430
|
-
return;
|
|
431
|
-
} catch (timErr) {
|
|
432
|
-
console.log('❌ Timelock scheduling failed:', String(timErr?.message||timErr));
|
|
433
|
-
console.log(' You can retry manually with:');
|
|
434
|
-
console.log(` sage timelock schedule-call --subdao ${ethers.getAddress(opts.subdao)} --to ${promptsAddr} --sig 'createPremiumPromptWithManifest(bytes32,address,uint256,string)' --args '${cidHash},${ethers.getAddress(opts.subdao)},${price.toString()},${manifestFinalCID}'`);
|
|
435
|
-
}
|
|
436
|
-
} else if (route === 'abort') {
|
|
437
|
-
throw new Error('Aborted by user due to insufficient voting power.');
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
} catch(_) {}
|
|
442
|
-
try {
|
|
443
|
-
const pid = await gm.createProposal(targets, values, calldatas, desc);
|
|
444
|
-
console.log('🗳️ proposal id:', pid);
|
|
445
|
-
try { const { execSync } = require('child_process'); console.log(`➡️ Watching proposal ${pid} for queue/execute...`); console.log(execSync(['node','packages/cli/src/index.js','governance','watch',String(pid),'--subdao',String(opts.subdao)].join(' '), { encoding:'utf8' })); } catch(_) {}
|
|
446
|
-
} catch (propErr) {
|
|
447
|
-
const em = String(propErr?.message||'');
|
|
448
|
-
if (em.includes('Operator mode detected') || em.toLowerCase().includes('not a timelock proposer')) {
|
|
449
|
-
console.log('ℹ️ Operator mode detected; scheduling on Timelock.');
|
|
450
|
-
const { resolveGovContext } = require('../utils/gov-context');
|
|
451
|
-
const ctx2 = await resolveGovContext({ govOpt: null, subdaoOpt: opts.subdao, provider: providerRO });
|
|
452
|
-
const tlAddr = ctx2.timelock || process.env.TIMELOCK || process.env.TIMELOCK_ADDRESS;
|
|
453
|
-
if (!tlAddr) throw new Error('Timelock not resolved for SubDAO');
|
|
454
|
-
const tlIface = new ethers.Interface(['function schedule(address,uint256,bytes,bytes32,bytes32,uint256)','function execute(address,uint256,bytes,bytes32,bytes32)','function getMinDelay() view returns (uint256)']);
|
|
455
|
-
const tl = new ethers.Contract(tlAddr, tlIface, signer);
|
|
456
|
-
const predecessor = ethers.ZeroHash; const salt = ethers.hexlify(ethers.randomBytes(32));
|
|
457
|
-
const delay = await tl.getMinDelay().catch(()=>0n);
|
|
458
|
-
const stx = await tl.schedule(promptsAddr, 0, data, predecessor, salt, delay);
|
|
459
|
-
console.log('⏳ Timelock schedule tx:', stx.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, stx, Number(process.env.SAGE_TX_WAIT_MS || 60000)); } console.log('✅ Scheduled');
|
|
460
|
-
if (delay === 0n) { const etx = await tl.execute(promptsAddr, 0, data, predecessor, salt); console.log('⚡ execute tx:', etx.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, etx, Number(process.env.SAGE_TX_WAIT_MS || 60000)); } console.log('✅ Executed'); }
|
|
461
|
-
} else { throw propErr; }
|
|
462
|
-
}
|
|
463
|
-
// Fall through to final JSON output
|
|
464
|
-
} else {
|
|
465
|
-
throw e;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
console.log('🔗 Identifiers:');
|
|
469
|
-
console.log(' encryptedCID:', cipherCID);
|
|
470
|
-
console.log(' tokenId :', tokenId);
|
|
471
|
-
console.log(' cidHash :', cidHash);
|
|
472
|
-
console.log(' manifestCID :', manifestFinalCID);
|
|
473
|
-
console.log(JSON.stringify({ manifestCID: manifestFinalCID, encryptedCID: cipherCID, cidHash, route: sent?'direct':'governance', pre: { provider: 'lit', chain: litChain, receipt, tokenId, encryptedSymmetricKey: eskB64 } }, null, 2));
|
|
474
|
-
} catch (e) { console.error('❌ premium-pre publish failed:', e.message); process.exit(1); }
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
cmd
|
|
478
|
-
.command('manifest')
|
|
479
|
-
.description('Resolve the manifest CID for a PRE (Lit) premium from subgraph/logs/cache or raw calldata')
|
|
480
|
-
.option('--proposal <id>', 'Proposal ID that registered the premium (uses ./.sage/proposals.json)')
|
|
481
|
-
.option('--gov <address>', 'Governor address (overrides --subdao/context)')
|
|
482
|
-
.option('--subdao <address>', 'SubDAO address to derive Governor from')
|
|
483
|
-
.option('--calldata <0xdata>', 'Raw calldata to decode (skips lookups)')
|
|
484
|
-
.option('--cid-hash <hex>', 'cidHash (bytes32 hex) to search via subgraph/logs')
|
|
485
|
-
.option('--subgraph <url>', 'Subgraph URL (defaults to SUBGRAPH_URL env)')
|
|
486
|
-
.option('--prompts <address>', 'PremiumPrompts contract address for logs fallback')
|
|
487
|
-
.option('--from <block>', 'Logs scan start block (default: current-100000)')
|
|
488
|
-
.option('--json', 'Output JSON', false)
|
|
489
|
-
.action(async (opts) => {
|
|
490
|
-
try {
|
|
491
|
-
const fs = require('fs'); const path = require('path'); const axios = require('axios');
|
|
492
|
-
let dataHex = null; let used = '';
|
|
493
|
-
if (opts.calldata && String(opts.calldata).startsWith('0x')) {
|
|
494
|
-
dataHex = String(opts.calldata); used = 'calldata';
|
|
495
|
-
} else {
|
|
496
|
-
// 1) Subgraph fast-path
|
|
497
|
-
const subgraphUrl = String(opts.subgraph || process.env.SUBGRAPH_URL || '').trim();
|
|
498
|
-
if (subgraphUrl && (opts['cid-hash'] || opts.subdao)) {
|
|
499
|
-
try {
|
|
500
|
-
if (opts['cid-hash']) {
|
|
501
|
-
const q = { query: 'query($id: ID!){ premiumPrompt(id:$id){ id manifestCID subdao price } }', variables: { id: String(opts['cid-hash']).toLowerCase() } };
|
|
502
|
-
const { data } = await axios.post(subgraphUrl, q, { headers: { 'content-type': 'application/json' } });
|
|
503
|
-
const manifestCID = data?.data?.premiumPrompt?.manifestCID || null;
|
|
504
|
-
if (manifestCID) { if (opts.json) console.log(JSON.stringify({ manifestCID, source: 'subgraph:id' }, null, 2)); else console.log('Manifest CID:', manifestCID); return; }
|
|
505
|
-
} else if (opts.subdao) {
|
|
506
|
-
const q = { query: 'query($sub: Bytes!){ premiumPrompts(where:{ subdao:$sub }, orderBy:blockTimestamp, orderDirection: desc, first: 20){ id manifestCID subdao price } }', variables: { sub: String(opts.subdao).toLowerCase() } };
|
|
507
|
-
const { data } = await axios.post(subgraphUrl, q, { headers: { 'content-type': 'application/json' } });
|
|
508
|
-
const list = data?.data?.premiumPrompts || [];
|
|
509
|
-
if (list.length) { if (opts.json) console.log(JSON.stringify({ results: list, source: 'subgraph:subdao' }, null, 2)); else { console.log('Premium prompts (latest 20):'); for (const it of list) console.log(`- cidHash: ${it.id} manifestCID: ${it.manifestCID || 'n/a'} price: ${it.price}`); } return; }
|
|
510
|
-
}
|
|
511
|
-
} catch (_) { /* fallthrough */ }
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// 2) Logs fallback if prompts address provided
|
|
515
|
-
if (opts.prompts) {
|
|
516
|
-
const { ethers } = require('ethers');
|
|
517
|
-
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
|
|
518
|
-
const ifaceEv = new ethers.Interface(['event PremiumPromptCreatedV2(bytes32 indexed cidHash, address indexed subdao, uint256 price, string manifestCID)']);
|
|
519
|
-
const topic0 = ifaceEv.getEvent('PremiumPromptCreatedV2').topicHash;
|
|
520
|
-
const topics = [topic0];
|
|
521
|
-
if (opts['cid-hash']) topics.push(ethers.zeroPadValue(String(opts['cid-hash']), 32));
|
|
522
|
-
const current = await provider.getBlockNumber();
|
|
523
|
-
const fromBlock = opts.from ? Number(opts.from) : Math.max(0, current - 100000);
|
|
524
|
-
const logs = await provider.getLogs({ address: ethers.getAddress(opts.prompts), topics, fromBlock, toBlock: 'latest' });
|
|
525
|
-
if (logs.length) {
|
|
526
|
-
const last = logs[logs.length-1];
|
|
527
|
-
const parsed = ifaceEv.parseLog(last);
|
|
528
|
-
const manifestCID = String(parsed?.args?.manifestCID || '');
|
|
529
|
-
if (manifestCID) { if (opts.json) console.log(JSON.stringify({ manifestCID, source: `logs:${opts.prompts}` }, null, 2)); else console.log('Manifest CID:', manifestCID); return; }
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// 3) Proposals cache fallback
|
|
534
|
-
const { ethers } = require('ethers');
|
|
535
|
-
const { resolveGovContext } = require('../utils/gov-context');
|
|
536
|
-
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
|
|
537
|
-
const ctx = await resolveGovContext({ govOpt: opts.gov, subdaoOpt: opts.subdao, provider });
|
|
538
|
-
const govAddr = (ctx.governor || process.env.GOV || '').toLowerCase();
|
|
539
|
-
const cachePath = path.join(process.cwd(), '.sage', 'proposals.json');
|
|
540
|
-
if (fs.existsSync(cachePath) && govAddr) {
|
|
541
|
-
const cache = JSON.parse(fs.readFileSync(cachePath, 'utf8')||'{}');
|
|
542
|
-
const entries = cache[govAddr] || {};
|
|
543
|
-
let pid = opts.proposal ? String(opts.proposal) : null;
|
|
544
|
-
if (!pid) { const keys = Object.keys(entries); if (keys.length) pid = keys[keys.length-1]; }
|
|
545
|
-
const t = entries[pid || ''] || null;
|
|
546
|
-
if (t && Array.isArray(t.calldatas) && t.calldatas.length) { dataHex = String(t.calldatas[0]); used = `cache:${ctx.governor}:${pid}`; }
|
|
547
|
-
}
|
|
548
|
-
if (!dataHex) throw new Error('No source available. Provide --calldata, or set SUBGRAPH_URL and use --cid-hash/--subdao, or pass --prompts for logs.');
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// Decode calldata as createPremiumPromptWithManifest(bytes32,address,uint256,string)
|
|
552
|
-
const { ethers: E } = require('ethers');
|
|
553
|
-
const iface = new E.Interface(['function createPremiumPromptWithManifest(bytes32,address,uint256,string)']);
|
|
554
|
-
let cid = null;
|
|
555
|
-
try { const decoded = iface.decodeFunctionData('createPremiumPromptWithManifest', dataHex); cid = String(decoded?.[3] || ''); }
|
|
556
|
-
catch (_) {
|
|
557
|
-
try { const buf = Buffer.from(dataHex.replace(/^0x/, ''), 'hex'); const s = buf.toString('utf8'); const m = s.match(/Qm[1-9A-HJ-NP-Za-km-z]{44,}/); if (m) cid = m[0]; } catch (_) {}
|
|
558
|
-
}
|
|
559
|
-
if (!cid) throw new Error('Could not extract manifest CID from calldata');
|
|
560
|
-
if (opts.json) console.log(JSON.stringify({ manifestCID: cid, source: used || 'decoded-calldata' }, null, 2));
|
|
561
|
-
else console.log('Manifest CID:', cid);
|
|
562
|
-
} catch (e) { console.error('❌ manifest resolve failed:', e.message); process.exit(1); }
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
cmd
|
|
566
|
-
.command('view')
|
|
567
|
-
.description('For buyers: request key from Lit via ERC-1155 gating and decrypt locally (no centralized reveal)')
|
|
568
|
-
.requiredOption('--cid <manifestCID>', 'Manifest CID')
|
|
569
|
-
.option('--auth-sig <path>', 'Path to Lit AuthSig JSON (buyer wallet SIWE signature)')
|
|
570
|
-
.option('--auto-auth', 'Auto-generate AuthSig from a keystore that owns the receipt', false)
|
|
571
|
-
.option('--chain <litChain>', 'Override Lit chain key (e.g., baseSepolia)')
|
|
572
|
-
.action(async (opts) => {
|
|
573
|
-
try {
|
|
574
|
-
const IPFSManager = require('../ipfs-manager');
|
|
575
|
-
const { fromB64, decryptAesGcm } = require('../utils/aes');
|
|
576
|
-
const ipfs = new IPFSManager(); await ipfs.initialize();
|
|
577
|
-
const manifest = await ipfs.downloadJson(opts.cid);
|
|
578
|
-
if (!manifest?.encryptedCID || !manifest?.pre || manifest.pre.provider !== 'lit') throw new Error('Manifest missing PRE payload');
|
|
579
|
-
const enc = await ipfs.downloadJson(manifest.encryptedCID);
|
|
580
|
-
const eskB64 = manifest.pre.encryptedSymmetricKey;
|
|
581
|
-
const dataToEncryptHash = manifest.pre.dataToEncryptHash;
|
|
582
|
-
let evmContractConditions = manifest.pre.evmContractConditions;
|
|
583
|
-
let chain = opts.chain || manifest.pre.chain || process.env.LIT_CHAIN || 'baseSepolia';
|
|
584
|
-
// Normalize chain key used historically
|
|
585
|
-
if (chain === 'base-sepolia') chain = 'baseSepolia';
|
|
586
|
-
// Normalize tokenId param to decimal string for Lit nodes AND update chain in access conditions
|
|
587
|
-
try {
|
|
588
|
-
if (Array.isArray(evmContractConditions) && evmContractConditions.length) {
|
|
589
|
-
const cond = evmContractConditions[0];
|
|
590
|
-
let updatedCond = { ...cond };
|
|
591
|
-
|
|
592
|
-
// Update chain identifier to normalized version
|
|
593
|
-
if (updatedCond.chain === 'base-sepolia') {
|
|
594
|
-
updatedCond.chain = 'baseSepolia';
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// Update tokenId to decimal format
|
|
598
|
-
if (cond?.functionName === 'balanceOf' && Array.isArray(cond.functionParams) && cond.functionParams.length >= 2) {
|
|
599
|
-
const p1 = String(cond.functionParams[1] || '').trim();
|
|
600
|
-
if (/^0x[0-9a-fA-F]+$/.test(p1)) {
|
|
601
|
-
const dec = BigInt(p1).toString();
|
|
602
|
-
updatedCond.functionParams = [cond.functionParams[0], dec];
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
evmContractConditions = [updatedCond];
|
|
607
|
-
}
|
|
608
|
-
} catch (_) {}
|
|
609
|
-
// Strong preflight: confirm tokenId matches on-chain receipt id expected by purchase path
|
|
610
|
-
try {
|
|
611
|
-
const { ethers } = require('ethers');
|
|
612
|
-
const { normalizeCidV1Base32 } = require('../utils/cid');
|
|
613
|
-
const expectedId = ethers.keccak256(ethers.toUtf8Bytes(normalizeCidV1Base32(manifest.encryptedCID)));
|
|
614
|
-
if (manifest.pre.tokenId?.toLowerCase?.() !== expectedId.toLowerCase()) {
|
|
615
|
-
console.log('⚠️ Warning: Manifest tokenId does not match encryptedCID-derived id.');
|
|
616
|
-
console.log(' manifest.pre.tokenId:', manifest.pre.tokenId);
|
|
617
|
-
console.log(' expected:', expectedId);
|
|
618
|
-
console.log(' If decryption fails, republish so tokenId is derived from encryptedCID.');
|
|
619
|
-
}
|
|
620
|
-
} catch(_) {}
|
|
621
|
-
const { LitNodeClient, decryptToUint8Array } = requireLit();
|
|
622
|
-
let litNetworkView = opts.litNetwork || process.env.LIT_NETWORK || 'serrano';
|
|
623
|
-
if (litNetworkView === 'jalapeno') { console.log('⚠️ Lit network "jalapeno" is deprecated; using "serrano".'); litNetworkView = 'serrano'; }
|
|
624
|
-
const client = new LitNodeClient({ litNetwork: litNetworkView });
|
|
625
|
-
await client.connect();
|
|
626
|
-
let authSig;
|
|
627
|
-
if (opts.authSig) {
|
|
628
|
-
authSig = loadAuthSig(opts.authSig);
|
|
629
|
-
} else if (opts.autoAuth) {
|
|
630
|
-
// Find an address with receipt balance>0 and sign SIWE via Cast keystore
|
|
631
|
-
const { ethers } = require('ethers');
|
|
632
|
-
const receipt = manifest.pre.receipt; const tokenId = manifest.pre.tokenId;
|
|
633
|
-
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
|
|
634
|
-
const rcpt = new ethers.Contract(receipt, ['function balanceOf(address,uint256) view returns (uint256)'], provider);
|
|
635
|
-
const Cast = require('../cast-wallet-manager'); const cast = new Cast(); await cast.connect();
|
|
636
|
-
const ksDir = cast.keystoreDir; const fs = require('fs'); const path = require('path'); const { spawnSync } = require('child_process');
|
|
637
|
-
if (!fs.existsSync(ksDir)) throw new Error('No Cast keystores found. Provide --auth-sig <file>.');
|
|
638
|
-
const files = fs.readdirSync(ksDir);
|
|
639
|
-
let match = null;
|
|
640
|
-
for (const f of files) {
|
|
641
|
-
const p = path.join(ksDir, f);
|
|
642
|
-
const res = spawnSync(cast.castCommand, ['wallet', 'address', p], { encoding: 'utf8' });
|
|
643
|
-
const addr = (res.stdout || '').trim();
|
|
644
|
-
if (!addr) continue;
|
|
645
|
-
try { const bal = await rcpt.balanceOf(addr, tokenId); if (BigInt(bal.toString()) > 0n) { match = { addr, ks: p }; break; } } catch (_) {}
|
|
646
|
-
}
|
|
647
|
-
if (!match) throw new Error('No keystore address holds the required receipt. Generate an AuthSig for the buyer wallet.');
|
|
648
|
-
// Build SIWE
|
|
649
|
-
const net = await provider.getNetwork(); const chainId = Number(net.chainId);
|
|
650
|
-
const nonce = Math.random().toString(36).slice(2); const issuedAt = new Date().toISOString();
|
|
651
|
-
const message = [`localhost wants you to sign in with your Ethereum account:`, `${match.addr}`, '', `Authorize with Lit Protocol`, '', `URI: http://localhost`, `Version: 1`, `Chain ID: ${chainId}`, `Nonce: ${nonce}`, `Issued At: ${issuedAt}`].join('\n');
|
|
652
|
-
const pw = await cast.promptForPassword();
|
|
653
|
-
const sigRes = spawnSync(cast.castCommand, ['wallet', 'sign', '--keystore', match.ks, '--password', pw, message], { encoding: 'utf8' });
|
|
654
|
-
if (sigRes.status !== 0) throw new Error(sigRes.stderr || 'cast wallet sign failed');
|
|
655
|
-
authSig = { sig: (sigRes.stdout||'').trim(), derivedVia: 'cast.personal_sign', signedMessage: message, address: match.addr };
|
|
656
|
-
} else {
|
|
657
|
-
throw new Error('Provide --auth-sig <file> or use --auto-auth');
|
|
658
|
-
}
|
|
659
|
-
// Build session signatures for Lit v7 (preferred over AuthSig)
|
|
660
|
-
// Compute the resource manually: hashOfConditions/hashOfPrivateData, using the proper Resource class
|
|
661
|
-
const { hashEVMContractConditions } = require('@lit-protocol/access-control-conditions');
|
|
662
|
-
const { uint8arrayToString } = require('@lit-protocol/uint8arrays');
|
|
663
|
-
const { LitAccessControlConditionResource } = requireLitAuthHelpers();
|
|
664
|
-
const condHashBuf = await hashEVMContractConditions(evmContractConditions);
|
|
665
|
-
const condHash = uint8arrayToString(new Uint8Array(condHashBuf), 'base16');
|
|
666
|
-
const resource = new LitAccessControlConditionResource(`${condHash}/${dataToEncryptHash}`);
|
|
667
|
-
|
|
668
|
-
// Get wallet manager for session signatures
|
|
669
|
-
const WalletManager = require('../wallet-manager');
|
|
670
|
-
const wm = new WalletManager();
|
|
671
|
-
await wm.connect();
|
|
672
|
-
|
|
673
|
-
// Create session signatures by signing a SIWE message via our CLI signer
|
|
674
|
-
const sessionSigs = await client.getSessionSigs({
|
|
675
|
-
chain: 'ethereum',
|
|
676
|
-
resourceAbilityRequests: [
|
|
677
|
-
{
|
|
678
|
-
resource,
|
|
679
|
-
ability: 'access-control-condition-decryption'
|
|
680
|
-
}
|
|
681
|
-
],
|
|
682
|
-
authNeededCallback: async ({ uri, expiration, resources, nonce, domain }) => {
|
|
683
|
-
const signer = wm.getSigner();
|
|
684
|
-
const address = await signer.getAddress();
|
|
685
|
-
const siweDomain = domain || process.env.LIT_SIWE_DOMAIN || 'localhost';
|
|
686
|
-
const siweUri = uri || 'lit:session';
|
|
687
|
-
const issuedAt = new Date().toISOString();
|
|
688
|
-
const chainId = 1; // ethereum for SIWE
|
|
689
|
-
const statement = 'Authorize access to Sage premium content via Lit';
|
|
690
|
-
const lines = [
|
|
691
|
-
`${siweDomain} wants you to sign in with your Ethereum account:`,
|
|
692
|
-
`${address}`,
|
|
693
|
-
'',
|
|
694
|
-
`${statement}`,
|
|
695
|
-
'',
|
|
696
|
-
`URI: ${siweUri}`,
|
|
697
|
-
`Version: 1`,
|
|
698
|
-
`Chain ID: ${chainId}`,
|
|
699
|
-
`Nonce: ${nonce}`,
|
|
700
|
-
`Issued At: ${issuedAt}`,
|
|
701
|
-
`Expiration Time: ${expiration}`
|
|
702
|
-
];
|
|
703
|
-
if (Array.isArray(resources) && resources.length) {
|
|
704
|
-
lines.push('Resources:');
|
|
705
|
-
for (const r of resources) lines.push(`- ${r}`);
|
|
706
|
-
}
|
|
707
|
-
const message = lines.join('\n');
|
|
708
|
-
const sig = await signer.signMessage(message);
|
|
709
|
-
return { sig, derivedVia: 'web3.eth.personal.sign', signedMessage: message, address };
|
|
710
|
-
}
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
// Optional preflight: ensure buyer holds ERC-1155
|
|
714
|
-
try {
|
|
715
|
-
const { ethers } = require('ethers');
|
|
716
|
-
const receipt = manifest.pre.receipt; const tokenId = manifest.pre.tokenId;
|
|
717
|
-
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
|
|
718
|
-
const rcpt = new ethers.Contract(receipt, ['function balanceOf(address,uint256) view returns (uint256)'], provider);
|
|
719
|
-
const owner = (authSig.address || authSig.siwe?.address || '').toString();
|
|
720
|
-
if (owner && owner.startsWith('0x')) {
|
|
721
|
-
const bal = await rcpt.balanceOf(owner, tokenId).catch(()=>null);
|
|
722
|
-
if (bal !== null && BigInt(bal.toString()) === 0n) {
|
|
723
|
-
console.log('❌ AuthSig address does not own the required receipt for this manifest.');
|
|
724
|
-
console.log(' Address:', owner);
|
|
725
|
-
console.log(' TokenId:', tokenId);
|
|
726
|
-
console.log(' Hint: generate an AuthSig for the buyer wallet that holds the receipt.');
|
|
727
|
-
console.log(' Example: sage wallet use <0xBuyer>; sage premium-pre authsig --account <0xBuyer> --out ./authSig.buyer.json');
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
} catch(_) {}
|
|
731
|
-
|
|
732
|
-
// Try multiple tokenId formats and chain combinations for backward compatibility
|
|
733
|
-
const tokenId = manifest.pre.tokenId;
|
|
734
|
-
// Preserve original tokenId format from manifest for exact condition matching
|
|
735
|
-
const originalTokenId = String(tokenId);
|
|
736
|
-
// Also prepare decimal format for legacy compatibility
|
|
737
|
-
const decimalTokenId = (/^0x[0-9a-fA-F]+$/.test(originalTokenId)) ? BigInt(originalTokenId).toString() : originalTokenId;
|
|
738
|
-
const attempts = [
|
|
739
|
-
// Primary: baseSepolia + original tokenId format from manifest (preserves publishing format)
|
|
740
|
-
{ evmContractConditions: evmContractConditions.map(cond => ({
|
|
741
|
-
...cond,
|
|
742
|
-
functionParams: [cond.functionParams[0], originalTokenId] // Use exact tokenId format from manifest
|
|
743
|
-
})), chain: 'baseSepolia' },
|
|
744
|
-
// Fallback: baseSepolia + decimal tokenId (for legacy compatibility)
|
|
745
|
-
{ evmContractConditions, chain: 'baseSepolia' },
|
|
746
|
-
// Legacy: base-sepolia + original tokenId format
|
|
747
|
-
{ evmContractConditions: evmContractConditions.map(cond => ({
|
|
748
|
-
...cond,
|
|
749
|
-
functionParams: [cond.functionParams[0], originalTokenId],
|
|
750
|
-
chain: 'base-sepolia'
|
|
751
|
-
})), chain: 'base-sepolia' }
|
|
752
|
-
];
|
|
753
|
-
|
|
754
|
-
let symmetricKey = null;
|
|
755
|
-
let lastError = null;
|
|
756
|
-
|
|
757
|
-
for (let i = 0; i < attempts.length; i++) {
|
|
758
|
-
const attempt = attempts[i];
|
|
759
|
-
try {
|
|
760
|
-
console.log(`🔍 Attempt ${i + 1}/${attempts.length}: chain=${attempt.chain}, tokenId=${attempt.evmContractConditions[0].functionParams[1]}`);
|
|
761
|
-
|
|
762
|
-
// v7 API: decrypt using session signatures
|
|
763
|
-
const decryptedKey = await decryptToUint8Array(
|
|
764
|
-
{
|
|
765
|
-
evmContractConditions: attempt.evmContractConditions,
|
|
766
|
-
chain: attempt.chain,
|
|
767
|
-
ciphertext: eskB64,
|
|
768
|
-
dataToEncryptHash,
|
|
769
|
-
// Prefer sessionSigs; fall back to authSig if explicitly provided
|
|
770
|
-
...(sessionSigs ? { sessionSigs } : {}),
|
|
771
|
-
...(sessionSigs ? {} : (authSig ? { authSig } : {}))
|
|
772
|
-
},
|
|
773
|
-
client
|
|
774
|
-
);
|
|
775
|
-
|
|
776
|
-
symmetricKey = decryptedKey;
|
|
777
|
-
console.log(`✅ Success with attempt ${i + 1}`);
|
|
778
|
-
break;
|
|
779
|
-
} catch (err) {
|
|
780
|
-
lastError = err;
|
|
781
|
-
console.log(`❌ Attempt ${i + 1} failed: ${err.message}`);
|
|
782
|
-
if (i === attempts.length - 1) {
|
|
783
|
-
throw new Error(`All decryption attempts failed. Last error: ${lastError.message}`);
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
// symmetricKey is Uint8Array
|
|
789
|
-
const keyBytes = Buffer.from(symmetricKey);
|
|
790
|
-
const plaintext = decryptAesGcm(fromB64(enc.ciphertext), keyBytes, fromB64(enc.iv), fromB64(enc.tag));
|
|
791
|
-
console.log(plaintext);
|
|
792
|
-
} catch (e) { console.error('❌ premium-pre view failed:', e.message); process.exit(1); }
|
|
793
|
-
});
|
|
19
|
+
This command has been replaced by personal premium.
|
|
794
20
|
|
|
795
|
-
|
|
796
|
-
.command('authsig')
|
|
797
|
-
.description('Generate a Lit AuthSig (SIWE-style)')
|
|
798
|
-
.option('--out <path>', 'Output JSON file', './authSig.json')
|
|
799
|
-
.option('--domain <domain>', 'SIWE domain', 'localhost')
|
|
800
|
-
.option('--uri <uri>', 'SIWE URI', 'http://localhost')
|
|
801
|
-
.option('--statement <text>', 'SIWE statement', 'Authorize with Lit Protocol')
|
|
802
|
-
.option('--chain-id <id>', 'Chain ID for SIWE (defaults to connected RPC)')
|
|
803
|
-
.option('--account <address>', 'Explicit account address to sign with (Cast keystore)')
|
|
804
|
-
.option('--wallet-type <type>', 'Signer type: cast|pk (default: cast)', 'cast')
|
|
805
|
-
.option('--private-key <hex>', 'Private key (0x...) when --wallet-type=pk')
|
|
806
|
-
.action(async (opts) => {
|
|
807
|
-
try {
|
|
808
|
-
const fs = require('fs'); const path = require('path');
|
|
809
|
-
const { ethers } = require('ethers');
|
|
810
|
-
const rpcUrl = process.env.RPC_URL || 'https://base-sepolia.publicnode.com';
|
|
811
|
-
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
812
|
-
const net = await provider.getNetwork();
|
|
813
|
-
const chainId = opts['chain-id'] ? Number(opts['chain-id']) : Number(net.chainId);
|
|
21
|
+
For personal premium prompts:
|
|
814
22
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
const issuedAt = new Date().toISOString();
|
|
819
|
-
const message = [
|
|
820
|
-
`${opts.domain} wants you to sign in with your Ethereum account:`,
|
|
821
|
-
`${addr}`,
|
|
822
|
-
'',
|
|
823
|
-
`${opts.statement}`,
|
|
824
|
-
'',
|
|
825
|
-
`URI: ${opts.uri}`,
|
|
826
|
-
`Version: 1`,
|
|
827
|
-
`Chain ID: ${chainId}`,
|
|
828
|
-
`Nonce: ${nonce}`,
|
|
829
|
-
`Issued At: ${issuedAt}`
|
|
830
|
-
].join('\n');
|
|
831
|
-
return message;
|
|
832
|
-
};
|
|
23
|
+
sage personal sell <key> <price> --encrypt --file <path> # List encrypted content
|
|
24
|
+
sage personal buy <creator> <key> # Purchase license
|
|
25
|
+
sage personal access <creator> <key> # Decrypt purchased content
|
|
833
26
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
if (!opts.privateKey) throw new Error('Provide --private-key when using --wallet-type=pk');
|
|
837
|
-
const pk = opts.privateKey.startsWith('0x') ? opts.privateKey : ('0x' + opts.privateKey);
|
|
838
|
-
const signer = new ethers.Wallet(pk, provider);
|
|
839
|
-
address = await signer.getAddress();
|
|
840
|
-
const msg = buildSiwe(address);
|
|
841
|
-
signature = await signer.signMessage(msg);
|
|
842
|
-
derivedVia = 'pk.personal_sign';
|
|
843
|
-
const outPath = path.resolve(opts.out);
|
|
844
|
-
fs.writeFileSync(outPath, JSON.stringify({ sig: signature, derivedVia, signedMessage: msg, address }, null, 2));
|
|
845
|
-
console.log('✅ Wrote AuthSig to', outPath);
|
|
846
|
-
return;
|
|
847
|
-
}
|
|
27
|
+
Enable personal commands:
|
|
28
|
+
export SAGE_ENABLE_PERSONAL=1
|
|
848
29
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
await cast.connect();
|
|
853
|
-
let targetAddr = cast.account;
|
|
854
|
-
let keystorePath = null;
|
|
855
|
-
if (opts.account && opts.account.startsWith('0x')) {
|
|
856
|
-
// Try resolve a keystore file matching the requested account
|
|
857
|
-
const ksDir = cast.keystoreDir;
|
|
858
|
-
if (!fs.existsSync(ksDir)) throw new Error('Cast keystore directory not found');
|
|
859
|
-
const files = fs.readdirSync(ksDir);
|
|
860
|
-
const { spawnSync } = require('child_process');
|
|
861
|
-
for (const f of files) {
|
|
862
|
-
const p = path.join(ksDir, f);
|
|
863
|
-
const res = spawnSync(cast.castCommand, ['wallet', 'address', p], { encoding: 'utf8' });
|
|
864
|
-
const addr = (res.stdout || '').trim();
|
|
865
|
-
if (addr && addr.toLowerCase() === opts.account.toLowerCase()) { keystorePath = p; targetAddr = addr; break; }
|
|
866
|
-
}
|
|
867
|
-
if (!keystorePath) throw new Error('No keystore found for --account');
|
|
868
|
-
} else {
|
|
869
|
-
// Use saved keystore from cast manager
|
|
870
|
-
const saved = cast.loadCastWallet();
|
|
871
|
-
if (!saved?.keystorePath) throw new Error('No Cast wallet configured');
|
|
872
|
-
keystorePath = saved.keystorePath; targetAddr = saved.address;
|
|
873
|
-
}
|
|
874
|
-
const msg = buildSiwe(targetAddr);
|
|
875
|
-
// Prompt for password
|
|
876
|
-
const pw = await cast.promptForPassword();
|
|
877
|
-
const { spawnSync } = require('child_process');
|
|
878
|
-
const res = spawnSync(cast.castCommand, ['wallet', 'sign', '--keystore', keystorePath, '--password', pw, msg], { encoding: 'utf8' });
|
|
879
|
-
if (res.status !== 0) throw new Error(res.stderr || 'cast wallet sign failed');
|
|
880
|
-
signature = (res.stdout || '').trim(); derivedVia = 'cast.personal_sign';
|
|
881
|
-
const outPath = path.resolve(opts.out);
|
|
882
|
-
fs.writeFileSync(outPath, JSON.stringify({ sig: signature, derivedVia, signedMessage: msg, address: targetAddr }, null, 2));
|
|
883
|
-
console.log('✅ Wrote AuthSig to', outPath);
|
|
884
|
-
} catch (e) { console.error('❌ authsig failed:', e.message); process.exit(1); }
|
|
30
|
+
For more information:
|
|
31
|
+
docs/specs/premium-endorsement-model.md
|
|
32
|
+
`);
|
|
885
33
|
});
|
|
886
34
|
|
|
887
35
|
program.addCommand(cmd);
|