dexe-mcp 0.20.1 → 0.22.0
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/CHANGELOG.md +114 -0
- package/README.md +2 -1
- package/dexe-plugin/skills/dexe-create-dao/SKILL.md +8 -0
- package/dexe-plugin/skills/dexe-create-proposal/SKILL.md +47 -8
- package/dexe-plugin/skills/dexe-otc/SKILL.md +5 -0
- package/dexe-plugin/skills/dexe-vote-execute/SKILL.md +16 -3
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +49 -9
- package/dist/config.js.map +1 -1
- package/dist/env/schema.d.ts +27 -6
- package/dist/env/schema.d.ts.map +1 -1
- package/dist/env/schema.js +36 -6
- package/dist/env/schema.js.map +1 -1
- package/dist/index.js +35 -7
- package/dist/index.js.map +1 -1
- package/dist/lib/avatarUpload.d.ts +57 -0
- package/dist/lib/avatarUpload.d.ts.map +1 -0
- package/dist/lib/avatarUpload.js +95 -0
- package/dist/lib/avatarUpload.js.map +1 -0
- package/dist/lib/errors.d.ts +33 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +65 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/params.d.ts +8 -0
- package/dist/lib/params.d.ts.map +1 -0
- package/dist/lib/params.js +14 -0
- package/dist/lib/params.js.map +1 -0
- package/dist/lib/proposalBuilders.d.ts +30 -2
- package/dist/lib/proposalBuilders.d.ts.map +1 -1
- package/dist/lib/proposalBuilders.js +583 -17
- package/dist/lib/proposalBuilders.js.map +1 -1
- package/dist/lib/requireEnv.d.ts.map +1 -1
- package/dist/lib/requireEnv.js +7 -4
- package/dist/lib/requireEnv.js.map +1 -1
- package/dist/lib/signer.d.ts.map +1 -1
- package/dist/lib/signer.js +7 -2
- package/dist/lib/signer.js.map +1 -1
- package/dist/lib/txWait.d.ts +28 -0
- package/dist/lib/txWait.d.ts.map +1 -0
- package/dist/lib/txWait.js +63 -0
- package/dist/lib/txWait.js.map +1 -0
- package/dist/lib/units.d.ts +32 -0
- package/dist/lib/units.d.ts.map +1 -0
- package/dist/lib/units.js +64 -0
- package/dist/lib/units.js.map +1 -0
- package/dist/rpc.d.ts +29 -1
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +86 -22
- package/dist/rpc.js.map +1 -1
- package/dist/tools/dao.d.ts.map +1 -1
- package/dist/tools/dao.js +16 -12
- package/dist/tools/dao.js.map +1 -1
- package/dist/tools/daoCreate.d.ts.map +1 -1
- package/dist/tools/daoCreate.js +37 -7
- package/dist/tools/daoCreate.js.map +1 -1
- package/dist/tools/flow.d.ts +32 -1
- package/dist/tools/flow.d.ts.map +1 -1
- package/dist/tools/flow.js +405 -120
- package/dist/tools/flow.js.map +1 -1
- package/dist/tools/gate.js +2 -2
- package/dist/tools/gov.d.ts.map +1 -1
- package/dist/tools/gov.js +17 -4
- package/dist/tools/gov.js.map +1 -1
- package/dist/tools/inbox.d.ts.map +1 -1
- package/dist/tools/inbox.js +4 -2
- package/dist/tools/inbox.js.map +1 -1
- package/dist/tools/ipfs.d.ts.map +1 -1
- package/dist/tools/ipfs.js +43 -47
- package/dist/tools/ipfs.js.map +1 -1
- package/dist/tools/operationalContext.d.ts.map +1 -1
- package/dist/tools/operationalContext.js +39 -5
- package/dist/tools/operationalContext.js.map +1 -1
- package/dist/tools/otc.d.ts.map +1 -1
- package/dist/tools/otc.js +53 -13
- package/dist/tools/otc.js.map +1 -1
- package/dist/tools/predict.d.ts.map +1 -1
- package/dist/tools/predict.js +7 -4
- package/dist/tools/predict.js.map +1 -1
- package/dist/tools/proposal.d.ts.map +1 -1
- package/dist/tools/proposal.js +8 -4
- package/dist/tools/proposal.js.map +1 -1
- package/dist/tools/proposalBuild.d.ts +1 -0
- package/dist/tools/proposalBuild.d.ts.map +1 -1
- package/dist/tools/proposalBuild.js +2 -1
- package/dist/tools/proposalBuild.js.map +1 -1
- package/dist/tools/proposalBuildComplex.d.ts +7 -0
- package/dist/tools/proposalBuildComplex.d.ts.map +1 -1
- package/dist/tools/proposalBuildComplex.js +7 -7
- package/dist/tools/proposalBuildComplex.js.map +1 -1
- package/dist/tools/proposalBuildInternal.d.ts +21 -0
- package/dist/tools/proposalBuildInternal.d.ts.map +1 -1
- package/dist/tools/proposalBuildInternal.js +1 -1
- package/dist/tools/proposalBuildInternal.js.map +1 -1
- package/dist/tools/proposalBuildMore.d.ts +2 -0
- package/dist/tools/proposalBuildMore.d.ts.map +1 -1
- package/dist/tools/proposalBuildMore.js +2 -2
- package/dist/tools/proposalBuildMore.js.map +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +25 -16
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/subgraph.d.ts.map +1 -1
- package/dist/tools/subgraph.js +5 -3
- package/dist/tools/subgraph.js.map +1 -1
- package/dist/tools/txSend.d.ts.map +1 -1
- package/dist/tools/txSend.js +46 -6
- package/dist/tools/txSend.js.map +1 -1
- package/dist/tools/vote.d.ts.map +1 -1
- package/dist/tools/vote.js +7 -4
- package/dist/tools/vote.js.map +1 -1
- package/package.json +1 -1
package/dist/tools/flow.js
CHANGED
|
@@ -2,7 +2,10 @@ import { z } from "zod";
|
|
|
2
2
|
import { Contract, Interface } from "ethers";
|
|
3
3
|
import { RpcProvider } from "../rpc.js";
|
|
4
4
|
import { multicall } from "../lib/multicall.js";
|
|
5
|
-
import { PinataClient, fetchIpfs } from "../lib/ipfs.js";
|
|
5
|
+
import { PinataClient, fetchIpfs, toCidV1 } from "../lib/ipfs.js";
|
|
6
|
+
import { buildAvatarUrl, pinAvatarFromInput } from "../lib/avatarUpload.js";
|
|
7
|
+
import { checkAvatarCidBytes } from "../lib/imageSniff.js";
|
|
8
|
+
import { resolveGateways } from "./ipfs.js";
|
|
6
9
|
import { qrFallbackUrl, wcQrBlocks } from "../lib/qr.js";
|
|
7
10
|
import { markdownToSlate } from "../lib/markdownToSlate.js";
|
|
8
11
|
import { resolveChain } from "../config.js";
|
|
@@ -12,14 +15,20 @@ import { AddressBook, CONTRACT_NAMES } from "../lib/addresses.js";
|
|
|
12
15
|
import { classifyTreasuryActions, quorumPctFromRaw, judgeQuorum, treasuryExecuteAdvisory, TREASURY_RISK_ADVISORY, } from "../lib/quorumRisk.js";
|
|
13
16
|
import { GET_PROPOSALS_FRAGMENT, decodeProposalView } from "../lib/govProposalView.js";
|
|
14
17
|
import { resolveControllingHoldersVotedFor } from "../lib/controllingVoters.js";
|
|
15
|
-
import { PROPOSAL_BUILDERS, FLOW_PROPOSAL_TYPES } from "../lib/proposalBuilders.js";
|
|
18
|
+
import { PROPOSAL_BUILDERS, INTERNAL_PROPOSAL_BUILDERS, OFFCHAIN_FLOW_TYPES, FLOW_PROPOSAL_TYPES, } from "../lib/proposalBuilders.js";
|
|
19
|
+
import { GOV_VALIDATORS_CREATE_ABI } from "./proposalBuild.js";
|
|
16
20
|
import { PROPOSAL_CATALOG } from "../lib/proposalCatalog.js";
|
|
17
21
|
import { checkProposalMetadata, proposalStateName } from "../lib/preflight.js";
|
|
22
|
+
import { waitWithTimeout, assertReceiptSuccess, txWaitTimeoutMs } from "../lib/txWait.js";
|
|
23
|
+
import { toActionableError } from "../lib/errors.js";
|
|
24
|
+
import { parseAmount, formatAmount } from "../lib/units.js";
|
|
18
25
|
// ---------- ABI fragments ----------
|
|
19
26
|
const ERC20_ABI = new Interface([
|
|
20
27
|
"function balanceOf(address) view returns (uint256)",
|
|
21
28
|
"function allowance(address owner, address spender) view returns (uint256)",
|
|
22
29
|
"function approve(address spender, uint256 amount) returns (bool)",
|
|
30
|
+
"function decimals() view returns (uint8)",
|
|
31
|
+
"function symbol() view returns (string)",
|
|
23
32
|
]);
|
|
24
33
|
const GOV_POOL_ABI = new Interface([
|
|
25
34
|
"function getHelperContracts() view returns (address settings, address userKeeper, address validators, address poolRegistry, address votePower)",
|
|
@@ -55,6 +64,21 @@ function ok(data) {
|
|
|
55
64
|
function bigintReplacer(_k, v) {
|
|
56
65
|
return typeof v === "bigint" ? v.toString() : v;
|
|
57
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Uniform composite-failure response (R7): failed step + actionable error +
|
|
69
|
+
* ledger of steps that already landed (gas spent) + how to resume.
|
|
70
|
+
*/
|
|
71
|
+
export function flowFailureResult(result, extra) {
|
|
72
|
+
return {
|
|
73
|
+
content: [
|
|
74
|
+
{
|
|
75
|
+
type: "text",
|
|
76
|
+
text: JSON.stringify({ mode: "failed", ...(extra ?? {}), failure: result.failure, steps: result.steps }, bigintReplacer, 2),
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
isError: true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
58
82
|
function makeTxPayload(to, iface, method, args, chainId, description, value) {
|
|
59
83
|
return {
|
|
60
84
|
to,
|
|
@@ -208,14 +232,18 @@ async function resolvePrereqs(rpc, govPool, user, config, chainId) {
|
|
|
208
232
|
const [balance, ownedBalance] = res2[2].value;
|
|
209
233
|
depositedPower = balance - ownedBalance;
|
|
210
234
|
}
|
|
211
|
-
// Batch 3: ERC20 balance + allowance
|
|
235
|
+
// Batch 3: ERC20 balance + allowance + display metadata (best-effort)
|
|
212
236
|
const batch3 = [
|
|
213
237
|
{ target: tokenAddress, iface: ERC20_ABI, method: "balanceOf", args: [user] },
|
|
214
238
|
{ target: tokenAddress, iface: ERC20_ABI, method: "allowance", args: [user, userKeeper] },
|
|
239
|
+
{ target: tokenAddress, iface: ERC20_ABI, method: "decimals", args: [], allowFailure: true },
|
|
240
|
+
{ target: tokenAddress, iface: ERC20_ABI, method: "symbol", args: [], allowFailure: true },
|
|
215
241
|
];
|
|
216
242
|
const res3 = await multicall(provider, batch3);
|
|
217
243
|
const walletBalance = res3[0].success ? res3[0].value : 0n;
|
|
218
244
|
const currentAllowance = res3[1].success ? res3[1].value : 0n;
|
|
245
|
+
const tokenDecimals = res3[2].success ? Number(res3[2].value) : 18;
|
|
246
|
+
const tokenSymbol = res3[3].success ? String(res3[3].value) : "";
|
|
219
247
|
return {
|
|
220
248
|
userKeeper,
|
|
221
249
|
settings,
|
|
@@ -225,6 +253,8 @@ async function resolvePrereqs(rpc, govPool, user, config, chainId) {
|
|
|
225
253
|
depositedPower,
|
|
226
254
|
minVotesForCreating: defaultSettings.minVotesForCreating,
|
|
227
255
|
minVotesForVoting: defaultSettings.minVotesForVoting,
|
|
256
|
+
tokenDecimals,
|
|
257
|
+
tokenSymbol,
|
|
228
258
|
};
|
|
229
259
|
}
|
|
230
260
|
/**
|
|
@@ -331,29 +361,55 @@ export async function sendOrCollect(signer, payloads, opts) {
|
|
|
331
361
|
const wallet = sg.ok;
|
|
332
362
|
const cfg = signer.getConfig();
|
|
333
363
|
for (const p of payloads) {
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
364
|
+
// Any step failing mid-sequence: STOP (dependent steps must not run on top
|
|
365
|
+
// of unchanged state — R3), report which steps already landed (gas spent),
|
|
366
|
+
// and tell the caller how to resume (R7). Composites re-check completed
|
|
367
|
+
// work (allowance, deposited power, proposal state) on re-run, so "fix the
|
|
368
|
+
// cause and re-run this same call" is the correct resume for every flow.
|
|
369
|
+
try {
|
|
370
|
+
// Same B6/B7/B10 broadcast guards as dexe_tx_send. B9 simulation is skipped:
|
|
371
|
+
// these payloads are an ordered, *dependent* sequence, so simming a later
|
|
372
|
+
// step against pre-sequence state would falsely revert. A BroadcastGuardError
|
|
373
|
+
// aborts the flow before the offending send (gas spent only on prior steps).
|
|
374
|
+
await runBroadcastGuards({
|
|
375
|
+
to: p.to,
|
|
376
|
+
data: p.data,
|
|
377
|
+
value: p.value,
|
|
378
|
+
chainId: Number(p.chainId),
|
|
379
|
+
from: wallet.address,
|
|
380
|
+
}, cfg, { skipSimulation: true });
|
|
381
|
+
const tx = await signer.withBroadcastLock(Number(p.chainId), () => wallet.sendTransaction({
|
|
382
|
+
to: p.to,
|
|
383
|
+
data: p.data,
|
|
384
|
+
value: BigInt(p.value),
|
|
385
|
+
chainId: BigInt(p.chainId),
|
|
386
|
+
}));
|
|
387
|
+
const receipt = await waitWithTimeout(tx, { timeoutMs: txWaitTimeoutMs() });
|
|
388
|
+
assertReceiptSuccess(receipt, p.description);
|
|
389
|
+
steps.push({
|
|
390
|
+
label: p.description,
|
|
391
|
+
skipped: false,
|
|
392
|
+
txHash: receipt?.hash ?? tx.hash,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
catch (e) {
|
|
396
|
+
const landed = steps.filter((s) => s.txHash);
|
|
397
|
+
const actionable = toActionableError(e, p.description);
|
|
398
|
+
return {
|
|
399
|
+
mode: "failed",
|
|
400
|
+
steps,
|
|
401
|
+
failure: {
|
|
402
|
+
failedStep: p.description,
|
|
403
|
+
error: actionable.message,
|
|
404
|
+
landedSteps: landed,
|
|
405
|
+
resume: landed.length > 0
|
|
406
|
+
? `${landed.length} earlier step(s) already landed on-chain (see landedSteps txHashes). ` +
|
|
407
|
+
"Fix the cause above and re-run this same call — already-satisfied steps (approve / deposit / vote) " +
|
|
408
|
+
"are detected on-chain and skipped automatically."
|
|
409
|
+
: "No steps landed on-chain. Fix the cause above and re-run this same call.",
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
}
|
|
357
413
|
}
|
|
358
414
|
return { mode: "executed", steps };
|
|
359
415
|
}
|
|
@@ -372,6 +428,24 @@ export async function runProposalCreate(inputRaw, deps) {
|
|
|
372
428
|
...inputRaw,
|
|
373
429
|
};
|
|
374
430
|
const { ctx, signer, rpc } = deps;
|
|
431
|
+
// Off-chain proposal types live on the DeXe backend, not on any contract —
|
|
432
|
+
// reject with the exact alternative flow instead of a dead-end.
|
|
433
|
+
if (OFFCHAIN_FLOW_TYPES.includes(input.proposalType)) {
|
|
434
|
+
const buildTool = `dexe_proposal_build_${input.proposalType}`;
|
|
435
|
+
return err(`proposalType '${input.proposalType}' is an OFF-CHAIN proposal — it is created on the DeXe backend ` +
|
|
436
|
+
`(api.dexe.io), not on-chain, so this composite cannot broadcast it. Flow instead:\n` +
|
|
437
|
+
`1) ${buildTool} → returns the ready-to-send HTTP request (JSON:API body).\n` +
|
|
438
|
+
`2) Authenticate: dexe_auth_request_nonce (get the message), sign it with the user's wallet, ` +
|
|
439
|
+
`dexe_auth_login_request (exchange for access_token).\n` +
|
|
440
|
+
`3) Send the request with 'Authorization: Bearer <access_token>'.\n` +
|
|
441
|
+
`Note: the backend indexes BSC mainnet (56) DAOs only.`);
|
|
442
|
+
}
|
|
443
|
+
// Internal proposal types are created on GovValidators (validators-only
|
|
444
|
+
// voting, no token deposit) — a different single-tx path.
|
|
445
|
+
const internalBuilder = INTERNAL_PROPOSAL_BUILDERS[input.proposalType];
|
|
446
|
+
if (internalBuilder) {
|
|
447
|
+
return runInternalProposalCreate(input, deps, internalBuilder);
|
|
448
|
+
}
|
|
375
449
|
if (!ctx.config.pinataJwt)
|
|
376
450
|
return err(pinataUploadHint("to create a proposal"));
|
|
377
451
|
const user = input.user ?? (signer.hasSigner() ? signer.getAddress() : undefined);
|
|
@@ -387,7 +461,10 @@ export async function runProposalCreate(inputRaw, deps) {
|
|
|
387
461
|
prereqs = await resolvePrereqs(rpc, govPool, user, ctx.config, chainId);
|
|
388
462
|
}
|
|
389
463
|
catch (e) {
|
|
390
|
-
|
|
464
|
+
const a = toActionableError(e, "resolve DAO prerequisites");
|
|
465
|
+
return err(a.slug
|
|
466
|
+
? a.message
|
|
467
|
+
: `${a.message}\nIf this repeats, verify the govPool address is a DeXe GovPool on chain ${chainId} (dexe_dao_info) — a wrong address or chain yields exactly this read failure.`);
|
|
391
468
|
}
|
|
392
469
|
// Mode 6 guard: the auto-approve targets the UserKeeper (which does
|
|
393
470
|
// transferFrom on deposit). If the resolved keeper collapses onto the
|
|
@@ -399,7 +476,12 @@ export async function runProposalCreate(inputRaw, deps) {
|
|
|
399
476
|
// Step 2: check creation threshold
|
|
400
477
|
const totalAvailable = prereqs.walletBalance + prereqs.depositedPower;
|
|
401
478
|
if (prereqs.minVotesForCreating > 0n && totalAvailable < prereqs.minVotesForCreating) {
|
|
402
|
-
|
|
479
|
+
const d = prereqs.tokenDecimals;
|
|
480
|
+
const sym = prereqs.tokenSymbol;
|
|
481
|
+
return err(`Insufficient tokens to create a proposal on this DAO. The DAO requires ${formatAmount(prereqs.minVotesForCreating, d, sym)} ` +
|
|
482
|
+
`but ${user} has ${formatAmount(totalAvailable, d, sym)} total (wallet ${formatAmount(prereqs.walletBalance, d, sym)}, ` +
|
|
483
|
+
`deposited ${formatAmount(prereqs.depositedPower, d, sym)}). ` +
|
|
484
|
+
`Next step: acquire more ${sym || "gov tokens"} (token ${prereqs.tokenAddress}), or have a holder with enough tokens create the proposal.`);
|
|
403
485
|
}
|
|
404
486
|
// Step 3: build actions + metadata based on type
|
|
405
487
|
let actionsOnFor;
|
|
@@ -467,7 +549,7 @@ export async function runProposalCreate(inputRaw, deps) {
|
|
|
467
549
|
" to merge partial update — refusing to broadcast (would blank unspecified fields). " +
|
|
468
550
|
(currentMetaFetchError ? `Last error: ${currentMetaFetchError}. ` : "") +
|
|
469
551
|
"Either set DEXE_IPFS_GATEWAY to a reachable gateway or pass all fields explicitly " +
|
|
470
|
-
"(newDaoName, newWebsiteUrl, newDaoDescription, newSocialLinks, newAvatarCID/newAvatarFileName).");
|
|
552
|
+
"(newDaoName, newWebsiteUrl, newDaoDescription, newSocialLinks, newAvatarPath or newAvatarCID/newAvatarFileName).");
|
|
471
553
|
}
|
|
472
554
|
// Decide which description body to use:
|
|
473
555
|
// - if caller passed newDaoDescription (or generic description), upload fresh
|
|
@@ -488,14 +570,35 @@ export async function runProposalCreate(inputRaw, deps) {
|
|
|
488
570
|
socialLinks: input.newSocialLinks ?? (Array.isArray(currentMeta.socialLinks) ? currentMeta.socialLinks : []),
|
|
489
571
|
documents: Array.isArray(currentMeta.documents) ? currentMeta.documents : [],
|
|
490
572
|
};
|
|
491
|
-
if (input.newAvatarCID) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
|
|
498
|
-
|
|
573
|
+
if ((input.newAvatarPath || input.newAvatarBase64) && input.newAvatarCID) {
|
|
574
|
+
return err("Pass either `newAvatarCID` or `newAvatarPath`/`newAvatarBase64`, not both.");
|
|
575
|
+
}
|
|
576
|
+
if (input.newAvatarPath || input.newAvatarBase64) {
|
|
577
|
+
// One-call avatar rotation: read + validate (magic bytes) + pin the
|
|
578
|
+
// image server-side. The agent should never read image files itself.
|
|
579
|
+
const pinned = await pinAvatarFromInput({
|
|
580
|
+
filePath: input.newAvatarPath,
|
|
581
|
+
base64: input.newAvatarBase64,
|
|
582
|
+
pinata,
|
|
583
|
+
});
|
|
584
|
+
daoMeta.avatarCID = pinned.avatarCID;
|
|
585
|
+
daoMeta.avatarFileName = pinned.avatarFileName;
|
|
586
|
+
daoMeta.avatarUrl = pinned.avatarUrl;
|
|
587
|
+
}
|
|
588
|
+
else if (input.newAvatarCID) {
|
|
589
|
+
// By-reference CID — the local byte gate never saw these bytes, so
|
|
590
|
+
// best-effort fetch + sniff (hard-block only on confirmed non-raster).
|
|
591
|
+
const avatarCidV1 = toCidV1(input.newAvatarCID);
|
|
592
|
+
const avatarFileName = input.newAvatarFileName ?? "avatar.jpeg";
|
|
593
|
+
const check = await checkAvatarCidBytes(avatarCidV1, avatarFileName, resolveGateways(ctx));
|
|
594
|
+
if (!check.ok)
|
|
595
|
+
return err(check.error ?? "newAvatarCID failed raster validation");
|
|
596
|
+
daoMeta.avatarCID = avatarCidV1;
|
|
597
|
+
daoMeta.avatarFileName = avatarFileName;
|
|
598
|
+
// The frontend rebuilds the URL itself (parseAvatarFromIpfsResponse)
|
|
599
|
+
// so the field is informational, but the CID + filename pair is
|
|
600
|
+
// load-bearing.
|
|
601
|
+
daoMeta.avatarUrl = buildAvatarUrl(avatarCidV1, avatarFileName);
|
|
499
602
|
}
|
|
500
603
|
const daoMetaRes = await pinata.pinJson(daoMeta, { name: `dao-meta:${govPool.slice(0, 10)}` });
|
|
501
604
|
const newDescriptionURL = `ipfs://${daoMetaRes.cid}`;
|
|
@@ -559,7 +662,7 @@ export async function runProposalCreate(inputRaw, deps) {
|
|
|
559
662
|
built = await builder.build(parsed.data, { ctx, govPool, chainId });
|
|
560
663
|
}
|
|
561
664
|
catch (e) {
|
|
562
|
-
return err(`
|
|
665
|
+
return err(toActionableError(e, `build ${input.proposalType} actions`).message);
|
|
563
666
|
}
|
|
564
667
|
actionsOnFor = built.actionsOnFor.map((a) => ({
|
|
565
668
|
executor: a.executor,
|
|
@@ -588,14 +691,28 @@ export async function runProposalCreate(inputRaw, deps) {
|
|
|
588
691
|
// Step 5: build tx payloads
|
|
589
692
|
const payloads = [];
|
|
590
693
|
const skippedSteps = [];
|
|
591
|
-
// Determine how much to deposit
|
|
592
|
-
|
|
694
|
+
// Determine how much to deposit. voteAmount accepts raw wei (digits-only)
|
|
695
|
+
// or human units with a decimal point, scaled by the gov token's decimals.
|
|
696
|
+
let voteAmount;
|
|
697
|
+
try {
|
|
698
|
+
voteAmount = input.voteAmount
|
|
699
|
+
? parseAmount(input.voteAmount, prereqs.tokenDecimals)
|
|
700
|
+
: prereqs.depositedPower + prereqs.walletBalance;
|
|
701
|
+
}
|
|
702
|
+
catch (e) {
|
|
703
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
704
|
+
}
|
|
593
705
|
if (voteAmount === 0n) {
|
|
594
|
-
return err(
|
|
706
|
+
return err(`No voting power available — ${user} holds 0 ${prereqs.tokenSymbol || "gov tokens"} (wallet + deposited). ` +
|
|
707
|
+
`Acquire the DAO's gov token (${prereqs.tokenAddress}) first, then re-run.`);
|
|
595
708
|
}
|
|
596
709
|
const needDeposit = voteAmount > prereqs.depositedPower ? voteAmount - prereqs.depositedPower : 0n;
|
|
597
710
|
if (needDeposit > prereqs.walletBalance) {
|
|
598
|
-
|
|
711
|
+
const d = prereqs.tokenDecimals;
|
|
712
|
+
const sym = prereqs.tokenSymbol;
|
|
713
|
+
return err(`Not enough tokens: voting with ${formatAmount(voteAmount, d, sym)} needs a deposit of ${formatAmount(needDeposit, d, sym)} ` +
|
|
714
|
+
`but the wallet only holds ${formatAmount(prereqs.walletBalance, d, sym)}. ` +
|
|
715
|
+
`Next step: lower voteAmount to at most ${formatAmount(prereqs.depositedPower + prereqs.walletBalance, d, sym)}, or acquire more tokens.`);
|
|
599
716
|
}
|
|
600
717
|
// Approve (if needed)
|
|
601
718
|
if (needDeposit > 0n && prereqs.currentAllowance < needDeposit) {
|
|
@@ -607,45 +724,25 @@ export async function runProposalCreate(inputRaw, deps) {
|
|
|
607
724
|
else {
|
|
608
725
|
skippedSteps.push({ label: "ERC20.approve", skipped: true, reason: "Allowance sufficient" });
|
|
609
726
|
}
|
|
610
|
-
//
|
|
611
|
-
|
|
612
|
-
//
|
|
727
|
+
// Deposit (if needed) — a SEPARATE tx, never bundled. Newly deployed
|
|
728
|
+
// pools ship with SphereX protection that rejects the old
|
|
729
|
+
// multicall([deposit, createProposalAndVote]) wrap with
|
|
730
|
+
// "SphereX error: disallowed tx pattern" (verified live on chain 97,
|
|
731
|
+
// v0.22). Sequential txs pass, and the failure ledger makes the
|
|
732
|
+
// two-step sequence safely resumable.
|
|
613
733
|
if (needDeposit > 0n) {
|
|
614
|
-
|
|
734
|
+
payloads.push(makeTxPayload(govPool, GOV_POOL_ABI, "deposit", [needDeposit, []], chainId, `GovPool.deposit(${needDeposit})`));
|
|
615
735
|
}
|
|
616
736
|
else {
|
|
617
737
|
skippedSteps.push({ label: "GovPool.deposit", skipped: true, reason: "Sufficient deposited power" });
|
|
618
738
|
}
|
|
619
|
-
// createProposalAndVote
|
|
620
739
|
const actionsForTuple = actionsOnFor.map(a => [a.executor, a.value, a.data]);
|
|
621
|
-
|
|
622
|
-
descriptionURL,
|
|
623
|
-
actionsForTuple,
|
|
624
|
-
[], // actionsOnAgainst
|
|
625
|
-
voteAmount,
|
|
626
|
-
input.voteNftIds.map(id => BigInt(id)),
|
|
627
|
-
]));
|
|
628
|
-
// Wrap in multicall if >1 call, otherwise single tx
|
|
629
|
-
if (govPoolCalls.length > 1) {
|
|
630
|
-
payloads.push({
|
|
631
|
-
to: govPool,
|
|
632
|
-
data: GOV_POOL_ABI.encodeFunctionData("multicall", [govPoolCalls]),
|
|
633
|
-
value: "0",
|
|
634
|
-
chainId,
|
|
635
|
-
description: `GovPool.multicall([deposit, createProposalAndVote])`,
|
|
636
|
-
});
|
|
637
|
-
}
|
|
638
|
-
else {
|
|
639
|
-
payloads.push({
|
|
640
|
-
to: govPool,
|
|
641
|
-
data: govPoolCalls[0],
|
|
642
|
-
value: "0",
|
|
643
|
-
chainId,
|
|
644
|
-
description: `GovPool.createProposalAndVote("${input.title}")`,
|
|
645
|
-
});
|
|
646
|
-
}
|
|
740
|
+
payloads.push(makeTxPayload(govPool, GOV_POOL_ABI, "createProposalAndVote", [descriptionURL, actionsForTuple, [], voteAmount, input.voteNftIds.map(id => BigInt(id))], chainId, `GovPool.createProposalAndVote("${input.title}")`));
|
|
647
741
|
// Step 6: send or return
|
|
648
742
|
const result = await sendOrCollect(signer, payloads, { dryRun: input.dryRun, chainId, wc: deps.wc });
|
|
743
|
+
if (result.mode === "failed") {
|
|
744
|
+
return flowFailureResult(result, { descriptionURL, proposalMetadataCID: proposalMetaRes.cid });
|
|
745
|
+
}
|
|
649
746
|
// Phase 3: record a broadcast proposal so dexe_context surfaces it next
|
|
650
747
|
// session. Best-effort — a state-write error never breaks the broadcast.
|
|
651
748
|
if (result.mode === "executed" && deps.state) {
|
|
@@ -680,24 +777,147 @@ export async function runProposalCreate(inputRaw, deps) {
|
|
|
680
777
|
...(result.pairing ? { pairing: result.pairing } : {}),
|
|
681
778
|
}), result.pairingContent);
|
|
682
779
|
}
|
|
780
|
+
/**
|
|
781
|
+
* v0.22 — internal-proposal path of `dexe_proposal_create`. Internal proposals
|
|
782
|
+
* (change_validator_balances / change_validator_settings / monthly_withdraw /
|
|
783
|
+
* offchain_internal_proposal) are created on GovValidators via
|
|
784
|
+
* `createInternalProposal(uint8, descriptionURL, bytes)` — validators vote with
|
|
785
|
+
* their own balances, so there is no approve/deposit sequence. Only a current
|
|
786
|
+
* validator can create one; the response notes that requirement.
|
|
787
|
+
*/
|
|
788
|
+
async function runInternalProposalCreate(inputRaw, deps, builder) {
|
|
789
|
+
const input = { proposalType: "custom", description: "", ...inputRaw };
|
|
790
|
+
const { ctx, signer, rpc } = deps;
|
|
791
|
+
if (!ctx.config.pinataJwt)
|
|
792
|
+
return err(pinataUploadHint("to create an internal proposal"));
|
|
793
|
+
const parsed = builder.schema.safeParse(input.params ?? {});
|
|
794
|
+
if (!parsed.success) {
|
|
795
|
+
return err(`Invalid params for proposalType '${input.proposalType}': ` +
|
|
796
|
+
parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; "));
|
|
797
|
+
}
|
|
798
|
+
let built;
|
|
799
|
+
try {
|
|
800
|
+
built = builder.build(parsed.data);
|
|
801
|
+
}
|
|
802
|
+
catch (e) {
|
|
803
|
+
return err(toActionableError(e, `build ${input.proposalType}`).message);
|
|
804
|
+
}
|
|
805
|
+
const chain = resolveChain(ctx.config, input.chainId);
|
|
806
|
+
const chainId = chain.chainId;
|
|
807
|
+
const govPool = input.govPool;
|
|
808
|
+
const pr = rpc.tryProvider(chainId);
|
|
809
|
+
if ("error" in pr)
|
|
810
|
+
return err(`${pr.error}\n${pr.remediation}`);
|
|
811
|
+
const provider = pr.ok;
|
|
812
|
+
// W10: same registered-pool check as the external flow.
|
|
813
|
+
try {
|
|
814
|
+
await assertRegisteredGovPool(provider, rpc, ctx.config, chainId, govPool);
|
|
815
|
+
}
|
|
816
|
+
catch (e) {
|
|
817
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
818
|
+
}
|
|
819
|
+
// Resolve the GovValidators helper from the pool.
|
|
820
|
+
let validators;
|
|
821
|
+
try {
|
|
822
|
+
const res = await multicall(provider, [
|
|
823
|
+
{ target: govPool, iface: GOV_POOL_ABI, method: "getHelperContracts", args: [] },
|
|
824
|
+
]);
|
|
825
|
+
if (!res[0].success)
|
|
826
|
+
throw new Error(res[0].error ?? "getHelperContracts reverted");
|
|
827
|
+
const helpers = res[0].value;
|
|
828
|
+
validators = helpers[2];
|
|
829
|
+
}
|
|
830
|
+
catch (e) {
|
|
831
|
+
return err(toActionableError(e, "resolve GovValidators").message);
|
|
832
|
+
}
|
|
833
|
+
// Metadata shape mirrors dexe_proposal_build_change_validator_* exactly
|
|
834
|
+
// (internal metadata carries no isMeta field).
|
|
835
|
+
const pinata = new PinataClient(ctx.config.pinataJwt);
|
|
836
|
+
const proposalMeta = {
|
|
837
|
+
proposalName: input.title,
|
|
838
|
+
proposalDescription: JSON.stringify(markdownToSlate(input.description)),
|
|
839
|
+
category: built.category,
|
|
840
|
+
...built.metadataExtra,
|
|
841
|
+
};
|
|
842
|
+
let cid;
|
|
843
|
+
try {
|
|
844
|
+
const res = await pinata.pinJson(proposalMeta, { name: `proposal:${input.title.slice(0, 30)}` });
|
|
845
|
+
cid = res.cid;
|
|
846
|
+
}
|
|
847
|
+
catch (e) {
|
|
848
|
+
return err(toActionableError(e, "upload internal-proposal metadata").message);
|
|
849
|
+
}
|
|
850
|
+
const descriptionURL = `ipfs://${cid}`;
|
|
851
|
+
const validatorsIface = new Interface(GOV_VALIDATORS_CREATE_ABI);
|
|
852
|
+
const payloads = [
|
|
853
|
+
makeTxPayload(validators, validatorsIface, "createInternalProposal", [built.internalType, descriptionURL, built.data], chainId, `GovValidators.createInternalProposal(${built.summary})`),
|
|
854
|
+
];
|
|
855
|
+
const result = await sendOrCollect(signer, payloads, { dryRun: input.dryRun, chainId, wc: deps.wc });
|
|
856
|
+
if (result.mode === "failed") {
|
|
857
|
+
return flowFailureResult(result, {
|
|
858
|
+
proposalKind: "internal",
|
|
859
|
+
descriptionURL,
|
|
860
|
+
note: "Internal proposals can only be created by a CURRENT validator of this DAO — a non-validator sender reverts.",
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
if (result.mode === "executed" && deps.state) {
|
|
864
|
+
try {
|
|
865
|
+
const txHash = [...result.steps].reverse().find((s) => s.txHash)?.txHash;
|
|
866
|
+
deps.state.recordProposal({
|
|
867
|
+
govPool,
|
|
868
|
+
chainId,
|
|
869
|
+
title: input.title,
|
|
870
|
+
descriptionURL,
|
|
871
|
+
txHash,
|
|
872
|
+
createdAt: new Date().toISOString(),
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
catch {
|
|
876
|
+
/* ignore */
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return attachPairingQr(ok({
|
|
880
|
+
mode: result.mode,
|
|
881
|
+
proposalKind: "internal",
|
|
882
|
+
validators,
|
|
883
|
+
internalType: built.internalType,
|
|
884
|
+
descriptionURL,
|
|
885
|
+
proposalMetadataCID: cid,
|
|
886
|
+
summary: built.summary,
|
|
887
|
+
steps: result.steps,
|
|
888
|
+
note: "Internal proposals are created and voted on by the DAO's validators only (their own validator balances — " +
|
|
889
|
+
"no token deposit). The sender must be a current validator or the tx reverts.",
|
|
890
|
+
...(result.enableWrites ? { enableWrites: result.enableWrites } : {}),
|
|
891
|
+
...(result.pairing ? { pairing: result.pairing } : {}),
|
|
892
|
+
}), result.pairingContent);
|
|
893
|
+
}
|
|
683
894
|
// ---------- register ----------
|
|
684
895
|
export function registerFlowTools(server, ctx, signer, wc, state) {
|
|
685
896
|
const rpc = new RpcProvider(ctx.config);
|
|
686
897
|
// =============================================
|
|
687
898
|
// dexe_proposal_create — thin shim around runProposalCreate
|
|
688
899
|
// =============================================
|
|
689
|
-
server.tool("dexe_proposal_create", "Create
|
|
690
|
-
"sequence, uploads correct IPFS metadata (category/isMeta/changes), signs+broadcasts when " +
|
|
691
|
-
"
|
|
692
|
-
"proposalType
|
|
693
|
-
"• 'modify_dao_profile' —
|
|
694
|
-
"
|
|
695
|
-
"•
|
|
900
|
+
server.tool("dexe_proposal_create", "Create ANY governance proposal in ONE call — handles the whole approve→deposit→createProposalAndVote " +
|
|
901
|
+
"sequence, uploads correct IPFS metadata (category/isMeta/changes), signs+broadcasts when a signer is " +
|
|
902
|
+
"configured (else returns ordered TxPayloads + a WalletConnect QR).\n\n" +
|
|
903
|
+
"proposalType (every DeXe catalog type is wired):\n" +
|
|
904
|
+
"• 'modify_dao_profile' — top-level fields (newDaoName/newDaoDescription/newWebsiteUrl/newSocialLinks; avatar via " +
|
|
905
|
+
"newAvatarPath — a local image path the server uploads itself — or newAvatarCID).\n" +
|
|
906
|
+
"• 'custom' — your own actionsOnFor [{executor,value,data}] (+ optional category).\n" +
|
|
907
|
+
"• On-chain external types (inputs go in `params`): 'token_transfer' {token,recipient,amount,isNative?}, " +
|
|
696
908
|
"'withdraw_treasury' {receiver,token?,amount?,nftAddress?,nftIds?}, 'change_voting_settings' {govSettings,settings[],settingsIds?}, " +
|
|
697
|
-
"'add_expert' {expertNftContract,scope,nominatedUser,uri?}, '
|
|
698
|
-
"'
|
|
699
|
-
"'
|
|
700
|
-
"
|
|
909
|
+
"'add_expert'/'remove_expert' {expertNftContract,scope,nominatedUser,uri?}, 'token_distribution', 'token_sale', " +
|
|
910
|
+
"'token_sale_whitelist' {tokenSaleProposal,requests[]}, 'token_sale_recover' {tokenSaleProposal,tierIds[]}, " +
|
|
911
|
+
"'manage_validators'/'validators_allocation' {govValidators,changes[{user,balance}]}, " +
|
|
912
|
+
"'delegate_to_expert'/'revoke_from_expert' {expert,amount,nftIds?}, 'create_staking_tier', " +
|
|
913
|
+
"'change_math_model' {newVotePower}, 'blacklist' {erc20Gov,addAddresses?,removeAddresses?}, " +
|
|
914
|
+
"'reward_multiplier' {mode,...}, 'apply_to_dao' {token,receiver,amount,treasuryBalance?}, " +
|
|
915
|
+
"'new_proposal_type'/'enable_staking' {govSettings,settings,executors,newSettingId}, 'custom_abi' {target,signature,method,args?}.\n" +
|
|
916
|
+
"• Internal (validators-only, auto-routed to GovValidators.createInternalProposal): " +
|
|
917
|
+
"'change_validator_balances' {changes[]}, 'change_validator_settings' {duration,executionDelay,quorum}, " +
|
|
918
|
+
"'monthly_withdraw' {withdrawals[],destination}, 'offchain_internal_proposal' {}.\n" +
|
|
919
|
+
"• Off-chain backend types ('offchain_single_option' etc.) are rejected with the exact backend flow to use instead.\n" +
|
|
920
|
+
"Full per-type recipes with examples: docs/PLAYBOOK.md (dexe://playbook resource) or dexe_proposal_catalog.", {
|
|
701
921
|
govPool: z.string().describe("GovPool contract address"),
|
|
702
922
|
chainId: z
|
|
703
923
|
.number()
|
|
@@ -706,13 +926,13 @@ export function registerFlowTools(server, ctx, signer, wc, state) {
|
|
|
706
926
|
.optional()
|
|
707
927
|
.describe("Target chain id. Defaults to the MCP's default chain. Rejects if no RPC is configured for the requested chain."),
|
|
708
928
|
proposalType: z
|
|
709
|
-
.
|
|
929
|
+
.enum(FLOW_PROPOSAL_TYPES)
|
|
710
930
|
.default("custom")
|
|
711
|
-
.describe("
|
|
931
|
+
.describe("One of the wired types listed in the tool description. Unknown values are rejected with the valid list."),
|
|
712
932
|
params: z
|
|
713
933
|
.record(z.unknown())
|
|
714
934
|
.optional()
|
|
715
|
-
.describe("Type-specific builder inputs for
|
|
935
|
+
.describe("Type-specific builder inputs for the chosen proposalType (recipes: tool description / dexe://playbook)."),
|
|
716
936
|
title: z.string().describe("Proposal title"),
|
|
717
937
|
description: z.string().default("").describe("Proposal description (markdown supported)"),
|
|
718
938
|
newDaoName: z.string().optional(),
|
|
@@ -720,6 +940,9 @@ export function registerFlowTools(server, ctx, signer, wc, state) {
|
|
|
720
940
|
newWebsiteUrl: z.string().optional(),
|
|
721
941
|
newAvatarCID: z.string().optional(),
|
|
722
942
|
newAvatarFileName: z.string().optional(),
|
|
943
|
+
newAvatarPath: z.string().optional().describe("Local image path for the new avatar (JPEG/PNG/WebP/GIF, max 10 MB) — the server uploads + validates it. " +
|
|
944
|
+
"Preferred over reading the file yourself; replaces the separate dexe_ipfs_upload_avatar call."),
|
|
945
|
+
newAvatarBase64: z.string().optional().describe("Base64 image bytes — only when the image isn't a local file."),
|
|
723
946
|
newSocialLinks: z.array(z.tuple([z.string(), z.string()])).optional(),
|
|
724
947
|
actionsOnFor: z.array(z.object({
|
|
725
948
|
executor: z.string(),
|
|
@@ -728,7 +951,10 @@ export function registerFlowTools(server, ctx, signer, wc, state) {
|
|
|
728
951
|
})).default([]).describe("Actions for custom proposals"),
|
|
729
952
|
category: z.string().optional().describe("Proposal category (included in IPFS metadata)."),
|
|
730
953
|
proposalMetadataExtra: z.record(z.unknown()).optional().describe("Extra fields merged into IPFS metadata."),
|
|
731
|
-
voteAmount: z
|
|
954
|
+
voteAmount: z
|
|
955
|
+
.string()
|
|
956
|
+
.optional()
|
|
957
|
+
.describe("Auto-vote amount: raw wei (digits-only) OR human units with a decimal point ('12.5', scaled by the gov token's decimals). Defaults to all available power."),
|
|
732
958
|
voteNftIds: z.array(z.string()).default([]),
|
|
733
959
|
user: z.string().optional().describe("User address. Required when DEXE_PRIVATE_KEY not set."),
|
|
734
960
|
dryRun: z.boolean().default(false).describe("If true, return ordered TxPayloads even when DEXE_PRIVATE_KEY is set."),
|
|
@@ -736,10 +962,10 @@ export function registerFlowTools(server, ctx, signer, wc, state) {
|
|
|
736
962
|
// =============================================
|
|
737
963
|
// dexe_proposal_vote_and_execute
|
|
738
964
|
// =============================================
|
|
739
|
-
server.tool("dexe_proposal_vote_and_execute", "Vote on a proposal and optionally execute it. " +
|
|
740
|
-
"Checks proposal state,
|
|
741
|
-
"
|
|
742
|
-
"
|
|
965
|
+
server.tool("dexe_proposal_vote_and_execute", "Vote on a proposal and optionally execute it — the ONE call for 'vote on / pass / execute proposal N'. " +
|
|
966
|
+
"Checks proposal state, AUTO-DEPOSITS wallet tokens when voting power is short (approve UserKeeper → deposit → vote, " +
|
|
967
|
+
"matching the frontend's bundled deposit+vote), and when autoExecute is true executes after the vote passes. " +
|
|
968
|
+
"Signs+broadcasts when a signer is configured; otherwise returns ordered TxPayloads + a WalletConnect QR.", {
|
|
743
969
|
govPool: z.string().describe("GovPool contract address"),
|
|
744
970
|
chainId: z
|
|
745
971
|
.number()
|
|
@@ -749,9 +975,17 @@ export function registerFlowTools(server, ctx, signer, wc, state) {
|
|
|
749
975
|
.describe("Target chain id. Defaults to the MCP's default chain. Rejects if no RPC is configured for the requested chain."),
|
|
750
976
|
proposalId: z.number().int().min(1).describe("Proposal ID (1-indexed)"),
|
|
751
977
|
isVoteFor: z.boolean().default(true).describe("Vote for (true) or against (false)"),
|
|
752
|
-
voteAmount: z
|
|
978
|
+
voteAmount: z
|
|
979
|
+
.string()
|
|
980
|
+
.optional()
|
|
981
|
+
.describe("Vote amount: raw wei (digits-only string) OR human units with a decimal point ('12.5', scaled by the gov " +
|
|
982
|
+
"token's decimals). Defaults to ALL available power (deposited + wallet)."),
|
|
753
983
|
voteNftIds: z.array(z.string()).default([]),
|
|
754
|
-
depositFirst: z
|
|
984
|
+
depositFirst: z
|
|
985
|
+
.union([z.boolean(), z.literal("auto")])
|
|
986
|
+
.default("auto")
|
|
987
|
+
.describe("'auto' (default): deposit exactly the missing amount from the wallet when deposited power is short of " +
|
|
988
|
+
"voteAmount. true: deposit the full wallet balance. false: never deposit (vote with already-deposited power only)."),
|
|
755
989
|
autoExecute: z.boolean().default(true).describe("Attempt execute if proposal passes after vote"),
|
|
756
990
|
dryRun: z.boolean().default(false).describe("If true, return ordered TxPayloads even when DEXE_PRIVATE_KEY is set (preview without broadcasting)."),
|
|
757
991
|
user: z.string().optional().describe("User address. Required when DEXE_PRIVATE_KEY not set."),
|
|
@@ -794,6 +1028,9 @@ export function registerFlowTools(server, ctx, signer, wc, state) {
|
|
|
794
1028
|
const execResult = await sendOrCollect(signer, [
|
|
795
1029
|
makeTxPayload(govPool, GOV_POOL_ABI, "execute", [proposalId], chainId, `GovPool.execute(${proposalId})`),
|
|
796
1030
|
], { dryRun: input.dryRun, chainId, wc });
|
|
1031
|
+
if (execResult.mode === "failed") {
|
|
1032
|
+
return flowFailureResult(execResult, { proposalId, proposalStateBefore: stateName });
|
|
1033
|
+
}
|
|
797
1034
|
return attachPairingQr(ok({
|
|
798
1035
|
mode: execResult.mode,
|
|
799
1036
|
proposalId,
|
|
@@ -809,43 +1046,86 @@ export function registerFlowTools(server, ctx, signer, wc, state) {
|
|
|
809
1046
|
}), execResult.pairingContent);
|
|
810
1047
|
}
|
|
811
1048
|
if (stateNum !== 0) {
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1049
|
+
const remedies = {
|
|
1050
|
+
1: "It is waiting for the validator-voting transfer — check again shortly with dexe_proposal_state.",
|
|
1051
|
+
2: "It is in validator voting — only the DAO's validators can act now; wait for their vote.",
|
|
1052
|
+
3: "It was DEFEATED — voting is over. Create a new proposal if the change is still wanted.",
|
|
1053
|
+
4: "It already PASSED — re-run this call with autoExecute:true (the default) to execute it, no vote needed.",
|
|
1054
|
+
5: "It already passed AGAINST — re-run this call with autoExecute:true to execute the against-actions.",
|
|
1055
|
+
6: "It is Locked (passed, execution delay running) — re-run this call with autoExecute:true once the delay elapses.",
|
|
1056
|
+
7: "It was already EXECUTED (for) — nothing left to do.",
|
|
1057
|
+
8: "It was already EXECUTED (against) — nothing left to do.",
|
|
1058
|
+
};
|
|
1059
|
+
return err(`Proposal #${proposalId} is in state "${stateName}" — voting is only possible in "Voting". ` +
|
|
1060
|
+
(remedies[stateNum] ?? "Check dexe_proposal_state for details."));
|
|
818
1061
|
}
|
|
1062
|
+
// Step 2: resolve prereqs — always needed now (auto-deposit detection +
|
|
1063
|
+
// minVotes threshold + human-unit rendering).
|
|
1064
|
+
const prereqs = await resolvePrereqs(rpc, govPool, user, ctx.config, chainId);
|
|
1065
|
+
const d = prereqs.tokenDecimals;
|
|
1066
|
+
const sym = prereqs.tokenSymbol;
|
|
819
1067
|
const payloads = [];
|
|
820
1068
|
const skippedSteps = [];
|
|
821
|
-
// Step 3:
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1069
|
+
// Step 3: target vote amount (raw wei digits-only, or human decimal).
|
|
1070
|
+
let voteAmt;
|
|
1071
|
+
try {
|
|
1072
|
+
voteAmt = input.voteAmount
|
|
1073
|
+
? parseAmount(input.voteAmount, d)
|
|
1074
|
+
: input.depositFirst === false
|
|
1075
|
+
? prereqs.depositedPower
|
|
1076
|
+
: prereqs.depositedPower + prereqs.walletBalance;
|
|
1077
|
+
}
|
|
1078
|
+
catch (e) {
|
|
1079
|
+
return err(e instanceof Error ? e.message : String(e));
|
|
830
1080
|
}
|
|
831
|
-
// Step 4: vote
|
|
832
|
-
const voteAmt = input.voteAmount
|
|
833
|
-
? BigInt(input.voteAmount)
|
|
834
|
-
: (prereqs ? prereqs.depositedPower + prereqs.walletBalance : 0n);
|
|
835
1081
|
if (voteAmt === 0n) {
|
|
836
|
-
return err(
|
|
1082
|
+
return err(input.depositFirst === false
|
|
1083
|
+
? `No deposited voting power (wallet holds ${formatAmount(prereqs.walletBalance, d, sym)}, deposited 0). ` +
|
|
1084
|
+
`Re-run with depositFirst:'auto' (the default) to deposit-and-vote in one call.`
|
|
1085
|
+
: `No voting power available — ${user} holds 0 ${sym || "gov tokens"} (wallet + deposited). ` +
|
|
1086
|
+
`Acquire the DAO's gov token (${prereqs.tokenAddress}) first.`);
|
|
837
1087
|
}
|
|
838
|
-
//
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1088
|
+
// Step 4: deposit decision.
|
|
1089
|
+
// 'auto' → deposit exactly the shortfall (frontend-equivalent bundled deposit+vote)
|
|
1090
|
+
// true → legacy explicit: deposit the full wallet balance
|
|
1091
|
+
// false → never deposit
|
|
1092
|
+
let depositAmount = 0n;
|
|
1093
|
+
if (input.depositFirst === true) {
|
|
1094
|
+
depositAmount = prereqs.walletBalance;
|
|
842
1095
|
}
|
|
843
|
-
if (
|
|
844
|
-
|
|
1096
|
+
else if (input.depositFirst !== false && voteAmt > prereqs.depositedPower) {
|
|
1097
|
+
const shortfall = voteAmt - prereqs.depositedPower;
|
|
1098
|
+
if (shortfall > prereqs.walletBalance) {
|
|
1099
|
+
return err(`Not enough tokens: voting with ${formatAmount(voteAmt, d, sym)} needs ${formatAmount(shortfall, d, sym)} more deposited, ` +
|
|
1100
|
+
`but the wallet only holds ${formatAmount(prereqs.walletBalance, d, sym)} ` +
|
|
1101
|
+
`(deposited ${formatAmount(prereqs.depositedPower, d, sym)}). ` +
|
|
1102
|
+
`Lower voteAmount to at most ${formatAmount(prereqs.depositedPower + prereqs.walletBalance, d, sym)}, or acquire more tokens.`);
|
|
1103
|
+
}
|
|
1104
|
+
depositAmount = shortfall;
|
|
1105
|
+
}
|
|
1106
|
+
if (depositAmount > 0n) {
|
|
1107
|
+
// Approve if needed — W10: exact-amount approve to the UserKeeper
|
|
1108
|
+
// (never GovPool, never MAX_UINT256).
|
|
1109
|
+
if (prereqs.currentAllowance < depositAmount) {
|
|
1110
|
+
payloads.push(makeTxPayload(prereqs.tokenAddress, ERC20_ABI, "approve", [prereqs.userKeeper, depositAmount], chainId, `ERC20.approve(${prereqs.userKeeper}, ${depositAmount})`));
|
|
1111
|
+
}
|
|
1112
|
+
payloads.push(makeTxPayload(govPool, GOV_POOL_ABI, "deposit", [depositAmount, []], chainId, `GovPool.deposit(${depositAmount})`));
|
|
1113
|
+
}
|
|
1114
|
+
else if (input.depositFirst !== false) {
|
|
1115
|
+
skippedSteps.push({ label: "GovPool.deposit", skipped: true, reason: "Deposited power already covers voteAmount" });
|
|
1116
|
+
}
|
|
1117
|
+
// Step 5: minVotesForVoting threshold
|
|
1118
|
+
if (prereqs.minVotesForVoting > 0n && voteAmt < prereqs.minVotesForVoting) {
|
|
1119
|
+
return err(`Vote below this DAO's minimum: voting requires at least ${formatAmount(prereqs.minVotesForVoting, d, sym)} ` +
|
|
1120
|
+
`but this vote would cast ${formatAmount(voteAmt, d, sym)}. ` +
|
|
1121
|
+
`Raise voteAmount (you have ${formatAmount(prereqs.depositedPower + prereqs.walletBalance, d, sym)} total) or acquire more tokens.`);
|
|
845
1122
|
}
|
|
846
1123
|
payloads.push(makeTxPayload(govPool, GOV_POOL_ABI, "vote", [proposalId, input.isVoteFor, voteAmt, input.voteNftIds.map(id => BigInt(id))], chainId, `GovPool.vote(${proposalId}, ${input.isVoteFor}, ${voteAmt})`));
|
|
847
1124
|
// Step 5: send or collect
|
|
848
1125
|
const result = await sendOrCollect(signer, payloads, { dryRun: input.dryRun, chainId, wc });
|
|
1126
|
+
if (result.mode === "failed") {
|
|
1127
|
+
return flowFailureResult(result, { proposalId, proposalStateBefore: stateName });
|
|
1128
|
+
}
|
|
849
1129
|
// Step 6: auto-execute (only in executed mode)
|
|
850
1130
|
let executed = false;
|
|
851
1131
|
if (input.autoExecute && result.mode === "executed") {
|
|
@@ -870,6 +1150,11 @@ export function registerFlowTools(server, ctx, signer, wc, state) {
|
|
|
870
1150
|
makeTxPayload(govPool, GOV_POOL_ABI, "execute", [proposalId], chainId, `GovPool.execute(${proposalId})`),
|
|
871
1151
|
], { dryRun: input.dryRun, chainId, wc });
|
|
872
1152
|
result.steps.push(...execResult.steps);
|
|
1153
|
+
if (execResult.mode === "failed") {
|
|
1154
|
+
// The vote landed; only the execute failed. Surface the ledger —
|
|
1155
|
+
// the proposal stays executable via a re-run or dexe_vote_build_execute.
|
|
1156
|
+
return flowFailureResult({ steps: result.steps, failure: execResult.failure }, { proposalId, proposalStateBefore: stateName, voteLanded: true });
|
|
1157
|
+
}
|
|
873
1158
|
executed = true;
|
|
874
1159
|
}
|
|
875
1160
|
else {
|