Skip to content

Commit 4d8efe5

Browse files
r4topunkclaude
andauthored
fix(propdates): drop re-added pre-simulate + restore friendly errors (#66)
* fix(propdates): drop re-added pre-simulate + restore friendly errors Thirdweb migration (#59) regressed c85d633 by re-adding the publicClient.simulateContract pre-flight call. Browser eth_call is blocked in CORS-strict, adblock-heavy, and Farcaster miniapp sandbox environments, so the pre-simulate throws "Failed to fetch" before the wallet ever gets a chance to sign. Wallet simulates internally, so the pre-flight is redundant. Also restore the error-message parser so users see the same "Network error reaching Base RPC..." / "Transaction rejected in wallet." toasts as before the migration instead of raw multi-line viem reverts. Closes #65 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(thirdweb-tx): verify chain after switchChain Zerion (and some other wallets) resolve wallet_switchEthereumChain without actually moving the provider, which caused propdate txs to broadcast on Ethereum mainnet even though the UI thought we were on Base. Verify getChain() after the switch and throw a clear "switch network manually" error so callers toast before signing. Benefits every write path (delegate, vote, bid, settle, POIDH, droposals, lootbox, propdates, splits). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(writes): switch chain on admin wallet, not SA wrapper Chain switching targeted the wrong wallet whenever a user signed in "view as EOA" mode on an AA-wrapped session: - THIRDWEB_AA_CONFIG pins the SA wrapper to Base, so useActiveWallet().getChain().id === 8453 always returns true. - ensureOnChain on that wrapper is a noop — switchChain never fires. - The admin wallet (the external Zerion/MetaMask provider that actually signs EOA transactions) stays on its current chain. - sendTransaction then broadcasts on whatever chain the admin provider has selected — Ethereum mainnet in the reported case. Fix: useWriteAccount now returns the admin wallet when isEoaSigner, so ensureOnChain operates on the provider that actually matters. useEoaDelegate is updated the same way. Auction bid/settle and propdates no longer pass the raw useActiveWallet() — they use writer.wallet consistently. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a8e4a40 commit 4d8efe5

6 files changed

Lines changed: 74 additions & 83 deletions

File tree

src/components/auction/AuctionBidForm.tsx

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,32 @@
11
"use client";
22

33
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4-
import { ChevronDown, MessageSquare, Wallet } from "lucide-react";
5-
import { concat, encodeFunctionData, formatEther, type Hex, parseEther, toHex } from "viem";
6-
import { base as wagmiBase } from "wagmi/chains";
7-
import { useBalance } from "wagmi";
84
import { useQueryClient } from "@tanstack/react-query";
5+
import { ChevronDown, MessageSquare, Wallet } from "lucide-react";
6+
import { toast } from "sonner";
97
import { getContract, prepareContractCall, prepareTransaction, sendTransaction } from "thirdweb";
108
import { base } from "thirdweb/chains";
11-
import {
12-
useActiveWallet,
13-
useActiveWalletChain,
14-
useConnectModal,
15-
} from "thirdweb/react";
9+
import { useActiveWalletChain, useConnectModal } from "thirdweb/react";
10+
import { concat, encodeFunctionData, formatEther, parseEther, toHex, type Hex } from "viem";
11+
import { useBalance } from "wagmi";
12+
import { base as wagmiBase } from "wagmi/chains";
1613
import { Button } from "@/components/ui/button";
14+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
1715
import {
1816
InputGroup,
1917
InputGroupAddon,
2018
InputGroupInput,
2119
InputGroupText,
2220
} from "@/components/ui/input-group";
2321
import { Spinner } from "@/components/ui/spinner";
24-
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible";
22+
import { useAuctionTransaction } from "@/hooks/use-auction-transaction";
23+
import { useUserAddress } from "@/hooks/use-user-address";
24+
import { useWriteAccount } from "@/hooks/use-write-account";
2525
import { DAO_ADDRESSES } from "@/lib/config";
2626
import { getThirdwebClient } from "@/lib/thirdweb";
2727
import { ensureOnChain } from "@/lib/thirdweb-tx";
2828
import { THIRDWEB_AA_CONFIG, THIRDWEB_WALLETS } from "@/lib/thirdweb-wallets";
2929
import auctionAbi from "@/utils/abis/auctionAbi";
30-
import { toast } from "sonner";
31-
import { useAuctionTransaction } from "@/hooks/use-auction-transaction";
32-
import { useUserAddress } from "@/hooks/use-user-address";
33-
import { useWriteAccount } from "@/hooks/use-write-account";
3430

3531
interface AuctionBidFormProps {
3632
tokenId: bigint | undefined;
@@ -50,7 +46,6 @@ export function AuctionBidForm({
5046
}: AuctionBidFormProps) {
5147
const { address, isConnected } = useUserAddress();
5248
const activeChain = useActiveWalletChain();
53-
const wallet = useActiveWallet();
5449
const writer = useWriteAccount();
5550
const { connect: openConnectModal } = useConnectModal();
5651
const queryClient = useQueryClient();
@@ -188,7 +183,7 @@ export function AuctionBidForm({
188183
pendingBidRef.current = { comment: trimmedComment, amount: bidAmount };
189184

190185
await bidTx.execute(async () => {
191-
await ensureOnChain(wallet, base);
186+
await ensureOnChain(writer.wallet, base);
192187

193188
if (trimmedComment.length > 0) {
194189
const baseCalldata = encodeFunctionData({
@@ -266,7 +261,7 @@ export function AuctionBidForm({
266261
}
267262
if (isWrongNetwork) {
268263
try {
269-
await ensureOnChain(wallet, base);
264+
await ensureOnChain(writer?.wallet, base);
270265
handleBid();
271266
} catch {
272267
// User rejected
@@ -352,7 +347,6 @@ export function AuctionBidForm({
352347
</div>
353348
</CollapsibleContent>
354349
</Collapsible>
355-
356350
</>
357351
);
358352
}

src/components/auction/AuctionSettleButton.tsx

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,24 @@
11
"use client";
22

33
import { useCallback, useEffect, useRef } from "react";
4-
import { Wallet } from "lucide-react";
5-
import { useReadContract, useSimulateContract } from "wagmi";
64
import { useQueryClient } from "@tanstack/react-query";
5+
import { Wallet } from "lucide-react";
6+
import { toast } from "sonner";
77
import { getContract, prepareContractCall, sendTransaction } from "thirdweb";
88
import { base } from "thirdweb/chains";
9-
import {
10-
useActiveWallet,
11-
useActiveWalletChain,
12-
useConnectModal,
13-
} from "thirdweb/react";
9+
import { useActiveWalletChain, useConnectModal } from "thirdweb/react";
10+
import { useReadContract, useSimulateContract } from "wagmi";
1411
import { Button } from "@/components/ui/button";
1512
import { Spinner } from "@/components/ui/spinner";
1613
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
14+
import { useAuctionTransaction } from "@/hooks/use-auction-transaction";
15+
import { useUserAddress } from "@/hooks/use-user-address";
16+
import { useWriteAccount } from "@/hooks/use-write-account";
1717
import { CHAIN, DAO_ADDRESSES } from "@/lib/config";
1818
import { getThirdwebClient } from "@/lib/thirdweb";
1919
import { ensureOnChain } from "@/lib/thirdweb-tx";
2020
import { THIRDWEB_AA_CONFIG, THIRDWEB_WALLETS } from "@/lib/thirdweb-wallets";
2121
import auctionAbi from "@/utils/abis/auctionAbi";
22-
import { toast } from "sonner";
23-
import { useAuctionTransaction } from "@/hooks/use-auction-transaction";
24-
import { useUserAddress } from "@/hooks/use-user-address";
25-
import { useWriteAccount } from "@/hooks/use-write-account";
2622

2723
interface AuctionSettleButtonProps {
2824
/** Whether the connected wallet is the auction winner */
@@ -35,7 +31,6 @@ export function AuctionSettleButton({ isWinner }: AuctionSettleButtonProps) {
3531

3632
const { address: userAddress, isConnected } = useUserAddress();
3733
const activeChain = useActiveWalletChain();
38-
const wallet = useActiveWallet();
3934
const writer = useWriteAccount();
4035
const { connect: openConnectModal } = useConnectModal();
4136
const queryClient = useQueryClient();
@@ -134,7 +129,7 @@ export function AuctionSettleButton({ isWinner }: AuctionSettleButtonProps) {
134129
const methodName = isPaused ? "settleAuction" : "settleCurrentAndCreateNewAuction";
135130

136131
await settleTx.execute(async () => {
137-
await ensureOnChain(wallet, base);
132+
await ensureOnChain(writer.wallet, base);
138133

139134
const contract = getContract({
140135
client,
@@ -215,7 +210,6 @@ export function AuctionSettleButton({ isWinner }: AuctionSettleButtonProps) {
215210
</TooltipContent>
216211
)}
217212
</Tooltip>
218-
219213
</>
220214
);
221215
}

src/hooks/use-eoa-delegate.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,10 @@
22

33
import { useCallback, useState } from "react";
44
import { toast } from "sonner";
5-
import {
6-
getContract,
7-
prepareContractCall,
8-
sendTransaction,
9-
waitForReceipt,
10-
} from "thirdweb";
5+
import { getContract, prepareContractCall, sendTransaction, waitForReceipt } from "thirdweb";
116
import { base } from "thirdweb/chains";
12-
import { useActiveWallet } from "thirdweb/react";
13-
import { type Address, type Hex, isAddress } from "viem";
7+
import { useActiveWallet, useAdminWallet } from "thirdweb/react";
8+
import { isAddress, type Address, type Hex } from "viem";
149
import { DAO_ADDRESSES } from "@/lib/config";
1510
import { getThirdwebClient } from "@/lib/thirdweb";
1611
import { ensureOnChain, normalizeTxError } from "@/lib/thirdweb-tx";
@@ -41,6 +36,7 @@ interface UseEoaDelegateArgs {
4136
*/
4237
export function useEoaDelegate({ onSubmitted, onSuccess }: UseEoaDelegateArgs = {}) {
4338
const wallet = useActiveWallet();
39+
const adminWallet = useAdminWallet();
4440
const [isPending, setIsPending] = useState(false);
4541
const [isConfirming, setIsConfirming] = useState(false);
4642
const [isConfirmed, setIsConfirmed] = useState(false);
@@ -100,10 +96,11 @@ export function useEoaDelegate({ onSubmitted, onSuccess }: UseEoaDelegateArgs =
10096

10197
try {
10298
// Switch the underlying wallet's chain explicitly. With AA on the
103-
// active wallet is the smart wallet; switching its chain does not
104-
// always propagate down to the EIP1193 provider that actually signs,
105-
// so we call ensureOnChain on the same Wallet instance here.
106-
await ensureOnChain(wallet, base);
99+
// active wallet is the smart wallet (pinned to Base by the AA
100+
// config), so switching its chain is a no-op and does NOT move the
101+
// admin EOA provider that actually signs. Use the admin wallet
102+
// directly so the Zerion / MetaMask chain actually changes.
103+
await ensureOnChain(adminWallet ?? wallet, base);
107104

108105
const contract = getContract({
109106
client,
@@ -160,7 +157,7 @@ export function useEoaDelegate({ onSubmitted, onSuccess }: UseEoaDelegateArgs =
160157
toast.error("Delegation failed", { description: message });
161158
}
162159
},
163-
[wallet, onSubmitted, onSuccess],
160+
[wallet, adminWallet, onSubmitted, onSuccess],
164161
);
165162

166163
return {

src/hooks/use-propdates.ts

Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,15 @@
22

33
import { useCallback, useRef, useState } from "react";
44
import { useQuery, useQueryClient } from "@tanstack/react-query";
5-
import { encodeFunctionData, type Hex } from "viem";
6-
import { usePublicClient } from "wagmi";
75
import { prepareTransaction, sendTransaction, waitForReceipt } from "thirdweb";
86
import { base } from "thirdweb/chains";
9-
import { useActiveWallet } from "thirdweb/react";
7+
import { encodeFunctionData, type Hex } from "viem";
8+
import { useUserAddress } from "@/hooks/use-user-address";
9+
import { useWriteAccount } from "@/hooks/use-write-account";
1010
import { EAS_CONTRACT_ADDRESS, easAbi } from "@/lib/eas";
11-
import { CHAIN } from "@/lib/config";
1211
import { getThirdwebClient } from "@/lib/thirdweb";
1312
import { ensureOnChain } from "@/lib/thirdweb-tx";
1413
import { createPropdate as encodePropdateRequest, listPropdates } from "@/services/propdates";
15-
import { useUserAddress } from "@/hooks/use-user-address";
16-
import { useWriteAccount } from "@/hooks/use-write-account";
1714

1815
interface CreatePropdateInput {
1916
proposalId: string;
@@ -24,9 +21,7 @@ interface CreatePropdateInput {
2421
export function usePropdates(proposalId: string) {
2522
const queryClient = useQueryClient();
2623
const { address, isConnected } = useUserAddress();
27-
const wallet = useActiveWallet();
2824
const writer = useWriteAccount();
29-
const publicClient = usePublicClient({ chainId: CHAIN.id });
3025
const [submissionPhase, setSubmissionPhase] = useState<
3126
"idle" | "confirming-wallet" | "pending-tx" | "syncing"
3227
>("idle");
@@ -42,10 +37,7 @@ export function usePropdates(proposalId: string) {
4237
});
4338

4439
const handleCreatePropdate = useCallback(
45-
async (
46-
input: CreatePropdateInput,
47-
options?: { onSuccess?: (txHash: string) => void },
48-
) => {
40+
async (input: CreatePropdateInput, options?: { onSuccess?: (txHash: string) => void }) => {
4941
setCreateError(null);
5042
setHasWriteError(false);
5143
try {
@@ -67,7 +59,7 @@ export function usePropdates(proposalId: string) {
6759
throw new Error("Thirdweb client not configured");
6860
}
6961

70-
await ensureOnChain(wallet, base);
62+
await ensureOnChain(writer.wallet, base);
7163

7264
pendingProposalIdRef.current = targetProposalId;
7365
setSubmissionPhase("confirming-wallet");
@@ -78,24 +70,9 @@ export function usePropdates(proposalId: string) {
7870
input.originalMessageId,
7971
);
8072

81-
if (!publicClient) {
82-
throw new Error("Public client not available");
83-
}
84-
85-
// Keep the simulation on wagmi's publicClient — simulateContract is a
86-
// read (eth_call) and stays on the wagmi side of the migration split.
87-
await publicClient.simulateContract({
88-
address: EAS_CONTRACT_ADDRESS,
89-
abi: easAbi,
90-
functionName: "attest",
91-
// @ts-expect-error - wagmi type inference issue with complex tuple args
92-
args: [attestationRequest],
93-
chainId: CHAIN.id,
94-
});
95-
96-
// Encode the attest call ourselves and send via thirdweb's
97-
// prepareTransaction so we don't have to wrestle with
98-
// prepareContractCall's type inference on the complex tuple arg.
73+
// Skip pre-simulate: browser eth_call fetch can be blocked (CORS,
74+
// adblock, Farcaster miniapp sandbox) and the wallet simulates
75+
// internally before signing. See c85d633.
9976
const attestCalldata = encodeFunctionData({
10077
abi: easAbi,
10178
functionName: "attest",
@@ -133,7 +110,12 @@ export function usePropdates(proposalId: string) {
133110
options?.onSuccess?.(txHash);
134111
return txHash;
135112
} catch (error) {
136-
const message = error instanceof Error ? error.message : "Propdate creation failed";
113+
const raw = error instanceof Error ? error.message : "Propdate creation failed";
114+
const message = raw.includes("Failed to fetch")
115+
? "Network error reaching Base RPC. Check connection or try a different wallet."
116+
: raw.includes("User rejected") || raw.includes("User denied")
117+
? "Transaction rejected in wallet."
118+
: raw.split("\n")[0];
137119
setCreateError(message);
138120
setHasWriteError(true);
139121
pendingProposalIdRef.current = null;
@@ -142,7 +124,7 @@ export function usePropdates(proposalId: string) {
142124
throw error;
143125
}
144126
},
145-
[address, isConnected, proposalId, publicClient, queryClient, wallet, writer],
127+
[address, isConnected, proposalId, queryClient, writer],
146128
);
147129

148130
return {

src/hooks/use-write-account.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
"use client";
22

3-
import { useActiveAccount, useActiveWallet } from "thirdweb/react";
3+
import { useActiveAccount, useActiveWallet, useAdminWallet } from "thirdweb/react";
44
import type { Account, Wallet } from "thirdweb/wallets";
55
import { useUserAddress } from "@/hooks/use-user-address";
66

77
export interface WriteAccount {
88
/** The account object to pass to thirdweb's `sendTransaction`. */
99
account: Account;
10-
/** The active thirdweb wallet — exposed for chain-ensure calls. */
10+
/**
11+
* The wallet that must be on the target chain before signing. For EOA
12+
* signing this is the admin wallet (the external provider that actually
13+
* broadcasts the tx); for SA signing it's the active wallet. Callers
14+
* should pass this to `ensureOnChain`.
15+
*
16+
* Using the active wallet unconditionally hides chain mismatches because
17+
* the SA wrapper is pinned to Base by `THIRDWEB_AA_CONFIG` — so the chain
18+
* check always returned "already on Base" while the admin EOA stayed on
19+
* mainnet and signed there. That caused propdate txs to land on Ethereum.
20+
*/
1121
wallet: Wallet;
1222
/**
1323
* True when the returned account is the admin EOA (i.e. the write will
@@ -37,6 +47,7 @@ export interface WriteAccount {
3747
* ```ts
3848
* const writer = useWriteAccount();
3949
* if (!writer) return;
50+
* await ensureOnChain(writer.wallet, base);
4051
* const result = await sendTransaction({
4152
* account: writer.account,
4253
* transaction: tx,
@@ -50,14 +61,15 @@ export interface WriteAccount {
5061
export function useWriteAccount(): WriteAccount | undefined {
5162
const activeAccount = useActiveAccount();
5263
const wallet = useActiveWallet();
64+
const adminWallet = useAdminWallet();
5365
const { viewMode, canSwitchView } = useUserAddress();
5466

5567
if (!wallet || !activeAccount) return undefined;
5668

5769
if (viewMode === "eoa" && canSwitchView) {
5870
const admin = wallet.getAdminAccount?.();
59-
if (admin) {
60-
return { account: admin, wallet, isEoaSigner: true };
71+
if (admin && adminWallet) {
72+
return { account: admin, wallet: adminWallet, isEoaSigner: true };
6173
}
6274
}
6375

src/lib/thirdweb-tx.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,22 @@ export function normalizeTxError(err: unknown): NormalizedTxError {
6060
/**
6161
* Ensures the active thirdweb wallet is on the requested chain before
6262
* signing. Noop if the wallet is absent or already on the right chain.
63-
* Any switch failure propagates so the caller can decide how to toast.
63+
*
64+
* Verifies the chain after `switchChain` because some wallets (notably
65+
* Zerion) resolve `wallet_switchEthereumChain` without actually moving
66+
* the provider — signing then happens on the stale chain. If the post
67+
* check still reports the wrong chain we throw so callers can show a
68+
* clear "switch network manually" toast instead of broadcasting to the
69+
* wrong network.
6470
*/
6571
export async function ensureOnChain(wallet: Wallet | undefined, chain: Chain): Promise<void> {
6672
if (!wallet) return;
6773
if (wallet.getChain()?.id === chain.id) return;
6874
await wallet.switchChain(chain);
75+
const afterId = wallet.getChain()?.id;
76+
if (afterId !== chain.id) {
77+
throw new Error(
78+
`Wallet did not switch to ${chain.name ?? chain.id} (still on chain ${afterId ?? "unknown"}). Switch network manually in your wallet and retry.`,
79+
);
80+
}
6981
}

0 commit comments

Comments
 (0)