governance.ts
1 /** 2 * Governance integration service 3 * Interfaces with governance.alpha and governance.delta for proposal management 4 */ 5 6 import { getChainEndpoint } from './chain' 7 import type { Chain } from '../types/vote' 8 9 // Governance proposal types (matches governance.alpha action types) 10 export const ACTION_TYPES = { 11 UPDATE_PARAMETER: 0, 12 ADD_GOVERNOR: 1, 13 REMOVE_GOVERNOR: 2, 14 EMERGENCY_PAUSE_MINTING: 3, 15 EMERGENCY_RESUME_MINTING: 4, 16 UPDATE_MINTING_RATE: 5, 17 UPDATE_FEES: 6, 18 TRANSFER_TREASURY: 7, 19 UPGRADE_CONTRACT: 8, 20 REGISTER_PROGRAM: 14, 21 UPDATE_PROGRAM: 15, 22 DEPLOY_PROGRAM: 16, 23 } as const 24 25 export type ActionType = (typeof ACTION_TYPES)[keyof typeof ACTION_TYPES] 26 27 // Proposal status 28 export const PROPOSAL_STATUS = { 29 PENDING: 0, 30 ACTIVE: 1, 31 PASSED: 2, 32 FAILED: 3, 33 EXECUTED: 4, 34 VETOED: 5, 35 EXPIRED: 6, 36 } as const 37 38 export type ProposalStatus = (typeof PROPOSAL_STATUS)[keyof typeof PROPOSAL_STATUS] 39 40 export interface GovernanceProposal { 41 proposalId: string 42 proposer: string 43 proposalType: 'standard' | 'critical' 44 actionType: ActionType 45 actionParam: string 46 yesVotes: number 47 noVotes: number 48 createdAt: number 49 votingEndsAt: number 50 timelockEndsAt: number 51 status: ProposalStatus 52 } 53 54 export interface GovernanceConfig { 55 standardQuorumBps: number 56 criticalQuorumBps: number 57 standardApprovalBps: number 58 criticalApprovalBps: number 59 timelockBlocks: number 60 votingPeriodBlocks: number 61 totalGovernors: number 62 } 63 64 /** 65 * Get governance configuration 66 */ 67 export async function getGovernanceConfig(chain: Chain): Promise<GovernanceConfig> { 68 const endpoint = getChainEndpoint(chain) 69 const program = chain === 'alpha' ? 'governance.alpha' : 'governance.delta' 70 71 try { 72 const response = await fetch(`${endpoint}/program/${program}/mapping/config/0u8`) 73 if (!response.ok) { 74 throw new Error('Failed to fetch governance config') 75 } 76 const data = await response.json() 77 return parseGovernanceConfig(data) 78 } catch { 79 // Return defaults if chain unavailable 80 return { 81 standardQuorumBps: 3400, 82 criticalQuorumBps: 5000, 83 standardApprovalBps: 6700, 84 criticalApprovalBps: 9000, 85 timelockBlocks: 60480, 86 votingPeriodBlocks: 30240, 87 totalGovernors: 0, 88 } 89 } 90 } 91 92 function parseGovernanceConfig(data: unknown): GovernanceConfig { 93 // Parse chain response format 94 if (typeof data === 'object' && data !== null) { 95 const config = data as Record<string, unknown> 96 return { 97 standardQuorumBps: Number(config.standard_quorum_bps || 3400), 98 criticalQuorumBps: Number(config.critical_quorum_bps || 5000), 99 standardApprovalBps: Number(config.standard_approval_bps || 6700), 100 criticalApprovalBps: Number(config.critical_approval_bps || 9000), 101 timelockBlocks: Number(config.timelock_blocks || 60480), 102 votingPeriodBlocks: Number(config.voting_period_blocks || 30240), 103 totalGovernors: Number(config.total_governors || 0), 104 } 105 } 106 throw new Error('Invalid governance config format') 107 } 108 109 /** 110 * Get a specific governance proposal 111 */ 112 export async function getProposal( 113 chain: Chain, 114 proposalId: string 115 ): Promise<GovernanceProposal | null> { 116 const endpoint = getChainEndpoint(chain) 117 const program = chain === 'alpha' ? 'governance.alpha' : 'governance.delta' 118 119 try { 120 const response = await fetch( 121 `${endpoint}/program/${program}/mapping/proposals/${proposalId}u128` 122 ) 123 if (!response.ok) return null 124 const data = await response.json() 125 return parseProposal(data, proposalId) 126 } catch { 127 return null 128 } 129 } 130 131 function parseProposal(data: unknown, proposalId: string): GovernanceProposal { 132 if (typeof data === 'object' && data !== null) { 133 const proposal = data as Record<string, unknown> 134 return { 135 proposalId, 136 proposer: String(proposal.proposer || ''), 137 proposalType: Number(proposal.proposal_type) === 1 ? 'critical' : 'standard', 138 actionType: Number(proposal.action_type || 0) as ActionType, 139 actionParam: String(proposal.action_param || ''), 140 yesVotes: Number(proposal.yes_votes || 0), 141 noVotes: Number(proposal.no_votes || 0), 142 createdAt: Number(proposal.created_at || 0), 143 votingEndsAt: Number(proposal.voting_ends_at || 0), 144 timelockEndsAt: Number(proposal.timelock_ends_at || 0), 145 status: Number(proposal.status || 0) as ProposalStatus, 146 } 147 } 148 throw new Error('Invalid proposal format') 149 } 150 151 /** 152 * Create a governance proposal for a passed PR 153 * This is called after a Forge vote passes to create the formal governance proposal 154 */ 155 export async function createProposalForPR( 156 chain: Chain, 157 prHash: string, 158 commitHash: string, 159 repoId: string 160 ): Promise<string> { 161 const endpoint = getChainEndpoint(chain) 162 const program = chain === 'alpha' ? 'forge_alpha.alpha' : 'forge_delta.delta' 163 164 // For now, simulate proposal creation 165 // In production, this would submit a transaction to governance.alpha/delta 166 console.log( 167 `Creating governance proposal for PR ${prHash} on ${chain}`, 168 `Program: ${program}, Endpoint: ${endpoint}`, 169 `Commit: ${commitHash}, Repo: ${repoId}` 170 ) 171 172 // Return simulated proposal ID 173 return `${Date.now()}` 174 } 175 176 /** 177 * Check if a governance proposal has been executed 178 */ 179 export async function isProposalExecuted( 180 chain: Chain, 181 proposalId: string 182 ): Promise<boolean> { 183 const endpoint = getChainEndpoint(chain) 184 const program = chain === 'alpha' ? 'governance.alpha' : 'governance.delta' 185 186 try { 187 const response = await fetch( 188 `${endpoint}/program/${program}/mapping/executed_proposals/${proposalId}u128` 189 ) 190 return response.ok 191 } catch { 192 return false 193 } 194 } 195 196 /** 197 * Get the PR hash linked to a governance proposal 198 */ 199 export async function getPRForProposal( 200 chain: Chain, 201 proposalId: string 202 ): Promise<string | null> { 203 const endpoint = getChainEndpoint(chain) 204 const program = chain === 'alpha' ? 'forge_alpha.alpha' : 'forge_delta.delta' 205 206 try { 207 const response = await fetch( 208 `${endpoint}/program/${program}/mapping/proposal_to_pr/${proposalId}u128` 209 ) 210 if (!response.ok) return null 211 return await response.text() 212 } catch { 213 return null 214 } 215 } 216 217 /** 218 * Get cross-chain attestation status for a shared repo PR 219 */ 220 export async function getCrossChainAttestation( 221 chain: Chain, 222 prHash: string 223 ): Promise<{ 224 alphaPassed: boolean 225 deltaPassed: boolean 226 alphaProposalId: string | null 227 deltaProposalId: string | null 228 } | null> { 229 const endpoint = getChainEndpoint(chain) 230 const program = chain === 'alpha' ? 'forge_alpha.alpha' : 'forge_delta.delta' 231 232 try { 233 const response = await fetch( 234 `${endpoint}/program/${program}/mapping/cross_chain_attestations/${prHash}` 235 ) 236 if (!response.ok) return null 237 238 const data = await response.json() 239 if (typeof data === 'object' && data !== null) { 240 const attestation = data as Record<string, unknown> 241 return { 242 alphaPassed: Boolean(attestation.alpha_passed), 243 deltaPassed: Boolean(attestation.delta_passed), 244 alphaProposalId: attestation.alpha_proposal_id 245 ? String(attestation.alpha_proposal_id) 246 : null, 247 deltaProposalId: attestation.delta_proposal_id 248 ? String(attestation.delta_proposal_id) 249 : null, 250 } 251 } 252 return null 253 } catch { 254 return null 255 } 256 } 257 258 /** 259 * Check if PR is under resubmission cooldown 260 */ 261 export async function getResubmissionCooldown( 262 chain: Chain, 263 commitHash: string 264 ): Promise<{ 265 failedAt: number 266 cooldownUntil: number 267 } | null> { 268 const endpoint = getChainEndpoint(chain) 269 const program = chain === 'alpha' ? 'forge_alpha.alpha' : 'forge_delta.delta' 270 271 try { 272 const response = await fetch( 273 `${endpoint}/program/${program}/mapping/resubmission_cooldowns/${commitHash}` 274 ) 275 if (!response.ok) return null 276 277 const data = await response.json() 278 if (typeof data === 'object' && data !== null) { 279 const cooldown = data as Record<string, unknown> 280 return { 281 failedAt: Number(cooldown.failed_at || 0), 282 cooldownUntil: Number(cooldown.cooldown_until || 0), 283 } 284 } 285 return null 286 } catch { 287 return null 288 } 289 } 290 291 /** 292 * Format proposal status for display 293 */ 294 export function formatProposalStatus(status: ProposalStatus): string { 295 switch (status) { 296 case PROPOSAL_STATUS.PENDING: 297 return 'Pending' 298 case PROPOSAL_STATUS.ACTIVE: 299 return 'Active' 300 case PROPOSAL_STATUS.PASSED: 301 return 'Passed' 302 case PROPOSAL_STATUS.FAILED: 303 return 'Failed' 304 case PROPOSAL_STATUS.EXECUTED: 305 return 'Executed' 306 case PROPOSAL_STATUS.VETOED: 307 return 'Vetoed' 308 case PROPOSAL_STATUS.EXPIRED: 309 return 'Expired' 310 default: 311 return 'Unknown' 312 } 313 } 314 315 /** 316 * Format action type for display 317 */ 318 export function formatActionType(actionType: ActionType): string { 319 switch (actionType) { 320 case ACTION_TYPES.UPDATE_PARAMETER: 321 return 'Update Parameter' 322 case ACTION_TYPES.ADD_GOVERNOR: 323 return 'Add Governor' 324 case ACTION_TYPES.REMOVE_GOVERNOR: 325 return 'Remove Governor' 326 case ACTION_TYPES.EMERGENCY_PAUSE_MINTING: 327 return 'Emergency Pause Minting' 328 case ACTION_TYPES.EMERGENCY_RESUME_MINTING: 329 return 'Emergency Resume Minting' 330 case ACTION_TYPES.UPDATE_MINTING_RATE: 331 return 'Update Minting Rate' 332 case ACTION_TYPES.UPDATE_FEES: 333 return 'Update Fees' 334 case ACTION_TYPES.TRANSFER_TREASURY: 335 return 'Transfer Treasury' 336 case ACTION_TYPES.UPGRADE_CONTRACT: 337 return 'Upgrade Contract' 338 case ACTION_TYPES.REGISTER_PROGRAM: 339 return 'Register Program' 340 case ACTION_TYPES.UPDATE_PROGRAM: 341 return 'Update Program' 342 case ACTION_TYPES.DEPLOY_PROGRAM: 343 return 'Deploy Program' 344 default: 345 return 'Unknown Action' 346 } 347 } 348 349 /** 350 * Check if this is a shared repo that requires both chain votes 351 */ 352 export function isSharedRepo(repoName: string): boolean { 353 const sharedRepos = ['acdc-core', 'sdk', 'adl', 'adl-examples'] 354 return sharedRepos.some((name) => repoName.toLowerCase().includes(name.toLowerCase())) 355 }