diff --git a/base-account/base-account-wagmi-template/src/app/globals.css b/base-account/base-account-wagmi-template/src/app/globals.css index 0733a7ee..2d96f8e0 100644 --- a/base-account/base-account-wagmi-template/src/app/globals.css +++ b/base-account/base-account-wagmi-template/src/app/globals.css @@ -1,6 +1,4 @@ :root { - background-color: #181818; - color: rgba(255, 255, 255, 0.87); color-scheme: light dark; font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; font-synthesis: none; @@ -13,9 +11,29 @@ -webkit-text-size-adjust: 100%; } -@media (prefers-color-scheme: light) { - :root { - background-color: #f8f8f8; - color: #181818; +body { + margin: 0; + padding: 0; +} + +/* Spinner animation */ +@keyframes spin { + 0% { + transform: rotate(0deg); } + 100% { + transform: rotate(360deg); + } +} + +/* Input focus styles */ +input:focus { + border-color: #667eea !important; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +/* Button hover effects */ +button:not(:disabled):hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } diff --git a/base-account/base-account-wagmi-template/src/app/page.tsx b/base-account/base-account-wagmi-template/src/app/page.tsx index 792f2149..26beaffe 100644 --- a/base-account/base-account-wagmi-template/src/app/page.tsx +++ b/base-account/base-account-wagmi-template/src/app/page.tsx @@ -10,51 +10,240 @@ function App() { const { disconnect } = useDisconnect(); return ( - <> -
-

Account

- -
- status: {account.status} -
- addresses: {JSON.stringify(account.addresses)} -
- chainId: {account.chainId} -
+
+
+ {/* Header */} +
+

🔷 Base Account Demo

+

+ Sign in to interact with batch transactions and mint NFTs +

+
+ {/* Account Info Card */} {account.status === "connected" && ( - +
+
+

Your Account

+ + Signed In + +
+
+
+ Address: + + {account.addresses?.[0] + ? `${account.addresses[0]}` + : "N/A"} + +
+
+ +
)} -
-
-

Connect

- {connectors.map((connector) => { - if (connector.name === "Base Account") { - return ( - - ); - } else { - return ( - - ); - } - })} -
{status}
-
{error?.message}
-
+ {/* Sign In Card */} + {account.status !== "connected" && ( +
+

Sign In

+

+ Choose your preferred sign-in method to get started +

+
+ {connectors.map((connector) => { + if (connector.name === "Base Account") { + return ( + + ); + } else { + return ( + + ); + } + })} +
+ {status && status !== "idle" && ( +
+ Status: {status} +
+ )} + {error && ( +
+ âš ī¸ {error.message} +
+ )} +
+ )} + + {/* Batch Transactions Card */} + {account.status === "connected" && ( +
+ +
+ )} - {account.status === "connected" && } - + {/* Footer */} + +
+
); } +const styles = { + container: { + minHeight: "100vh", + background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)", + padding: "2rem", + } as React.CSSProperties, + innerContainer: { + maxWidth: "800px", + margin: "0 auto", + } as React.CSSProperties, + header: { + textAlign: "center", + marginBottom: "3rem", + color: "white", + } as React.CSSProperties, + title: { + fontSize: "3rem", + fontWeight: "bold", + margin: "0 0 1rem 0", + textShadow: "0 2px 4px rgba(0,0,0,0.1)", + } as React.CSSProperties, + subtitle: { + fontSize: "1.125rem", + opacity: 0.9, + margin: 0, + } as React.CSSProperties, + card: { + backgroundColor: "white", + borderRadius: "16px", + padding: "2rem", + marginBottom: "2rem", + boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)", + } as React.CSSProperties, + cardHeader: { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + marginBottom: "1.5rem", + } as React.CSSProperties, + cardTitle: { + fontSize: "1.5rem", + fontWeight: "600", + color: "#1f2937", + margin: 0, + } as React.CSSProperties, + cardDescription: { + color: "#6b7280", + marginBottom: "1.5rem", + } as React.CSSProperties, + statusBadge: { + padding: "0.5rem 1rem", + borderRadius: "9999px", + color: "white", + fontSize: "0.875rem", + fontWeight: "500", + } as React.CSSProperties, + accountInfo: { + backgroundColor: "#f9fafb", + borderRadius: "8px", + padding: "1.5rem", + marginBottom: "1.5rem", + } as React.CSSProperties, + infoRow: { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + marginBottom: "0.75rem", + } as React.CSSProperties, + infoLabel: { + fontWeight: "500", + color: "#6b7280", + } as React.CSSProperties, + infoValue: { + color: "#1f2937", + fontWeight: "500", + } as React.CSSProperties, + connectButtons: { + display: "flex", + flexDirection: "column", + gap: "0.75rem", + alignItems: "center", + } as React.CSSProperties, + connectButton: { + width: "100%", + padding: "1rem", + backgroundColor: "#667eea", + color: "white", + border: "none", + borderRadius: "8px", + fontSize: "1rem", + fontWeight: "500", + cursor: "pointer", + transition: "all 0.2s", + } as React.CSSProperties, + disconnectButton: { + width: "100%", + padding: "0.75rem", + backgroundColor: "#ef4444", + color: "white", + border: "none", + borderRadius: "8px", + fontSize: "1rem", + fontWeight: "500", + cursor: "pointer", + transition: "all 0.2s", + } as React.CSSProperties, + statusMessage: { + marginTop: "1rem", + padding: "0.75rem", + backgroundColor: "#dbeafe", + borderRadius: "8px", + textAlign: "center", + } as React.CSSProperties, + statusText: { + color: "#1e40af", + fontWeight: "500", + } as React.CSSProperties, + errorMessage: { + marginTop: "1rem", + padding: "0.75rem", + backgroundColor: "#fee2e2", + borderRadius: "8px", + color: "#991b1b", + textAlign: "center", + } as React.CSSProperties, + footer: { + textAlign: "center", + marginTop: "3rem", + color: "white", + } as React.CSSProperties, + footerText: { + opacity: 0.8, + fontSize: "0.875rem", + } as React.CSSProperties, +}; + export default App; diff --git a/base-account/base-account-wagmi-template/src/components/BatchTransactions.tsx b/base-account/base-account-wagmi-template/src/components/BatchTransactions.tsx index 3e7e80bb..75390c3b 100644 --- a/base-account/base-account-wagmi-template/src/components/BatchTransactions.tsx +++ b/base-account/base-account-wagmi-template/src/components/BatchTransactions.tsx @@ -1,52 +1,77 @@ "use client"; import { useState } from "react"; -import { useSendCalls } from "wagmi"; +import { useSendCalls, useAccount } from "wagmi"; import { encodeFunctionData, parseUnits } from "viem"; import { baseSepolia } from "wagmi/chains"; // USDC contract address on Base Sepolia const USDC_ADDRESS = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"; -// ERC20 ABI for the transfer function +// NFT contract address on Base Sepolia +const NFT_CONTRACT_ADDRESS = "0x82039e7C37D7aAc98D0F4d0A762F4E0d8c8DC273"; + +// ERC20 ABI for the transfer, approve, and transferFrom functions const erc20Abi = [ { inputs: [ - { name: "to", type: "address" }, + { name: "spender", type: "address" }, { name: "amount", type: "uint256" }, ], - name: "transfer", + name: "approve", outputs: [{ name: "", type: "bool" }], stateMutability: "nonpayable", type: "function", + } +] as const; + +// ERC721 ABI for the mint function +const erc721Abi = [ + { + inputs: [ + { name: "to", type: "address" }, + { name: "tokenId", type: "uint256" }, + ], + name: "mint", + outputs: [], + stateMutability: "nonpayable", + type: "function", }, ] as const; export function BatchTransactions() { const { sendCalls, data, isPending, isSuccess, error } = useSendCalls(); - const [amount1, setAmount1] = useState("1"); - const [amount2, setAmount2] = useState("1"); + const account = useAccount(); + const [approvalAmount, setApprovalAmount] = useState("1"); + const [tokenId, setTokenId] = useState("1"); const [usePaymaster, setUsePaymaster] = useState(false); async function handleBatchTransfer() { try { - // Encode the first transfer call + // Get the user's address + const userAddress = account.addresses?.[0]; + if (!userAddress) { + console.error("No user address found"); + return; + } + + // Encode the first approve call - approve USDC to NFT contract const call1Data = encodeFunctionData({ abi: erc20Abi, - functionName: "transfer", + functionName: "approve", args: [ - "0x2211d1D0020DAEA8039E46Cf1367962070d77DA9", - parseUnits(amount1, 6), // USDC has 6 decimals + NFT_CONTRACT_ADDRESS, + parseUnits(approvalAmount, 6), // USDC has 6 decimals ], }); - // Encode the second transfer call + // Encode the second call - mint NFT to the user's address const call2Data = encodeFunctionData({ - abi: erc20Abi, - functionName: "transfer", + abi: erc721Abi, + functionName: "mint", args: [ - "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", - parseUnits(amount2, 6), // USDC has 6 decimals + userAddress as `0x${string}`, + BigInt(tokenId), ], }); @@ -67,7 +92,7 @@ export function BatchTransactions() { data: call1Data, }, { - to: USDC_ADDRESS, + to: NFT_CONTRACT_ADDRESS, data: call2Data, }, ], @@ -81,60 +106,247 @@ export function BatchTransactions() { return (
-

