@poa-box/core 0.1.0 → 1.0.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 (95) hide show
  1. package/README.md +2 -0
  2. package/dist/abis/AuthorityRouter.d.ts +448 -0
  3. package/dist/abis/AuthorityRouter.js +584 -0
  4. package/dist/abis/CutoverVerifier.d.ts +194 -0
  5. package/dist/abis/CutoverVerifier.js +259 -0
  6. package/dist/abis/DirectDemocracyVotingNew.d.ts +168 -30
  7. package/dist/abis/DirectDemocracyVotingNew.js +214 -35
  8. package/dist/abis/EducationHubNew.d.ts +32 -75
  9. package/dist/abis/EducationHubNew.js +38 -94
  10. package/dist/abis/Executor.d.ts +56 -86
  11. package/dist/abis/Executor.js +68 -104
  12. package/dist/abis/HybridVotingNew.d.ts +279 -35
  13. package/dist/abis/HybridVotingNew.js +357 -40
  14. package/dist/abis/{EligibilityModuleNew.d.ts → MembershipAuthority.d.ts} +1437 -718
  15. package/dist/abis/{EligibilityModuleNew.js → MembershipAuthority.js} +1821 -899
  16. package/dist/abis/OrgDeployerNew.d.ts +193 -127
  17. package/dist/abis/OrgDeployerNew.js +244 -160
  18. package/dist/abis/OrgRegistry.d.ts +40 -0
  19. package/dist/abis/OrgRegistry.js +51 -0
  20. package/dist/abis/ParticipationToken.d.ts +32 -75
  21. package/dist/abis/ParticipationToken.js +38 -94
  22. package/dist/abis/PasskeyAccount.d.ts +28 -0
  23. package/dist/abis/PasskeyAccount.js +35 -0
  24. package/dist/abis/PaymasterHub.d.ts +527 -47
  25. package/dist/abis/PaymasterHub.js +750 -135
  26. package/dist/abis/QuickJoinNew.d.ts +59 -170
  27. package/dist/abis/QuickJoinNew.js +71 -215
  28. package/dist/abis/TaskManagerNew.d.ts +27 -44
  29. package/dist/abis/TaskManagerNew.js +31 -52
  30. package/dist/abis/UniversalAccountRegistry.d.ts +24 -0
  31. package/dist/abis/UniversalAccountRegistry.js +30 -0
  32. package/dist/abis/ZkEmailInvites.d.ts +44 -0
  33. package/dist/abis/ZkEmailInvites.js +55 -0
  34. package/dist/abis/index.d.ts +6 -4
  35. package/dist/abis/index.js +10 -7
  36. package/dist/chains.js +3 -3
  37. package/dist/error-catalog.js +125 -7
  38. package/dist/graph/documents/activity.d.ts +3 -1
  39. package/dist/graph/documents/activity.js +19 -14
  40. package/dist/graph/documents/org.d.ts +4 -4
  41. package/dist/graph/documents/org.js +10 -4
  42. package/dist/graph/documents/task.d.ts +10 -1
  43. package/dist/graph/documents/task.js +157 -3
  44. package/dist/graph/documents/user.d.ts +5 -30
  45. package/dist/graph/documents/user.js +2 -42
  46. package/dist/graph/documents/voting-classes.d.ts +23 -2
  47. package/dist/graph/documents/voting-classes.js +119 -10
  48. package/dist/metadata/index.d.ts +0 -1
  49. package/dist/metadata/index.js +1 -2
  50. package/dist/perms.d.ts +0 -15
  51. package/dist/perms.js +1 -21
  52. package/dist/preflight.d.ts +2 -2
  53. package/dist/preflight.js +14 -22
  54. package/dist/reads/authority.d.ts +33 -0
  55. package/dist/reads/authority.js +196 -0
  56. package/dist/reads/index.d.ts +1 -1
  57. package/dist/reads/index.js +2 -2
  58. package/dist/reads/org.d.ts +20 -20
  59. package/dist/reads/org.js +94 -28
  60. package/dist/reads/project.js +3 -3
  61. package/dist/reads/resolve.d.ts +1 -1
  62. package/dist/reads/resolve.js +12 -3
  63. package/dist/reads/task.d.ts +44 -22
  64. package/dist/reads/task.js +126 -67
  65. package/dist/reads/user.d.ts +8 -43
  66. package/dist/reads/user.js +27 -44
  67. package/dist/reads/vote.d.ts +26 -2
  68. package/dist/reads/vote.js +104 -23
  69. package/dist/tx/authority.d.ts +33 -0
  70. package/dist/tx/authority.js +62 -0
  71. package/dist/tx/governance.d.ts +6 -0
  72. package/dist/tx/governance.js +20 -2
  73. package/dist/tx/index.d.ts +1 -1
  74. package/dist/tx/index.js +2 -2
  75. package/dist/tx/org.d.ts +23 -18
  76. package/dist/tx/org.js +203 -67
  77. package/dist/tx/project.d.ts +3 -2
  78. package/dist/tx/project.js +28 -10
  79. package/dist/tx/task.d.ts +13 -24
  80. package/dist/tx/task.js +45 -85
  81. package/dist/tx/user.d.ts +2 -40
  82. package/dist/tx/user.js +2 -76
  83. package/dist/tx/vote.d.ts +5 -0
  84. package/dist/tx/vote.js +54 -43
  85. package/dist/version.d.ts +0 -5
  86. package/dist/version.js +1 -13
  87. package/package.json +1 -1
  88. package/dist/abis/ToggleModule.d.ts +0 -193
  89. package/dist/abis/ToggleModule.js +0 -255
  90. package/dist/metadata/role.d.ts +0 -38
  91. package/dist/metadata/role.js +0 -35
  92. package/dist/reads/eligibility.d.ts +0 -377
  93. package/dist/reads/eligibility.js +0 -687
  94. package/dist/tx/eligibility.d.ts +0 -511
  95. package/dist/tx/eligibility.js +0 -818
