/ frontend / src / services / governance.ts
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  }