@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,911 @@
1
+ "use strict";
2
+ /**
3
+ * Treasury transaction builders — ports of `pop treasury *` write paths
4
+ * (src/commands/treasury/).
5
+ *
6
+ * Direct writes (Level 1, PaymentManager / ERC20):
7
+ * buildApproveErc20 + buildPayErc20 — pop treasury deposit (two txs)
8
+ * buildClaimDistribution — pop treasury claim / claim-mine
9
+ * buildOptOut — pop treasury opt-out / opt-in
10
+ *
11
+ * Governance wraps (option-0 execution batch on HybridVoting, via ./governance):
12
+ * buildSendProposal — pop treasury send
13
+ * buildDistributionProposal — pop treasury propose-distribution
14
+ * buildSwapProposal — pop treasury propose-swap
15
+ * buildSdaiProposal — pop treasury propose-sdai
16
+ * buildFinalizeDistributionProposal — pop treasury propose-finalize
17
+ *
18
+ * Merkle machinery: the PT-proportional allocation + OZ StandardMerkleTree
19
+ * logic that was DUPLICATED between compute-merkle.ts (builder) and
20
+ * claim-mine.ts (reconstructor) lives here once, as pure functions.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.SDAI = exports.WXDAI = exports.GNOSIS_CHAIN_ID = void 0;
24
+ exports.buildApproveErc20 = buildApproveErc20;
25
+ exports.buildPayErc20 = buildPayErc20;
26
+ exports.buildClaimDistribution = buildClaimDistribution;
27
+ exports.buildOptOut = buildOptOut;
28
+ exports.parseRecipients = parseRecipients;
29
+ exports.parseProof = parseProof;
30
+ exports.parseClaimAmount = parseClaimAmount;
31
+ exports.computeDistributionAllocations = computeDistributionAllocations;
32
+ exports.buildDistributionTree = buildDistributionTree;
33
+ exports.getAllocationProof = getAllocationProof;
34
+ exports.buildSendProposal = buildSendProposal;
35
+ exports.buildDistributionProposal = buildDistributionProposal;
36
+ exports.buildSwapProposal = buildSwapProposal;
37
+ exports.buildSdaiProposal = buildSdaiProposal;
38
+ exports.buildFinalizeDistributionProposal = buildFinalizeDistributionProposal;
39
+ exports.sendProposalIntent = sendProposalIntent;
40
+ exports.proposeDistributionIntent = proposeDistributionIntent;
41
+ exports.proposeSwapIntent = proposeSwapIntent;
42
+ exports.proposeSdaiIntent = proposeSdaiIntent;
43
+ exports.proposeFinalizeIntent = proposeFinalizeIntent;
44
+ exports.depositIntents = depositIntents;
45
+ exports.claimDistributionIntent = claimDistributionIntent;
46
+ exports.optOutIntent = optOutIntent;
47
+ const ethers_1 = require("ethers");
48
+ const merkle_tree_1 = require("@openzeppelin/merkle-tree");
49
+ const contracts_1 = require("../contracts");
50
+ const governance_1 = require("./governance");
51
+ const resolve_1 = require("../reads/resolve");
52
+ const treasury_1 = require("../reads/treasury");
53
+ const proposal_1 = require("../metadata/proposal");
54
+ const ipfs_1 = require("../ipfs");
55
+ const encoding_1 = require("../encoding");
56
+ const validation_1 = require("../validation");
57
+ const chains_1 = require("../chains");
58
+ const format_1 = require("../format");
59
+ const errors_1 = require("../errors");
60
+ const exit_codes_1 = require("../exit-codes");
61
+ // ---------------------------------------------------------------------------
62
+ // Inline ABI fragments — identical to the CLI's, so calldata is byte-for-byte
63
+ // ---------------------------------------------------------------------------
64
+ const ERC20_TRANSFER_ABI = ['function transfer(address to, uint256 amount) returns (bool)'];
65
+ const ERC20_APPROVE_ABI = ['function approve(address spender, uint256 amount) returns (bool)'];
66
+ const CURVE_EXCHANGE_ABI = ['function exchange(int128 i, int128 j, uint256 dx, uint256 min_dy) returns (uint256)'];
67
+ const PM_CREATE_DISTRIBUTION_ABI = [
68
+ 'function createDistribution(address payoutToken, uint256 amount, bytes32 merkleRoot, uint256 checkpointBlock) returns (uint256)',
69
+ ];
70
+ const PM_WITHDRAW_ABI = ['function withdraw(address token, address to, uint256 amount)'];
71
+ const PM_FINALIZE_ABI = ['function finalizeDistribution(uint256 distributionId, uint256 minClaimPeriodBlocks)'];
72
+ /** Gnosis mainnet WXDAI/sDAI — the propose-sdai strategy is Gnosis-only. */
73
+ exports.GNOSIS_CHAIN_ID = 100;
74
+ exports.WXDAI = '0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d';
75
+ exports.SDAI = '0xaf204776c7245bF4147c2612BF6e5972Ee483701';
76
+ const WXDAI_IFACE = new ethers_1.ethers.utils.Interface([
77
+ 'function deposit() payable',
78
+ 'function approve(address spender, uint256 amount) returns (bool)',
79
+ ]);
80
+ const SDAI_IFACE = new ethers_1.ethers.utils.Interface([
81
+ 'function deposit(uint256 assets, address receiver) returns (uint256 shares)',
82
+ 'function balanceOf(address) view returns (uint256)',
83
+ 'function convertToAssets(uint256 shares) view returns (uint256)',
84
+ ]);
85
+ /**
86
+ * Port of `pop treasury deposit` step 1 — src/commands/treasury/deposit.ts:
87
+ * ERC20.approve(paymentManagerAddress, amountWei).
88
+ */
89
+ function buildApproveErc20(a) {
90
+ return {
91
+ to: a.tokenAddress,
92
+ abi: (0, contracts_1.getAbi)('ERC20'),
93
+ method: 'approve',
94
+ args: [a.spender, a.amountWei],
95
+ meta: {
96
+ domain: 'treasury',
97
+ action: 'deposit-approve',
98
+ orgId: a.orgId,
99
+ summary: { token: a.tokenAddress, spender: a.spender, amountWei: String(a.amountWei) },
100
+ },
101
+ };
102
+ }
103
+ /**
104
+ * Port of `pop treasury deposit` step 2 — src/commands/treasury/deposit.ts:
105
+ * PaymentManager.payERC20(token, amountWei) (pulls via transferFrom, emits
106
+ * PaymentReceived). DESTRUCTIVE for the depositor: withdrawing later requires
107
+ * a governance vote.
108
+ */
109
+ function buildPayErc20(a) {
110
+ return {
111
+ to: a.paymentManagerAddress,
112
+ abi: (0, contracts_1.getAbi)('PaymentManager'),
113
+ method: 'payERC20',
114
+ args: [a.tokenAddress, a.amountWei],
115
+ meta: {
116
+ domain: 'treasury',
117
+ action: 'deposit',
118
+ orgId: a.orgId,
119
+ summary: { token: a.tokenAddress, amountWei: String(a.amountWei) },
120
+ },
121
+ };
122
+ }
123
+ /**
124
+ * Port of `pop treasury claim` / `pop treasury claim-mine` —
125
+ * src/commands/treasury/claim.ts, claim-mine.ts:
126
+ * PaymentManager.claimDistribution(distId, amountWei, proof).
127
+ *
128
+ * Host-side revert predictors the CLI runs before this (deliberately NOT in
129
+ * the builder — they need a provider and gate broadcasting, not encoding):
130
+ * finalized, hasClaimed(dist, addr), isOptedOut(addr). Opt-out BLOCKS on the
131
+ * deployed PaymentManager (pre-L-19 vintage still reverts OptedOut).
132
+ */
133
+ function buildClaimDistribution(a) {
134
+ return {
135
+ to: a.paymentManagerAddress,
136
+ abi: (0, contracts_1.getAbi)('PaymentManager'),
137
+ method: 'claimDistribution',
138
+ args: [a.distributionId, a.amountWei, a.proof],
139
+ meta: {
140
+ domain: 'treasury',
141
+ action: 'claim',
142
+ orgId: a.orgId,
143
+ summary: { distributionId: String(a.distributionId), amountWei: String(a.amountWei) },
144
+ },
145
+ };
146
+ }
147
+ /**
148
+ * Port of `pop treasury opt-out` / `pop treasury opt-in` —
149
+ * src/commands/treasury/opt-out.ts: PaymentManager.optOut(bool).
150
+ * Permissionless and idempotent; the CLI's no-op short-circuit reads
151
+ * isOptedOut over RPC (deliberately never the subgraph — the OptOutToggle
152
+ * table is empty on every live deployment AND the read gates a write).
153
+ */
154
+ function buildOptOut(a) {
155
+ return {
156
+ to: a.paymentManagerAddress,
157
+ abi: (0, contracts_1.getAbi)('PaymentManager'),
158
+ method: 'optOut',
159
+ args: [a.optOut],
160
+ meta: {
161
+ domain: 'treasury',
162
+ action: a.optOut ? 'opt-out' : 'opt-in',
163
+ orgId: a.orgId,
164
+ summary: { optedOut: a.optOut },
165
+ },
166
+ };
167
+ }
168
+ /**
169
+ * Parse recipients into a validated list (max 8 — the Executor batch cap).
170
+ * Port of parseRecipients in src/commands/treasury/send.ts; accepts the
171
+ * already-parsed array or the CLI's raw --recipients JSON string.
172
+ */
173
+ function parseRecipients(input) {
174
+ let recipientList;
175
+ if (input.recipients) {
176
+ if (typeof input.recipients === 'string') {
177
+ try {
178
+ recipientList = JSON.parse(input.recipients);
179
+ if (!Array.isArray(recipientList) || recipientList.length === 0)
180
+ throw new Error('empty array');
181
+ }
182
+ catch (e) {
183
+ throw new errors_1.CliError(`--recipients must be a JSON array: [{"to":"0x...","amount":5},...]. ${e.message}`, exit_codes_1.EXIT.USAGE);
184
+ }
185
+ }
186
+ else {
187
+ recipientList = input.recipients;
188
+ if (!Array.isArray(recipientList) || recipientList.length === 0) {
189
+ throw new errors_1.CliError('--recipients must be a JSON array: [{"to":"0x...","amount":5},...]. empty array', exit_codes_1.EXIT.USAGE);
190
+ }
191
+ }
192
+ if (recipientList.length > 8) {
193
+ throw new errors_1.CliError('The Executor supports at most 8 calls per batch — split into multiple proposals.', exit_codes_1.EXIT.USAGE);
194
+ }
195
+ }
196
+ else if (input.to && input.amount) {
197
+ recipientList = [{ to: input.to, amount: input.amount }];
198
+ }
199
+ else {
200
+ throw new errors_1.CliError('Provide either --to + --amount, or --recipients for a batch send.', exit_codes_1.EXIT.USAGE);
201
+ }
202
+ return recipientList.map((r, i) => {
203
+ const to = (0, validation_1.requireAddress)(r.to, `recipients[${i}].to`);
204
+ const amount = Number(r.amount);
205
+ if (!Number.isFinite(amount) || amount <= 0) {
206
+ throw new errors_1.CliError(`Recipient ${to}: amount must be a positive number, got "${r.amount}".`, exit_codes_1.EXIT.USAGE);
207
+ }
208
+ return { to, amount };
209
+ });
210
+ }
211
+ /**
212
+ * Validate a merkle proof array — port of the shape check in parseProof
213
+ * (src/commands/treasury/claim.ts), minus the fs half. Accepts the array or
214
+ * the CLI's inline JSON string; the allocation-object form ({ proof: [...] })
215
+ * is unwrapped like the CLI's --proof-file path.
216
+ */
217
+ function parseProof(input) {
218
+ let raw = input;
219
+ if (typeof input === 'string') {
220
+ try {
221
+ raw = JSON.parse(input);
222
+ }
223
+ catch {
224
+ throw new errors_1.CliError('--proof must be a JSON array of bytes32 hex strings.', exit_codes_1.EXIT.USAGE);
225
+ }
226
+ }
227
+ if (raw && !Array.isArray(raw) && Array.isArray(raw.proof))
228
+ raw = raw.proof; // allocation-object form
229
+ if (!Array.isArray(raw) || raw.some((p) => typeof p !== 'string' || !/^0x[0-9a-fA-F]{64}$/.test(p))) {
230
+ throw new errors_1.CliError('The merkle proof must be a JSON array of bytes32 hex strings.', exit_codes_1.EXIT.USAGE, 'Each entry looks like 0x followed by 64 hex characters (see merkle-distribution.json from compute-merkle).');
231
+ }
232
+ return raw;
233
+ }
234
+ /**
235
+ * Convert an amount to the raw claim value: decimal token units, or raw wei
236
+ * with `wei`. Port of parseClaimAmount in src/commands/treasury/claim.ts.
237
+ * The merkle leaf hashes the raw amount, so exactness matters.
238
+ */
239
+ function parseClaimAmount(amount, wei, decimals) {
240
+ const trimmed = String(amount).trim();
241
+ if (wei) {
242
+ if (!/^\d+$/.test(trimmed)) {
243
+ throw new errors_1.CliError(`--wei expects a raw integer amount, got "${amount}".`, exit_codes_1.EXIT.USAGE, 'Drop --wei to pass decimal token units instead.');
244
+ }
245
+ return ethers_1.ethers.BigNumber.from(trimmed);
246
+ }
247
+ try {
248
+ return ethers_1.ethers.utils.parseUnits(trimmed, decimals);
249
+ }
250
+ catch {
251
+ throw new errors_1.CliError(`Invalid --amount "${amount}" for a token with ${decimals} decimals.`, exit_codes_1.EXIT.USAGE, 'Pass a decimal token amount (e.g. 12.5), or the exact raw integer with --wei.');
252
+ }
253
+ }
254
+ /**
255
+ * PT-proportional allocations — the shared rule of compute-merkle.ts (builder)
256
+ * and claim-mine.ts (reconstructor):
257
+ * - eligible = membershipStatus 'Active' AND PT > 0 AND not opted out
258
+ * - allocation = totalAmount * memberPT / eligiblePT
259
+ * - rounding dust goes to the FIRST member — callers must pass members
260
+ * ordered by PT balance desc (both member queries do), so that is the
261
+ * largest holder.
262
+ *
263
+ * Throws with the CLI's messages when no one is eligible.
264
+ */
265
+ function computeDistributionAllocations(members, totalAmountWei, optedOut = new Set()) {
266
+ const totalAmount = ethers_1.ethers.BigNumber.from(totalAmountWei);
267
+ const ptHolders = members.filter((u) => u.membershipStatus === 'Active' &&
268
+ ethers_1.ethers.BigNumber.from(u.participationTokenBalance).gt(0));
269
+ if (ptHolders.length === 0) {
270
+ throw new Error('No active members with PT balance');
271
+ }
272
+ const activeMembers = ptHolders.filter((m) => !optedOut.has(String(m.address).toLowerCase()));
273
+ if (activeMembers.length === 0) {
274
+ throw new Error(optedOut.size > 0
275
+ ? `All ${ptHolders.length} PT-holding member(s) have opted out of payouts.`
276
+ : 'No active members with PT balance');
277
+ }
278
+ // Sum PT of eligible members (may differ from totalSupply if some are inactive)
279
+ const eligiblePT = activeMembers.reduce((sum, m) => sum.add(ethers_1.ethers.BigNumber.from(m.participationTokenBalance)), ethers_1.ethers.BigNumber.from(0));
280
+ const allocations = activeMembers.map((m) => {
281
+ const ptBal = ethers_1.ethers.BigNumber.from(m.participationTokenBalance);
282
+ return {
283
+ address: ethers_1.ethers.utils.getAddress(m.address),
284
+ username: m.username ?? null,
285
+ ptBalance: ptBal,
286
+ // Pro-rata: allocation = totalAmount * memberPT / eligiblePT
287
+ amount: totalAmount.mul(ptBal).div(eligiblePT),
288
+ };
289
+ });
290
+ // Handle rounding dust — give remainder to largest holder (index 0).
291
+ const allocatedTotal = allocations.reduce((sum, a) => sum.add(a.amount), ethers_1.ethers.BigNumber.from(0));
292
+ const dust = totalAmount.sub(allocatedTotal);
293
+ if (dust.gt(0) && allocations.length > 0) {
294
+ allocations[0] = { ...allocations[0], amount: allocations[0].amount.add(dust) };
295
+ }
296
+ return allocations;
297
+ }
298
+ /**
299
+ * Build the OZ StandardMerkleTree over ['address', 'uint256'] leaves —
300
+ * exactly what compute-merkle.ts commits and claim-mine.ts reconstructs.
301
+ * StandardMerkleTree hashes each value pair itself (double-keccak over
302
+ * abi.encode) and sorts leaves internally, so proofs are looked up by the
303
+ * ORIGINAL value, not by index.
304
+ */
305
+ function buildDistributionTree(allocations) {
306
+ return merkle_tree_1.StandardMerkleTree.of(allocations.map((a) => [a.address, a.amount.toString()]), ['address', 'uint256']);
307
+ }
308
+ /** Proof for one allocation — looked up by VALUE (OZ sorts leaves internally). */
309
+ function getAllocationProof(tree, allocation) {
310
+ return tree.getProof([allocation.address, allocation.amount.toString()]);
311
+ }
312
+ /**
313
+ * Port of `pop treasury send` — src/commands/treasury/send.ts.
314
+ * HybridVoting.createProposal wrapping an option-0 batch of at most 8 calls:
315
+ * native transfers as [to, amountWei, '0x'], ERC20 as
316
+ * [token, 0, transfer(to, amountWei)]. Option 1 is a no-op.
317
+ */
318
+ function buildSendProposal(a) {
319
+ const isNative = a.tokenAddress == null;
320
+ const iface = new ethers_1.ethers.utils.Interface(ERC20_TRANSFER_ABI);
321
+ const calls = [];
322
+ let totalAmount = 0;
323
+ for (const r of a.recipients) {
324
+ const amountWei = ethers_1.ethers.utils.parseUnits(r.amount.toString(), a.decimals);
325
+ totalAmount += r.amount;
326
+ if (isNative) {
327
+ calls.push({ target: r.to, value: amountWei, calldata: '0x' });
328
+ }
329
+ else {
330
+ calls.push({
331
+ target: a.tokenAddress,
332
+ value: ethers_1.ethers.BigNumber.from(0),
333
+ calldata: iface.encodeFunctionData('transfer', [r.to, amountWei]),
334
+ });
335
+ }
336
+ }
337
+ return (0, governance_1.buildGovernanceProposal)({
338
+ votingAddress: a.hybridVotingAddress,
339
+ votingAbiName: 'HybridVotingNew',
340
+ title: `Send ${totalAmount} ${a.tokenLabel} to ${a.recipients.length} recipient(s)`,
341
+ descriptionHash: a.descriptionHash,
342
+ durationMinutes: a.durationMinutes,
343
+ numOptions: 2,
344
+ batches: [calls, []], // option 0 = send, option 1 = no-op
345
+ hatIds: [],
346
+ domain: 'treasury',
347
+ action: 'send',
348
+ orgId: a.orgId,
349
+ summary: {
350
+ totalAmount: totalAmount.toString(),
351
+ token: a.tokenLabel,
352
+ recipients: a.recipients.length,
353
+ },
354
+ ipfs: a.ipfs,
355
+ });
356
+ }
357
+ /**
358
+ * Port of `pop treasury propose-distribution` —
359
+ * src/commands/treasury/propose-distribution.ts. HybridVoting.createProposal
360
+ * wrapping PaymentManager.createDistribution(payoutToken, amount, merkleRoot,
361
+ * checkpointBlock) — an executor-only call, hence the governance wrap.
362
+ * createDistribution reverts InsufficientFunds at EXECUTION time if the
363
+ * PaymentManager's balance can't cover total committed + this amount.
364
+ */
365
+ function buildDistributionProposal(a) {
366
+ const pmIface = new ethers_1.ethers.utils.Interface(PM_CREATE_DISTRIBUTION_ABI);
367
+ const createDistData = pmIface.encodeFunctionData('createDistribution', [
368
+ a.tokenAddress,
369
+ a.totalAmountWei,
370
+ a.merkleRoot,
371
+ a.checkpointBlock,
372
+ ]);
373
+ const calls = [
374
+ { target: a.paymentManagerAddress, value: ethers_1.ethers.BigNumber.from(0), calldata: createDistData },
375
+ ];
376
+ return (0, governance_1.buildGovernanceProposal)({
377
+ votingAddress: a.hybridVotingAddress,
378
+ votingAbiName: 'HybridVotingNew',
379
+ title: `Distribute ${a.totalHuman} tokens to ${a.memberCount} members`,
380
+ descriptionHash: a.descriptionHash,
381
+ durationMinutes: a.durationMinutes,
382
+ numOptions: 2,
383
+ batches: [calls, []], // option 0 = distribute, option 1 = no-op
384
+ hatIds: [],
385
+ domain: 'treasury',
386
+ action: 'propose-distribution',
387
+ orgId: a.orgId,
388
+ summary: {
389
+ tokenAddress: a.tokenAddress,
390
+ totalAmountWei: String(a.totalAmountWei),
391
+ merkleRoot: a.merkleRoot,
392
+ checkpointBlock: String(a.checkpointBlock),
393
+ memberCount: a.memberCount,
394
+ },
395
+ ipfs: a.ipfs,
396
+ });
397
+ }
398
+ /**
399
+ * Port of `pop treasury propose-swap` — src/commands/treasury/propose-swap.ts.
400
+ * HybridVoting.createProposal wrapping THREE option-0 calls:
401
+ * 1. PaymentManager.withdraw(fromToken, executor, amountWei) (executor-only)
402
+ * 2. ERC20.approve(pool, amountWei)
403
+ * 3. CurvePool.exchange(int128 i, int128 j, dx, min_dy)
404
+ */
405
+ function buildSwapProposal(a) {
406
+ const withdrawIface = new ethers_1.ethers.utils.Interface(PM_WITHDRAW_ABI);
407
+ const erc20Iface = new ethers_1.ethers.utils.Interface(ERC20_APPROVE_ABI);
408
+ const curveIface = new ethers_1.ethers.utils.Interface(CURVE_EXCHANGE_ABI);
409
+ const calls = [
410
+ {
411
+ target: a.paymentManagerAddress,
412
+ value: ethers_1.ethers.BigNumber.from(0),
413
+ calldata: withdrawIface.encodeFunctionData('withdraw', [a.fromToken, a.executorAddress, a.amountWei]),
414
+ },
415
+ {
416
+ target: a.fromToken,
417
+ value: ethers_1.ethers.BigNumber.from(0),
418
+ calldata: erc20Iface.encodeFunctionData('approve', [a.poolAddress, a.amountWei]),
419
+ },
420
+ {
421
+ target: a.poolAddress,
422
+ value: ethers_1.ethers.BigNumber.from(0),
423
+ calldata: curveIface.encodeFunctionData('exchange', [a.fromIndex, a.toIndex, a.amountWei, a.minOut]),
424
+ },
425
+ ];
426
+ return (0, governance_1.buildGovernanceProposal)({
427
+ votingAddress: a.hybridVotingAddress,
428
+ votingAbiName: 'HybridVotingNew',
429
+ title: `Treasury swap: ${a.amountLabel} tokens via Curve`,
430
+ descriptionHash: a.descriptionHash,
431
+ durationMinutes: a.durationMinutes,
432
+ numOptions: 2,
433
+ batches: [calls, []], // option 0 = swap, option 1 = no-op
434
+ hatIds: [],
435
+ domain: 'treasury',
436
+ action: 'propose-swap',
437
+ orgId: a.orgId,
438
+ summary: {
439
+ fromToken: a.fromToken,
440
+ pool: a.poolAddress,
441
+ amountWei: String(a.amountWei),
442
+ minOut: String(a.minOut),
443
+ },
444
+ ipfs: a.ipfs,
445
+ });
446
+ }
447
+ /**
448
+ * Port of `pop treasury propose-sdai` — src/commands/treasury/propose-sdai.ts.
449
+ * Gnosis-only (the WXDAI/SDAI constants are Gnosis mainnet deployments) —
450
+ * callers must enforce chainId === 100 (the Level-2 builder does).
451
+ * Option 0 executes three calls from the Executor:
452
+ * 1. [WXDAI, amountWei, deposit()] (wraps the Executor's xDAI)
453
+ * 2. [WXDAI, 0, approve(SDAI, amountWei)]
454
+ * 3. [SDAI, 0, deposit(amountWei, executor)]
455
+ */
456
+ function buildSdaiProposal(a) {
457
+ const calls = [
458
+ { target: exports.WXDAI, value: a.amountWei, calldata: WXDAI_IFACE.encodeFunctionData('deposit', []) },
459
+ { target: exports.WXDAI, value: ethers_1.ethers.BigNumber.from(0), calldata: WXDAI_IFACE.encodeFunctionData('approve', [exports.SDAI, a.amountWei]) },
460
+ { target: exports.SDAI, value: ethers_1.ethers.BigNumber.from(0), calldata: SDAI_IFACE.encodeFunctionData('deposit', [a.amountWei, a.executorAddress]) },
461
+ ];
462
+ return (0, governance_1.buildGovernanceProposal)({
463
+ votingAddress: a.hybridVotingAddress,
464
+ votingAbiName: 'HybridVotingNew',
465
+ title: `Deposit ${a.amountLabel} xDAI into sDAI for yield`,
466
+ descriptionHash: a.descriptionHash,
467
+ durationMinutes: a.durationMinutes,
468
+ numOptions: 2,
469
+ batches: [calls, []], // option 0 = deposit, option 1 = keep liquid
470
+ hatIds: [],
471
+ domain: 'treasury',
472
+ action: 'propose-sdai',
473
+ orgId: a.orgId,
474
+ summary: { amountWei: String(a.amountWei), executor: a.executorAddress },
475
+ ipfs: a.ipfs,
476
+ });
477
+ }
478
+ /**
479
+ * Port of `pop treasury propose-finalize` —
480
+ * src/commands/treasury/propose-finalize.ts. HybridVoting.createProposal
481
+ * wrapping PaymentManager.finalizeDistribution(distId, minClaimPeriodBlocks)
482
+ * (onlyOwner = the Executor, hence the wrap). On success the unclaimed
483
+ * remainder returns to the Executor treasury and further claims are blocked.
484
+ */
485
+ function buildFinalizeDistributionProposal(a) {
486
+ const pmIface = new ethers_1.ethers.utils.Interface(PM_FINALIZE_ABI);
487
+ const finalizeCall = pmIface.encodeFunctionData('finalizeDistribution', [a.distributionId, a.minClaimPeriodBlocks]);
488
+ const calls = [
489
+ { target: a.paymentManagerAddress, value: ethers_1.ethers.BigNumber.from(0), calldata: finalizeCall },
490
+ ];
491
+ return (0, governance_1.buildGovernanceProposal)({
492
+ votingAddress: a.hybridVotingAddress,
493
+ votingAbiName: 'HybridVotingNew',
494
+ title: `Finalize distribution #${a.distributionId}`,
495
+ descriptionHash: a.descriptionHash,
496
+ durationMinutes: a.durationMinutes,
497
+ numOptions: 2,
498
+ batches: [calls, []], // option 0: finalize; option 1: keep open
499
+ hatIds: [],
500
+ domain: 'treasury',
501
+ action: 'propose-finalize',
502
+ orgId: a.orgId,
503
+ summary: { distributionId: a.distributionId, minClaimPeriodBlocks: a.minClaimPeriodBlocks },
504
+ ipfs: a.ipfs,
505
+ });
506
+ }
507
+ // ---------------------------------------------------------------------------
508
+ // Level 2 — resolved builders
509
+ // ---------------------------------------------------------------------------
510
+ async function pinProposalMetadata(ctx, metadata) {
511
+ const cid = await (0, ipfs_1.pinJson)((0, proposal_1.serializeProposalMetadata)(metadata), ctx.ipfs);
512
+ return { cid, descriptionHash: (0, encoding_1.ipfsCidToBytes32)(cid) };
513
+ }
514
+ /**
515
+ * Resolved port of `pop treasury send` — src/commands/treasury/send.ts.
516
+ * Resolves the org's HybridVoting, derives token label/decimals, pins the
517
+ * proposal metadata {description, optionNames, createdAt}, and delegates to
518
+ * buildSendProposal.
519
+ */
520
+ async function sendProposalIntent(ctx, params) {
521
+ const token = params.token ?? 'native';
522
+ const isNative = token === 'native';
523
+ const tokenAddr = isNative ? null : (0, validation_1.requireAddress)(token, 'token');
524
+ const recipientList = parseRecipients(params);
525
+ const durationMinutes = params.durationMinutes ?? 60;
526
+ const modules = await (0, resolve_1.resolveOrgModules)(ctx.client, params.org, ctx.chainId);
527
+ const hybridVotingAddr = modules.hybridVotingAddress;
528
+ if (!hybridVotingAddr) {
529
+ throw new errors_1.PreconditionError('HybridVoting not deployed for this org — cannot create a governance proposal.');
530
+ }
531
+ const network = (0, chains_1.resolveNetworkConfig)(ctx.chainId, ctx.env);
532
+ const tokenLabel = isNative
533
+ ? (network?.nativeCurrency?.symbol ?? 'native')
534
+ : tokenAddr;
535
+ const decimals = isNative ? 18 : (0, chains_1.getTokenDecimals)(tokenAddr);
536
+ const totalAmount = recipientList.reduce((sum, r) => sum + r.amount, 0);
537
+ const recipientSummary = recipientList.map(r => `${r.amount} ${tokenLabel} → ${r.to.slice(0, 10)}...`).join(', ');
538
+ const metadata = (0, proposal_1.buildProposalMetadata)({
539
+ description: `Transfer ${totalAmount} ${tokenLabel} from Executor: ${recipientSummary}. ${recipientList.length} recipient(s).`,
540
+ optionNames: [`Send ${totalAmount} ${tokenLabel} (${recipientList.length} recipients)`, 'Do not send'],
541
+ });
542
+ const { cid, descriptionHash } = await pinProposalMetadata(ctx, metadata);
543
+ return buildSendProposal({
544
+ hybridVotingAddress: hybridVotingAddr,
545
+ recipients: recipientList,
546
+ tokenAddress: tokenAddr,
547
+ decimals,
548
+ tokenLabel,
549
+ descriptionHash,
550
+ durationMinutes,
551
+ orgId: modules.orgId,
552
+ ipfs: { cid, metadata },
553
+ });
554
+ }
555
+ /**
556
+ * Resolved port of `pop treasury propose-distribution` —
557
+ * src/commands/treasury/propose-distribution.ts. Takes the parsed
558
+ * merkle-distribution.json (file I/O stays in the host), resolves modules and
559
+ * payout-token decimals, pins the metadata and delegates to
560
+ * buildDistributionProposal. The CLI's PaymentManager-balance check is an
561
+ * advisory warning only (InsufficientFunds fires at execution time) and is
562
+ * not ported.
563
+ */
564
+ async function proposeDistributionIntent(ctx, params) {
565
+ const { merkleRoot, totalAmount, tokenAddress, checkpointBlock, memberCount, allocations } = params.merkle ?? {};
566
+ if (!merkleRoot || !totalAmount || !tokenAddress || !checkpointBlock) {
567
+ throw new errors_1.CliError('Invalid merkle file — missing required fields (merkleRoot, totalAmount, tokenAddress, checkpointBlock).', exit_codes_1.EXIT.USAGE, 'Regenerate it with: pop treasury compute-merkle');
568
+ }
569
+ const durationMinutes = params.durationMinutes ?? 1440;
570
+ const modules = await (0, resolve_1.resolveOrgModules)(ctx.client, params.org, ctx.chainId);
571
+ const paymentManagerAddr = (0, resolve_1.requireModule)(modules, 'paymentManagerAddress');
572
+ const hybridVotingAddr = modules.hybridVotingAddress;
573
+ if (!hybridVotingAddr) {
574
+ throw new errors_1.PreconditionError('HybridVoting not deployed for this org — cannot create a governance proposal.');
575
+ }
576
+ const amountWei = ethers_1.ethers.BigNumber.from(totalAmount);
577
+ const token = await (0, treasury_1.resolvePayoutTokenInfo)(ctx.provider, tokenAddress, ctx.chainId);
578
+ const totalHuman = ethers_1.ethers.utils.formatUnits(amountWei, token.decimals);
579
+ // Build allocation summary for proposal description
580
+ const allocationSummary = (allocations ?? [])
581
+ .map((a) => `${a.username || a.address.slice(0, 10) + '...'} (${a.share})`)
582
+ .join(', ');
583
+ const metadata = (0, proposal_1.buildProposalMetadata)({
584
+ description: `Create distribution of ${totalHuman} tokens to ${memberCount} members proportional to PT holdings. Allocations: ${allocationSummary}. Merkle root: ${merkleRoot}. Checkpoint block: ${checkpointBlock}.`,
585
+ optionNames: [`Distribute ${totalHuman} tokens`, 'Do not distribute'],
586
+ });
587
+ const { cid, descriptionHash } = await pinProposalMetadata(ctx, metadata);
588
+ return buildDistributionProposal({
589
+ hybridVotingAddress: hybridVotingAddr,
590
+ paymentManagerAddress: paymentManagerAddr,
591
+ tokenAddress,
592
+ totalAmountWei: amountWei,
593
+ merkleRoot,
594
+ checkpointBlock,
595
+ memberCount,
596
+ totalHuman,
597
+ descriptionHash,
598
+ durationMinutes,
599
+ orgId: modules.orgId,
600
+ ipfs: { cid, metadata },
601
+ });
602
+ }
603
+ /**
604
+ * Resolved port of `pop treasury propose-swap` —
605
+ * src/commands/treasury/propose-swap.ts. Decimals come from the static
606
+ * known-token table only (no live read — same as the CLI); the pool quote
607
+ * (get_dy) requires ctx.provider and falls back exactly like the CLI when
608
+ * unavailable WITH a provider present: min_dy defaults to 95% of the quote,
609
+ * or 95% of the input when get_dy reverted — the CLI's exact fallback ladder.
610
+ * With NO provider and no explicit minOut this refuses instead: the CLI can
611
+ * never reach that state (it always has a provider), and silently deriving
612
+ * min_dy from the INPUT amount across a decimals mismatch would let a
613
+ * governance swap execute at a catastrophic rate.
614
+ */
615
+ async function proposeSwapIntent(ctx, params) {
616
+ if (!Number.isFinite(params.amount) || params.amount <= 0) {
617
+ throw new errors_1.CliError(`--amount must be a positive number, got "${params.amount}".`, exit_codes_1.EXIT.USAGE);
618
+ }
619
+ const fromToken = (0, validation_1.requireAddress)(params.fromToken, 'from-token');
620
+ const toToken = (0, validation_1.requireAddress)(params.toToken, 'to-token');
621
+ const poolAddr = (0, validation_1.requireAddress)(params.pool, 'pool');
622
+ const durationMinutes = params.durationMinutes ?? 1440;
623
+ const modules = await (0, resolve_1.resolveOrgModules)(ctx.client, params.org, ctx.chainId);
624
+ const executorAddr = (0, resolve_1.requireModule)(modules, 'executorAddress');
625
+ const pmAddr = (0, resolve_1.requireModule)(modules, 'paymentManagerAddress');
626
+ const hybridVotingAddr = modules.hybridVotingAddress;
627
+ if (!hybridVotingAddr) {
628
+ throw new errors_1.PreconditionError('HybridVoting not deployed for this org — cannot create a governance proposal.');
629
+ }
630
+ const fromDecimals = (0, chains_1.getTokenDecimals)(fromToken);
631
+ const toDecimals = (0, chains_1.getTokenDecimals)(toToken);
632
+ const amountWei = ethers_1.ethers.utils.parseUnits(params.amount.toString(), fromDecimals);
633
+ // Get a quote from the pool (RPC; quote unavailable → CLI's fallback path).
634
+ let expectedOut = null;
635
+ if (ctx.provider) {
636
+ try {
637
+ const poolContract = new ethers_1.ethers.Contract(poolAddr, [
638
+ 'function get_dy(int128 i, int128 j, uint256 dx) view returns (uint256)',
639
+ ], ctx.provider);
640
+ expectedOut = await poolContract.get_dy(params.fromIndex, params.toIndex, amountWei);
641
+ }
642
+ catch {
643
+ expectedOut = null; // quote unavailable — fall back below
644
+ }
645
+ }
646
+ if (params.minOut === undefined && !ctx.provider) {
647
+ throw new errors_1.PreconditionError('proposeSwapIntent needs a pool quote to derive min_dy safely — supply ctx.provider (get_dy read) or pass minOut explicitly.');
648
+ }
649
+ const minOut = params.minOut !== undefined
650
+ ? ethers_1.ethers.utils.parseUnits(params.minOut.toString(), toDecimals)
651
+ : (expectedOut ?? amountWei).mul(95).div(100); // 5% slippage default (get_dy reverted — CLI parity)
652
+ const expectedLabel = expectedOut ? ethers_1.ethers.utils.formatUnits(expectedOut, toDecimals) : 'unavailable';
653
+ const metadata = (0, proposal_1.buildProposalMetadata)({
654
+ description: `Swap ${params.amount} tokens via Curve pool ${poolAddr}. Withdraws from PaymentManager, approves pool, executes swap. Expected output: ~${expectedLabel}. Min output: ${ethers_1.ethers.utils.formatUnits(minOut, toDecimals)}.`,
655
+ optionNames: [`Execute swap (${params.amount} tokens)`, 'Do not swap'],
656
+ });
657
+ const { cid, descriptionHash } = await pinProposalMetadata(ctx, metadata);
658
+ return buildSwapProposal({
659
+ hybridVotingAddress: hybridVotingAddr,
660
+ paymentManagerAddress: pmAddr,
661
+ executorAddress: executorAddr,
662
+ fromToken,
663
+ poolAddress: poolAddr,
664
+ fromIndex: params.fromIndex,
665
+ toIndex: params.toIndex,
666
+ amountWei,
667
+ minOut,
668
+ amountLabel: params.amount,
669
+ descriptionHash,
670
+ durationMinutes,
671
+ orgId: modules.orgId,
672
+ ipfs: { cid, metadata },
673
+ });
674
+ }
675
+ /**
676
+ * Resolved port of `pop treasury propose-sdai` —
677
+ * src/commands/treasury/propose-sdai.ts. Gnosis-only. REQUIRES ctx.provider:
678
+ * the proposal metadata interpolates the Executor's live xDAI balance and
679
+ * current sDAI holdings, so the exact CLI document cannot be built without the
680
+ * reads. Replicates the CLI's Executor-balance precondition (execution would
681
+ * fail if the wrap call can't be funded).
682
+ */
683
+ async function proposeSdaiIntent(ctx, params) {
684
+ const amount = params.amount;
685
+ if (!Number.isFinite(amount) || amount <= 0) {
686
+ throw new errors_1.CliError(`--amount must be a positive number, got "${params.amount}".`, exit_codes_1.EXIT.USAGE);
687
+ }
688
+ const durationMinutes = params.durationMinutes ?? 60;
689
+ const network = (0, chains_1.resolveNetworkConfig)(ctx.chainId, ctx.env);
690
+ if (network.chainId !== exports.GNOSIS_CHAIN_ID) {
691
+ throw new errors_1.PreconditionError(`The sDAI strategy is Gnosis-only (WXDAI/sDAI addresses are Gnosis mainnet); current chain is ${network.name}.`, 'Re-run with --chain 100.');
692
+ }
693
+ if (!ctx.provider) {
694
+ throw new errors_1.CliError('proposeSdaiIntent requires ctx.provider — the proposal metadata embeds the Executor\'s live xDAI balance and sDAI holdings.', exit_codes_1.EXIT.USAGE);
695
+ }
696
+ const modules = await (0, resolve_1.resolveOrgModules)(ctx.client, params.org, ctx.chainId);
697
+ const executorAddr = (0, resolve_1.requireModule)(modules, 'executorAddress');
698
+ const hybridVotingAddr = modules.hybridVotingAddress;
699
+ if (!hybridVotingAddr) {
700
+ throw new errors_1.PreconditionError('HybridVoting not deployed for this org — cannot create a governance proposal.');
701
+ }
702
+ const amountWei = ethers_1.ethers.utils.parseEther(amount.toString());
703
+ // The wrap call spends the Executor's native balance at execution time.
704
+ const execBalance = await ctx.provider.getBalance(executorAddr);
705
+ if (execBalance.lt(amountWei)) {
706
+ throw new errors_1.PreconditionError(`Executor holds ${(0, format_1.formatToken)(execBalance, 18, 'xDAI')} but the deposit needs ${amount} xDAI — execution would fail.`, 'Lower --amount or fund the treasury first.');
707
+ }
708
+ // Current sDAI holdings (context for the proposal metadata).
709
+ const sdaiContract = new ethers_1.ethers.Contract(exports.SDAI, SDAI_IFACE.fragments, ctx.provider);
710
+ const currentShares = await sdaiContract.balanceOf(executorAddr);
711
+ const currentAssets = currentShares.gt(0)
712
+ ? await sdaiContract.convertToAssets(currentShares)
713
+ : ethers_1.ethers.BigNumber.from(0);
714
+ const metadata = (0, proposal_1.buildProposalMetadata)({
715
+ description: `Deposit ${amount} xDAI into sDAI (${exports.SDAI}) for yield. Three execution steps: wrap xDAI to WXDAI, approve WXDAI, deposit into sDAI vault. Current sDAI holdings: ${ethers_1.ethers.utils.formatEther(currentShares)} shares (${ethers_1.ethers.utils.formatEther(currentAssets)} WXDAI equivalent). Executor retains ${ethers_1.ethers.utils.formatEther(execBalance.sub(amountWei))} xDAI liquid.`,
716
+ optionNames: [`Deposit ${amount} xDAI into sDAI`, 'Keep xDAI liquid'],
717
+ });
718
+ const { cid, descriptionHash } = await pinProposalMetadata(ctx, metadata);
719
+ return buildSdaiProposal({
720
+ hybridVotingAddress: hybridVotingAddr,
721
+ executorAddress: executorAddr,
722
+ amountWei,
723
+ amountLabel: amount,
724
+ descriptionHash,
725
+ durationMinutes,
726
+ orgId: modules.orgId,
727
+ ipfs: { cid, metadata },
728
+ });
729
+ }
730
+ /**
731
+ * Resolved port of `pop treasury propose-finalize` —
732
+ * src/commands/treasury/propose-finalize.ts. When ctx.provider is present the
733
+ * CLI's authoritative pre-flight runs (getDistribution →
734
+ * DistributionNotFound / AlreadyFinalized fail fast, and the metadata carries
735
+ * the payout token + unclaimed remainder); without a provider it behaves like
736
+ * the CLI under --no-preflight (generic 'tokens' label, no unclaimed figure).
737
+ */
738
+ async function proposeFinalizeIntent(ctx, params) {
739
+ const distId = params.distribution;
740
+ if (!Number.isInteger(distId) || distId < 0) {
741
+ throw new errors_1.CliError(`Invalid --distribution "${params.distribution}".`, exit_codes_1.EXIT.USAGE, 'Pass the numeric distribution ID (pop treasury distributions).');
742
+ }
743
+ const minClaimBlocks = params.minClaimBlocks ?? 0;
744
+ if (!Number.isInteger(minClaimBlocks) || minClaimBlocks < 0) {
745
+ throw new errors_1.CliError('--min-claim-blocks must be a non-negative integer.', exit_codes_1.EXIT.USAGE);
746
+ }
747
+ const durationMinutes = params.durationMinutes ?? 60;
748
+ const modules = await (0, resolve_1.resolveOrgModules)(ctx.client, params.org, ctx.chainId);
749
+ const paymentManagerAddr = (0, resolve_1.requireModule)(modules, 'paymentManagerAddress');
750
+ const hybridVotingAddr = modules.hybridVotingAddress;
751
+ if (!hybridVotingAddr) {
752
+ throw new errors_1.PreconditionError('HybridVoting not deployed for this org — cannot create a governance proposal.');
753
+ }
754
+ // Authoritative read (RPC — mirrors the contract's own gates so the
755
+ // proposal isn't doomed at execution). Optional: no provider = --no-preflight.
756
+ let tokenLabel = 'tokens';
757
+ let unclaimedLabel;
758
+ if (ctx.provider) {
759
+ const pmRead = new ethers_1.ethers.Contract(paymentManagerAddr, (0, contracts_1.getAbi)('PaymentManager'), ctx.provider);
760
+ let dist;
761
+ try {
762
+ dist = await pmRead.getDistribution(distId);
763
+ }
764
+ catch {
765
+ throw new errors_1.PreconditionError(`Could not read distribution ${distId} on-chain.`, 'List distributions with: pop treasury distributions');
766
+ }
767
+ if (ethers_1.ethers.BigNumber.from(dist.totalAmount).isZero()) {
768
+ throw new errors_1.PreconditionError(`Distribution ${distId} does not exist (the contract would revert DistributionNotFound).`, 'List distributions with: pop treasury distributions');
769
+ }
770
+ if (dist.finalized) {
771
+ throw new errors_1.PreconditionError(`Distribution ${distId} is already finalized.`);
772
+ }
773
+ const token = await (0, treasury_1.resolvePayoutTokenInfo)(ctx.provider, dist.payoutToken, ctx.chainId);
774
+ tokenLabel = token.symbol;
775
+ const unclaimed = ethers_1.ethers.BigNumber.from(dist.totalAmount).sub(dist.totalClaimed);
776
+ unclaimedLabel = (0, format_1.formatToken)(unclaimed, token.decimals, token.symbol);
777
+ }
778
+ const title = `Finalize distribution #${distId}`;
779
+ const metadata = (0, proposal_1.buildProposalMetadata)({
780
+ description: `Finalize distribution #${distId} via PaymentManager.finalizeDistribution(${distId}, ${minClaimBlocks}). `
781
+ + `Blocks further claims and returns the unclaimed remainder${unclaimedLabel ? ` (${unclaimedLabel})` : ''} to the Executor treasury. `
782
+ + `Payout token: ${tokenLabel}. Min claim period: ${minClaimBlocks} blocks from the checkpoint block.`,
783
+ optionNames: [title, 'Keep the distribution open'],
784
+ });
785
+ const { cid, descriptionHash } = await pinProposalMetadata(ctx, metadata);
786
+ return buildFinalizeDistributionProposal({
787
+ hybridVotingAddress: hybridVotingAddr,
788
+ paymentManagerAddress: paymentManagerAddr,
789
+ distributionId: distId,
790
+ minClaimPeriodBlocks: minClaimBlocks,
791
+ descriptionHash,
792
+ durationMinutes,
793
+ orgId: modules.orgId,
794
+ ipfs: { cid, metadata },
795
+ });
796
+ }
797
+ /**
798
+ * Resolved port of `pop treasury deposit` — src/commands/treasury/deposit.ts.
799
+ * Returns BOTH intents of the two-transaction flow (approve, then payERC20);
800
+ * hosts execute them in order and stop if the approve fails. Decimals follow
801
+ * the CLI exactly: known-token table first (unknown tokens throw), then a
802
+ * live decimals()/symbol() override when a provider is available.
803
+ */
804
+ async function depositIntents(ctx, params) {
805
+ const tokenAddr = (0, validation_1.requireAddress)(params.token, 'token');
806
+ if (!Number.isFinite(params.amount) || params.amount <= 0) {
807
+ throw new errors_1.CliError(`--amount must be a positive number, got "${params.amount}".`, exit_codes_1.EXIT.USAGE);
808
+ }
809
+ const modules = await (0, resolve_1.resolveOrgModules)(ctx.client, params.org, ctx.chainId);
810
+ const paymentManagerAddress = (0, resolve_1.requireModule)(modules, 'paymentManagerAddress');
811
+ let decimals = (0, chains_1.getTokenDecimals)(tokenAddr);
812
+ let symbol;
813
+ if (ctx.provider) {
814
+ try {
815
+ const erc20Read = new ethers_1.ethers.Contract(tokenAddr, (0, contracts_1.getAbi)('ERC20'), ctx.provider);
816
+ const [liveDecimals, liveSymbol] = await Promise.all([
817
+ erc20Read.decimals(),
818
+ erc20Read.symbol().catch(() => undefined),
819
+ ]);
820
+ decimals = Number(liveDecimals);
821
+ symbol = liveSymbol;
822
+ }
823
+ catch {
824
+ // Live read failed — fall back to the known-token table / 18.
825
+ }
826
+ }
827
+ const amountWei = ethers_1.ethers.utils.parseUnits(params.amount.toString(), decimals);
828
+ return {
829
+ approve: buildApproveErc20({
830
+ tokenAddress: tokenAddr,
831
+ spender: paymentManagerAddress,
832
+ amountWei,
833
+ orgId: modules.orgId,
834
+ }),
835
+ payERC20: buildPayErc20({
836
+ paymentManagerAddress,
837
+ tokenAddress: tokenAddr,
838
+ amountWei,
839
+ orgId: modules.orgId,
840
+ }),
841
+ amountWei,
842
+ decimals,
843
+ symbol,
844
+ };
845
+ }
846
+ /**
847
+ * Resolved port of `pop treasury claim` — src/commands/treasury/claim.ts.
848
+ * Resolves the payout token's decimals subgraph-FIRST (FETCH_DISTRIBUTION_BY_ID
849
+ * — null means "not indexed yet" and falls through to getDistribution() when a
850
+ * provider is available, never DistributionNotFound), converts the amount with
851
+ * parseClaimAmount, and delegates to buildClaimDistribution.
852
+ *
853
+ * The CLI's revert-predicting pre-flight (finalized / hasClaimed / isOptedOut
854
+ * — all authoritative eth_calls) stays host-side; opt-out BLOCKS on the
855
+ * deployed pre-L-19 PaymentManager.
856
+ */
857
+ async function claimDistributionIntent(ctx, params) {
858
+ const proofArray = parseProof(params.proof);
859
+ const modules = await (0, resolve_1.resolveOrgModules)(ctx.client, params.org, ctx.chainId);
860
+ const paymentManagerAddress = (0, resolve_1.requireModule)(modules, 'paymentManagerAddress');
861
+ // The distribution read is core to amount encoding (payout token →
862
+ // decimals), so it runs unless --wei makes it unnecessary.
863
+ let decimals = 18;
864
+ if (!params.wei) {
865
+ const indexed = await (0, treasury_1.fetchDistributionById)(ctx.client, paymentManagerAddress, params.distribution, ctx.chainId);
866
+ let payoutToken;
867
+ let totalAmount;
868
+ if (indexed) {
869
+ payoutToken = indexed.payoutToken;
870
+ totalAmount = ethers_1.ethers.BigNumber.from(indexed.totalAmount);
871
+ }
872
+ else if (ctx.provider) {
873
+ let onChain;
874
+ try {
875
+ onChain = await new ethers_1.ethers.Contract(paymentManagerAddress, (0, contracts_1.getAbi)('PaymentManager'), ctx.provider)
876
+ .getDistribution(params.distribution);
877
+ }
878
+ catch {
879
+ throw new errors_1.PreconditionError(`Could not read distribution ${params.distribution} on-chain.`, 'List distributions with: pop treasury distributions');
880
+ }
881
+ payoutToken = onChain.payoutToken;
882
+ totalAmount = ethers_1.ethers.BigNumber.from(onChain.totalAmount);
883
+ }
884
+ else {
885
+ throw new errors_1.PreconditionError(`Could not read distribution ${params.distribution} on-chain.`, 'List distributions with: pop treasury distributions');
886
+ }
887
+ if (totalAmount.isZero()) {
888
+ throw new errors_1.PreconditionError(`Distribution ${params.distribution} does not exist.`, 'List distributions with: pop treasury distributions');
889
+ }
890
+ const token = await (0, treasury_1.resolvePayoutTokenInfo)(ctx.provider, payoutToken, ctx.chainId);
891
+ decimals = token.decimals;
892
+ }
893
+ const amountWei = parseClaimAmount(params.amount, Boolean(params.wei), decimals);
894
+ return buildClaimDistribution({
895
+ paymentManagerAddress,
896
+ distributionId: params.distribution,
897
+ amountWei,
898
+ proof: proofArray,
899
+ orgId: modules.orgId,
900
+ });
901
+ }
902
+ /**
903
+ * Resolved port of `pop treasury opt-out` / `opt-in` —
904
+ * src/commands/treasury/opt-out.ts. The CLI's isOptedOut no-op short-circuit
905
+ * is a host-side pre-flight (the call is idempotent on-chain anyway).
906
+ */
907
+ async function optOutIntent(ctx, params) {
908
+ const modules = await (0, resolve_1.resolveOrgModules)(ctx.client, params.org, ctx.chainId);
909
+ const paymentManagerAddress = (0, resolve_1.requireModule)(modules, 'paymentManagerAddress');
910
+ return buildOptOut({ paymentManagerAddress, optOut: params.optOut, orgId: modules.orgId });
911
+ }