Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
55da917
Merge pull request #20 from alchemyplatform/blake/update-quickstart
avarobinson May 29, 2025
ac1a5e3
feat: fix readme and clean up ui
blakecduncan May 30, 2025
373ae76
Update README.md with the app image
blakecduncan May 30, 2025
4d4b4fa
feat: add tooltip on copying of the address
blakecduncan May 30, 2025
2f8b3e0
fix: use latest
avarobinson Aug 20, 2025
5820afe
feat: allow the chain to be configured through an env variable
androbwebb Sep 11, 2025
0cfeedb
chore: fix example
androbwebb Sep 11, 2025
775e3d6
Add chainNFTMintContract across a bunch of other chains
androbwebb Oct 28, 2025
56f6769
wip
androbwebb Oct 28, 2025
b1eef19
List the chain name too
androbwebb Oct 28, 2025
d28f147
Clean up, switch to numbers
androbwebb Oct 28, 2025
4efb140
lint
androbwebb Oct 28, 2025
8cd6216
lint
androbwebb Oct 28, 2025
9051ec7
lint
androbwebb Oct 28, 2025
0c56702
lint
androbwebb Oct 28, 2025
68364b3
Fix nftContractAddress
androbwebb Oct 28, 2025
673acc6
refactors a little. Allow undefined
androbwebb Oct 28, 2025
6a5b71a
fix hook name
androbwebb Oct 29, 2025
0dcac1b
use chainNFTMintContractData in config
androbwebb Oct 29, 2025
33bc3fc
Change order of imports
androbwebb Oct 29, 2025
c04779a
Change order of imports
androbwebb Oct 29, 2025
7e54ade
change hook return
androbwebb Oct 29, 2025
b7b3b29
Update app/components/nft-mint-card.tsx
androbwebb Oct 29, 2025
91a7cc0
Merge pull request #36 from androbwebb/webb/configurable-chain-with-e…
androbwebb Oct 29, 2025
1c4e0cc
feat: update readme (#175) (#183)
Dargon789 Apr 6, 2026
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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Retrieve your API Key and Policy ID from the smart wallets dashboard default configuration or create new keys: https://dashboard.alchemy.com/services/smart-wallets/configuration
NEXT_PUBLIC_ALCHEMY_API_KEY=YOUR_APP_API_KEY # Get your app API key: https://dashboard.alchemy.com/apps
NEXT_PUBLIC_ALCHEMY_POLICY_ID=YOUR_SPONSORSHIP_POLICY_ID # Get your gas sponsorship policy ID: https://dashboard.alchemy.com/services/gas-manager/configuration
NEXT_PUBLIC_CHAIN_ID=421614 # Arbitrum Sepolia as default
# NOTE: make sure to set up a smart wallet configuration for your app to enable login
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ Use this template to get started with **embedded smart wallets** using [Alchemy
- Email, passkey & social login using pre‑built UI components
- Flexible, secure, and cheap smart accounts
- Gasless transactions powered by ERC-4337 Account Abstraction
- One‑click NFT mint on Arbitrum Sepolia (no ETH required)
- One‑click NFT mint (no ETH required)
- Server‑side rendering ready – session persisted with cookies
- TailwindCSS + shadcn/ui components, React Query, TypeScript

![Smart Wallet Quickstart](https://github.com/user-attachments/assets/2903fb78-e632-4aaa-befd-5775c60e1ca2)

## 📍 Network & Demo Contract

This quickstart is configured to run on **Arbitrum Sepolia** testnet. A free demo NFT contract has been deployed specifically for this quickstart, allowing you to mint NFTs without any setup or deployment steps. The contract is pre-configured and ready to use out of the box.
This quickstart is configured to run on **Arbitrum Sepolia** testnet, by default. A free demo NFT contract has been deployed specifically for this quickstart, allowing you to mint NFTs without any setup or deployment steps. The contract is pre-configured and ready to use out of the box.

## 🚀 Quick start

Expand Down Expand Up @@ -73,7 +75,7 @@ tailwind.config.ts

## 🏗️ How it works

1. `config.ts` initializes Account Kit with your API key, Base Sepolia chain, and Gas Sponsorship policy.
1. `config.ts` initializes Account Kit with your API key, chain, and Gas Sponsorship policy.
2. `Providers` wraps the app with `AlchemyAccountProvider` & React Query.
3. `LoginCard` opens the authentication modal (`useAuthModal`).
4. After login, `useSmartAccountClient` exposes the smart wallet.
Expand Down
71 changes: 62 additions & 9 deletions app/components/nft-mint-card.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { useState } from "react";
import { ExternalLink, Loader2, PlusCircle } from "lucide-react";
import { useState, useEffect } from "react";
import {
ExternalLink,
Loader2,
PlusCircle,
ImageIcon,
CheckCircle,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Card,
Expand All @@ -15,15 +21,17 @@ import Link from "next/link";
import { useReadNFTData } from "@/app/hooks/useReadNFTData";
import { useMint } from "@/app/hooks/useMintNFT";
import { useSmartAccountClient } from "@account-kit/react";
import { NFT_CONTRACT_ADDRESS } from "@/lib/constants";
import { useNftContractAddress } from "@/app/hooks/useNftContractAddress";

export default function NftMintCard() {
const [isImageLoading, setIsImageLoading] = useState(true);
const [showSuccess, setShowSuccess] = useState(true);
const nftContractAddress = useNftContractAddress();

const { client } = useSmartAccountClient({});

const { uri, count, isLoadingCount, refetchCount } = useReadNFTData({
contractAddress: NFT_CONTRACT_ADDRESS,
contractAddress: nftContractAddress,
ownerAddress: client?.account?.address,
});

Expand All @@ -33,6 +41,17 @@ export default function NftMintCard() {
},
});

// Reset success animation when new transaction appears
useEffect(() => {
if (transactionUrl) {
setShowSuccess(true);
const timer = setTimeout(() => {
setShowSuccess(false);
}, 4000);
return () => clearTimeout(timer);
}
}, [transactionUrl]);

return (
<Card className="overflow-hidden">
<CardHeader className="pb-0">
Expand Down Expand Up @@ -105,7 +124,7 @@ export default function NftMintCard() {
)}
>
<PlusCircle className="h-[18px] w-[18px]" />
Mint New NFT
Mint New NFT {!!client?.chain?.name && `on ${client.chain.name}`}
</span>
<span
className={cn(
Expand All @@ -124,16 +143,50 @@ export default function NftMintCard() {
<Button
variant="outline"
size="lg"
className="gap-2 w-full sm:w-auto"
asChild
className={cn(
"gap-2 w-full sm:w-auto relative overflow-hidden transition-all duration-500",
"border-green-400 text-green-700 hover:bg-green-50",
"animate-in fade-in duration-700"
)}
>
<Link
href={transactionUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 w-full sm:w-auto"
>
<span>View Transaction</span>
<ExternalLink className="h-4 w-4" />
{showSuccess ? (
<>
<div
className="absolute inset-0 bg-gradient-to-r from-green-400 to-green-600 opacity-10"
style={{
animation: "sweep 1.5s ease-out",
}}
/>
<span className="relative z-10">Successful mint!</span>
<CheckCircle className="h-4 w-4 relative z-10" />
<style jsx>{`
@keyframes sweep {
0% {
transform: translateX(-100%);
opacity: 0;
}
50% {
opacity: 0.2;
}
100% {
transform: translateX(100%);
opacity: 0;
}
}
`}</style>
</>
) : (
<>
<span>View Transaction</span>
<ExternalLink className="h-4 w-4" />
</>
)}
</Link>
</Button>
)}
Expand Down
47 changes: 34 additions & 13 deletions app/components/user-info-card.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Info, ExternalLink, Copy } from "lucide-react";
import { useState } from "react";
import { ExternalLink, Copy } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Card,
Expand All @@ -8,14 +9,27 @@ import {
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { formatAddress } from "@/lib/utils";
import { useUser, useSmartAccountClient } from "@account-kit/react";

export default function UserInfo() {
const [isCopied, setIsCopied] = useState(false);
const user = useUser();
const userEmail = user?.email ?? "anon";
const { client } = useSmartAccountClient({});

const handleCopy = () => {
navigator.clipboard.writeText(client?.account?.address ?? "");
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
};

return (
<Card>
<CardHeader>
Expand All @@ -41,25 +55,32 @@ export default function UserInfo() {
<Badge variant="outline" className="font-mono text-xs py-1 px-2">
{formatAddress(client?.account?.address ?? "")}
</Badge>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => {
navigator.clipboard.writeText(client?.account?.address ?? "");
}}
>
<Copy className="h-4 w-4" />
</Button>
<TooltipProvider>
<Tooltip open={isCopied}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={handleCopy}
>
<Copy className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Copied!</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => {
const address = client?.account?.address;
if (address) {
if (address && client?.chain?.blockExplorers?.default?.url) {
window.open(
`https://sepolia.basescan.org/address/${address}`,
`${client.chain.blockExplorers.default.url}/address/${address}`,
"_blank"
);
}
Expand Down
13 changes: 10 additions & 3 deletions app/hooks/useMintNFT.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
useSendUserOperation,
} from "@account-kit/react";
import { encodeFunctionData } from "viem";
import { NFT_MINTABLE_ABI_PARSED, NFT_CONTRACT_ADDRESS } from "@/lib/constants";
import { NFT_MINTABLE_ABI_PARSED } from "@/lib/constants";
import { useNftContractAddress } from "@/app/hooks/useNftContractAddress";

export interface UseMintNFTParams {
onSuccess?: () => void;
Expand All @@ -19,6 +20,7 @@ export interface UseMintReturn {
export const useMint = ({ onSuccess }: UseMintNFTParams): UseMintReturn => {
const [isMinting, setIsMinting] = useState(false);
const [error, setError] = useState<string>();
const nftContractAddress = useNftContractAddress();

const { client } = useSmartAccountClient({});

Expand Down Expand Up @@ -51,17 +53,22 @@ export const useMint = ({ onSuccess }: UseMintNFTParams): UseMintReturn => {
return;
}

if (!nftContractAddress) {
setError("Contract address is not defined.");
return;
}

sendUserOperation({
uo: {
target: NFT_CONTRACT_ADDRESS,
target: nftContractAddress,
data: encodeFunctionData({
abi: NFT_MINTABLE_ABI_PARSED,
functionName: "mintTo",
args: [client.getAddress()],
}),
},
});
}, [client, sendUserOperation]);
}, [client, sendUserOperation, nftContractAddress]);

const transactionUrl = useMemo(() => {
if (!client?.chain?.blockExplorers || !sendUserOperationResult?.hash) {
Expand Down
10 changes: 10 additions & 0 deletions app/hooks/useNftContractAddress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useSmartAccountClient } from "@account-kit/react";
import { arbitrumSepolia } from "@account-kit/infra";
import { ChainData, chainNFTMintContractData } from "@/lib/chains";

export const useNftContractAddress = (): ChainData['nftContractAddress'] | undefined => {
const { client } = useSmartAccountClient({});
const chain = client?.chain || arbitrumSepolia;

return chainNFTMintContractData[chain.id]?.nftContractAddress;
}
12 changes: 9 additions & 3 deletions config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import {
cookieStorage,
createConfig,
} from "@account-kit/react";
import { alchemy, arbitrumSepolia } from "@account-kit/infra";
import { QueryClient } from "@tanstack/react-query";
import { chainNFTMintContractData } from "@/lib/chains";
import { alchemy } from "@account-kit/infra";

const API_KEY = process.env.NEXT_PUBLIC_ALCHEMY_API_KEY;
if (!API_KEY) {
Expand All @@ -16,6 +17,12 @@ if (!SPONSORSHIP_POLICY_ID) {
throw new Error("NEXT_PUBLIC_ALCHEMY_POLICY_ID is not set");
}

const CHAIN_ID = parseInt(process.env.NEXT_PUBLIC_CHAIN_ID ?? '') || 421614;
const chain = chainNFTMintContractData[CHAIN_ID]?.chain;
if (!chain) {
throw new Error("Invalid chain ID")
}

const uiConfig: AlchemyAccountsUIConfig = {
illustrationStyle: "outline",
auth: {
Expand All @@ -34,8 +41,7 @@ const uiConfig: AlchemyAccountsUIConfig = {
export const config = createConfig(
{
transport: alchemy({ apiKey: API_KEY }),
// Note: This quickstart is configured for Arbitrum Sepolia.
chain: arbitrumSepolia,
chain,
ssr: true, // more about ssr: https://www.alchemy.com/docs/wallets/react/ssr
storage: cookieStorage, // more about persisting state with cookies: https://www.alchemy.com/docs/wallets/react/ssr#persisting-the-account-state
enablePopupOauth: true, // must be set to "true" if you plan on using popup rather than redirect in the social login flow
Expand Down
71 changes: 71 additions & 0 deletions lib/chains.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Address, Chain } from "viem";
import {
arbitrumSepolia,
baseSepolia,
sepolia,
polygonAmoy,
shapeSepolia,
soneiumMinato,
unichainSepolia,
inkSepolia,
monadTestnet,
riseTestnet,
storyAeneid,
teaSepolia
} from "@account-kit/infra";

export interface ChainData {
chain: Chain;
nftContractAddress: Address;
}

export const chainNFTMintContractData: Record<Chain['id'], ChainData> = {
[arbitrumSepolia.id]: {
chain: arbitrumSepolia,
nftContractAddress: "0x6D1BaA7951f26f600b4ABc3a9CF8F18aBf36fac1",
},
[baseSepolia.id]: {
chain: baseSepolia,
nftContractAddress: "0x6d15c130d9B2548597C1d2D0c8CB2067Ce9C4525",
},
[polygonAmoy.id]: {
chain: polygonAmoy,
nftContractAddress: "0x6d15c130d9B2548597C1d2D0c8CB2067Ce9C4525",
},
[sepolia.id]: {
chain: sepolia,
nftContractAddress: "0xc59b508C90425C8e25e3F9dA30e52057908E2838",
},
[shapeSepolia.id]: {
chain: shapeSepolia,
nftContractAddress: "0x6d15c130d9B2548597C1d2D0c8CB2067Ce9C4525",
},
[soneiumMinato.id]: {
chain: soneiumMinato,
nftContractAddress: "0x6d15c130d9B2548597C1d2D0c8CB2067Ce9C4525",
},
[unichainSepolia.id]: {
chain: unichainSepolia,
nftContractAddress: "0x6d15c130d9B2548597C1d2D0c8CB2067Ce9C4525",
},
[inkSepolia.id]: {
chain: inkSepolia,
nftContractAddress: "0x6d15c130d9B2548597C1d2D0c8CB2067Ce9C4525",
},
[monadTestnet.id]: {
chain: monadTestnet,
nftContractAddress: "0x6d15c130d9B2548597C1d2D0c8CB2067Ce9C4525",
},
[riseTestnet.id]: {
chain: riseTestnet,
nftContractAddress: "0x6d15c130d9B2548597C1d2D0c8CB2067Ce9C4525",
},
[storyAeneid.id]: {
chain: storyAeneid,
nftContractAddress: "0x6d15c130d9B2548597C1d2D0c8CB2067Ce9C4525",
},
[teaSepolia.id]: {
chain: teaSepolia,
nftContractAddress: "0x6d15c130d9B2548597C1d2D0c8CB2067Ce9C4525",
},
}
Loading