From 4e63c2293579d6a9efad9c2babfaaf6e7c809e3b Mon Sep 17 00:00:00 2001 From: Martin Marinov Date: Wed, 10 Jun 2026 14:45:42 +0300 Subject: [PATCH] fix(web): DAO writes target the correct chain + chain-aware SDK snippet On the Ethereum DAO tab, delegate/vote failed with "wallet chain (id: 1) does not match target chain (id: 9200)": the app's network toggle drives wagmi's tracked chain (LightChain 9200) independently of the wallet's real chain, so the reactive useWalletClient() stayed bound to 9200 while the wallet was on Ethereum. Replace it with an imperative getSigner() that switches to the target chain and fetches a fresh wallet client bound to it (@wagmi/core getWalletClient), so the write always targets the right chain. - Voting-power card: drop the misleading "you hold 0 X but 0 power, delegate to activate" prompt when the balance is genuinely 0; show a neutral "you hold no X on this chain" line and hide the delegate button. - SDK snippet is now chain-aware (correct RPC + token note per chain) and shows the full surface we added: proposal, quorum, getVotes, hasVoted, getDelegate, delegate, castVote. Verified against the live contracts (no code change needed): the LightChain treasury really holds 4.56 LCAI native (correct units, not billions - mid migration), and "self-delegated" is accurate (delegates(you) == you and getVotes == your native balance; the native predeploy self-delegates). --- app/build/dao/cast-vote.tsx | 12 ++++++------ app/build/dao/page.tsx | 30 +++++++++++++++++++---------- app/build/dao/use-dao-wallet.ts | 24 +++++++++++++++++------ app/build/dao/voting-power-card.tsx | 19 +++++++++++++----- 4 files changed, 58 insertions(+), 27 deletions(-) diff --git a/app/build/dao/cast-vote.tsx b/app/build/dao/cast-vote.tsx index 72f16fc..e116156 100644 --- a/app/build/dao/cast-vote.tsx +++ b/app/build/dao/cast-vote.tsx @@ -26,7 +26,7 @@ const CHOICES: { label: string; support: Support; cls: string }[] = [ ]; export function CastVote({ chain, proposalId, onVoted }: { chain: DaoChain; proposalId: string; onVoted?: () => void }) { - const { address, isConnected, walletClient, open, onTargetChain, ensureChain } = useDaoWallet(chain); + const { address, isConnected, open, getSigner } = useDaoWallet(chain); const [voted, setVoted] = useState(null); const [powerWei, setPowerWei] = useState(null); const [vote, setVote] = useState({ phase: "idle" }); @@ -55,12 +55,12 @@ export function CastVote({ chain, proposalId, onVoted }: { chain: DaoChain; prop }, [isConnected, address, chain, proposalId, check]); const castVote = async (support: Support) => { - if (!walletClient || !address) return open(); - setVote({ phase: "working", support, msg: onTargetChain ? "Confirm your vote in your wallet..." : "Switch network in your wallet..." }); + if (!isConnected || !address) return open(); + setVote({ phase: "working", support, msg: "Confirm in your wallet (you may be asked to switch network first)..." }); try { - await ensureChain(); + const signer = await getSigner(); const fees = chain === "lightchain" ? await pinnedFees(daoPublicClient(chain)) : undefined; - const hash = await walletClient.writeContract({ + const hash = await signer.writeContract({ address: GOVERNOR[chain], abi: GOVERNOR_ABI, functionName: "castVote", @@ -78,7 +78,7 @@ export function CastVote({ chain, proposalId, onVoted }: { chain: DaoChain; prop }; return ( -
+

Cast your vote

The SDK behind it

- +
); diff --git a/app/build/dao/use-dao-wallet.ts b/app/build/dao/use-dao-wallet.ts index c0415f7..11da9b9 100644 --- a/app/build/dao/use-dao-wallet.ts +++ b/app/build/dao/use-dao-wallet.ts @@ -1,34 +1,46 @@ "use client"; -import { useAccount, useWalletClient, useSwitchChain, useChainId } from "wagmi"; +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 { data: walletClient } = useWalletClient(); const { switchChainAsync } = useSwitchChain(); const { open } = useAppKit(); const chainId = useChainId(); const targetChainId = DAO_CHAIN_ID[chain]; - const ensureChain = async () => { - if (chainId !== targetChainId) await switchChainAsync({ chainId: targetChainId }); + 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, - walletClient, open, chainId, targetChainId, onTargetChain: chainId === targetChainId, - ensureChain, + getSigner, }; } diff --git a/app/build/dao/voting-power-card.tsx b/app/build/dao/voting-power-card.tsx index e7cf723..37467f4 100644 --- a/app/build/dao/voting-power-card.tsx +++ b/app/build/dao/voting-power-card.tsx @@ -23,7 +23,7 @@ type Tx = { phase: "idle" | "working" | "submitted" | "confirmed" | "error"; msg const SYMBOL: Record = { ethereum: "LCAIB", lightchain: "LCAI" }; export function VotingPowerCard({ chain }: { chain: DaoChain }) { - const { address, isConnected, walletClient, open, onTargetChain, ensureChain } = useDaoWallet(chain); + const { address, isConnected, open, getSigner } = useDaoWallet(chain); const [reads, setReads] = useState(null); const [loading, setLoading] = useState(false); const [tx, setTx] = useState({ phase: "idle" }); @@ -47,12 +47,12 @@ export function VotingPowerCard({ chain }: { chain: DaoChain }) { }, [isConnected, address, chain, refresh]); const delegateToSelf = async () => { - if (!walletClient || !address) return open(); - setTx({ phase: "working", msg: onTargetChain ? "Confirm the delegation in your wallet..." : "Switch network in your wallet..." }); + if (!isConnected || !address) return open(); + setTx({ phase: "working", msg: "Confirm in your wallet (you may be asked to switch network first)..." }); try { - await ensureChain(); + const signer = await getSigner(); const fees = chain === "lightchain" ? await pinnedFees(daoPublicClient(chain)) : undefined; - const hash = await walletClient.writeContract({ + const hash = await signer.writeContract({ address: VOTE_TOKEN[chain], abi: VOTES_ABI, functionName: "delegate", @@ -137,6 +137,15 @@ 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 ( +

+ You hold no {SYMBOL[chain]} on this chain, so you have no voting power here. +

+ ); + } const msg = status.kind === "undelegated" ? `You hold ${formatLcaiWei(reads.balanceWei, 2)} ${SYMBOL[chain]} but 0 voting power. Delegate to yourself to activate it.`