From 0261a8235ca9f7d4d538316c91327e0b4605b8cd Mon Sep 17 00:00:00 2001 From: Cyber-preacher Date: Thu, 12 Mar 2026 16:19:09 +0400 Subject: [PATCH] phase-81: expose delegation state across governance and chamber views --- src/lib/apiClient.ts | 53 ++++++ src/pages/MyGovernance.tsx | 222 +++++++++++++++++++++++- src/pages/chambers/Chamber.tsx | 37 +++- src/pages/human-nodes/HumanNode.tsx | 55 ++++++ src/pages/profile/Profile.tsx | 54 ++++++ src/pages/proposals/ProposalChamber.tsx | 121 +++++++++++++ src/types/api.ts | 34 ++++ 7 files changed, 570 insertions(+), 6 deletions(-) diff --git a/src/lib/apiClient.ts b/src/lib/apiClient.ts index 0146ccd..cc737f8 100644 --- a/src/lib/apiClient.ts +++ b/src/lib/apiClient.ts @@ -1274,6 +1274,59 @@ export async function apiLegitimacyObjectSet(input: { ); } +export async function apiDelegationSet(input: { + chamberId: string; + delegateeAddress: string; + idempotencyKey?: string; +}): Promise<{ + ok: true; + type: "delegation.set"; + chamberId: string; + delegatorAddress: string; + delegateeAddress: string; + updatedAt: string; +}> { + return await apiPost( + "/api/command", + { + type: "delegation.set", + payload: { + chamberId: input.chamberId, + delegateeAddress: input.delegateeAddress, + }, + idempotencyKey: input.idempotencyKey, + }, + input.idempotencyKey + ? { headers: { "idempotency-key": input.idempotencyKey } } + : undefined, + ); +} + +export async function apiDelegationClear(input: { + chamberId: string; + idempotencyKey?: string; +}): Promise<{ + ok: true; + type: "delegation.clear"; + chamberId: string; + delegatorAddress: string; + cleared: boolean; +}> { + return await apiPost( + "/api/command", + { + type: "delegation.clear", + payload: { + chamberId: input.chamberId, + }, + idempotencyKey: input.idempotencyKey, + }, + input.idempotencyKey + ? { headers: { "idempotency-key": input.idempotencyKey } } + : undefined, + ); +} + export async function apiCmMe(): Promise { return await apiGet("/api/cm/me"); } diff --git a/src/pages/MyGovernance.tsx b/src/pages/MyGovernance.tsx index 90327ba..28c6a2e 100644 --- a/src/pages/MyGovernance.tsx +++ b/src/pages/MyGovernance.tsx @@ -12,6 +12,8 @@ import { AppCard } from "@/components/AppCard"; import { HintLabel } from "@/components/Hint"; import { Badge } from "@/components/primitives/badge"; import { Button } from "@/components/primitives/button"; +import { Input } from "@/components/primitives/input"; +import { AddressInline } from "@/components/AddressInline"; import { PipelineList } from "@/components/PipelineList"; import { StatGrid, makeChamberStats } from "@/components/StatGrid"; import { Surface } from "@/components/Surface"; @@ -20,6 +22,8 @@ import { Kicker } from "@/components/Kicker"; import { apiChambers, apiClock, + apiDelegationClear, + apiDelegationSet, apiLegitimacyObjectSet, apiCmMe, apiMyGovernance, @@ -152,6 +156,15 @@ const formatDayHourMinute = (targetMs: number, nowMs: number): string => { return `${days}d:${String(hours).padStart(2, "0")}h:${String(minutes).padStart(2, "0")}m`; }; +const chamberLabel = ( + chamberId: string, + chambers: ChamberDto[] | null, +): string => { + if (chamberId === "general") return "General chamber"; + const match = chambers?.find((item) => item.id === chamberId); + return match?.name ?? chamberId; +}; + const MyGovernance: React.FC = () => { const [gov, setGov] = useState(null); const [chambers, setChambers] = useState(null); @@ -160,6 +173,15 @@ const MyGovernance: React.FC = () => { const [loadError, setLoadError] = useState(null); const [legitimacyPending, setLegitimacyPending] = useState(false); const [legitimacyError, setLegitimacyError] = useState(null); + const [delegationDrafts, setDelegationDrafts] = useState< + Record + >({}); + const [delegationPendingByChamber, setDelegationPendingByChamber] = useState< + Record + >({}); + const [delegationErrorByChamber, setDelegationErrorByChamber] = useState< + Record + >({}); const [nowMs, setNowMs] = useState(() => Date.now()); useEffect(() => { @@ -182,6 +204,15 @@ const MyGovernance: React.FC = () => { setChambers(chambersRes.items); setClock(clockRes); setCmSummary(cmRes); + setDelegationDrafts((current) => { + const next = { ...current }; + for (const item of govRes.delegation.chambers) { + if (next[item.chamberId] === undefined) { + next[item.chamberId] = item.delegateeAddress ?? ""; + } + } + return next; + }); setLoadError(null); } catch (error) { if (!active) return; @@ -256,6 +287,90 @@ const MyGovernance: React.FC = () => { triggerThresholdPercent: 33.3, }; + const updateDelegationDraft = (chamberId: string, value: string) => { + setDelegationDrafts((current) => ({ + ...current, + [chamberId]: value, + })); + }; + + const refreshGovernance = async () => { + const fresh = await apiMyGovernance(); + setGov(fresh); + setDelegationDrafts((current) => { + const next = { ...current }; + for (const item of fresh.delegation.chambers) { + next[item.chamberId] = item.delegateeAddress ?? ""; + } + return next; + }); + }; + + const handleDelegationSet = async (chamberId: string) => { + const delegateeAddress = delegationDrafts[chamberId]?.trim() ?? ""; + if (!delegateeAddress) { + setDelegationErrorByChamber((current) => ({ + ...current, + [chamberId]: "Enter an address to delegate to.", + })); + return; + } + try { + setDelegationPendingByChamber((current) => ({ + ...current, + [chamberId]: true, + })); + setDelegationErrorByChamber((current) => ({ + ...current, + [chamberId]: null, + })); + await apiDelegationSet({ + chamberId, + delegateeAddress, + idempotencyKey: crypto.randomUUID(), + }); + await refreshGovernance(); + } catch (error) { + setDelegationErrorByChamber((current) => ({ + ...current, + [chamberId]: formatLoadError((error as Error).message), + })); + } finally { + setDelegationPendingByChamber((current) => ({ + ...current, + [chamberId]: false, + })); + } + }; + + const handleDelegationClear = async (chamberId: string) => { + try { + setDelegationPendingByChamber((current) => ({ + ...current, + [chamberId]: true, + })); + setDelegationErrorByChamber((current) => ({ + ...current, + [chamberId]: null, + })); + await apiDelegationClear({ + chamberId, + idempotencyKey: crypto.randomUUID(), + }); + await refreshGovernance(); + } catch (error) { + setDelegationErrorByChamber((current) => ({ + ...current, + [chamberId]: formatLoadError((error as Error).message), + })); + } finally { + setDelegationPendingByChamber((current) => ({ + ...current, + [chamberId]: false, + })); + } + }; + const handleLegitimacyToggle = async () => { try { setLegitimacyPending(true); @@ -264,8 +379,7 @@ const MyGovernance: React.FC = () => { active: !legitimacy.objecting, idempotencyKey: crypto.randomUUID(), }); - const fresh = await apiMyGovernance(); - setGov(fresh); + await refreshGovernance(); } catch (error) { setLegitimacyError(formatLoadError((error as Error).message)); } finally { @@ -677,6 +791,110 @@ const MyGovernance: React.FC = () => { + + + Delegation + + + {gov?.delegation.chambers.length ? ( +
+ {gov.delegation.chambers.map((item) => { + const pending = + delegationPendingByChamber[item.chamberId] ?? false; + const currentValue = + delegationDrafts[item.chamberId] ?? + item.delegateeAddress ?? + ""; + return ( + +
+
+ + {chamberLabel(item.chamberId, chambers)} + +

+ Current delegate +

+ {item.delegateeAddress ? ( + + ) : ( +

No delegate set

+ )} +
+ + Inbound weight {item.inboundWeight} + +
+ +
+ + updateDelegationDraft( + item.chamberId, + event.target.value, + ) + } + placeholder="Delegate address" + disabled={pending} + /> +
+ + +
+
+ + {delegationErrorByChamber[item.chamberId] ? ( +

+ {delegationErrorByChamber[item.chamberId]} +

+ ) : null} +
+ ); + })} +
+ ) : ( + + Delegation becomes available once you are participating in chamber + governance. + + )} +
+
+ System legitimacy diff --git a/src/pages/chambers/Chamber.tsx b/src/pages/chambers/Chamber.tsx index 8d8056a..86a99a6 100644 --- a/src/pages/chambers/Chamber.tsx +++ b/src/pages/chambers/Chamber.tsx @@ -179,7 +179,12 @@ const Chamber: React.FC = () => { String(gov.acm).includes(term) || String(gov.lcm).includes(term) || String(gov.mcm).includes(term) || - String(gov.delegatedWeight).includes(term), + String(gov.delegatedWeight).includes(term) || + String(gov.effectiveVotingPower).includes(term) || + gov.delegateeAddress?.toLowerCase().includes(term) || + gov.inboundDelegators.some((address) => + address.toLowerCase().includes(term), + ), ); }, [data, governorSearch]); @@ -969,14 +974,38 @@ const Chamber: React.FC = () => {

· {gov.focus}

+

+ Vote power {gov.effectiveVotingPower} + {gov.delegatedWeight > 0 + ? ` · Carries +${gov.delegatedWeight} delegated` + : ""} +

ACM {gov.acm.toLocaleString()} · LCM{" "} {gov.lcm.toLocaleString()} · MCM{" "} {gov.mcm.toLocaleString()} - {gov.delegatedWeight > 0 - ? ` · Delegated +${gov.delegatedWeight}` - : ""}

+ {gov.delegateeAddress ? ( +
+ Delegates to + +
+ ) : null} + {gov.inboundDelegators.length > 0 ? ( +
+ Backed by + {gov.inboundDelegators.map((address) => ( + + ))} +
+ ) : null}