Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion wagmi-disperse/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"dependencies": {
"@tanstack/react-query": "5.80.0",
"fuse.js": "^7.1.0",
"html5-qrcode": "^2.3.8",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"viem": "^2.31.6",
Expand All @@ -38,5 +39,6 @@
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vitest": "^3.2.0"
}
},
"packageManager": "pnpm@10.6.3+sha512.bb45e34d50a9a76e858a95837301bfb6bd6d35aea2c5d52094fa497a467c43f5c440103ce2511e9e0a2f89c3d6071baac3358fc68ac6fb75e2ceb3d2736065e6"
}
8 changes: 8 additions & 0 deletions wagmi-disperse/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

164 changes: 85 additions & 79 deletions wagmi-disperse/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { useCallback, useMemo, useRef, useState } from "react";
import { useCallback, useMemo, useState } from "react";
import { formatUnits } from "viem";
import { useAccount, useBalance, useChainId, useConfig, useConnect } from "wagmi";

import { Suspense, lazy } from "react";
import CurrencySelector from "./components/CurrencySelector";
import Header from "./components/Header";
import NetworkStatus from "./components/NetworkStatus";
import RecipientInput from "./components/RecipientInput";
import TokenLoader from "./components/TokenLoader";
import NetworkSwitcher from "./components/NetworkSwitcher";
import QRRecipientInput from "./components/QRRecipientInput";
import TransactionSection from "./components/TransactionSection";
const DebugPanel = lazy(() => import("./components/debug/DebugPanel"));
import { AppState } from "./constants";
import { AppState, EXPECTED_CHAIN_ID } from "./constants";
import { useAppState } from "./hooks/useAppState";
import { useContractVerification } from "./hooks/useContractVerification";
import { useCurrencySelection } from "./hooks/useCurrencySelection";
import { useTokenAllowance } from "./hooks/useTokenAllowance";
import type { Recipient, TokenInfo } from "./types";
import type { Recipient } from "./types";
import {
getBalance,
getDecimals,
Expand All @@ -26,7 +26,14 @@ import {
getTotalAmount,
} from "./utils/balanceCalculations";
import { canDeployToNetwork } from "./utils/contractVerify";
import { parseRecipients } from "./utils/parseRecipients";

// PNK token constant for Arbitrum Sepolia
const PNK_TOKEN = {
address: "0xA13c3e5f8F19571859F4Ab1003B960a5DF694C10" as `0x${string}`,
symbol: "PNK",
name: "Kleros",
decimals: 18,
};

function App() {
const config = useConfig();
Expand All @@ -36,6 +43,17 @@ function App() {
address,
chainId: chainId,
});

// Log current state for debugging
console.log("[App] Connection state:", { chainId, isConnected, address, status });

// Fetch PNK token balance
const { data: pnkBalanceData } = useBalance({
address,
token: PNK_TOKEN.address,
chainId: chainId,
});

const { connectors, connect } = useConnect();

const isChainSupported = chainId ? config.chains.some((chain) => chain.id === chainId) : false;
Expand All @@ -57,8 +75,8 @@ function App() {
}, []);

const [recipients, setRecipients] = useState<Recipient[]>([]);
const [amount, setAmount] = useState<string>("");
const walletStatus = status === "connected" ? `logged in as ${address}` : "please unlock wallet";
const textareaRef = useRef<HTMLTextAreaElement>(null);

const { sending, token, setSending, setToken } = useCurrencySelection();

Expand All @@ -74,23 +92,6 @@ function App() {
token,
});

const parseAmounts = useCallback(() => {
if (!textareaRef.current) return;

const text = textareaRef.current.value;
const decimals = getDecimals(sending, token);
const newRecipients = parseRecipients(text, decimals);

setRecipients(newRecipients);

if (
newRecipients.length &&
(sending === "ether" || (sending === "token" && token.address && token.decimals !== undefined))
) {
setAppState(AppState.ENTERED_AMOUNTS);
}
}, [sending, token, setAppState]);

