@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,687 +0,0 @@
1
- "use strict";
2
- /**
3
- * Shared EligibilityModule read layer for the vouch + role domains.
4
- * Port of src/commands/vouch/helpers.ts (plus the subgraph list reads from
5
- * src/commands/vouch/list.ts and src/commands/role/applications.ts): functions
6
- * take the GraphClient / provider explicitly instead of importing
7
- * process-global singletons.
8
- *
9
- * Both domains read the same module, whose admin surface is superAdmin-gated.
10
- * Facts VERIFIED against contracts origin/main src/EligibilityModule.sol:
11
- *
12
- * - `superAdmin()` public getter exists; every eligibility/hat-admin write is
13
- * `onlySuperAdmin` (setWearerEligibility, setDefaultEligibility,
14
- * clearWearerEligibility, batchSetWearerEligibility, setDefaultEligibility,
15
- * createHatWithEligibility, mintHatToAddress, batchMintHats, pause/unpause,
16
- * transferSuperAdmin, setUserJoinTime, setUserJoinTimeNow, configureVouching,
17
- * resetVouches, clearWearerVouches).
18
- * - VouchConfig struct: (uint32 quorum, uint256 membershipHatId, uint8 flags)
19
- * with flags bit 0 = enabled (ENABLED_FLAG 0x01) and bit 1 =
20
- * combineWithHierarchy (COMBINE_HIERARCHY_FLAG 0x02). Getter: getVouchConfig.
21
- * - Rate limiting: getMaxDailyVouches() (DEFAULT_MAX_DAILY_VOUCHES = 20 when
22
- * unset), getCurrentDailyVouchCount(user) keyed by UTC day
23
- * (block.timestamp / 86400), getUserJoinTime(user), and the authoritative
24
- * combined answer canUserVouch(user).
25
- * - vouchFor does NOT auto-mint at quorum: reaching quorum makes
26
- * getWearerStatus() return eligible, and the wearer claims explicitly via
27
- * claimVouchedHat (claim-based pattern per the contract doc comment).
28
- *
29
- * READ SOURCING (subgraph vs RPC), settled 2026-07-30 against the live
30
- * poa-gnosis-v-1 / poa-arb-v-1 deployments:
31
- *
32
- * - DISPLAY reads (pop vouch status, pop vouch config show) go SUBGRAPH-FIRST
33
- * with the RPC getter kept as fallback: VouchConfig.{quorum,membershipHatId,
34
- * enabled,combinesWithHierarchy} and a count of active Vouch rows are
35
- * verified-populated and byte-for-byte equal to the on-chain values.
36
- * - VOUCH COUNTS ARE EPOCH-SCOPED. configureVouching/batchConfigureVouching/
37
- * resetVouches bump vouchConfigEpoch[hatId] and clearWearerVouches parks
38
- * wearerVouchEpoch on a 2^256-1 sentinel; currentVouchCount() returns 0 when
39
- * the two disagree, and NO event marks the individual vouches dead. Counting
40
- * active Vouch rows therefore overcounts after any reconfiguration and would
41
- * report quorum met against a contract that says zero. Newer deployments
42
- * index the epochs (WearerVouchState), which is exact; older ones can only
43
- * detect the ambiguity and refuse. See graph/documents/vouch.ts for both tiers.
44
- * - WRITE PRE-FLIGHT reads stay on RPC. superAdmin(), paused(),
45
- * hasVouched(), hasActiveApplication(), getWearerStatus(), getDefaultRules()
46
- * and the vouch-for/claim gates all exist to PREDICT A REVERT; indexing lag
47
- * there either broadcasts a doomed transaction or blocks a valid one.
48
- * They are instead batched through Multicall3 so each pre-flight costs ONE
49
- * round-trip instead of N.
50
- * - NO subgraph field exists for getMaxDailyVouches(),
51
- * getCurrentDailyVouchCount() or canUserVouch(); UserJoinTime has ZERO rows
52
- * on the live Gnosis deployment. The whole VoucherGate therefore stays RPC.
53
- */
54
- Object.defineProperty(exports, "__esModule", { value: true });
55
- exports.VOUCH_FLAG_COMBINE_HIERARCHY = exports.VOUCH_FLAG_ENABLED = void 0;
56
- exports.resolveEligibilityModule = resolveEligibilityModule;
57
- exports.parseHatId = parseHatId;
58
- exports.decodeVouchConfig = decodeVouchConfig;
59
- exports.batchEligibilityReadsSettled = batchEligibilityReadsSettled;
60
- exports.batchEligibilityReads = batchEligibilityReads;
61
- exports.fetchVouchConfigFromSubgraph = fetchVouchConfigFromSubgraph;
62
- exports.readVouchConfigStatus = readVouchConfigStatus;
63
- exports.fetchWearerVouchStateFromSubgraph = fetchWearerVouchStateFromSubgraph;
64
- exports.readWearerVouchStateOnchain = readWearerVouchStateOnchain;
65
- exports.readWearerVouchState = readWearerVouchState;
66
- exports.getSuperAdmin = getSuperAdmin;
67
- exports.requireSuperAdmin = requireSuperAdmin;
68
- exports.requireSuperAdminWithRead = requireSuperAdminWithRead;
69
- exports.readVoucherGate = readVoucherGate;
70
- exports.vouchRestriction = vouchRestriction;
71
- exports.vouchQuotaLabel = vouchQuotaLabel;
72
- exports.readVouchPreflight = readVouchPreflight;
73
- exports.gateVouch = gateVouch;
74
- exports.readClaimGate = readClaimGate;
75
- exports.hasVouched = hasVouched;
76
- exports.readRevokeGate = readRevokeGate;
77
- exports.hasActiveApplication = hasActiveApplication;
78
- exports.isPaused = isPaused;
79
- exports.listVouches = listVouches;
80
- exports.listRoleApplications = listRoleApplications;
81
- exports.resolveRoleNameToHatId = resolveRoleNameToHatId;
82
- const ethers_1 = require("ethers");
83
- const resolve_1 = require("./resolve");
84
- const contracts_1 = require("../contracts");
85
- const multicall_1 = require("../multicall");
86
- const vouch_1 = require("../graph/documents/vouch");
87
- const role_1 = require("../graph/documents/role");
88
- const format_1 = require("../format");
89
- const errors_1 = require("../errors");
90
- const exit_codes_1 = require("../exit-codes");
91
- const perms_1 = require("../perms");
92
- Object.defineProperty(exports, "VOUCH_FLAG_ENABLED", { enumerable: true, get: function () { return perms_1.VOUCH_FLAG_ENABLED; } });
93
- Object.defineProperty(exports, "VOUCH_FLAG_COMBINE_HIERARCHY", { enumerable: true, get: function () { return perms_1.VOUCH_FLAG_COMBINE_HIERARCHY; } });
94
- /**
95
- * Resolve an org to its EligibilityModule address.
96
- * Port of src/commands/vouch/helpers.ts resolveEligibilityModule.
97
- */
98
- async function resolveEligibilityModule(client, orgIdOrName, chainId) {
99
- const modules = await (0, resolve_1.resolveOrgModules)(client, orgIdOrName, chainId);
100
- return {
101
- orgId: modules.orgId,
102
- eligibilityModuleAddress: (0, resolve_1.requireModule)(modules, 'eligibilityModuleAddress'),
103
- };
104
- }
105
- /** Parse a --hat argument (decimal or 0x-hex) into a BigNumber hat ID. */
106
- function parseHatId(input) {
107
- try {
108
- return ethers_1.ethers.BigNumber.from(String(input).trim());
109
- }
110
- catch {
111
- throw new errors_1.CliError(`Invalid --hat "${input}".`, exit_codes_1.EXIT.USAGE, 'Pass the hat ID as a decimal or 0x-hex integer (see pop org roles).');
112
- }
113
- }
114
- /** Decode a raw getVouchConfig/vouchConfigs result into named fields. */
115
- function decodeVouchConfig(raw) {
116
- const flags = ethers_1.ethers.BigNumber.from(raw.flags).toNumber();
117
- return {
118
- quorum: ethers_1.ethers.BigNumber.from(raw.quorum).toNumber(),
119
- membershipHatId: ethers_1.ethers.BigNumber.from(raw.membershipHatId).toString(),
120
- enabled: (flags & perms_1.VOUCH_FLAG_ENABLED) !== 0,
121
- combineWithHierarchy: (flags & perms_1.VOUCH_FLAG_COMBINE_HIERARCHY) !== 0,
122
- };
123
- }
124
- /**
125
- * Batch EligibilityModule view calls into ONE Multicall3 round-trip
126
- * (multicall.ts degrades to parallel provider.call on chains without the
127
- * canonical deployment, so callers always get one result per call).
128
- *
129
- * Every caller is a write pre-flight, so a sub-call that reverts must NEVER be
130
- * silently decoded as a falsy answer: Multicall3.tryAggregate is invoked with
131
- * requireSuccess=false, and any call that comes back unsuccessful is re-issued
132
- * through the ethers contract so the original error propagates exactly as it
133
- * did before batching.
134
- *
135
- * Return shape matches `contract.fn(...)`: a single output is unwrapped, a
136
- * multi-output (getWearerStatus, getDefaultRules) stays an ethers Result with
137
- * both named and positional access.
138
- */
139
- async function batchEligibilityReadsSettled(provider, eligibilityModuleAddress, reads) {
140
- const contract = (0, contracts_1.createReadContract)(eligibilityModuleAddress, 'EligibilityModuleNew', provider);
141
- const iface = contract.interface;
142
- let results = [];
143
- try {
144
- const calls = reads.map(r => ({
145
- to: eligibilityModuleAddress,
146
- data: iface.encodeFunctionData(r.fn, r.args),
147
- }));
148
- results = await (0, multicall_1.tryAggregate)(provider, calls);
149
- }
150
- catch {
151
- results = [];
152
- }
153
- return Promise.allSettled(reads.map(async (read, i) => {
154
- const result = results[i];
155
- if (!result || !result.success || !result.returnData || result.returnData === '0x') {
156
- // Not batchable / reverted — re-issue so the natural error surfaces.
157
- return contract[read.fn](...read.args);
158
- }
159
- const decoded = iface.decodeFunctionResult(read.fn, result.returnData);
160
- return decoded.length === 1 ? decoded[0] : decoded;
161
- }));
162
- }
163
- /** batchEligibilityReadsSettled, rethrowing the first failure (Promise.all semantics). */
164
- async function batchEligibilityReads(provider, eligibilityModuleAddress, reads) {
165
- const settled = await batchEligibilityReadsSettled(provider, eligibilityModuleAddress, reads);
166
- return settled.map((entry) => {
167
- if (entry.status === 'rejected')
168
- throw entry.reason;
169
- return entry.value;
170
- });
171
- }
172
- /**
173
- * Subgraph VouchConfig row → the same VouchConfigView the RPC tuple decodes to.
174
- * `enabled` is authoritative in the subgraph (the contract derives it as
175
- * quorum > 0 and emits it on VouchConfigSet), so it is used directly rather
176
- * than re-derived from flags.
177
- */
178
- function vouchConfigFromSubgraph(row) {
179
- if (!row)
180
- return null;
181
- if (row.quorum === null || row.quorum === undefined)
182
- return null;
183
- if (row.enabled === null || row.enabled === undefined)
184
- return null;
185
- if (row.membershipHatId === null || row.membershipHatId === undefined)
186
- return null;
187
- if (row.combinesWithHierarchy === null || row.combinesWithHierarchy === undefined)
188
- return null;
189
- return {
190
- quorum: Number(row.quorum),
191
- membershipHatId: String(row.membershipHatId),
192
- enabled: Boolean(row.enabled),
193
- combineWithHierarchy: Boolean(row.combinesWithHierarchy),
194
- };
195
- }
196
- /** The Graph caps a page at 1000 entities; a full page means "cannot derive". */
197
- const VOUCH_PAGE_CAP = 1000;
198
- /**
199
- * Subgraph read of a hat's vouch config. Returns null — meaning "fall back to
200
- * the RPC getter" — when the query fails, when the row is missing (absence is
201
- * ambiguous between never-configured and not-yet-indexed), or when a field is
202
- * null on an older deployment.
203
- */
204
- async function fetchVouchConfigFromSubgraph(client, eligibilityModuleAddress, hatId, chainId) {
205
- try {
206
- const data = await client.query(vouch_1.FETCH_VOUCH_CONFIG, { eligibilityModuleId: eligibilityModuleAddress, hatId: hatId.toString() }, chainId);
207
- return vouchConfigFromSubgraph(data?.vouchConfigs?.[0]);
208
- }
209
- catch {
210
- return null;
211
- }
212
- }
213
- /**
214
- * Port of `pop vouch config show` — src/commands/vouch/config.ts.
215
- * Config is SUBGRAPH-FIRST (display only) with getVouchConfig() as fallback;
216
- * `preferSubgraph: false` (the CLI's explicit --rpc) pins the config read to
217
- * the provider. getMaxDailyVouches() has no subgraph field and stays on chain.
218
- */
219
- async function readVouchConfigStatus(client, provider, eligibilityModuleAddress, hatId, opts = {}) {
220
- const contract = (0, contracts_1.createReadContract)(eligibilityModuleAddress, 'EligibilityModuleNew', provider);
221
- const [subgraphConfig, maxDailyVouches] = await Promise.all([
222
- opts.preferSubgraph !== false
223
- ? fetchVouchConfigFromSubgraph(client, eligibilityModuleAddress, hatId, opts.chainId)
224
- : Promise.resolve(null),
225
- contract.getMaxDailyVouches(),
226
- ]);
227
- const config = subgraphConfig ?? decodeVouchConfig(await contract.getVouchConfig(hatId));
228
- return {
229
- config,
230
- maxDailyVouches: Number(maxDailyVouches),
231
- source: subgraphConfig ? 'subgraph' : 'rpc',
232
- };
233
- }
234
- /**
235
- * TIER 0 reader — reproduce `currentVouchCount(hatId, wearer)` from the subgraph's
236
- * epoch mirror. Returns null to mean "cannot derive, fall back to RPC".
237
- */
238
- function wearerCountFromEpochMirror(data, configRow) {
239
- const configEpoch = configRow?.epoch;
240
- if (configEpoch === null || configEpoch === undefined)
241
- return null;
242
- const states = data?.wearerVouchStates;
243
- if (!Array.isArray(states))
244
- return null;
245
- // No row means the wearer has never been vouched for on this hat. The config row
246
- // proved the module is indexed, so absence here is a real zero rather than lag.
247
- if (states.length === 0)
248
- return 0;
249
- const state = states[0];
250
- if (state?.count === null || state?.count === undefined)
251
- return null;
252
- if (state?.epoch === null || state?.epoch === undefined)
253
- return null;
254
- // The contract's getter, verbatim: a tally from a superseded epoch reads as zero.
255
- // String compare because epochs are uint256 and exceed Number's safe range.
256
- const count = String(state.epoch) === String(configEpoch) ? Number(state.count) : 0;
257
- if (!Number.isFinite(count))
258
- return null;
259
- // effectiveCount is the subgraph's own materialisation of the same expression, kept
260
- // in step by a sweep. Disagreement means the deployment is buggy — refuse to pick a
261
- // winner and let the authoritative on-chain getter answer.
262
- const materialised = state?.effectiveCount;
263
- if (materialised !== null && materialised !== undefined && Number(materialised) !== count) {
264
- return null;
265
- }
266
- return count;
267
- }
268
- /**
269
- * TIER 1 reader — count active Vouch rows on deployments with no epoch mirror.
270
- * Returns null to mean "cannot derive, fall back to RPC".
271
- */
272
- function wearerCountFromVouchRows(data, configRow) {
273
- const vouches = data?.vouches;
274
- if (!Array.isArray(vouches) || vouches.length >= VOUCH_PAGE_CAP)
275
- return null;
276
- // EPOCH GUARD. These deployments do not index the vouch epoch at all, and the
277
- // contract emits no event when a configureVouching/resetVouches invalidates every
278
- // outstanding vouch — so the superseded rows are still isActive here. A vouch
279
- // created BEFORE the config's last update may therefore be dead on chain, and
280
- // counting it would report quorum met when the contract says otherwise. Block-number
281
- // ordering is the only signal available, so any such row makes the count underivable.
282
- const configBlock = Number(configRow?.updatedAtBlock ?? 0);
283
- if (configBlock > 0 && vouches.some((v) => Number(v?.createdAtBlock ?? 0) < configBlock)) {
284
- return null;
285
- }
286
- return vouches.length;
287
- }
288
- /**
289
- * Subgraph read of a hat's vouch config PLUS the wearer's current vouch count.
290
- * Returns null to mean "fall back to RPC".
291
- *
292
- * Two tiers (graph/documents/vouch.ts): the epoch mirror when the deployment indexes
293
- * it — which is exact, and lets a reconfigured hat still be served from the subgraph —
294
- * and the legacy active-row count, which can only refuse to answer once an epoch bump
295
- * is suspected. `queryWithFieldFallback` drops to the legacy tier on the unknown-field
296
- * validation error, so this works before and after the subgraph ships.
297
- */
298
- async function fetchWearerVouchStateFromSubgraph(client, eligibilityModuleAddress, hatId, wearer, chainId) {
299
- const variables = {
300
- eligibilityModuleId: eligibilityModuleAddress,
301
- hatId: hatId.toString(),
302
- wearer,
303
- };
304
- try {
305
- const { data, tierIndex } = await client.queryWithFieldFallback([
306
- { query: vouch_1.FETCH_VOUCH_STATUS_EPOCH_AWARE, variables },
307
- { query: vouch_1.FETCH_VOUCH_STATUS, variables },
308
- ], { chainId });
309
- const configRow = data?.vouchConfigs?.[0];
310
- const config = vouchConfigFromSubgraph(configRow);
311
- if (!config)
312
- return null;
313
- const currentCount = tierIndex === 0
314
- ? wearerCountFromEpochMirror(data, configRow)
315
- : wearerCountFromVouchRows(data, configRow);
316
- if (currentCount === null)
317
- return null;
318
- return { config, currentCount };
319
- }
320
- catch {
321
- return null;
322
- }
323
- }
324
- /**
325
- * RPC read of the same wearer-side state, batched into one round-trip.
326
- * `enabled` is derived from the config flags rather than a separate
327
- * isVouchingEnabled() call — the contract's getter is literally
328
- * `_isVouchingEnabled(vouchConfigs[hatId].flags)` (VERIFIED in
329
- * src/EligibilityModule.sol), so the answer is identical.
330
- */
331
- async function readWearerVouchStateOnchain(provider, eligibilityModuleAddress, hatId, wearer) {
332
- const [rawConfig, currentCount] = await batchEligibilityReads(provider, eligibilityModuleAddress, [
333
- { fn: 'vouchConfigs', args: [hatId] },
334
- { fn: 'currentVouchCount', args: [hatId, wearer] },
335
- ]);
336
- return {
337
- config: decodeVouchConfig(rawConfig),
338
- currentCount: ethers_1.ethers.BigNumber.from(currentCount).toNumber(),
339
- };
340
- }
341
- /**
342
- * Wearer-side vouch progress, SUBGRAPH-FIRST with the RPC batch as fallback.
343
- * Display-only (`pop vouch status`) — never used to gate a write.
344
- *
345
- * `preferSubgraph: false` (set when the caller pinned an explicit RPC node)
346
- * skips the subgraph entirely so an explicitly targeted node stays the
347
- * source of truth.
348
- */
349
- async function readWearerVouchState(client, provider, eligibilityModuleAddress, hatId, wearer, opts = {}) {
350
- if (opts.preferSubgraph !== false) {
351
- const fromSubgraph = await fetchWearerVouchStateFromSubgraph(client, eligibilityModuleAddress, hatId, wearer, opts.chainId);
352
- if (fromSubgraph)
353
- return { state: fromSubgraph, source: 'subgraph' };
354
- }
355
- const state = await readWearerVouchStateOnchain(provider, eligibilityModuleAddress, hatId, wearer);
356
- return { state, source: 'rpc' };
357
- }
358
- /**
359
- * Read the module's superAdmin address (public getter, verified).
360
- *
361
- * NOT sourced from the subgraph even though EligibilityModuleContract
362
- * .superAdmin is populated and matches on chain (verified: module
363
- * 0x27114cb7… → 0x23f90b38… on both). Every caller is requireSuperAdmin, a
364
- * pure revert predictor for the onlySuperAdmin modifier. Indexing lag over a
365
- * transferSuperAdmin would either broadcast a doomed admin transaction (the
366
- * outgoing admin still looks authorised) or hard-block the incoming one, and
367
- * these are all write paths where one extra eth_call is noise next to the
368
- * transaction itself.
369
- */
370
- async function getSuperAdmin(provider, eligibilityModuleAddress) {
371
- const contract = (0, contracts_1.createReadContract)(eligibilityModuleAddress, 'EligibilityModuleNew', provider);
372
- return await contract.superAdmin();
373
- }
374
- /** Shared superAdmin mismatch/unreadable errors for both requireSuperAdmin forms. */
375
- function assertSuperAdmin(admin, signerAddress, action) {
376
- if (admin.toLowerCase() !== signerAddress.toLowerCase()) {
377
- throw new errors_1.PreconditionError(`${action} is superAdmin-only — the module superAdmin is ${admin}, but you are signing as ${signerAddress}.`, 'Run with the superAdmin key. If the superAdmin is the org executor, route this change through a governance proposal instead.');
378
- }
379
- return admin;
380
- }
381
- function unreadableSuperAdmin(eligibilityModuleAddress) {
382
- return new errors_1.PreconditionError(`Could not read superAdmin() from the eligibility module at ${eligibilityModuleAddress}.`, 'Check RPC connectivity and that the org has an EligibilityModule deployed.');
383
- }
384
- /**
385
- * Fail fast (exit 4) when the signer is not the module superAdmin, naming
386
- * who the superAdmin actually is — BEFORE any gas is spent. Returns the
387
- * superAdmin address on success. All EligibilityModule admin writes are
388
- * onlySuperAdmin (verified, see module header).
389
- */
390
- async function requireSuperAdmin(provider, eligibilityModuleAddress, signerAddress, action) {
391
- let admin;
392
- try {
393
- admin = await getSuperAdmin(provider, eligibilityModuleAddress);
394
- }
395
- catch {
396
- throw unreadableSuperAdmin(eligibilityModuleAddress);
397
- }
398
- return assertSuperAdmin(admin, signerAddress, action);
399
- }
400
- /**
401
- * requireSuperAdmin PLUS one extra pre-flight read, in ONE Multicall3
402
- * round-trip instead of two sequential eth_calls to the same contract.
403
- *
404
- * The two are independent: a superAdmin() failure raises the same
405
- * PreconditionError requireSuperAdmin does, while a failure of the extra read
406
- * (older module without the getter, for instance) is returned as
407
- * `{ extra: null, extraError }` so callers can keep downgrading it to a
408
- * skipped soft check rather than blocking the write.
409
- */
410
- async function requireSuperAdminWithRead(provider, eligibilityModuleAddress, signerAddress, action, extra) {
411
- const [adminResult, extraResult] = await batchEligibilityReadsSettled(provider, eligibilityModuleAddress, [{ fn: 'superAdmin', args: [] }, extra]);
412
- if (adminResult.status === 'rejected')
413
- throw unreadableSuperAdmin(eligibilityModuleAddress);
414
- const admin = assertSuperAdmin(adminResult.value, signerAddress, action);
415
- return extraResult.status === 'fulfilled'
416
- ? { admin, extra: extraResult.value, extraError: null }
417
- : { admin, extra: null, extraError: extraResult.reason };
418
- }
419
- /**
420
- * The four gate reads, in batch order. ALL FOUR ARE RPC-ONLY and cannot be
421
- * served by the subgraph (verified 2026-07-30 against poa-gnosis-v-1):
422
- * canUserVouch — composite of join-grace + daily limit, no field
423
- * getCurrentDailyVouchCount — no field (derivable from Vouch.createdAt, but
424
- * only by re-deriving the contract's UTC-day
425
- * bucketing client-side; left on RPC)
426
- * getMaxDailyVouches — EligibilityModuleContract has no maxDailyVouches
427
- * field even though MaxDailyVouchesSet is emitted
428
- * getUserJoinTime — UserJoinTime entity has ZERO rows live
429
- */
430
- const VOUCHER_GATE_READS = (voucher) => [
431
- { fn: 'canUserVouch', args: [voucher] },
432
- { fn: 'getCurrentDailyVouchCount', args: [voucher] },
433
- { fn: 'getMaxDailyVouches', args: [] },
434
- { fn: 'getUserJoinTime', args: [voucher] },
435
- ];
436
- function decodeVoucherGate(raw) {
437
- const [canVouch, dailyUsed, maxDaily, joinTime] = raw;
438
- return {
439
- canVouch: Boolean(canVouch),
440
- dailyUsed: ethers_1.ethers.BigNumber.from(dailyUsed).toNumber(),
441
- maxDaily: ethers_1.ethers.BigNumber.from(maxDaily).toNumber(),
442
- joinTime: ethers_1.ethers.BigNumber.from(joinTime).toNumber(),
443
- };
444
- }
445
- /**
446
- * Read the signer-side vouching gate in ONE Multicall3 round-trip:
447
- * canUserVouch (the authoritative on-chain answer), getCurrentDailyVouchCount
448
- * vs getMaxDailyVouches, and getUserJoinTime for the new-account grace.
449
- */
450
- async function readVoucherGate(provider, eligibilityModuleAddress, voucher) {
451
- const raw = await batchEligibilityReads(provider, eligibilityModuleAddress, VOUCHER_GATE_READS(voucher));
452
- return decodeVoucherGate(raw);
453
- }
454
- /** Approximate new-user grace used only for the unlock estimate (deployed v4 modules). */
455
- const NEW_USER_GRACE_SECONDS = 2 * 86400;
456
- /**
457
- * Human reason why a signer cannot vouch right now, derived from the gate
458
- * reads. canUserVouch is authoritative; this maps its `false` to the same
459
- * friendly reasons the contract enforces (daily rate limit, else the
460
- * new-account grace). Returns null when the signer can vouch.
461
- */
462
- function vouchRestriction(gate, now = Math.floor(Date.now() / 1000)) {
463
- if (gate.canVouch)
464
- return null;
465
- if (gate.dailyUsed >= gate.maxDaily) {
466
- const nextUtcMidnight = (Math.floor(now / 86400) + 1) * 86400;
467
- return {
468
- message: `Daily vouch limit reached (${gate.dailyUsed}/${gate.maxDaily} used today).`,
469
- suggestion: `The counter resets at the next UTC midnight (${(0, format_1.formatRelativeTime)(nextUtcMidnight, now)}).`,
470
- };
471
- }
472
- const unlockAt = gate.joinTime > 0 ? gate.joinTime + NEW_USER_GRACE_SECONDS : 0;
473
- const unlockHint = unlockAt > now
474
- ? `Vouching unlocks ${(0, format_1.formatRelativeTime)(unlockAt, now)} (about 2 days after joining).`
475
- : 'New accounts must wait out a short grace period before vouching.';
476
- return {
477
- message: 'Account too new — new members cannot vouch during the join grace period.',
478
- suggestion: unlockHint,
479
- };
480
- }
481
- /** "2/3 used today" quota fragment shared by vouch status + vouch for. */
482
- function vouchQuotaLabel(gate) {
483
- return `${gate.dailyUsed}/${gate.maxDaily} used today`;
484
- }
485
- /**
486
- * Pre-flight reads for `pop vouch for`, in ONE Multicall3 round-trip (was six
487
- * separate eth_calls): the signer gate, the hat's vouch config
488
- * (enabled/quorum), and the wearer's current count for the progress line.
489
- *
490
- * Deliberately RPC — this gates a write, and every field predicts a revert
491
- * (VouchingNotEnabled, NewUserVouchingRestricted, VouchingRateLimitExceeded).
492
- */
493
- async function readVouchPreflight(provider, eligibilityModuleAddress, hatId, voucher, wearer) {
494
- const raw = await batchEligibilityReads(provider, eligibilityModuleAddress, [
495
- ...VOUCHER_GATE_READS(voucher),
496
- { fn: 'getVouchConfig', args: [hatId] },
497
- { fn: 'currentVouchCount', args: [hatId, wearer] },
498
- ]);
499
- const [rawConfig, currentCount] = raw.slice(4);
500
- return {
501
- gate: decodeVoucherGate(raw.slice(0, 4)),
502
- config: decodeVouchConfig(rawConfig),
503
- currentCount: ethers_1.ethers.BigNumber.from(currentCount).toNumber(),
504
- };
505
- }
506
- /**
507
- * Fail fast (exit 4) on every vouch precondition the contract would revert
508
- * on: vouching disabled, daily rate limit, new-account grace. Mirrors
509
- * vouchFor's own gate order so the friendly message matches the revert the
510
- * tx would have produced. Port of `pop vouch for`'s gateVouch —
511
- * src/commands/vouch/for.ts.
512
- */
513
- function gateVouch(state, hatId) {
514
- if (!state.config.enabled) {
515
- throw new errors_1.PreconditionError(`Vouching is not enabled for hat ${hatId}.`, 'Inspect/enable it with: pop vouch config show --hat ' + hatId);
516
- }
517
- const restriction = vouchRestriction(state.gate);
518
- if (restriction) {
519
- throw new errors_1.PreconditionError(restriction.message, restriction.suggestion);
520
- }
521
- }
522
- /**
523
- * Read getWearerStatus + vouch progress for a claim pre-flight, in ONE
524
- * Multicall3 round-trip (was three eth_calls).
525
- *
526
- * getWearerStatus stays RPC on purpose. Besides gating a write, the subgraph's
527
- * WearerEligibility entity is NOT equivalent: it only materialises when an
528
- * admin sets a wearer-specific rule. VERIFIED live — wearer
529
- * 0x1302e867… reached quorum on hat 29089782865237956866… and
530
- * getWearerStatus() returns (true, true) on chain, yet the Gnosis subgraph has
531
- * ZERO WearerEligibility rows for that address. Sourcing the claim gate from
532
- * the subgraph would reject every vouch-eligible claimer.
533
- */
534
- async function readClaimGate(provider, eligibilityModuleAddress, wearer, hatId) {
535
- const [status, rawConfig, currentCount] = await batchEligibilityReads(provider, eligibilityModuleAddress, [
536
- { fn: 'getWearerStatus', args: [wearer, hatId] },
537
- { fn: 'getVouchConfig', args: [hatId] },
538
- { fn: 'currentVouchCount', args: [hatId, wearer] },
539
- ]);
540
- return {
541
- eligible: Boolean(status.eligible ?? status[0]),
542
- standing: Boolean(status.standing ?? status[1]),
543
- config: decodeVouchConfig(rawConfig),
544
- currentCount: ethers_1.ethers.BigNumber.from(currentCount).toNumber(),
545
- };
546
- }
547
- /**
548
- * True when the signer's own vouch record exists (raw, epoch-unaware).
549
- *
550
- * RPC-only: the sole caller is `pop vouch revoke`'s pre-flight, which exists
551
- * to predict revokeVouch's HasNotVouched revert. The subgraph could answer it
552
- * (Vouch filtered on hatId+wearer+voucher+isActive is populated), but a stale
553
- * "false" blocks a legitimate revoke and a stale "true" broadcasts a doomed
554
- * one.
555
- */
556
- async function hasVouched(provider, eligibilityModuleAddress, hatId, wearer, voucher) {
557
- const contract = (0, contracts_1.createReadContract)(eligibilityModuleAddress, 'EligibilityModuleNew', provider);
558
- return Boolean(await contract.hasVouched(hatId, wearer, voucher));
559
- }
560
- /**
561
- * Revoke pre-flight, RPC-only (it predicts a revert, so indexing lag is not
562
- * acceptable) and batched into ONE round-trip.
563
- *
564
- * `hasVouched` ALONE IS NOT SUFFICIENT: the getter returns the raw `vouchers`
565
- * mapping with no epoch filter (VERIFIED, contracts origin/main — it is a bare
566
- * `return _layout().vouchers[hatId][wearer][voucher]`), while revokeVouch first
567
- * requires `wearerVouchEpoch[hatId][wearer] == vouchConfigEpoch[hatId]`. After a
568
- * configureVouching/resetVouches/clearWearerVouches, hasVouched still says true
569
- * and the transaction still reverts HasNotVouched.
570
- *
571
- * currentVouchCount closes most of that gap: it returns 0 for exactly the stale-epoch
572
- * case, and a zero count also makes the `newCount = count - 1` underflow unavoidable —
573
- * so `currentCount == 0` guarantees a revert and can be blocked with no false negatives.
574
- *
575
- * One residual case stays unpredictable off-chain: the wearer's epoch is current
576
- * (someone else vouched after the bump) but THIS voucher's record is not. There is no
577
- * getter for voucherRecordEpoch, so the decoded HasNotVouched revert still covers it.
578
- */
579
- async function readRevokeGate(provider, eligibilityModuleAddress, hatId, wearer, voucher) {
580
- const [vouched, currentCount] = await batchEligibilityReads(provider, eligibilityModuleAddress, [
581
- { fn: 'hasVouched', args: [hatId, wearer, voucher] },
582
- { fn: 'currentVouchCount', args: [hatId, wearer] },
583
- ]);
584
- return {
585
- hasVouched: Boolean(vouched),
586
- currentCount: ethers_1.ethers.BigNumber.from(currentCount).toNumber(),
587
- };
588
- }
589
- /**
590
- * True when `applicant` has an active application for `hatId`.
591
- *
592
- * RPC-only. RoleApplication.active IS populated on the live subgraph, but the
593
- * ONLY caller of this helper is `pop role withdraw-application`'s pre-flight
594
- * (src/commands/role/withdraw-application.ts), which predicts the
595
- * NoActiveApplication revert — there is no display caller. `pop role
596
- * applications` reads the subgraph via listRoleApplications below.
597
- */
598
- async function hasActiveApplication(provider, eligibilityModuleAddress, hatId, applicant) {
599
- const contract = (0, contracts_1.createReadContract)(eligibilityModuleAddress, 'EligibilityModuleNew', provider);
600
- return Boolean(await contract.hasActiveApplication(hatId, applicant));
601
- }
602
- /**
603
- * Read paused() (superAdmin emergency-pause state).
604
- *
605
- * RPC-only. EligibilityModuleContract.isPaused is populated and agrees with
606
- * chain (verified: false on module 0x27114cb7… both ways), but the only caller
607
- * is `pop role admin pause/unpause`, which uses it to predict OpenZeppelin's
608
- * ExpectedPause / EnforcedPause revert.
609
- */
610
- async function isPaused(provider, eligibilityModuleAddress) {
611
- const contract = (0, contracts_1.createReadContract)(eligibilityModuleAddress, 'EligibilityModuleNew', provider);
612
- return Boolean(await contract.paused());
613
- }
614
- /**
615
- * Port of `pop vouch list` — src/commands/vouch/list.ts. Pure subgraph
616
- * (FETCH_VOUCHES_FOR_ORG); display only, never a quorum decision (see the
617
- * document's isActive epoch note in graph/documents/vouch.ts). `hatId` filter
618
- * is applied client-side by string equality, exactly as the CLI does.
619
- */
620
- async function listVouches(client, eligibilityModuleAddress, opts = {}) {
621
- const result = await client.query(vouch_1.FETCH_VOUCHES_FOR_ORG, { eligibilityModuleId: eligibilityModuleAddress }, opts.chainId);
622
- let vouches = result.vouches || [];
623
- if (opts.hatId) {
624
- vouches = vouches.filter((v) => v.hatId === opts.hatId);
625
- }
626
- return vouches;
627
- }
628
- /**
629
- * Port of `pop role applications` — src/commands/role/applications.ts.
630
- * Pure subgraph: FETCH_USER_ROLE_APPLICATIONS when `applicant` is given
631
- * (lowercased, as the CLI does), else FETCH_ALL_ROLE_APPLICATIONS — both
632
- * filtered by eligibilityModuleId. `hatId` filter is applied client-side by
633
- * string equality, exactly as the CLI does.
634
- */
635
- async function listRoleApplications(client, eligibilityModuleAddress, opts = {}) {
636
- const result = opts.applicant
637
- ? await client.query(role_1.FETCH_USER_ROLE_APPLICATIONS, { eligibilityModuleId: eligibilityModuleAddress, applicant: opts.applicant.toLowerCase() }, opts.chainId)
638
- : await client.query(role_1.FETCH_ALL_ROLE_APPLICATIONS, { eligibilityModuleId: eligibilityModuleAddress }, opts.chainId);
639
- let applications = result.roleApplications || [];
640
- if (opts.hatId) {
641
- applications = applications.filter((a) => a.hatId === opts.hatId);
642
- }
643
- return applications;
644
- }
645
- /**
646
- * Resolve a role name to its hat ID by querying the org's roles.
647
- * Port of `pop vouch for`'s resolveRoleToHatId — src/commands/vouch/for.ts —
648
- * minus the TTY disambiguation select (CLI UX): exact match first, then
649
- * partial; multiple matches always fail with the candidate list.
650
- */
651
- async function resolveRoleNameToHatId(client, orgIdOrName, roleName, chainId) {
652
- const orgId = await (0, resolve_1.resolveOrgId)(client, orgIdOrName, chainId);
653
- const query = `{
654
- organization(id: "${orgId}") {
655
- roles(where: { isUserRole: true }) { hatId name }
656
- }
657
- }`;
658
- const results = await client.queryAllChains(query, {});
659
- const matches = [];
660
- for (const r of results) {
661
- const roles = r.data?.organization?.roles || [];
662
- for (const role of roles) {
663
- if (role.name && role.name.toLowerCase() === roleName.toLowerCase()) {
664
- matches.push({ hatId: role.hatId, name: role.name });
665
- }
666
- }
667
- }
668
- if (matches.length === 0) {
669
- // Try partial match
670
- for (const r of results) {
671
- const roles = r.data?.organization?.roles || [];
672
- for (const role of roles) {
673
- if (role.name && role.name.toLowerCase().includes(roleName.toLowerCase())) {
674
- matches.push({ hatId: role.hatId, name: role.name });
675
- }
676
- }
677
- }
678
- }
679
- if (matches.length === 0) {
680
- throw new errors_1.CliError(`No role named "${roleName}" found.`, exit_codes_1.EXIT.USAGE, 'Use pop org roles to list available roles.');
681
- }
682
- if (matches.length > 1) {
683
- const names = matches.map(m => `${m.name} (${m.hatId})`).join(', ');
684
- throw new errors_1.CliError(`Multiple roles match "${roleName}": ${names}.`, exit_codes_1.EXIT.USAGE, 'Use --hat with the specific hat ID.');
685
- }
686
- return matches[0].hatId;
687
- }