diff --git a/app/build/dao/cast-vote.tsx b/app/build/dao/cast-vote.tsx deleted file mode 100644 index 7060b0e..0000000 --- a/app/build/dao/cast-vote.tsx +++ /dev/null @@ -1,180 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useState } from "react"; -import { Loader2, ExternalLink, CheckCircle2, AlertTriangle, Wallet } from "lucide-react"; -import { GOVERNOR_ABI, VOTES_ABI } from "lightnode-sdk"; -import { humanizeError } from "@/lib/humanize-error"; -import { cn } from "@/lib/utils"; -import { - DAO_EXPLORER, - GOVERNOR, - VOTE_TOKEN, - daoPublicClient, - pinnedFees, - readHasVoted, - type DaoChain, -} from "./dao-chain"; -import { useDaoWallet } from "./use-dao-wallet"; - -type Support = 0 | 1 | 2; -type Vote = { phase: "idle" | "working" | "submitted" | "confirmed" | "error"; support?: Support; msg?: string; tx?: `0x${string}` }; - -const CHOICES: { label: string; support: Support; cls: string }[] = [ - { label: "For", support: 1, cls: "text-success border-success/40 hover:bg-success/10" }, - { label: "Against", support: 0, cls: "text-destructive border-destructive/40 hover:bg-destructive/10" }, - { label: "Abstain", support: 2, cls: "text-content-default border-bdr-soft hover:bg-surface-base-faint" }, -]; - -export function CastVote({ chain, proposalId, onVoted }: { chain: DaoChain; proposalId: string; onVoted?: () => void }) { - const { address, isConnected, open, getSigner } = useDaoWallet(chain); - const [voted, setVoted] = useState(null); - const [powerWei, setPowerWei] = useState(null); - const [vote, setVote] = useState({ phase: "idle" }); - - const check = useCallback(async () => { - if (!address) return; - const pub = daoPublicClient(chain); - try { - const [hasVoted, power] = await Promise.all([ - readHasVoted(chain, BigInt(proposalId), address), - pub.readContract({ address: VOTE_TOKEN[chain], abi: VOTES_ABI, functionName: "getVotes", args: [address] }) as Promise, - ]); - setVoted(hasVoted); - setPowerWei(power); - } catch { - setVoted(null); - setPowerWei(null); - } - }, [chain, proposalId, address]); - - useEffect(() => { - setVote({ phase: "idle" }); - setVoted(null); - setPowerWei(null); - if (isConnected && address) void check(); - }, [isConnected, address, chain, proposalId, check]); - - const castVote = async (support: Support) => { - if (!isConnected || !address) return open(); - setVote({ phase: "working", support, msg: "Confirm in your wallet (you may be asked to switch network first)..." }); - try { - const signer = await getSigner(); - const fees = chain === "lightchain" ? await pinnedFees(daoPublicClient(chain)) : undefined; - const hash = await signer.writeContract({ - address: GOVERNOR[chain], - abi: GOVERNOR_ABI, - functionName: "castVote", - args: [BigInt(proposalId), support], - ...(fees ?? {}), - }); - setVote({ phase: "submitted", support, msg: "Vote submitted - confirming...", tx: hash }); - await daoPublicClient(chain).waitForTransactionReceipt({ hash }); - setVote({ phase: "confirmed", support, msg: "Vote confirmed on-chain.", tx: hash }); - setVoted(true); - onVoted?.(); - } catch (e) { - setVote({ phase: "error", support, msg: humanizeError(e, { action: "casting your vote" }) }); - } - }; - - return ( -
-

Cast your vote

- open()} - onVote={castVote} - /> - {vote.phase !== "idle" && } -
- ); -} - -function CastBody({ - connected, - voted, - powerWei, - busy, - activeSupport, - onConnect, - onVote, -}: { - connected: boolean; - voted: boolean | null; - powerWei: bigint | null; - busy: boolean; - activeSupport?: Support; - onConnect: () => void; - onVote: (s: Support) => void; -}) { - if (!connected) { - return ( - - ); - } - if (voted) { - return ( -

- You already voted on this proposal. -

- ); - } - const noPower = powerWei === 0n; - return ( - <> -
- {CHOICES.map((c) => ( - - ))} -
- {noPower && ( -

- No voting power at this proposal's snapshot. Delegate before the next proposal opens to vote. -

- )} - - ); -} - -function VoteStatus({ chain, vote }: { chain: DaoChain; vote: Vote }) { - if (vote.phase === "working") return

{vote.msg}

; - if (vote.phase === "error") { - return ( -

- {vote.msg} -

- ); - } - return ( -
- - {vote.phase === "submitted" && } - {vote.phase === "confirmed" && } - {vote.msg} - - {vote.tx && ( - - tx - - )} -
- ); -} diff --git a/app/build/dao/dao-chain.ts b/app/build/dao/dao-chain.ts index f49ca80..3dc3d71 100644 --- a/app/build/dao/dao-chain.ts +++ b/app/build/dao/dao-chain.ts @@ -1,13 +1,15 @@ /** - * Client-side DAO chain config + on-chain reads shared by the voting-power and - * cast-vote controls. Both governance chains expose the same OZ Governor surface - * but different vote tokens: + * Client-side DAO chain config + on-chain reads for the governance-intelligence + * panel. lightnode reads + decodes governance state; the actual vote/delegate + * transactions happen on LightChain's official UIs (see DAO_VOTE_UI), so there + * are no writes here. The two chains share the OZ Governor surface but differ on + * the vote token: * - Ethereum -> LCAIB, an ERC20Votes token (balanceOf works). * - LightChain -> a native genesis predeploy (balanceOf reverts; voting weight * tracks the native LCAI balance, so we read getBalance there instead). */ import { createPublicClient, http } from "viem"; -import { DAO_ADDRESSES, GOVERNOR_ABI, VOTES_ABI } from "lightnode-sdk"; +import { DAO_ADDRESSES, VOTES_ABI } from "lightnode-sdk"; import { NETWORKS } from "@/lib/network"; export type DaoChain = "ethereum" | "lightchain"; @@ -17,11 +19,6 @@ export const DAO_RPC: Record = { lightchain: NETWORKS.mainnet.rpc, }; -export const DAO_CHAIN_ID: Record = { - ethereum: 1, - lightchain: NETWORKS.mainnet.chainId, -}; - export const DAO_EXPLORER: Record = { ethereum: "https://etherscan.io", lightchain: NETWORKS.mainnet.explorer, @@ -33,11 +30,6 @@ export const VOTE_TOKEN: Record = { lightchain: DAO_ADDRESSES.lightchain.ballots as `0x${string}`, }; -export const GOVERNOR: Record = { - ethereum: DAO_ADDRESSES.ethereum.governor, - lightchain: DAO_ADDRESSES.lightchain.governor, -}; - // Both governors count voting in BLOCKS (CLOCK_MODE=blocknumber), so block time // is needed to turn votingDelay/votingPeriod + deadline distance into real time. // Measured on mainnet: Ethereum ~12.04s (slot time), LightChain ~6.00s. @@ -46,6 +38,15 @@ export const SECONDS_PER_BLOCK: Record = { lightchain: 6, }; +// lightnode is an ecosystem intelligence layer, NOT a governance app: the actual +// vote / delegate / execute transactions happen on LightChain's own official UIs. +// We read + decode everything here and link out for the writes. +export const DAO_VOTE_UI = "https://dao.lightchain.ai"; +export const DELEGATION_UI: Record = { + ethereum: "https://ballots.lightchain.ai", // wrap LCAI -> LCAIB + delegate + lightchain: null, // native voting self-delegates; no wrapper UI +}; + export function daoPublicClient(chain: DaoChain) { return createPublicClient({ transport: http(DAO_RPC[chain]) }); } @@ -78,34 +79,3 @@ export async function loadVotingPower(chain: DaoChain, address: `0x${string}`): ]); return { votesWei, delegate, balanceWei }; } - -// LightChain's gas is tiny enough that MetaMask renders the fee red and blocks -// confirmation unless we pin chain-estimated values. Returns one fee arm or the -// other (never both) so viem's writeContract accepts it. Ethereum renders fine -// natively, so callers only pin for LightChain. -export type PinnedFees = { maxFeePerGas: bigint; maxPriorityFeePerGas: bigint } | { gasPrice: bigint } | undefined; - -export async function pinnedFees(pub: ReturnType): Promise { - try { - const f = await pub.estimateFeesPerGas(); - return f?.maxFeePerGas - ? { maxFeePerGas: f.maxFeePerGas, maxPriorityFeePerGas: f.maxPriorityFeePerGas ?? f.maxFeePerGas } - : { gasPrice: await pub.getGasPrice() }; - } catch { - try { - return { gasPrice: await pub.getGasPrice() }; - } catch { - return undefined; - } - } -} - -export async function readHasVoted(chain: DaoChain, proposalId: bigint, address: `0x${string}`): Promise { - const pub = daoPublicClient(chain); - return pub.readContract({ - address: GOVERNOR[chain], - abi: GOVERNOR_ABI, - functionName: "hasVoted", - args: [proposalId, address], - }) as Promise; -} diff --git a/app/build/dao/page.tsx b/app/build/dao/page.tsx index 36eb835..e62b9ae 100644 --- a/app/build/dao/page.tsx +++ b/app/build/dao/page.tsx @@ -2,18 +2,17 @@ import { useCallback, useEffect, useState } from "react"; import Image from "next/image"; -import { Loader2, RefreshCw, ChevronDown, ExternalLink } from "lucide-react"; +import { Loader2, RefreshCw, ChevronDown, ExternalLink, Landmark, Vote } from "lucide-react"; import { ConsolePanel } from "@/components/build/console/panel"; import { CodeTabs } from "@/components/build/console/code-tabs"; import { Notice, short } from "@/components/build/console/panel-kit"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; -import { DAO_EXPLORER, SECONDS_PER_BLOCK, type DaoChain } from "./dao-chain"; +import { DAO_EXPLORER, DAO_VOTE_UI, SECONDS_PER_BLOCK, type DaoChain } from "./dao-chain"; import { humanizeDuration } from "./dao-math"; import { TreasuryBar } from "./treasury-bar"; import { VotingPowerCard } from "./voting-power-card"; import { QuorumLine } from "./quorum-line"; -import { CastVote } from "./cast-vote"; const CHAIN_META: Record = { ethereum: { label: "Ethereum", icon: "/logos/eth.svg" }, @@ -167,8 +166,8 @@ export default function DaoPanel() {
@@ -206,6 +205,20 @@ export default function DaoPanel() { } > @@ -282,7 +295,14 @@ export default function DaoPanel() { {p.stateLabel.toLowerCase() === "active" && ( - void load(chain, limit)} /> + + Vote on this proposal at the LightChain DAO + )} {(() => { diff --git a/app/build/dao/use-dao-wallet.ts b/app/build/dao/use-dao-wallet.ts deleted file mode 100644 index 11da9b9..0000000 --- a/app/build/dao/use-dao-wallet.ts +++ /dev/null @@ -1,46 +0,0 @@ -"use client"; - -import { useAccount, useSwitchChain, useChainId } from "wagmi"; -import { getWalletClient } from "@wagmi/core"; -import { useAppKit } from "@reown/appkit/react"; -import { wagmiConfig } from "@/lib/wagmi"; -import { DAO_CHAIN_ID, type DaoChain } from "./dao-chain"; - -/** - * Shared wallet wiring for the DAO write controls (delegate, cast vote). Uses - * `open()` from AppKit for connect rather than the global ConnectButton, which - * treats Ethereum as "unsupported" and would nag the user back to LightChain. - * - * `getSigner` is the load-bearing piece: the app's network toggle drives wagmi's - * tracked chain (e.g. LightChain 9200) independently of the wallet's real chain, - * so the reactive `useWalletClient()` can stay bound to the wrong chain and viem - * then rejects the write ("wallet chain ... does not match target chain ..."). We - * instead switch to the target chain and fetch a FRESH client bound to it from - * the connector's actual state. - */ -export function useDaoWallet(chain: DaoChain) { - const { address, isConnected } = useAccount(); - const { switchChainAsync } = useSwitchChain(); - const { open } = useAppKit(); - const chainId = useChainId(); - const targetChainId = DAO_CHAIN_ID[chain]; - - const getSigner = async () => { - // Switching to the chain you're already on is a no-op (no wallet prompt), - // and it realigns wagmi's tracked chain with the connector before we read it. - await switchChainAsync({ chainId: targetChainId }); - const signer = await getWalletClient(wagmiConfig, { chainId: targetChainId }); - if (!signer) throw new Error("Could not get a wallet client for this network. Reconnect and try again."); - return signer; - }; - - return { - address, - isConnected, - open, - chainId, - targetChainId, - onTargetChain: chainId === targetChainId, - getSigner, - }; -} diff --git a/app/build/dao/voting-power-card.tsx b/app/build/dao/voting-power-card.tsx index a7ce3b7..7064e11 100644 --- a/app/build/dao/voting-power-card.tsx +++ b/app/build/dao/voting-power-card.tsx @@ -1,32 +1,25 @@ "use client"; import { useCallback, useEffect, useState } from "react"; -import { Loader2, ShieldCheck, AlertTriangle, ExternalLink, Wallet } from "lucide-react"; -import { VOTES_ABI } from "lightnode-sdk"; -import { humanizeError } from "@/lib/humanize-error"; +import { ShieldCheck, AlertTriangle, ExternalLink, Wallet } from "lucide-react"; +import { useAccount } from "wagmi"; +import { useAppKit } from "@reown/appkit/react"; import { short } from "@/components/build/console/panel-kit"; -import { cn } from "@/lib/utils"; -import { - DAO_EXPLORER, - VOTE_TOKEN, - daoPublicClient, - loadVotingPower, - pinnedFees, - type DaoChain, - type VotingPowerReads, -} from "./dao-chain"; +import { DAO_VOTE_UI, DELEGATION_UI, loadVotingPower, type DaoChain, type VotingPowerReads } from "./dao-chain"; import { delegationStatus, formatLcaiWei } from "./dao-math"; -import { useDaoWallet } from "./use-dao-wallet"; - -type Tx = { phase: "idle" | "working" | "submitted" | "confirmed" | "error"; msg?: string; tx?: `0x${string}` }; const SYMBOL: Record = { ethereum: "LCAIB", lightchain: "LCAI" }; +/** + * Read-only "your standing" card. lightnode surfaces voting power + delegation + * state from the on-chain registries; the actual delegate transaction happens on + * LightChain's official UI, so we link out rather than sign here. + */ export function VotingPowerCard({ chain }: { chain: DaoChain }) { - const { address, isConnected, open, getSigner } = useDaoWallet(chain); + const { address, isConnected } = useAccount(); + const { open } = useAppKit(); const [reads, setReads] = useState(null); const [loading, setLoading] = useState(false); - const [tx, setTx] = useState({ phase: "idle" }); const refresh = useCallback(async () => { if (!address) return; @@ -42,32 +35,9 @@ export function VotingPowerCard({ chain }: { chain: DaoChain }) { useEffect(() => { setReads(null); - setTx({ phase: "idle" }); if (isConnected && address) void refresh(); }, [isConnected, address, chain, refresh]); - const delegateToSelf = async () => { - if (!isConnected || !address) return open(); - setTx({ phase: "working", msg: "Confirm in your wallet (you may be asked to switch network first)..." }); - try { - const signer = await getSigner(); - const fees = chain === "lightchain" ? await pinnedFees(daoPublicClient(chain)) : undefined; - const hash = await signer.writeContract({ - address: VOTE_TOKEN[chain], - abi: VOTES_ABI, - functionName: "delegate", - args: [address], - ...(fees ?? {}), - }); - setTx({ phase: "submitted", msg: "Delegation submitted - confirming...", tx: hash }); - await daoPublicClient(chain).waitForTransactionReceipt({ hash }); - setTx({ phase: "confirmed", msg: "Voting power activated.", tx: hash }); - await refresh(); - } catch (e) { - setTx({ phase: "error", msg: humanizeError(e, { action: "delegating your votes" }) }); - } - }; - if (!isConnected || !address) { return (
); } @@ -113,19 +82,7 @@ function PowerHeader({ chain, reads, loading }: { chain: DaoChain; reads: Voting ); } -function DelegationRow({ - chain, - reads, - address, - onDelegate, - working, -}: { - chain: DaoChain; - reads: VotingPowerReads; - address: `0x${string}`; - onDelegate: () => void; - working: boolean; -}) { +function DelegationRow({ chain, reads, address }: { chain: DaoChain; reads: VotingPowerReads; address: `0x${string}` }) { const status = delegationStatus(reads.votesWei, reads.balanceWei, reads.delegate, address); if (status.kind === "self") { const gap = formatLcaiWei(status.gapWei, 2); @@ -137,8 +94,6 @@ function DelegationRow({

); } - // Nothing to delegate: holding zero balance on this chain. Don't push a - // pointless "delegate to activate" prompt. if (reads.balanceWei === 0n) { return (

@@ -148,50 +103,22 @@ function DelegationRow({ } const msg = status.kind === "undelegated" - ? `You hold ${formatLcaiWei(reads.balanceWei, 2)} ${SYMBOL[chain]} but 0 voting power. Delegate to yourself to activate it.` - : `Delegated to ${short(reads.delegate)} - they hold your voting power. Reclaim it below.`; + ? `You hold ${formatLcaiWei(reads.balanceWei, 2)} ${SYMBOL[chain]} but 0 voting power. Activate it by delegating.` + : `Delegated to ${short(reads.delegate)} - they hold your voting power.`; + const href = DELEGATION_UI[chain] ?? DAO_VOTE_UI; return (

{msg}

- -
- ); -} - -function TxLine({ chain, tx }: { chain: DaoChain; tx: Tx }) { - if (tx.phase === "error") { - return ( -

- {tx.msg} -

- ); - } - return ( -
- - {tx.phase === "submitted" && } - {tx.phase === "confirmed" && } - {tx.msg} - - {tx.tx && ( - - tx - - )} + Manage delegation +
); }