const handleRecipientsChange = useCallback(
(newRecipients: Recipient[]) => {
setRecipients(newRecipients);
Expand All @@ -105,57 +106,24 @@ function App() {
[sending, token, setAppState],
);

const resetToken = useCallback(() => {
setToken({});
setAppState(AppState.CONNECTED_TO_WALLET);
}, [setToken, setAppState]);

const selectCurrency = useCallback(
(type: "ether" | "token") => {
setSending(type);

if (type === "ether") {
setAppState(AppState.SELECTED_CURRENCY);
requestAnimationFrame(() => {
if (textareaRef.current?.value) {
parseAmounts();
}
});
} else if (type === "token") {
if (token.address && token.decimals !== undefined && token.symbol) {
setAppState(AppState.SELECTED_CURRENCY);
requestAnimationFrame(() => {
if (textareaRef.current?.value) {
parseAmounts();
}
});
} else {
resetToken();
}
// Auto-populate PNK token
setToken(PNK_TOKEN);
setAppState(AppState.SELECTED_CURRENCY);
}
},
[setSending, setAppState, token, parseAmounts, resetToken],
[setSending, setAppState, setToken],
);

const selectToken = useCallback(
(tokenInfo: TokenInfo) => {
setToken(tokenInfo);
setSending("token");
setAppState(AppState.SELECTED_CURRENCY);

requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (textareaRef.current) {
textareaRef.current.focus();
if (tokenInfo.decimals !== undefined) {
parseAmounts();
}
}
});
});
},
[setToken, setSending, setAppState, parseAmounts],
);
const handleAmountChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setAmount(e.target.value);
}, []);

