From 4e26fc5a6dae89bfe5b62d351db8adc20bf7e450 Mon Sep 17 00:00:00 2001 From: 0xward <0xward.dev@gmail.com> Date: Mon, 3 Aug 2026 06:36:29 +0000 Subject: [PATCH] The wallet was connected. It was on the wrong chain. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OWNER, testing in the OKX in-app browser: *"posisi auto connect tapi gabisa mint pass dan buy ada tulisan 'wallet not connected', itu kenapa?"* Because the message was a lie. One line inside wagmi decides it (@wagmi/core/actions/getConnectorClient): if (assertChainId && connectorChainId !== chainId) throw new ConnectorChainMismatchError(...) The app asks for a Celo-pinned client — useWalletClient({chainId: CELO}) — so on ANY other chain that throws, useWalletClient swallows it into `data: undefined`, and every payment path hits its `!walletClient` guard and reports a connection problem to somebody whose wallet was connected and one tap from working. OKX opens on its own default chain. MiniPay is Celo-only and could never reach it, which is why this survived to the first browser-wallet test. The part that stings: `switchToCelo` already existed, plumbed through the bridge and out to WalletProvider — and had no caller anywhere in the app. The escape hatch was built and never fitted. AUTO-SWITCH, IMMEDIATELY. The owner's call, and the right one for a listing review: *"itu bener2 autoconnect dan autoswitch jaringan ke celo dr awal login dengan wallet."* The moment a connection lands on a non-Celo chain the wallet is asked to switch — not deferred to the first purchase, because a reviewer who opens the shop and sees an error has already formed their opinion. Asked ONCE per chain, via a ref. Asking on every render is a popup loop, which is worse than the bug it fixes. Declining leaves the bar up; moving the wallet to another chain arms one fresh attempt. THE MESSAGE STOPS LYING. All three signing paths in the island go through one assertReady(), and the two hooks that guard on walletClient themselves — the Season Pass mint, which is the exact screen reported, and the reward claim, which has the identical shape — say the same thing: wrong network, NullState runs on Celo. AND A WAY OUT. WrongNetworkBar renders on the three screens that can spend, only when connected to the wrong chain, with a Switch to Celo button wired to the switchToCelo that never had a caller. It uses switchChainAsync so the pending state means something; the fire-and-forget variant resolves instantly whether the wallet agreed or not. A wallet that cannot switch at all gets told to pick Celo by hand rather than a button that silently does nothing. Nothing here can reach MiniPay: it is Celo-only, so wrongNetwork is false there and the bar returns null. check:copy still passes — the copy names Celo and avoids the banned phrase. Verified: 20/20 test:network, tsc clean, build unchanged at 142/242 kB, check:copy, check:cssvars, lint 35. What is NOT covered, stated plainly: no browser test simulates a wrong-chain wallet, because the sandbox has no wallet at all. The assertions are source-level, including one against wagmi's own source so a future upgrade that changes this behaviour fails here rather than in a player's purchase. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017A764RdnwpyWnG7uCNhMiQ --- components/game/CraftingScreen.tsx | 5 ++ components/game/MarketplaceScreen.tsx | 5 ++ components/game/SeasonPassScreen.tsx | 3 + components/game/WrongNetworkBar.tsx | 62 +++++++++++++++ hooks/usePassSBT.ts | 12 ++- hooks/useReward.ts | 9 ++- lib/WagmiIsland.tsx | 76 ++++++++++++++++-- lib/WalletProvider.tsx | 4 + lib/walletBridge.tsx | 14 ++++ package.json | 3 +- scripts/test-wrong-network.js | 109 ++++++++++++++++++++++++++ 11 files changed, 289 insertions(+), 13 deletions(-) create mode 100644 components/game/WrongNetworkBar.tsx create mode 100644 scripts/test-wrong-network.js diff --git a/components/game/CraftingScreen.tsx b/components/game/CraftingScreen.tsx index 0770819..ae7254b 100644 --- a/components/game/CraftingScreen.tsx +++ b/components/game/CraftingScreen.tsx @@ -3,6 +3,7 @@ import { useEffect, useState, useCallback, useRef } from 'react' import { GiAnvil, GiCheckedShield, GiSandsOfTime } from 'react-icons/gi' import { useWallet, CELO_CHAIN_ID } from '@/lib/WalletProvider' +import WrongNetworkBar from './WrongNetworkBar' import { pickBestPaymentToken } from '@/lib/constants/tokens' import { ACCEPTED_TOKENS, getMarketplaceItem, resolveItemId, maxWeaponTier, tokenLabel, TOKEN_LOGOS, @@ -492,6 +493,10 @@ export default function CraftingScreen({ onBack, onGoToRun, address }: CraftingS + {/* Connected but on the wrong chain — see WrongNetworkBar. Renders + nothing at all on Celo, and cannot appear inside MiniPay. */} + + {/* shard balance strip */}
{(['t1', 't2', 't3'] as TierKey[]).map(k => ( diff --git a/components/game/MarketplaceScreen.tsx b/components/game/MarketplaceScreen.tsx index 524f5e1..ab2282f 100644 --- a/components/game/MarketplaceScreen.tsx +++ b/components/game/MarketplaceScreen.tsx @@ -2,6 +2,7 @@ import { useEffect, useState, useCallback, useRef } from 'react' import { useWallet, CELO_CHAIN_ID } from '@/lib/WalletProvider' +import WrongNetworkBar from './WrongNetworkBar' import { GiCrossedSwords, GiCheckedShield, GiMagnifyingGlass } from 'react-icons/gi' import { pickBestPaymentToken, readStablecoinBalances } from '@/lib/constants/tokens' import { MARKETPLACE_ITEMS, ACCEPTED_TOKENS, getMarketplaceItem, resolveItemId, tokenLabel, TOKEN_LOGOS, type MarketplaceItem, type MarketplaceTokenSymbol } from '@/lib/constants/marketplace' @@ -364,6 +365,10 @@ export default function MarketplaceScreen({ onBack, address }: MarketplaceScreen + {/* Connected but on the wrong chain — see WrongNetworkBar. Renders + nothing at all on Celo, and cannot appear inside MiniPay. */} + + {/* ARMORY TRIAL picker — shows once: Act 1 cleared + never granted. Icon-first, two taps + one button, no paragraphs to read. */} {act1Cleared && trialEligible && !isGuest && ( diff --git a/components/game/SeasonPassScreen.tsx b/components/game/SeasonPassScreen.tsx index 3942304..84378ad 100644 --- a/components/game/SeasonPassScreen.tsx +++ b/components/game/SeasonPassScreen.tsx @@ -5,6 +5,7 @@ import { usePassSBT, SeasonInfo } from '@/hooks/usePassSBT' import { getUserFriendlyError } from '@/lib/errorUtils' import SeasonPassCard from './SeasonPassCard' import { SEASON_IDS, currentSeasonId } from '@/lib/season' +import WrongNetworkBar from './WrongNetworkBar' // SEASON_IDS / currentSeasonId moved to lib/season.ts — the world map shows // pass status too now, and two copies of "which season is it" would drift. @@ -184,6 +185,8 @@ export default function SeasonPassScreen({ onBack, address }: SeasonPassScreenPr {/* Not "connect your wallet": inside MiniPay the address arrives with no interaction, so a connect prompt is only ever shown to someone it cannot help. Naming MiniPay is the actionable step. */} + + {!address && (
Open NullState in MiniPay to mint a pass. diff --git a/components/game/WrongNetworkBar.tsx b/components/game/WrongNetworkBar.tsx new file mode 100644 index 0000000..775e217 --- /dev/null +++ b/components/game/WrongNetworkBar.tsx @@ -0,0 +1,62 @@ +'use client' + +import { useCallback, useState } from 'react' +import { useWallet } from '@/lib/WalletProvider' + +// ─── Connected, but not on Celo ────────────────────────────────────────────── +// +// OWNER, testing in the OKX in-app browser: *"posisi auto connect tapi gabisa +// mint pass dan buy ada tulisan 'wallet not connected'."* +// +// His wallet was connected. It was on OKX's default chain, and wagmi's +// getConnectorClient throws ConnectorChainMismatchError when the connector's +// chain is not the one asked for — so useWalletClient({chainId: CELO}) resolved +// to undefined and every payment reported a connection problem that did not +// exist. +// +// WagmiIsland now asks the wallet to switch the moment it lands on the wrong +// chain, which fixes it for anyone who taps Approve. This bar is for the rest: +// someone who declined, or a wallet that ignores the request. It says the true +// thing and gives them the one control that fixes it. +// +// Rendered only on the three screens that can actually spend. Not global, +// because a player who never opens the shop does not need to be told which +// chain the shop wants — and never inside MiniPay, which cannot reach this +// state at all. +// +// The copy deliberately avoids the phrase check:copy bans. It also names Celo +// rather than saying "wrong network", because "wrong" without a target is a +// complaint rather than an instruction. + +export default function WrongNetworkBar() { + const { wrongNetwork, switchToCelo } = useWallet() + const [busy, setBusy] = useState(false) + + const onSwitch = useCallback(async () => { + setBusy(true) + try { + await switchToCelo() + } finally { + // The wallet owns the prompt, so there is nothing to await past the + // request. Clearing shortly after keeps a declined prompt from leaving + // the button stuck. + setTimeout(() => setBusy(false), 1200) + } + }, [switchToCelo]) + + if (!wrongNetwork) return null + + return ( +
+
Your wallet is on another network. NullState runs on Celo.
+ +
+ ) +} diff --git a/hooks/usePassSBT.ts b/hooks/usePassSBT.ts index 6300cd1..4d0da60 100644 --- a/hooks/usePassSBT.ts +++ b/hooks/usePassSBT.ts @@ -28,7 +28,7 @@ export function usePassSBT(walletAddress: string | undefined) { // stablecoin, to TREASURY_WALLET (see lib/WalletProvider.tsx). Reused // here for the v2.1 flexible-payment pass mint flow — see // mintPaidPassFlexible below. - const { payToTreasury, publicClient, walletClient } = useWallet() + const { payToTreasury, publicClient, walletClient, wrongNetwork } = useWallet() const [hasPass, setHasPass] = useState(false) const [passSeasonId, setPassSeasonId] = useState(BigInt(0)) @@ -145,6 +145,14 @@ export function usePassSBT(walletAddress: string | undefined) { // charged varies based on the wallet's balances. const mintPaidPassFlexible = useCallback( async (seasonId: bigint): Promise<{ success: boolean; mintTxHash: `0x${string}` }> => { + // The message the owner actually hit in the OKX browser. walletClient is + // undefined on any chain but Celo — wagmi's getConnectorClient throws + // ConnectorChainMismatchError, useWalletClient swallows it into + // `data: undefined` — so "not connected" was reported to someone whose + // wallet was connected and one tap from working. Name the real problem. + if (wrongNetwork) { + throw new Error('Wrong network — NullState runs on Celo. Switch your wallet to Celo and try again.') + } if (!walletClient || !publicClient || !walletClient.account) { throw new Error('Wallet not connected') } @@ -198,7 +206,7 @@ export function usePassSBT(walletAddress: string | undefined) { setPayingToken(null) } }, - [walletClient, publicClient, payToTreasury, fetchPassStatus], + [walletClient, publicClient, payToTreasury, fetchPassStatus, wrongNetwork], ) useEffect(() => { diff --git a/hooks/useReward.ts b/hooks/useReward.ts index fd8304e..c2e0388 100644 --- a/hooks/useReward.ts +++ b/hooks/useReward.ts @@ -32,7 +32,7 @@ export function useReward(walletAddress: string | undefined) { // WagmiProviderNotFoundError — which is exactly what it did: opening Rewards // in MiniPay died with "a client-side exception has occurred". Both are null // until wagmi has loaded, and every path below already guards on that. - const { publicClient, walletClient } = useWallet() + const { publicClient, walletClient, wrongNetwork } = useWallet() const [seasonLeaderboard, setSeasonLeaderboard] = useState(null) const [hasClaimedSeasonBonus, setHasClaimedSeasonBonus] = useState(false) @@ -98,6 +98,11 @@ export function useReward(walletAddress: string | undefined) { // `account` is narrower on viem's generic WalletClient than on the one // wagmi's useWalletClient() returned, so it is checked here rather than // asserted — a client with no account cannot sign anyway. + // Same wrong-chain trap as the pass mint: on any chain but Celo the + // wallet client is undefined and this used to blame the connection. + if (wrongNetwork) { + throw new Error('Wrong network — NullState runs on Celo. Switch your wallet to Celo and try again.') + } if (!walletClient?.account || !publicClient) throw new Error('Wallet not connected') const account = walletClient.account @@ -128,7 +133,7 @@ export function useReward(walletAddress: string | undefined) { setIsLoading(false) } }, - [walletClient, publicClient, currentSeason, checkSeasonBonusClaimed], + [walletClient, publicClient, currentSeason, checkSeasonBonusClaimed, wrongNetwork], ) useEffect(() => { diff --git a/lib/WagmiIsland.tsx b/lib/WagmiIsland.tsx index 2bbb0e3..b9090c0 100644 --- a/lib/WagmiIsland.tsx +++ b/lib/WagmiIsland.tsx @@ -22,7 +22,7 @@ import { celo, celoSepolia } from 'wagmi/chains' import { celoTransport } from '@/lib/celoRpc' import { injected } from 'wagmi/connectors' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { encodeFunctionData } from 'viem' import { USDM_ADDRESS, USDM_ABI } from './contract-abi' import { getUserFriendlyError, WalletFriendlyError, MINIPAY_ADD_CASH_URL } from './errorUtils' @@ -86,7 +86,7 @@ export default function WagmiIsland() { export function WagmiWalletIsland() { const { address, isConnected, chain } = useAccount() const { disconnect } = useDisconnect() - const { switchChain } = useSwitchChain() + const { switchChain, switchChainAsync } = useSwitchChain() const publicClient = usePublicClient({ chainId: CELO_CHAIN_ID }) const { data: walletClient } = useWalletClient({ chainId: CELO_CHAIN_ID }) const { data: balanceData } = useBalance({ address, chainId: CELO_CHAIN_ID }) @@ -115,14 +115,60 @@ export function WagmiWalletIsland() { if (injectedConnector) connect({ connector: injectedConnector, chainId: CELO_CHAIN_ID }) }, [isConnected, connect, connectors]) + // ── AND ONTO CELO, IMMEDIATELY ─────────────────────────────────────────── + // + // OWNER, testing in the OKX in-app browser: *"posisi auto connect tapi gabisa + // mint pass dan buy ada tulisan 'wallet not connected'."* The wallet was + // connected. It was on OKX's own default chain. + // + // That is fatal rather than cosmetic, because of one line inside wagmi + // (getConnectorClient.js): + // + // if (assertChainId && connectorChainId !== chainId) + // throw new ConnectorChainMismatchError(...) + // + // useWalletClient({chainId: CELO}) therefore resolves to undefined on any + // other chain, every payment hits its `!walletClient` guard, and the player + // is told their wallet is not connected. `switchToCelo` already existed for + // exactly this — plumbed through the bridge and never called from anywhere. + // + // Owner's decision, because MiniPay's reviewers may well test in a browser + // wallet: *"itu bener2 autoconnect dan autoswitch jaringan ke celo dr awal + // login dengan wallet."* So the switch is requested the moment a connection + // lands on the wrong chain, not deferred to the first purchase. + // + // Attempted ONCE per chain the wallet lands on. A player who declines is not + // asked again on every render — that is a popup loop, and it is worse than + // the bug. They get the banner and its button instead, and moving the wallet + // to another chain arms one fresh attempt. + const askedFor = useRef(null) + const onWrongChain = isConnected && !!chain && chain.id !== CELO_CHAIN_ID + useEffect(() => { + if (!onWrongChain || !chain) return + if (askedFor.current === chain.id) return + askedFor.current = chain.id + try { switchChain({ chainId: CELO_CHAIN_ID }) } catch { /* declined or unsupported */ } + }, [onWrongChain, chain, switchChain]) + const celoBalance = balanceData ? (Number(balanceData.value) / 10 ** balanceData.decimals).toFixed(2) : '0.00' // ── Send transaction helper ─────────────────────────────────────────────── + // "Wallet not connected" was a lie on every wrong-chain failure, and it sent + // the owner looking for a connection problem that did not exist. One helper, + // so all three payment paths tell the same truth. + const assertReady = useCallback(() => { + if (onWrongChain) { + throw new Error('Wrong network — NullState runs on Celo. Switch your wallet to Celo and try again.') + } + if (!walletClient || !address) throw new Error('Wallet not connected') + }, [onWrongChain, walletClient, address]) + const sendTx = useCallback( async (data: `0x${string}`, value?: bigint): Promise => { + assertReady() if (!walletClient || !address) throw new Error('Wallet not connected') setError(null) setInsufficientFunds(false) @@ -152,7 +198,7 @@ export function WagmiWalletIsland() { throw new WalletFriendlyError(friendlyError, e) } }, - [walletClient, publicClient, address] + [walletClient, publicClient, address, assertReady] ) // ── Contract write functions ────────────────────────────────────────────── @@ -171,6 +217,7 @@ export function WagmiWalletIsland() { // directly to `toAddress` (the reward contract), NOT through NullState.sol. const payUsdmFee = useCallback( async (amountWei: bigint, toAddress: `0x${string}`): Promise => { + assertReady() if (!walletClient || !address) throw new Error('Wallet not connected') if (typeof amountWei !== 'bigint' || amountWei <= BigInt(0)) { throw new Error('Invalid USDm fee amount') @@ -204,7 +251,7 @@ export function WagmiWalletIsland() { throw new WalletFriendlyError(friendlyError, e) } }, - [walletClient, publicClient, address] + [walletClient, publicClient, address, assertReady] ) // ── Generic treasury payment ───────────────────────────────────────────── @@ -218,6 +265,7 @@ export function WagmiWalletIsland() { // under both names below so each call site reads clearly. const payToTreasury = useCallback( async (priceUsd: number, token: MarketplaceTokenSymbol): Promise => { + assertReady() if (!walletClient || !address) throw new Error('Wallet not connected') const cfg = MARKETPLACE_TOKENS[token] if (!cfg) throw new Error('Unsupported token') @@ -249,7 +297,7 @@ export function WagmiWalletIsland() { throw new WalletFriendlyError(friendlyError, e) } }, - [walletClient, address] + [walletClient, address, assertReady] ) // NOTE (removed 2026-07-12): `attackRaid`, `readPlayer`, and `readRaid` @@ -264,15 +312,27 @@ export function WagmiWalletIsland() { if (target) connect({ connector: target, chainId: CELO_CHAIN_ID }) }, [connect, connectors]) + // The async variant, because the button in WrongNetworkBar shows a pending + // state and the fire-and-forget `switchChain` resolves instantly whether the + // wallet agreed or not — a spinner that means nothing. A decline is not an + // app error: the bar simply stays up, which is already the honest report. const switchToCelo = useCallback(async () => { - try { switchChain({ chainId: CELO_CHAIN_ID }) } catch { /* user declined */ } - }, [switchChain]) + try { + await switchChainAsync({ chainId: CELO_CHAIN_ID }) + setError(null) + } catch { + // Wallets that cannot switch (some in-app browsers pin their chain) end + // up here. Saying so beats a button that silently does nothing. + setError('Could not switch automatically — please choose Celo in your wallet app.') + } + }, [switchChainAsync]) const value: WalletBridgeValue = useMemo(() => ({ ready: true, address: address ?? null, isConnected, chainId: chain?.id ?? null, + wrongNetwork: onWrongChain, isMiniPay, celoBalance, error, @@ -285,7 +345,7 @@ export function WagmiWalletIsland() { switchToCelo, payUsdmFee, payToTreasury, - }), [address, isConnected, chain?.id, isMiniPay, celoBalance, error, insufficientFunds, + }), [address, isConnected, chain?.id, isMiniPay, onWrongChain, celoBalance, error, insufficientFunds, publicClient, walletClient, connectWallet, disconnect, switchToCelo, payUsdmFee, payToTreasury]) const publish = useWalletPublish() diff --git a/lib/WalletProvider.tsx b/lib/WalletProvider.tsx index 3ffd11c..f4792ab 100644 --- a/lib/WalletProvider.tsx +++ b/lib/WalletProvider.tsx @@ -146,6 +146,10 @@ export function useWallet() { isConnecting: !w.ready, walletReady: w.ready, isMiniPay: w.isMiniPay, + // Connected, wrong chain. Screens that can spend need this because it is + // the difference between "you have no wallet" and "your wallet is one tap + // away from working" — and until now both reported the former. + wrongNetwork: w.wrongNetwork, celoBalance: w.celoBalance, error: w.error, insufficientFunds: w.insufficientFunds, diff --git a/lib/walletBridge.tsx b/lib/walletBridge.tsx index 9db0e51..e2b2ad6 100644 --- a/lib/walletBridge.tsx +++ b/lib/walletBridge.tsx @@ -41,6 +41,19 @@ export interface WalletBridgeValue { isConnected: boolean chainId: number | null isMiniPay: boolean + /** + * Connected, but the wallet is sitting on some other chain. + * + * NOT a cosmetic flag. wagmi's getConnectorClient throws + * ConnectorChainMismatchError when the connector's chain is not the one + * asked for, so useWalletClient({chainId: CELO}) resolves to undefined and + * every payment path fails its `!walletClient` guard — which used to report + * "Wallet not connected" to somebody whose wallet was perfectly connected. + * OKX and most browser wallets open on their own default chain, so this is + * the normal state for them until the switch lands. MiniPay is Celo-only and + * can never be in it. + */ + wrongNetwork: boolean celoBalance: string error: string | null insufficientFunds: boolean @@ -72,6 +85,7 @@ export const WALLET_BRIDGE_DEFAULT: WalletBridgeValue = { isConnected: false, chainId: null, isMiniPay: false, + wrongNetwork: false, celoBalance: '0.00', error: null, insufficientFunds: false, diff --git a/package.json b/package.json index de72820..9ddd7e0 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ "test:exit-streak": "node scripts/test-exit-streak.js", "test:privy": "node scripts/test-privy-gate.js --serve", "test:kills": "node scripts/test-leaderboard-writes.js", - "test:proploot": "node scripts/test-prop-loot.js" + "test:proploot": "node scripts/test-prop-loot.js", + "test:network": "node scripts/test-wrong-network.js" }, "overrides": { "permissionless": { diff --git a/scripts/test-wrong-network.js b/scripts/test-wrong-network.js new file mode 100644 index 0000000..b5cecca --- /dev/null +++ b/scripts/test-wrong-network.js @@ -0,0 +1,109 @@ +#!/usr/bin/env node +/** + * test-wrong-network.js — "Wallet not connected" was a lie. + * + * OWNER, testing in the OKX in-app browser: *"posisi auto connect tapi gabisa + * mint pass dan buy ada tulisan 'wallet not connected', itu kenapa?"* + * + * His wallet was connected. It was on OKX's default chain, and this line lives + * inside wagmi (@wagmi/core/actions/getConnectorClient): + * + * if (assertChainId && connectorChainId !== chainId) + * throw new ConnectorChainMismatchError(...) + * + * So useWalletClient({chainId: CELO_CHAIN_ID}) resolves to undefined on ANY + * other chain, every payment path hit its `!walletClient` guard, and the player + * was told they had no wallet. MiniPay is Celo-only and never saw it, which is + * why it survived to the first OKX test. + * + * The nastiest part was already-written-and-never-called: `switchToCelo` had + * been plumbed through the bridge and had no caller anywhere in the app. + * + * These are source-level assertions rather than a browser run, deliberately. + * Reproducing this needs a wallet that connects on a non-Celo chain, and the + * sandbox has no wallet at all — so what can honestly be checked is that the + * guarantees exist, are wired, and cannot quietly come undone. + * + * node scripts/test-wrong-network.js + */ +const fs = require('fs'), path = require('path') + +let fails = 0 +const ok = (l, c, d) => { console.log((c ? ' ✓ ' : ' ✗ FAIL: ') + l + (d !== undefined ? ' (' + d + ')' : '')); if (!c) fails++ } +const read = (p) => fs.readFileSync(path.join(__dirname, '..', p), 'utf8') + +const island = read('lib/WagmiIsland.tsx') +const bridge = read('lib/walletBridge.tsx') +const provider = read('lib/WalletProvider.tsx') +const bar = read('components/game/WrongNetworkBar.tsx') + +// ── the upstream rule this whole file exists because of ───────────────────── +// Asserted against wagmi's own source, so a future upgrade that changes the +// behaviour shows up here rather than in a player's failed purchase. +const wagmi = fs.readFileSync( + path.join(__dirname, '..', 'node_modules/@wagmi/core/dist/esm/actions/getConnectorClient.js'), 'utf8') +ok('wagmi still throws when the connector is on another chain', + /assertChainId && connectorChainId !== chainId/.test(wagmi) && + /ConnectorChainMismatchError/.test(wagmi)) +ok('and the app still asks for a Celo-pinned wallet client, which is what trips it', + /useWalletClient\(\{ chainId: CELO_CHAIN_ID \}\)/.test(island)) + +// ── the fix: ask, once per chain ──────────────────────────────────────────── +ok('a connection on the wrong chain triggers a switch', + /const onWrongChain = isConnected && !!chain && chain\.id !== CELO_CHAIN_ID/.test(island) && + /switchChain\(\{ chainId: CELO_CHAIN_ID \}\)/.test(island)) +// Asking on every render is a popup loop, which is worse than the bug it fixes. +ok('and only ONCE per chain, never in a render loop', + /askedFor = useRef\(null\)/.test(island) && + /if \(askedFor\.current === chain\.id\) return/.test(island) && + /askedFor\.current = chain\.id/.test(island)) + +// ── the message stops lying ───────────────────────────────────────────────── +// sendTx, payUsdmFee and payToTreasury — every path in the island that signs. +ok('all three payment paths in the island check the network first', + (island.match(/^\s+assertReady\(\)$/gm) || []).length === 3, + String((island.match(/^\s+assertReady\(\)$/gm) || []).length)) +ok('and the message names Celo instead of claiming there is no wallet', + /Wrong network — NullState runs on Celo/.test(island)) + +// The two hooks that guard on walletClient THEMSELVES rather than going +// through the island — the pass mint is the exact screen the owner reported, +// and the reward claim has the identical shape. +for (const [hook, file] of [['the Season Pass mint', 'hooks/usePassSBT.ts'], + ['the reward claim', 'hooks/useReward.ts']]) { + const src = read(file) + ok(`${hook} reports the network too, not a missing wallet`, + /if \(wrongNetwork\) \{/.test(src) && /Wrong network — NullState runs on Celo/.test(src)) + ok(` …and re-runs when the chain changes`, /wrongNetwork\]/.test(src) || /wrongNetwork,/.test(src)) +} + +// ── the escape hatch is finally wired ─────────────────────────────────────── +// switchToCelo existed, was plumbed through the bridge, and had no caller. +ok('switchToCelo is awaited, so the button can show a truthful pending state', + /await switchChainAsync\(\{ chainId: CELO_CHAIN_ID \}\)/.test(island)) +ok('and something actually calls it now', + /switchToCelo\(\)/.test(bar), 'WrongNetworkBar') + +// ── the flag reaches the screens that can spend ───────────────────────────── +ok('wrongNetwork is on the bridge', /wrongNetwork: boolean/.test(bridge)) +ok('defaulted false, so nothing shows before wagmi mounts', /wrongNetwork: false,/.test(bridge)) +ok('and published through WalletProvider', /wrongNetwork: w\.wrongNetwork,/.test(provider)) + +for (const screen of ['MarketplaceScreen', 'CraftingScreen', 'SeasonPassScreen']) { + const src = read(`components/game/${screen}.tsx`) + ok(`${screen} shows the bar`, //.test(src) && + /import WrongNetworkBar from '\.\/WrongNetworkBar'/.test(src)) +} + +// ── and it cannot become a screen MiniPay would reject ────────────────────── +// MiniPay is Celo-only, so wrongNetwork is unreachable there — but the copy +// rule is checked anyway, because that is the check that would catch a future +// edit adding the banned phrase to this file. +ok('the bar never uses the phrase MiniPay bans', + !/connect\s+(a|an|the|your|my)?\s*wallet/i.test(bar)) +ok('and it renders nothing at all when the network is right', + /if \(!wrongNetwork\) return null/.test(bar)) + +console.log(fails ? ` ${fails} GAGAL` : ' semua lolos') +console.log(' NOTE: a real wrong-chain wallet is NOT simulated — the sandbox has no wallet.') +process.exit(fails ? 1 : 0)