@sage-protocol/cli 0.3.0 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/doctor.js +87 -8
- package/dist/cli/commands/gov-config.js +81 -0
- package/dist/cli/commands/governance.js +152 -72
- package/dist/cli/commands/library.js +9 -0
- package/dist/cli/commands/proposals.js +187 -17
- package/dist/cli/commands/skills.js +175 -21
- package/dist/cli/commands/subdao.js +22 -2
- package/dist/cli/config/playbooks.json +15 -0
- package/dist/cli/governance-manager.js +25 -4
- package/dist/cli/index.js +5 -6
- package/dist/cli/library-manager.js +79 -0
- package/dist/cli/mcp-server-stdio.js +1655 -82
- package/dist/cli/schemas/manifest.schema.json +55 -0
- package/dist/cli/services/doctor/fixers.js +134 -0
- package/dist/cli/services/mcp/bulk-operations.js +272 -0
- package/dist/cli/services/mcp/dependency-analyzer.js +202 -0
- package/dist/cli/services/mcp/library-listing.js +2 -2
- package/dist/cli/services/mcp/local-prompt-collector.js +1 -0
- package/dist/cli/services/mcp/manifest-downloader.js +5 -3
- package/dist/cli/services/mcp/manifest-fetcher.js +17 -1
- package/dist/cli/services/mcp/manifest-workflows.js +127 -15
- package/dist/cli/services/mcp/quick-start.js +287 -0
- package/dist/cli/services/mcp/stdio-runner.js +30 -5
- package/dist/cli/services/mcp/template-manager.js +156 -0
- package/dist/cli/services/mcp/templates/default-templates.json +84 -0
- package/dist/cli/services/mcp/tool-args-validator.js +66 -0
- package/dist/cli/services/mcp/trending-formatter.js +1 -1
- package/dist/cli/services/mcp/unified-prompt-search.js +2 -2
- package/dist/cli/services/metaprompt/designer.js +12 -5
- package/dist/cli/services/subdao/applier.js +208 -196
- package/dist/cli/services/subdao/planner.js +41 -6
- package/dist/cli/subdao-manager.js +14 -0
- package/dist/cli/utils/aliases.js +17 -2
- package/dist/cli/utils/contract-error-decoder.js +61 -0
- package/dist/cli/utils/suggestions.js +17 -12
- package/package.json +3 -2
- package/src/schemas/manifest.schema.json +55 -0
|
@@ -7,211 +7,223 @@ const FactoryABI = require('../../utils/factory-abi');
|
|
|
7
7
|
const { resolveArtifact } = require('../../utils/artifacts');
|
|
8
8
|
|
|
9
9
|
async function applyPlan(planOrPath) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
console.log(`Applying Plan: ${plan.config.name} (${plan.playbook.name})`);
|
|
19
|
-
|
|
20
|
-
// Instantiate manager
|
|
21
|
-
const manager = new SubDAOManager();
|
|
22
|
-
const WalletManager = require('../../wallet-manager');
|
|
23
|
-
const wm = new WalletManager();
|
|
24
|
-
await wm.connect();
|
|
25
|
-
manager.signer = wm.getSigner();
|
|
26
|
-
manager.walletManager = wm;
|
|
27
|
-
await manager.initialize();
|
|
28
|
-
const provider = wm.getProvider();
|
|
29
|
-
|
|
30
|
-
// Idempotency Check: Check if SubDAO with this name is already registered in Factory
|
|
31
|
-
// Or locally in subdao store.
|
|
32
|
-
// The Factory doesn't expose `isSubDAO(name)`. It exposes `isSubDAO(address)`.
|
|
33
|
-
// But we can't know address until we deploy.
|
|
34
|
-
// Best effort: check local profiles for name collision?
|
|
35
|
-
// Or rely on user?
|
|
36
|
-
// "Preflight: if a SubDAO with same name/owner exists, skip create" - User Requirement
|
|
37
|
-
// Factory events scan is too slow.
|
|
38
|
-
// We will warn if we see a similar entry in .sage/subdaos.json.
|
|
39
|
-
// And/Or if user passed --subdao override? No, create generates one.
|
|
40
|
-
// We will skip this heavy scan and rely on "check-then-set" via create2 if possible, but Factory uses create.
|
|
41
|
-
// For Operator mode, we can check if the Safe is already an admin of a SubDAO? Hard.
|
|
42
|
-
// Let's implement a simple check: if file "subdao-plan-<timestamp>.json" exists? No.
|
|
43
|
-
// User: "if a SubDAO with same name/owner exists".
|
|
44
|
-
// We will skip if we find a local alias with same name.
|
|
45
|
-
const SubDAOStore = require('../../utils/subdao-store');
|
|
46
|
-
const store = new SubDAOStore();
|
|
47
|
-
const existing = store.list().find(s => s.alias === plan.config.name);
|
|
48
|
-
if (existing) {
|
|
49
|
-
console.warn(`⚠️ SubDAO with name "${plan.config.name}" already exists locally (${existing.address}).`);
|
|
50
|
-
// We proceed but warn.
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Stamping: Pin Playbook to IPFS
|
|
54
|
-
console.log('📌 Stamping Playbook to IPFS...');
|
|
55
|
-
let playbookCid = '';
|
|
56
|
-
try {
|
|
57
|
-
const ipfs = new IpfsManager();
|
|
58
|
-
const res = await ipfs.upload(plan.playbook);
|
|
59
|
-
playbookCid = res.cid;
|
|
60
|
-
console.log(` CID: ${playbookCid}`);
|
|
61
|
-
} catch (e) {
|
|
62
|
-
console.warn(`⚠️ Failed to pin playbook: ${e.message}`);
|
|
63
|
-
// Fail fast if provenance is critical? User said "No IPFS stamping... Fix: Stamping".
|
|
64
|
-
// We should probably fail or retry.
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Convert accessModel string to int
|
|
68
|
-
const accessMap = { 'free': 0, 'governance': 1, 'hybrid': 2 };
|
|
69
|
-
const accessModel = accessMap[plan.config.accessModel || 'governance'] || 1;
|
|
70
|
-
|
|
71
|
-
// Creation options
|
|
72
|
-
const creationOpts = {
|
|
73
|
-
quorumPercentage: plan.config.quorumBps ? BigInt(plan.config.quorumBps) / 100n : 4n,
|
|
74
|
-
votingPeriodBlocks: plan.config.votingPeriodBlocks,
|
|
75
|
-
proposalThreshold: plan.config.proposalThreshold,
|
|
76
|
-
forkingPolicy: plan.config.forkingPolicy,
|
|
77
|
-
membershipPolicy: plan.playbook.governance === 'operator' ? 'admin' : 'stake',
|
|
78
|
-
profileCid: playbookCid // Stamp CID into profileCid field of SubDAO
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
if (plan.playbook.governance === 'operator') {
|
|
82
|
-
console.log('🏗️ Creating Operator/Team SubDAO...');
|
|
83
|
-
|
|
84
|
-
const factory = new ethers.Contract(manager.factoryAddress, FactoryABI, manager.signer);
|
|
85
|
-
|
|
86
|
-
// Validate Factory Support (ABI check)
|
|
87
|
-
if (typeof factory.createSubDAOOperator !== 'function') {
|
|
88
|
-
throw new Error('Factory does not support createSubDAOOperator. Upgrade factory or use Community playbook.');
|
|
10
|
+
let plan;
|
|
11
|
+
if (typeof planOrPath === 'string') {
|
|
12
|
+
const content = fs.readFileSync(planOrPath, 'utf8');
|
|
13
|
+
plan = JSON.parse(content);
|
|
14
|
+
} else {
|
|
15
|
+
plan = planOrPath;
|
|
89
16
|
}
|
|
90
17
|
|
|
91
|
-
|
|
92
|
-
let executor = plan.config.owners;
|
|
93
|
-
if (!executor) executor = await manager.signer.getAddress();
|
|
94
|
-
|
|
95
|
-
// If comma separated, warn and pick first.
|
|
96
|
-
if (executor.includes(',')) {
|
|
97
|
-
console.warn('⚠️ Multiple owners provided. Operator mode requires a SINGLE executor address (Safe or EOA).');
|
|
98
|
-
const parts = executor.split(',');
|
|
99
|
-
executor = parts[0].trim();
|
|
100
|
-
console.warn(` Using first address: ${executor}`);
|
|
101
|
-
}
|
|
18
|
+
console.log(`Applying Plan: ${plan.config.name} (${plan.playbook.name})`);
|
|
102
19
|
|
|
103
|
-
//
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
20
|
+
// Instantiate manager
|
|
21
|
+
const manager = new SubDAOManager();
|
|
22
|
+
const WalletManager = require('../../wallet-manager');
|
|
23
|
+
const wm = new WalletManager();
|
|
24
|
+
await wm.connect();
|
|
25
|
+
manager.signer = wm.getSigner();
|
|
26
|
+
manager.walletManager = wm;
|
|
27
|
+
await manager.initialize();
|
|
28
|
+
const provider = wm.getProvider();
|
|
29
|
+
|
|
30
|
+
// Idempotency Check: Check if SubDAO with this name is already registered in Factory
|
|
31
|
+
// Or locally in subdao store.
|
|
32
|
+
const store = require('../../utils/subdao-store');
|
|
33
|
+
const existing = (store.listFavorites?.() || []).find(f => (f.alias || '').toLowerCase() === String(plan.config.name || '').toLowerCase());
|
|
34
|
+
if (existing) {
|
|
35
|
+
console.warn(`⚠️ SubDAO with name "${plan.config.name}" already exists locally (${existing.address}).`);
|
|
36
|
+
// We proceed but warn.
|
|
116
37
|
}
|
|
117
38
|
|
|
118
|
-
|
|
119
|
-
|
|
39
|
+
// Stamping: Pin Playbook to IPFS
|
|
40
|
+
console.log('📌 Stamping Playbook to IPFS...');
|
|
41
|
+
let playbookCid = '';
|
|
120
42
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// OR use createSubDAOOperatorWithParams if available?
|
|
126
|
-
// Assuming standard factory.
|
|
127
|
-
|
|
128
|
-
const tx = await factory.createSubDAOOperator(
|
|
129
|
-
plan.config.name,
|
|
130
|
-
plan.config.description,
|
|
131
|
-
accessModel,
|
|
132
|
-
executor, // executor (Timelock role)
|
|
133
|
-
executor, // admin (SubDAO admin)
|
|
134
|
-
plan.config.minStake || 0,
|
|
135
|
-
plan.config.burnAmount || 0
|
|
136
|
-
);
|
|
137
|
-
console.log('⏳ Waiting for tx:', tx.hash);
|
|
138
|
-
const receipt = await tx.wait();
|
|
139
|
-
|
|
140
|
-
// Parse logs to find SubDAO address
|
|
141
|
-
// Event: SubDAOCreated(address indexed subDAO, ...)
|
|
142
|
-
// We reuse SubDAOManager logic or parse manually
|
|
143
|
-
let subdaoAddr = null;
|
|
144
|
-
for (const log of receipt.logs) {
|
|
145
|
-
try {
|
|
146
|
-
const parsed = factory.interface.parseLog(log);
|
|
147
|
-
if (parsed.name === 'SubDAOCreated') {
|
|
148
|
-
subdaoAddr = parsed.args[0];
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
} catch(e){}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (subdaoAddr) {
|
|
155
|
-
console.log(`✅ SubDAO created: ${subdaoAddr}`);
|
|
156
|
-
|
|
157
|
-
// Post-Create Stamping (if CID exists)
|
|
158
|
-
if (playbookCid) {
|
|
159
|
-
console.log('📌 Applying Playbook Stamp (Profile CID)...');
|
|
160
|
-
// We need to call setProfileCid via the Admin.
|
|
161
|
-
// If WE (signer) are not the admin (Safe is), we can't directly set it.
|
|
162
|
-
// We must PROPOSE it to the Safe if we are an owner.
|
|
163
|
-
// Or assume user will do it.
|
|
164
|
-
// "Fix: ...stamp the playbook manifest CID into SubDAO metadata."
|
|
165
|
-
// If executor == Safe, we can't easily set it now synchronously.
|
|
166
|
-
// We warn and output the instruction.
|
|
167
|
-
|
|
168
|
-
// Check if setProfileCid exists on SubDAO contract
|
|
169
|
-
const subdao = new ethers.Contract(subdaoAddr, ['function setProfileCid(string)'], manager.signer);
|
|
170
|
-
// Checking function existence without call is hard on ethers v6 without ABI check or call.
|
|
171
|
-
// We can try to estimateGas or call static.
|
|
172
|
-
// Or check code? No.
|
|
173
|
-
// Check if ABI fragment matches?
|
|
174
|
-
// If standard SubDAO doesn't have it, we skip.
|
|
175
|
-
// Let's try to see if `setProfileCid` is in the contract by calling it? No, that reverts.
|
|
176
|
-
// We rely on the artifact/ABI we used.
|
|
177
|
-
// If the artifact has it, we try. If it reverts (method missing), we catch.
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
if (typeof subdao.setProfileCid === 'function') {
|
|
181
|
-
if (executor.toLowerCase() === (await manager.signer.getAddress()).toLowerCase()) {
|
|
182
|
-
// We are admin
|
|
183
|
-
const tx2 = await subdao.setProfileCid(playbookCid);
|
|
184
|
-
await tx2.wait();
|
|
185
|
-
console.log('✅ Stamped.');
|
|
186
|
-
} else {
|
|
187
|
-
console.warn('⚠️ Cannot stamp profileCid automatically (Admin is Safe/Other).');
|
|
188
|
-
console.warn(` Please propose call: setProfileCid("${playbookCid}") on ${subdaoAddr}`);
|
|
189
|
-
}
|
|
190
|
-
} else {
|
|
191
|
-
console.warn('⚠️ SubDAO contract does not support setProfileCid.');
|
|
192
|
-
}
|
|
193
|
-
} catch (e) {
|
|
194
|
-
console.warn('⚠️ Failed to stamp profileCid (likely not supported by contract):', e.message);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
43
|
+
const ipfs = new IpfsManager();
|
|
44
|
+
const res = await ipfs.uploadJson(plan.playbook, 'playbook');
|
|
45
|
+
playbookCid = res;
|
|
46
|
+
console.log(` CID: ${playbookCid}`);
|
|
199
47
|
} catch (e) {
|
|
200
|
-
console.
|
|
201
|
-
|
|
48
|
+
console.warn(`⚠️ Failed to pin playbook: ${e.message}`);
|
|
49
|
+
// Fail fast if provenance is critical? User said "No IPFS stamping... Fix: Stamping".
|
|
50
|
+
// We should probably fail or retry.
|
|
202
51
|
}
|
|
203
52
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
plan.config.
|
|
211
|
-
plan.config.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
53
|
+
// Convert accessModel string to int
|
|
54
|
+
const accessMap = { 'free': 0, 'governance': 1, 'hybrid': 2 };
|
|
55
|
+
const accessModel = accessMap[plan.config.accessModel || 'governance'] || 1;
|
|
56
|
+
|
|
57
|
+
// Creation options
|
|
58
|
+
const creationOpts = {
|
|
59
|
+
quorumPercentage: plan.config.quorumBps ? BigInt(plan.config.quorumBps) / 100n : 4n,
|
|
60
|
+
votingPeriodBlocks: plan.config.votingPeriodBlocks,
|
|
61
|
+
proposalThreshold: plan.config.proposalThreshold || '0',
|
|
62
|
+
forkingPolicy: plan.config.forkingPolicy || 'gov_only',
|
|
63
|
+
membershipPolicy: plan.playbook.governance === 'operator' ? 'admin' : 'stake',
|
|
64
|
+
profileCid: playbookCid // Stamp CID into profileCid field of SubDAO
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (plan.playbook.governance === 'operator') {
|
|
68
|
+
console.log('🏗️ Creating Operator/Team SubDAO...');
|
|
69
|
+
|
|
70
|
+
const factory = new ethers.Contract(manager.factoryAddress, FactoryABI, manager.signer);
|
|
71
|
+
|
|
72
|
+
// Validate Factory Support (ABI check)
|
|
73
|
+
if (typeof factory.createSubDAOOperator !== 'function') {
|
|
74
|
+
throw new Error('Factory does not support createSubDAOOperator. Upgrade factory or use Community playbook.');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Resolve Executor (Safe)
|
|
78
|
+
let executor = plan.config.owners;
|
|
79
|
+
if (!executor) executor = await manager.signer.getAddress();
|
|
80
|
+
|
|
81
|
+
// If comma separated, warn and pick first.
|
|
82
|
+
if (executor.includes(',')) {
|
|
83
|
+
console.warn('⚠️ Multiple owners provided. Operator mode requires a SINGLE executor address (Safe or EOA).');
|
|
84
|
+
const parts = executor.split(',');
|
|
85
|
+
executor = parts[0].trim();
|
|
86
|
+
console.warn(` Using first address: ${executor}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Enforce Safe (Code Check)
|
|
90
|
+
const code = await provider.getCode(executor);
|
|
91
|
+
if (code === '0x') {
|
|
92
|
+
// EOA.
|
|
93
|
+
console.warn('⚠️ Executor is an EOA (External Account), not a Safe Contract.');
|
|
94
|
+
console.warn(' "Squad" playbook implies a Safe. Ensure you trust this single key.');
|
|
95
|
+
// User requirement: "Require a Safe address... or a created Safe".
|
|
96
|
+
// If playbook is 'squad', we MUST have a Safe.
|
|
97
|
+
if (plan.playbook.id === 'squad') {
|
|
98
|
+
throw new Error('Squad playbook requires a Safe contract address. Use "sage safe create" (if available) or create at app.safe.global and provide via --owners.');
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
console.log('✅ Executor is a Contract (Likely Safe).');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log(` Executor: ${executor}`);
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
// Check for createSubDAOOperatorWithParams (Prioritize Factory Path)
|
|
108
|
+
const hasNewOperatorCreate = factory.interface.fragments.some(
|
|
109
|
+
f => f.name === 'createSubDAOOperatorWithParams'
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
let tx;
|
|
113
|
+
if (hasNewOperatorCreate) {
|
|
114
|
+
console.log('✨ Using factory.createSubDAOOperatorWithParams (stamping supported)...');
|
|
115
|
+
tx = await factory.createSubDAOOperatorWithParams(
|
|
116
|
+
plan.config.name,
|
|
117
|
+
plan.config.description,
|
|
118
|
+
accessModel,
|
|
119
|
+
executor, // executor
|
|
120
|
+
executor, // admin
|
|
121
|
+
plan.config.minStake || 0,
|
|
122
|
+
plan.config.burnAmount || 0,
|
|
123
|
+
playbookCid || ''
|
|
124
|
+
);
|
|
125
|
+
} else {
|
|
126
|
+
console.log('⚠️ Factory does not support atomic stamping. Using legacy createSubDAOOperator...');
|
|
127
|
+
tx = await factory.createSubDAOOperator(
|
|
128
|
+
plan.config.name,
|
|
129
|
+
plan.config.description,
|
|
130
|
+
accessModel,
|
|
131
|
+
executor, // executor
|
|
132
|
+
executor, // admin
|
|
133
|
+
plan.config.minStake || 0,
|
|
134
|
+
plan.config.burnAmount || 0
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log('⏳ Waiting for tx:', tx.hash);
|
|
139
|
+
const receipt = await tx.wait();
|
|
140
|
+
|
|
141
|
+
// Parse logs to find SubDAO address
|
|
142
|
+
let subdaoAddr = null;
|
|
143
|
+
for (const log of receipt.logs) {
|
|
144
|
+
try {
|
|
145
|
+
const parsed = factory.interface.parseLog(log);
|
|
146
|
+
if (parsed.name === 'SubDAOCreated') {
|
|
147
|
+
subdaoAddr = parsed.args[0];
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
} catch (e) { }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (subdaoAddr) {
|
|
154
|
+
console.log(`✅ SubDAO created: ${subdaoAddr}`);
|
|
155
|
+
|
|
156
|
+
// Post-Create Stamping (only if NOT stamped via params)
|
|
157
|
+
if (playbookCid && !hasNewOperatorCreate) {
|
|
158
|
+
console.log('📌 Applying Playbook Stamp (Profile CID)...');
|
|
159
|
+
|
|
160
|
+
const subdao = new ethers.Contract(subdaoAddr, ['function setProfileCid(string)'], manager.signer);
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
// Check if we are the admin (executor)
|
|
164
|
+
const myAddr = await manager.signer.getAddress();
|
|
165
|
+
if (executor.toLowerCase() === myAddr.toLowerCase()) {
|
|
166
|
+
// We are admin
|
|
167
|
+
console.log(' Directly calling setProfileCid...');
|
|
168
|
+
const tx2 = await subdao.setProfileCid(playbookCid);
|
|
169
|
+
await tx2.wait();
|
|
170
|
+
console.log('✅ Stamped.');
|
|
171
|
+
} else {
|
|
172
|
+
console.warn('⚠️ Cannot stamp profileCid automatically (Admin is Safe/Other).');
|
|
173
|
+
|
|
174
|
+
// Generate Safe Payload (Post-Create Fallback)
|
|
175
|
+
const iface = new ethers.Interface(['function setProfileCid(string)']);
|
|
176
|
+
const calldata = iface.encodeFunctionData('setProfileCid', [playbookCid]);
|
|
177
|
+
|
|
178
|
+
const safeBatch = {
|
|
179
|
+
version: "1.0",
|
|
180
|
+
chainId: (await provider.getNetwork()).chainId.toString(),
|
|
181
|
+
createdAt: Date.now(),
|
|
182
|
+
meta: { name: "Stamp Playbook CID", description: `Set profileCid to ${playbookCid}` },
|
|
183
|
+
transactions: [{
|
|
184
|
+
to: subdaoAddr,
|
|
185
|
+
value: "0",
|
|
186
|
+
data: calldata,
|
|
187
|
+
operation: 0 // Call
|
|
188
|
+
}]
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const filename = `safe-tx-stamp-${Date.now()}.json`;
|
|
192
|
+
fs.writeFileSync(filename, JSON.stringify(safeBatch, null, 2));
|
|
193
|
+
|
|
194
|
+
console.log(`\n📋 Safe Transaction Payload saved to ${filename}`);
|
|
195
|
+
console.log(JSON.stringify(safeBatch, null, 2));
|
|
196
|
+
console.log('\n Use this JSON in the Safe Transaction Builder to stamp the playbook.');
|
|
197
|
+
}
|
|
198
|
+
} catch (e) {
|
|
199
|
+
console.warn('⚠️ Failed to stamp profileCid:', e.message);
|
|
200
|
+
console.warn(' This SubDAO contract version might not support setProfileCid.');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
} catch (e) {
|
|
206
|
+
console.error('❌ Operator creation failed:', e.message);
|
|
207
|
+
throw e;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
} else {
|
|
211
|
+
// Token Governance (Community)
|
|
212
|
+
// Community playbook uses createSubDAO (legacy) or createSubDAOWithParams (new) logic.
|
|
213
|
+
// If token mode, we usually rely on `createSubDAO` which calls internal logic.
|
|
214
|
+
// But check if we can stamp there too.
|
|
215
|
+
// The `creationOpts` already has `profileCid`.
|
|
216
|
+
// If SubDAOManager.createSubDAO supports it, we are good.
|
|
217
|
+
|
|
218
|
+
await manager.createSubDAO(
|
|
219
|
+
plan.config.name,
|
|
220
|
+
plan.config.description,
|
|
221
|
+
accessModel,
|
|
222
|
+
plan.config.minStake || '0',
|
|
223
|
+
plan.config.burnAmount || '0',
|
|
224
|
+
creationOpts // Ensure manager uses this
|
|
225
|
+
);
|
|
226
|
+
}
|
|
215
227
|
}
|
|
216
228
|
|
|
217
229
|
module.exports = { applyPlan };
|
|
@@ -2,6 +2,8 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const inquirer = require('inquirer');
|
|
4
4
|
const playbooksService = require('../governance/playbooks');
|
|
5
|
+
const { ethers } = require('ethers');
|
|
6
|
+
const config = require('../../config');
|
|
5
7
|
|
|
6
8
|
function parseDurationToBlocks(durationStr, blockTimeSec = 12) {
|
|
7
9
|
if (!durationStr) return Math.ceil(3 * 24 * 3600 / blockTimeSec); // Default ~3 days
|
|
@@ -27,6 +29,25 @@ function parseDurationToBlocks(durationStr, blockTimeSec = 12) {
|
|
|
27
29
|
return Math.ceil(seconds / blockTimeSec);
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
async function detectBlockTimeSec(rpcUrl) {
|
|
33
|
+
try {
|
|
34
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
35
|
+
const latest = await provider.getBlockNumber();
|
|
36
|
+
const older = latest > 100 ? latest - 100 : (latest > 1 ? 1 : 0);
|
|
37
|
+
const [bLatest, bOlder] = await Promise.all([
|
|
38
|
+
provider.getBlock(latest),
|
|
39
|
+
provider.getBlock(older),
|
|
40
|
+
]);
|
|
41
|
+
if (!bLatest || !bOlder || !bLatest.timestamp || !bOlder.timestamp) return 12;
|
|
42
|
+
const dt = Number(bLatest.timestamp) - Number(bOlder.timestamp);
|
|
43
|
+
if (dt <= 0) return 12;
|
|
44
|
+
const sec = Math.max(1, Math.round(dt / Math.max(1, latest - older)));
|
|
45
|
+
return sec;
|
|
46
|
+
} catch (_) {
|
|
47
|
+
return 12;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
30
51
|
async function planSubDAO(options) {
|
|
31
52
|
// 1. Select Playbook
|
|
32
53
|
let playbookId = options.playbook;
|
|
@@ -69,6 +90,13 @@ async function planSubDAO(options) {
|
|
|
69
90
|
questions.push({ type: 'input', name: 'minStake', message: 'Minimum Stake (SXXX):', default: '0' });
|
|
70
91
|
}
|
|
71
92
|
|
|
93
|
+
// Governance tuning (overrides) — prompt only in wizard mode or when overrides are missing
|
|
94
|
+
if (options.wizard) {
|
|
95
|
+
if (!options.votingPeriod) questions.push({ type: 'input', name: 'votingPeriod', message: 'Voting period (e.g., 7 days):', default: (playbook.params?.votingPeriod || '') });
|
|
96
|
+
if (!options.quorumBps) questions.push({ type: 'input', name: 'quorumBps', message: 'Quorum (bps, e.g., 200 = 2%):', default: (playbook.params?.quorumBps ?? '') });
|
|
97
|
+
if (!options.proposalThreshold) questions.push({ type: 'input', name: 'proposalThreshold', message: 'Proposal threshold (string/wei):', default: (playbook.params?.proposalThreshold || '') });
|
|
98
|
+
}
|
|
99
|
+
|
|
72
100
|
const answers = questions.length > 0 ? await inquirer.prompt(questions) : {};
|
|
73
101
|
|
|
74
102
|
const merged = { ...options, ...answers };
|
|
@@ -87,18 +115,25 @@ async function planSubDAO(options) {
|
|
|
87
115
|
name: merged.name,
|
|
88
116
|
description: merged.description,
|
|
89
117
|
accessModel: 'governance',
|
|
90
|
-
minStake: merged.minStake,
|
|
118
|
+
minStake: merged.minStake || '0',
|
|
91
119
|
owners: merged.owners,
|
|
92
120
|
threshold: merged.threshold,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
121
|
+
// Default to 1000 SXXX (minimum burn) if not provided
|
|
122
|
+
burnAmount: merged.burnAmount || '1000000000000000000000',
|
|
123
|
+
proposalThreshold: merged.proposalThreshold || playbook.params?.proposalThreshold || '0',
|
|
124
|
+
votingPeriod: merged.votingPeriod || playbook.params?.votingPeriod || '3 days',
|
|
125
|
+
quorumBps: merged.quorumBps != null && merged.quorumBps !== '' ? Number(merged.quorumBps) : (playbook.params?.quorumBps ?? 400),
|
|
96
126
|
forkingPolicy: 'gov_only',
|
|
97
127
|
}
|
|
98
128
|
};
|
|
99
129
|
|
|
100
|
-
// Normalize voting period to blocks with override
|
|
101
|
-
|
|
130
|
+
// Normalize voting period to blocks with override or auto-detect
|
|
131
|
+
let blockTime = 12;
|
|
132
|
+
if (options.blockTimeSec) blockTime = parseInt(options.blockTimeSec, 10);
|
|
133
|
+
else {
|
|
134
|
+
const rpc = config.resolveRpcUrl ? config.resolveRpcUrl() : (process.env.RPC_URL || 'https://base-sepolia.publicnode.com');
|
|
135
|
+
blockTime = await detectBlockTimeSec(rpc);
|
|
136
|
+
}
|
|
102
137
|
plan.config.votingPeriodBlocks = parseDurationToBlocks(plan.config.votingPeriod, blockTime).toString();
|
|
103
138
|
|
|
104
139
|
return plan;
|
|
@@ -15,6 +15,7 @@ const GAS_FLAGS = '--legacy --gas-limit 3000000 --gas-price 2gwei';
|
|
|
15
15
|
const { resolvePermitOrApprove, normalizePermit, zeroPermit } = require('./utils/permit');
|
|
16
16
|
const { ethers } = require('ethers');
|
|
17
17
|
const FactoryABI = require('./utils/factory-abi');
|
|
18
|
+
const { decodeContractError } = require('./utils/contract-error-decoder');
|
|
18
19
|
|
|
19
20
|
const DEFAULT_TIMELOCK_MIN_DELAY = process.env.SAGE_TIMELOCK_MIN_DELAY_SECONDS || '1';
|
|
20
21
|
const DEFAULT_TIMELOCK_DELAY_FLOOR =
|
|
@@ -31,6 +32,19 @@ function toBigInt(value) {
|
|
|
31
32
|
|
|
32
33
|
function formatFactoryCreationError(err) {
|
|
33
34
|
if (!err) return 'Unknown error';
|
|
35
|
+
|
|
36
|
+
// Try precise decoding first
|
|
37
|
+
try {
|
|
38
|
+
const iface = new ethers.Interface(FactoryABI);
|
|
39
|
+
const decoded = decodeContractError(err, iface);
|
|
40
|
+
if (decoded && decoded !== 'Unknown Error' && !decoded.startsWith('Transaction failed')) {
|
|
41
|
+
const reason = decoded;
|
|
42
|
+
if (reason.includes('BurnOutOfBounds')) return `${reason} (Burn amount is outside the allowed range)`;
|
|
43
|
+
if (reason.includes('InsufficientSXXXBalance')) return `${reason} (Check SXXX balance and allowance)`;
|
|
44
|
+
return reason;
|
|
45
|
+
}
|
|
46
|
+
} catch (_) {}
|
|
47
|
+
|
|
34
48
|
const name = err.error?.errorName || err.errorName || '';
|
|
35
49
|
const reason = err.reason || err.shortMessage || err.message || '';
|
|
36
50
|
const details = reason || name;
|
|
@@ -19,6 +19,7 @@ const COMMAND_CATALOG = {
|
|
|
19
19
|
needsSubcommand: true,
|
|
20
20
|
subcommands: [
|
|
21
21
|
'create',
|
|
22
|
+
'create-playbook',
|
|
22
23
|
'create-template',
|
|
23
24
|
'list',
|
|
24
25
|
'list-all',
|
|
@@ -50,14 +51,20 @@ const COMMAND_CATALOG = {
|
|
|
50
51
|
'info',
|
|
51
52
|
'list',
|
|
52
53
|
'vote',
|
|
54
|
+
'vote-with-reason',
|
|
53
55
|
'queue',
|
|
54
56
|
'execute',
|
|
55
57
|
'status',
|
|
56
58
|
'inspect',
|
|
57
59
|
'watch',
|
|
58
60
|
'power',
|
|
61
|
+
'preflight',
|
|
59
62
|
'doctor',
|
|
60
|
-
'propose'
|
|
63
|
+
'propose',
|
|
64
|
+
'propose-custom',
|
|
65
|
+
'list-all-subdaos',
|
|
66
|
+
'wait',
|
|
67
|
+
'wizard'
|
|
61
68
|
],
|
|
62
69
|
subcommandAliases: {
|
|
63
70
|
ls: 'list',
|
|
@@ -71,6 +78,11 @@ const COMMAND_CATALOG = {
|
|
|
71
78
|
needsSubcommand: true,
|
|
72
79
|
subcommands: [
|
|
73
80
|
'push',
|
|
81
|
+
'template',
|
|
82
|
+
'add-prompt',
|
|
83
|
+
'preview',
|
|
84
|
+
'examples',
|
|
85
|
+
'propose-import',
|
|
74
86
|
'cache',
|
|
75
87
|
'check',
|
|
76
88
|
'check',
|
|
@@ -89,7 +101,10 @@ const COMMAND_CATALOG = {
|
|
|
89
101
|
subcommandAliases: {
|
|
90
102
|
ls: 'status',
|
|
91
103
|
status: 'status',
|
|
92
|
-
|
|
104
|
+
tpl: 'template',
|
|
105
|
+
add: 'add-prompt',
|
|
106
|
+
view: 'preview',
|
|
107
|
+
ex: 'examples'
|
|
93
108
|
}
|
|
94
109
|
},
|
|
95
110
|
prompt: {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const { ethers } = require('ethers');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Attempts to decode a contract error using provided interfaces.
|
|
5
|
+
* @param {Error} error - The error object caught from ethers.js
|
|
6
|
+
* @param {ethers.Interface|ethers.Contract|Array<ethers.Interface|ethers.Contract>} contractsOrInterfaces - Context to decode against
|
|
7
|
+
* @returns {string} - Human readable error message or original message
|
|
8
|
+
*/
|
|
9
|
+
function decodeContractError(error, contractsOrInterfaces = []) {
|
|
10
|
+
if (!error) return 'Unknown Error';
|
|
11
|
+
|
|
12
|
+
// Normalize inputs
|
|
13
|
+
const contexts = Array.isArray(contractsOrInterfaces) ? contractsOrInterfaces : [contractsOrInterfaces];
|
|
14
|
+
const interfaces = contexts.map(c => {
|
|
15
|
+
if (c instanceof ethers.Interface) return c;
|
|
16
|
+
if (c.interface instanceof ethers.Interface) return c.interface;
|
|
17
|
+
return null;
|
|
18
|
+
}).filter(Boolean);
|
|
19
|
+
|
|
20
|
+
// Extract data
|
|
21
|
+
// Ethers v6 often puts the revert data in error.data, error.info.error.data, or error.revert.data
|
|
22
|
+
let data = error.data;
|
|
23
|
+
if (!data && error.info?.error?.data) data = error.info.error.data;
|
|
24
|
+
if (!data && error.revert?.data) data = error.revert.data;
|
|
25
|
+
if (!data && typeof error.message === 'string') {
|
|
26
|
+
// Sometimes it's buried in the message string: "execution reverted: ..." or hex
|
|
27
|
+
// But parsing message is brittle.
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// If we have data, try to decode
|
|
31
|
+
if (data && data !== '0x') {
|
|
32
|
+
for (const iface of interfaces) {
|
|
33
|
+
try {
|
|
34
|
+
const parsed = iface.parseError(data);
|
|
35
|
+
if (parsed) {
|
|
36
|
+
// Format: "ErrorName(arg1, arg2)"
|
|
37
|
+
const args = parsed.args ? Array.from(parsed.args).join(', ') : '';
|
|
38
|
+
return `Reverted: ${parsed.name}(${args})`;
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
// Ignore parse errors, try next interface
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Fallback: check for string revert reasons in the error object
|
|
47
|
+
if (error.reason) return `Reverted: ${error.reason}`;
|
|
48
|
+
if (error.shortMessage) return `Error: ${error.shortMessage}`;
|
|
49
|
+
if (error.message) {
|
|
50
|
+
// Clean up common noise
|
|
51
|
+
if (error.message.includes('execution reverted')) {
|
|
52
|
+
return error.message.split('execution reverted')[1].trim() || 'Execution Reverted';
|
|
53
|
+
}
|
|
54
|
+
return error.message;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return 'Transaction failed with unknown error';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { decodeContractError };
|
|
61
|
+
|