// Use reactive allowance hook
const { allowance: currentAllowance } = useTokenAllowance({
Expand All @@ -182,6 +150,9 @@ function App() {
const symbol = useMemo(() => getSymbol(sending, token, chainId), [sending, token, chainId]);
const decimals = useMemo(() => getDecimals(sending, token), [sending, token]);
const nativeCurrencyName = useMemo(() => getNativeCurrencyName(chainId), [chainId]);

// Check if on wrong network
const isWrongNetwork = isConnected && chainId !== EXPECTED_CHAIN_ID;

// Display all wallet connectors
const renderConnectors = () => {
Expand Down Expand Up @@ -230,7 +201,11 @@ function App() {
</section>
)}

{appState >= AppState.CONNECTED_TO_WALLET && (
{/* Show network switcher PROMINENTLY if connected but on wrong network - AUTO-SWITCHES */}
{isConnected && isWrongNetwork && <NetworkSwitcher currentChainId={chainId} />}

{/* Only show currency selector and beyond if on CORRECT network */}
{appState >= AppState.CONNECTED_TO_WALLET && !isWrongNetwork && (
<section>
<CurrencySelector onSelect={selectCurrency} />
{sending === "ether" && (
Expand All @@ -242,19 +217,30 @@ function App() {
</section>
)}

{appState >= AppState.CONNECTED_TO_WALLET && sending === "token" && (
{appState >= AppState.SELECTED_CURRENCY && !isWrongNetwork && (
<section>
<TokenLoader
onSelect={selectToken}
onError={resetToken}
chainId={chainId}
account={address}
token={token}
contractAddress={verifiedAddress?.address}
/>
{token.symbol && (
<h2>amount to send</h2>
<p>Enter the amount in {symbol} to send to each address (up to 18 decimals).</p>
<div className="shadow">
<input
type="text"
value={amount}
onChange={handleAmountChange}
placeholder={`0.0 ${symbol}`}
className="amount-input"
pattern="[0-9]*\.?[0-9]*"
/>
</div>
{sending === "ether" && (
<p className="mt">
you have {formatUnits(balanceData?.value || 0n, 18)} {nativeCurrencyName}
{balanceData?.value === 0n && chainId && <span className="warning">(make sure to add funds)</span>}
</p>
)}
{sending === "token" && token.symbol && (
<p className="mt">
you have {formatUnits(token.balance || 0n, token.decimals || 18)} {token.symbol}
you have {formatUnits(pnkBalanceData?.value || 0n, token.decimals || 18)} {token.symbol}
{pnkBalanceData?.value === 0n && chainId && <span className="warning">(make sure to add funds)</span>}
</p>
)}
</section>
Expand All @@ -264,13 +250,19 @@ function App() {
1. Ether is selected and we're connected to a supported wallet/network, or
2. We're in SELECTED_CURRENCY state or higher (any currency),
3. Token is selected and we have a valid token (with symbol)
BUT never show when on an unsupported network (NETWORK_UNAVAILABLE state)
BUT never show when on an unsupported network (NETWORK_UNAVAILABLE state) or WRONG network
*/}
{appState !== AppState.NETWORK_UNAVAILABLE &&
{!isWrongNetwork &&
appState !== AppState.NETWORK_UNAVAILABLE &&
((appState >= AppState.CONNECTED_TO_WALLET && sending === "ether") ||
appState >= AppState.SELECTED_CURRENCY ||
(sending === "token" && !!token.symbol)) && (
<RecipientInput sending={sending} token={token} onRecipientsChange={handleRecipientsChange} />
<QRRecipientInput
sending={sending}
token={token}
amount={amount}
onRecipientsChange={handleRecipientsChange}
/>
)}

{appState >= AppState.ENTERED_AMOUNTS && (
Expand All @@ -289,6 +281,7 @@ function App() {
account={address}
nativeCurrencyName={nativeCurrencyName}
effectiveAllowance={effectiveAllowance}
isWrongNetwork={isWrongNetwork}
/>
)}

Expand All @@ -312,6 +305,19 @@ function App() {
recipientsCount={recipients.length}
/>
</Suspense>

<footer className="app-footer">
<p>
Built on{" "}
<a href="https://disperse.app" target="_blank" rel="noopener noreferrer">
disperse.app
</a>
{" "}by{" "}
<a href="https://x.com/bantg" target="_blank" rel="noopener noreferrer">
banteg
</a>
</p>
</footer>
</article>
);
}
Expand Down
10 changes: 2 additions & 8 deletions wagmi-disperse/src/components/CurrencySelector.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import { type ChangeEvent, useState } from "react";
import { useChainId } from "wagmi";
import { nativeCurrencyName } from "../networks";

interface CurrencySelectorProps {
onSelect: (type: "ether" | "token") => void;
}

const CurrencySelector = ({ onSelect }: CurrencySelectorProps) => {
const [selectedCurrency, setSelectedCurrency] = useState<"ether" | "token">("ether");
const chainId = useChainId();

// Get native currency name for display
const nativeCurrency = nativeCurrencyName(chainId);

// Don't auto-select ether on mount - this causes issues when switching back from token
// The parent component should control the initial state instead
Expand All @@ -33,7 +27,7 @@ const CurrencySelector = ({ onSelect }: CurrencySelectorProps) => {
checked={selectedCurrency === "ether"}
onChange={handleChange}
/>
<label htmlFor="ether">{nativeCurrency}</label>
<label htmlFor="ether">Ether</label>
<span>or</span>
<input
type="radio"
Expand All @@ -43,7 +37,7 @@ const CurrencySelector = ({ onSelect }: CurrencySelectorProps) => {
checked={selectedCurrency === "token"}
onChange={handleChange}
/>
<label htmlFor="token">token</label>
<label htmlFor="token">PNK</label>
</div>
);
};
Expand Down
11 changes: 10 additions & 1 deletion wagmi-disperse/src/components/DeployContract.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useState } from "react";
import type { BaseError } from "viem";
import { useBytecode, useWaitForTransactionReceipt, useWriteContract } from "wagmi";
import { useBytecode, useChainId, useWaitForTransactionReceipt, useWriteContract } from "wagmi";
import { EXPECTED_CHAIN_ID } from "../constants";
import { disperse_createx } from "../deploy";
import { createXAbi } from "../generated";
import { explorerTx, networkName } from "../networks";
Expand All @@ -18,6 +19,7 @@ const DeployContract = ({ chainId, onSuccess }: DeployContractProps) => {
const [txHash, setTxHash] = useState<`0x${string}` | null>(null);
const [errorMessage, setErrorMessage] = useState("");
const [deployedAddress, setDeployedAddress] = useState<`0x${string}` | null>(null);
const currentChainId = useChainId();

// Use CreateX to deploy the contract - use generic writeContract to set address for any chain
const { writeContract, isPending, isError, error, data: contractWriteData } = useWriteContract();
Expand Down Expand Up @@ -86,6 +88,13 @@ const DeployContract = ({ chainId, onSuccess }: DeployContractProps) => {
setIsDeploying(true);
setErrorMessage("");

// CRITICAL: Verify we're on the correct network before deployment
if (currentChainId !== EXPECTED_CHAIN_ID) {
setErrorMessage(`Wrong network! Please switch to Arbitrum Sepolia (chain ID ${EXPECTED_CHAIN_ID}). Currently on chain ${currentChainId}.`);
setIsDeploying(false);
return;
}

// If contract is already deployed at expected address, just notify success
if (isAlreadyDeployed) {
console.log("Contract already deployed at expected address:", expectedAddress);
Expand Down
Loading