@@ -1,21 +1,4 @@
1
- /**
2
- * User & account reads — typed wrappers over graph/documents/user.
3
- *
4
- * Serves the read paths of `pop user whoami` (src/commands/user/whoami.ts),
5
- * `pop user profile` (src/commands/user/profile.ts), and the pre-write reads
6
- * of `pop user join` / `pop user claim-hats` / `pop user update-profile`.
7
- *
8
- * Usernames and profile metadata are HOME-CHAIN account-registry state
9
- * (that's where `pop user register` writes and `pop user profile` reads), so
10
- * the account-scoped helpers default to HOME_CHAIN_ID rather than the
11
- * client's default chain. An explicit chainId always wins.
12
- *
13
- * A null row from any of these means NOT-INDEXED, not nonexistent — the CLI
14
- * keeps live RPC fallbacks (getUsername, balanceOf, Hats.balanceOf) for every
15
- * decision where that difference matters, and `isAccountAuthoritative`
16
- * (re-exported below) is the guard that says when an indexed account may
17
- * stand in for a live registry read.
18
- */
1
+ /** Account/token history is retained; current organization membership comes from MembershipAuthority. */
19
2
  import type { GraphClient } from '../graph/client';
20
3
  import { IndexedAccount, IndexedQuickJoin } from '../graph/documents/user';
21
4
  export { isAccountAuthoritative } from '../graph/documents/user';
@@ -73,13 +56,7 @@ export declare function getQuickJoinAccount(client: GraphClient, quickJoinAddres
73
56
  quickJoinContract: IndexedQuickJoin | null;
74
57
  account: IndexedAccount | null;
75
58
  }>;
76
- /**
77
- * Just the QuickJoin module pointers (accountRegistry / hatsContract /
78
- * memberHatIds) — the address read of `pop user claim-hats`
79
- * (src/commands/user/claim-hats.ts). The pointers are static deploy-time
80
- * values, byte-verified against the live eth_calls on Gnosis and Arbitrum;
81
- * the claim pre-flight probes themselves stay on-chain in the CLI.
82
- */
59
+ /** Read the indexed account registry pointer for QuickJoin. */
83
60
  export declare function getQuickJoinModules(client: GraphClient, quickJoinAddress: string, chainId?: number): Promise<IndexedQuickJoin | null>;
84
61
  /** Account row served by FETCH_REGISTRY_AND_ACCOUNT (profile-merge shape). */