Batch USDC Transfers

+

🎨 Approve USDC & Mint NFT

+

+ Batch approve USDC and mint an NFT in a single transaction +

-
-
- +
+
+ setAmount1(e.target.value)} + value={approvalAmount} + onChange={(e) => setApprovalAmount(e.target.value)} placeholder="1" step="0.000001" min="0" + style={styles.input} />
-
- +
+ setAmount2(e.target.value)} + value={tokenId} + onChange={(e) => setTokenId(e.target.value)} placeholder="1" - step="0.000001" + step="1" min="0" + style={styles.input} />
-
-
- {isPending &&
Transaction pending...
} + {isPending && ( +
+
+

Transaction pending...

+
+ )} {isSuccess && data && ( -
-

Batch sent successfully!

-

Batch ID: {data.id}

+
+
✓
+

Batch sent successfully!

+

Batch ID: {data.id}

)} - {error &&
Error: {error.message}
} + {error && ( +
+ âš ī¸ + {error.message} +
+ )}
); } +const styles = { + title: { + fontSize: "1.5rem", + fontWeight: "600", + color: "#1f2937", + margin: "0 0 0.5rem 0", + } as React.CSSProperties, + description: { + color: "#6b7280", + marginBottom: "2rem", + fontSize: "0.95rem", + } as React.CSSProperties, + form: { + display: "flex", + flexDirection: "column", + gap: "1.5rem", + } as React.CSSProperties, + inputGroup: { + display: "flex", + flexDirection: "column", + gap: "0.5rem", + } as React.CSSProperties, + label: { + fontWeight: "500", + color: "#374151", + fontSize: "0.875rem", + } as React.CSSProperties, + input: { + padding: "0.75rem 1rem", + border: "2px solid #e5e7eb", + borderRadius: "8px", + fontSize: "1rem", + transition: "all 0.2s", + outline: "none", + width: "100%", + boxSizing: "border-box", + } as React.CSSProperties, + infoBox: { + display: "flex", + alignItems: "center", + gap: "0.75rem", + padding: "1rem", + backgroundColor: "#eff6ff", + borderRadius: "8px", + fontSize: "0.875rem", + color: "#1e40af", + } as React.CSSProperties, + infoIcon: { + fontSize: "1.25rem", + } as React.CSSProperties, + checkboxGroup: { + display: "flex", + alignItems: "center", + padding: "1rem", + backgroundColor: "#f9fafb", + borderRadius: "8px", + } as React.CSSProperties, + checkboxLabel: { + display: "flex", + alignItems: "center", + gap: "0.75rem", + cursor: "pointer", + fontSize: "0.95rem", + color: "#374151", + } as React.CSSProperties, + checkbox: { + width: "1.25rem", + height: "1.25rem", + cursor: "pointer", + } as React.CSSProperties, + button: { + width: "100%", + padding: "1rem", + backgroundColor: "#667eea", + color: "white", + border: "none", + borderRadius: "8px", + fontSize: "1rem", + fontWeight: "600", + cursor: "pointer", + transition: "all 0.2s", + marginTop: "0.5rem", + } as React.CSSProperties, + buttonDisabled: { + backgroundColor: "#9ca3af", + cursor: "not-allowed", + opacity: 0.6, + } as React.CSSProperties, + statusBox: { + marginTop: "1.5rem", + padding: "1.5rem", + backgroundColor: "#dbeafe", + borderRadius: "8px", + textAlign: "center", + display: "flex", + flexDirection: "column", + alignItems: "center", + gap: "1rem", + } as React.CSSProperties, + spinner: { + width: "24px", + height: "24px", + border: "3px solid #60a5fa", + borderTop: "3px solid transparent", + borderRadius: "50%", + animation: "spin 1s linear infinite", + } as React.CSSProperties, + statusText: { + color: "#1e40af", + fontWeight: "500", + margin: 0, + } as React.CSSProperties, + successBox: { + marginTop: "1.5rem", + padding: "1.5rem", + backgroundColor: "#d1fae5", + borderRadius: "8px", + textAlign: "center", + } as React.CSSProperties, + successIcon: { + display: "inline-block", + width: "48px", + height: "48px", + backgroundColor: "#10b981", + color: "white", + borderRadius: "50%", + lineHeight: "48px", + fontSize: "24px", + fontWeight: "bold", + marginBottom: "0.75rem", + } as React.CSSProperties, + successTitle: { + color: "#065f46", + fontWeight: "600", + margin: "0 0 0.5rem 0", + fontSize: "1.1rem", + } as React.CSSProperties, + successId: { + color: "#047857", + fontSize: "0.875rem", + margin: 0, + wordBreak: "break-all", + } as React.CSSProperties, + errorBox: { + marginTop: "1.5rem", + padding: "1rem", + backgroundColor: "#fee2e2", + borderRadius: "8px", + color: "#991b1b", + display: "flex", + alignItems: "center", + gap: "0.75rem", + } as React.CSSProperties, + errorIcon: { + fontSize: "1.25rem", + } as React.CSSProperties, +}; + diff --git a/base-account/base-account-wagmi-template/src/components/SignInWithBase.tsx b/base-account/base-account-wagmi-template/src/components/SignInWithBase.tsx index bd423b74..9e19632a 100644 --- a/base-account/base-account-wagmi-template/src/components/SignInWithBase.tsx +++ b/base-account/base-account-wagmi-template/src/components/SignInWithBase.tsx @@ -1,6 +1,6 @@ "use client"; -import { Connector } from "wagmi"; +import { Connector, useConnect } from "wagmi"; import { SignInWithBaseButton } from "@base-org/account-ui/react"; import { useState } from "react"; @@ -10,63 +10,71 @@ interface SignInWithBaseProps { export function SignInWithBase({ connector }: SignInWithBaseProps) { const [verificationResult, setVerificationResult] = useState(""); + const { connect } = useConnect(); async function handleBaseAccountConnect() { - const provider = await connector.getProvider(); - if (provider) { - try { - // Generate a fresh nonce (this will be overwritten with the backend nonce) - const clientNonce = - Math.random().toString(36).substring(2, 15) + - Math.random().toString(36).substring(2, 15); - console.log("clientNonce", clientNonce); - // Connect with SIWE to get signature, message, and address - const accounts = await (provider as any).request({ - method: "wallet_connect", - params: [ - { - version: "1", - capabilities: { - signInWithEthereum: { - nonce: clientNonce, - chainId: "0x2105", // Base Mainnet - 8453 - }, + try { + const provider = await connector.getProvider(); + if (!provider) { + console.error("No provider"); + return; + } + + // Generate a fresh nonce (this will be overwritten with the backend nonce) + const clientNonce = + Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15); + console.log("clientNonce", clientNonce); + + // Connect with SIWE to get signature, message, and address + // This wallet_connect request will trigger the connection AND update wagmi's state + const accounts = await (provider as any).request({ + method: "wallet_connect", + params: [ + { + version: "1", + capabilities: { + signInWithEthereum: { + nonce: clientNonce, + chainId: "0x2105", // Base Mainnet - 8453 }, }, - ], - }); + }, + ], + }); + + // After successful wallet_connect, explicitly connect through wagmi to update state + connect({ connector }); - const walletAddress = accounts.accounts[0].address; - const signature = - accounts.accounts[0].capabilities.signInWithEthereum.signature; - const message = - accounts.accounts[0].capabilities.signInWithEthereum.message; - // Verify the signature on the backend - const verifyResponse = await fetch("/api/auth/verify", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - address: walletAddress, - message, - signature, - }), - }); + const walletAddress = accounts.accounts[0].address; + const signature = + accounts.accounts[0].capabilities.signInWithEthereum.signature; + const message = + accounts.accounts[0].capabilities.signInWithEthereum.message; + + // Verify the signature on the backend + const verifyResponse = await fetch("/api/auth/verify", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + address: walletAddress, + message, + signature, + }), + }); - const result = await verifyResponse.json(); + const result = await verifyResponse.json(); - if (result.success) { - setVerificationResult(`Verified! Address: ${result.address}`); - } else { - setVerificationResult(`Verification failed: ${result.error}`); - } - } catch (err) { - console.error("Error:", err); - setVerificationResult( - `Error: ${err instanceof Error ? err.message : "Unknown error"}` - ); + if (result.success) { + setVerificationResult(`Verified! Address: ${result.address}`); + } else { + setVerificationResult(`Verification failed: ${result.error}`); } - } else { - console.error("No provider"); + } catch (err) { + console.error("Error:", err); + setVerificationResult( + `Error: ${err instanceof Error ? err.message : "Unknown error"}` + ); } } @@ -76,7 +84,7 @@ export function SignInWithBase({ connector }: SignInWithBaseProps) {
diff --git a/base-account/base-account-wagmi-template/src/wagmi.ts b/base-account/base-account-wagmi-template/src/wagmi.ts index 5eda7cfb..5f129f5d 100644 --- a/base-account/base-account-wagmi-template/src/wagmi.ts +++ b/base-account/base-account-wagmi-template/src/wagmi.ts @@ -17,8 +17,8 @@ export function getConfig() { }), ssr: true, transports: { - [base.id]: http(), [baseSepolia.id]: http(), + [base.id]: http(), }, }); }