@continuumdao/ctm-mpc-defi 0.2.28 → 0.2.30
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/agent/catalog.cjs +1305 -21
- package/dist/agent/catalog.cjs.map +1 -1
- package/dist/agent/catalog.d.ts +1468 -1
- package/dist/agent/catalog.js +1236 -23
- package/dist/agent/catalog.js.map +1 -1
- package/dist/agent/skills/continuum-dao/SKILL.md +139 -0
- package/dist/agent/skills/ethena/SKILL.md +1 -1
- package/dist/agent/skills/lido/SKILL.md +4 -1
- package/dist/core/index.cjs +42 -39
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +42 -39
- package/dist/core/index.js.map +1 -1
- package/dist/{eip712Multisign-xhXpUYp3.d.ts → eip712Multisign-DCW7heqX.d.ts} +11 -8
- package/dist/index.cjs +64 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +64 -57
- package/dist/index.js.map +1 -1
- package/dist/protocols/evm/arcus/index.cjs +62 -55
- package/dist/protocols/evm/arcus/index.cjs.map +1 -1
- package/dist/protocols/evm/arcus/index.js +62 -55
- package/dist/protocols/evm/arcus/index.js.map +1 -1
- package/dist/protocols/evm/continuum-dao/index.cjs +2237 -0
- package/dist/protocols/evm/continuum-dao/index.cjs.map +1 -0
- package/dist/protocols/evm/continuum-dao/index.d.ts +700 -0
- package/dist/protocols/evm/continuum-dao/index.js +2110 -0
- package/dist/protocols/evm/continuum-dao/index.js.map +1 -0
- package/dist/protocols/evm/ethena/index.cjs +41 -4
- package/dist/protocols/evm/ethena/index.cjs.map +1 -1
- package/dist/protocols/evm/ethena/index.d.ts +12 -2
- package/dist/protocols/evm/ethena/index.js +38 -5
- package/dist/protocols/evm/ethena/index.js.map +1 -1
- package/dist/protocols/evm/hyperliquid/index.cjs +125 -110
- package/dist/protocols/evm/hyperliquid/index.cjs.map +1 -1
- package/dist/protocols/evm/hyperliquid/index.js +125 -110
- package/dist/protocols/evm/hyperliquid/index.js.map +1 -1
- package/dist/protocols/evm/lido/index.cjs +66 -1
- package/dist/protocols/evm/lido/index.cjs.map +1 -1
- package/dist/protocols/evm/lido/index.d.ts +11 -1
- package/dist/protocols/evm/lido/index.js +60 -2
- package/dist/protocols/evm/lido/index.js.map +1 -1
- package/dist/protocols/evm/permit2/index.cjs +66 -59
- package/dist/protocols/evm/permit2/index.cjs.map +1 -1
- package/dist/protocols/evm/permit2/index.js +66 -59
- package/dist/protocols/evm/permit2/index.js.map +1 -1
- package/dist/protocols/evm/uniswap-v4/index.cjs +63 -56
- package/dist/protocols/evm/uniswap-v4/index.cjs.map +1 -1
- package/dist/protocols/evm/uniswap-v4/index.d.ts +1 -1
- package/dist/protocols/evm/uniswap-v4/index.js +63 -56
- package/dist/protocols/evm/uniswap-v4/index.js.map +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,2110 @@
|
|
|
1
|
+
import { parseAbi, zeroAddress, getAddress, parseUnits, encodeFunctionData, createPublicClient, http, defineChain, keccak256, encodePacked, padHex, toHex, hashTypedData, parseGwei, serializeTransaction, stringToHex } from 'viem';
|
|
2
|
+
import { fetchChainFeeParams, gasLimitFromEstimateAndChainConfig, gweiToDecimalString, proposalTxParamsToFeeSnapshot, alignEip1559FeesWithLatestBase, getClientIdFromKeyGenResult } from '@continuumdao/continuum-node-sdk';
|
|
3
|
+
|
|
4
|
+
// src/core/registry.ts
|
|
5
|
+
var modules = [];
|
|
6
|
+
function registerProtocolModule(mod) {
|
|
7
|
+
const existing = modules.findIndex((m) => m.id === mod.id);
|
|
8
|
+
if (existing >= 0) {
|
|
9
|
+
modules[existing] = mod;
|
|
10
|
+
} else {
|
|
11
|
+
modules.push(mod);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/chains/evm/chainIdParse.ts
|
|
16
|
+
function parseEvmChainIdToNumber(chainId) {
|
|
17
|
+
if (chainId == null) return Number.NaN;
|
|
18
|
+
if (typeof chainId === "bigint") {
|
|
19
|
+
const n = Number(chainId);
|
|
20
|
+
return Number.isSafeInteger(n) && n >= 0 ? n : Number.NaN;
|
|
21
|
+
}
|
|
22
|
+
if (typeof chainId === "number") {
|
|
23
|
+
return Number.isInteger(chainId) && chainId >= 0 ? chainId : Number.NaN;
|
|
24
|
+
}
|
|
25
|
+
const t = String(chainId).trim();
|
|
26
|
+
if (!t) return Number.NaN;
|
|
27
|
+
const low = t.toLowerCase();
|
|
28
|
+
if (low.startsWith("eip155:")) {
|
|
29
|
+
const rest = t.slice("eip155:".length).trim();
|
|
30
|
+
const n = Number.parseInt(rest, 10);
|
|
31
|
+
return Number.isNaN(n) || n < 0 ? Number.NaN : n;
|
|
32
|
+
}
|
|
33
|
+
if (low.startsWith("0x")) {
|
|
34
|
+
return Number.parseInt(t, 16);
|
|
35
|
+
}
|
|
36
|
+
return Number.parseInt(t, 10);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/protocols/evm/continuum-dao/constants.ts
|
|
40
|
+
var CONTINUUM_DAO_PROTOCOL_ID = "continuum-dao";
|
|
41
|
+
var CONTINUUM_DAO_ETHEREUM_CHAIN_ID = 1;
|
|
42
|
+
var CONTINUUM_DAO_LINEA_CHAIN_ID = 59144;
|
|
43
|
+
var CONTINUUM_DAO_LINEA_SEPOLIA_CHAIN_ID = 59141;
|
|
44
|
+
var CTM_TOKEN_NAME = "Continuum";
|
|
45
|
+
var CTM_TOKEN_SYMBOL = "CTM";
|
|
46
|
+
var VECTM_TOKEN_NAME = "Voting Escrow Continuum";
|
|
47
|
+
var VECTM_TOKEN_SYMBOL = "veCTM";
|
|
48
|
+
var CTM_TOKEN_DECIMALS = 18;
|
|
49
|
+
var WEEK_SECONDS = 7 * 86400;
|
|
50
|
+
var MAXTIME_SECONDS = 4 * 365 * 86400;
|
|
51
|
+
var UNSET = {
|
|
52
|
+
ctm: zeroAddress,
|
|
53
|
+
votingEscrow: zeroAddress,
|
|
54
|
+
nodeProperties: zeroAddress,
|
|
55
|
+
rewards: zeroAddress,
|
|
56
|
+
governor: zeroAddress
|
|
57
|
+
};
|
|
58
|
+
var CONTINUUM_DAO_DEPLOYMENTS = {
|
|
59
|
+
[CONTINUUM_DAO_LINEA_CHAIN_ID]: { ...UNSET },
|
|
60
|
+
[CONTINUUM_DAO_LINEA_SEPOLIA_CHAIN_ID]: { ...UNSET },
|
|
61
|
+
[CONTINUUM_DAO_ETHEREUM_CHAIN_ID]: {
|
|
62
|
+
ctm: zeroAddress,
|
|
63
|
+
votingEscrow: zeroAddress,
|
|
64
|
+
nodeProperties: zeroAddress,
|
|
65
|
+
rewards: zeroAddress,
|
|
66
|
+
governor: zeroAddress
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var CONTINUUM_DAO_GOVERNANCE_API = "https://app-api.continuumdao.org";
|
|
70
|
+
function isConfiguredAddress(addr) {
|
|
71
|
+
if (!addr?.trim()) return false;
|
|
72
|
+
try {
|
|
73
|
+
return getAddress(addr) !== zeroAddress;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function continuumDaoDeployment(chainId) {
|
|
79
|
+
return CONTINUUM_DAO_DEPLOYMENTS[chainId] ?? null;
|
|
80
|
+
}
|
|
81
|
+
function isContinuumDaoLockChain(chainId) {
|
|
82
|
+
return chainId === CONTINUUM_DAO_LINEA_CHAIN_ID || chainId === CONTINUUM_DAO_LINEA_SEPOLIA_CHAIN_ID;
|
|
83
|
+
}
|
|
84
|
+
function isContinuumDaoSupportedChainId(chainId) {
|
|
85
|
+
return continuumDaoDeployment(chainId) != null;
|
|
86
|
+
}
|
|
87
|
+
function ctmTokenAddressOnEvmChain(chainId) {
|
|
88
|
+
const ctm = continuumDaoDeployment(chainId)?.ctm;
|
|
89
|
+
return ctm && isConfiguredAddress(ctm) ? getAddress(ctm) : null;
|
|
90
|
+
}
|
|
91
|
+
function votingEscrowAddressOnEvmChain(chainId) {
|
|
92
|
+
const ve = continuumDaoDeployment(chainId)?.votingEscrow;
|
|
93
|
+
return ve && isConfiguredAddress(ve) ? getAddress(ve) : null;
|
|
94
|
+
}
|
|
95
|
+
function governorAddressOnEvmChain(chainId) {
|
|
96
|
+
const gov = continuumDaoDeployment(chainId)?.governor;
|
|
97
|
+
return gov && isConfiguredAddress(gov) ? getAddress(gov) : null;
|
|
98
|
+
}
|
|
99
|
+
function sameAddr(a, b) {
|
|
100
|
+
try {
|
|
101
|
+
return getAddress(a) === getAddress(b);
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function isContinuumDaoCtmOnAssetsChain(assetsChainId, contractAddress) {
|
|
107
|
+
const n = parseEvmChainIdToNumber(assetsChainId);
|
|
108
|
+
if (Number.isNaN(n) || n < 0) return false;
|
|
109
|
+
const expected = ctmTokenAddressOnEvmChain(n);
|
|
110
|
+
if (!expected) return false;
|
|
111
|
+
return sameAddr(contractAddress, expected);
|
|
112
|
+
}
|
|
113
|
+
function isContinuumDaoVeCtmOnAssetsChain(assetsChainId, contractAddress) {
|
|
114
|
+
const n = parseEvmChainIdToNumber(assetsChainId);
|
|
115
|
+
if (Number.isNaN(n) || n < 0) return false;
|
|
116
|
+
const expected = votingEscrowAddressOnEvmChain(n);
|
|
117
|
+
if (!expected) return false;
|
|
118
|
+
return sameAddr(contractAddress, expected);
|
|
119
|
+
}
|
|
120
|
+
function isVotingEscrowContinuumName(name) {
|
|
121
|
+
return (name ?? "").trim() === VECTM_TOKEN_NAME;
|
|
122
|
+
}
|
|
123
|
+
function isCtmTokenSymbol(symbol) {
|
|
124
|
+
return (symbol ?? "").trim().toUpperCase() === CTM_TOKEN_SYMBOL;
|
|
125
|
+
}
|
|
126
|
+
function listContinuumDaoDefaultAssets() {
|
|
127
|
+
const out = [];
|
|
128
|
+
for (const chainId of [
|
|
129
|
+
CONTINUUM_DAO_LINEA_CHAIN_ID,
|
|
130
|
+
CONTINUUM_DAO_LINEA_SEPOLIA_CHAIN_ID,
|
|
131
|
+
CONTINUUM_DAO_ETHEREUM_CHAIN_ID
|
|
132
|
+
]) {
|
|
133
|
+
const dep = continuumDaoDeployment(chainId);
|
|
134
|
+
if (!dep) continue;
|
|
135
|
+
if (isConfiguredAddress(dep.ctm)) {
|
|
136
|
+
out.push({
|
|
137
|
+
chainId,
|
|
138
|
+
tokenType: "CTMERC20",
|
|
139
|
+
contractAddress: getAddress(dep.ctm),
|
|
140
|
+
name: CTM_TOKEN_NAME,
|
|
141
|
+
symbol: CTM_TOKEN_SYMBOL,
|
|
142
|
+
decimals: CTM_TOKEN_DECIMALS
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (isContinuumDaoLockChain(chainId) && isConfiguredAddress(dep.votingEscrow)) {
|
|
146
|
+
out.push({
|
|
147
|
+
chainId,
|
|
148
|
+
tokenType: "ERC721",
|
|
149
|
+
contractAddress: getAddress(dep.votingEscrow),
|
|
150
|
+
name: VECTM_TOKEN_NAME,
|
|
151
|
+
symbol: VECTM_TOKEN_SYMBOL
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/core/purpose.ts
|
|
159
|
+
function mergePurposeText(purposeText, purposeSuffix) {
|
|
160
|
+
const t = (purposeText ?? "").trim();
|
|
161
|
+
const suffix = (purposeSuffix ?? "").trim();
|
|
162
|
+
if (!suffix) return t;
|
|
163
|
+
return t ? `${t}
|
|
164
|
+
|
|
165
|
+
${suffix}` : suffix;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/core/envelope.ts
|
|
169
|
+
function finalizeMultisign(input) {
|
|
170
|
+
const { keyGen, destinationChainID, legs } = input;
|
|
171
|
+
if (legs.length === 0) {
|
|
172
|
+
throw new Error("finalizeMultisign requires at least one leg");
|
|
173
|
+
}
|
|
174
|
+
const ph = (keyGen.pubkeyhex ?? "").trim();
|
|
175
|
+
if (!ph) throw new Error("keyGen pubKey (pubkeyhex) is required");
|
|
176
|
+
const keyList = keyGen.keylist ?? [];
|
|
177
|
+
const clientId = getClientIdFromKeyGenResult(keyGen);
|
|
178
|
+
const first = legs[0];
|
|
179
|
+
const messageHashes = legs.map((l) => l.msgHash);
|
|
180
|
+
const messageRawBatch = legs.map((l) => l.msgRaw);
|
|
181
|
+
const batchMeta = legs.map((l) => ({
|
|
182
|
+
destinationAddress: l.destinationAddress,
|
|
183
|
+
signatureText: l.signatureText,
|
|
184
|
+
...l.audit
|
|
185
|
+
}));
|
|
186
|
+
const proposalTxParams = legs.map((l) => l.proposalTxParams).filter((p) => p != null && typeof p === "object");
|
|
187
|
+
const extraPayload = {
|
|
188
|
+
batchMeta,
|
|
189
|
+
...input.extraJSON ?? {}
|
|
190
|
+
};
|
|
191
|
+
const extraJSON = JSON.stringify(extraPayload, (_, v) => typeof v === "bigint" ? v.toString() : v);
|
|
192
|
+
const bodyForSign = {
|
|
193
|
+
keyList,
|
|
194
|
+
pubKey: ph,
|
|
195
|
+
msgHash: messageHashes[0],
|
|
196
|
+
msgRaw: first.msgRaw,
|
|
197
|
+
destinationChainID,
|
|
198
|
+
destinationAddress: input.destinationAddress ?? first.destinationAddress,
|
|
199
|
+
extraJSON,
|
|
200
|
+
signatureText: first.signatureText,
|
|
201
|
+
purpose: mergePurposeText(input.purposeText, input.purposeSuffix),
|
|
202
|
+
...first.feeSnapshot
|
|
203
|
+
};
|
|
204
|
+
if (legs.length > 1) {
|
|
205
|
+
bodyForSign.messageHashes = messageHashes;
|
|
206
|
+
bodyForSign.messageRawBatch = messageRawBatch;
|
|
207
|
+
}
|
|
208
|
+
if (proposalTxParams.length > 0) {
|
|
209
|
+
bodyForSign.proposalTxParams = proposalTxParams;
|
|
210
|
+
}
|
|
211
|
+
const valueWei = first.valueWei;
|
|
212
|
+
if (valueWei != null && valueWei > 0n) {
|
|
213
|
+
bodyForSign.value = valueWei.toString();
|
|
214
|
+
}
|
|
215
|
+
if (clientId) bodyForSign.clientId = clientId;
|
|
216
|
+
if (input.expiryDate != null && input.expiryDate > 0) {
|
|
217
|
+
bodyForSign.expiryDate = Math.floor(input.expiryDate);
|
|
218
|
+
}
|
|
219
|
+
return { bodyForSign, messageToSign: JSON.stringify(bodyForSign) };
|
|
220
|
+
}
|
|
221
|
+
function routerSwapGasLimitFromEstimate(estimatedGas, chainGasLimit) {
|
|
222
|
+
if (chainGasLimit != null && Number.isFinite(chainGasLimit) && chainGasLimit > 0) {
|
|
223
|
+
return gasLimitFromEstimateAndChainConfig(estimatedGas, chainGasLimit);
|
|
224
|
+
}
|
|
225
|
+
return (estimatedGas * 12n + 9n) / 10n;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/chains/evm/buildBatch.ts
|
|
229
|
+
async function buildEvmMultisignBatch(args) {
|
|
230
|
+
const { context, steps } = args;
|
|
231
|
+
const {
|
|
232
|
+
chainId,
|
|
233
|
+
rpcUrl,
|
|
234
|
+
executorAddress,
|
|
235
|
+
chainDetail,
|
|
236
|
+
useCustomGas,
|
|
237
|
+
customGasChainDetails,
|
|
238
|
+
keyGen,
|
|
239
|
+
purposeText
|
|
240
|
+
} = context;
|
|
241
|
+
if (steps.length === 0) throw new Error("buildEvmMultisignBatch requires at least one step");
|
|
242
|
+
const ch = defineChain({
|
|
243
|
+
id: chainId,
|
|
244
|
+
name: "Destination",
|
|
245
|
+
nativeCurrency: { decimals: 18, name: "Ether", symbol: "ETH" },
|
|
246
|
+
rpcUrls: { default: { http: [rpcUrl] } }
|
|
247
|
+
});
|
|
248
|
+
const publicClient2 = createPublicClient({ chain: ch, transport: http(rpcUrl) });
|
|
249
|
+
const feeParams = await fetchChainFeeParams(rpcUrl, chainId);
|
|
250
|
+
const legacy = Boolean(chainDetail?.legacy) || !feeParams.isEip1559;
|
|
251
|
+
const latestBaseFeeWei = !legacy ? (await publicClient2.getBlock({ blockTag: "latest" })).baseFeePerGas ?? 0n : 0n;
|
|
252
|
+
const gasLimitConfig = useCustomGas && chainDetail?.gasLimit != null ? Number(chainDetail.gasLimit) : void 0;
|
|
253
|
+
const chainGasLimitRouter = chainDetail?.gasLimit != null && Number.isFinite(Number(chainDetail.gasLimit)) && Number(chainDetail.gasLimit) > 0 ? Number(chainDetail.gasLimit) : void 0;
|
|
254
|
+
const gasFeeMultiplier = useCustomGas && chainDetail?.gasMultiplier != null ? Number(chainDetail.gasMultiplier) : void 0;
|
|
255
|
+
const executor = getAddress(executorAddress);
|
|
256
|
+
const baseNonce = await publicClient2.getTransactionCount({ address: executor, blockTag: "pending" });
|
|
257
|
+
const legs = [];
|
|
258
|
+
for (let i = 0; i < steps.length; i++) {
|
|
259
|
+
const step = steps[i];
|
|
260
|
+
const currentNonce = baseNonce + i;
|
|
261
|
+
let estimatedGas;
|
|
262
|
+
if (args.estimateGasForStep) {
|
|
263
|
+
estimatedGas = await args.estimateGasForStep({ step, index: i, publicClient: publicClient2, executor });
|
|
264
|
+
} else {
|
|
265
|
+
try {
|
|
266
|
+
estimatedGas = await publicClient2.estimateGas({
|
|
267
|
+
to: step.to,
|
|
268
|
+
data: step.data,
|
|
269
|
+
value: step.value,
|
|
270
|
+
account: executor
|
|
271
|
+
});
|
|
272
|
+
} catch {
|
|
273
|
+
estimatedGas = step.fallbackGas ?? 100000n;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
let gasLimitI;
|
|
277
|
+
if (args.resolveGasLimit) {
|
|
278
|
+
gasLimitI = await args.resolveGasLimit({ step, index: i, estimatedGas, publicClient: publicClient2 });
|
|
279
|
+
} else if (step.routerSwap) {
|
|
280
|
+
gasLimitI = routerSwapGasLimitFromEstimate(estimatedGas, chainGasLimitRouter);
|
|
281
|
+
} else {
|
|
282
|
+
gasLimitI = useCustomGas ? gasLimitFromEstimateAndChainConfig(estimatedGas, gasLimitConfig) : estimatedGas;
|
|
283
|
+
}
|
|
284
|
+
let proposalTxParams;
|
|
285
|
+
let feeSnapshot;
|
|
286
|
+
let serialized;
|
|
287
|
+
if (legacy) {
|
|
288
|
+
let gasPriceWei = await publicClient2.getGasPrice();
|
|
289
|
+
if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
|
|
290
|
+
gasPriceWei = gasPriceWei * BigInt(100 + gasFeeMultiplier) / 100n;
|
|
291
|
+
}
|
|
292
|
+
if (useCustomGas && chainDetail?.gasPrice != null && chainDetail.gasPrice > 0) {
|
|
293
|
+
const configured = parseGwei(gweiToDecimalString(Number(chainDetail.gasPrice)));
|
|
294
|
+
if (configured > gasPriceWei) gasPriceWei = configured;
|
|
295
|
+
}
|
|
296
|
+
serialized = serializeTransaction({
|
|
297
|
+
type: "legacy",
|
|
298
|
+
to: step.to,
|
|
299
|
+
data: step.data,
|
|
300
|
+
value: step.value,
|
|
301
|
+
gas: gasLimitI,
|
|
302
|
+
gasPrice: gasPriceWei,
|
|
303
|
+
nonce: currentNonce,
|
|
304
|
+
chainId
|
|
305
|
+
});
|
|
306
|
+
proposalTxParams = {
|
|
307
|
+
nonce: currentNonce,
|
|
308
|
+
gasLimit: gasLimitI.toString(),
|
|
309
|
+
txType: "legacy",
|
|
310
|
+
gasPrice: gasPriceWei.toString()
|
|
311
|
+
};
|
|
312
|
+
feeSnapshot = proposalTxParamsToFeeSnapshot(proposalTxParams);
|
|
313
|
+
} else {
|
|
314
|
+
const fetchedBase = feeParams.baseFeeGwei ?? 0;
|
|
315
|
+
const fetchedPriority = feeParams.priorityFeeGwei ?? 0;
|
|
316
|
+
const configuredBase = useCustomGas && chainDetail?.baseFee != null ? Number(chainDetail.baseFee) : 0;
|
|
317
|
+
const configuredPriority = useCustomGas && chainDetail?.priorityFee != null ? Number(chainDetail.priorityFee) : 0;
|
|
318
|
+
const effectiveBaseFeeGwei = Math.max(fetchedBase, configuredBase);
|
|
319
|
+
const effectivePriorityFeeGwei = Math.max(fetchedPriority, configuredPriority);
|
|
320
|
+
const baseFeeMultiplierPct = useCustomGas && chainDetail?.baseFeeMultiplier != null ? Math.max(100, Number(chainDetail.baseFeeMultiplier)) : 100;
|
|
321
|
+
const baseComponentGwei = effectiveBaseFeeGwei * baseFeeMultiplierPct / 100;
|
|
322
|
+
const maxFeePerGasGwei = baseComponentGwei + effectivePriorityFeeGwei;
|
|
323
|
+
let maxPriorityFeePerGas = effectivePriorityFeeGwei > 0 ? parseGwei(gweiToDecimalString(effectivePriorityFeeGwei)) : parseGwei("1");
|
|
324
|
+
let maxFeePerGas = parseGwei(gweiToDecimalString(maxFeePerGasGwei));
|
|
325
|
+
if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
|
|
326
|
+
maxPriorityFeePerGas = maxPriorityFeePerGas * BigInt(100 + gasFeeMultiplier) / 100n;
|
|
327
|
+
maxFeePerGas = maxFeePerGas * BigInt(100 + gasFeeMultiplier) / 100n;
|
|
328
|
+
}
|
|
329
|
+
({ maxFeePerGas, maxPriorityFeePerGas } = alignEip1559FeesWithLatestBase(
|
|
330
|
+
maxFeePerGas,
|
|
331
|
+
maxPriorityFeePerGas,
|
|
332
|
+
latestBaseFeeWei
|
|
333
|
+
));
|
|
334
|
+
serialized = serializeTransaction({
|
|
335
|
+
type: "eip1559",
|
|
336
|
+
to: step.to,
|
|
337
|
+
data: step.data,
|
|
338
|
+
value: step.value,
|
|
339
|
+
gas: gasLimitI,
|
|
340
|
+
maxFeePerGas,
|
|
341
|
+
maxPriorityFeePerGas,
|
|
342
|
+
nonce: currentNonce,
|
|
343
|
+
chainId
|
|
344
|
+
});
|
|
345
|
+
proposalTxParams = {
|
|
346
|
+
nonce: currentNonce,
|
|
347
|
+
gasLimit: gasLimitI.toString(),
|
|
348
|
+
txType: "eip1559",
|
|
349
|
+
maxFeePerGas: maxFeePerGas.toString(),
|
|
350
|
+
maxPriorityFeePerGas: maxPriorityFeePerGas.toString()
|
|
351
|
+
};
|
|
352
|
+
feeSnapshot = i === 0 ? proposalTxParamsToFeeSnapshot(proposalTxParams) : {};
|
|
353
|
+
}
|
|
354
|
+
const h = keccak256(serialized);
|
|
355
|
+
const msgHash = h.startsWith("0x") ? h.slice(2) : h;
|
|
356
|
+
const batchMetaExtra = args.buildBatchMeta({ step, index: i, gasLimit: gasLimitI });
|
|
357
|
+
legs.push({
|
|
358
|
+
msgHash,
|
|
359
|
+
msgRaw: i === 0 && args.firstMsgRawNo0x != null ? args.firstMsgRawNo0x : serialized,
|
|
360
|
+
destinationAddress: step.to,
|
|
361
|
+
signatureText: typeof batchMetaExtra.signatureText === "string" ? batchMetaExtra.signatureText : JSON.stringify(batchMetaExtra.signatureText ?? {}),
|
|
362
|
+
audit: batchMetaExtra,
|
|
363
|
+
feeSnapshot: i === 0 ? feeSnapshot : {},
|
|
364
|
+
proposalTxParams,
|
|
365
|
+
valueWei: i === 0 ? step.value : void 0
|
|
366
|
+
});
|
|
367
|
+
if (i === 0 && args.firstMsgRawNo0x != null) {
|
|
368
|
+
legs[0].msgRaw = args.firstMsgRawNo0x;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const extraJSON = {};
|
|
372
|
+
if (useCustomGas && customGasChainDetails && Object.keys(customGasChainDetails).length > 0) {
|
|
373
|
+
extraJSON.customGasChainDetails = customGasChainDetails;
|
|
374
|
+
}
|
|
375
|
+
const result = finalizeMultisign({
|
|
376
|
+
keyGen,
|
|
377
|
+
purposeText,
|
|
378
|
+
purposeSuffix: args.purposeSuffix,
|
|
379
|
+
destinationChainID: String(chainId),
|
|
380
|
+
destinationAddress: args.destinationAddress ?? steps[0].to,
|
|
381
|
+
legs,
|
|
382
|
+
extraJSON: Object.keys(extraJSON).length > 0 ? extraJSON : void 0,
|
|
383
|
+
expiryDate: context.expiryDate
|
|
384
|
+
});
|
|
385
|
+
const pv = args.payableValueWei;
|
|
386
|
+
if (pv != null && pv > 0n) {
|
|
387
|
+
result.bodyForSign.value = pv.toString();
|
|
388
|
+
}
|
|
389
|
+
return result;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// src/protocols/evm/continuum-dao/multisign.ts
|
|
393
|
+
var CONTINUUM_DAO_APPROVE_FALLBACK = 100000n;
|
|
394
|
+
var CONTINUUM_DAO_CREATE_LOCK_FALLBACK = 550000n;
|
|
395
|
+
var CONTINUUM_DAO_INCREASE_AMOUNT_FALLBACK = 400000n;
|
|
396
|
+
var CONTINUUM_DAO_INCREASE_UNLOCK_FALLBACK = 400000n;
|
|
397
|
+
var CONTINUUM_DAO_SPLIT_FALLBACK = 650000n;
|
|
398
|
+
var CONTINUUM_DAO_MERGE_FALLBACK = 550000n;
|
|
399
|
+
var CONTINUUM_DAO_WITHDRAW_FALLBACK = 350000n;
|
|
400
|
+
var CONTINUUM_DAO_LIQUIDATE_FALLBACK = 450000n;
|
|
401
|
+
var CONTINUUM_DAO_DELEGATE_FALLBACK = 160000n;
|
|
402
|
+
var CONTINUUM_DAO_TRANSFER_FALLBACK = 160000n;
|
|
403
|
+
var CONTINUUM_DAO_ATTACH_FALLBACK = 450000n;
|
|
404
|
+
var CONTINUUM_DAO_DETACH_FALLBACK = 220000n;
|
|
405
|
+
var VECTM_NODE_INFO_TUPLE = "(string,string,uint8[4],uint16[8],string,uint256,uint256,string,string,bytes)";
|
|
406
|
+
var ATTACH_VECTM_SIGNATURE = `attachVeCtm(string,uint256,${VECTM_NODE_INFO_TUPLE})`;
|
|
407
|
+
var erc20Abi = parseAbi([
|
|
408
|
+
"function allowance(address owner, address spender) view returns (uint256)",
|
|
409
|
+
"function approve(address spender, uint256 amount) returns (bool)"
|
|
410
|
+
]);
|
|
411
|
+
var veWriteAbi = parseAbi([
|
|
412
|
+
"function create_lock(uint256 _value, uint256 _lock_duration) returns (uint256)",
|
|
413
|
+
"function increase_amount(uint256 _tokenId, uint256 _value)",
|
|
414
|
+
"function increase_unlock_time(uint256 _tokenId, uint256 _lock_duration)",
|
|
415
|
+
"function withdraw(uint256 _tokenId)",
|
|
416
|
+
"function split(uint256 _tokenId, uint256 _extracted) returns (uint256)",
|
|
417
|
+
"function merge(uint256 _from, uint256 _to)",
|
|
418
|
+
"function liquidate(uint256 _tokenId)",
|
|
419
|
+
"function delegate(address delegatee)",
|
|
420
|
+
"function transferFrom(address _from, address _to, uint256 _tokenId)"
|
|
421
|
+
]);
|
|
422
|
+
var attachAbi = parseAbi([
|
|
423
|
+
`function attachVeCtm(string nodeKey, uint256 tokenId, ${VECTM_NODE_INFO_TUPLE} nodeInfo)`
|
|
424
|
+
]);
|
|
425
|
+
var detachAbi = parseAbi(["function setNodeRemovalStatus(uint256 tokenId, bool status)"]);
|
|
426
|
+
var CONTINUUM_DAO_EVM_TYPES = /* @__PURE__ */ new Set([
|
|
427
|
+
"continuum_dao_approve",
|
|
428
|
+
"continuum_dao_create_lock",
|
|
429
|
+
"continuum_dao_increase_amount",
|
|
430
|
+
"continuum_dao_increase_unlock_time",
|
|
431
|
+
"continuum_dao_split",
|
|
432
|
+
"continuum_dao_merge",
|
|
433
|
+
"continuum_dao_withdraw",
|
|
434
|
+
"continuum_dao_liquidate",
|
|
435
|
+
"continuum_dao_delegate",
|
|
436
|
+
"continuum_dao_undelegate",
|
|
437
|
+
"continuum_dao_transfer",
|
|
438
|
+
"continuum_dao_attach",
|
|
439
|
+
"continuum_dao_request_detach"
|
|
440
|
+
]);
|
|
441
|
+
function requireLockChain(chainId) {
|
|
442
|
+
if (!isContinuumDaoLockChain(chainId)) {
|
|
443
|
+
throw new Error("ContinuumDAO lock and attach actions are Linea-only.");
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function requireVe(chainId) {
|
|
447
|
+
requireLockChain(chainId);
|
|
448
|
+
const ve = votingEscrowAddressOnEvmChain(chainId);
|
|
449
|
+
if (!ve) throw new Error("Voting Escrow address is not configured for this chain yet.");
|
|
450
|
+
return ve;
|
|
451
|
+
}
|
|
452
|
+
function requireCtm(chainId) {
|
|
453
|
+
const ctm = ctmTokenAddressOnEvmChain(chainId);
|
|
454
|
+
if (!ctm) throw new Error("CTM address is not configured for this chain yet.");
|
|
455
|
+
return ctm;
|
|
456
|
+
}
|
|
457
|
+
function parseTokenId(raw) {
|
|
458
|
+
const id = typeof raw === "bigint" ? raw : BigInt(String(raw).trim());
|
|
459
|
+
if (id <= 0n) throw new Error("veCTM token id must be greater than zero.");
|
|
460
|
+
return id;
|
|
461
|
+
}
|
|
462
|
+
function meta(evmType, chainId, to, extra, gasLimit) {
|
|
463
|
+
return {
|
|
464
|
+
destinationAddress: to,
|
|
465
|
+
signatureText: JSON.stringify({
|
|
466
|
+
kind: "ContinuumDAO",
|
|
467
|
+
...extra,
|
|
468
|
+
chainId
|
|
469
|
+
}),
|
|
470
|
+
evm: { type: evmType, version: 1, chainId: String(chainId), protocolId: CONTINUUM_DAO_PROTOCOL_ID },
|
|
471
|
+
continuumDao: { ...extra, gas: { baseGasUnits: gasLimit.toString() } }
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
async function finalize(steps, args, purposeSuffix) {
|
|
475
|
+
if (steps.length === 0) throw new Error("ContinuumDAO batch requires at least one step.");
|
|
476
|
+
const evmSteps = steps.map((s) => ({
|
|
477
|
+
to: s.to,
|
|
478
|
+
data: s.data,
|
|
479
|
+
value: s.value,
|
|
480
|
+
fallbackGas: s.fallbackGas
|
|
481
|
+
}));
|
|
482
|
+
const firstDataNo0x = evmSteps[0].data.startsWith("0x") ? evmSteps[0].data.slice(2) : evmSteps[0].data;
|
|
483
|
+
return buildEvmMultisignBatch({
|
|
484
|
+
context: {
|
|
485
|
+
chainCategory: "evm",
|
|
486
|
+
keyGen: args.keyGen,
|
|
487
|
+
purposeText: args.purposeText,
|
|
488
|
+
chainId: args.chainId,
|
|
489
|
+
rpcUrl: args.rpcUrl.trim(),
|
|
490
|
+
executorAddress: args.executorAddress,
|
|
491
|
+
chainDetail: args.chainDetail,
|
|
492
|
+
useCustomGas: args.useCustomGas,
|
|
493
|
+
customGasChainDetails: args.customGasChainDetails
|
|
494
|
+
},
|
|
495
|
+
steps: evmSteps,
|
|
496
|
+
purposeSuffix,
|
|
497
|
+
firstMsgRawNo0x: firstDataNo0x,
|
|
498
|
+
destinationAddress: steps[0].to,
|
|
499
|
+
buildBatchMeta: ({ index, gasLimit }) => steps[index].buildBatchMeta({ gasLimit })
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
async function maybeApproveCtmStep(args) {
|
|
503
|
+
const ch = defineChain({
|
|
504
|
+
id: args.chainId,
|
|
505
|
+
name: "ContinuumDAO",
|
|
506
|
+
nativeCurrency: { decimals: 18, name: "Ether", symbol: "ETH" },
|
|
507
|
+
rpcUrls: { default: { http: [args.rpcUrl.trim()] } }
|
|
508
|
+
});
|
|
509
|
+
const client = createPublicClient({ chain: ch, transport: http(args.rpcUrl.trim()) });
|
|
510
|
+
let allowance = 0n;
|
|
511
|
+
try {
|
|
512
|
+
allowance = await client.readContract({
|
|
513
|
+
address: args.ctm,
|
|
514
|
+
abi: erc20Abi,
|
|
515
|
+
functionName: "allowance",
|
|
516
|
+
args: [args.owner, args.spender]
|
|
517
|
+
});
|
|
518
|
+
} catch {
|
|
519
|
+
allowance = 0n;
|
|
520
|
+
}
|
|
521
|
+
if (allowance >= args.amountWei) return null;
|
|
522
|
+
const data = encodeFunctionData({
|
|
523
|
+
abi: erc20Abi,
|
|
524
|
+
functionName: "approve",
|
|
525
|
+
args: [args.spender, args.amountWei]
|
|
526
|
+
});
|
|
527
|
+
return {
|
|
528
|
+
to: args.ctm,
|
|
529
|
+
data,
|
|
530
|
+
value: 0n,
|
|
531
|
+
fallbackGas: CONTINUUM_DAO_APPROVE_FALLBACK,
|
|
532
|
+
buildBatchMeta: ({ gasLimit }) => meta("continuum_dao_approve", args.chainId, args.ctm, {
|
|
533
|
+
name: "CTM.approve",
|
|
534
|
+
spender: args.spender,
|
|
535
|
+
amountWei: args.amountWei.toString()
|
|
536
|
+
}, gasLimit)
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
async function buildEvmMultisignBodyContinuumDaoCreateLock(args) {
|
|
540
|
+
const ve = args.votingEscrow && isConfiguredAddress(args.votingEscrow) ? getAddress(args.votingEscrow) : requireVe(args.chainId);
|
|
541
|
+
const ctm = args.ctm && isConfiguredAddress(args.ctm) ? getAddress(args.ctm) : requireCtm(args.chainId);
|
|
542
|
+
const amountWei = parseUnits(String(args.amountHuman).trim().replace(/,/g, ""), CTM_TOKEN_DECIMALS);
|
|
543
|
+
if (amountWei <= 0n) throw new Error("CTM amount must be greater than zero.");
|
|
544
|
+
const duration = BigInt(String(args.lockDurationSeconds).trim());
|
|
545
|
+
if (duration <= 0n) throw new Error("Lock duration must be greater than zero.");
|
|
546
|
+
const owner = getAddress(args.executorAddress);
|
|
547
|
+
const approve = await maybeApproveCtmStep({
|
|
548
|
+
rpcUrl: args.rpcUrl,
|
|
549
|
+
chainId: args.chainId,
|
|
550
|
+
owner,
|
|
551
|
+
ctm,
|
|
552
|
+
spender: ve,
|
|
553
|
+
amountWei
|
|
554
|
+
});
|
|
555
|
+
const data = encodeFunctionData({
|
|
556
|
+
abi: veWriteAbi,
|
|
557
|
+
functionName: "create_lock",
|
|
558
|
+
args: [amountWei, duration]
|
|
559
|
+
});
|
|
560
|
+
const lockStep = {
|
|
561
|
+
to: ve,
|
|
562
|
+
data,
|
|
563
|
+
value: 0n,
|
|
564
|
+
fallbackGas: CONTINUUM_DAO_CREATE_LOCK_FALLBACK,
|
|
565
|
+
buildBatchMeta: ({ gasLimit }) => meta("continuum_dao_create_lock", args.chainId, ve, {
|
|
566
|
+
name: "VotingEscrow.create_lock",
|
|
567
|
+
amountWei: amountWei.toString(),
|
|
568
|
+
lockDurationSeconds: duration.toString()
|
|
569
|
+
}, gasLimit)
|
|
570
|
+
};
|
|
571
|
+
const steps = approve ? [approve, lockStep] : [lockStep];
|
|
572
|
+
return finalize(steps, args, `ContinuumDAO: create lock ${args.amountHuman} CTM.`);
|
|
573
|
+
}
|
|
574
|
+
async function buildEvmMultisignBodyContinuumDaoIncreaseAmount(args) {
|
|
575
|
+
const ve = args.votingEscrow && isConfiguredAddress(args.votingEscrow) ? getAddress(args.votingEscrow) : requireVe(args.chainId);
|
|
576
|
+
const ctm = args.ctm && isConfiguredAddress(args.ctm) ? getAddress(args.ctm) : requireCtm(args.chainId);
|
|
577
|
+
const tokenId = parseTokenId(args.tokenId);
|
|
578
|
+
const amountWei = parseUnits(String(args.amountHuman).trim().replace(/,/g, ""), CTM_TOKEN_DECIMALS);
|
|
579
|
+
if (amountWei <= 0n) throw new Error("CTM amount must be greater than zero.");
|
|
580
|
+
const owner = getAddress(args.executorAddress);
|
|
581
|
+
const approve = await maybeApproveCtmStep({
|
|
582
|
+
rpcUrl: args.rpcUrl,
|
|
583
|
+
chainId: args.chainId,
|
|
584
|
+
owner,
|
|
585
|
+
ctm,
|
|
586
|
+
spender: ve,
|
|
587
|
+
amountWei
|
|
588
|
+
});
|
|
589
|
+
const data = encodeFunctionData({
|
|
590
|
+
abi: veWriteAbi,
|
|
591
|
+
functionName: "increase_amount",
|
|
592
|
+
args: [tokenId, amountWei]
|
|
593
|
+
});
|
|
594
|
+
const step = {
|
|
595
|
+
to: ve,
|
|
596
|
+
data,
|
|
597
|
+
value: 0n,
|
|
598
|
+
fallbackGas: CONTINUUM_DAO_INCREASE_AMOUNT_FALLBACK,
|
|
599
|
+
buildBatchMeta: ({ gasLimit }) => meta("continuum_dao_increase_amount", args.chainId, ve, {
|
|
600
|
+
name: "VotingEscrow.increase_amount",
|
|
601
|
+
tokenId: tokenId.toString(),
|
|
602
|
+
amountWei: amountWei.toString()
|
|
603
|
+
}, gasLimit)
|
|
604
|
+
};
|
|
605
|
+
const steps = approve ? [approve, step] : [step];
|
|
606
|
+
return finalize(steps, args, `ContinuumDAO: increase amount on veCTM #${tokenId}.`);
|
|
607
|
+
}
|
|
608
|
+
async function buildEvmMultisignBodyContinuumDaoIncreaseUnlockTime(args) {
|
|
609
|
+
const ve = args.votingEscrow && isConfiguredAddress(args.votingEscrow) ? getAddress(args.votingEscrow) : requireVe(args.chainId);
|
|
610
|
+
const tokenId = parseTokenId(args.tokenId);
|
|
611
|
+
const duration = BigInt(String(args.lockDurationSeconds).trim());
|
|
612
|
+
if (duration <= 0n) throw new Error("Unlock duration must be greater than zero.");
|
|
613
|
+
const data = encodeFunctionData({
|
|
614
|
+
abi: veWriteAbi,
|
|
615
|
+
functionName: "increase_unlock_time",
|
|
616
|
+
args: [tokenId, duration]
|
|
617
|
+
});
|
|
618
|
+
const step = {
|
|
619
|
+
to: ve,
|
|
620
|
+
data,
|
|
621
|
+
value: 0n,
|
|
622
|
+
fallbackGas: CONTINUUM_DAO_INCREASE_UNLOCK_FALLBACK,
|
|
623
|
+
buildBatchMeta: ({ gasLimit }) => meta("continuum_dao_increase_unlock_time", args.chainId, ve, {
|
|
624
|
+
name: "VotingEscrow.increase_unlock_time",
|
|
625
|
+
tokenId: tokenId.toString(),
|
|
626
|
+
lockDurationSeconds: duration.toString()
|
|
627
|
+
}, gasLimit)
|
|
628
|
+
};
|
|
629
|
+
return finalize([step], args, `ContinuumDAO: increase unlock time on veCTM #${tokenId}.`);
|
|
630
|
+
}
|
|
631
|
+
async function buildEvmMultisignBodyContinuumDaoSplit(args) {
|
|
632
|
+
const ve = args.votingEscrow && isConfiguredAddress(args.votingEscrow) ? getAddress(args.votingEscrow) : requireVe(args.chainId);
|
|
633
|
+
const tokenId = parseTokenId(args.tokenId);
|
|
634
|
+
const extracted = parseUnits(String(args.extractedHuman).trim().replace(/,/g, ""), CTM_TOKEN_DECIMALS);
|
|
635
|
+
if (extracted <= 0n) throw new Error("Split amount must be greater than zero.");
|
|
636
|
+
const data = encodeFunctionData({
|
|
637
|
+
abi: veWriteAbi,
|
|
638
|
+
functionName: "split",
|
|
639
|
+
args: [tokenId, extracted]
|
|
640
|
+
});
|
|
641
|
+
const step = {
|
|
642
|
+
to: ve,
|
|
643
|
+
data,
|
|
644
|
+
value: 0n,
|
|
645
|
+
fallbackGas: CONTINUUM_DAO_SPLIT_FALLBACK,
|
|
646
|
+
buildBatchMeta: ({ gasLimit }) => meta("continuum_dao_split", args.chainId, ve, {
|
|
647
|
+
name: "VotingEscrow.split",
|
|
648
|
+
tokenId: tokenId.toString(),
|
|
649
|
+
extractedWei: extracted.toString()
|
|
650
|
+
}, gasLimit)
|
|
651
|
+
};
|
|
652
|
+
return finalize([step], args, `ContinuumDAO: split veCTM #${tokenId}.`);
|
|
653
|
+
}
|
|
654
|
+
async function buildEvmMultisignBodyContinuumDaoMerge(args) {
|
|
655
|
+
const ve = args.votingEscrow && isConfiguredAddress(args.votingEscrow) ? getAddress(args.votingEscrow) : requireVe(args.chainId);
|
|
656
|
+
const fromId = parseTokenId(args.fromTokenId);
|
|
657
|
+
const toId = parseTokenId(args.toTokenId);
|
|
658
|
+
if (fromId === toId) throw new Error("Merge requires two different token ids.");
|
|
659
|
+
const data = encodeFunctionData({
|
|
660
|
+
abi: veWriteAbi,
|
|
661
|
+
functionName: "merge",
|
|
662
|
+
args: [fromId, toId]
|
|
663
|
+
});
|
|
664
|
+
const step = {
|
|
665
|
+
to: ve,
|
|
666
|
+
data,
|
|
667
|
+
value: 0n,
|
|
668
|
+
fallbackGas: CONTINUUM_DAO_MERGE_FALLBACK,
|
|
669
|
+
buildBatchMeta: ({ gasLimit }) => meta("continuum_dao_merge", args.chainId, ve, {
|
|
670
|
+
name: "VotingEscrow.merge",
|
|
671
|
+
fromTokenId: fromId.toString(),
|
|
672
|
+
toTokenId: toId.toString()
|
|
673
|
+
}, gasLimit)
|
|
674
|
+
};
|
|
675
|
+
return finalize([step], args, `ContinuumDAO: merge veCTM #${fromId} into #${toId}.`);
|
|
676
|
+
}
|
|
677
|
+
async function buildEvmMultisignBodyContinuumDaoWithdraw(args) {
|
|
678
|
+
const ve = args.votingEscrow && isConfiguredAddress(args.votingEscrow) ? getAddress(args.votingEscrow) : requireVe(args.chainId);
|
|
679
|
+
const tokenId = parseTokenId(args.tokenId);
|
|
680
|
+
const data = encodeFunctionData({ abi: veWriteAbi, functionName: "withdraw", args: [tokenId] });
|
|
681
|
+
const step = {
|
|
682
|
+
to: ve,
|
|
683
|
+
data,
|
|
684
|
+
value: 0n,
|
|
685
|
+
fallbackGas: CONTINUUM_DAO_WITHDRAW_FALLBACK,
|
|
686
|
+
buildBatchMeta: ({ gasLimit }) => meta("continuum_dao_withdraw", args.chainId, ve, {
|
|
687
|
+
name: "VotingEscrow.withdraw",
|
|
688
|
+
tokenId: tokenId.toString()
|
|
689
|
+
}, gasLimit)
|
|
690
|
+
};
|
|
691
|
+
return finalize([step], args, `ContinuumDAO: withdraw veCTM #${tokenId}.`);
|
|
692
|
+
}
|
|
693
|
+
async function buildEvmMultisignBodyContinuumDaoLiquidate(args) {
|
|
694
|
+
const ve = args.votingEscrow && isConfiguredAddress(args.votingEscrow) ? getAddress(args.votingEscrow) : requireVe(args.chainId);
|
|
695
|
+
const tokenId = parseTokenId(args.tokenId);
|
|
696
|
+
const data = encodeFunctionData({ abi: veWriteAbi, functionName: "liquidate", args: [tokenId] });
|
|
697
|
+
const step = {
|
|
698
|
+
to: ve,
|
|
699
|
+
data,
|
|
700
|
+
value: 0n,
|
|
701
|
+
fallbackGas: CONTINUUM_DAO_LIQUIDATE_FALLBACK,
|
|
702
|
+
buildBatchMeta: ({ gasLimit }) => meta("continuum_dao_liquidate", args.chainId, ve, {
|
|
703
|
+
name: "VotingEscrow.liquidate",
|
|
704
|
+
tokenId: tokenId.toString()
|
|
705
|
+
}, gasLimit)
|
|
706
|
+
};
|
|
707
|
+
return finalize([step], args, `ContinuumDAO: liquidate veCTM #${tokenId}.`);
|
|
708
|
+
}
|
|
709
|
+
async function buildEvmMultisignBodyContinuumDaoDelegate(args) {
|
|
710
|
+
const ve = args.votingEscrow && isConfiguredAddress(args.votingEscrow) ? getAddress(args.votingEscrow) : requireVe(args.chainId);
|
|
711
|
+
const delegatee = getAddress(args.delegatee);
|
|
712
|
+
const data = encodeFunctionData({ abi: veWriteAbi, functionName: "delegate", args: [delegatee] });
|
|
713
|
+
const step = {
|
|
714
|
+
to: ve,
|
|
715
|
+
data,
|
|
716
|
+
value: 0n,
|
|
717
|
+
fallbackGas: CONTINUUM_DAO_DELEGATE_FALLBACK,
|
|
718
|
+
buildBatchMeta: ({ gasLimit }) => meta("continuum_dao_delegate", args.chainId, ve, {
|
|
719
|
+
name: "VotingEscrow.delegate",
|
|
720
|
+
delegatee
|
|
721
|
+
}, gasLimit)
|
|
722
|
+
};
|
|
723
|
+
return finalize([step], args, `ContinuumDAO: delegate voting power to ${delegatee}.`);
|
|
724
|
+
}
|
|
725
|
+
async function buildEvmMultisignBodyContinuumDaoUndelegate(args) {
|
|
726
|
+
const ve = args.votingEscrow && isConfiguredAddress(args.votingEscrow) ? getAddress(args.votingEscrow) : requireVe(args.chainId);
|
|
727
|
+
const self = getAddress(args.executorAddress);
|
|
728
|
+
const data = encodeFunctionData({ abi: veWriteAbi, functionName: "delegate", args: [self] });
|
|
729
|
+
const step = {
|
|
730
|
+
to: ve,
|
|
731
|
+
data,
|
|
732
|
+
value: 0n,
|
|
733
|
+
fallbackGas: CONTINUUM_DAO_DELEGATE_FALLBACK,
|
|
734
|
+
buildBatchMeta: ({ gasLimit }) => meta("continuum_dao_undelegate", args.chainId, ve, {
|
|
735
|
+
name: "VotingEscrow.delegate",
|
|
736
|
+
delegatee: self,
|
|
737
|
+
undelegate: true
|
|
738
|
+
}, gasLimit)
|
|
739
|
+
};
|
|
740
|
+
return finalize([step], args, `ContinuumDAO: undelegate voting power (delegate back to ${self}).`);
|
|
741
|
+
}
|
|
742
|
+
var veReadAbi = parseAbi(["function delegates(address account) view returns (address)"]);
|
|
743
|
+
async function continuumDaoFetchDelegates(args) {
|
|
744
|
+
const chainId = Number(args.chainId);
|
|
745
|
+
requireLockChain(chainId);
|
|
746
|
+
const ve = args.votingEscrow && isConfiguredAddress(args.votingEscrow) ? getAddress(args.votingEscrow) : requireVe(chainId);
|
|
747
|
+
const account = getAddress(args.account);
|
|
748
|
+
const client = createPublicClient({
|
|
749
|
+
chain: defineChain({
|
|
750
|
+
id: chainId,
|
|
751
|
+
name: "ContinuumDAO",
|
|
752
|
+
nativeCurrency: { decimals: 18, name: "Ether", symbol: "ETH" },
|
|
753
|
+
rpcUrls: { default: { http: [args.rpcUrl.trim()] } }
|
|
754
|
+
}),
|
|
755
|
+
transport: http(args.rpcUrl.trim())
|
|
756
|
+
});
|
|
757
|
+
const delegatee = getAddress(
|
|
758
|
+
await client.readContract({ address: ve, abi: veReadAbi, functionName: "delegates", args: [account] })
|
|
759
|
+
);
|
|
760
|
+
return { account, delegatee, self: delegatee === account };
|
|
761
|
+
}
|
|
762
|
+
async function buildEvmMultisignBodyContinuumDaoTransfer(args) {
|
|
763
|
+
const ve = args.votingEscrow && isConfiguredAddress(args.votingEscrow) ? getAddress(args.votingEscrow) : requireVe(args.chainId);
|
|
764
|
+
const tokenId = parseTokenId(args.tokenId);
|
|
765
|
+
const to = getAddress(args.to);
|
|
766
|
+
const from = getAddress(args.executorAddress);
|
|
767
|
+
const data = encodeFunctionData({
|
|
768
|
+
abi: veWriteAbi,
|
|
769
|
+
functionName: "transferFrom",
|
|
770
|
+
args: [from, to, tokenId]
|
|
771
|
+
});
|
|
772
|
+
const step = {
|
|
773
|
+
to: ve,
|
|
774
|
+
data,
|
|
775
|
+
value: 0n,
|
|
776
|
+
fallbackGas: CONTINUUM_DAO_TRANSFER_FALLBACK,
|
|
777
|
+
buildBatchMeta: ({ gasLimit }) => meta("continuum_dao_transfer", args.chainId, ve, {
|
|
778
|
+
name: "VotingEscrow.transferFrom",
|
|
779
|
+
tokenId: tokenId.toString(),
|
|
780
|
+
from,
|
|
781
|
+
to
|
|
782
|
+
}, gasLimit)
|
|
783
|
+
};
|
|
784
|
+
return finalize([step], args, `ContinuumDAO: transfer veCTM #${tokenId} to ${to}.`);
|
|
785
|
+
}
|
|
786
|
+
function encodeContinuumDaoNodeInfo(info) {
|
|
787
|
+
const ipv4Src = info?.ipv4 ?? [0, 0, 0, 0];
|
|
788
|
+
const ipv6Src = info?.ipv6 ?? [0, 0, 0, 0, 0, 0, 0, 0];
|
|
789
|
+
const ipv4 = [
|
|
790
|
+
ipv4Src[0] ?? 0,
|
|
791
|
+
ipv4Src[1] ?? 0,
|
|
792
|
+
ipv4Src[2] ?? 0,
|
|
793
|
+
ipv4Src[3] ?? 0
|
|
794
|
+
];
|
|
795
|
+
const ipv6 = [
|
|
796
|
+
ipv6Src[0] ?? 0,
|
|
797
|
+
ipv6Src[1] ?? 0,
|
|
798
|
+
ipv6Src[2] ?? 0,
|
|
799
|
+
ipv6Src[3] ?? 0,
|
|
800
|
+
ipv6Src[4] ?? 0,
|
|
801
|
+
ipv6Src[5] ?? 0,
|
|
802
|
+
ipv6Src[6] ?? 0,
|
|
803
|
+
ipv6Src[7] ?? 0
|
|
804
|
+
];
|
|
805
|
+
const extraRaw = (info?.extra ?? "0x").trim() || "0x";
|
|
806
|
+
const extra = extraRaw.startsWith("0x") ? extraRaw : `0x${extraRaw}`;
|
|
807
|
+
return [
|
|
808
|
+
info?.forumHandle ?? "",
|
|
809
|
+
info?.email ?? "",
|
|
810
|
+
ipv4,
|
|
811
|
+
ipv6,
|
|
812
|
+
info?.vps ?? "",
|
|
813
|
+
BigInt(String(info?.ram ?? 0)),
|
|
814
|
+
BigInt(String(info?.cpu ?? 0)),
|
|
815
|
+
info?.dIDType ?? "",
|
|
816
|
+
info?.dID ?? "",
|
|
817
|
+
extra
|
|
818
|
+
];
|
|
819
|
+
}
|
|
820
|
+
async function buildEvmMultisignBodyContinuumDaoAttachVeCtm(args) {
|
|
821
|
+
requireLockChain(args.chainId);
|
|
822
|
+
const wallet = getAddress(args.mpaWallet);
|
|
823
|
+
if (wallet === zeroAddress) throw new Error("MPA wallet address is required to attach veCTM.");
|
|
824
|
+
const tokenId = parseTokenId(args.tokenId);
|
|
825
|
+
const nodeKey = args.nodeKey.trim();
|
|
826
|
+
if (!nodeKey) throw new Error("nodeKey is required to attach veCTM.");
|
|
827
|
+
const tuple = args.nodeInfoTuple ?? encodeContinuumDaoNodeInfo(args.nodeInfo);
|
|
828
|
+
const data = encodeFunctionData({
|
|
829
|
+
abi: attachAbi,
|
|
830
|
+
functionName: "attachVeCtm",
|
|
831
|
+
args: [nodeKey, tokenId, tuple]
|
|
832
|
+
});
|
|
833
|
+
const step = {
|
|
834
|
+
to: wallet,
|
|
835
|
+
data,
|
|
836
|
+
value: 0n,
|
|
837
|
+
fallbackGas: CONTINUUM_DAO_ATTACH_FALLBACK,
|
|
838
|
+
buildBatchMeta: ({ gasLimit }) => meta("continuum_dao_attach", args.chainId, wallet, {
|
|
839
|
+
name: "MultiSignAgentWallet.attachVeCtm",
|
|
840
|
+
nodeKey,
|
|
841
|
+
tokenId: tokenId.toString()
|
|
842
|
+
}, gasLimit)
|
|
843
|
+
};
|
|
844
|
+
return finalize([step], args, `ContinuumDAO: attach veCTM #${tokenId}.`);
|
|
845
|
+
}
|
|
846
|
+
async function buildEvmMultisignBodyContinuumDaoRequestDetach(args) {
|
|
847
|
+
requireLockChain(args.chainId);
|
|
848
|
+
const np = getAddress(args.nodeProperties);
|
|
849
|
+
if (np === zeroAddress) throw new Error("NodeProperties address is required to request detach.");
|
|
850
|
+
const tokenId = parseTokenId(args.tokenId);
|
|
851
|
+
const data = encodeFunctionData({
|
|
852
|
+
abi: detachAbi,
|
|
853
|
+
functionName: "setNodeRemovalStatus",
|
|
854
|
+
args: [tokenId, true]
|
|
855
|
+
});
|
|
856
|
+
const step = {
|
|
857
|
+
to: np,
|
|
858
|
+
data,
|
|
859
|
+
value: 0n,
|
|
860
|
+
fallbackGas: CONTINUUM_DAO_DETACH_FALLBACK,
|
|
861
|
+
buildBatchMeta: ({ gasLimit }) => meta("continuum_dao_request_detach", args.chainId, np, {
|
|
862
|
+
name: "NodeProperties.setNodeRemovalStatus",
|
|
863
|
+
tokenId: tokenId.toString(),
|
|
864
|
+
status: true
|
|
865
|
+
}, gasLimit)
|
|
866
|
+
};
|
|
867
|
+
return finalize([step], args, `ContinuumDAO: request detach of veCTM #${tokenId}.`);
|
|
868
|
+
}
|
|
869
|
+
async function buildEvmMultisignBodyContinuumDaoAttachVeCtmFromMcp(args) {
|
|
870
|
+
return buildEvmMultisignBodyContinuumDaoAttachVeCtm({
|
|
871
|
+
...args,
|
|
872
|
+
nodeInfo: {
|
|
873
|
+
forumHandle: args.forumHandle,
|
|
874
|
+
email: args.email,
|
|
875
|
+
vps: args.vps,
|
|
876
|
+
ram: args.ram,
|
|
877
|
+
cpu: args.cpu,
|
|
878
|
+
dIDType: args.dIDType,
|
|
879
|
+
dID: args.dID
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
var EIP712_SIGN_REQUEST_KIND = "eip712";
|
|
884
|
+
function msgHashNo0x(digest) {
|
|
885
|
+
return digest.startsWith("0x") ? digest.slice(2) : digest;
|
|
886
|
+
}
|
|
887
|
+
function eip712SignatureText(domain, primaryType) {
|
|
888
|
+
return JSON.stringify({
|
|
889
|
+
kind: "EIP-712",
|
|
890
|
+
...typeof domain.name === "string" ? { name: domain.name } : {},
|
|
891
|
+
primaryType
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
function jsonStringifyForAudit(value) {
|
|
895
|
+
return JSON.stringify(value, (_, v) => typeof v === "bigint" ? v.toString() : v);
|
|
896
|
+
}
|
|
897
|
+
function eip712LegEnvelope(typedData, delivery, digest) {
|
|
898
|
+
return {
|
|
899
|
+
version: 1,
|
|
900
|
+
digest,
|
|
901
|
+
domain: typedData.domain,
|
|
902
|
+
types: typedData.types,
|
|
903
|
+
primaryType: typedData.primaryType,
|
|
904
|
+
message: typedData.message,
|
|
905
|
+
delivery
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
function eip712MsgRawHex(envelope) {
|
|
909
|
+
return stringToHex(jsonStringifyForAudit(envelope));
|
|
910
|
+
}
|
|
911
|
+
function buildEip712Multisign(args) {
|
|
912
|
+
if (args.legs.length < 1) {
|
|
913
|
+
throw new Error("buildEip712Multisign requires at least one leg");
|
|
914
|
+
}
|
|
915
|
+
const eip712 = [];
|
|
916
|
+
const legs = args.legs.map((leg) => {
|
|
917
|
+
const digest = hashTypedData({
|
|
918
|
+
domain: leg.typedData.domain,
|
|
919
|
+
types: leg.typedData.types,
|
|
920
|
+
primaryType: leg.typedData.primaryType,
|
|
921
|
+
message: leg.typedData.message
|
|
922
|
+
});
|
|
923
|
+
const envelope = eip712LegEnvelope(leg.typedData, leg.delivery, digest);
|
|
924
|
+
eip712.push(envelope);
|
|
925
|
+
return {
|
|
926
|
+
msgHash: msgHashNo0x(digest),
|
|
927
|
+
msgRaw: eip712MsgRawHex(envelope),
|
|
928
|
+
destinationAddress: args.destinationAddress,
|
|
929
|
+
signatureText: eip712SignatureText(leg.typedData.domain, leg.typedData.primaryType),
|
|
930
|
+
audit: {
|
|
931
|
+
kind: "EIP712",
|
|
932
|
+
primaryType: leg.typedData.primaryType,
|
|
933
|
+
deliveryKind: leg.delivery.kind,
|
|
934
|
+
...leg.audit ?? {}
|
|
935
|
+
},
|
|
936
|
+
feeSnapshot: {}
|
|
937
|
+
};
|
|
938
|
+
});
|
|
939
|
+
return finalizeMultisign({
|
|
940
|
+
keyGen: args.keyGen,
|
|
941
|
+
purposeText: args.purposeText,
|
|
942
|
+
purposeSuffix: args.purposeSuffix,
|
|
943
|
+
destinationChainID: args.destinationChainID,
|
|
944
|
+
destinationAddress: args.destinationAddress,
|
|
945
|
+
extraJSON: {
|
|
946
|
+
signRequestKind: EIP712_SIGN_REQUEST_KIND,
|
|
947
|
+
eip712
|
|
948
|
+
},
|
|
949
|
+
expiryDate: args.expiryDate,
|
|
950
|
+
legs
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// src/protocols/evm/continuum-dao/forum.ts
|
|
955
|
+
var DEFAULT_FORUM_URL = "https://forum.continuumdao.org";
|
|
956
|
+
var FORUM_GOVERNANCE_SECTIONS = [
|
|
957
|
+
"decision",
|
|
958
|
+
"election",
|
|
959
|
+
"treasury",
|
|
960
|
+
"constitution",
|
|
961
|
+
"admin"
|
|
962
|
+
];
|
|
963
|
+
var FORUM_IDEA_SECTION = "ideas";
|
|
964
|
+
var PROPOSAL_TYPE_TO_FORUM_SECTION = {
|
|
965
|
+
Admin: "admin",
|
|
966
|
+
Constitution: "constitution",
|
|
967
|
+
Decision: "decision",
|
|
968
|
+
Election: "election",
|
|
969
|
+
Treasury: "treasury"
|
|
970
|
+
};
|
|
971
|
+
var IDEA_SECTION_ALIASES = /* @__PURE__ */ new Set(["ideas", "ideas-suggestions", "suggestions"]);
|
|
972
|
+
function expectedForumSectionForProposalType(typeLabel2) {
|
|
973
|
+
return PROPOSAL_TYPE_TO_FORUM_SECTION[typeLabel2] ?? null;
|
|
974
|
+
}
|
|
975
|
+
function isForumIdeaSection(section) {
|
|
976
|
+
return IDEA_SECTION_ALIASES.has(String(section || "").toLowerCase());
|
|
977
|
+
}
|
|
978
|
+
function applyForumSectionCheck(out, section, cid) {
|
|
979
|
+
const forumSection = section?.trim() || null;
|
|
980
|
+
const forumCid = cid != null && Number(cid) > 0 ? Number(cid) : null;
|
|
981
|
+
const expected = expectedForumSectionForProposalType(out.typeLabel);
|
|
982
|
+
const added = [];
|
|
983
|
+
if (forumSection === FORUM_IDEA_SECTION || isForumIdeaSection(forumSection ?? void 0)) {
|
|
984
|
+
added.push(
|
|
985
|
+
"Forum thread is in Ideas & Suggestions, not a Governance proposal category. Ideas are discussion only and must not be used as forumKey."
|
|
986
|
+
);
|
|
987
|
+
} else if (expected && forumSection && forumSection !== expected) {
|
|
988
|
+
added.push(`Forum section is ${forumSection} but proposal type is ${out.typeLabel} (expected ${expected}).`);
|
|
989
|
+
}
|
|
990
|
+
const extraLine = forumSection === FORUM_IDEA_SECTION || isForumIdeaSection(forumSection ?? void 0) ? `Forum section: ideas (mismatch \u2014 expected ${expected ?? "a Governance category"}).` : expected && forumSection && forumSection !== expected ? `Forum section: ${forumSection} (mismatch \u2014 expected ${expected}).` : forumSection ? `Forum section: ${forumSection}` : null;
|
|
991
|
+
const lines = out.briefing.split("\n");
|
|
992
|
+
if (extraLine) {
|
|
993
|
+
const forumLine = lines.findIndex((l) => l.startsWith("Forum:"));
|
|
994
|
+
if (forumLine >= 0) lines.splice(forumLine + 1, 0, extraLine);
|
|
995
|
+
else lines.splice(1, 0, extraLine);
|
|
996
|
+
}
|
|
997
|
+
if (added.length) {
|
|
998
|
+
if (!lines.includes("Risks:")) lines.push("Risks:");
|
|
999
|
+
for (const r of added) lines.push(`- ${r}`);
|
|
1000
|
+
}
|
|
1001
|
+
return { ...out, briefing: lines.join("\n"), risks: [...out.risks, ...added], forumSection, forumCid };
|
|
1002
|
+
}
|
|
1003
|
+
var FORUM_LOGIN_DELIVERY_KIND = "forumLogin";
|
|
1004
|
+
function continuumDaoForumUrl(explicit) {
|
|
1005
|
+
const raw = explicit?.trim() || (typeof process !== "undefined" ? process.env.FORUM_URL || process.env.NEXT_PUBLIC_FORUM_URL : "") || DEFAULT_FORUM_URL;
|
|
1006
|
+
return raw.replace(/\/+$/, "");
|
|
1007
|
+
}
|
|
1008
|
+
function assertContinuumDaoForumTopicKey(forumKey, forumUrl) {
|
|
1009
|
+
const raw = String(forumKey || "").trim();
|
|
1010
|
+
if (!raw) {
|
|
1011
|
+
throw new Error(
|
|
1012
|
+
"A forum thread is required. Create one with ctm_continuum_dao_forum_create_topic in a Governance section (decision / election / treasury / constitution / admin) before propose or register_proposal. Ideas & Suggestions threads are not valid forumKey values."
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
let url;
|
|
1016
|
+
try {
|
|
1017
|
+
url = new URL(raw);
|
|
1018
|
+
} catch {
|
|
1019
|
+
throw new Error("forumKey must be a full forum topic URL (https://forum.continuumdao.org/topic/123).");
|
|
1020
|
+
}
|
|
1021
|
+
const expected = new URL(`${continuumDaoForumUrl(forumUrl)}/`);
|
|
1022
|
+
if (url.host !== expected.host) {
|
|
1023
|
+
throw new Error(`forumKey must be on ${expected.host}.`);
|
|
1024
|
+
}
|
|
1025
|
+
if (!/\/(?:topic|t)\/\d+/.test(url.pathname)) {
|
|
1026
|
+
throw new Error("forumKey must be a created thread (/topic/:tid or /t/:tid), not the forum homepage.");
|
|
1027
|
+
}
|
|
1028
|
+
return raw.replace(/\/+$/, "");
|
|
1029
|
+
}
|
|
1030
|
+
function forumError(body, status) {
|
|
1031
|
+
const o = body && typeof body === "object" ? body : {};
|
|
1032
|
+
const st = o.status && typeof o.status === "object" ? o.status : null;
|
|
1033
|
+
const msg = st && typeof st.message === "string" && st.message || typeof o.message === "string" && o.message || `forum HTTP ${status}`;
|
|
1034
|
+
return new Error(msg);
|
|
1035
|
+
}
|
|
1036
|
+
async function forumFetch(path, init = {}) {
|
|
1037
|
+
const url = `${continuumDaoForumUrl(init.forumUrl)}${path}`;
|
|
1038
|
+
const headers = {
|
|
1039
|
+
Accept: "application/json",
|
|
1040
|
+
...init.body ? { "Content-Type": "application/json" } : {}
|
|
1041
|
+
};
|
|
1042
|
+
if (init.ticket) {
|
|
1043
|
+
headers.Authorization = `Bearer ${init.ticket}`;
|
|
1044
|
+
}
|
|
1045
|
+
const res = await fetch(url, {
|
|
1046
|
+
method: init.method || "GET",
|
|
1047
|
+
headers,
|
|
1048
|
+
body: init.body,
|
|
1049
|
+
cache: "no-store"
|
|
1050
|
+
});
|
|
1051
|
+
const body = await res.json().catch(() => ({}));
|
|
1052
|
+
if (!res.ok) {
|
|
1053
|
+
throw forumError(body, res.status);
|
|
1054
|
+
}
|
|
1055
|
+
return body;
|
|
1056
|
+
}
|
|
1057
|
+
async function continuumDaoForumResolve(args) {
|
|
1058
|
+
const qs = new URLSearchParams();
|
|
1059
|
+
if (args.url) qs.set("url", String(args.url));
|
|
1060
|
+
if (args.tid != null) qs.set("tid", String(args.tid));
|
|
1061
|
+
return forumFetch(`/api/continuum/forum/resolve?${qs}`, { forumUrl: args.forumUrl });
|
|
1062
|
+
}
|
|
1063
|
+
async function continuumDaoForumFetchThread(args) {
|
|
1064
|
+
const qs = new URLSearchParams();
|
|
1065
|
+
if (args.url) qs.set("url", String(args.url));
|
|
1066
|
+
if (args.tid != null) qs.set("tid", String(args.tid));
|
|
1067
|
+
if (args.index != null) qs.set("index", String(args.index));
|
|
1068
|
+
if (args.start != null) qs.set("start", String(args.start));
|
|
1069
|
+
if (args.limit != null) qs.set("limit", String(args.limit));
|
|
1070
|
+
return forumFetch(`/api/continuum/forum/thread?${qs}`, { forumUrl: args.forumUrl });
|
|
1071
|
+
}
|
|
1072
|
+
async function continuumDaoForumReplyCount(args) {
|
|
1073
|
+
const qs = new URLSearchParams();
|
|
1074
|
+
if (args.url) qs.set("url", String(args.url));
|
|
1075
|
+
if (args.tid != null) qs.set("tid", String(args.tid));
|
|
1076
|
+
return forumFetch(`/api/continuum/forum/thread/replies?${qs}`, { forumUrl: args.forumUrl });
|
|
1077
|
+
}
|
|
1078
|
+
async function continuumDaoForumFetchPost(args) {
|
|
1079
|
+
return forumFetch(`/api/continuum/forum/post?pid=${encodeURIComponent(String(args.pid))}`, {
|
|
1080
|
+
forumUrl: args.forumUrl
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
function assertForumHours(hours) {
|
|
1084
|
+
if (hours != null && (!Number.isFinite(hours) || hours <= 0)) {
|
|
1085
|
+
throw new Error("hours must be a positive number");
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
function assertContinuumDaoForumSearchInput(args) {
|
|
1089
|
+
assertForumHours(args.hours);
|
|
1090
|
+
if (!args.query?.trim() && args.hours == null && !args.since?.trim()) {
|
|
1091
|
+
throw new Error("Provide query and/or a time filter (hours or since)");
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
function assertContinuumDaoForumRecentInput(args) {
|
|
1095
|
+
assertForumHours(args.hours);
|
|
1096
|
+
if (args.hours == null && !args.since?.trim()) {
|
|
1097
|
+
throw new Error("Provide a time filter (hours or since)");
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
async function continuumDaoForumSearch(args) {
|
|
1101
|
+
assertContinuumDaoForumSearchInput(args);
|
|
1102
|
+
const qs = new URLSearchParams();
|
|
1103
|
+
if (args.query?.trim()) qs.set("query", args.query.trim());
|
|
1104
|
+
if (args.hours != null) qs.set("hours", String(args.hours));
|
|
1105
|
+
if (args.since?.trim()) qs.set("since", args.since.trim());
|
|
1106
|
+
if (args.start != null) qs.set("start", String(args.start));
|
|
1107
|
+
if (args.limit != null) qs.set("limit", String(args.limit));
|
|
1108
|
+
return forumFetch(`/api/continuum/forum/search?${qs}`, { forumUrl: args.forumUrl });
|
|
1109
|
+
}
|
|
1110
|
+
async function continuumDaoForumRecent(args) {
|
|
1111
|
+
assertContinuumDaoForumRecentInput(args);
|
|
1112
|
+
const qs = new URLSearchParams();
|
|
1113
|
+
if (args.hours != null) qs.set("hours", String(args.hours));
|
|
1114
|
+
if (args.since?.trim()) qs.set("since", args.since.trim());
|
|
1115
|
+
if (args.start != null) qs.set("start", String(args.start));
|
|
1116
|
+
if (args.limit != null) qs.set("limit", String(args.limit));
|
|
1117
|
+
return forumFetch(`/api/continuum/forum/recent?${qs}`, {
|
|
1118
|
+
forumUrl: args.forumUrl,
|
|
1119
|
+
ticket: args.ticket
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
function assertContinuumDaoForumDeleteInput(args) {
|
|
1123
|
+
if (!args.ticket?.trim()) {
|
|
1124
|
+
throw new Error("ticket is required to delete forum posts");
|
|
1125
|
+
}
|
|
1126
|
+
assertForumHours(args.hours);
|
|
1127
|
+
const hasPost = args.pid != null && String(args.pid).trim() !== "";
|
|
1128
|
+
const hasThread = Boolean(args.url?.trim() || args.tid != null && String(args.tid).trim() !== "");
|
|
1129
|
+
const hasUser = Boolean(args.username?.trim());
|
|
1130
|
+
if (Number(hasPost) + Number(hasThread) + Number(hasUser) !== 1) {
|
|
1131
|
+
throw new Error("Provide exactly one of pid, tid/url, or username");
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
async function continuumDaoForumDelete(args) {
|
|
1135
|
+
assertContinuumDaoForumDeleteInput(args);
|
|
1136
|
+
return forumFetch("/api/continuum/forum/delete", {
|
|
1137
|
+
method: "POST",
|
|
1138
|
+
forumUrl: args.forumUrl,
|
|
1139
|
+
ticket: args.ticket,
|
|
1140
|
+
body: JSON.stringify({
|
|
1141
|
+
pid: args.pid,
|
|
1142
|
+
tid: args.tid,
|
|
1143
|
+
url: args.url,
|
|
1144
|
+
username: args.username,
|
|
1145
|
+
hours: args.hours,
|
|
1146
|
+
since: args.since
|
|
1147
|
+
})
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
async function continuumDaoForumUserPostIds(args) {
|
|
1151
|
+
const qs = new URLSearchParams({ username: args.username });
|
|
1152
|
+
if (args.start != null) qs.set("start", String(args.start));
|
|
1153
|
+
if (args.limit != null) qs.set("limit", String(args.limit));
|
|
1154
|
+
return forumFetch(`/api/continuum/forum/user/posts?${qs}`, { forumUrl: args.forumUrl });
|
|
1155
|
+
}
|
|
1156
|
+
async function continuumDaoForumReply(args) {
|
|
1157
|
+
return forumFetch("/api/continuum/forum/reply", {
|
|
1158
|
+
method: "POST",
|
|
1159
|
+
forumUrl: args.forumUrl,
|
|
1160
|
+
ticket: args.ticket,
|
|
1161
|
+
body: JSON.stringify({
|
|
1162
|
+
url: args.url,
|
|
1163
|
+
tid: args.tid,
|
|
1164
|
+
content: args.content,
|
|
1165
|
+
toPid: args.toPid
|
|
1166
|
+
})
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
async function continuumDaoForumReact(args) {
|
|
1170
|
+
return forumFetch("/api/continuum/forum/react", {
|
|
1171
|
+
method: "POST",
|
|
1172
|
+
forumUrl: args.forumUrl,
|
|
1173
|
+
ticket: args.ticket,
|
|
1174
|
+
body: JSON.stringify({ pid: args.pid, emoji: args.emoji })
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
async function continuumDaoForumCreateTopic(args) {
|
|
1178
|
+
if (isForumIdeaSection(args.section)) {
|
|
1179
|
+
throw new Error(
|
|
1180
|
+
"Ideas & Suggestions is not a proposal category. Use ctm_continuum_dao_forum_create_idea for discussion. Formal proposals must use decision, election, treasury, constitution, or admin."
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
return forumFetch("/api/continuum/forum/topic", {
|
|
1184
|
+
method: "POST",
|
|
1185
|
+
forumUrl: args.forumUrl,
|
|
1186
|
+
ticket: args.ticket,
|
|
1187
|
+
body: JSON.stringify({
|
|
1188
|
+
section: args.section,
|
|
1189
|
+
cid: args.cid,
|
|
1190
|
+
title: args.title,
|
|
1191
|
+
content: args.content
|
|
1192
|
+
})
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
async function continuumDaoForumCreateIdea(args) {
|
|
1196
|
+
return forumFetch("/api/continuum/forum/topic", {
|
|
1197
|
+
method: "POST",
|
|
1198
|
+
forumUrl: args.forumUrl,
|
|
1199
|
+
ticket: args.ticket,
|
|
1200
|
+
body: JSON.stringify({
|
|
1201
|
+
section: FORUM_IDEA_SECTION,
|
|
1202
|
+
title: args.title,
|
|
1203
|
+
content: args.content
|
|
1204
|
+
})
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
async function continuumDaoForumMe(args = {}) {
|
|
1208
|
+
return forumFetch("/api/continuum/forum/me", { forumUrl: args.forumUrl, ticket: args.ticket });
|
|
1209
|
+
}
|
|
1210
|
+
function assertContinuumDaoForumMarkReadInput(args) {
|
|
1211
|
+
if (!args.ticket?.trim()) {
|
|
1212
|
+
throw new Error("ticket is required to mark forum topics read");
|
|
1213
|
+
}
|
|
1214
|
+
const all = Boolean(args.all);
|
|
1215
|
+
const one = Boolean(args.url?.trim() || args.tid != null && String(args.tid).trim() !== "");
|
|
1216
|
+
const many = Array.isArray(args.tids) && args.tids.length > 0;
|
|
1217
|
+
if (Number(all) + Number(one) + Number(many) !== 1) {
|
|
1218
|
+
throw new Error("Provide exactly one of tid/url, tids, or all");
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
async function continuumDaoForumUnread(args) {
|
|
1222
|
+
if (!args.ticket?.trim()) {
|
|
1223
|
+
throw new Error("ticket is required to list unread forum topics");
|
|
1224
|
+
}
|
|
1225
|
+
const qs = new URLSearchParams();
|
|
1226
|
+
if (args.filter?.trim()) qs.set("filter", args.filter.trim());
|
|
1227
|
+
if (args.start != null) qs.set("start", String(args.start));
|
|
1228
|
+
if (args.limit != null) qs.set("limit", String(args.limit));
|
|
1229
|
+
const suffix = qs.toString() ? `?${qs}` : "";
|
|
1230
|
+
return forumFetch(`/api/continuum/forum/unread${suffix}`, {
|
|
1231
|
+
forumUrl: args.forumUrl,
|
|
1232
|
+
ticket: args.ticket
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
async function continuumDaoForumMarkRead(args) {
|
|
1236
|
+
assertContinuumDaoForumMarkReadInput(args);
|
|
1237
|
+
return forumFetch("/api/continuum/forum/read", {
|
|
1238
|
+
method: "POST",
|
|
1239
|
+
forumUrl: args.forumUrl,
|
|
1240
|
+
ticket: args.ticket,
|
|
1241
|
+
body: JSON.stringify({
|
|
1242
|
+
tid: args.tid,
|
|
1243
|
+
url: args.url,
|
|
1244
|
+
tids: args.tids,
|
|
1245
|
+
all: args.all
|
|
1246
|
+
})
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
async function continuumDaoForumMarkUnread(args) {
|
|
1250
|
+
if (!args.ticket?.trim()) {
|
|
1251
|
+
throw new Error("ticket is required to mark a forum topic unread");
|
|
1252
|
+
}
|
|
1253
|
+
if (!args.url?.trim() && (args.tid == null || String(args.tid).trim() === "")) {
|
|
1254
|
+
throw new Error("Provide tid or url");
|
|
1255
|
+
}
|
|
1256
|
+
return forumFetch("/api/continuum/forum/unread", {
|
|
1257
|
+
method: "POST",
|
|
1258
|
+
forumUrl: args.forumUrl,
|
|
1259
|
+
ticket: args.ticket,
|
|
1260
|
+
body: JSON.stringify({ tid: args.tid, url: args.url })
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
async function continuumDaoForumSections(args = {}) {
|
|
1264
|
+
return forumFetch("/api/continuum/forum/sections", { forumUrl: args.forumUrl });
|
|
1265
|
+
}
|
|
1266
|
+
async function continuumDaoForumSignOut(args = {}) {
|
|
1267
|
+
if (!args.ticket?.trim()) {
|
|
1268
|
+
throw new Error("ticket is required to sign out");
|
|
1269
|
+
}
|
|
1270
|
+
return forumFetch("/api/continuum/eip712/logout", {
|
|
1271
|
+
method: "POST",
|
|
1272
|
+
forumUrl: args.forumUrl,
|
|
1273
|
+
ticket: args.ticket,
|
|
1274
|
+
body: JSON.stringify({})
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
async function continuumDaoForumSignInEligible(args) {
|
|
1278
|
+
const address = getAddress(args.address);
|
|
1279
|
+
const url = `${continuumDaoForumUrl(args.forumUrl)}/api/continuum/eip712/eligible?address=${encodeURIComponent(address)}`;
|
|
1280
|
+
const res = await fetch(url, { headers: { Accept: "application/json" }, cache: "no-store" });
|
|
1281
|
+
const body = await res.json().catch(() => ({}));
|
|
1282
|
+
const st = body.status && typeof body.status === "object" ? body.status : null;
|
|
1283
|
+
const reason = typeof body.reason === "string" && body.reason || st && typeof st.message === "string" && st.message || void 0;
|
|
1284
|
+
const ok = res.ok && Boolean(body.ok ?? body.eligible);
|
|
1285
|
+
return {
|
|
1286
|
+
ok,
|
|
1287
|
+
eligible: ok,
|
|
1288
|
+
address: typeof body.address === "string" ? body.address : address,
|
|
1289
|
+
reason: ok ? void 0 : reason || "Address does not meet the veCTM threshold"
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
async function continuumDaoForumSignInNonce(args) {
|
|
1293
|
+
const qs = new URLSearchParams({
|
|
1294
|
+
address: args.address,
|
|
1295
|
+
nodeKey: args.nodeKey || "",
|
|
1296
|
+
username: args.username || ""
|
|
1297
|
+
});
|
|
1298
|
+
return forumFetch(`/api/continuum/eip712/nonce?${qs}`, { forumUrl: args.forumUrl });
|
|
1299
|
+
}
|
|
1300
|
+
async function buildEvmMultisignBodyContinuumDaoForumSignIn(args) {
|
|
1301
|
+
const owner = getAddress(args.executorAddress);
|
|
1302
|
+
const gate = await continuumDaoForumSignInEligible({ address: owner, forumUrl: args.forumUrl });
|
|
1303
|
+
if (!gate.ok) {
|
|
1304
|
+
throw new Error(
|
|
1305
|
+
`${gate.reason || "Address does not meet the veCTM threshold"}. Do not start a multi-sign request.`
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
const typed = await continuumDaoForumSignInNonce({
|
|
1309
|
+
address: owner,
|
|
1310
|
+
nodeKey: args.nodeKey,
|
|
1311
|
+
username: args.username,
|
|
1312
|
+
forumUrl: args.forumUrl
|
|
1313
|
+
});
|
|
1314
|
+
const types = { ...typed.types };
|
|
1315
|
+
delete types.EIP712Domain;
|
|
1316
|
+
return buildEip712Multisign({
|
|
1317
|
+
keyGen: args.keyGen,
|
|
1318
|
+
purposeText: args.purposeText,
|
|
1319
|
+
destinationChainID: String(args.chainId),
|
|
1320
|
+
destinationAddress: owner,
|
|
1321
|
+
legs: [
|
|
1322
|
+
{
|
|
1323
|
+
typedData: {
|
|
1324
|
+
domain: typed.domain,
|
|
1325
|
+
types,
|
|
1326
|
+
primaryType: typed.primaryType,
|
|
1327
|
+
message: typed.message
|
|
1328
|
+
},
|
|
1329
|
+
delivery: {
|
|
1330
|
+
kind: FORUM_LOGIN_DELIVERY_KIND,
|
|
1331
|
+
forum: continuumDaoForumUrl(args.forumUrl),
|
|
1332
|
+
nodeKey: args.nodeKey || typed.nodeKey || ""
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
],
|
|
1336
|
+
expiryDate: args.expiryDate
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
// src/protocols/evm/continuum-dao/governance.ts
|
|
1341
|
+
var CONTINUUM_DAO_PROPOSE_FALLBACK = 550000n;
|
|
1342
|
+
var CONTINUUM_DAO_VOTE_FALLBACK = 220000n;
|
|
1343
|
+
var CONTINUUM_DAO_EXECUTE_FALLBACK = 800000n;
|
|
1344
|
+
var CONTINUUM_DAO_CANCEL_FALLBACK = 180000n;
|
|
1345
|
+
var CONTINUUM_DAO_GOV_EVM_TYPES = /* @__PURE__ */ new Set([
|
|
1346
|
+
"continuum_dao_propose",
|
|
1347
|
+
"continuum_dao_cast_vote",
|
|
1348
|
+
"continuum_dao_execute",
|
|
1349
|
+
"continuum_dao_cancel"
|
|
1350
|
+
]);
|
|
1351
|
+
var governorWriteAbi = parseAbi([
|
|
1352
|
+
"function propose(address[] targets, uint256[] values, bytes[] calldatas, string description) returns (uint256)",
|
|
1353
|
+
"function castVote(uint256 proposalId, uint8 support) returns (uint256)",
|
|
1354
|
+
"function castVoteWithReasonAndParams(uint256 proposalId, uint8 support, string reason, bytes params) returns (uint256)",
|
|
1355
|
+
"function execute(uint256 proposalId) payable",
|
|
1356
|
+
"function cancel(uint256 proposalId)"
|
|
1357
|
+
]);
|
|
1358
|
+
var governorReadAbi = parseAbi([
|
|
1359
|
+
"function hashProposal(address[] targets, uint256[] values, bytes[] calldatas, bytes32 descriptionHash) view returns (uint256)",
|
|
1360
|
+
"function state(uint256 proposalId) view returns (uint8)",
|
|
1361
|
+
"function getVotes(address account, uint256 timepoint) view returns (uint256)",
|
|
1362
|
+
"function proposalThreshold() view returns (uint256)",
|
|
1363
|
+
"function proposalCount() view returns (uint256)",
|
|
1364
|
+
"function clock() view returns (uint48)",
|
|
1365
|
+
"function proposalVotes(uint256 proposalId) view returns (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes)"
|
|
1366
|
+
]);
|
|
1367
|
+
function splitAbiParamList(inner) {
|
|
1368
|
+
const out = [];
|
|
1369
|
+
let buf = "";
|
|
1370
|
+
let depth = 0;
|
|
1371
|
+
for (const ch of inner) {
|
|
1372
|
+
if (ch === "(") depth += 1;
|
|
1373
|
+
if (ch === ")") depth -= 1;
|
|
1374
|
+
if (ch === "," && depth === 0) {
|
|
1375
|
+
if (buf.trim()) out.push(buf.trim());
|
|
1376
|
+
buf = "";
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
buf += ch;
|
|
1380
|
+
}
|
|
1381
|
+
if (buf.trim()) out.push(buf.trim());
|
|
1382
|
+
return out;
|
|
1383
|
+
}
|
|
1384
|
+
function signatureToAbiInputs(signature) {
|
|
1385
|
+
const start = signature.indexOf("(");
|
|
1386
|
+
if (start === -1) return [];
|
|
1387
|
+
let depth = 0;
|
|
1388
|
+
let end = -1;
|
|
1389
|
+
for (let i = start; i < signature.length; i++) {
|
|
1390
|
+
const ch = signature[i];
|
|
1391
|
+
if (ch === "(") depth += 1;
|
|
1392
|
+
if (ch === ")") {
|
|
1393
|
+
depth -= 1;
|
|
1394
|
+
if (depth === 0) {
|
|
1395
|
+
end = i;
|
|
1396
|
+
break;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
if (end === -1) return [];
|
|
1401
|
+
const inner = signature.slice(start + 1, end);
|
|
1402
|
+
if (!inner.trim()) return [];
|
|
1403
|
+
return splitAbiParamList(inner).map((t) => ({ type: t }));
|
|
1404
|
+
}
|
|
1405
|
+
function coerceAbiValue(type, value) {
|
|
1406
|
+
const trimmed = (value ?? "").trim();
|
|
1407
|
+
if (type.startsWith("(") && type.endsWith(")")) {
|
|
1408
|
+
const parsed = JSON.parse(trimmed || "[]");
|
|
1409
|
+
if (!Array.isArray(parsed)) throw new Error("tuple value must be a JSON array");
|
|
1410
|
+
const parts = splitAbiParamList(type.slice(1, -1));
|
|
1411
|
+
return parts.map((part, i) => {
|
|
1412
|
+
const child = parsed[i];
|
|
1413
|
+
if (typeof child === "string") return coerceAbiValue(part, child);
|
|
1414
|
+
return coerceAbiValue(part, JSON.stringify(child ?? ""));
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
const fixed = type.match(/^(.+)\[(\d+)\]$/);
|
|
1418
|
+
if (fixed && !type.endsWith("[]")) {
|
|
1419
|
+
const parsed = trimmed.startsWith("[") ? JSON.parse(trimmed) : trimmed.split(",");
|
|
1420
|
+
if (!Array.isArray(parsed)) throw new Error("fixed array value must be a JSON array");
|
|
1421
|
+
return parsed.map((x) => coerceAbiValue(fixed[1], String(x)));
|
|
1422
|
+
}
|
|
1423
|
+
if (type.endsWith("[]")) {
|
|
1424
|
+
const arr = trimmed.startsWith("[") ? JSON.parse(trimmed).map((x) => String(x)) : trimmed ? trimmed.split(",").map((s) => s.trim()) : [];
|
|
1425
|
+
const baseType = type.slice(0, -2);
|
|
1426
|
+
return arr.map((v) => coerceAbiValue(baseType, v));
|
|
1427
|
+
}
|
|
1428
|
+
if (type === "address") return trimmed;
|
|
1429
|
+
if (type.startsWith("uint") || type.startsWith("int")) return BigInt(trimmed || "0");
|
|
1430
|
+
if (type === "bool") return trimmed === "true" || trimmed === "1";
|
|
1431
|
+
return trimmed;
|
|
1432
|
+
}
|
|
1433
|
+
function encodeGovActionCalldata(signature, inputs = []) {
|
|
1434
|
+
const sig = signature.trim();
|
|
1435
|
+
if (!sig) return "0x";
|
|
1436
|
+
const name = sig.slice(0, sig.indexOf("(") === -1 ? sig.length : sig.indexOf("("));
|
|
1437
|
+
const types = signatureToAbiInputs(sig);
|
|
1438
|
+
if (types.length !== inputs.length) throw new Error("encodeGovActionCalldata: inputs length mismatch");
|
|
1439
|
+
const args = inputs.map((inp, i) => coerceAbiValue(types[i].type, inp.value));
|
|
1440
|
+
return encodeFunctionData({
|
|
1441
|
+
abi: [
|
|
1442
|
+
{
|
|
1443
|
+
type: "function",
|
|
1444
|
+
name,
|
|
1445
|
+
inputs: types.map((t, i) => ({ name: `arg${i}`, type: t.type })),
|
|
1446
|
+
outputs: [],
|
|
1447
|
+
stateMutability: "nonpayable"
|
|
1448
|
+
}
|
|
1449
|
+
],
|
|
1450
|
+
functionName: name,
|
|
1451
|
+
args
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
function proposalDescriptionHash(description) {
|
|
1455
|
+
return keccak256(encodePacked(["string"], [description]));
|
|
1456
|
+
}
|
|
1457
|
+
function encodeDeltaMetadata(nOptions, nWinners, optionStartIndices) {
|
|
1458
|
+
if (optionStartIndices.length !== nOptions) {
|
|
1459
|
+
throw new Error("encodeDeltaMetadata: optionStartIndices length must equal nOptions");
|
|
1460
|
+
}
|
|
1461
|
+
const parts = [
|
|
1462
|
+
padHex(toHex(nOptions), { size: 32 }).slice(2),
|
|
1463
|
+
padHex(toHex(nWinners), { size: 32 }).slice(2),
|
|
1464
|
+
...optionStartIndices.map((i) => padHex(toHex(i), { size: 32 }).slice(2))
|
|
1465
|
+
];
|
|
1466
|
+
return `0x${parts.join("")}`;
|
|
1467
|
+
}
|
|
1468
|
+
function encodeDeltaVoteParams(weights) {
|
|
1469
|
+
if (weights.length < 3) throw new Error("Delta vote params need nOptions + NOTA (at least 3 slots).");
|
|
1470
|
+
return `0x${weights.map((w) => padHex(toHex(BigInt(w)), { size: 32 }).slice(2)).join("")}`;
|
|
1471
|
+
}
|
|
1472
|
+
function noOpAction(noOpTarget) {
|
|
1473
|
+
return { target: getAddress(noOpTarget), value: 0n, calldata: "0x" };
|
|
1474
|
+
}
|
|
1475
|
+
function encodeUserAction(a) {
|
|
1476
|
+
const target = getAddress(a.target);
|
|
1477
|
+
if (target === zeroAddress) throw new Error("Action target cannot be the zero address (reserved for Delta metadata).");
|
|
1478
|
+
const value = BigInt(String(a.value || "0"));
|
|
1479
|
+
const calldata = encodeGovActionCalldata(a.signature ?? "", a.inputs ?? []);
|
|
1480
|
+
return { target, value, calldata };
|
|
1481
|
+
}
|
|
1482
|
+
function buildBravoProposalArgs(actions, description, noOpTarget) {
|
|
1483
|
+
const encoded = actions.length === 0 ? [noOpAction(getAddress(noOpTarget))] : actions.map(encodeUserAction);
|
|
1484
|
+
if (encoded[0].target === zeroAddress) {
|
|
1485
|
+
throw new Error("Bravo first target cannot be address(0); that encoding is Delta.");
|
|
1486
|
+
}
|
|
1487
|
+
return {
|
|
1488
|
+
targets: encoded.map((e) => e.target),
|
|
1489
|
+
values: encoded.map((e) => e.value),
|
|
1490
|
+
calldatas: encoded.map((e) => e.calldata),
|
|
1491
|
+
description
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
function buildDeltaProposalArgs(options, nWinners, description, noOpTarget) {
|
|
1495
|
+
if (options.length < 2) throw new Error("Delta proposals need at least 2 options.");
|
|
1496
|
+
if (nWinners < 1 || nWinners >= options.length) throw new Error("Delta nWinners must be >= 1 and < nOptions.");
|
|
1497
|
+
const fallback = getAddress(noOpTarget);
|
|
1498
|
+
const targets = [zeroAddress];
|
|
1499
|
+
const values = [0n];
|
|
1500
|
+
const calldatas = [];
|
|
1501
|
+
const optionStartIndices = [];
|
|
1502
|
+
let nextIndex = 1;
|
|
1503
|
+
for (const opt of options) {
|
|
1504
|
+
optionStartIndices.push(nextIndex);
|
|
1505
|
+
const userActions = opt.actions ?? [];
|
|
1506
|
+
const encoded = userActions.length === 0 ? [noOpAction(fallback)] : userActions.map(encodeUserAction);
|
|
1507
|
+
for (const e of encoded) {
|
|
1508
|
+
targets.push(e.target);
|
|
1509
|
+
values.push(e.value);
|
|
1510
|
+
calldatas.push(e.calldata);
|
|
1511
|
+
nextIndex += 1;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
calldatas.unshift(encodeDeltaMetadata(options.length, nWinners, optionStartIndices));
|
|
1515
|
+
return { targets, values, calldatas, description };
|
|
1516
|
+
}
|
|
1517
|
+
function requireGovChain(chainId) {
|
|
1518
|
+
if (!isContinuumDaoLockChain(chainId)) {
|
|
1519
|
+
throw new Error("ContinuumDAO governance is Linea-only.");
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
function requireGovernor(chainId, override) {
|
|
1523
|
+
requireGovChain(chainId);
|
|
1524
|
+
if (override && isConfiguredAddress(override)) return getAddress(override);
|
|
1525
|
+
const gov = governorAddressOnEvmChain(chainId);
|
|
1526
|
+
if (!gov) throw new Error("Governor address is not configured for this chain yet.");
|
|
1527
|
+
return gov;
|
|
1528
|
+
}
|
|
1529
|
+
function meta2(evmType, chainId, to, extra, gasLimit) {
|
|
1530
|
+
return {
|
|
1531
|
+
destinationAddress: to,
|
|
1532
|
+
signatureText: JSON.stringify({ kind: "ContinuumDAO", ...extra, chainId }),
|
|
1533
|
+
evm: { type: evmType, version: 1, chainId: String(chainId), protocolId: CONTINUUM_DAO_PROTOCOL_ID },
|
|
1534
|
+
continuumDao: { ...extra, gas: { baseGasUnits: gasLimit.toString() } }
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
async function finalizeGov(args, to, data, fallbackGas, evmType, extra, purposeSuffix, value = 0n) {
|
|
1538
|
+
const step = { to, data, value, fallbackGas };
|
|
1539
|
+
const firstDataNo0x = data.startsWith("0x") ? data.slice(2) : data;
|
|
1540
|
+
return buildEvmMultisignBatch({
|
|
1541
|
+
context: {
|
|
1542
|
+
chainCategory: "evm",
|
|
1543
|
+
keyGen: args.keyGen,
|
|
1544
|
+
purposeText: args.purposeText,
|
|
1545
|
+
chainId: args.chainId,
|
|
1546
|
+
rpcUrl: args.rpcUrl.trim(),
|
|
1547
|
+
executorAddress: args.executorAddress,
|
|
1548
|
+
chainDetail: args.chainDetail,
|
|
1549
|
+
useCustomGas: args.useCustomGas,
|
|
1550
|
+
customGasChainDetails: args.customGasChainDetails
|
|
1551
|
+
},
|
|
1552
|
+
steps: [step],
|
|
1553
|
+
purposeSuffix,
|
|
1554
|
+
firstMsgRawNo0x: firstDataNo0x,
|
|
1555
|
+
destinationAddress: to,
|
|
1556
|
+
buildBatchMeta: ({ gasLimit }) => meta2(evmType, args.chainId, to, extra, gasLimit)
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
async function requireProposalThreshold(args) {
|
|
1560
|
+
const power = await continuumDaoFetchVotingPower({
|
|
1561
|
+
chainId: args.chainId,
|
|
1562
|
+
rpcUrl: args.rpcUrl,
|
|
1563
|
+
account: args.executorAddress,
|
|
1564
|
+
governor: args.governor
|
|
1565
|
+
});
|
|
1566
|
+
if (BigInt(power.votes) < BigInt(power.threshold)) {
|
|
1567
|
+
throw new Error(
|
|
1568
|
+
`Proposer voting power ${power.votes} is below ContinuumDAO.proposalThreshold() ${power.threshold}. On-chain rule: max(1000e18 ve power, 1% of total voting power). Delegation applies at the KeyGen ETH address.`
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
return power;
|
|
1572
|
+
}
|
|
1573
|
+
async function buildEvmMultisignBodyContinuumDaoProposeBravo(args) {
|
|
1574
|
+
const forumKey = assertContinuumDaoForumTopicKey(args.forumKey, args.forumUrl);
|
|
1575
|
+
const gov = requireGovernor(args.chainId, args.governor);
|
|
1576
|
+
await requireProposalThreshold(args);
|
|
1577
|
+
const noOp = args.noOpTarget?.trim() || args.executorAddress;
|
|
1578
|
+
const proposalArgs = buildBravoProposalArgs(args.actions ?? [], args.title.trim(), noOp);
|
|
1579
|
+
const data = encodeFunctionData({
|
|
1580
|
+
abi: governorWriteAbi,
|
|
1581
|
+
functionName: "propose",
|
|
1582
|
+
args: [proposalArgs.targets, proposalArgs.values, proposalArgs.calldatas, proposalArgs.description]
|
|
1583
|
+
});
|
|
1584
|
+
const built = await finalizeGov(
|
|
1585
|
+
args,
|
|
1586
|
+
gov,
|
|
1587
|
+
data,
|
|
1588
|
+
CONTINUUM_DAO_PROPOSE_FALLBACK,
|
|
1589
|
+
"continuum_dao_propose",
|
|
1590
|
+
{ name: "ContinuumDAO.propose", configuration: "bravo", title: proposalArgs.description, forumKey },
|
|
1591
|
+
"continuum-dao propose bravo"
|
|
1592
|
+
);
|
|
1593
|
+
return { ...built, proposalArgs };
|
|
1594
|
+
}
|
|
1595
|
+
async function buildEvmMultisignBodyContinuumDaoProposeDelta(args) {
|
|
1596
|
+
const forumKey = assertContinuumDaoForumTopicKey(args.forumKey, args.forumUrl);
|
|
1597
|
+
const gov = requireGovernor(args.chainId, args.governor);
|
|
1598
|
+
await requireProposalThreshold(args);
|
|
1599
|
+
const noOp = args.noOpTarget?.trim() || args.executorAddress;
|
|
1600
|
+
const proposalArgs = buildDeltaProposalArgs(args.options, Number(args.nWinners), args.title.trim(), noOp);
|
|
1601
|
+
const data = encodeFunctionData({
|
|
1602
|
+
abi: governorWriteAbi,
|
|
1603
|
+
functionName: "propose",
|
|
1604
|
+
args: [proposalArgs.targets, proposalArgs.values, proposalArgs.calldatas, proposalArgs.description]
|
|
1605
|
+
});
|
|
1606
|
+
const built = await finalizeGov(
|
|
1607
|
+
args,
|
|
1608
|
+
gov,
|
|
1609
|
+
data,
|
|
1610
|
+
CONTINUUM_DAO_PROPOSE_FALLBACK,
|
|
1611
|
+
"continuum_dao_propose",
|
|
1612
|
+
{ name: "ContinuumDAO.propose", configuration: "delta", title: proposalArgs.description, nWinners: args.nWinners, forumKey },
|
|
1613
|
+
"continuum-dao propose delta"
|
|
1614
|
+
);
|
|
1615
|
+
return { ...built, proposalArgs };
|
|
1616
|
+
}
|
|
1617
|
+
async function buildEvmMultisignBodyContinuumDaoCastVoteBravo(args) {
|
|
1618
|
+
const gov = requireGovernor(args.chainId, args.governor);
|
|
1619
|
+
const proposalId = BigInt(String(args.proposalId).trim());
|
|
1620
|
+
const data = encodeFunctionData({
|
|
1621
|
+
abi: governorWriteAbi,
|
|
1622
|
+
functionName: "castVote",
|
|
1623
|
+
args: [proposalId, args.support]
|
|
1624
|
+
});
|
|
1625
|
+
return finalizeGov(
|
|
1626
|
+
args,
|
|
1627
|
+
gov,
|
|
1628
|
+
data,
|
|
1629
|
+
CONTINUUM_DAO_VOTE_FALLBACK,
|
|
1630
|
+
"continuum_dao_cast_vote",
|
|
1631
|
+
{ name: "ContinuumDAO.castVote", proposalId: proposalId.toString(), support: args.support },
|
|
1632
|
+
"continuum-dao vote bravo"
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
async function buildEvmMultisignBodyContinuumDaoCastVoteDelta(args) {
|
|
1636
|
+
const gov = requireGovernor(args.chainId, args.governor);
|
|
1637
|
+
const proposalId = BigInt(String(args.proposalId).trim());
|
|
1638
|
+
const params = encodeDeltaVoteParams(args.weights);
|
|
1639
|
+
const data = encodeFunctionData({
|
|
1640
|
+
abi: governorWriteAbi,
|
|
1641
|
+
functionName: "castVoteWithReasonAndParams",
|
|
1642
|
+
args: [proposalId, 0, "", params]
|
|
1643
|
+
});
|
|
1644
|
+
return finalizeGov(
|
|
1645
|
+
args,
|
|
1646
|
+
gov,
|
|
1647
|
+
data,
|
|
1648
|
+
CONTINUUM_DAO_VOTE_FALLBACK,
|
|
1649
|
+
"continuum_dao_cast_vote",
|
|
1650
|
+
{ name: "ContinuumDAO.castVoteWithReasonAndParams", proposalId: proposalId.toString() },
|
|
1651
|
+
"continuum-dao vote delta"
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
async function buildEvmMultisignBodyContinuumDaoExecute(args) {
|
|
1655
|
+
const gov = requireGovernor(args.chainId, args.governor);
|
|
1656
|
+
const proposalId = BigInt(String(args.proposalId).trim());
|
|
1657
|
+
const data = encodeFunctionData({
|
|
1658
|
+
abi: governorWriteAbi,
|
|
1659
|
+
functionName: "execute",
|
|
1660
|
+
args: [proposalId]
|
|
1661
|
+
});
|
|
1662
|
+
return finalizeGov(
|
|
1663
|
+
args,
|
|
1664
|
+
gov,
|
|
1665
|
+
data,
|
|
1666
|
+
CONTINUUM_DAO_EXECUTE_FALLBACK,
|
|
1667
|
+
"continuum_dao_execute",
|
|
1668
|
+
{ name: "ContinuumDAO.execute", proposalId: proposalId.toString() },
|
|
1669
|
+
"continuum-dao execute"
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
async function buildEvmMultisignBodyContinuumDaoCancel(args) {
|
|
1673
|
+
const gov = requireGovernor(args.chainId, args.governor);
|
|
1674
|
+
const proposalId = BigInt(String(args.proposalId).trim());
|
|
1675
|
+
const data = encodeFunctionData({
|
|
1676
|
+
abi: governorWriteAbi,
|
|
1677
|
+
functionName: "cancel",
|
|
1678
|
+
args: [proposalId]
|
|
1679
|
+
});
|
|
1680
|
+
return finalizeGov(
|
|
1681
|
+
args,
|
|
1682
|
+
gov,
|
|
1683
|
+
data,
|
|
1684
|
+
CONTINUUM_DAO_CANCEL_FALLBACK,
|
|
1685
|
+
"continuum_dao_cancel",
|
|
1686
|
+
{ name: "ContinuumDAO.cancel", proposalId: proposalId.toString() },
|
|
1687
|
+
"continuum-dao cancel"
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
function publicClient(rpcUrl, chainId) {
|
|
1691
|
+
return createPublicClient({
|
|
1692
|
+
chain: defineChain({
|
|
1693
|
+
id: chainId,
|
|
1694
|
+
name: "ContinuumDAO",
|
|
1695
|
+
nativeCurrency: { decimals: 18, name: "Ether", symbol: "ETH" },
|
|
1696
|
+
rpcUrls: { default: { http: [rpcUrl.trim()] } }
|
|
1697
|
+
}),
|
|
1698
|
+
transport: http(rpcUrl.trim())
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
async function continuumDaoHashProposal(args) {
|
|
1702
|
+
const chainId = Number(args.chainId);
|
|
1703
|
+
const gov = requireGovernor(chainId, args.governor);
|
|
1704
|
+
const client = publicClient(args.rpcUrl, chainId);
|
|
1705
|
+
const id = await client.readContract({
|
|
1706
|
+
address: gov,
|
|
1707
|
+
abi: governorReadAbi,
|
|
1708
|
+
functionName: "hashProposal",
|
|
1709
|
+
args: [args.targets, args.values, args.calldatas, proposalDescriptionHash(args.description)]
|
|
1710
|
+
});
|
|
1711
|
+
return { proposalId: id.toString() };
|
|
1712
|
+
}
|
|
1713
|
+
async function continuumDaoFetchVotingPower(args) {
|
|
1714
|
+
const chainId = Number(args.chainId);
|
|
1715
|
+
const gov = requireGovernor(chainId, args.governor);
|
|
1716
|
+
const client = publicClient(args.rpcUrl, chainId);
|
|
1717
|
+
const clock = await client.readContract({ address: gov, abi: governorReadAbi, functionName: "clock" });
|
|
1718
|
+
const timepoint = clock > 0 ? clock - 1 : 0;
|
|
1719
|
+
const [votes, threshold] = await Promise.all([
|
|
1720
|
+
client.readContract({
|
|
1721
|
+
address: gov,
|
|
1722
|
+
abi: governorReadAbi,
|
|
1723
|
+
functionName: "getVotes",
|
|
1724
|
+
args: [getAddress(args.account), BigInt(timepoint)]
|
|
1725
|
+
}),
|
|
1726
|
+
client.readContract({ address: gov, abi: governorReadAbi, functionName: "proposalThreshold" })
|
|
1727
|
+
]);
|
|
1728
|
+
return { votes: votes.toString(), threshold: threshold.toString(), clock: clock.toString() };
|
|
1729
|
+
}
|
|
1730
|
+
async function continuumDaoFetchProposalState(args) {
|
|
1731
|
+
const chainId = Number(args.chainId);
|
|
1732
|
+
const gov = requireGovernor(chainId, args.governor);
|
|
1733
|
+
const client = publicClient(args.rpcUrl, chainId);
|
|
1734
|
+
const proposalId = BigInt(String(args.proposalId).trim());
|
|
1735
|
+
const [state, votes] = await Promise.all([
|
|
1736
|
+
client.readContract({ address: gov, abi: governorReadAbi, functionName: "state", args: [proposalId] }),
|
|
1737
|
+
client.readContract({ address: gov, abi: governorReadAbi, functionName: "proposalVotes", args: [proposalId] })
|
|
1738
|
+
]);
|
|
1739
|
+
return {
|
|
1740
|
+
state: Number(state),
|
|
1741
|
+
againstVotes: votes[0].toString(),
|
|
1742
|
+
forVotes: votes[1].toString(),
|
|
1743
|
+
abstainVotes: votes[2].toString()
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
async function continuumDaoFetchProposalCount(args) {
|
|
1747
|
+
const gov = requireGovernor(args.chainId, args.governor);
|
|
1748
|
+
const client = publicClient(args.rpcUrl, args.chainId);
|
|
1749
|
+
const count = await client.readContract({ address: gov, abi: governorReadAbi, functionName: "proposalCount" });
|
|
1750
|
+
return { count: count.toString() };
|
|
1751
|
+
}
|
|
1752
|
+
async function continuumDaoFetchProposals(args) {
|
|
1753
|
+
const start = args.start ?? 0;
|
|
1754
|
+
const end = args.end ?? start + 20;
|
|
1755
|
+
const res = await fetch(`${CONTINUUM_DAO_GOVERNANCE_API}/proposals`, {
|
|
1756
|
+
method: "POST",
|
|
1757
|
+
headers: { "Content-Type": "application/json" },
|
|
1758
|
+
body: JSON.stringify({ start, end })
|
|
1759
|
+
});
|
|
1760
|
+
if (!res.ok) throw new Error(`proposals: ${res.status} ${await res.text()}`);
|
|
1761
|
+
return await res.json();
|
|
1762
|
+
}
|
|
1763
|
+
var GOVERNOR_STATE = {
|
|
1764
|
+
Pending: 0,
|
|
1765
|
+
Active: 1,
|
|
1766
|
+
Canceled: 2,
|
|
1767
|
+
Defeated: 3,
|
|
1768
|
+
Succeeded: 4,
|
|
1769
|
+
Queued: 5,
|
|
1770
|
+
Expired: 6,
|
|
1771
|
+
Executed: 7
|
|
1772
|
+
};
|
|
1773
|
+
var GOVERNOR_STATE_LABELS = [
|
|
1774
|
+
"Pending",
|
|
1775
|
+
"Active",
|
|
1776
|
+
"Canceled",
|
|
1777
|
+
"Defeated",
|
|
1778
|
+
"Succeeded",
|
|
1779
|
+
"Queued",
|
|
1780
|
+
"Expired",
|
|
1781
|
+
"Executed"
|
|
1782
|
+
];
|
|
1783
|
+
function bucketGovernorState(state) {
|
|
1784
|
+
if (state == null || !Number.isFinite(state)) return "unknown";
|
|
1785
|
+
if (state === GOVERNOR_STATE.Active) return "live";
|
|
1786
|
+
if (state === GOVERNOR_STATE.Pending) return "pending";
|
|
1787
|
+
if (state === GOVERNOR_STATE.Succeeded) return "readyToExecute";
|
|
1788
|
+
if (state === GOVERNOR_STATE.Canceled || state === GOVERNOR_STATE.Defeated || state === GOVERNOR_STATE.Queued || state === GOVERNOR_STATE.Expired || state === GOVERNOR_STATE.Executed) {
|
|
1789
|
+
return "recentlyCompleted";
|
|
1790
|
+
}
|
|
1791
|
+
return "unknown";
|
|
1792
|
+
}
|
|
1793
|
+
function asRecord(value) {
|
|
1794
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1795
|
+
}
|
|
1796
|
+
function parseBackendProposal(raw) {
|
|
1797
|
+
const row = asRecord(raw);
|
|
1798
|
+
if (!row) return null;
|
|
1799
|
+
const onchainId = String(row.onchainId ?? row.onchain_id ?? row.proposalId ?? "").trim();
|
|
1800
|
+
if (!onchainId) return null;
|
|
1801
|
+
const idRaw = row.id;
|
|
1802
|
+
const id = typeof idRaw === "number" ? idRaw : Number(idRaw);
|
|
1803
|
+
return {
|
|
1804
|
+
id: Number.isFinite(id) ? id : void 0,
|
|
1805
|
+
onchainId,
|
|
1806
|
+
title: String(row.title ?? "Untitled"),
|
|
1807
|
+
configuration: typeof row.configuration === "number" ? row.configuration : void 0
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
function summarizeLiveProposals(result) {
|
|
1811
|
+
if (result.overlay === "backend-only") {
|
|
1812
|
+
return `Scanned ${result.scanned} backend proposal(s) but could not overlay ContinuumDAO.state. Do not call them live. Pass Linea chainId + rpcUrl + a non-zero governor.`;
|
|
1813
|
+
}
|
|
1814
|
+
const liveTitles = result.live.map((p) => `#${p.id ?? "\u2014"} ${p.title}`).join("; ") || "none";
|
|
1815
|
+
const execTitles = result.readyToExecute.map((p) => `#${p.id ?? "\u2014"} ${p.title}`).join("; ") || "none";
|
|
1816
|
+
return `Live (Active, voting now): ${result.live.length} (${liveTitles}). Pending: ${result.pending.length}. Passed, ready to execute (Succeeded): ${result.readyToExecute.length} (${execTitles}). Recently completed in this scan: ${result.recentlyCompleted.length}. Queue is not implemented.`;
|
|
1817
|
+
}
|
|
1818
|
+
async function mapPool(items, concurrency, fn) {
|
|
1819
|
+
const out = [];
|
|
1820
|
+
let i = 0;
|
|
1821
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
1822
|
+
while (i < items.length) {
|
|
1823
|
+
const idx = i;
|
|
1824
|
+
i += 1;
|
|
1825
|
+
out[idx] = await fn(items[idx]);
|
|
1826
|
+
}
|
|
1827
|
+
});
|
|
1828
|
+
await Promise.all(workers);
|
|
1829
|
+
return out;
|
|
1830
|
+
}
|
|
1831
|
+
async function continuumDaoFetchLiveProposals(args) {
|
|
1832
|
+
const limit = Math.min(Math.max(Number(args.limit) || 40, 1), 100);
|
|
1833
|
+
const completedLimit = Math.min(Math.max(Number(args.completedLimit) || 8, 0), 20);
|
|
1834
|
+
const listed = await continuumDaoFetchProposals({ start: 0, end: limit });
|
|
1835
|
+
const rawList = Array.isArray(listed.proposals) ? listed.proposals : [];
|
|
1836
|
+
const parsed = rawList.map(parseBackendProposal).filter((p) => p != null);
|
|
1837
|
+
const chainId = args.chainId != null && String(args.chainId).trim() !== "" ? Number(args.chainId) : NaN;
|
|
1838
|
+
const rpcUrl = (args.rpcUrl ?? "").trim();
|
|
1839
|
+
let overlay = "backend-only";
|
|
1840
|
+
try {
|
|
1841
|
+
if (rpcUrl && Number.isFinite(chainId)) {
|
|
1842
|
+
requireGovernor(chainId, args.governor);
|
|
1843
|
+
overlay = "on-chain";
|
|
1844
|
+
}
|
|
1845
|
+
} catch {
|
|
1846
|
+
overlay = "backend-only";
|
|
1847
|
+
}
|
|
1848
|
+
const rows = overlay === "on-chain" ? await mapPool(parsed, 6, async (p) => {
|
|
1849
|
+
try {
|
|
1850
|
+
const st = await continuumDaoFetchProposalState({
|
|
1851
|
+
chainId,
|
|
1852
|
+
rpcUrl,
|
|
1853
|
+
proposalId: p.onchainId,
|
|
1854
|
+
governor: args.governor
|
|
1855
|
+
});
|
|
1856
|
+
return {
|
|
1857
|
+
...p,
|
|
1858
|
+
state: st.state,
|
|
1859
|
+
stateLabel: GOVERNOR_STATE_LABELS[st.state] ?? String(st.state),
|
|
1860
|
+
bucket: bucketGovernorState(st.state),
|
|
1861
|
+
againstVotes: st.againstVotes,
|
|
1862
|
+
forVotes: st.forVotes,
|
|
1863
|
+
abstainVotes: st.abstainVotes
|
|
1864
|
+
};
|
|
1865
|
+
} catch {
|
|
1866
|
+
return { ...p, state: null, stateLabel: "Unknown", bucket: "unknown" };
|
|
1867
|
+
}
|
|
1868
|
+
}) : parsed.map((p) => ({ ...p, state: null, stateLabel: "Unknown", bucket: "unknown" }));
|
|
1869
|
+
const live = rows.filter((r) => r.bucket === "live");
|
|
1870
|
+
const pending = rows.filter((r) => r.bucket === "pending");
|
|
1871
|
+
const readyToExecute = rows.filter((r) => r.bucket === "readyToExecute");
|
|
1872
|
+
const recentlyCompleted = rows.filter((r) => r.bucket === "recentlyCompleted").slice(0, completedLimit);
|
|
1873
|
+
const unknown = rows.filter((r) => r.bucket === "unknown");
|
|
1874
|
+
const grouped = {
|
|
1875
|
+
overlay,
|
|
1876
|
+
scanned: rows.length,
|
|
1877
|
+
live,
|
|
1878
|
+
pending,
|
|
1879
|
+
readyToExecute,
|
|
1880
|
+
recentlyCompleted,
|
|
1881
|
+
unknown
|
|
1882
|
+
};
|
|
1883
|
+
return { ...grouped, note: summarizeLiveProposals(grouped) };
|
|
1884
|
+
}
|
|
1885
|
+
async function continuumDaoFetchProposal(args) {
|
|
1886
|
+
const res = await fetch(`${CONTINUUM_DAO_GOVERNANCE_API}/proposals/${args.id}`, { cache: "no-store" });
|
|
1887
|
+
if (!res.ok) throw new Error(`proposal: ${res.status} ${await res.text()}`);
|
|
1888
|
+
return { proposal: await res.json() };
|
|
1889
|
+
}
|
|
1890
|
+
async function buildEvmMultisignBodyContinuumDaoProposeDeltaFromMcp(args) {
|
|
1891
|
+
return buildEvmMultisignBodyContinuumDaoProposeDelta({
|
|
1892
|
+
...args,
|
|
1893
|
+
nWinners: Number(args.nWinners)
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
async function buildEvmMultisignBodyContinuumDaoCastVoteBravoFromMcp(args) {
|
|
1897
|
+
const support = Number(args.support);
|
|
1898
|
+
if (support !== 0 && support !== 1 && support !== 2) throw new Error("Bravo support must be 0, 1, or 2.");
|
|
1899
|
+
return buildEvmMultisignBodyContinuumDaoCastVoteBravo({ ...args, support });
|
|
1900
|
+
}
|
|
1901
|
+
async function continuumDaoRegisterProposal(payload) {
|
|
1902
|
+
assertContinuumDaoForumTopicKey(payload.forumKey);
|
|
1903
|
+
const res = await fetch(`${CONTINUUM_DAO_GOVERNANCE_API}/proposals/create`, {
|
|
1904
|
+
method: "POST",
|
|
1905
|
+
headers: { "Content-Type": "application/json" },
|
|
1906
|
+
body: JSON.stringify(payload)
|
|
1907
|
+
});
|
|
1908
|
+
if (!res.ok) throw new Error(`proposals/create: ${res.status} ${await res.text()}`);
|
|
1909
|
+
return { ok: true, result: await res.json() };
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
// src/protocols/evm/continuum-dao/explainProposal.ts
|
|
1913
|
+
var PROPOSAL_TYPE_LABELS = ["Admin", "Constitution", "Decision", "Election", "Treasury"];
|
|
1914
|
+
var APPROVE_RE = /^(approve|increaseAllowance|increaseapproval)\(/i;
|
|
1915
|
+
var DENY_SIG_RE = /^(transfer|transferFrom|approve|increaseAllowance|setOwner|upgradeTo|upgradeToAndCall)\(/i;
|
|
1916
|
+
function asRecord2(value) {
|
|
1917
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1918
|
+
}
|
|
1919
|
+
function str(v, fallback = "") {
|
|
1920
|
+
return v == null ? fallback : String(v);
|
|
1921
|
+
}
|
|
1922
|
+
function isGovNoOpAction(action) {
|
|
1923
|
+
const value = (action.value ?? "0").trim();
|
|
1924
|
+
const sig = (action.signature ?? "").trim();
|
|
1925
|
+
const data = (action.calldata ?? "").trim();
|
|
1926
|
+
const emptyData = data === "" || data === "0x";
|
|
1927
|
+
return (value === "" || value === "0") && sig === "" && emptyData;
|
|
1928
|
+
}
|
|
1929
|
+
function explainGovAction(raw) {
|
|
1930
|
+
const a = asRecord2(raw) ?? {};
|
|
1931
|
+
const net = asRecord2(a.network) ?? {};
|
|
1932
|
+
const networkLabel = str(net.label || net.name, "Unknown network");
|
|
1933
|
+
const chainId = str(net.chainId);
|
|
1934
|
+
const c3 = str(net.c3governor);
|
|
1935
|
+
const viaC3 = Boolean(c3 && c3 !== "0x0000000000000000000000000000000000000000");
|
|
1936
|
+
const target = str(a.target);
|
|
1937
|
+
const value = str(a.value, "0");
|
|
1938
|
+
const signature = str(a.signature);
|
|
1939
|
+
const inputs = Array.isArray(a.inputs) ? a.inputs : [];
|
|
1940
|
+
const inputParts = inputs.map((inp) => {
|
|
1941
|
+
const row = asRecord2(inp) ?? {};
|
|
1942
|
+
return `${str(row.name || row.type, "arg")}=${str(row.value)}`;
|
|
1943
|
+
});
|
|
1944
|
+
const noOp = isGovNoOpAction({ value, signature, calldata: str(a.calldata) });
|
|
1945
|
+
const flags = [];
|
|
1946
|
+
if (noOp) flags.push("signaling-noop");
|
|
1947
|
+
if (viaC3) flags.push("c3-remote");
|
|
1948
|
+
if (BigInt(value || "0") > 0n) flags.push("sends-value");
|
|
1949
|
+
if (APPROVE_RE.test(signature)) flags.push("allowance");
|
|
1950
|
+
if (DENY_SIG_RE.test(signature)) flags.push("sensitive-signature");
|
|
1951
|
+
if (!noOp && !signature) flags.push("unknown-signature");
|
|
1952
|
+
const valueBit = BigInt(value || "0") > 0n ? `; send ${value} wei` : "";
|
|
1953
|
+
const argsBit = inputParts.length ? ` with ${inputParts.join(", ")}` : "";
|
|
1954
|
+
const c3Bit = viaC3 ? " via C3" : "";
|
|
1955
|
+
const line = noOp ? `On ${networkLabel}${c3Bit}: no on-chain effect (encoder no-op / signaling).` : `On ${networkLabel} (chain ${chainId || "\u2014"})${c3Bit}: call ${signature || "(no signature)"} on ${target || "\u2014"} ${argsBit}${valueBit}.`.replace(/\s+/g, " ").trim();
|
|
1956
|
+
return { line, noOp, networkLabel, chainId, viaC3, target, value, signature, flags };
|
|
1957
|
+
}
|
|
1958
|
+
function typeLabel(type) {
|
|
1959
|
+
const n = Number(type);
|
|
1960
|
+
return Number.isFinite(n) && PROPOSAL_TYPE_LABELS[n] ? PROPOSAL_TYPE_LABELS[n] : str(type, "Unknown");
|
|
1961
|
+
}
|
|
1962
|
+
function explainProposalRecord(raw) {
|
|
1963
|
+
const row = asRecord2(raw);
|
|
1964
|
+
if (!row) throw new Error("explain_proposal: empty proposal");
|
|
1965
|
+
const onchainId = str(row.onchainId ?? row.onchain_id ?? row.proposalId);
|
|
1966
|
+
if (!onchainId) throw new Error("explain_proposal: missing onchainId");
|
|
1967
|
+
const configuration = Number(row.configuration) === 1 ? "delta" : "bravo";
|
|
1968
|
+
const nWinners = row.nWinners != null ? Number(row.nWinners) : void 0;
|
|
1969
|
+
const actionsRaw = Array.isArray(row.actions) ? row.actions : [];
|
|
1970
|
+
const optionsRaw = Array.isArray(row.options) ? row.options : [];
|
|
1971
|
+
const actions = actionsRaw.map(explainGovAction);
|
|
1972
|
+
const options = optionsRaw.map((opt) => {
|
|
1973
|
+
const o = asRecord2(opt) ?? {};
|
|
1974
|
+
const optActions = Array.isArray(o.actions) ? o.actions.map(explainGovAction) : [];
|
|
1975
|
+
return { label: str(o.label, "Untitled option"), actions: optActions };
|
|
1976
|
+
});
|
|
1977
|
+
const risks = [];
|
|
1978
|
+
const t = typeLabel(row.type);
|
|
1979
|
+
if (t === "Treasury" || t === "Admin" || t === "Constitution") {
|
|
1980
|
+
risks.push(`${t} proposal \u2014 treat as high impact.`);
|
|
1981
|
+
}
|
|
1982
|
+
const all = configuration === "delta" ? options.flatMap((o) => o.actions) : actions;
|
|
1983
|
+
if (all.some((a) => a.flags.includes("sends-value"))) risks.push("At least one action sends native value.");
|
|
1984
|
+
if (all.some((a) => a.flags.includes("allowance"))) risks.push("At least one action changes token allowance.");
|
|
1985
|
+
if (all.some((a) => a.flags.includes("sensitive-signature"))) risks.push("Sensitive signature (transfer / approve / upgrade / ownership).");
|
|
1986
|
+
if (all.some((a) => a.flags.includes("unknown-signature") && !a.noOp)) risks.push("An action has no readable signature.");
|
|
1987
|
+
if (all.some((a) => a.viaC3)) risks.push("One or more actions execute on another network via C3, not on Linea.");
|
|
1988
|
+
if (!str(row.description).trim() && all.some((a) => !a.noOp)) {
|
|
1989
|
+
risks.push("Empty description with on-chain actions.");
|
|
1990
|
+
}
|
|
1991
|
+
const lines = [];
|
|
1992
|
+
lines.push(`#${row.id ?? "\u2014"} ${str(row.title, "Untitled")} (${t}, ${configuration === "delta" ? "Delta" : "Bravo"}).`);
|
|
1993
|
+
lines.push(`Proposer ${str(row.proposer) || "\u2014"} (any EOA or contract \u2014 not required to be a KeyGen).`);
|
|
1994
|
+
if (str(row.forumKey)) lines.push(`Forum: ${str(row.forumKey)}`);
|
|
1995
|
+
if (configuration === "bravo") {
|
|
1996
|
+
lines.push("If this passes, the DAO will execute, in order:");
|
|
1997
|
+
for (const a of actions) lines.push(`- ${a.line}`);
|
|
1998
|
+
if (actions.length === 0) lines.push("- No user actions (signaling / no-op).");
|
|
1999
|
+
} else {
|
|
2000
|
+
lines.push(`Delta: top ${nWinners ?? "?"} option(s) execute. None of the above (NOTA) is a vote slot, not an option with actions.`);
|
|
2001
|
+
for (const [i, opt] of options.entries()) {
|
|
2002
|
+
lines.push(`Option ${i + 1} \u2014 ${opt.label}:`);
|
|
2003
|
+
for (const a of opt.actions) lines.push(` - ${a.line}`);
|
|
2004
|
+
if (opt.actions.length === 0) lines.push(" - No user actions (signaling / no-op).");
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
if (risks.length) {
|
|
2008
|
+
lines.push("Risks:");
|
|
2009
|
+
for (const r of risks) lines.push(`- ${r}`);
|
|
2010
|
+
}
|
|
2011
|
+
lines.push("Queue is not implemented. Vote while Active; execute from Succeeded.");
|
|
2012
|
+
return {
|
|
2013
|
+
id: typeof row.id === "number" ? row.id : Number(row.id) || void 0,
|
|
2014
|
+
onchainId,
|
|
2015
|
+
title: str(row.title, "Untitled"),
|
|
2016
|
+
description: str(row.description),
|
|
2017
|
+
proposer: str(row.proposer),
|
|
2018
|
+
forumKey: str(row.forumKey),
|
|
2019
|
+
typeLabel: t,
|
|
2020
|
+
configuration,
|
|
2021
|
+
nWinners: Number.isFinite(nWinners) ? nWinners : void 0,
|
|
2022
|
+
briefing: lines.join("\n"),
|
|
2023
|
+
actions,
|
|
2024
|
+
options,
|
|
2025
|
+
risks
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
async function withForumSection(out, forumUrl) {
|
|
2029
|
+
if (!out.forumKey) return out;
|
|
2030
|
+
try {
|
|
2031
|
+
const thread = await continuumDaoForumFetchThread({ url: out.forumKey, index: 0, forumUrl });
|
|
2032
|
+
return applyForumSectionCheck(out, thread.section ?? null, thread.cid ?? null);
|
|
2033
|
+
} catch {
|
|
2034
|
+
return { ...out, forumSection: null, forumCid: null };
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
async function continuumDaoExplainProposal(args) {
|
|
2038
|
+
if (args.id != null && String(args.id).trim() !== "") {
|
|
2039
|
+
const { proposal } = await continuumDaoFetchProposal({ id: args.id });
|
|
2040
|
+
return withForumSection(explainProposalRecord(proposal), args.forumUrl);
|
|
2041
|
+
}
|
|
2042
|
+
const want = (args.onchainId ?? "").trim();
|
|
2043
|
+
if (!want) throw new Error("explain_proposal: pass id or onchainId");
|
|
2044
|
+
const listed = await continuumDaoFetchProposals({ start: 0, end: 80 });
|
|
2045
|
+
const rawList = Array.isArray(listed.proposals) ? listed.proposals : [];
|
|
2046
|
+
const hit = rawList.find((p) => {
|
|
2047
|
+
const row = asRecord2(p);
|
|
2048
|
+
return row && str(row.onchainId ?? row.onchain_id ?? row.proposalId) === want;
|
|
2049
|
+
});
|
|
2050
|
+
if (!hit) throw new Error(`explain_proposal: no catalog row with onchainId ${want}`);
|
|
2051
|
+
const id = asRecord2(hit)?.id;
|
|
2052
|
+
if (id != null) {
|
|
2053
|
+
const { proposal } = await continuumDaoFetchProposal({ id: Number(id) });
|
|
2054
|
+
return withForumSection(explainProposalRecord(proposal), args.forumUrl);
|
|
2055
|
+
}
|
|
2056
|
+
return withForumSection(explainProposalRecord(hit), args.forumUrl);
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// src/protocols/evm/continuum-dao/index.ts
|
|
2060
|
+
var continuumDaoProtocolModule = {
|
|
2061
|
+
id: CONTINUUM_DAO_PROTOCOL_ID,
|
|
2062
|
+
chainCategory: "evm",
|
|
2063
|
+
isChainSupported(ctx) {
|
|
2064
|
+
if (ctx.chainCategory !== "evm") return false;
|
|
2065
|
+
const n = typeof ctx.chainId === "number" ? ctx.chainId : Number.parseInt(String(ctx.chainId), 10);
|
|
2066
|
+
return isContinuumDaoSupportedChainId(n);
|
|
2067
|
+
},
|
|
2068
|
+
isTokenSupported(token) {
|
|
2069
|
+
return token.category === "evm" && (token.kind === "erc20" || token.kind === "erc721");
|
|
2070
|
+
},
|
|
2071
|
+
actions: [
|
|
2072
|
+
{ id: "continuum-dao.create-lock", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Approve CTM + create_lock", commonParams: ["keyGen", "purposeText", "useCustomGas"], params: { amountHuman: { type: "string", required: true, description: "CTM amount" }, lockDurationSeconds: { type: "string", required: true, description: "Lock duration in seconds" } } },
|
|
2073
|
+
{ id: "continuum-dao.increase-amount", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Increase lock amount", commonParams: ["keyGen", "purposeText", "useCustomGas"], params: { tokenId: { type: "string", required: true, description: "veCTM token id" }, amountHuman: { type: "string", required: true, description: "CTM amount to add" } } },
|
|
2074
|
+
{ id: "continuum-dao.increase-unlock-time", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Increase unlock time", commonParams: ["keyGen", "purposeText", "useCustomGas"], params: { tokenId: { type: "string", required: true, description: "veCTM token id" }, lockDurationSeconds: { type: "string", required: true, description: "Duration from week boundary to new unlock" } } },
|
|
2075
|
+
{ id: "continuum-dao.split", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Split a veCTM lock", commonParams: ["keyGen", "purposeText", "useCustomGas"], params: { tokenId: { type: "string", required: true, description: "Source token id" }, extractedHuman: { type: "string", required: true, description: "CTM extracted into the new lock" } } },
|
|
2076
|
+
{ id: "continuum-dao.merge", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Merge two veCTM locks", commonParams: ["keyGen", "purposeText", "useCustomGas"], params: { fromTokenId: { type: "string", required: true, description: "Burned token id" }, toTokenId: { type: "string", required: true, description: "Surviving token id" } } },
|
|
2077
|
+
{ id: "continuum-dao.withdraw", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Withdraw an expired lock", commonParams: ["keyGen", "purposeText", "useCustomGas"], params: { tokenId: { type: "string", required: true, description: "veCTM token id" } } },
|
|
2078
|
+
{ id: "continuum-dao.liquidate", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Liquidate a lock", commonParams: ["keyGen", "purposeText", "useCustomGas"], params: { tokenId: { type: "string", required: true, description: "veCTM token id" } } },
|
|
2079
|
+
{ id: "continuum-dao.delegate", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Delegate voting power", commonParams: ["keyGen", "purposeText", "useCustomGas"], params: { delegatee: { type: "string", required: true, description: "Delegatee address" } } },
|
|
2080
|
+
{ id: "continuum-dao.undelegate", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Return voting power to the KeyGen (delegate to self)", commonParams: ["keyGen", "purposeText", "useCustomGas"], params: {} },
|
|
2081
|
+
{ id: "continuum-dao.fetch-delegates", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Read VotingEscrow.delegates(account)", commonParams: [], params: { account: { type: "string", required: true, description: "KeyGen or wallet address" } } },
|
|
2082
|
+
{ id: "continuum-dao.transfer", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Transfer a veCTM NFT", commonParams: ["keyGen", "purposeText", "useCustomGas"], params: { tokenId: { type: "string", required: true, description: "veCTM token id" }, to: { type: "string", required: true, description: "Recipient" } } },
|
|
2083
|
+
{ id: "continuum-dao.attach", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Attach veCTM to the node (authority KeyGen)", commonParams: ["keyGen", "purposeText", "useCustomGas"], params: { nodeKey: { type: "string", required: true, description: "Node key" }, tokenId: { type: "string", required: true, description: "veCTM token id" }, mpaWallet: { type: "string", required: true, description: "MultiSignAgentWallet address" } } },
|
|
2084
|
+
{ id: "continuum-dao.request-detach", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Request governance detach", commonParams: ["keyGen", "purposeText", "useCustomGas"], params: { tokenId: { type: "string", required: true, description: "Attached token id" }, nodeProperties: { type: "string", required: true, description: "NodeProperties address" } } },
|
|
2085
|
+
{ id: "continuum-dao.propose-bravo", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Propose a Bravo (For/Against/Abstain) proposal", commonParams: ["keyGen", "purposeText", "useCustomGas"], params: { title: { type: "string", required: true, description: "On-chain description (title)" }, forumKey: { type: "string", required: true, description: "Created forum thread URL from forum_create_topic" } } },
|
|
2086
|
+
{ id: "continuum-dao.propose-delta", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Propose a Delta multi-option proposal", commonParams: ["keyGen", "purposeText", "useCustomGas"], params: { title: { type: "string", required: true, description: "On-chain description (title)" }, nWinners: { type: "number", required: true, description: "Winning options to execute" }, forumKey: { type: "string", required: true, description: "Created forum thread URL from forum_create_topic" } } },
|
|
2087
|
+
{ id: "continuum-dao.cast-vote-bravo", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Cast a Bravo vote", commonParams: ["keyGen", "purposeText", "useCustomGas"], params: { proposalId: { type: "string", required: true, description: "On-chain proposal id" }, support: { type: "number", required: true, description: "0 against, 1 for, 2 abstain" } } },
|
|
2088
|
+
{ id: "continuum-dao.cast-vote-delta", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Cast a Delta vote including NOTA", commonParams: ["keyGen", "purposeText", "useCustomGas"], params: { proposalId: { type: "string", required: true, description: "On-chain proposal id" } } },
|
|
2089
|
+
{ id: "continuum-dao.execute", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Execute a succeeded proposal", commonParams: ["keyGen", "purposeText", "useCustomGas"], params: { proposalId: { type: "string", required: true, description: "On-chain proposal id" } } },
|
|
2090
|
+
{ id: "continuum-dao.cancel", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Cancel a proposal", commonParams: ["keyGen", "purposeText", "useCustomGas"], params: { proposalId: { type: "string", required: true, description: "On-chain proposal id" } } },
|
|
2091
|
+
{ id: "continuum-dao.fetch-live-proposals", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "List live/pending/succeeded proposals with on-chain state overlay", commonParams: [], params: { chainId: { type: "number", required: false, description: "Linea chain id" }, rpcUrl: { type: "string", required: false, description: "Linea RPC for state overlay" } } },
|
|
2092
|
+
{ id: "continuum-dao.explain-proposal", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Deconstruct a proposal into a readable briefing (actions, C3, no-ops, risks)", commonParams: [], params: { id: { type: "string", required: false, description: "Backend proposal id" }, onchainId: { type: "string", required: false, description: "Governor proposal id" } } },
|
|
2093
|
+
{ id: "continuum-dao.forum-sign-in", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "EIP-712 forum login (veCTM gate first)", commonParams: ["keyGen", "purposeText"], params: { nodeKey: { type: "string", required: false, description: "Attached node key" }, username: { type: "string", required: false, description: "Username when there is no node key" } } },
|
|
2094
|
+
{ id: "continuum-dao.forum-sign-in-eligible", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Check veCTM forum login eligibility", commonParams: [], params: { address: { type: "string", required: true, description: "KeyGen or wallet address" } } },
|
|
2095
|
+
{ id: "continuum-dao.forum-sign-out", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Revoke forum ticket and sessions", commonParams: [], params: { ticket: { type: "string", required: true, description: "Forum write ticket" } } },
|
|
2096
|
+
{ id: "continuum-dao.forum-create-topic", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Create a Governance proposal thread (not Ideas)", commonParams: [], params: { section: { type: "string", required: false, description: "decision | election | treasury | constitution | admin" }, title: { type: "string", required: true, description: "Thread title" }, content: { type: "string", required: true, description: "Proposal body" } } },
|
|
2097
|
+
{ id: "continuum-dao.forum-create-idea", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Post an Idea or Suggestion (not a proposal)", commonParams: [], params: { title: { type: "string", required: true, description: "Thread title" }, content: { type: "string", required: true, description: "Idea body" } } },
|
|
2098
|
+
{ id: "continuum-dao.forum-search", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Search forum titles and bodies; optional hours/since", commonParams: [], params: { query: { type: "string", required: false, description: "Case-insensitive title or body substring" }, hours: { type: "number", required: false, description: "Only posts from the last X hours" }, since: { type: "string", required: false, description: "Only posts since this ISO date or unix timestamp" } } },
|
|
2099
|
+
{ id: "continuum-dao.forum-recent", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "List recent readable posts (id, title, username, createdAt); requires hours or since", commonParams: [], params: { hours: { type: "number", required: false, description: "Only posts from the last X hours" }, since: { type: "string", required: false, description: "Only posts since this ISO date or unix timestamp" } } },
|
|
2100
|
+
{ id: "continuum-dao.forum-delete", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Admin/moderator soft-delete a post, thread, or a user\u2019s posts", commonParams: [], params: { pid: { type: "string", required: false, description: "Post id" }, tid: { type: "string", required: false, description: "Thread id" }, username: { type: "string", required: false, description: "Delete all posts by this user" }, hours: { type: "number", required: false, description: "Only in the last X hours (default all)" }, since: { type: "string", required: false, description: "Only since this date (default all)" } } },
|
|
2101
|
+
{ id: "continuum-dao.forum-unread", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "List unread topics for the logged-in user", commonParams: [], params: { filter: { type: "string", required: false, description: "all | new | watched | unreplied" } } },
|
|
2102
|
+
{ id: "continuum-dao.forum-mark-read", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Mark topic(s) read (tid/url, tids, or all)", commonParams: [], params: { tid: { type: "string", required: false, description: "Thread id" }, all: { type: "boolean", required: false, description: "Mark every unread topic read" } } },
|
|
2103
|
+
{ id: "continuum-dao.forum-mark-unread", protocolId: CONTINUUM_DAO_PROTOCOL_ID, chainCategory: "evm", description: "Mark one topic unread", commonParams: [], params: { tid: { type: "string", required: false, description: "Thread id" } } }
|
|
2104
|
+
]
|
|
2105
|
+
};
|
|
2106
|
+
registerProtocolModule(continuumDaoProtocolModule);
|
|
2107
|
+
|
|
2108
|
+
export { ATTACH_VECTM_SIGNATURE, CONTINUUM_DAO_APPROVE_FALLBACK, CONTINUUM_DAO_ATTACH_FALLBACK, CONTINUUM_DAO_CANCEL_FALLBACK, CONTINUUM_DAO_CREATE_LOCK_FALLBACK, CONTINUUM_DAO_DELEGATE_FALLBACK, CONTINUUM_DAO_DEPLOYMENTS, CONTINUUM_DAO_DETACH_FALLBACK, CONTINUUM_DAO_ETHEREUM_CHAIN_ID, CONTINUUM_DAO_EVM_TYPES, CONTINUUM_DAO_EXECUTE_FALLBACK, CONTINUUM_DAO_GOVERNANCE_API, CONTINUUM_DAO_GOV_EVM_TYPES, CONTINUUM_DAO_INCREASE_AMOUNT_FALLBACK, CONTINUUM_DAO_INCREASE_UNLOCK_FALLBACK, CONTINUUM_DAO_LINEA_CHAIN_ID, CONTINUUM_DAO_LINEA_SEPOLIA_CHAIN_ID, CONTINUUM_DAO_LIQUIDATE_FALLBACK, CONTINUUM_DAO_MERGE_FALLBACK, CONTINUUM_DAO_PROPOSE_FALLBACK, CONTINUUM_DAO_PROTOCOL_ID, CONTINUUM_DAO_SPLIT_FALLBACK, CONTINUUM_DAO_TRANSFER_FALLBACK, CONTINUUM_DAO_VOTE_FALLBACK, CONTINUUM_DAO_WITHDRAW_FALLBACK, CTM_TOKEN_DECIMALS, CTM_TOKEN_NAME, CTM_TOKEN_SYMBOL, FORUM_GOVERNANCE_SECTIONS, FORUM_IDEA_SECTION, FORUM_LOGIN_DELIVERY_KIND, GOVERNOR_STATE, GOVERNOR_STATE_LABELS, MAXTIME_SECONDS, PROPOSAL_TYPE_LABELS, PROPOSAL_TYPE_TO_FORUM_SECTION, VECTM_NODE_INFO_TUPLE, VECTM_TOKEN_NAME, VECTM_TOKEN_SYMBOL, WEEK_SECONDS, applyForumSectionCheck, assertContinuumDaoForumDeleteInput, assertContinuumDaoForumMarkReadInput, assertContinuumDaoForumRecentInput, assertContinuumDaoForumSearchInput, assertContinuumDaoForumTopicKey, bucketGovernorState, buildBravoProposalArgs, buildDeltaProposalArgs, buildEvmMultisignBodyContinuumDaoAttachVeCtm, buildEvmMultisignBodyContinuumDaoAttachVeCtmFromMcp, buildEvmMultisignBodyContinuumDaoCancel, buildEvmMultisignBodyContinuumDaoCastVoteBravo, buildEvmMultisignBodyContinuumDaoCastVoteBravoFromMcp, buildEvmMultisignBodyContinuumDaoCastVoteDelta, buildEvmMultisignBodyContinuumDaoCreateLock, buildEvmMultisignBodyContinuumDaoDelegate, buildEvmMultisignBodyContinuumDaoExecute, buildEvmMultisignBodyContinuumDaoForumSignIn, buildEvmMultisignBodyContinuumDaoIncreaseAmount, buildEvmMultisignBodyContinuumDaoIncreaseUnlockTime, buildEvmMultisignBodyContinuumDaoLiquidate, buildEvmMultisignBodyContinuumDaoMerge, buildEvmMultisignBodyContinuumDaoProposeBravo, buildEvmMultisignBodyContinuumDaoProposeDelta, buildEvmMultisignBodyContinuumDaoProposeDeltaFromMcp, buildEvmMultisignBodyContinuumDaoRequestDetach, buildEvmMultisignBodyContinuumDaoSplit, buildEvmMultisignBodyContinuumDaoTransfer, buildEvmMultisignBodyContinuumDaoUndelegate, buildEvmMultisignBodyContinuumDaoWithdraw, continuumDaoDeployment, continuumDaoExplainProposal, continuumDaoFetchDelegates, continuumDaoFetchLiveProposals, continuumDaoFetchProposal, continuumDaoFetchProposalCount, continuumDaoFetchProposalState, continuumDaoFetchProposals, continuumDaoFetchVotingPower, continuumDaoForumCreateIdea, continuumDaoForumCreateTopic, continuumDaoForumDelete, continuumDaoForumFetchPost, continuumDaoForumFetchThread, continuumDaoForumMarkRead, continuumDaoForumMarkUnread, continuumDaoForumMe, continuumDaoForumReact, continuumDaoForumRecent, continuumDaoForumReply, continuumDaoForumReplyCount, continuumDaoForumResolve, continuumDaoForumSearch, continuumDaoForumSections, continuumDaoForumSignInEligible, continuumDaoForumSignInNonce, continuumDaoForumSignOut, continuumDaoForumUnread, continuumDaoForumUrl, continuumDaoForumUserPostIds, continuumDaoHashProposal, continuumDaoProtocolModule, continuumDaoRegisterProposal, ctmTokenAddressOnEvmChain, encodeContinuumDaoNodeInfo, encodeDeltaMetadata, encodeDeltaVoteParams, encodeGovActionCalldata, expectedForumSectionForProposalType, explainGovAction, explainProposalRecord, governorAddressOnEvmChain, isConfiguredAddress, isContinuumDaoCtmOnAssetsChain, isContinuumDaoLockChain, isContinuumDaoSupportedChainId, isContinuumDaoVeCtmOnAssetsChain, isCtmTokenSymbol, isForumIdeaSection, isGovNoOpAction, isVotingEscrowContinuumName, listContinuumDaoDefaultAssets, proposalDescriptionHash, summarizeLiveProposals, votingEscrowAddressOnEvmChain };
|
|
2109
|
+
//# sourceMappingURL=index.js.map
|
|
2110
|
+
//# sourceMappingURL=index.js.map
|