85
62
  export interface RegistryAccount {
@@ -110,6 +87,8 @@ export interface OrgUser {
110
87
  id: string;
111
88
  address: string;
112
89
  participationTokenBalance: string | null;
90
+ /** False when authority membership exists but its historical User is not indexed. */
91
+ historyIndexed?: boolean;
113
92
  membershipStatus: string | null;
114
93
  currentHatIds: string[] | null;
115
94
  joinMethod: string | null;
@@ -179,10 +158,8 @@ export interface WhoamiOrgData {
179
158
  name: string | null;
180
159
  roles: Array<{
181
160
  hatId: string;
161
+ subjectId: string;
182
162
  name: string | null;
183
- hat: {
184
- active: boolean;
185
- } | null;
186
163
  }>;
187
164
  } | null;
188
165
  quickJoinContract: IndexedQuickJoin | null;
@@ -196,27 +173,15 @@ export interface WhoamiOrgData {
196
173
  membershipStatus: string | null;
197
174
  participationTokenBalance: string | null;
198
175
  currentHatIds: string[] | null;
176
+ /** False when current membership is indexed before its historical User row. */
177
+ historyIndexed?: boolean;
199
178
  } | null;
200
179
  /** Pending requests by this user — the CLI reports `tokenRequests.length`. */
201
180
  tokenRequests: Array<{
202
181
  id: string;
203
182
  }>;
204
183
  }
205
- /**
206
- * One-round-trip org snapshot for `pop user whoami`
207
- * (src/commands/user/whoami.ts): org name + role hats (with the Hats toggle
208
- * flag), QuickJoin pointers, the caller's account/PT balance/org-user row,
209
- * and their pending token requests. Variables are composed exactly as the
210
- * CLI does — AddressZero placeholders keep the document valid when a module
211
- * is missing (those selections then simply return null).
212
- *
213
- * Interpretation stays with the caller (the CLI keeps RPC fallbacks for
214
- * username/PT/membership): gate `account.username` with
215
- * isAccountAuthoritative(account, quickJoinContract?.accountRegistry), AND
216
- * membership derived from currentHatIds with the role's `hat.active` flag —
217
- * a toggled-off hat still appears in currentHatIds because Hats does not
218
- * burn the token.
219
- */
184
+ /** Account/token history merged with current authority roles and membership. */
220
185
  export declare function getWhoamiOrgData(client: GraphClient, params: {
221
186
  /** Org hex ID (as returned by resolveOrgId / OrgModules.orgId). */
222
187
  orgId: string;
@@ -1,22 +1,5 @@
1
1
  "use strict";
2
- /**
3
- * User & account reads — typed wrappers over graph/documents/user.
4
- *
5
- * Serves the read paths of `pop user whoami` (src/commands/user/whoami.ts),
6
- * `pop user profile` (src/commands/user/profile.ts), and the pre-write reads
7
- * of `pop user join` / `pop user claim-hats` / `pop user update-profile`.
8
- *
9
- * Usernames and profile metadata are HOME-CHAIN account-registry state
10
- * (that's where `pop user register` writes and `pop user profile` reads), so
11
- * the account-scoped helpers default to HOME_CHAIN_ID rather than the
12
- * client's default chain. An explicit chainId always wins.
13
- *
14
- * A null row from any of these means NOT-INDEXED, not nonexistent — the CLI
15
- * keeps live RPC fallbacks (getUsername, balanceOf, Hats.balanceOf) for every
16
- * decision where that difference matters, and `isAccountAuthoritative`
17
- * (re-exported below) is the guard that says when an indexed account may
18
- * stand in for a live registry read.
19
- */
2
+ /** Account/token history is retained; current organization membership comes from MembershipAuthority. */
20
3
  Object.defineProperty(exports, "__esModule", { value: true });
21
4
  exports.isAccountAuthoritative = void 0;
22
5
  exports.getAccountByUsername = getAccountByUsername;
@@ -31,6 +14,7 @@ const ethers_1 = require("ethers");
31
14
  const user_1 = require("../graph/documents/user");
32
15
  const chains_1 = require("../chains");
33
16
  const resolve_1 = require("./resolve");
17
+ const authority_1 = require("./authority");
34
18
  // Pure selection helpers live in the documents module — re-exported here so
35
19
  // read-layer consumers get the guard next to the reads it guards.
36
20
  var user_2 = require("../graph/documents/user");
@@ -80,13 +64,7 @@ async function getQuickJoinAccount(client, quickJoinAddress, accountAddress, cha
80
64
  account: result?.account ?? null,
81
65
  };
82
66
  }
83
- /**
84
- * Just the QuickJoin module pointers (accountRegistry / hatsContract /
85
- * memberHatIds) — the address read of `pop user claim-hats`
86
- * (src/commands/user/claim-hats.ts). The pointers are static deploy-time
87
- * values, byte-verified against the live eth_calls on Gnosis and Arbitrum;
88
- * the claim pre-flight probes themselves stay on-chain in the CLI.
89
- */
67
+ /** Read the indexed account registry pointer for QuickJoin. */
90
68
  async function getQuickJoinModules(client, quickJoinAddress, chainId) {
91
69
  const result = await client.query(user_1.FETCH_QUICKJOIN_MODULES, { quickJoinAddress: quickJoinAddress.toLowerCase() }, chainId);
92
70
  return result?.quickJoinContract ?? null;
@@ -117,39 +95,44 @@ async function getOrgUserProfile(client, orgIdOrName, address, chainId) {
117
95
  const orgId = await (0, resolve_1.resolveOrgId)(client, orgIdOrName, chainId);
118
96
  const orgUserID = `${orgId.toLowerCase()}-${address.toLowerCase()}`;
119
97
  const { data, tierIndex } = await client.queryWithFieldFallback((0, user_1.userDataTiers)(orgUserID, address.toLowerCase()), { chainId });
98
+ const users = await (0, authority_1.readAuthorityUsers)(client, orgId, data.user ? [data.user] : [], chainId);
99
+ const currentUser = users.find(user => user.address.toLowerCase() === address.toLowerCase());
120
100
  return {
121
- user: data.user ?? null,
101
+ user: currentUser ?? null,
122
102
  account: data.account ?? null,
123
103
  hasChurnCounters: tierIndex === 0,
124
104
  };
125
105
  }
126
- /**
127
- * One-round-trip org snapshot for `pop user whoami`
128
- * (src/commands/user/whoami.ts): org name + role hats (with the Hats toggle
129
- * flag), QuickJoin pointers, the caller's account/PT balance/org-user row,
130
- * and their pending token requests. Variables are composed exactly as the
131
- * CLI does — AddressZero placeholders keep the document valid when a module
132
- * is missing (those selections then simply return null).
133
- *
134
- * Interpretation stays with the caller (the CLI keeps RPC fallbacks for
135
- * username/PT/membership): gate `account.username` with
136
- * isAccountAuthoritative(account, quickJoinContract?.accountRegistry), AND
137
- * membership derived from currentHatIds with the role's `hat.active` flag —
138
- * a toggled-off hat still appears in currentHatIds because Hats does not
139
- * burn the token.
140
- */
106
+ /** Account/token history merged with current authority roles and membership. */
141
107
  async function getWhoamiOrgData(client, params, chainId) {
108
+ const orgId = await (0, resolve_1.resolveOrgId)(client, params.orgId, chainId);
142
109
  const address = params.address.toLowerCase();
143
110
  const tokenAddress = params.participationTokenAddress || ethers_1.ethers.constants.AddressZero;
144
111
  const quickJoinAddress = (params.quickJoinAddress || ethers_1.ethers.constants.AddressZero).toLowerCase();
145
112
  const result = await client.query(user_1.FETCH_WHOAMI_ORG_DATA, {
146
- orgId: params.orgId,
147
- orgUserID: `${params.orgId}-${address}`,
113
+ orgId,
114
+ orgUserID: `${orgId}-${address}`,
148
115
  tokenAddress,
149
116
  userAddress: address,
150
117
  quickJoinAddress,
151
118
  accountID: address,
152
119
  tokenBalanceID: `${tokenAddress.toLowerCase()}-${address}`,
153
120
  }, chainId);
154
- return result ?? null;
121
+ if (!result)
122
+ return null;
123
+ const [subjects, memberships] = await Promise.all([
124
+ (0, authority_1.readAuthorityRows)(client, orgId, authority_1.FETCH_AUTHORITY_SUBJECTS, 'subjects', chainId),
125
+ (0, authority_1.readAuthorityMemberships)(client, orgId, chainId),
126
+ ]);
127
+ const current = memberships.filter(row => row.user.toLowerCase() === address && row.isMember && row.subject.kind === 'Role');
128
+ if (result.organization)
129
+ result.organization.roles = subjects.filter(subject => subject.kind === 'Role').map(subject => ({ hatId: subject.subjectId, subjectId: subject.subjectId, name: subject.name }));
130
+ if (result.user || current.length)
131
+ result.user = {
132
+ id: `${orgId}-${address}`, participationTokenBalance: null,
133
+ ...result.user, historyIndexed: result.user != null,
134
+ membershipStatus: current.length ? 'Active' : 'Inactive',
135
+ currentHatIds: [...new Set(current.map(row => row.subject.subjectId))],
136
+ };
137
+ return result;
155
138
  }
@@ -11,6 +11,7 @@ import type { GraphClient } from '../graph/client';
11
11
  import { SubgraphVotingClass } from '../graph/documents/voting-classes';
12
12
  /** One ballot as indexed by the subgraph (FETCH_VOTING_DATA field set). */
13
13
  export interface SubgraphProposalVote {
14
+ id?: string;
14
15
  voter: string;
15
16
  voterUsername?: string | null;
16
17
  optionIndexes: string[];
@@ -216,9 +217,19 @@ export interface ProposalResults {
216
217
  totalVoters: number;
217
218
  supportThresholdPct?: number;
218
219
  quorumVoterCount?: number;
220
+ /** Compatibility quorumVoterCount is the live global config, not a restricted poll's override. */
221
+ quorumSource: 'current-global-config';
222
+ /** Null when the subgraph cannot identify the proposal's per-poll override. */
223
+ effectiveQuorumVoterCount: number | null;
219
224
  ranking: ProposalResultsRankEntry[];
220
225
  voters: ProposalResultsVoter[];
221
- winner?: ProposalResultsRankEntry;
226
+ /** Raw ballot allocation leader; this is not the class-weighted on-chain winner. */
227
+ allocationLeader: ProposalResultsRankEntry | null;
228
+ rankingBasis: 'sum-of-ballot-allocations';
229
+ winnerSource: 'announced' | 'unannounced' | 'invalid';
230
+ announcedWinningOption: number | null;
231
+ announcedValid: boolean | null;
232
+ winner: ProposalResultsRankEntry | null;
222
233
  }
223
234
  /**
224
235
  * The `pop vote results` proposal document (src/commands/vote/results.ts).
@@ -228,7 +239,7 @@ export interface ProposalResults {
228
239
  * this read exists to serve. The legacy tier is spelled out rather than
229
240
  * derived so a reformat cannot silently turn it into a copy of the modern one.
230
241
  */
231
- export declare function buildProposalResultsQuery(orgId: string, proposalId: number, modern: boolean): string;
242
+ export declare function buildProposalResultsQuery(orgId: string, proposalId: number, modern: boolean, after?: string): string;
232
243
  /**
233
244
  * Pure port of the `pop vote results` tally/ranking/attribution computation
234
245
  * (src/commands/vote/results.ts) over the fetched hybridVoting row.
@@ -262,6 +273,15 @@ export declare function computeProposalResults(hybridVoting: {
262
273
  * the proposal is not indexed.
263
274
  */
264
275
  export declare function fetchProposalResults(client: GraphClient, orgIdOrName: string, proposalId: number, chainId?: number): Promise<ProposalResults>;
276
+ /** Shared CLI/SDK read after organization resolution; cursor-pagination prevents truncated tallies. */
277
+ export declare function fetchProposalResultsByOrgId(client: Pick<GraphClient, 'queryWithFieldFallback'>, orgId: string, proposalId: number, chainId?: number): Promise<ProposalResults>;
278
+ /** Full indexed ballot history plus exact class-emission pointers, with legacy schema fallback. */
279
+ export declare function fetchProposalVoteAnalysis(client: Pick<GraphClient, 'queryWithFieldFallback'>, hybridVotingAddress: string, proposalId: number, chainId?: number): Promise<any>;
280
+ /** Reproduce HybridVotingCore vote rounding and VotingMath.pickWinnerNSlices scores. */
281
+ export declare function computeClassWeightedScores(ballots: Array<{
282
+ weights: number[];
283
+ classRawPowers: bigint[];
284
+ }>, slices: number[], numOptions: number): number[];
265
285
  /** Normalized class row — identical shape from the subgraph and the contract. */
266
286
  export interface NormalizedClass {
267
287
  classIndex: number;
@@ -271,11 +291,15 @@ export interface NormalizedClass {
271
291
  minBalance: string;
272
292
  asset: string;
273
293
  hatIds: string[];
294
+ /** On-chain stable authority binding is not indexed; hatIds are only its fallback list. */
295
+ subjectId: string | null;
296
+ subjectBindingKnown: boolean;
274
297
  }
275
298
  export interface ClassConfigSnapshot {
276
299
  classes: NormalizedClass[];
277
300
  supportThresholdPct: number;
278
301
  quorumVoterCount: number;
302
+ effectiveQuorumVoterCount: number | null;
279
303
  }
280
304
  /**
281
305
  * Subgraph rows → the exact shape the contract path produces. `asset` is
@@ -19,6 +19,9 @@ exports.evaluateExecuteGate = evaluateExecuteGate;
19
19
  exports.buildProposalResultsQuery = buildProposalResultsQuery;
20
20
  exports.computeProposalResults = computeProposalResults;
21
21
  exports.fetchProposalResults = fetchProposalResults;
22
+ exports.fetchProposalResultsByOrgId = fetchProposalResultsByOrgId;
23
+ exports.fetchProposalVoteAnalysis = fetchProposalVoteAnalysis;
24
+ exports.computeClassWeightedScores = computeClassWeightedScores;
22
25
  exports.normalizeSubgraphClasses = normalizeSubgraphClasses;
23
26
  exports.fetchClassConfig = fetchClassConfig;
24
27
  const ethers_1 = require("ethers");
@@ -210,8 +213,8 @@ function evaluateExecuteGate(proposal) {
210
213
  * this read exists to serve. The legacy tier is spelled out rather than
211
214
  * derived so a reformat cannot silently turn it into a copy of the modern one.
212
215
  */
213
- function buildProposalResultsQuery(orgId, proposalId, modern) {
214
- const proposalCore = 'proposalId title status';
216
+ function buildProposalResultsQuery(orgId, proposalId, modern, after = '') {
217
+ const proposalCore = 'proposalId title status numOptions isHatRestricted winningOption isValid';
215
218
  return `{
216
219
  organization(id: "${orgId}") {
217
220
  hybridVoting {
@@ -223,7 +226,9 @@ function buildProposalResultsQuery(orgId, proposalId, modern) {
223
226
  ${modern ? 'proposer proposerUsername creatorUsername' : ''}
224
227
  ${modern ? 'classesVersion winnerAnnouncedAt executedAt executedCallsCount' : ''}
225
228
  metadata { description optionNames${modern ? ' actionSummaries promotedFrom' : ''} }
226
- votes { voterUsername optionIndexes optionWeights }
229
+ votes(first: 1000, orderBy: id, orderDirection: asc, where: { id_gt: ${JSON.stringify(after)} }) {
230
+ id voter voterUsername optionIndexes optionWeights
231
+ }
227
232
  }
228
233
  }
229
234
  }
@@ -246,17 +251,19 @@ function computeProposalResults(hybridVoting) {
246
251
  return null;
247
252
  // Two DISTINCT validity parameters — threshold is a % of weighted power,
248
253
  // 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 || [];
254
+ const supportThresholdPct = hybridVoting.thresholdPct != null ? Number(hybridVoting.thresholdPct) : undefined;
255
+ const quorumVoterCount = hybridVoting.quorum != null ? Number(hybridVoting.quorum) : undefined;
256
+ const namedOptions = proposal.metadata?.optionNames ?? [];
257
+ const optionCount = Number(proposal.numOptions ?? namedOptions.length);
258
+ const optionNames = Array.from({ length: optionCount }, (_, i) => namedOptions[i] || `Option ${i}`);
252
259
  const votes = proposal.votes || [];
253
- // Tally weighted votes per option
260
+ // Compatibility ranking sums ballot percentages; it does not apply class power.
254
261
  const tallies = new Array(Math.max(optionNames.length, 1)).fill(0);
255
262
  for (const v of votes) {
256
263
  for (let i = 0; i < (v.optionIndexes || []).length; i++) {
257
264
  const idx = parseInt(v.optionIndexes[i]);
258
265
  const weight = parseInt(v.optionWeights[i]);
259
- if (idx < tallies.length)
266
+ if (idx >= 0 && idx < tallies.length)
260
267
  tallies[idx] += weight;
261
268
  }
262
269
  }
@@ -273,12 +280,10 @@ function computeProposalResults(hybridVoting) {
273
280
  const name = optionNames[idx] || `Option ${idx}`;
274
281
  allocations[name] = parseInt(v.optionWeights[i]);
275
282
  }
276
- return { voter: v.voterUsername || 'unknown', allocations };
283
+ return { voter: v.voterUsername || v.voter || 'unknown', allocations };
277
284
  });
278
285
  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.
286
+ // Class membership and slices are snapshotted; threshold and global quorum remain live.
282
287
  const classVersionAtCreation = proposal.classesVersion != null ? String(proposal.classesVersion) : null;
283
288
  const liveClassVersion = hybridVoting.classVersion != null ? String(hybridVoting.classVersion) : null;
284
289
  const classConfigDrifted = Boolean(classVersionAtCreation && liveClassVersion && classVersionAtCreation !== liveClassVersion);
@@ -299,9 +304,17 @@ function computeProposalResults(hybridVoting) {
299
304
  totalVoters: votes.length,
300
305
  supportThresholdPct,
301
306
  quorumVoterCount,
307
+ quorumSource: 'current-global-config',
308
+ effectiveQuorumVoterCount: proposal.isHatRestricted === false ? quorumVoterCount ?? null : null,
302
309
  ranking: ranked,
303
310
  voters: voterBreakdown,
304
- winner: ranked[0],
311
+ rankingBasis: 'sum-of-ballot-allocations',
312
+ allocationLeader: ranked[0] ?? null,
313
+ announcedWinningOption: proposal.winningOption != null ? Number(proposal.winningOption) : null,
314
+ announcedValid: proposal.isValid ?? null,
315
+ winnerSource: proposal.winningOption == null ? 'unannounced' : proposal.isValid === true ? 'announced' : 'invalid',
316
+ winner: proposal.winningOption != null && proposal.isValid === true
317
+ ? ranked.find(r => r.option === Number(proposal.winningOption)) ?? null : null,
305
318
  };
306
319
  }
307
320
  /**
@@ -311,15 +324,73 @@ function computeProposalResults(hybridVoting) {
311
324
  */
312
325
  async function fetchProposalResults(client, orgIdOrName, proposalId, chainId) {
313
326
  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;
327
+ return fetchProposalResultsByOrgId(client, orgId, proposalId, chainId);
328
+ }
329
+ /** Shared CLI/SDK read after organization resolution; cursor-pagination prevents truncated tallies. */
330
+ async function fetchProposalResultsByOrgId(client, orgId, proposalId, chainId) {
331
+ let after = '';
332
+ let hybridVoting;
333
+ const votes = [];
334
+ for (;;) {
335
+ const { data } = await client.queryWithFieldFallback([
336
+ { query: buildProposalResultsQuery(orgId, proposalId, true, after) },
337
+ { query: buildProposalResultsQuery(orgId, proposalId, false, after) },
338
+ ], { chainId });
339
+ const row = data.organization?.hybridVoting;
340
+ if (!row?.proposals?.[0])
341
+ throw new errors_1.CliError(`Proposal #${proposalId} not found`, exit_codes_1.EXIT.USAGE);
342
+ hybridVoting ??= row;
343
+ const page = row.proposals[0].votes ?? [];
344
+ votes.push(...page);
345
+ if (page.length < 1000)
346
+ break;
347
+ const next = page[page.length - 1].id;
348
+ if (!next || next <= after)
349
+ throw new errors_1.CliError('Cannot paginate proposal votes without an advancing vote ID.', exit_codes_1.EXIT.PRECONDITION);
350
+ after = next;
351
+ }
352
+ hybridVoting.proposals[0].votes = votes;
353
+ return computeProposalResults(hybridVoting);
354
+ }
355
+ /** Full indexed ballot history plus exact class-emission pointers, with legacy schema fallback. */
356
+ async function fetchProposalVoteAnalysis(client, hybridVotingAddress, proposalId, chainId) {
357
+ let after = '';
358
+ let first;
359
+ const votes = [];
360
+ for (;;) {
361
+ const variables = { hybridVoting: hybridVotingAddress.toLowerCase(), proposalId: String(proposalId), after };
362
+ const { data } = await client.queryWithFieldFallback([
363
+ { query: voting_classes_1.FETCH_PROPOSAL_VOTE_ANALYSIS_EXACT, variables },
364
+ { query: voting_classes_1.FETCH_PROPOSAL_VOTE_ANALYSIS, variables },
365
+ ], { chainId });
366
+ first ??= data;
367
+ const page = data.hybridVotingContract?.proposals?.[0]?.votes ?? [];
368
+ votes.push(...page);
369
+ if (page.length < 1000)
370
+ break;
371
+ const next = page[page.length - 1].id;
372
+ if (!next || next <= after)
373
+ throw new errors_1.CliError('Cannot paginate proposal votes without an advancing vote ID.', exit_codes_1.EXIT.PRECONDITION);
374
+ after = next;
375
+ }
376
+ if (first.hybridVotingContract?.proposals?.[0])
377
+ first.hybridVotingContract.proposals[0].votes = votes;
378
+ return first;
379
+ }
380
+ /** Reproduce HybridVotingCore vote rounding and VotingMath.pickWinnerNSlices scores. */
381
+ function computeClassWeightedScores(ballots, slices, numOptions) {
382
+ const totals = slices.map((_, cls) => ballots.reduce((sum, v) => sum + (v.classRawPowers[cls] ?? 0n), 0n));
383
+ return Array.from({ length: numOptions }, (_, option) => {
384
+ let score = 0n;
385
+ for (let cls = 0; cls < slices.length; cls++) {
386
+ if (totals[cls] === 0n)
387
+ continue;
388
+ const optionRaw = ballots.reduce((sum, v) => sum + (v.classRawPowers[cls] ?? 0n) * BigInt(v.weights[option] ?? 0) / 100n, 0n);
389
+ score += optionRaw * BigInt(slices[cls]) * 10000n / totals[cls];
390
+ }
391
+ // Scores are bounded by 100 * 10000, so this conversion is exact.
392
+ return Number(score) / 10000;
393
+ });
323
394
  }
324
395
  /**
325
396
  * Subgraph rows → the exact shape the contract path produces. `asset` is
@@ -336,6 +407,8 @@ function normalizeSubgraphClasses(rows) {
336
407
  minBalance: ethers_1.ethers.BigNumber.from(String(c.minBalance ?? '0')).toString(),
337
408
  asset: ethers_1.ethers.utils.getAddress(String(c.asset)),
338
409
  hatIds: (c.hatIds ?? []).map(h => String(h)),
410
+ subjectId: null,
411
+ subjectBindingKnown: false,
339
412
  }));
340
413
  }
341
414
  /**
@@ -355,23 +428,30 @@ async function fetchClassConfig(client, hybridVotingAddress, proposalId, chainId
355
428
  // guessing the newest version would silently misreport an old proposal.
356
429
  const tiers = proposalId === undefined
357
430
  ? [
431
+ { query: voting_classes_1.FETCH_VOTING_CLASS_CONFIG_EXACT, variables: { hybridVoting } },
358
432
  { query: voting_classes_1.FETCH_VOTING_CLASS_CONFIG, variables: { hybridVoting } },
359
433
  { query: voting_classes_1.FETCH_VOTING_CLASS_CONFIG_LEGACY, variables: { hybridVoting } },
360
434
  ]
361
435
  : [
436
+ { query: voting_classes_1.FETCH_PROPOSAL_VOTING_CLASSES_EXACT, variables: { hybridVoting, proposalId: String(proposalId) } },
362
437
  { query: voting_classes_1.FETCH_PROPOSAL_VOTING_CLASSES, variables: { hybridVoting, proposalId: String(proposalId) } },
363
438
  ];
364
439
  const { data } = await client.queryWithFieldFallback(tiers, { chainId });
365
440
  const contract = data?.hybridVotingContract;
366
441
  if (!contract)
367
442
  return null;
443
+ // createProposalV2 may snapshot a synthetic equal-weight class for a restricted poll.
444
+ // The index has no ProposalConfigV2/synthetic snapshot fields; its ordinary class version
445
+ // cannot distinguish those proposals. Unknown restriction metadata is equally untrusted.
446
+ if (proposalId !== undefined && contract.proposals?.[0]?.isHatRestricted !== false)
447
+ return null;
368
448
  const version = proposalId === undefined
369
449
  ? contract.classVersion
370
450
  : contract.proposals?.[0]?.classesVersion;
371
451
  // For a proposal we must know its frozen version — no version, no answer.
372
452
  if (proposalId !== undefined && (version === null || version === undefined))
373
453
  return null;
374
- const rows = (0, voting_classes_1.selectClassSnapshot)(contract.votingClasses, version);
454
+ const rows = (0, voting_classes_1.selectIndexedClassSnapshot)(contract, proposalId);
375
455
  if (rows.length === 0)
376
456
  return null;
377
457
  if (contract.thresholdPct === null || contract.thresholdPct === undefined)
@@ -382,6 +462,7 @@ async function fetchClassConfig(client, hybridVotingAddress, proposalId, chainId
382
462
  classes: normalizeSubgraphClasses(rows),
383
463
  supportThresholdPct: Number(contract.thresholdPct),
384
464
  quorumVoterCount: Number(contract.quorum),
465
+ effectiveQuorumVoterCount: proposalId === undefined || contract.proposals?.[0]?.isHatRestricted === false ? Number(contract.quorum) : null,
385
466
  };
386
467
  }
387
468
  catch {
@@ -0,0 +1,33 @@
1
+ /** MembershipAuthority intent builders. No legacy Hats/EligibilityModule write surface. */
2
+ import { ethers } from 'ethers';
3
+ import type { TxIntent } from './intent';
4
+ /** The high byte selects the fold; the remaining 248 bits are the label hash shifted right. */
5
+ export declare function permissionKey(label: string, foldTag?: number): string;
6
+ export declare const AUTHORITY_KEYS: {
7
+ DD_VOTE: string;
8
+ DD_CREATE: string;
9
+ HV_CREATE: string;
10
+ TM_PERMS: string;
11
+ PT_MEMBER: string;
12
+ PT_APPROVE: string;
13
+ EDU_CREATE: string;
14
+ EDU_MEMBER: string;
15
+ QJ_AUTOJOIN: string;
16
+ PAY_CREATE: string;
17
+ SUBJECT_RENAME: string;
18
+ };
19
+ export declare const EXISTS_BIT: ethers.BigNumber;
20
+ export declare const INHERIT_GLOBAL_BIT: ethers.BigNumber;
21
+ export declare const VALUE_MASK: ethers.BigNumber;
22
+ export declare function permissionWord(value: ethers.BigNumberish, inheritGlobal?: boolean): ethers.BigNumber;
23
+ /** Project zero must not collide with the global context. */
24
+ export declare function projectContext(projectId: ethers.BigNumberish): string;
25
+ /** Explicit allowlist excludes initialization, migration seeds and compatibility mint selectors. */
26
+ export declare const AUTHORITY_METHODS: readonly ["claim", "renounce", "reconcile(uint256,address)", "reconcile(uint256,address[])", "grant", "offer", "withdrawOffer", "remove", "unremove", "setRule", "clearRule", "delegatedGrant", "delegatedOffer", "delegatedRemove", "delegatedUnremove", "finalize", "cancel", "setManagerConfig", "createRole", "createGroup", "addRoleToGroup", "removeRoleFromGroup", "renameSubject", "setMaxMembers", "setSubjectDefault", "configureVouchAttestor", "resetVouchEpoch", "clearUserVouches", "setMaxDailyVouches", "vouch", "revokeVouch", "setPerm", "clearPerm", "setPaused"];
27
+ export type AuthorityMethod = typeof AUTHORITY_METHODS[number];
28
+ export declare function buildAuthorityAction(a: {
29
+ authorityAddress: string;
30
+ method: AuthorityMethod;
31
+ args: unknown[];
32
+ orgId?: string;
33
+ }): TxIntent;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AUTHORITY_METHODS = exports.VALUE_MASK = exports.INHERIT_GLOBAL_BIT = exports.EXISTS_BIT = exports.AUTHORITY_KEYS = void 0;
4
+ exports.permissionKey = permissionKey;
5
+ exports.permissionWord = permissionWord;
6
+ exports.projectContext = projectContext;
7
+ exports.buildAuthorityAction = buildAuthorityAction;
8
+ /** MembershipAuthority intent builders. No legacy Hats/EligibilityModule write surface. */
9
+ const ethers_1 = require("ethers");
10
+ const contracts_1 = require("../contracts");
11
+ /** The high byte selects the fold; the remaining 248 bits are the label hash shifted right. */
12
+ function permissionKey(label, foldTag = 0) {
13
+ if (!Number.isInteger(foldTag) || foldTag < 0 || foldTag > 2)
14
+ throw new Error('Invalid permission fold tag');
15
+ return ethers_1.ethers.utils.hexZeroPad(ethers_1.ethers.BigNumber.from(ethers_1.ethers.utils.id(label)).shr(8)
16
+ .or(ethers_1.ethers.BigNumber.from(foldTag).shl(248)).toHexString(), 32);
17
+ }
18
+ exports.AUTHORITY_KEYS = {
19
+ DD_VOTE: permissionKey('poa.perm.dd.vote'), DD_CREATE: permissionKey('poa.perm.dd.create'),
20
+ HV_CREATE: permissionKey('poa.perm.hv.create'), TM_PERMS: permissionKey('poa.perm.tm.perms', 1),
21
+ PT_MEMBER: permissionKey('poa.perm.pt.member'), PT_APPROVE: permissionKey('poa.perm.pt.approve'),
22
+ EDU_CREATE: permissionKey('poa.perm.edu.create'), EDU_MEMBER: permissionKey('poa.perm.edu.member'),
23
+ QJ_AUTOJOIN: permissionKey('poa.perm.qj.autojoin'), PAY_CREATE: permissionKey('poa.perm.pay.create'),
24
+ SUBJECT_RENAME: permissionKey('poa.perm.subject.rename'),
25
+ };
26
+ exports.EXISTS_BIT = ethers_1.ethers.BigNumber.from(1).shl(255);
27
+ exports.INHERIT_GLOBAL_BIT = ethers_1.ethers.BigNumber.from(1).shl(254);
28
+ exports.VALUE_MASK = exports.INHERIT_GLOBAL_BIT.sub(1);
29
+ function permissionWord(value, inheritGlobal = false) {
30
+ const word = ethers_1.ethers.BigNumber.from(value);
31
+ if (word.lt(0) || word.gt(exports.VALUE_MASK))
32
+ throw new Error('Permission value must fit in 254 bits');
33
+ return word.or(exports.EXISTS_BIT).or(inheritGlobal ? exports.INHERIT_GLOBAL_BIT : 0);
34
+ }
35
+ /** Project zero must not collide with the global context. */
36
+ function projectContext(projectId) {
37
+ const id = ethers_1.ethers.BigNumber.from(projectId);
38
+ if (id.lt(0) || id.gte(ethers_1.ethers.constants.MaxUint256))
39
+ throw new Error('Project ID cannot be encoded as projectId + 1');
40
+ return ethers_1.ethers.utils.hexZeroPad(id.add(1).toHexString(), 32);
41
+ }
42
+ /** Explicit allowlist excludes initialization, migration seeds and compatibility mint selectors. */
43
+ exports.AUTHORITY_METHODS = [
44
+ 'claim', 'renounce', 'reconcile(uint256,address)', 'reconcile(uint256,address[])',
45
+ 'grant', 'offer', 'withdrawOffer', 'remove', 'unremove', 'setRule', 'clearRule',
46
+ 'delegatedGrant', 'delegatedOffer', 'delegatedRemove', 'delegatedUnremove', 'finalize', 'cancel',
47
+ 'setManagerConfig', 'createRole', 'createGroup', 'addRoleToGroup', 'removeRoleFromGroup',
48
+ 'renameSubject', 'setMaxMembers', 'setSubjectDefault', 'configureVouchAttestor',
49
+ 'resetVouchEpoch', 'clearUserVouches', 'setMaxDailyVouches', 'vouch', 'revokeVouch',
50
+ 'setPerm', 'clearPerm', 'setPaused',
51
+ ];
52
+ function buildAuthorityAction(a) {
53
+ if (!exports.AUTHORITY_METHODS.includes(a.method))
54
+ throw new Error('Unsupported authority action');
55
+ if (!ethers_1.ethers.utils.isAddress(a.authorityAddress) || a.authorityAddress === ethers_1.ethers.constants.AddressZero) {
56
+ throw new Error('A nonzero MembershipAuthority address is required');
57
+ }
58
+ const abi = (0, contracts_1.getAbi)('MembershipAuthority');
59
+ new ethers_1.ethers.utils.Interface(abi).encodeFunctionData(a.method, a.args);
60
+ return { to: a.authorityAddress, abi, method: a.method, args: a.args,
61
+ meta: { domain: 'role', action: a.method, orgId: a.orgId, summary: { authority: a.authorityAddress } } };
62
+ }
@@ -47,6 +47,12 @@ export interface GovernanceWrapParams {
47
47
  batches: ExecutionCall[][];
48
48
  /** Restricted-voting hats; [] default. */
49
49
  hatIds?: ethers.BigNumberish[];
50
+ /** Native MembershipAuthority subjects restricting voting; preferred over the legacy hatIds name. */
51
+ subjectIds?: ethers.BigNumberish[];
52
+ /** Restricted polls only. Executable polls use max(global quorum, override). */
53
+ quorumOverride?: number;
54
+ /** Hybrid restricted polls only: count each eligible voter once. */
55
+ equalWeight?: boolean;
50
56
  orgId?: string;
51
57
  /** meta.action override (default 'create'). */
52
58
  action?: string;
@@ -39,11 +39,28 @@ function buildGovernanceProposal(p) {
39
39
  // [target, BigNumber(value), data] — same tuple construction as the CLI
40
40
  // (`c.value || '0'` handles undefined/empty values as zero).
41
41
  const batchTuples = p.batches.map(batch => batch.map(call => [call.target, ethers_1.ethers.BigNumber.from(call.value || 0), call.calldata]));
42
- const hatIds = p.hatIds ?? [];
42
+ if (p.subjectIds !== undefined && p.hatIds !== undefined)
43
+ throw new Error('Specify subjectIds or hatIds, not both.');
44
+ const hatIds = (p.subjectIds ?? p.hatIds ?? []).map(id => {
45
+ const value = ethers_1.ethers.BigNumber.from(id);
46
+ if (value.lt(0) || value.gt(ethers_1.ethers.constants.MaxUint256))
47
+ throw new Error('Subject IDs must fit uint256.');
48
+ return value;
49
+ });
50
+ const quorum = p.quorumOverride ?? 0;
51
+ if (!Number.isInteger(quorum) || quorum < 0 || quorum > 4294967295)
52
+ throw new Error('quorumOverride must be a uint32 voter count.');
53
+ if (p.equalWeight !== undefined && typeof p.equalWeight !== 'boolean')
54
+ throw new Error('equalWeight must be a boolean.');
55
+ if (p.equalWeight && p.votingAbiName !== 'HybridVotingNew')
56
+ throw new Error('Equal-weight voting is supported only by HybridVoting.');
57
+ if ((quorum !== 0 || p.equalWeight) && hatIds.length === 0)
58
+ throw new Error('Quorum overrides and equal-weight voting require restricted subject IDs.');
59
+ const v2 = p.quorumOverride !== undefined || p.equalWeight === true;
43
60
  return {
44
61
  to: p.votingAddress,
45
62
  abi: (0, contracts_1.getAbi)(p.votingAbiName),
46
- method: 'createProposal',
63
+ method: v2 ? 'createProposalV2' : 'createProposal',
47
64
  args: [
48
65
  (0, encoding_1.stringToBytes)(p.title),
49
66
  p.descriptionHash,
@@ -51,6 +68,7 @@ function buildGovernanceProposal(p) {
51
68
  p.numOptions,
52
69
  batchTuples,
53
70
  hatIds,
71
+ ...(v2 ? [quorum, ...(p.votingAbiName === 'HybridVotingNew' ? [p.equalWeight ?? false] : [])] : []),
54
72
  ],
55
73
  meta: {
56
74
  domain: p.domain ?? 'vote',
@@ -4,7 +4,6 @@
4
4
  */
5
5
  export * from './intent';
6
6
  export * as education from './education';
7
- export * as eligibility from './eligibility';
8
7
  export * as governance from './governance';
9
8
  export * as org from './org';
10
9
  export * as paymaster from './paymaster';
@@ -15,3 +14,4 @@ export * as treasury from './treasury';
15
14
  export * as user from './user';
16
15
  export * as vote from './vote';
17
16
  export * as zkemail from './zkemail';
17
+ export * as authority from './authority';