@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.
Files changed (220) hide show
  1. package/README.md +201 -0
  2. package/dist/abis/DirectDemocracyVotingNew.d.ts +749 -0
  3. package/dist/abis/DirectDemocracyVotingNew.js +961 -0
  4. package/dist/abis/ERC20.d.ts +90 -0
  5. package/dist/abis/ERC20.js +125 -0
  6. package/dist/abis/EducationHubNew.d.ts +590 -0
  7. package/dist/abis/EducationHubNew.js +762 -0
  8. package/dist/abis/EligibilityModuleNew.d.ts +1615 -0
  9. package/dist/abis/EligibilityModuleNew.js +2082 -0
  10. package/dist/abis/Executor.d.ts +619 -0
  11. package/dist/abis/Executor.js +796 -0
  12. package/dist/abis/HybridVotingNew.d.ts +898 -0
  13. package/dist/abis/HybridVotingNew.js +1149 -0
  14. package/dist/abis/ImplementationRegistry.d.ts +270 -0
  15. package/dist/abis/ImplementationRegistry.js +355 -0
  16. package/dist/abis/OrgDeployerNew.d.ts +1275 -0
  17. package/dist/abis/OrgDeployerNew.js +1630 -0
  18. package/dist/abis/OrgRegistry.d.ts +670 -0
  19. package/dist/abis/OrgRegistry.js +868 -0
  20. package/dist/abis/ParticipationToken.d.ts +1086 -0
  21. package/dist/abis/ParticipationToken.js +1415 -0
  22. package/dist/abis/PasskeyAccount.d.ts +788 -0
  23. package/dist/abis/PasskeyAccount.js +1019 -0
  24. package/dist/abis/PasskeyAccountFactory.d.ts +344 -0
  25. package/dist/abis/PasskeyAccountFactory.js +449 -0
  26. package/dist/abis/PaymasterHub.d.ts +1603 -0
  27. package/dist/abis/PaymasterHub.js +2048 -0
  28. package/dist/abis/PaymentManager.d.ts +520 -0
  29. package/dist/abis/PaymentManager.js +671 -0
  30. package/dist/abis/PoaManager.d.ts +344 -0
  31. package/dist/abis/PoaManager.js +449 -0
  32. package/dist/abis/QuickJoinNew.d.ts +855 -0
  33. package/dist/abis/QuickJoinNew.js +1098 -0
  34. package/dist/abis/TaskManagerNew.d.ts +1236 -0
  35. package/dist/abis/TaskManagerNew.js +1573 -0
  36. package/dist/abis/ToggleModule.d.ts +193 -0
  37. package/dist/abis/ToggleModule.js +255 -0
  38. package/dist/abis/UniversalAccountRegistry.d.ts +577 -0
  39. package/dist/abis/UniversalAccountRegistry.js +750 -0
  40. package/dist/abis/ZkEmailInvites.d.ts +823 -0
  41. package/dist/abis/ZkEmailInvites.js +1060 -0
  42. package/dist/abis/external/AaveGovernanceV2.d.ts +128 -0
  43. package/dist/abis/external/AaveGovernanceV2.js +177 -0
  44. package/dist/abis/external/AaveGovernanceV3.d.ts +160 -0
  45. package/dist/abis/external/AaveGovernanceV3.js +220 -0
  46. package/dist/abis/external/AragonVoting.d.ts +107 -0
  47. package/dist/abis/external/AragonVoting.js +150 -0
  48. package/dist/abis/external/CurveGaugeController.d.ts +95 -0
  49. package/dist/abis/external/CurveGaugeController.js +130 -0
  50. package/dist/abis/external/CurveVotingEscrow.d.ts +86 -0
  51. package/dist/abis/external/CurveVotingEscrow.js +116 -0
  52. package/dist/abis/external/GovernorAlpha.d.ts +172 -0
  53. package/dist/abis/external/GovernorAlpha.js +228 -0
  54. package/dist/abis/external/MakerDAOChief.d.ts +92 -0
  55. package/dist/abis/external/MakerDAOChief.js +129 -0
  56. package/dist/abis/external/OZGovernor.d.ts +227 -0
  57. package/dist/abis/external/OZGovernor.js +317 -0
  58. package/dist/abis/external/SolidlyVotingEscrow.d.ts +196 -0
  59. package/dist/abis/external/SolidlyVotingEscrow.js +260 -0
  60. package/dist/abis/index.d.ts +62 -0
  61. package/dist/abis/index.js +98 -0
  62. package/dist/chains.d.ts +150 -0
  63. package/dist/chains.js +320 -0
  64. package/dist/context.d.ts +48 -0
  65. package/dist/context.js +38 -0
  66. package/dist/contracts.d.ts +24 -0
  67. package/dist/contracts.js +41 -0
  68. package/dist/encoding.d.ts +58 -0
  69. package/dist/encoding.js +243 -0
  70. package/dist/env.d.ts +10 -0
  71. package/dist/env.js +4 -0
  72. package/dist/error-catalog.d.ts +41 -0
  73. package/dist/error-catalog.js +941 -0
  74. package/dist/errors.d.ts +27 -0
  75. package/dist/errors.js +60 -0
  76. package/dist/execute/ethers.d.ts +93 -0
  77. package/dist/execute/ethers.js +323 -0
  78. package/dist/execute/index.d.ts +7 -0
  79. package/dist/execute/index.js +23 -0
  80. package/dist/execute/sponsored.d.ts +68 -0
  81. package/dist/execute/sponsored.js +277 -0
  82. package/dist/exit-codes.d.ts +18 -0
  83. package/dist/exit-codes.js +21 -0
  84. package/dist/format.d.ts +21 -0
  85. package/dist/format.js +89 -0
  86. package/dist/graph/client.d.ts +298 -0
  87. package/dist/graph/client.js +702 -0
  88. package/dist/graph/documents/activity.d.ts +9 -0
  89. package/dist/graph/documents/activity.js +164 -0
  90. package/dist/graph/documents/beacons.d.ts +172 -0
  91. package/dist/graph/documents/beacons.js +345 -0
  92. package/dist/graph/documents/index.d.ts +21 -0
  93. package/dist/graph/documents/index.js +57 -0
  94. package/dist/graph/documents/infrastructure.d.ts +30 -0
  95. package/dist/graph/documents/infrastructure.js +34 -0
  96. package/dist/graph/documents/org.d.ts +15 -0
  97. package/dist/graph/documents/org.js +249 -0
  98. package/dist/graph/documents/paymaster.d.ts +95 -0
  99. package/dist/graph/documents/paymaster.js +101 -0
  100. package/dist/graph/documents/role.d.ts +6 -0
  101. package/dist/graph/documents/role.js +41 -0
  102. package/dist/graph/documents/roles.d.ts +50 -0
  103. package/dist/graph/documents/roles.js +112 -0
  104. package/dist/graph/documents/task.d.ts +84 -0
  105. package/dist/graph/documents/task.js +236 -0
  106. package/dist/graph/documents/token.d.ts +13 -0
  107. package/dist/graph/documents/token.js +126 -0
  108. package/dist/graph/documents/treasury.d.ts +39 -0
  109. package/dist/graph/documents/treasury.js +136 -0
  110. package/dist/graph/documents/user.d.ts +120 -0
  111. package/dist/graph/documents/user.js +302 -0
  112. package/dist/graph/documents/voting-classes.d.ts +69 -0
  113. package/dist/graph/documents/voting-classes.js +149 -0
  114. package/dist/graph/documents/voting.d.ts +28 -0
  115. package/dist/graph/documents/voting.js +163 -0
  116. package/dist/graph/documents/vouch.d.ts +79 -0
  117. package/dist/graph/documents/vouch.js +171 -0
  118. package/dist/graph/documents/zkemail.d.ts +63 -0
  119. package/dist/graph/documents/zkemail.js +188 -0
  120. package/dist/graph/index.d.ts +2 -0
  121. package/dist/graph/index.js +41 -0
  122. package/dist/index.d.ts +49 -0
  123. package/dist/index.js +92 -0
  124. package/dist/ipfs.d.ts +39 -0
  125. package/dist/ipfs.js +214 -0
  126. package/dist/label-aliases.d.ts +31 -0
  127. package/dist/label-aliases.js +70 -0
  128. package/dist/metadata/education.d.ts +30 -0
  129. package/dist/metadata/education.js +31 -0
  130. package/dist/metadata/index.d.ts +12 -0
  131. package/dist/metadata/index.js +48 -0
  132. package/dist/metadata/org.d.ts +92 -0
  133. package/dist/metadata/org.js +93 -0
  134. package/dist/metadata/proposal.d.ts +44 -0
  135. package/dist/metadata/proposal.js +45 -0
  136. package/dist/metadata/role.d.ts +38 -0
  137. package/dist/metadata/role.js +35 -0
  138. package/dist/metadata/task.d.ts +108 -0
  139. package/dist/metadata/task.js +81 -0
  140. package/dist/metadata/token.d.ts +32 -0
  141. package/dist/metadata/token.js +36 -0
  142. package/dist/metadata/user.d.ts +63 -0
  143. package/dist/metadata/user.js +70 -0
  144. package/dist/multicall.d.ts +32 -0
  145. package/dist/multicall.js +96 -0
  146. package/dist/payout.d.ts +59 -0
  147. package/dist/payout.js +96 -0
  148. package/dist/perms.d.ts +47 -0
  149. package/dist/perms.js +120 -0
  150. package/dist/preflight.d.ts +72 -0
  151. package/dist/preflight.js +269 -0
  152. package/dist/reads/education.d.ts +79 -0
  153. package/dist/reads/education.js +122 -0
  154. package/dist/reads/eligibility.d.ts +377 -0
  155. package/dist/reads/eligibility.js +687 -0
  156. package/dist/reads/index.d.ts +16 -0
  157. package/dist/reads/index.js +55 -0
  158. package/dist/reads/org.d.ts +258 -0
  159. package/dist/reads/org.js +220 -0
  160. package/dist/reads/paymaster.d.ts +147 -0
  161. package/dist/reads/paymaster.js +235 -0
  162. package/dist/reads/project.d.ts +33 -0
  163. package/dist/reads/project.js +62 -0
  164. package/dist/reads/resolve.d.ts +34 -0
  165. package/dist/reads/resolve.js +64 -0
  166. package/dist/reads/task.d.ts +286 -0
  167. package/dist/reads/task.js +280 -0
  168. package/dist/reads/token.d.ts +94 -0
  169. package/dist/reads/token.js +75 -0
  170. package/dist/reads/treasury.d.ts +222 -0
  171. package/dist/reads/treasury.js +241 -0
  172. package/dist/reads/user.d.ts +229 -0
  173. package/dist/reads/user.js +155 -0
  174. package/dist/reads/vote.d.ts +296 -0
  175. package/dist/reads/vote.js +392 -0
  176. package/dist/reads/zkemail.d.ts +117 -0
  177. package/dist/reads/zkemail.js +217 -0
  178. package/dist/similarity.d.ts +36 -0
  179. package/dist/similarity.js +67 -0
  180. package/dist/sponsorship-config.d.ts +47 -0
  181. package/dist/sponsorship-config.js +41 -0
  182. package/dist/stats.d.ts +11 -0
  183. package/dist/stats.js +27 -0
  184. package/dist/task-lens.d.ts +113 -0
  185. package/dist/task-lens.js +210 -0
  186. package/dist/tx/education.d.ts +169 -0
  187. package/dist/tx/education.js +344 -0
  188. package/dist/tx/eligibility.d.ts +511 -0
  189. package/dist/tx/eligibility.js +818 -0
  190. package/dist/tx/governance.d.ts +85 -0
  191. package/dist/tx/governance.js +87 -0
  192. package/dist/tx/index.d.ts +17 -0
  193. package/dist/tx/index.js +56 -0
  194. package/dist/tx/intent.d.ts +67 -0
  195. package/dist/tx/intent.js +48 -0
  196. package/dist/tx/org.d.ts +321 -0
  197. package/dist/tx/org.js +544 -0
  198. package/dist/tx/paymaster.d.ts +140 -0
  199. package/dist/tx/paymaster.js +251 -0
  200. package/dist/tx/project.d.ts +146 -0
  201. package/dist/tx/project.js +217 -0
  202. package/dist/tx/task.d.ts +480 -0
  203. package/dist/tx/task.js +1149 -0
  204. package/dist/tx/token.d.ts +96 -0
  205. package/dist/tx/token.js +165 -0
  206. package/dist/tx/treasury.d.ts +474 -0
  207. package/dist/tx/treasury.js +911 -0
  208. package/dist/tx/user.d.ts +177 -0
  209. package/dist/tx/user.js +292 -0
  210. package/dist/tx/vote.d.ts +332 -0
  211. package/dist/tx/vote.js +762 -0
  212. package/dist/tx/zkemail.d.ts +90 -0
  213. package/dist/tx/zkemail.js +210 -0
  214. package/dist/validation.d.ts +8 -0
  215. package/dist/validation.js +39 -0
  216. package/dist/version.d.ts +142 -0
  217. package/dist/version.js +257 -0
  218. package/dist/zkemail.d.ts +161 -0
  219. package/dist/zkemail.js +299 -0
  220. package/package.json +130 -0
@@ -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
+ }