@poa-box/core 0.1.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/README.md +201 -0
- package/dist/abis/DirectDemocracyVotingNew.d.ts +749 -0
- package/dist/abis/DirectDemocracyVotingNew.js +961 -0
- package/dist/abis/ERC20.d.ts +90 -0
- package/dist/abis/ERC20.js +125 -0
- package/dist/abis/EducationHubNew.d.ts +590 -0
- package/dist/abis/EducationHubNew.js +762 -0
- package/dist/abis/EligibilityModuleNew.d.ts +1615 -0
- package/dist/abis/EligibilityModuleNew.js +2082 -0
- package/dist/abis/Executor.d.ts +619 -0
- package/dist/abis/Executor.js +796 -0
- package/dist/abis/HybridVotingNew.d.ts +898 -0
- package/dist/abis/HybridVotingNew.js +1149 -0
- package/dist/abis/ImplementationRegistry.d.ts +270 -0
- package/dist/abis/ImplementationRegistry.js +355 -0
- package/dist/abis/OrgDeployerNew.d.ts +1275 -0
- package/dist/abis/OrgDeployerNew.js +1630 -0
- package/dist/abis/OrgRegistry.d.ts +670 -0
- package/dist/abis/OrgRegistry.js +868 -0
- package/dist/abis/ParticipationToken.d.ts +1086 -0
- package/dist/abis/ParticipationToken.js +1415 -0
- package/dist/abis/PasskeyAccount.d.ts +788 -0
- package/dist/abis/PasskeyAccount.js +1019 -0
- package/dist/abis/PasskeyAccountFactory.d.ts +344 -0
- package/dist/abis/PasskeyAccountFactory.js +449 -0
- package/dist/abis/PaymasterHub.d.ts +1603 -0
- package/dist/abis/PaymasterHub.js +2048 -0
- package/dist/abis/PaymentManager.d.ts +520 -0
- package/dist/abis/PaymentManager.js +671 -0
- package/dist/abis/PoaManager.d.ts +344 -0
- package/dist/abis/PoaManager.js +449 -0
- package/dist/abis/QuickJoinNew.d.ts +855 -0
- package/dist/abis/QuickJoinNew.js +1098 -0
- package/dist/abis/TaskManagerNew.d.ts +1236 -0
- package/dist/abis/TaskManagerNew.js +1573 -0
- package/dist/abis/ToggleModule.d.ts +193 -0
- package/dist/abis/ToggleModule.js +255 -0
- package/dist/abis/UniversalAccountRegistry.d.ts +577 -0
- package/dist/abis/UniversalAccountRegistry.js +750 -0
- package/dist/abis/ZkEmailInvites.d.ts +823 -0
- package/dist/abis/ZkEmailInvites.js +1060 -0
- package/dist/abis/external/AaveGovernanceV2.d.ts +128 -0
- package/dist/abis/external/AaveGovernanceV2.js +177 -0
- package/dist/abis/external/AaveGovernanceV3.d.ts +160 -0
- package/dist/abis/external/AaveGovernanceV3.js +220 -0
- package/dist/abis/external/AragonVoting.d.ts +107 -0
- package/dist/abis/external/AragonVoting.js +150 -0
- package/dist/abis/external/CurveGaugeController.d.ts +95 -0
- package/dist/abis/external/CurveGaugeController.js +130 -0
- package/dist/abis/external/CurveVotingEscrow.d.ts +86 -0
- package/dist/abis/external/CurveVotingEscrow.js +116 -0
- package/dist/abis/external/GovernorAlpha.d.ts +172 -0
- package/dist/abis/external/GovernorAlpha.js +228 -0
- package/dist/abis/external/MakerDAOChief.d.ts +92 -0
- package/dist/abis/external/MakerDAOChief.js +129 -0
- package/dist/abis/external/OZGovernor.d.ts +227 -0
- package/dist/abis/external/OZGovernor.js +317 -0
- package/dist/abis/external/SolidlyVotingEscrow.d.ts +196 -0
- package/dist/abis/external/SolidlyVotingEscrow.js +260 -0
- package/dist/abis/index.d.ts +62 -0
- package/dist/abis/index.js +98 -0
- package/dist/chains.d.ts +150 -0
- package/dist/chains.js +320 -0
- package/dist/context.d.ts +48 -0
- package/dist/context.js +38 -0
- package/dist/contracts.d.ts +24 -0
- package/dist/contracts.js +41 -0
- package/dist/encoding.d.ts +58 -0
- package/dist/encoding.js +243 -0
- package/dist/env.d.ts +10 -0
- package/dist/env.js +4 -0
- package/dist/error-catalog.d.ts +41 -0
- package/dist/error-catalog.js +941 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.js +60 -0
- package/dist/execute/ethers.d.ts +93 -0
- package/dist/execute/ethers.js +323 -0
- package/dist/execute/index.d.ts +7 -0
- package/dist/execute/index.js +23 -0
- package/dist/execute/sponsored.d.ts +68 -0
- package/dist/execute/sponsored.js +277 -0
- package/dist/exit-codes.d.ts +18 -0
- package/dist/exit-codes.js +21 -0
- package/dist/format.d.ts +21 -0
- package/dist/format.js +89 -0
- package/dist/graph/client.d.ts +298 -0
- package/dist/graph/client.js +702 -0
- package/dist/graph/documents/activity.d.ts +9 -0
- package/dist/graph/documents/activity.js +164 -0
- package/dist/graph/documents/beacons.d.ts +172 -0
- package/dist/graph/documents/beacons.js +345 -0
- package/dist/graph/documents/index.d.ts +21 -0
- package/dist/graph/documents/index.js +57 -0
- package/dist/graph/documents/infrastructure.d.ts +30 -0
- package/dist/graph/documents/infrastructure.js +34 -0
- package/dist/graph/documents/org.d.ts +15 -0
- package/dist/graph/documents/org.js +249 -0
- package/dist/graph/documents/paymaster.d.ts +95 -0
- package/dist/graph/documents/paymaster.js +101 -0
- package/dist/graph/documents/role.d.ts +6 -0
- package/dist/graph/documents/role.js +41 -0
- package/dist/graph/documents/roles.d.ts +50 -0
- package/dist/graph/documents/roles.js +112 -0
- package/dist/graph/documents/task.d.ts +84 -0
- package/dist/graph/documents/task.js +236 -0
- package/dist/graph/documents/token.d.ts +13 -0
- package/dist/graph/documents/token.js +126 -0
- package/dist/graph/documents/treasury.d.ts +39 -0
- package/dist/graph/documents/treasury.js +136 -0
- package/dist/graph/documents/user.d.ts +120 -0
- package/dist/graph/documents/user.js +302 -0
- package/dist/graph/documents/voting-classes.d.ts +69 -0
- package/dist/graph/documents/voting-classes.js +149 -0
- package/dist/graph/documents/voting.d.ts +28 -0
- package/dist/graph/documents/voting.js +163 -0
- package/dist/graph/documents/vouch.d.ts +79 -0
- package/dist/graph/documents/vouch.js +171 -0
- package/dist/graph/documents/zkemail.d.ts +63 -0
- package/dist/graph/documents/zkemail.js +188 -0
- package/dist/graph/index.d.ts +2 -0
- package/dist/graph/index.js +41 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +92 -0
- package/dist/ipfs.d.ts +39 -0
- package/dist/ipfs.js +214 -0
- package/dist/label-aliases.d.ts +31 -0
- package/dist/label-aliases.js +70 -0
- package/dist/metadata/education.d.ts +30 -0
- package/dist/metadata/education.js +31 -0
- package/dist/metadata/index.d.ts +12 -0
- package/dist/metadata/index.js +48 -0
- package/dist/metadata/org.d.ts +92 -0
- package/dist/metadata/org.js +93 -0
- package/dist/metadata/proposal.d.ts +44 -0
- package/dist/metadata/proposal.js +45 -0
- package/dist/metadata/role.d.ts +38 -0
- package/dist/metadata/role.js +35 -0
- package/dist/metadata/task.d.ts +108 -0
- package/dist/metadata/task.js +81 -0
- package/dist/metadata/token.d.ts +32 -0
- package/dist/metadata/token.js +36 -0
- package/dist/metadata/user.d.ts +63 -0
- package/dist/metadata/user.js +70 -0
- package/dist/multicall.d.ts +32 -0
- package/dist/multicall.js +96 -0
- package/dist/payout.d.ts +59 -0
- package/dist/payout.js +96 -0
- package/dist/perms.d.ts +47 -0
- package/dist/perms.js +120 -0
- package/dist/preflight.d.ts +72 -0
- package/dist/preflight.js +269 -0
- package/dist/reads/education.d.ts +79 -0
- package/dist/reads/education.js +122 -0
- package/dist/reads/eligibility.d.ts +377 -0
- package/dist/reads/eligibility.js +687 -0
- package/dist/reads/index.d.ts +16 -0
- package/dist/reads/index.js +55 -0
- package/dist/reads/org.d.ts +258 -0
- package/dist/reads/org.js +220 -0
- package/dist/reads/paymaster.d.ts +147 -0
- package/dist/reads/paymaster.js +235 -0
- package/dist/reads/project.d.ts +33 -0
- package/dist/reads/project.js +62 -0
- package/dist/reads/resolve.d.ts +34 -0
- package/dist/reads/resolve.js +64 -0
- package/dist/reads/task.d.ts +286 -0
- package/dist/reads/task.js +280 -0
- package/dist/reads/token.d.ts +94 -0
- package/dist/reads/token.js +75 -0
- package/dist/reads/treasury.d.ts +222 -0
- package/dist/reads/treasury.js +241 -0
- package/dist/reads/user.d.ts +229 -0
- package/dist/reads/user.js +155 -0
- package/dist/reads/vote.d.ts +296 -0
- package/dist/reads/vote.js +392 -0
- package/dist/reads/zkemail.d.ts +117 -0
- package/dist/reads/zkemail.js +217 -0
- package/dist/similarity.d.ts +36 -0
- package/dist/similarity.js +67 -0
- package/dist/sponsorship-config.d.ts +47 -0
- package/dist/sponsorship-config.js +41 -0
- package/dist/stats.d.ts +11 -0
- package/dist/stats.js +27 -0
- package/dist/task-lens.d.ts +113 -0
- package/dist/task-lens.js +210 -0
- package/dist/tx/education.d.ts +169 -0
- package/dist/tx/education.js +344 -0
- package/dist/tx/eligibility.d.ts +511 -0
- package/dist/tx/eligibility.js +818 -0
- package/dist/tx/governance.d.ts +85 -0
- package/dist/tx/governance.js +87 -0
- package/dist/tx/index.d.ts +17 -0
- package/dist/tx/index.js +56 -0
- package/dist/tx/intent.d.ts +67 -0
- package/dist/tx/intent.js +48 -0
- package/dist/tx/org.d.ts +321 -0
- package/dist/tx/org.js +544 -0
- package/dist/tx/paymaster.d.ts +140 -0
- package/dist/tx/paymaster.js +251 -0
- package/dist/tx/project.d.ts +146 -0
- package/dist/tx/project.js +217 -0
- package/dist/tx/task.d.ts +480 -0
- package/dist/tx/task.js +1149 -0
- package/dist/tx/token.d.ts +96 -0
- package/dist/tx/token.js +165 -0
- package/dist/tx/treasury.d.ts +474 -0
- package/dist/tx/treasury.js +911 -0
- package/dist/tx/user.d.ts +177 -0
- package/dist/tx/user.js +292 -0
- package/dist/tx/vote.d.ts +332 -0
- package/dist/tx/vote.js +762 -0
- package/dist/tx/zkemail.d.ts +90 -0
- package/dist/tx/zkemail.js +210 -0
- package/dist/validation.d.ts +8 -0
- package/dist/validation.js +39 -0
- package/dist/version.d.ts +142 -0
- package/dist/version.js +257 -0
- package/dist/zkemail.d.ts +161 -0
- package/dist/zkemail.js +299 -0
- package/package.json +130 -0
package/dist/tx/vote.js
ADDED
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Vote domain tx builders — ports of the `pop vote *` write commands.
|
|
4
|
+
*
|
|
5
|
+
* Level 1 (pure, sync): buildVote, buildAnnounceWinner — plus
|
|
6
|
+
* buildGovernanceProposal in ./governance (the Level-1 builder for
|
|
7
|
+
* createProposal itself).
|
|
8
|
+
*
|
|
9
|
+
* Level 2 (resolved, async): createProposalIntent, castVoteIntent,
|
|
10
|
+
* announceWinnerIntent, executeProposalIntent, and the three governance-wrap
|
|
11
|
+
* consumers proposeQuorumIntent / proposeConfigIntent / proposeClassesIntent.
|
|
12
|
+
*
|
|
13
|
+
* Receipt parsing (parseProposalCreatedLogs lives in ./governance;
|
|
14
|
+
* parseAnnounceReceiptLogs here) and the callStatic announce probe are
|
|
15
|
+
* exported because "tx mined" is NOT success for announceWinner — the
|
|
16
|
+
* Executor swallows failed sub-calls.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.MAX_CLASSES = exports.CLASS_STRATEGY = exports.CONFIG_PARAMS = exports.DD_QUORUM_KEY = exports.HYBRID_QUORUM_KEY = exports.MODULE_LABELS = void 0;
|
|
20
|
+
exports.buildVote = buildVote;
|
|
21
|
+
exports.buildAnnounceWinner = buildAnnounceWinner;
|
|
22
|
+
exports.parseAnnounceReceiptLogs = parseAnnounceReceiptLogs;
|
|
23
|
+
exports.announceWinnerProbe = announceWinnerProbe;
|
|
24
|
+
exports.describeExecutionCalls = describeExecutionCalls;
|
|
25
|
+
exports.createProposalIntent = createProposalIntent;
|
|
26
|
+
exports.castVoteIntent = castVoteIntent;
|
|
27
|
+
exports.announceWinnerIntent = announceWinnerIntent;
|
|
28
|
+
exports.executeProposalIntent = executeProposalIntent;
|
|
29
|
+
exports.proposeQuorumIntent = proposeQuorumIntent;
|
|
30
|
+
exports.proposeConfigIntent = proposeConfigIntent;
|
|
31
|
+
exports.parseClassConfigs = parseClassConfigs;
|
|
32
|
+
exports.describeClass = describeClass;
|
|
33
|
+
exports.proposeClassesIntent = proposeClassesIntent;
|
|
34
|
+
const ethers_1 = require("ethers");
|
|
35
|
+
const governance_1 = require("./governance");
|
|
36
|
+
const contracts_1 = require("../contracts");
|
|
37
|
+
const encoding_1 = require("../encoding");
|
|
38
|
+
const format_1 = require("../format");
|
|
39
|
+
const ipfs_1 = require("../ipfs");
|
|
40
|
+
const resolve_1 = require("../reads/resolve");
|
|
41
|
+
const vote_1 = require("../reads/vote");
|
|
42
|
+
const proposal_1 = require("../metadata/proposal");
|
|
43
|
+
const errors_1 = require("../errors");
|
|
44
|
+
const exit_codes_1 = require("../exit-codes");
|
|
45
|
+
/**
|
|
46
|
+
* Port of `pop vote cast` — src/commands/vote/cast.ts:
|
|
47
|
+
* HybridVotingNew|DirectDemocracyVotingNew.vote(proposalId, optionIndices, weights).
|
|
48
|
+
*/
|
|
49
|
+
function buildVote(a) {
|
|
50
|
+
return {
|
|
51
|
+
to: a.votingAddress,
|
|
52
|
+
abi: (0, contracts_1.getAbi)(a.votingAbiName),
|
|
53
|
+
method: 'vote',
|
|
54
|
+
args: [a.proposalId, a.optionIndices, a.weights],
|
|
55
|
+
meta: {
|
|
56
|
+
domain: 'vote',
|
|
57
|
+
action: 'cast',
|
|
58
|
+
orgId: a.orgId,
|
|
59
|
+
summary: {
|
|
60
|
+
proposalId: String(a.proposalId),
|
|
61
|
+
options: a.optionIndices.join(','),
|
|
62
|
+
weights: a.weights.join(','),
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Port of `pop vote announce` / `pop vote announce-all` / `pop vote execute`
|
|
69
|
+
* — src/commands/vote/announce.ts, announce-all.ts, execute.ts:
|
|
70
|
+
* HybridVotingNew|DirectDemocracyVotingNew.announceWinner(proposalId).
|
|
71
|
+
*
|
|
72
|
+
* DESTRUCTIVE: irreversibly finalizes the vote and executes the winning
|
|
73
|
+
* option's calls through the Executor. Hosts should (a) callStatic-probe
|
|
74
|
+
* first (announceWinnerProbe) and (b) scan the receipt with
|
|
75
|
+
* parseAnnounceReceiptLogs — the outer tx can succeed while the proposal's
|
|
76
|
+
* calls reverted.
|
|
77
|
+
*/
|
|
78
|
+
function buildAnnounceWinner(a) {
|
|
79
|
+
return {
|
|
80
|
+
to: a.votingAddress,
|
|
81
|
+
abi: (0, contracts_1.getAbi)(a.votingAbiName),
|
|
82
|
+
method: 'announceWinner',
|
|
83
|
+
args: [a.proposalId],
|
|
84
|
+
meta: {
|
|
85
|
+
domain: 'vote',
|
|
86
|
+
action: a.action ?? 'announce',
|
|
87
|
+
orgId: a.orgId,
|
|
88
|
+
summary: { proposalId: String(a.proposalId) },
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// ────────────────────── receipt parsing + announce probe ──────────────────────
|
|
93
|
+
/**
|
|
94
|
+
* Port of `parseAnnounceReceipt` (src/commands/vote/helpers.ts), decoupled
|
|
95
|
+
* from TxResult: takes the parsed receipt logs. The Executor catches failed
|
|
96
|
+
* sub-calls and emits CallFailed / ProposalExecutionFailed while the OUTER tx
|
|
97
|
+
* still succeeds — callers MUST treat innerFailure as an execution failure
|
|
98
|
+
* even though the transaction mined successfully.
|
|
99
|
+
*/
|
|
100
|
+
function parseAnnounceReceiptLogs(logs) {
|
|
101
|
+
const parsed = logs ?? [];
|
|
102
|
+
const winnerEvent = parsed.find(l => l.name === 'Winner');
|
|
103
|
+
const executedEvent = parsed.find(l => l.name === 'ProposalExecuted');
|
|
104
|
+
const callFailedEvents = parsed.filter(l => l.name === 'CallFailed');
|
|
105
|
+
const execFailedEvent = parsed.find(l => l.name === 'ProposalExecutionFailed');
|
|
106
|
+
return {
|
|
107
|
+
winningOption: winnerEvent?.args?.winningIdx?.toString(),
|
|
108
|
+
valid: winnerEvent?.args?.valid,
|
|
109
|
+
executed: Boolean(executedEvent) || Boolean(winnerEvent?.args?.executed),
|
|
110
|
+
innerFailure: callFailedEvents.length > 0 || Boolean(execFailedEvent),
|
|
111
|
+
failedCalls: callFailedEvents.map(e => ({
|
|
112
|
+
index: e.args?.index?.toString(),
|
|
113
|
+
data: e.args?.lowLevelData || e.args?.data,
|
|
114
|
+
})),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Port of `announceWinnerProbe` (src/commands/vote/helpers.ts): callStatic
|
|
119
|
+
* probe shared by announce/execute pre-flights — would announceWinner(id)
|
|
120
|
+
* revert right now (voting still open, already executed, quorum edge...)?
|
|
121
|
+
* Returns the decoded reason on failure. This is an RPC revert predictor:
|
|
122
|
+
* per the architecture rules it must stay RPC, never subgraph.
|
|
123
|
+
*/
|
|
124
|
+
async function announceWinnerProbe(contract, proposalId) {
|
|
125
|
+
try {
|
|
126
|
+
await contract.callStatic.announceWinner(proposalId);
|
|
127
|
+
return { ok: true };
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
const reason = err?.errorName || err?.reason || err?.error?.reason || err?.message || 'unknown';
|
|
131
|
+
return { ok: false, reason: String(reason) };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// ────────────────── execution-call description (confirm UIs) ──────────────────
|
|
135
|
+
/**
|
|
136
|
+
* Org-module address → human name map for decoding proposal execution
|
|
137
|
+
* targets (src/commands/vote/helpers.ts MODULE_LABELS). Order matters only
|
|
138
|
+
* for display; keys come from resolveOrgModules.
|
|
139
|
+
*/
|
|
140
|
+
exports.MODULE_LABELS = [
|
|
141
|
+
{ key: 'taskManagerAddress', label: 'TaskManager', abi: 'TaskManagerNew' },
|
|
142
|
+
{ key: 'hybridVotingAddress', label: 'HybridVoting', abi: 'HybridVotingNew' },
|
|
143
|
+
{ key: 'ddVotingAddress', label: 'DirectDemocracyVoting', abi: 'DirectDemocracyVotingNew' },
|
|
144
|
+
{ key: 'participationTokenAddress', label: 'ParticipationToken', abi: 'ParticipationToken' },
|
|
145
|
+
{ key: 'educationHubAddress', label: 'EducationHub', abi: 'EducationHubNew' },
|
|
146
|
+
{ key: 'executorAddress', label: 'Executor', abi: 'Executor' },
|
|
147
|
+
{ key: 'quickJoinAddress', label: 'QuickJoin', abi: 'QuickJoinNew' },
|
|
148
|
+
{ key: 'eligibilityModuleAddress', label: 'EligibilityModule', abi: 'EligibilityModuleNew' },
|
|
149
|
+
{ key: 'paymentManagerAddress', label: 'PaymentManager', abi: 'PaymentManager' },
|
|
150
|
+
{ key: 'zkEmailInvitesAddress', label: 'ZkEmailInvites', abi: 'ZkEmailInvites' },
|
|
151
|
+
];
|
|
152
|
+
/** Fallback ABIs scanned when the target is not a known org module. */
|
|
153
|
+
const FALLBACK_ABIS = ['ERC20', 'UniversalAccountRegistry'];
|
|
154
|
+
function tryGetFunction(abiName, selector) {
|
|
155
|
+
try {
|
|
156
|
+
const iface = new ethers_1.ethers.utils.Interface((0, contracts_1.getAbi)(abiName));
|
|
157
|
+
const fn = iface.getFunction(selector);
|
|
158
|
+
return `${fn.name}(${fn.inputs.map(i => i.type).join(',')})`;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Port of `describeExecutionCalls` (src/commands/vote/helpers.ts) —
|
|
166
|
+
* governance-safety decode of proposal execution calls for confirm UIs:
|
|
167
|
+
* each call's target is resolved against the org's known module addresses
|
|
168
|
+
* (named when recognized) and its calldata selector is decoded to a function
|
|
169
|
+
* signature via the known ABIs. Never throws — undecodable calls degrade to
|
|
170
|
+
* the raw selector so the human still sees SOMETHING.
|
|
171
|
+
*/
|
|
172
|
+
function describeExecutionCalls(calls, modules) {
|
|
173
|
+
return calls.map((call) => {
|
|
174
|
+
const target = String(call.target ?? '');
|
|
175
|
+
const targetLower = target.toLowerCase();
|
|
176
|
+
const known = exports.MODULE_LABELS.find(m => {
|
|
177
|
+
const addr = modules?.[m.key];
|
|
178
|
+
return typeof addr === 'string' && addr.toLowerCase() === targetLower;
|
|
179
|
+
});
|
|
180
|
+
const name = known ? `${known.label} (${(0, encoding_1.formatAddress)(target)})` : `unknown contract ${target}`;
|
|
181
|
+
const data = String(call.data ?? '0x');
|
|
182
|
+
let action;
|
|
183
|
+
if (!data || data === '0x') {
|
|
184
|
+
action = 'plain native-token transfer (no calldata)';
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
const selector = data.slice(0, 10);
|
|
188
|
+
let signature = known ? tryGetFunction(known.abi, selector) : null;
|
|
189
|
+
if (!signature) {
|
|
190
|
+
for (const m of exports.MODULE_LABELS) {
|
|
191
|
+
signature = tryGetFunction(m.abi, selector);
|
|
192
|
+
if (signature)
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (!signature) {
|
|
197
|
+
for (const abiName of FALLBACK_ABIS) {
|
|
198
|
+
signature = tryGetFunction(abiName, selector);
|
|
199
|
+
if (signature)
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
action = signature ? `${signature} [${selector}]` : `UNDECODABLE call [selector ${selector}]`;
|
|
204
|
+
}
|
|
205
|
+
let valueSuffix = '';
|
|
206
|
+
try {
|
|
207
|
+
const value = ethers_1.ethers.BigNumber.from(call.value ?? 0);
|
|
208
|
+
if (!value.isZero())
|
|
209
|
+
valueSuffix = ` + ${(0, format_1.formatToken)(value)} native`;
|
|
210
|
+
}
|
|
211
|
+
catch { /* unparseable value — omit */ }
|
|
212
|
+
return `${name} → ${action}${valueSuffix}`;
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
// ────────────────────────── shared Level-2 plumbing ──────────────────────────
|
|
216
|
+
/**
|
|
217
|
+
* Resolve the org's hybrid or DD voting module with the CLI's exact
|
|
218
|
+
* missing-module error (`pop vote create/cast/announce`).
|
|
219
|
+
*/
|
|
220
|
+
async function resolveVotingModule(ctx, org, type) {
|
|
221
|
+
const modules = await (0, resolve_1.resolveOrgModules)(ctx.client, org, ctx.chainId);
|
|
222
|
+
const isHybrid = type === 'hybrid';
|
|
223
|
+
const votingAddress = isHybrid ? modules.hybridVotingAddress : modules.ddVotingAddress;
|
|
224
|
+
if (!votingAddress) {
|
|
225
|
+
throw new errors_1.CliError(`${isHybrid ? 'HybridVoting' : 'DirectDemocracyVoting'} not deployed for this org`, exit_codes_1.EXIT.PRECONDITION);
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
modules,
|
|
229
|
+
votingAddress,
|
|
230
|
+
votingAbiName: isHybrid ? 'HybridVotingNew' : 'DirectDemocracyVotingNew',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/** Pin proposal metadata exactly as every CLI wrap does (JSON.stringify → pinJson). */
|
|
234
|
+
async function pinProposalMetadata(ctx, metadata) {
|
|
235
|
+
const cid = await (0, ipfs_1.pinJson)((0, proposal_1.serializeProposalMetadata)(metadata), ctx.ipfs);
|
|
236
|
+
return { cid, descriptionHash: (0, encoding_1.ipfsCidToBytes32)(cid) };
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Port of `pop vote create` — src/commands/vote/create.ts.
|
|
240
|
+
*
|
|
241
|
+
* Resolves the org's voting module, validates the option count, parses hat
|
|
242
|
+
* IDs as BigNumbers, applies the MAX_POLL_HATS cap check (only when
|
|
243
|
+
* ctx.provider is supplied — best-effort, exactly like the CLI: an older
|
|
244
|
+
* contract has no getter and no cap, so a failing read means the check does
|
|
245
|
+
* not apply), builds the option-0 execution batch, pins the proposal
|
|
246
|
+
* metadata {description, optionNames, createdAt}, and delegates to
|
|
247
|
+
* buildGovernanceProposal.
|
|
248
|
+
*
|
|
249
|
+
* Batch quirk preserved: with calls OMITTED the batches argument is the
|
|
250
|
+
* EMPTY array [], while calls GIVEN (even as an empty list) build per-option
|
|
251
|
+
* batches [[…], [], …] — matching the CLI's `if (argv.calls)` branch exactly.
|
|
252
|
+
*/
|
|
253
|
+
async function createProposalIntent(ctx, params) {
|
|
254
|
+
const { modules, votingAddress, votingAbiName } = await resolveVotingModule(ctx, params.org, params.type);
|
|
255
|
+
const optionNames = params.optionNames;
|
|
256
|
+
const numOptions = optionNames.length;
|
|
257
|
+
if (numOptions < 2) {
|
|
258
|
+
throw new errors_1.CliError('At least 2 options are required', exit_codes_1.EXIT.USAGE);
|
|
259
|
+
}
|
|
260
|
+
const hatIds = (params.hatIds ?? []).map(h => ethers_1.ethers.BigNumber.from(typeof h === 'string' ? h.trim() : h));
|
|
261
|
+
// Hat-restriction cap: both voting contracts reject more than MAX_POLL_HATS
|
|
262
|
+
// restricted hats. Read the live constant — an older contract has no getter
|
|
263
|
+
// and no cap, so the check simply does not apply there.
|
|
264
|
+
if (hatIds.length > 0 && ctx.provider) {
|
|
265
|
+
try {
|
|
266
|
+
const hv = new ethers_1.ethers.Contract(votingAddress, ['function MAX_POLL_HATS() view returns (uint16)'], ctx.provider);
|
|
267
|
+
const cap = Number(await hv.MAX_POLL_HATS());
|
|
268
|
+
if (cap > 0 && hatIds.length > cap) {
|
|
269
|
+
throw new errors_1.CliError(`${hatIds.length} restricted hats supplied but the contract allows at most ${cap}.`, exit_codes_1.EXIT.USAGE, 'Reduce --hat-ids, or omit it entirely to let every eligible member vote.');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
if (err instanceof errors_1.CliError)
|
|
274
|
+
throw err;
|
|
275
|
+
// Cap check skipped — no getter on this deployment.
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Build execution batches: calls go to option 0, other options get empty
|
|
279
|
+
// batches. Keyed on calls being GIVEN, not non-empty — the CLI branches on
|
|
280
|
+
// `if (argv.calls)`, so `--calls '[]'` encodes [[], [], …] while an omitted
|
|
281
|
+
// flag encodes [] entirely. Both are valid on-chain but the bytes differ,
|
|
282
|
+
// and calldata parity is the contract here.
|
|
283
|
+
const batches = [];
|
|
284
|
+
if (params.calls !== undefined) {
|
|
285
|
+
batches.push(params.calls.map(c => ({
|
|
286
|
+
target: c.target,
|
|
287
|
+
value: ethers_1.ethers.BigNumber.from(c.value || '0'),
|
|
288
|
+
calldata: c.data,
|
|
289
|
+
})));
|
|
290
|
+
for (let i = 1; i < numOptions; i++) {
|
|
291
|
+
batches.push([]);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Proposal metadata (key order matches the frontend).
|
|
295
|
+
const metadata = (0, proposal_1.buildProposalMetadata)({
|
|
296
|
+
description: params.description,
|
|
297
|
+
optionNames,
|
|
298
|
+
createdAt: params.createdAt,
|
|
299
|
+
});
|
|
300
|
+
const { cid, descriptionHash } = await pinProposalMetadata(ctx, metadata);
|
|
301
|
+
const summary = {
|
|
302
|
+
type: params.type,
|
|
303
|
+
title: params.name,
|
|
304
|
+
options: optionNames.join(', '),
|
|
305
|
+
duration: `${params.durationMinutes} minutes`,
|
|
306
|
+
};
|
|
307
|
+
if (params.calls && params.calls.length > 0) {
|
|
308
|
+
describeExecutionCalls(params.calls, modules).forEach((line, i) => {
|
|
309
|
+
summary[`call ${i + 1}/${params.calls.length}`] = line;
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return (0, governance_1.buildGovernanceProposal)({
|
|
313
|
+
votingAddress,
|
|
314
|
+
votingAbiName,
|
|
315
|
+
title: params.name,
|
|
316
|
+
descriptionHash,
|
|
317
|
+
durationMinutes: params.durationMinutes,
|
|
318
|
+
numOptions,
|
|
319
|
+
batches,
|
|
320
|
+
hatIds,
|
|
321
|
+
orgId: modules.orgId,
|
|
322
|
+
action: 'create',
|
|
323
|
+
summary,
|
|
324
|
+
ipfs: { cid, metadata },
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Port of `pop vote cast` — src/commands/vote/cast.ts.
|
|
329
|
+
*
|
|
330
|
+
* Input safety kept from the CLI: option/weight counts must match, weights
|
|
331
|
+
* must be non-negative and sum to exactly 100 (the contract's invariant) —
|
|
332
|
+
* all validated before any network traffic. The proposal argument accepts a
|
|
333
|
+
* numeric ID or fuzzy title query (resolved with preferActive: an ended
|
|
334
|
+
* homonym never shadows the one still-open match).
|
|
335
|
+
*/
|
|
336
|
+
async function castVoteIntent(ctx, params) {
|
|
337
|
+
const { optionIndices, weights } = params;
|
|
338
|
+
if (optionIndices.length !== weights.length) {
|
|
339
|
+
throw new errors_1.CliError('Number of options must match number of weights', exit_codes_1.EXIT.USAGE);
|
|
340
|
+
}
|
|
341
|
+
if (weights.some(w => w < 0)) {
|
|
342
|
+
throw new errors_1.CliError('Weights must be non-negative', exit_codes_1.EXIT.USAGE);
|
|
343
|
+
}
|
|
344
|
+
const weightSum = weights.reduce((a, b) => a + b, 0);
|
|
345
|
+
if (weightSum !== 100) {
|
|
346
|
+
throw new errors_1.CliError(`Weights must sum to 100, got ${weightSum}`, exit_codes_1.EXIT.USAGE);
|
|
347
|
+
}
|
|
348
|
+
const { modules, votingAddress, votingAbiName } = await resolveVotingModule(ctx, params.org, params.type);
|
|
349
|
+
const proposalId = await (0, vote_1.resolveProposalId)(ctx.client, String(params.proposal), votingAddress, ctx.chainId, { preferActive: true });
|
|
350
|
+
return buildVote({
|
|
351
|
+
votingAddress,
|
|
352
|
+
votingAbiName,
|
|
353
|
+
proposalId,
|
|
354
|
+
optionIndices,
|
|
355
|
+
weights,
|
|
356
|
+
orgId: modules.orgId,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Port of `pop vote announce` — src/commands/vote/announce.ts (also the
|
|
361
|
+
* per-proposal intent of `pop vote announce-all`).
|
|
362
|
+
*
|
|
363
|
+
* DESTRUCTIVE. This builder only resolves and encodes; the CLI's safety
|
|
364
|
+
* layers stay host-side: (1) announceWinnerProbe before broadcasting,
|
|
365
|
+
* (2) parseAnnounceReceiptLogs after — outer success + innerFailure means
|
|
366
|
+
* the proposal finalized but its execution reverted.
|
|
367
|
+
*/
|
|
368
|
+
async function announceWinnerIntent(ctx, params) {
|
|
369
|
+
const { modules, votingAddress, votingAbiName } = await resolveVotingModule(ctx, params.org, params.type);
|
|
370
|
+
const proposalId = await (0, vote_1.resolveProposalId)(ctx.client, String(params.proposal), votingAddress, ctx.chainId);
|
|
371
|
+
return buildAnnounceWinner({ votingAddress, votingAbiName, proposalId, orgId: modules.orgId, action: 'announce' });
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Port of `pop vote execute` — src/commands/vote/execute.ts. Hybrid-only:
|
|
375
|
+
* announceWinner is the one canonical finalize path (the Executor is
|
|
376
|
+
* allowedCaller-gated, so proposal calls cannot be executed directly).
|
|
377
|
+
*
|
|
378
|
+
* The CLI's subgraph state gate is NOT run here — hosts replicate it with
|
|
379
|
+
* reads/vote fetchProposalExecutionState + evaluateExecuteGate, then probe
|
|
380
|
+
* with announceWinnerProbe, then scan the receipt with
|
|
381
|
+
* parseAnnounceReceiptLogs (executionFailed proposals are retryable since
|
|
382
|
+
* audit H-05; the callStatic probe is the arbiter on older deployments).
|
|
383
|
+
*/
|
|
384
|
+
async function executeProposalIntent(ctx, params) {
|
|
385
|
+
const modules = await (0, resolve_1.resolveOrgModules)(ctx.client, params.org, ctx.chainId);
|
|
386
|
+
const hybridVotingAddress = modules.hybridVotingAddress;
|
|
387
|
+
if (!hybridVotingAddress) {
|
|
388
|
+
throw new errors_1.CliError('No HybridVoting contract found for this org', exit_codes_1.EXIT.PRECONDITION);
|
|
389
|
+
}
|
|
390
|
+
const proposalId = await (0, vote_1.resolveProposalId)(ctx.client, String(params.proposal), hybridVotingAddress, ctx.chainId);
|
|
391
|
+
return buildAnnounceWinner({
|
|
392
|
+
votingAddress: hybridVotingAddress,
|
|
393
|
+
votingAbiName: 'HybridVotingNew',
|
|
394
|
+
proposalId,
|
|
395
|
+
orgId: modules.orgId,
|
|
396
|
+
action: 'execute',
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
// ───────────────────── Level 2 — vote propose-quorum ─────────────────────
|
|
400
|
+
/**
|
|
401
|
+
* setConfig keys for quorum (contracts origin/main, v6):
|
|
402
|
+
* HybridVoting: key 3 (QUORUM)
|
|
403
|
+
* DirectDemocracyVoting: key 4 (QUORUM)
|
|
404
|
+
*
|
|
405
|
+
* The voting contracts use setConfig(uint8, bytes) instead of a direct
|
|
406
|
+
* setter, and decode the value as uint32. Since PR #119 quorum is a minimum
|
|
407
|
+
* voter COUNT (0 disables), not a percentage.
|
|
408
|
+
*/
|
|
409
|
+
exports.HYBRID_QUORUM_KEY = 3;
|
|
410
|
+
exports.DD_QUORUM_KEY = 4;
|
|
411
|
+
/**
|
|
412
|
+
* Port of `pop vote propose-quorum` — src/commands/vote/propose-quorum.ts.
|
|
413
|
+
*
|
|
414
|
+
* Governance wrap: createProposal on HybridVoting with an option-0 batch of
|
|
415
|
+
* setConfig(3, abi.encode(uint32 quorum)) on HybridVoting plus
|
|
416
|
+
* setConfig(4, same) on DD when the org has one; option 1 (keep current) has
|
|
417
|
+
* no calls; hatIds [].
|
|
418
|
+
*/
|
|
419
|
+
async function proposeQuorumIntent(ctx, params) {
|
|
420
|
+
const newQuorum = params.quorum;
|
|
421
|
+
const durationMinutes = params.durationMinutes ?? 60;
|
|
422
|
+
if (newQuorum < 1)
|
|
423
|
+
throw new errors_1.CliError('Quorum must be at least 1', exit_codes_1.EXIT.USAGE);
|
|
424
|
+
const modules = await (0, resolve_1.resolveOrgModules)(ctx.client, params.org, ctx.chainId);
|
|
425
|
+
if (!modules.hybridVotingAddress)
|
|
426
|
+
throw new errors_1.CliError('HybridVoting not deployed', exit_codes_1.EXIT.USAGE);
|
|
427
|
+
// Encode setConfig calls for both voting contracts. Both ABIs carry exactly
|
|
428
|
+
// one setConfig(uint8,bytes) fragment, so this matches the CLI's inline
|
|
429
|
+
// `['function setConfig(uint8 key, bytes value)']` encoding byte-for-byte.
|
|
430
|
+
const encodedValue = ethers_1.ethers.utils.defaultAbiCoder.encode(['uint32'], [newQuorum]);
|
|
431
|
+
const option0Batch = [
|
|
432
|
+
(0, governance_1.encodeExecutorCall)('HybridVotingNew', modules.hybridVotingAddress, 'setConfig', [exports.HYBRID_QUORUM_KEY, encodedValue]),
|
|
433
|
+
];
|
|
434
|
+
if (modules.ddVotingAddress) {
|
|
435
|
+
option0Batch.push((0, governance_1.encodeExecutorCall)('DirectDemocracyVotingNew', modules.ddVotingAddress, 'setConfig', [exports.DD_QUORUM_KEY, encodedValue]));
|
|
436
|
+
}
|
|
437
|
+
const batches = [option0Batch, []]; // option 1 (keep current) has no calls
|
|
438
|
+
const metadata = (0, proposal_1.buildProposalMetadata)({
|
|
439
|
+
description: `Change voting quorum to ${newQuorum}. Updates both Hybrid (setConfig key ${exports.HYBRID_QUORUM_KEY}) and DD (setConfig key ${exports.DD_QUORUM_KEY}) voting contracts via execution calls.`,
|
|
440
|
+
optionNames: [`Set quorum to ${newQuorum}`, 'Keep current quorum'],
|
|
441
|
+
createdAt: params.createdAt,
|
|
442
|
+
});
|
|
443
|
+
const { cid, descriptionHash } = await pinProposalMetadata(ctx, metadata);
|
|
444
|
+
const contractList = modules.ddVotingAddress ? 'Hybrid + DD' : 'Hybrid only';
|
|
445
|
+
return (0, governance_1.buildGovernanceProposal)({
|
|
446
|
+
votingAddress: modules.hybridVotingAddress,
|
|
447
|
+
votingAbiName: 'HybridVotingNew',
|
|
448
|
+
title: `Set voting quorum to ${newQuorum}`,
|
|
449
|
+
descriptionHash,
|
|
450
|
+
durationMinutes,
|
|
451
|
+
numOptions: 2,
|
|
452
|
+
batches,
|
|
453
|
+
hatIds: [],
|
|
454
|
+
orgId: modules.orgId,
|
|
455
|
+
action: 'propose-quorum',
|
|
456
|
+
summary: { newQuorum, duration: `${durationMinutes} minutes`, contracts: contractList },
|
|
457
|
+
ipfs: { cid, metadata },
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
/** Port of CONFIG_PARAMS (src/commands/vote/propose-config.ts) — verbatim. */
|
|
461
|
+
exports.CONFIG_PARAMS = {
|
|
462
|
+
threshold: {
|
|
463
|
+
name: 'threshold',
|
|
464
|
+
hybridKey: 0,
|
|
465
|
+
ddKey: 0,
|
|
466
|
+
valueType: 'uint8',
|
|
467
|
+
description: 'Support threshold percentage (1-100)',
|
|
468
|
+
encode: (v) => ethers_1.ethers.utils.defaultAbiCoder.encode(['uint8'], [parseInt(v, 10)]),
|
|
469
|
+
validate: (v) => {
|
|
470
|
+
const n = parseInt(v, 10);
|
|
471
|
+
if (isNaN(n) || n < 1 || n > 100)
|
|
472
|
+
throw new Error('Threshold must be 1-100');
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
quorum: {
|
|
476
|
+
name: 'quorum',
|
|
477
|
+
hybridKey: 3,
|
|
478
|
+
ddKey: 4,
|
|
479
|
+
valueType: 'uint32',
|
|
480
|
+
description: 'Minimum voter count for validity (0 disables)',
|
|
481
|
+
encode: (v) => ethers_1.ethers.utils.defaultAbiCoder.encode(['uint32'], [parseInt(v, 10)]),
|
|
482
|
+
validate: (v) => {
|
|
483
|
+
const n = Number(v);
|
|
484
|
+
if (!Number.isInteger(n) || n < 0 || n > 4294967295) {
|
|
485
|
+
throw new Error('Quorum must be an integer >= 0 (uint32 voter count, 0 disables)');
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
'target-allowed': {
|
|
490
|
+
name: 'target-allowed',
|
|
491
|
+
hybridKey: -1, // deprecated no-op on HybridVoting v6 — setConfig silently ignores it
|
|
492
|
+
ddKey: 2,
|
|
493
|
+
valueType: 'address,bool',
|
|
494
|
+
description: 'Allow/disallow an execution target address on DD voting (format: 0xaddr,true/false)',
|
|
495
|
+
hybridUnavailableReason: 'TARGET_ALLOWED is a deprecated no-op on HybridVoting (setConfig silently ignores it); this key only applies to DirectDemocracyVoting',
|
|
496
|
+
encode: (v) => {
|
|
497
|
+
const [addr, allowed] = v.split(',');
|
|
498
|
+
return ethers_1.ethers.utils.defaultAbiCoder.encode(['address', 'bool'], [addr.trim(), allowed.trim() === 'true']);
|
|
499
|
+
},
|
|
500
|
+
validate: (v) => {
|
|
501
|
+
const parts = v.split(',');
|
|
502
|
+
if (parts.length !== 2)
|
|
503
|
+
throw new Error('Format: 0xaddress,true/false');
|
|
504
|
+
if (!ethers_1.ethers.utils.isAddress(parts[0].trim()))
|
|
505
|
+
throw new Error('Invalid address');
|
|
506
|
+
if (!['true', 'false'].includes(parts[1].trim()))
|
|
507
|
+
throw new Error('Second value must be true or false');
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
executor: {
|
|
511
|
+
name: 'executor',
|
|
512
|
+
hybridKey: 2,
|
|
513
|
+
ddKey: 1,
|
|
514
|
+
valueType: 'address',
|
|
515
|
+
description: 'Change the executor contract address',
|
|
516
|
+
encode: (v) => ethers_1.ethers.utils.defaultAbiCoder.encode(['address'], [v.trim()]),
|
|
517
|
+
validate: (v) => {
|
|
518
|
+
if (!ethers_1.ethers.utils.isAddress(v.trim()))
|
|
519
|
+
throw new Error('Invalid address');
|
|
520
|
+
if (v.trim() === ethers_1.ethers.constants.AddressZero)
|
|
521
|
+
throw new Error('Cannot set executor to zero address');
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
'hat-allowed': {
|
|
525
|
+
name: 'hat-allowed',
|
|
526
|
+
hybridKey: -1, // not available on Hybrid
|
|
527
|
+
ddKey: 3,
|
|
528
|
+
hybridUnavailableReason: 'HAT_ALLOWED is not a HybridVoting config key; this key only applies to DirectDemocracyVoting',
|
|
529
|
+
valueType: 'uint256,bool',
|
|
530
|
+
description: 'Allow/disallow a hat ID for DD voting (format: hatId,true/false)',
|
|
531
|
+
encode: (v) => {
|
|
532
|
+
const [hatId, allowed] = v.split(',');
|
|
533
|
+
return ethers_1.ethers.utils.defaultAbiCoder.encode(['uint256', 'bool'], [hatId.trim(), allowed.trim() === 'true']);
|
|
534
|
+
},
|
|
535
|
+
validate: (v) => {
|
|
536
|
+
const parts = v.split(',');
|
|
537
|
+
if (parts.length !== 2)
|
|
538
|
+
throw new Error('Format: hatId,true/false');
|
|
539
|
+
if (!['true', 'false'].includes(parts[1].trim()))
|
|
540
|
+
throw new Error('Second value must be true or false');
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
/**
|
|
545
|
+
* Port of `pop vote propose-config` — src/commands/vote/propose-config.ts.
|
|
546
|
+
*
|
|
547
|
+
* Governance wrap: createProposal on HybridVoting whose option-0 batch calls
|
|
548
|
+
* setConfig(key, encodedValue) on the applicable voting contract(s) per the
|
|
549
|
+
* CONFIG_PARAMS table. Validation errors are the CLI's own (plain Error,
|
|
550
|
+
* verbatim messages).
|
|
551
|
+
*/
|
|
552
|
+
async function proposeConfigIntent(ctx, params) {
|
|
553
|
+
const paramName = params.key;
|
|
554
|
+
const durationMinutes = params.durationMinutes ?? 60;
|
|
555
|
+
const param = exports.CONFIG_PARAMS[paramName];
|
|
556
|
+
if (!param)
|
|
557
|
+
throw new Error(`Unknown config key: ${paramName}. Available: ${Object.keys(exports.CONFIG_PARAMS).join(', ')}`);
|
|
558
|
+
param.validate(params.value);
|
|
559
|
+
const modules = await (0, resolve_1.resolveOrgModules)(ctx.client, params.org, ctx.chainId);
|
|
560
|
+
if (!modules.hybridVotingAddress)
|
|
561
|
+
throw new Error('HybridVoting not deployed');
|
|
562
|
+
const encodedValue = param.encode(params.value);
|
|
563
|
+
const option0Batch = [];
|
|
564
|
+
// Add Hybrid call if applicable
|
|
565
|
+
if (param.hybridKey >= 0) {
|
|
566
|
+
option0Batch.push((0, governance_1.encodeExecutorCall)('HybridVotingNew', modules.hybridVotingAddress, 'setConfig', [param.hybridKey, encodedValue]));
|
|
567
|
+
}
|
|
568
|
+
// Add DD call if applicable and contract exists
|
|
569
|
+
if (param.ddKey !== null && modules.ddVotingAddress) {
|
|
570
|
+
option0Batch.push((0, governance_1.encodeExecutorCall)('DirectDemocracyVotingNew', modules.ddVotingAddress, 'setConfig', [param.ddKey, encodedValue]));
|
|
571
|
+
}
|
|
572
|
+
if (option0Batch.length === 0) {
|
|
573
|
+
const reason = param.hybridKey < 0 && param.hybridUnavailableReason
|
|
574
|
+
? ` ${param.hybridUnavailableReason}.`
|
|
575
|
+
: '';
|
|
576
|
+
throw new Error(`Config key "${paramName}" has no applicable voting contracts.${reason}` +
|
|
577
|
+
(param.ddKey !== null && !modules.ddVotingAddress
|
|
578
|
+
? ' No DirectDemocracyVoting contract is deployed for this org.'
|
|
579
|
+
: ''));
|
|
580
|
+
}
|
|
581
|
+
const batches = [option0Batch, []]; // option 0 = change, option 1 = keep current
|
|
582
|
+
const contractList = option0Batch.length > 1 ? 'Hybrid + DD' : (param.hybridKey >= 0 ? 'Hybrid' : 'DD');
|
|
583
|
+
const metadata = (0, proposal_1.buildProposalMetadata)({
|
|
584
|
+
description: `Change ${paramName} to ${params.value}. Updates ${contractList} voting contract(s) via setConfig execution calls.`,
|
|
585
|
+
optionNames: [`Set ${paramName} to ${params.value}`, 'Keep current value'],
|
|
586
|
+
createdAt: params.createdAt,
|
|
587
|
+
});
|
|
588
|
+
const { cid, descriptionHash } = await pinProposalMetadata(ctx, metadata);
|
|
589
|
+
return (0, governance_1.buildGovernanceProposal)({
|
|
590
|
+
votingAddress: modules.hybridVotingAddress,
|
|
591
|
+
votingAbiName: 'HybridVotingNew',
|
|
592
|
+
title: `Set ${paramName} to ${params.value}`,
|
|
593
|
+
descriptionHash,
|
|
594
|
+
durationMinutes,
|
|
595
|
+
numOptions: 2,
|
|
596
|
+
batches,
|
|
597
|
+
hatIds: [],
|
|
598
|
+
orgId: modules.orgId,
|
|
599
|
+
action: 'propose-config',
|
|
600
|
+
summary: { key: paramName, value: params.value, contracts: contractList, duration: `${durationMinutes} minutes` },
|
|
601
|
+
ipfs: { cid, metadata },
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
// ──────────────────── Level 2 — vote classes propose ────────────────────
|
|
605
|
+
/** ClassStrategy enum — verified origin/main src/HybridVoting.sol. */
|
|
606
|
+
exports.CLASS_STRATEGY = { DIRECT: 0, ERC20_BAL: 1 };
|
|
607
|
+
const STRATEGY_NAMES = ['DIRECT', 'ERC20_BAL'];
|
|
608
|
+
/** Contract constant MAX_CLASSES (origin/main src/HybridVoting.sol). */
|
|
609
|
+
exports.MAX_CLASSES = 8;
|
|
610
|
+
function usageError(message) {
|
|
611
|
+
return new errors_1.CliError(message, exit_codes_1.EXIT.USAGE, 'Expected a JSON array of ClassConfig objects: ' +
|
|
612
|
+
'[{"strategy":"DIRECT"|"ERC20_BAL"|0|1,"slicePct":n,"quadratic":bool,"minBalance":"0","asset":"0x…","hatIds":["…"]}] ' +
|
|
613
|
+
'with slicePct values summing to 100.');
|
|
614
|
+
}
|
|
615
|
+
function parseStrategy(value, index) {
|
|
616
|
+
if (value === 0 || value === 1)
|
|
617
|
+
return value;
|
|
618
|
+
if (typeof value === 'string') {
|
|
619
|
+
const upper = value.trim().toUpperCase();
|
|
620
|
+
if (upper === 'DIRECT' || upper === '0')
|
|
621
|
+
return exports.CLASS_STRATEGY.DIRECT;
|
|
622
|
+
if (upper === 'ERC20_BAL' || upper === '1')
|
|
623
|
+
return exports.CLASS_STRATEGY.ERC20_BAL;
|
|
624
|
+
}
|
|
625
|
+
throw usageError(`Class ${index}: invalid strategy ${JSON.stringify(value)} — use 'DIRECT'|'ERC20_BAL'|0|1.`);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Port of `parseClassConfigs` (src/commands/vote/classes.ts): validate +
|
|
629
|
+
* normalize a ClassConfig[] JSON document. Mirrors HybridVotingConfig
|
|
630
|
+
* setClasses validation so bad configs fail locally with a pointer to the
|
|
631
|
+
* offending class instead of a generic on-chain revert: 1..MAX_CLASSES(8)
|
|
632
|
+
* classes, each slicePct 1..100 and summing to exactly 100, ERC20_BAL needs a
|
|
633
|
+
* non-zero asset. Hat IDs are parsed as BigNumbers straight from their raw
|
|
634
|
+
* strings — real Hats IDs exceed 2^53 and would be mangled by Number.
|
|
635
|
+
*/
|
|
636
|
+
function parseClassConfigs(doc) {
|
|
637
|
+
if (!Array.isArray(doc))
|
|
638
|
+
throw usageError('Classes file must contain a JSON array of ClassConfig objects.');
|
|
639
|
+
if (doc.length === 0)
|
|
640
|
+
throw usageError('At least one voting class is required.');
|
|
641
|
+
if (doc.length > exports.MAX_CLASSES) {
|
|
642
|
+
throw usageError(`Too many classes: ${doc.length} (the contract allows at most ${exports.MAX_CLASSES}).`);
|
|
643
|
+
}
|
|
644
|
+
const classes = doc.map((entry, i) => {
|
|
645
|
+
if (typeof entry !== 'object' || entry === null)
|
|
646
|
+
throw usageError(`Class ${i}: expected an object.`);
|
|
647
|
+
const strategy = parseStrategy(entry.strategy, i);
|
|
648
|
+
const slicePct = Number(entry.slicePct);
|
|
649
|
+
if (!Number.isInteger(slicePct) || slicePct < 1 || slicePct > 100) {
|
|
650
|
+
throw usageError(`Class ${i}: slicePct must be an integer 1-100 (got ${JSON.stringify(entry.slicePct)}).`);
|
|
651
|
+
}
|
|
652
|
+
let minBalance;
|
|
653
|
+
try {
|
|
654
|
+
minBalance = ethers_1.ethers.BigNumber.from(String(entry.minBalance ?? 0));
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
throw usageError(`Class ${i}: invalid minBalance ${JSON.stringify(entry.minBalance)} — pass a base-unit integer string.`);
|
|
658
|
+
}
|
|
659
|
+
const asset = String(entry.asset ?? ethers_1.ethers.constants.AddressZero);
|
|
660
|
+
if (!ethers_1.ethers.utils.isAddress(asset)) {
|
|
661
|
+
throw usageError(`Class ${i}: invalid asset address ${JSON.stringify(entry.asset)}.`);
|
|
662
|
+
}
|
|
663
|
+
if (strategy === exports.CLASS_STRATEGY.ERC20_BAL && asset === ethers_1.ethers.constants.AddressZero) {
|
|
664
|
+
throw usageError(`Class ${i}: ERC20_BAL strategy requires a non-zero asset address (the contract reverts ZeroAddress).`);
|
|
665
|
+
}
|
|
666
|
+
const rawHatIds = entry.hatIds ?? [];
|
|
667
|
+
if (!Array.isArray(rawHatIds))
|
|
668
|
+
throw usageError(`Class ${i}: hatIds must be an array.`);
|
|
669
|
+
const hatIds = rawHatIds.map((h) => {
|
|
670
|
+
try {
|
|
671
|
+
return ethers_1.ethers.BigNumber.from(String(h).trim());
|
|
672
|
+
}
|
|
673
|
+
catch {
|
|
674
|
+
throw usageError(`Class ${i}: invalid hat ID ${JSON.stringify(h)}.`);
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
return { strategy, slicePct, quadratic: Boolean(entry.quadratic), minBalance, asset, hatIds };
|
|
678
|
+
});
|
|
679
|
+
const sliceSum = classes.reduce((sum, c) => sum + c.slicePct, 0);
|
|
680
|
+
if (sliceSum !== 100) {
|
|
681
|
+
throw new errors_1.CliError(`Class slice percentages must sum to exactly 100 — got ${sliceSum} ` +
|
|
682
|
+
`(${classes.map(c => `${c.slicePct}%`).join(' + ')}). The contract reverts InvalidSliceSum otherwise.`, exit_codes_1.EXIT.USAGE, 'Adjust the slicePct values in the classes file so they total 100.');
|
|
683
|
+
}
|
|
684
|
+
return classes;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Port of `describeClass` (src/commands/vote/classes.ts): one-line human
|
|
688
|
+
* description of a class (confirm summary + metadata description).
|
|
689
|
+
*/
|
|
690
|
+
function describeClass(c) {
|
|
691
|
+
const name = STRATEGY_NAMES[c.strategy] ?? String(c.strategy);
|
|
692
|
+
const parts = [`${name} ${c.slicePct}%`];
|
|
693
|
+
if (c.quadratic)
|
|
694
|
+
parts.push('quadratic');
|
|
695
|
+
if (!c.minBalance.isZero())
|
|
696
|
+
parts.push(`min balance ${(0, format_1.formatToken)(c.minBalance)}`);
|
|
697
|
+
if (c.asset !== ethers_1.ethers.constants.AddressZero)
|
|
698
|
+
parts.push(`asset ${(0, encoding_1.formatAddress)(c.asset)}`);
|
|
699
|
+
if (c.hatIds.length > 0)
|
|
700
|
+
parts.push(`hats ${c.hatIds.map(h => h.toString()).join(',')}`);
|
|
701
|
+
return parts.join(', ');
|
|
702
|
+
}
|
|
703
|
+
function isParsedClassConfigs(value) {
|
|
704
|
+
return Array.isArray(value) && value.every((c) => c && typeof c === 'object' && ethers_1.ethers.BigNumber.isBigNumber(c.minBalance));
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Port of `pop vote classes propose` — src/commands/vote/classes.ts
|
|
708
|
+
* (classesProposeHandler).
|
|
709
|
+
*
|
|
710
|
+
* setClasses(ClassConfig[]) is executor-gated on HybridVoting (verified:
|
|
711
|
+
* `function setClasses(ClassConfig[] calldata) external onlyExecutor`), so it
|
|
712
|
+
* ships as a governance proposal whose option-0 execution batch calls the
|
|
713
|
+
* voting contract through the Executor (propose-config pattern). ClassConfig
|
|
714
|
+
* tuple order: [strategy, slicePct, quadratic, minBalance, asset, hatIds].
|
|
715
|
+
*/
|
|
716
|
+
async function proposeClassesIntent(ctx, params) {
|
|
717
|
+
const durationMinutes = params.durationMinutes ?? 60;
|
|
718
|
+
const classes = isParsedClassConfigs(params.classes)
|
|
719
|
+
? params.classes
|
|
720
|
+
: parseClassConfigs(params.classes);
|
|
721
|
+
const modules = await (0, resolve_1.resolveOrgModules)(ctx.client, params.org, ctx.chainId);
|
|
722
|
+
const hybridVotingAddress = modules.hybridVotingAddress;
|
|
723
|
+
if (!hybridVotingAddress) {
|
|
724
|
+
throw new errors_1.CliError('HybridVoting not deployed for this org — voting classes only apply to hybrid voting.', exit_codes_1.EXIT.PRECONDITION);
|
|
725
|
+
}
|
|
726
|
+
const setClassesCall = (0, governance_1.encodeExecutorCall)('HybridVotingNew', hybridVotingAddress, 'setClasses', [
|
|
727
|
+
classes.map(c => [c.strategy, c.slicePct, c.quadratic, c.minBalance, c.asset, c.hatIds]),
|
|
728
|
+
]);
|
|
729
|
+
const batches = [
|
|
730
|
+
[setClassesCall], // option 0: apply
|
|
731
|
+
[], // option 1: keep current
|
|
732
|
+
];
|
|
733
|
+
const classLines = classes.map(describeClass);
|
|
734
|
+
const title = `Update hybrid voting classes (${classes.length} class${classes.length === 1 ? '' : 'es'})`;
|
|
735
|
+
const metadata = (0, proposal_1.buildProposalMetadata)({
|
|
736
|
+
description: `Replace the hybrid voting class configuration via HybridVoting.setClasses (executor-gated). ` +
|
|
737
|
+
`New classes: ${classLines.map((line, i) => `[${i}] ${line}`).join('; ')}.`,
|
|
738
|
+
optionNames: [title, 'Keep current classes'],
|
|
739
|
+
createdAt: params.createdAt,
|
|
740
|
+
});
|
|
741
|
+
const { cid, descriptionHash } = await pinProposalMetadata(ctx, metadata);
|
|
742
|
+
const summary = {
|
|
743
|
+
classes: classes.length,
|
|
744
|
+
target: `HybridVoting ${(0, encoding_1.formatAddress)(hybridVotingAddress)}`,
|
|
745
|
+
via: `governance proposal (${durationMinutes} min vote)`,
|
|
746
|
+
};
|
|
747
|
+
classLines.forEach((line, i) => { summary[`class ${i}`] = line; });
|
|
748
|
+
return (0, governance_1.buildGovernanceProposal)({
|
|
749
|
+
votingAddress: hybridVotingAddress,
|
|
750
|
+
votingAbiName: 'HybridVotingNew',
|
|
751
|
+
title,
|
|
752
|
+
descriptionHash,
|
|
753
|
+
durationMinutes,
|
|
754
|
+
numOptions: 2,
|
|
755
|
+
batches,
|
|
756
|
+
hatIds: [],
|
|
757
|
+
orgId: modules.orgId,
|
|
758
|
+
action: 'classes-propose',
|
|
759
|
+
summary,
|
|
760
|
+
ipfs: { cid, metadata },
|
|
761
|
+
});
|
|
762
|
+
}
|