@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,392 @@
1
+ "use strict";
2
+ /**
3
+ * Vote/governance reads — typed wrappers over the voting graph documents for
4
+ * the entities the CLI's `pop vote list / results / execute / classes show`
5
+ * commands read, plus the pure selection helpers those commands share
6
+ * (fuzzy --proposal resolution, announce-all candidate selection, the
7
+ * execute state gate, results tallying, class-snapshot normalization).
8
+ *
9
+ * Raw subgraph entity shapes only — scalars arrive as strings.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.GET_PROPOSAL_EXECUTION_STATE = void 0;
13
+ exports.fetchVotingData = fetchVotingData;
14
+ exports.fetchRecentProposals = fetchRecentProposals;
15
+ exports.resolveProposalId = resolveProposalId;
16
+ exports.selectAnnounceCandidates = selectAnnounceCandidates;
17
+ exports.fetchProposalExecutionState = fetchProposalExecutionState;
18
+ exports.evaluateExecuteGate = evaluateExecuteGate;
19
+ exports.buildProposalResultsQuery = buildProposalResultsQuery;
20
+ exports.computeProposalResults = computeProposalResults;
21
+ exports.fetchProposalResults = fetchProposalResults;
22
+ exports.normalizeSubgraphClasses = normalizeSubgraphClasses;
23
+ exports.fetchClassConfig = fetchClassConfig;
24
+ const ethers_1 = require("ethers");
25
+ const voting_1 = require("../graph/documents/voting");
26
+ const voting_classes_1 = require("../graph/documents/voting-classes");
27
+ const resolve_1 = require("./resolve");
28
+ const similarity_1 = require("../similarity");
29
+ const errors_1 = require("../errors");
30
+ const exit_codes_1 = require("../exit-codes");
31
+ // ────────────────────────── proposals list ──────────────────────────
32
+ /**
33
+ * Port of the `pop vote list` / `pop vote announce-all` data fetch
34
+ * (src/commands/vote/list.ts, src/commands/vote/announce-all.ts):
35
+ * FETCH_VOTING_DATA with the pre-#195 legacy tier as fallback.
36
+ *
37
+ * A GraphQL document validates as a whole, so one unknown field fails the
38
+ * entire query — the legacy tier keeps listing (and announcing) working
39
+ * against a pre-#195 subgraph deployment.
40
+ */
41
+ async function fetchVotingData(client, orgIdOrName, chainId) {
42
+ const orgId = await (0, resolve_1.resolveOrgId)(client, orgIdOrName, chainId);
43
+ const { data } = await client.queryWithFieldFallback([
44
+ { query: voting_1.FETCH_VOTING_DATA, variables: { orgId } },
45
+ { query: voting_1.FETCH_VOTING_DATA_LEGACY, variables: { orgId } },
46
+ ], { chainId });
47
+ return data.organization ?? null;
48
+ }
49
+ /**
50
+ * Port of `fetchRecentProposals` (src/commands/vote/helpers.ts): recent
51
+ * proposal titles for ONE voting contract, addressed by contract address —
52
+ * works for both Hybrid and DD (whichever entity matches answers, the other
53
+ * root field returns null).
54
+ */
55
+ async function fetchRecentProposals(client, contractAddr, chainId, first = 50) {
56
+ const data = await client.query(voting_1.RECENT_PROPOSALS_FOR_RESOLVE, { votingId: contractAddr.toLowerCase(), first }, chainId);
57
+ const raw = [
58
+ ...(data?.hybridVotingContract?.proposals ?? []),
59
+ ...(data?.directDemocracyVotingContract?.ddvProposals ?? []),
60
+ ];
61
+ return raw.map((p) => ({
62
+ proposalId: String(p.proposalId),
63
+ title: String(p.title ?? ''),
64
+ status: String(p.status ?? ''),
65
+ endTimestamp: p.endTimestamp !== undefined && p.endTimestamp !== null ? String(p.endTimestamp) : undefined,
66
+ }));
67
+ }
68
+ function candidateLine(c) {
69
+ return `#${c.proposalId} '${c.title}' (${c.status})`;
70
+ }
71
+ /**
72
+ * Port of `resolveProposalId` (src/commands/vote/helpers.ts): resolve a
73
+ * user-supplied proposal argument to a numeric proposal ID.
74
+ *
75
+ * - Numeric input → returned as-is (no network round-trip).
76
+ * - Anything else → fuzzy title query against the voting contract's recent
77
+ * proposals, scored with Jaccard bestMatches at threshold 0.5.
78
+ * - exactly one match → resolved
79
+ * - multiple matches → CliError listing the candidates (the CLI's
80
+ * non-TTY behavior; the interactive picker is CLI UX and not ported)
81
+ * - zero matches → CliError listing the 5 most recent titles
82
+ *
83
+ * opts.preferActive (used by `vote cast`): when several proposals match but
84
+ * exactly ONE is still Active, that one wins — you can only cast on an open
85
+ * proposal, so ended homonyms are not ambiguity.
86
+ */
87
+ async function resolveProposalId(client, input, contractAddr, chainId, opts) {
88
+ const trimmed = input.trim();
89
+ const n = Number(trimmed);
90
+ if (Number.isFinite(n) && Number.isInteger(n) && n >= 0 && String(n) === trimmed) {
91
+ return n;
92
+ }
93
+ const candidates = await fetchRecentProposals(client, contractAddr, chainId);
94
+ if (candidates.length === 0) {
95
+ throw new errors_1.CliError(`Cannot resolve proposal '${input}': no proposals are indexed for this voting contract yet.`, exit_codes_1.EXIT.USAGE, 'Check the org/--type, or pass the numeric proposal ID (pop vote list).');
96
+ }
97
+ let matches = (0, similarity_1.bestMatches)(trimmed, candidates, c => c.title, { threshold: 0.5, topN: 5 });
98
+ if (matches.length > 1 && opts?.preferActive) {
99
+ const active = matches.filter(m => m.item.status === 'Active');
100
+ if (active.length === 1)
101
+ matches = active;
102
+ }
103
+ if (matches.length === 1) {
104
+ return Number(matches[0].item.proposalId);
105
+ }
106
+ if (matches.length > 1) {
107
+ throw new errors_1.CliError(`Proposal query '${input}' is ambiguous — ${matches.length} proposals match:\n` +
108
+ matches.map(m => ` ${candidateLine(m.item)}`).join('\n'), exit_codes_1.EXIT.USAGE, `Pass the numeric ID instead, e.g. --proposal ${matches[0].item.proposalId}`);
109
+ }
110
+ const recent = candidates.slice(0, 5);
111
+ throw new errors_1.CliError(`No proposal title matches '${input}'. The ${recent.length} most recent proposals are:\n` +
112
+ recent.map(c => ` ${candidateLine(c)}`).join('\n'), exit_codes_1.EXIT.USAGE, 'Pass a numeric proposal ID from pop vote list, or refine the title query.');
113
+ }
114
+ /**
115
+ * Pure port of the `pop vote announce-all` candidate filter
116
+ * (src/commands/vote/announce-all.ts): proposals ready to announce are
117
+ * - status "Ended" (subgraph updated), OR
118
+ * - status "Active" but endTimestamp has passed (subgraph hasn't updated)
119
+ * excluding already-announced ones — EXCEPT the H-05 retry case: a FAILED
120
+ * execution is not "done". Audit H-05 releases the in-flight `executed` lock
121
+ * in the catch branch, so the proposal is re-announceable — but it already
122
+ * has a winningOption (Winner is emitted regardless), so the plain
123
+ * already-announced test would skip it forever and silently.
124
+ *
125
+ * Callers should still per-proposal callStatic-probe announceWinner before
126
+ * broadcasting (the CLI skips would-revert candidates without burning gas).
127
+ *
128
+ * @param nowSeconds unix seconds "now" — pass chain time when available.
129
+ */
130
+ function selectAnnounceCandidates(org, nowSeconds) {
131
+ const toAnnounce = [];
132
+ const isEnded = (p) => p.status === 'Ended' ||
133
+ (p.status === 'Active' && !!p.endTimestamp && parseInt(p.endTimestamp) < nowSeconds);
134
+ for (const p of org.hybridVoting?.proposals ?? []) {
135
+ const alreadyDone = (p.status === 'Executed' || p.winningOption != null) && !p.executionFailed;
136
+ if (alreadyDone)
137
+ continue;
138
+ if (isEnded(p)) {
139
+ toAnnounce.push({
140
+ id: p.proposalId,
141
+ type: 'hybrid',
142
+ title: p.title || `Proposal #${p.proposalId}`,
143
+ retry: Boolean(p.executionFailed),
144
+ });
145
+ }
146
+ }
147
+ for (const p of org.directDemocracyVoting?.ddvProposals ?? []) {
148
+ // Same H-05 retry case as hybrid above.
149
+ const alreadyDone = (p.status === 'Executed' || p.winningOption != null) && !p.executionFailed;
150
+ if (alreadyDone)
151
+ continue;
152
+ if (isEnded(p)) {
153
+ toAnnounce.push({
154
+ id: p.proposalId,
155
+ type: 'dd',
156
+ title: p.title || `DD Proposal #${p.proposalId}`,
157
+ retry: Boolean(p.executionFailed),
158
+ });
159
+ }
160
+ }
161
+ return toAnnounce;
162
+ }
163
+ /** The `pop vote execute` state-gate document (src/commands/vote/execute.ts). */
164
+ exports.GET_PROPOSAL_EXECUTION_STATE = `
165
+ query GetProposal($votingId: String!, $proposalId: String!) {
166
+ proposals(where: { hybridVoting: $votingId, proposalId: $proposalId }, first: 1) {
167
+ proposalId
168
+ status
169
+ winningOption
170
+ wasExecuted
171
+ executionFailed
172
+ isValid
173
+ winnerAnnouncedAt
174
+ }
175
+ }
176
+ `;
177
+ /**
178
+ * Port of the `pop vote execute` subgraph state gate: fetch the one proposal
179
+ * row the finalize decision is made from. Null row means the proposal is not
180
+ * indexed (the CLI treats that as not found).
181
+ */
182
+ async function fetchProposalExecutionState(client, hybridVotingAddress, proposalId, chainId) {
183
+ const result = await client.query(exports.GET_PROPOSAL_EXECUTION_STATE, { votingId: hybridVotingAddress, proposalId: proposalId.toString() }, chainId);
184
+ return result.proposals?.[0] ?? null;
185
+ }
186
+ /**
187
+ * Pure port of the `pop vote execute` decision ladder over the subgraph row
188
+ * (same evaluation order as src/commands/vote/execute.ts). The caller maps
189
+ * 'not-ended' to the CLI's PRECONDITION error and the two "nothing to do"
190
+ * kinds to graceful no-ops.
191
+ */
192
+ function evaluateExecuteGate(proposal) {
193
+ if (proposal.wasExecuted)
194
+ return { kind: 'already-executed' };
195
+ // executionFailed proposals ARE announced — the announce succeeded and the
196
+ // execution reverted. Audit H-05 made that retryable, so it must NOT be
197
+ // treated as already-announced.
198
+ if (proposal.winnerAnnouncedAt && !proposal.executionFailed) {
199
+ return { kind: 'already-announced', isValid: proposal.isValid };
200
+ }
201
+ if (proposal.status !== 'Ended')
202
+ return { kind: 'not-ended', status: proposal.status };
203
+ return { kind: 'ready', retryingFailedExecution: Boolean(proposal.executionFailed) };
204
+ }
205
+ /**
206
+ * The `pop vote results` proposal document (src/commands/vote/results.ts).
207
+ * Two tiers: the #195 attribution/provenance fields, then the pre-#195 field
208
+ * set. A GraphQL document validates as a whole, so without the fallback an
209
+ * endpoint that predates #195 would lose the rankings and voter breakdown
210
+ * this read exists to serve. The legacy tier is spelled out rather than
211
+ * derived so a reformat cannot silently turn it into a copy of the modern one.
212
+ */
213
+ function buildProposalResultsQuery(orgId, proposalId, modern) {
214
+ const proposalCore = 'proposalId title status';
215
+ return `{
216
+ organization(id: "${orgId}") {
217
+ hybridVoting {
218
+ thresholdPct
219
+ quorum
220
+ ${modern ? 'classVersion' : ''}
221
+ proposals(where: {proposalId: ${proposalId}}) {
222
+ ${proposalCore}
223
+ ${modern ? 'proposer proposerUsername creatorUsername' : ''}
224
+ ${modern ? 'classesVersion winnerAnnouncedAt executedAt executedCallsCount' : ''}
225
+ metadata { description optionNames${modern ? ' actionSummaries promotedFrom' : ''} }
226
+ votes { voterUsername optionIndexes optionWeights }
227
+ }
228
+ }
229
+ }
230
+ }`;
231
+ }
232
+ /**
233
+ * Pure port of the `pop vote results` tally/ranking/attribution computation
234
+ * (src/commands/vote/results.ts) over the fetched hybridVoting row.
235
+ *
236
+ * Attribution: subgraph #195. proposer* are the current names; creator* the
237
+ * older aliases populated identically from transaction.from. proposedBy is an
238
+ * IDENTITY or null — never an address: most proposers are Executor or
239
+ * smart-account addresses with no username, and returning the address here
240
+ * would make `proposedBy === 'someone'` silently false for them while looking
241
+ * like a resolved name. The address is always available as proposerAddress.
242
+ */
243
+ function computeProposalResults(hybridVoting) {
244
+ const proposal = hybridVoting.proposals?.[0];
245
+ if (!proposal)
246
+ return null;
247
+ // Two DISTINCT validity parameters — threshold is a % of weighted power,
248
+ // quorum is a raw voter count. Never conflate them.
249
+ const supportThresholdPct = hybridVoting.thresholdPct !== undefined ? Number(hybridVoting.thresholdPct) : undefined;
250
+ const quorumVoterCount = hybridVoting.quorum !== undefined ? Number(hybridVoting.quorum) : undefined;
251
+ const optionNames = proposal.metadata?.optionNames || [];
252
+ const votes = proposal.votes || [];
253
+ // Tally weighted votes per option
254
+ const tallies = new Array(Math.max(optionNames.length, 1)).fill(0);
255
+ for (const v of votes) {
256
+ for (let i = 0; i < (v.optionIndexes || []).length; i++) {
257
+ const idx = parseInt(v.optionIndexes[i]);
258
+ const weight = parseInt(v.optionWeights[i]);
259
+ if (idx < tallies.length)
260
+ tallies[idx] += weight;
261
+ }
262
+ }
263
+ // Rank options
264
+ const ranked = optionNames
265
+ .map((name, i) => ({ rank: 0, option: i, name, score: tallies[i] || 0 }))
266
+ .sort((a, b) => b.score - a.score);
267
+ ranked.forEach((r, i) => { r.rank = i + 1; });
268
+ // Per-voter breakdown
269
+ const voterBreakdown = votes.map((v) => {
270
+ const allocations = {};
271
+ for (let i = 0; i < (v.optionIndexes || []).length; i++) {
272
+ const idx = parseInt(v.optionIndexes[i]);
273
+ const name = optionNames[idx] || `Option ${idx}`;
274
+ allocations[name] = parseInt(v.optionWeights[i]);
275
+ }
276
+ return { voter: v.voterUsername || 'unknown', allocations };
277
+ });
278
+ const proposedBy = proposal.proposerUsername || proposal.creatorUsername || null;
279
+ // A proposal is tallied against the voting-class config in force when it was
280
+ // CREATED, so a live config change mid-flight makes the org-level
281
+ // threshold/quorum shown here stale.
282
+ const classVersionAtCreation = proposal.classesVersion != null ? String(proposal.classesVersion) : null;
283
+ const liveClassVersion = hybridVoting.classVersion != null ? String(hybridVoting.classVersion) : null;
284
+ const classConfigDrifted = Boolean(classVersionAtCreation && liveClassVersion && classVersionAtCreation !== liveClassVersion);
285
+ return {
286
+ proposalId: proposal.proposalId,
287
+ title: proposal.title,
288
+ status: proposal.status,
289
+ proposedBy,
290
+ proposerAddress: proposal.proposer || null,
291
+ classVersionAtCreation,
292
+ liveClassVersion,
293
+ classConfigDrifted,
294
+ winnerAnnouncedAt: proposal.winnerAnnouncedAt ? Number(proposal.winnerAnnouncedAt) : null,
295
+ executedAt: proposal.executedAt ? Number(proposal.executedAt) : null,
296
+ executedCallsCount: proposal.executedCallsCount != null ? Number(proposal.executedCallsCount) : null,
297
+ actionSummaries: proposal.metadata?.actionSummaries || [],
298
+ promotedFrom: proposal.metadata?.promotedFrom || null,
299
+ totalVoters: votes.length,
300
+ supportThresholdPct,
301
+ quorumVoterCount,
302
+ ranking: ranked,
303
+ voters: voterBreakdown,
304
+ winner: ranked[0],
305
+ };
306
+ }
307
+ /**
308
+ * Port of `pop vote results` (src/commands/vote/results.ts): rankings +
309
+ * per-voter breakdown for one proposal. Throws the CLI's not-found error when
310
+ * the proposal is not indexed.
311
+ */
312
+ async function fetchProposalResults(client, orgIdOrName, proposalId, chainId) {
313
+ const orgId = await (0, resolve_1.resolveOrgId)(client, orgIdOrName, chainId);
314
+ const { data } = await client.queryWithFieldFallback([
315
+ { query: buildProposalResultsQuery(orgId, proposalId, true) },
316
+ { query: buildProposalResultsQuery(orgId, proposalId, false) },
317
+ ], { chainId });
318
+ const hybridVoting = data.organization?.hybridVoting;
319
+ const report = hybridVoting ? computeProposalResults(hybridVoting) : null;
320
+ if (!report)
321
+ throw new errors_1.CliError(`Proposal #${proposalId} not found`, exit_codes_1.EXIT.USAGE);
322
+ return report;
323
+ }
324
+ /**
325
+ * Subgraph rows → the exact shape the contract path produces. `asset` is
326
+ * checksummed because ethers returns a checksummed address and the subgraph
327
+ * returns lowercase Bytes; without this the `asset` value would change casing
328
+ * depending on which source answered.
329
+ */
330
+ function normalizeSubgraphClasses(rows) {
331
+ return rows.map((c, i) => ({
332
+ classIndex: i,
333
+ strategy: String(c.strategy),
334
+ slicePct: Number(c.slicePct),
335
+ quadratic: Boolean(c.quadratic),
336
+ minBalance: ethers_1.ethers.BigNumber.from(String(c.minBalance ?? '0')).toString(),
337
+ asset: ethers_1.ethers.utils.getAddress(String(c.asset)),
338
+ hatIds: (c.hatIds ?? []).map(h => String(h)),
339
+ }));
340
+ }
341
+ /**
342
+ * Port of `pop vote classes show`'s subgraph read
343
+ * (fetchClassConfigFromSubgraph, src/commands/vote/classes.ts): the class
344
+ * table + both validity parameters. Returns null whenever the subgraph cannot
345
+ * answer authoritatively (contract not indexed, no rows for the requested
346
+ * version, missing schema fields, network error) so the caller falls back to
347
+ * the contract (getClasses()/getProposalClasses()/thresholdPct()/quorum())
348
+ * instead of showing an empty table.
349
+ */
350
+ async function fetchClassConfig(client, hybridVotingAddress, proposalId, chainId) {
351
+ try {
352
+ const hybridVoting = hybridVotingAddress.toLowerCase();
353
+ // The proposal-snapshot query has a single tier on purpose: a deployment
354
+ // without Proposal.classesVersion cannot identify the frozen snapshot, and
355
+ // guessing the newest version would silently misreport an old proposal.
356
+ const tiers = proposalId === undefined
357
+ ? [
358
+ { query: voting_classes_1.FETCH_VOTING_CLASS_CONFIG, variables: { hybridVoting } },
359
+ { query: voting_classes_1.FETCH_VOTING_CLASS_CONFIG_LEGACY, variables: { hybridVoting } },
360
+ ]
361
+ : [
362
+ { query: voting_classes_1.FETCH_PROPOSAL_VOTING_CLASSES, variables: { hybridVoting, proposalId: String(proposalId) } },
363
+ ];
364
+ const { data } = await client.queryWithFieldFallback(tiers, { chainId });
365
+ const contract = data?.hybridVotingContract;
366
+ if (!contract)
367
+ return null;
368
+ const version = proposalId === undefined
369
+ ? contract.classVersion
370
+ : contract.proposals?.[0]?.classesVersion;
371
+ // For a proposal we must know its frozen version — no version, no answer.
372
+ if (proposalId !== undefined && (version === null || version === undefined))
373
+ return null;
374
+ const rows = (0, voting_classes_1.selectClassSnapshot)(contract.votingClasses, version);
375
+ if (rows.length === 0)
376
+ return null;
377
+ if (contract.thresholdPct === null || contract.thresholdPct === undefined)
378
+ return null;
379
+ if (contract.quorum === null || contract.quorum === undefined)
380
+ return null;
381
+ return {
382
+ classes: normalizeSubgraphClasses(rows),
383
+ supportThresholdPct: Number(contract.thresholdPct),
384
+ quorumVoterCount: Number(contract.quorum),
385
+ };
386
+ }
387
+ catch {
388
+ // Any subgraph problem (unknown field on an old deployment, lag, HTTP) —
389
+ // the contract is authoritative anyway, so just fall back.
390
+ return null;
391
+ }
392
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * ZK Email reads — module resolution + state for `pop zkemail status` /
3
+ * `pop zkemail claims`. Port of src/commands/zkemail/helpers.ts (client- and
4
+ * provider-injected) plus the claim-history read from
5
+ * src/commands/zkemail/claims.ts.
6
+ *
7
+ * The ZkEmailInvites module is OPTIONAL per org — `organization.zkEmailInvites`
8
+ * is null for orgs deployed without it — and a deployed module with
9
+ * `merkleRoot == 0` is DORMANT (every claim reverts until governance publishes
10
+ * an allowlist). Both states get a named error rather than a confusing revert.
11
+ */
12
+ import { ethers } from 'ethers';
13
+ import type { GraphClient } from '../graph/client';
14
+ import type { EnvSource } from '../env';
15
+ export interface ZkEmailModuleState {
16
+ orgId: string;
17
+ orgName: string | null;
18
+ address: string;
19
+ /** On-chain active merkle root; ethers.constants.HashZero when dormant. */
20
+ merkleRoot: string;
21
+ /** On-chain allowlist CID digest (bytes32). */
22
+ allowlistCidDigest: string;
23
+ /** allowlistCidDigest decoded back to a CIDv0, or null when unset. */
24
+ allowlistCid: string | null;
25
+ /** True when no allowlist is committed — every claim reverts with AllowlistNotActive. */
26
+ dormant: boolean;
27
+ executor?: string;
28
+ domainVerifier?: string;
29
+ emailVerifier?: string;
30
+ dkimRegistry?: string;
31
+ accountRegistry?: string;
32
+ universalFactory?: string;
33
+ /** True when the wiring came from the subgraph rather than eth_calls. */
34
+ indexedWiring: boolean;
35
+ }
36
+ export interface SubgraphAllowlistEntry {
37
+ index: number;
38
+ entryType: string;
39
+ identifier: string | null;
40
+ identifierHash: string | null;
41
+ hatIds: string[];
42
+ roleIndexes: number[];
43
+ }
44
+ /**
45
+ * Resolve the org's ZkEmailInvites proxy from the subgraph. Port of
46
+ * resolveZkEmailModule in src/commands/zkemail/helpers.ts.
47
+ * Throws a named error when the org has no module rather than letting a null
48
+ * address turn into an opaque call failure.
49
+ */
50
+ export declare function resolveZkEmailModule(client: GraphClient, orgArg: string | undefined, chainId?: number): Promise<{
51
+ orgId: string;
52
+ orgName: string | null;
53
+ address: string;
54
+ subgraph: any;
55
+ indexedWiring: boolean;
56
+ }>;
57
+ /**
58
+ * Read the module's state, preferring the subgraph. Port of readModuleState in
59
+ * src/commands/zkemail/helpers.ts (the read behind `pop zkemail status`).
60
+ *
61
+ * `activeRoot` and `activeAllowlistCid` are indexed on the ZkEmailInvites entity, so the
62
+ * allowlist half needs no RPC at all — `resolveZkEmailModule` has already fetched it. Only the
63
+ * wiring addresses (executor + the four verifier/registry pointers) are unindexed pre-#197, so
64
+ * they are fetched on demand: pass `withWiring` when the caller actually renders them.
65
+ *
66
+ * Pass an existing `provider` to avoid opening a second connection; when omitted and an RPC
67
+ * read is needed, a JsonRpcProvider is built from resolveNetworkConfig(chainId, env).
68
+ */
69
+ export declare function readModuleState(address: string, orgId: string, orgName: string | null, chainId?: number, provider?: ethers.providers.Provider, opts?: {
70
+ subgraph?: any;
71
+ withWiring?: boolean;
72
+ withDkimRegistry?: boolean;
73
+ env?: EnvSource;
74
+ }): Promise<ZkEmailModuleState>;
75
+ /**
76
+ * The indexed active root for an org's module, identity-guarded — the display
77
+ * read used by `pop zkemail propose-allowlist`
78
+ * (src/commands/zkemail/propose-allowlist.ts).
79
+ *
80
+ * Returns:
81
+ * - a root string (HashZero for a dormant module whose row EXISTS), or
82
+ * - null when the row is missing / the indexed module id does not match
83
+ * `moduleAddress` (a stale org->module pointer must not describe a
84
+ * different contract than the one a proposal targets). Callers fall back
85
+ * to merkleRoot() on-chain for the null case.
86
+ */
87
+ export declare function fetchIndexedActiveRoot(client: GraphClient, orgId: string, moduleAddress: string, chainId?: number): Promise<string | null>;
88
+ export interface ZkEmailClaimRow {
89
+ id: string;
90
+ kind: string;
91
+ claimer: string;
92
+ claimerUsername: string | null;
93
+ /** The allowlist leaf the claim used — may belong to a SUPERSEDED allowlist. */
94
+ identifierHash: string;
95
+ hatIds: string[];
96
+ /** Absent on the passkey onboarding path, which does not emit it (majority of live Gnosis claims). */
97
+ nullifier: string | null;
98
+ registeredUsername: string | null;
99
+ claimedAt: string;
100
+ claimedAtBlock: string;
101
+ transactionHash: string;
102
+ }
103
+ /**
104
+ * Claim history for a module, most recent first — the read behind
105
+ * `pop zkemail claims` (src/commands/zkemail/claims.ts). Indexed since
106
+ * subgraph-pop #197; on a chain still serving the legacy tier the claim events
107
+ * were seen but every parameter was discarded, so there is nothing to read —
108
+ * callers should gate on `indexedWiring` from resolveZkEmailModule like the
109
+ * CLI does.
110
+ */
111
+ export declare function fetchZkEmailClaims(client: GraphClient, moduleAddress: string, chainId?: number, limit?: number): Promise<ZkEmailClaimRow[]>;
112
+ /** Map hatId -> role name so allowlists render as "gmail.com -> Member". */
113
+ export declare function fetchRoleNames(client: GraphClient, orgId: string, chainId?: number): Promise<Map<string, string>>;
114
+ /** Render a hat ID as "Member (0x2a)" when the name is known, else the raw id. */
115
+ export declare function labelHat(hatId: string, names: Map<string, string>): string;
116
+ /** Normalize the subgraph's allowlist entries into a stable shape. */
117
+ export declare function normalizeEntries(allowlist: any): SubgraphAllowlistEntry[];