- {timeLeft.days}
+
+ {(!hideZeroUnits || timeLeft.days > 0) && (
+
+
{timeLeft.days}
+
+ {timeLeft.days === 1
+ ? timeLeftLabels.days.singular
+ : timeLeftLabels.days.plural}
+
-
- {timeLeft.days === 1
- ? timeLeftLabels.days.singular
- : timeLeftLabels.days.plural}
+ )}
+ {(!hideZeroUnits || timeLeft.days > 0 || timeLeft.hours > 0) && (
+
+
{timeLeft.hours}
+
+ {timeLeft.hours === 1
+ ? timeLeftLabels.hours.singular
+ : timeLeftLabels.hours.plural}
+
-
-
-
- {timeLeft.hours}
+ )}
+ {(!hideZeroUnits ||
+ timeLeft.days > 0 ||
+ timeLeft.hours > 0 ||
+ timeLeft.minutes > 0) && (
+
+
{timeLeft.minutes}
+
+ {timeLeft.minutes === 1
+ ? timeLeftLabels.minutes.singular
+ : timeLeftLabels.minutes.plural}
+
-
- {timeLeft.hours === 1
- ? timeLeftLabels.hours.singular
- : timeLeftLabels.hours.plural}
+ )}
+ {(!hideZeroUnits ||
+ timeLeft.days > 0 ||
+ timeLeft.hours > 0 ||
+ timeLeft.minutes > 0 ||
+ timeLeft.seconds > 0) && (
+
+
{timeLeft.seconds}
+
+ {timeLeft.seconds === 1
+ ? timeLeftLabels.seconds.singular
+ : timeLeftLabels.seconds.plural}
+
-
-
-
- {timeLeft.minutes}
-
-
- {timeLeft.minutes === 1
- ? timeLeftLabels.minutes.singular
- : timeLeftLabels.minutes.plural}
-
-
-
-
- {timeLeft.seconds}
-
-
- {timeLeft.seconds === 1
- ? timeLeftLabels.seconds.singular
- : timeLeftLabels.seconds.plural}
-
-
+ )}
)
}
diff --git a/app/[locale]/10years/_components/NFTMintCard/Connected.tsx b/app/[locale]/10years/_components/NFTMintCard/Connected.tsx
new file mode 100644
index 00000000000..d910e58af21
--- /dev/null
+++ b/app/[locale]/10years/_components/NFTMintCard/Connected.tsx
@@ -0,0 +1,77 @@
+import { Address } from "viem"
+import { useDisconnect, useSwitchChain } from "wagmi"
+
+import { Avatar } from "@/components/ui/avatar"
+import { Button } from "@/components/ui/buttons/Button"
+
+import { useNetworkContract } from "@/hooks/useNetworkContract"
+import {
+ formatAddress,
+ getAddressEtherscanUrl,
+ getBlockieImage,
+} from "@/lib/torch"
+
+export default function Connected({
+ address,
+ ensName,
+}: {
+ address: Address
+ ensName?: string | null
+}) {
+ const { disconnect } = useDisconnect()
+ const { isSupportedNetwork, networkName, chainId } = useNetworkContract()
+ const { switchChain, isPending } = useSwitchChain()
+
+ const handleSwitchNetwork = () => {
+ switchChain({ chainId })
+ }
+
+ return (
+
+ {/* Wallet Info */}
+
+
+
{ensName || formatAddress(address)}
+
+
+ {/* Network Status */}
+
+ {isSupportedNetwork ? (
+
+
+
Connected to {networkName}
+
+ ) : (
+
+
+
+
Unsupported Network
+
+
+
+ )}
+
+
+ {/* Disconnect Button */}
+
+
+ )
+}
diff --git a/app/[locale]/10years/_components/NFTMintCard/Connection.tsx b/app/[locale]/10years/_components/NFTMintCard/Connection.tsx
new file mode 100644
index 00000000000..2229608438d
--- /dev/null
+++ b/app/[locale]/10years/_components/NFTMintCard/Connection.tsx
@@ -0,0 +1,14 @@
+import { useAccount } from "wagmi"
+
+import MintConnect from "./views/MintConnect"
+import Prechecks from "./Prechecks"
+
+export default function Connection() {
+ const { address, isConnected } = useAccount()
+
+ if (!isConnected || !address) {
+ return
+ }
+
+ return
+}
diff --git a/app/[locale]/10years/_components/NFTMintCard/GasFeeInformation.tsx b/app/[locale]/10years/_components/NFTMintCard/GasFeeInformation.tsx
new file mode 100644
index 00000000000..76499cd04da
--- /dev/null
+++ b/app/[locale]/10years/_components/NFTMintCard/GasFeeInformation.tsx
@@ -0,0 +1,26 @@
+import { Info } from "lucide-react"
+
+import {
+ Alert,
+ AlertContent,
+ AlertDescription,
+ AlertTitle,
+} from "@/components/ui/alert"
+
+const GasFeeInformation = () => {
+ return (
+
+
+
+ About Network Fees
+
+ While the NFT is free, you'll need to pay Ethereum network fees
+ to complete the transaction. Network fees vary throughout the day -
+ consider waiting for lower network fees periods to save on costs.
+
+
+
+ )
+}
+
+export default GasFeeInformation
diff --git a/app/[locale]/10years/_components/NFTMintCard/GasPriceDisplay.tsx b/app/[locale]/10years/_components/NFTMintCard/GasPriceDisplay.tsx
new file mode 100644
index 00000000000..1622d8ae59a
--- /dev/null
+++ b/app/[locale]/10years/_components/NFTMintCard/GasPriceDisplay.tsx
@@ -0,0 +1,95 @@
+"use client"
+
+import { cn } from "@/lib/utils/cn"
+
+import { type GasPriceLevel, useGasPrice } from "@/hooks/useGasPrice"
+
+const getGasLevelConfig = (level: GasPriceLevel | null) => {
+ switch (level) {
+ case "low":
+ return {
+ color: "text-green-600",
+ bgColor: "bg-green-100",
+ indicator: "bg-green-500",
+ label: "Low",
+ description: "Good time to mint!",
+ }
+ case "moderate":
+ return {
+ color: "text-yellow-600",
+ bgColor: "bg-yellow-100",
+ indicator: "bg-yellow-500",
+ label: "Moderate",
+ description: "Normal gas prices",
+ }
+ case "high":
+ return {
+ color: "text-orange-600",
+ bgColor: "bg-orange-100",
+ indicator: "bg-orange-500",
+ label: "High",
+ description: "Consider waiting for lower gas",
+ }
+ case "very_high":
+ return {
+ color: "text-red-600",
+ bgColor: "bg-red-100",
+ indicator: "bg-red-500",
+ label: "Very High",
+ description: "Wait for lower gas prices",
+ }
+ default:
+ return {
+ color: "text-gray-600",
+ bgColor: "bg-gray-100",
+ indicator: "bg-gray-400",
+ label: "Loading",
+ description: "Fetching gas prices...",
+ }
+ }
+}
+
+interface GasPriceDisplayProps {
+ className?: string
+}
+
+const GasPriceDisplay = ({ className }: GasPriceDisplayProps) => {
+ const { error, gasLevel } = useGasPrice()
+
+ const loading = false
+
+ const config = getGasLevelConfig(gasLevel)
+
+ if (error) {
+ return (
+
+
Unable to fetch network fees
+
+ )
+ }
+
+ return (
+
+ {/* Gas Price Display */}
+
+
+
+
Current Network Fee:
+
+
+ {loading ? (
+
+ ) : (
+
+
+ {config.label}
+
+
+ )}
+
+
+
+ )
+}
+
+export default GasPriceDisplay
diff --git a/app/[locale]/10years/_components/NFTMintCard/Mint.tsx b/app/[locale]/10years/_components/NFTMintCard/Mint.tsx
new file mode 100644
index 00000000000..ead3f20508b
--- /dev/null
+++ b/app/[locale]/10years/_components/NFTMintCard/Mint.tsx
@@ -0,0 +1,97 @@
+import { Address } from "viem"
+import {
+ useEnsName,
+ useWaitForTransactionReceipt,
+ useWriteContract,
+} from "wagmi"
+
+import { Button } from "@/components/ui/buttons/Button"
+
+import MintError from "./views/MintError"
+import MintSuccess from "./views/MintSuccess"
+import Connected from "./Connected"
+
+import { useNetworkContract } from "@/hooks/useNetworkContract"
+import { getErrorMessage } from "@/lib/torch"
+
+export default function Mint({ address }: { address: Address }) {
+ const { data: ensName } = useEnsName({ address })
+ const { contractData, isSupportedNetwork } = useNetworkContract()
+
+ const {
+ writeContract: mint,
+ isPending: isMinting,
+ data: hash,
+ error: writeError,
+ reset: resetWriteContract,
+ } = useWriteContract()
+
+ const {
+ isSuccess: isConfirmed,
+ isLoading: isReceiptLoading,
+ error: receiptError,
+ } = useWaitForTransactionReceipt({
+ hash,
+ query: {
+ refetchOnWindowFocus: false,
+ staleTime: 60 * 60 * 1000,
+ enabled: !!hash,
+ },
+ })
+
+ const handleMintClick = async () => {
+ mint({
+ address: contractData.address as `0x${string}`,
+ abi: contractData.abi,
+ functionName: "mint",
+ })
+ }
+
+ const resetMintState = () => {
+ resetWriteContract()
+ }
+
+ if (isConfirmed && hash) {
+ return
+ }
+
+ if (writeError) {
+ const errorMessage = getErrorMessage(writeError)
+ return (
+
+ )
+ }
+
+ if (receiptError) {
+ const errorMessage = getErrorMessage(receiptError)
+ return (
+
+ )
+ }
+
+ return (
+
+ {isSupportedNetwork && (
+ <>
+
+ >
+ )}
+
+
+
+ )
+}
diff --git a/app/[locale]/10years/_components/NFTMintCard/Prechecks.tsx b/app/[locale]/10years/_components/NFTMintCard/Prechecks.tsx
new file mode 100644
index 00000000000..856f0f2caeb
--- /dev/null
+++ b/app/[locale]/10years/_components/NFTMintCard/Prechecks.tsx
@@ -0,0 +1,31 @@
+import { Address } from "viem"
+import { useReadContract } from "wagmi"
+
+import MintAlreadyMinted from "./views/MintAlreadyMinted"
+import Mint from "./Mint"
+
+import { useNetworkContract } from "@/hooks/useNetworkContract"
+
+export default function Prechecks({ address }: { address: Address }) {
+ const { contractData, isSupportedNetwork } = useNetworkContract()
+
+ const { data: hasMinted, error: hasMintedError } = useReadContract({
+ address: contractData.address,
+ abi: contractData.abi,
+ functionName: "hasMinted",
+ args: [address],
+ query: {
+ enabled: isSupportedNetwork,
+ },
+ })
+
+ if (hasMintedError) {
+ return
Error checking minted
+ }
+
+ if (hasMinted) {
+ return
+ }
+
+ return
+}
diff --git a/app/[locale]/10years/_components/NFTMintCard/index.tsx b/app/[locale]/10years/_components/NFTMintCard/index.tsx
new file mode 100644
index 00000000000..8995aa40e54
--- /dev/null
+++ b/app/[locale]/10years/_components/NFTMintCard/index.tsx
@@ -0,0 +1,133 @@
+"use client"
+
+import { useMemo, useState } from "react"
+
+import { Alert, AlertContent, AlertTitle } from "@/components/ui/alert"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+
+import { cn } from "@/lib/utils/cn"
+
+import CountDown from "../CountDown"
+
+import Connection from "./Connection"
+import GasFeeInformation from "./GasFeeInformation"
+import GasPriceDisplay from "./GasPriceDisplay"
+
+import Curved10YearsText from "@/public/images/10-year-anniversary/10y-curved-heading.svg"
+
+interface NFTMintCardProps {
+ className?: string
+}
+
+const endTimestamp = process.env.NEXT_PUBLIC_MINT_TIMESTAMP_END
+
+if (!endTimestamp) {
+ throw new Error("NEXT_PUBLIC_MINT_TIMESTAMP_END is not set")
+}
+
+const NFTMintCard = ({ className }: NFTMintCardProps) => {
+ const endDateTime = useMemo(() => {
+ return new Date(Number(endTimestamp) * 1000).toISOString()
+ }, [])
+
+ const [isExpired, setIsExpired] = useState(false)
+
+ const handleExpired = () => {
+ setIsExpired(true)
+ }
+
+ return (
+ <>
+
+
+
+ {/* Torch/flame video */}
+
+
+ {/* Curved text */}
+
+
+
+ Mint the moment
+
+
+
+
+ Celebrate a decade of decentralization with a free, limited-time
+ 10th anniversary NFT. Mint yours before time runs out.
+
+
+ {isExpired ? (
+
+
+
+ The claim period has ended
+
+
+ Thank you all for joining the celebration
+
+
+
+ ) : (
+ <>
+
+
+
+ Time remaining to mint
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+ >
+ )
+}
+
+export default NFTMintCard
diff --git a/app/[locale]/10years/_components/NFTMintCard/views/MintAlreadyMinted.tsx b/app/[locale]/10years/_components/NFTMintCard/views/MintAlreadyMinted.tsx
new file mode 100644
index 00000000000..8de1889d247
--- /dev/null
+++ b/app/[locale]/10years/_components/NFTMintCard/views/MintAlreadyMinted.tsx
@@ -0,0 +1,26 @@
+import { Alert, AlertContent, AlertDescription } from "@/components/ui/alert"
+import { BaseLink } from "@/components/ui/Link"
+
+export default function MintAlreadyMinted() {
+ const tweetText = encodeURIComponent(
+ "🎉 I have my free 10th-Anniversary collectible NFT from ethereum.org 🔷 Celebrating a decade of open, decentralized innovation. Join me 👉 https://ethereum.org/en/10years/ #Ethereum10"
+ )
+ const tweetUrl = `https://twitter.com/intent/tweet?text=${tweetText}`
+
+ return (
+
+
+
+
+ Already minted
+
+
+
+
+
Share the celebration
+
+ )
+}
diff --git a/app/[locale]/10years/_components/NFTMintCard/views/MintConnect.tsx b/app/[locale]/10years/_components/NFTMintCard/views/MintConnect.tsx
new file mode 100644
index 00000000000..f73065edbd4
--- /dev/null
+++ b/app/[locale]/10years/_components/NFTMintCard/views/MintConnect.tsx
@@ -0,0 +1,50 @@
+import { useState } from "react"
+import { ConnectButton } from "@rainbow-me/rainbowkit"
+
+import { Button } from "@/components/ui/buttons/Button"
+import Checkbox from "@/components/ui/checkbox"
+import InlineLink from "@/components/ui/Link"
+
+export default function MintConnect() {
+ const [acceptedTerms, setAcceptedTerms] = useState(false)
+
+ return (
+
+
+ {({ account, chain, openConnectModal, mounted }) => {
+ const ready = mounted
+ if (!ready) return null
+
+ if (account && chain) {
+ return
+ }
+
+ return (
+
+ )
+ }}
+
+
+
+
+
+
+ )
+}
diff --git a/app/[locale]/10years/_components/NFTMintCard/views/MintError.tsx b/app/[locale]/10years/_components/NFTMintCard/views/MintError.tsx
new file mode 100644
index 00000000000..b2991528184
--- /dev/null
+++ b/app/[locale]/10years/_components/NFTMintCard/views/MintError.tsx
@@ -0,0 +1,42 @@
+import { Alert, AlertContent, AlertDescription } from "@/components/ui/alert"
+import { Button } from "@/components/ui/buttons/Button"
+import { BaseLink } from "@/components/ui/Link"
+
+import { getTxEtherscanUrl } from "@/lib/torch"
+
+export default function MintError({
+ errorMessage,
+ onTryAgain,
+ hash,
+}: {
+ errorMessage: string
+ onTryAgain: () => void
+ hash?: string
+}) {
+ return (
+
+
+
+
+ {errorMessage}
+
+ {hash && (
+
+
+ View transaction on Etherscan
+
+
+ )}
+
+
+
+
+
+
+ )
+}
diff --git a/app/[locale]/10years/_components/NFTMintCard/views/MintSuccess.tsx b/app/[locale]/10years/_components/NFTMintCard/views/MintSuccess.tsx
new file mode 100644
index 00000000000..dde859367f0
--- /dev/null
+++ b/app/[locale]/10years/_components/NFTMintCard/views/MintSuccess.tsx
@@ -0,0 +1,78 @@
+import { useEffect } from "react"
+import confetti from "canvas-confetti"
+
+import {
+ Alert,
+ AlertContent,
+ AlertDescription,
+ AlertTitle,
+} from "@/components/ui/alert"
+import { BaseLink } from "@/components/ui/Link"
+
+import { getTxEtherscanUrl } from "@/lib/torch"
+
+export default function MintSuccess({ txHash }: { txHash?: string }) {
+ const tweetText = encodeURIComponent(
+ "🎉 I just claimed my free 10th-Anniversary collectible NFT from ethereum.org 🔷 Celebrating a decade of open, decentralized innovation. Join me 👉 https://ethereum.org/en/10years/ #Ethereum10"
+ )
+ const tweetUrl = `https://twitter.com/intent/tweet?text=${tweetText}`
+
+ const triggerConfetti = () => {
+ const duration = 5000
+ const animationEnd = Date.now() + duration
+ const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 }
+
+ const randomInRange = (min: number, max: number) =>
+ Math.random() * (max - min) + min
+
+ const interval = window.setInterval(() => {
+ const timeLeft = animationEnd - Date.now()
+
+ if (timeLeft <= 0) {
+ return clearInterval(interval)
+ }
+
+ const particleCount = 50 * (timeLeft / duration)
+
+ confetti({
+ ...defaults,
+ particleCount,
+ origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
+ })
+ confetti({
+ ...defaults,
+ particleCount,
+ origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
+ })
+ }, 250)
+ }
+
+ useEffect(() => {
+ triggerConfetti()
+ }, [])
+
+ return (
+
+
+
+ Minting successful!
+ {txHash && (
+
+
+ View transaction on Etherscan
+
+
+ )}
+
+
+
+
Share the celebration
+
+ )
+}
diff --git a/app/[locale]/10years/_components/NFTMintCardWrapper.tsx b/app/[locale]/10years/_components/NFTMintCardWrapper.tsx
new file mode 100644
index 00000000000..e28c08cfedd
--- /dev/null
+++ b/app/[locale]/10years/_components/NFTMintCardWrapper.tsx
@@ -0,0 +1,40 @@
+"use client"
+
+import dynamic from "next/dynamic"
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
+
+import { Skeleton } from "@/components/ui/skeleton"
+
+// Dynamically import Wagmi/RainbowKit components
+const WalletProviders = dynamic(() => import("@/components/WalletProviders"), {
+ ssr: false,
+ loading: () => (
+
+ ),
+})
+
+const NFTMintCard = dynamic(() => import("./NFTMintCard"), {
+ ssr: false,
+ loading: () => (
+
+ ),
+})
+
+const queryClient = new QueryClient()
+
+interface NFTMintCardWrapperProps {
+ className?: string
+ locale?: string
+}
+
+const NFTMintCardWrapper = ({ className, locale }: NFTMintCardWrapperProps) => {
+ return (
+
+
+
+
+
+ )
+}
+
+export default NFTMintCardWrapper
diff --git a/app/[locale]/10years/_components/TenYearHomeBanner.tsx b/app/[locale]/10years/_components/TenYearHomeBanner.tsx
index 354442d3525..5d51ca47778 100644
--- a/app/[locale]/10years/_components/TenYearHomeBanner.tsx
+++ b/app/[locale]/10years/_components/TenYearHomeBanner.tsx
@@ -52,6 +52,7 @@ const TenYearHomeBanner = async () => {
{/* CLIENT SIDE, lazy loaded */}
{
+ const mintTimestamp = process.env.NEXT_PUBLIC_MINT_TIMESTAMP_START
+
+ if (!mintTimestamp) {
+ return false
+ }
+
+ try {
+ const mintDate = new Date(Number(mintTimestamp) * 1000)
+ const now = new Date()
+
+ // Check if the mint date has passed (or is current)
+ return now >= mintDate
+ } catch (error) {
+ console.error("Invalid NFT_MINT_DATE format:", mintTimestamp)
+ return false
+ }
+}
diff --git a/app/[locale]/10years/page.tsx b/app/[locale]/10years/page.tsx
index 23ffd6722a4..66f153bf0b4 100644
--- a/app/[locale]/10years/page.tsx
+++ b/app/[locale]/10years/page.tsx
@@ -29,7 +29,8 @@ import CountDown from "./_components/CountDown/lazy"
import CurrentTorchHolderCard from "./_components/CurrentTorchHolderCard"
import { adoptionStyles } from "./_components/data"
import InnovationSwiper from "./_components/InnovationSwiper/lazy"
-import TenYearGlobe from "./_components/TenYearGlobe/lazy"
+import NFTMintCardWrapper from "./_components/NFTMintCardWrapper"
+// import TenYearGlobe from "./_components/TenYearGlobe/lazy"
import TenYearHero from "./_components/TenYearHero"
import TorchHistorySwiper from "./_components/TorchHistorySwiper/lazy"
import Stories from "./_components/UserStories/lazy"
@@ -39,6 +40,7 @@ import {
getTimeUnitTranslations,
parseStoryDates,
} from "./_components/utils"
+import { shouldShowNFTMintCard } from "./_components/utils/nftMintDate"
import { routing } from "@/i18n/routing"
import { fetch10YearEvents } from "@/lib/api/fetch10YearEvents"
@@ -121,19 +123,28 @@ const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => {
)
const currentHolder = getCurrentHolder(torchHolders)
+ const showNFTMint = shouldShowNFTMintCard()
return (
-
-
-
+ {!showNFTMint && (
+
+
+
+ )}
-
+
@@ -146,12 +157,16 @@ const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => {
{t("page-10-year-hero-tagline")}
-
-
+
+ {showNFTMint ? (
+
+ ) : (
+
+ )}
@@ -167,7 +182,7 @@ const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => {
{/* CLIENT SIDE, lazy loaded */}
-
region.events.map((event) => ({
@@ -176,7 +191,7 @@ const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => {
lng: Number(event.lng),
}))
)}
- />
+ /> */}
diff --git a/package.json b/package.json
index ff0bee03bd5..3c5d02889af 100644
--- a/package.json
+++ b/package.json
@@ -56,8 +56,10 @@
"@socialgouv/matomo-next": "^1.8.0",
"@tanstack/react-query": "^5.66.7",
"@tanstack/react-table": "^8.19.3",
+ "@types/canvas-confetti": "^1.9.0",
"@types/three": "^0.177.0",
"@wagmi/core": "^2.17.3",
+ "canvas-confetti": "^1.9.3",
"chart.js": "^4.4.2",
"chartjs-plugin-datalabels": "^2.2.0",
"class-variance-authority": "^0.7.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9d39391527a..1a73147f708 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -86,12 +86,18 @@ importers:
'@tanstack/react-table':
specifier: ^8.19.3
version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@types/canvas-confetti':
+ specifier: ^1.9.0
+ version: 1.9.0
'@types/three':
specifier: ^0.177.0
version: 0.177.0
'@wagmi/core':
specifier: ^2.17.3
version: 2.17.3(@tanstack/query-core@5.80.2)(@types/react@18.2.57)(react@18.3.1)(typescript@5.8.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.30.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.22.4))
+ canvas-confetti:
+ specifier: ^1.9.3
+ version: 1.9.3
chart.js:
specifier: ^4.4.2
version: 4.4.9
@@ -2878,6 +2884,9 @@ packages:
'@types/babel__traverse@7.20.7':
resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==}
+ '@types/canvas-confetti@1.9.0':
+ resolution: {integrity: sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==}
+
'@types/d3-array@3.2.1':
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
@@ -3890,6 +3899,9 @@ packages:
caniuse-lite@1.0.30001723:
resolution: {integrity: sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==}
+ canvas-confetti@1.9.3:
+ resolution: {integrity: sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==}
+
case-sensitive-paths-webpack-plugin@2.4.0:
resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==}
engines: {node: '>=4'}
@@ -11621,6 +11633,8 @@ snapshots:
dependencies:
'@babel/types': 7.27.3
+ '@types/canvas-confetti@1.9.0': {}
+
'@types/d3-array@3.2.1': {}
'@types/d3-color@3.1.3': {}
@@ -13241,6 +13255,8 @@ snapshots:
caniuse-lite@1.0.30001723: {}
+ canvas-confetti@1.9.3: {}
+
case-sensitive-paths-webpack-plugin@2.4.0: {}
ccount@2.0.1: {}
diff --git a/public/content/10years/terms-and-conditions/index.md b/public/content/10years/terms-and-conditions/index.md
new file mode 100644
index 00000000000..13d2d214c78
--- /dev/null
+++ b/public/content/10years/terms-and-conditions/index.md
@@ -0,0 +1,65 @@
+---
+title: Ethereum 10-Year Anniversary NFT Mint Terms & Conditions
+lang: en
+hideEditButton: true
+---
+
+# Commemorative NFT minting terms {#commemorative-nft-minting-terms}
+
+20 June 2025
+
+**10 YEARS OF ETHEREUM TORCH MINTING TERMS & CONDITIONS**
+
+**PLEASE READ THESE TERMS & CONDITIONS BEFORE MINTING THE 10 YEARS OF ETHEREUM TORCH**
+
+These terms and conditions constitute a binding agreement (the “**Agreement**”) between you (“**You**”) and the Ethereum Foundation, a Swiss foundation registered in Zug, Switzerland (the “**EF**”), governing Your minting and use of the non-fungible token known as the **10 Years of Ethereum Torch** (the “**NFT**”). By initiating the minting transaction, You acknowledge that You have read, understood, and agree to be bound by this Agreement. If You do not agree, do not proceed with minting.
+
+## 1. Nature of the NFT {#nature-of-the-nft}
+
+1. **Commemorative Purpose**. The NFT is issued solely to commemorate the ten-year anniversary of the Ethereum Genesis Block. It confers no ownership interest, financial right, expectation of profit, reward, dividend, governance right, utility, or any other right of any kind.
+
+2. **No Consideration**. The NFT is minted without charge; You are responsible only for the network transaction gas fees required to execute the minting transaction. EF receives no payment, royalty, or other consideration from Your mint.
+
+## 2. Intellectual property {#intellectual-property}
+
+1. **CC BY 4.0 Licence**. The artwork embodied in or associated with the NFT is licensed to the public under the [Creative Commons CCÂ BYÂ 4.0 International Licence](https://creativecommons.org/licenses/by/4.0/).
+
+2. **No Ownership Rights Conferred**. Minting, holding, or transferring the NFT does not transfer or confer any ownership right, title, or interest in or to the artwork or any other intellectual property of EF.
+
+## 3. Representations and warranties {#representations-and-warranties}
+
+You represent, warrant, and covenant that:
+
+1) You are at least eighteen (18) years old and have legal capacity to enter into this Agreement;
+
+2) You are not: (i) the subject of any economic or trade sanctions imposed or administered by Switzerland, the United States (including the OFAC SDN List), the European Union, the United Kingdom, the United Nations, or any other similar regime; and/or (ii) located, organised, or resident in a comprehensively sanctioned jurisdiction (currently North Korea Crimea, Iran, Syria, Cuba, Donetsk, or Luhansk);
+
+3) You are minting the NFT solely for commemorative and personal purposes and not as an investment or with an expectation of profit; and
+
+4) You are not acting on behalf of, or for the benefit of, any person or entity that fails to satisfy the foregoing.
+
+## 4. Disclaimers {#disclaimers}
+
+THE NFT AND ANY RELATED MATERIALS ARE PROVIDED “AS IS” AND “AS AVAILABLE”, WITHOUT WARRANTIES OF ANY KIND, WHETHER EXPRESS, IMPLIED, OR STATUTORY, INCLUDING WARRANTIES OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR QUIET ENJOYMENT. EF DOES NOT WARRANT THAT THE NFT OR THE MINTING PROCESS WILL BE ERROR-FREE OR UNINTERRUPTED.
+
+## 5. Limitation of liability {#limitation-of-liability}
+
+To the maximum extent permitted by applicable law, EF, its directors, officers, employees, contractors, and agents shall not be liable to You for any indirect, incidental, consequential, special, exemplary, or punitive damages, or for any loss of profits, data, or goodwill, arising out of or related to the NFT or this Agreement, whether based in contract, tort, strict liability, or otherwise. EF’s aggregate liability to You for any direct damages shall not exceed one USD.
+
+## 6. Termination {#termination}
+
+EF may terminate this Agreement at any time. EF reserves the right to suspend or terminate minting, or to take reasonable remedial action (including nullification of the NFT) where required by law or regulation.
+
+## 7. Governing law and jurisdiction {#governing-law-and-jurisdiction}
+
+Any dispute, controversy, or claim arising out of or relating to this Agreement, including the validity, invalidity, breach, or termination thereof, shall be resolved by arbitration in accordance with the Swiss Rules of International Arbitration of the Swiss Chambers’ Arbitration Institution in force on the date on which the Notice of Arbitration is submitted in accordance with these Rules. The number of arbitrators shall be one. The seat of the arbitration shall be Zurich unless the parties agree on a different seat. The arbitral proceedings shall be conducted in English.
+
+## 8. Miscellaneous {#miscellaneous}
+
+1. **Entire Agreement**. This Agreement constitutes the entire agreement between You and EF with respect to the NFT and supersede all prior understandings.
+
+2. **Severability**. If any provision is held invalid or unenforceable, the remaining provisions shall remain in full force and effect.
+
+3. **No Waiver**. Failure or delay by EF to exercise any right shall not operate as a waiver thereof.
+
+4. **Assignment**. You may not assign or transfer Your rights or obligations under this Agreement without EF’s prior written consent.
\ No newline at end of file
diff --git a/public/images/10-year-anniversary/10y-cover.png b/public/images/10-year-anniversary/10y-cover.png
new file mode 100644
index 00000000000..e62dcaa454b
Binary files /dev/null and b/public/images/10-year-anniversary/10y-cover.png differ
diff --git a/public/images/10-year-anniversary/10y-curved-heading.svg b/public/images/10-year-anniversary/10y-curved-heading.svg
new file mode 100644
index 00000000000..d32d71ead92
--- /dev/null
+++ b/public/images/10-year-anniversary/10y-curved-heading.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/videos/10y-video.mp4 b/public/videos/10y-video.mp4
new file mode 100755
index 00000000000..21e08ebbf50
Binary files /dev/null and b/public/videos/10y-video.mp4 differ
diff --git a/src/config/rainbow-kit.ts b/src/config/rainbow-kit.ts
index 5ed4ba6c3bd..5d580f113d9 100644
--- a/src/config/rainbow-kit.ts
+++ b/src/config/rainbow-kit.ts
@@ -1,4 +1,5 @@
-import { mainnet } from "wagmi/chains"
+import { http } from "wagmi"
+import { hardhat, mainnet, sepolia } from "wagmi/chains"
import { getDefaultConfig } from "@rainbow-me/rainbowkit"
import {
coinbaseWallet,
@@ -9,10 +10,62 @@ import {
zerionWallet,
} from "@rainbow-me/rainbowkit/wallets"
+const CHAIN_MAP = {
+ hardhat,
+ sepolia,
+ mainnet,
+} as const
+
+// Determine which chains to use based on env vars
+export const getTargetChains = () => {
+ const chainNames =
+ process.env.NEXT_PUBLIC_CHAIN_NAMES?.split(",").map((name) =>
+ name.trim()
+ ) || []
+
+ if (chainNames.length === 0) {
+ return [hardhat]
+ }
+
+ // Map chain names to actual chain objects
+ const validChains = chainNames
+ .map((name) => CHAIN_MAP[name as keyof typeof CHAIN_MAP])
+ .filter(Boolean)
+
+ // If no valid chains found, fallback to just hardhat
+ if (validChains.length === 0) {
+ console.warn(
+ `No valid chains found for: ${chainNames.join(", ")}. Falling back to hardhat.`
+ )
+ return [hardhat]
+ }
+
+ return validChains
+}
+
+const getTransports = () => {
+ const alchemyApiKey = process.env.NEXT_PUBLIC_ALCHEMY_API_KEY
+
+ if (!alchemyApiKey) {
+ console.warn(
+ "NEXT_PUBLIC_ALCHEMY_API_KEY not found, falling back to public RPC"
+ )
+ return undefined
+ }
+
+ return {
+ [mainnet.id]: http(`https://eth-mainnet.g.alchemy.com/v2/${alchemyApiKey}`),
+ [sepolia.id]: http(`https://eth-sepolia.g.alchemy.com/v2/${alchemyApiKey}`),
+ [hardhat.id]: http("http://127.0.0.1:8545"),
+ }
+}
+
export const rainbowkitConfig = getDefaultConfig({
appName: "ethereum.org",
projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID!,
- chains: [mainnet],
+ // @ts-expect-error - TODO: fix this
+ chains: getTargetChains(),
+ transports: getTransports(),
wallets: [
{
groupName: "New to crypto",
diff --git a/src/data/contracts/TenYearsNFT.json b/src/data/contracts/TenYearsNFT.json
new file mode 100644
index 00000000000..ca7693f728c
--- /dev/null
+++ b/src/data/contracts/TenYearsNFT.json
@@ -0,0 +1,678 @@
+{
+ "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3",
+ "blockNumber": 1,
+ "abi": [
+ {
+ "inputs": [
+ {
+ "internalType": "string",
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "internalType": "string",
+ "name": "symbol",
+ "type": "string"
+ },
+ {
+ "internalType": "address",
+ "name": "initialOwner",
+ "type": "address"
+ },
+ {
+ "internalType": "string",
+ "name": "baseURI",
+ "type": "string"
+ }
+ ],
+ "stateMutability": "nonpayable",
+ "type": "constructor"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "sender",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ },
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ }
+ ],
+ "name": "ERC721IncorrectOwner",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "ERC721InsufficientApproval",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "approver",
+ "type": "address"
+ }
+ ],
+ "name": "ERC721InvalidApprover",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address"
+ }
+ ],
+ "name": "ERC721InvalidOperator",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ }
+ ],
+ "name": "ERC721InvalidOwner",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "receiver",
+ "type": "address"
+ }
+ ],
+ "name": "ERC721InvalidReceiver",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "sender",
+ "type": "address"
+ }
+ ],
+ "name": "ERC721InvalidSender",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "ERC721NonexistentToken",
+ "type": "error"
+ },
+ {
+ "inputs": [],
+ "name": "EnforcedPause",
+ "type": "error"
+ },
+ {
+ "inputs": [],
+ "name": "ExpectedPause",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ }
+ ],
+ "name": "OwnableInvalidOwner",
+ "type": "error"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "account",
+ "type": "address"
+ }
+ ],
+ "name": "OwnableUnauthorizedAccount",
+ "type": "error"
+ },
+ {
+ "inputs": [],
+ "name": "ReentrancyGuardReentrantCall",
+ "type": "error"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "approved",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "Approval",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "operator",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "internalType": "bool",
+ "name": "approved",
+ "type": "bool"
+ }
+ ],
+ "name": "ApprovalForAll",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "previousOwner",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "newOwner",
+ "type": "address"
+ }
+ ],
+ "name": "OwnershipTransferred",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": false,
+ "internalType": "address",
+ "name": "account",
+ "type": "address"
+ }
+ ],
+ "name": "Paused",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "TokenMinted",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "from",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "Transfer",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": false,
+ "internalType": "address",
+ "name": "account",
+ "type": "address"
+ }
+ ],
+ "name": "Unpaused",
+ "type": "event"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "approve",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ }
+ ],
+ "name": "balanceOf",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "getApproved",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "name": "hasMinted",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "owner",
+ "type": "address"
+ },
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address"
+ }
+ ],
+ "name": "isApprovedForAll",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "mint",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "name",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "owner",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "ownerOf",
+ "outputs": [
+ {
+ "internalType": "address",
+ "name": "",
+ "type": "address"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "pause",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "paused",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "renounceOwnership",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address"
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "safeTransferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address"
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ },
+ {
+ "internalType": "bytes",
+ "name": "data",
+ "type": "bytes"
+ }
+ ],
+ "name": "safeTransferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "operator",
+ "type": "address"
+ },
+ {
+ "internalType": "bool",
+ "name": "approved",
+ "type": "bool"
+ }
+ ],
+ "name": "setApprovalForAll",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "string",
+ "name": "baseURI",
+ "type": "string"
+ }
+ ],
+ "name": "setBaseURI",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "bytes4",
+ "name": "interfaceId",
+ "type": "bytes4"
+ }
+ ],
+ "name": "supportsInterface",
+ "outputs": [
+ {
+ "internalType": "bool",
+ "name": "",
+ "type": "bool"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "symbol",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "tokenURI",
+ "outputs": [
+ {
+ "internalType": "string",
+ "name": "",
+ "type": "string"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "totalSupply",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "from",
+ "type": "address"
+ },
+ {
+ "internalType": "address",
+ "name": "to",
+ "type": "address"
+ },
+ {
+ "internalType": "uint256",
+ "name": "tokenId",
+ "type": "uint256"
+ }
+ ],
+ "name": "transferFrom",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
+ "internalType": "address",
+ "name": "newOwner",
+ "type": "address"
+ }
+ ],
+ "name": "transferOwnership",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "name": "unpause",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ }
+ ]
+}
diff --git a/src/data/contracts/TenYearsNFT.ts b/src/data/contracts/TenYearsNFT.ts
new file mode 100644
index 00000000000..f4248eab6c8
--- /dev/null
+++ b/src/data/contracts/TenYearsNFT.ts
@@ -0,0 +1,25 @@
+import { hardhat, sepolia } from "wagmi/chains"
+
+import TenYearsNFT from "./TenYearsNFT.json"
+
+export const TEN_YEARS_NFT_CONTRACTS = {
+ [hardhat.id]: {
+ address: "0x5FbDB2315678afecb367f032d93F642f64180aa3",
+ blockNumber: 1,
+ abi: TenYearsNFT.abi,
+ },
+ [sepolia.id]: {
+ address: "0x388B10E1F9aC2a0a6bd874d00a971875Ae89Ec6E",
+ blockNumber: 8863414,
+ abi: TenYearsNFT.abi,
+ },
+} as const
+
+export const getTenYearsNFTContract = (chainId: number) => {
+ return TEN_YEARS_NFT_CONTRACTS[
+ chainId as keyof typeof TEN_YEARS_NFT_CONTRACTS
+ ]
+}
+
+export type TenYearsNFTContract =
+ (typeof TEN_YEARS_NFT_CONTRACTS)[keyof typeof TEN_YEARS_NFT_CONTRACTS]
diff --git a/src/hooks/useGasPrice.ts b/src/hooks/useGasPrice.ts
new file mode 100644
index 00000000000..1616dba1a71
--- /dev/null
+++ b/src/hooks/useGasPrice.ts
@@ -0,0 +1,100 @@
+import { useEffect, useState } from "react"
+import { usePublicClient } from "wagmi"
+
+interface GasPriceData {
+ standard: number // in gwei
+ fast: number // in gwei
+ instant: number // in gwei
+ timestamp: number
+}
+
+interface GasPriceState {
+ data: GasPriceData | null
+ loading: boolean
+ error: Error | null
+}
+
+// Gas price thresholds in gwei
+export const GAS_THRESHOLDS = {
+ LOW: 20,
+ MODERATE: 40,
+ HIGH: 80,
+ VERY_HIGH: 150,
+} as const
+
+export type GasPriceLevel = "low" | "moderate" | "high" | "very_high"
+
+export const getGasPriceLevel = (gasPrice: number): GasPriceLevel => {
+ if (gasPrice >= GAS_THRESHOLDS.VERY_HIGH) return "very_high"
+ if (gasPrice >= GAS_THRESHOLDS.HIGH) return "high"
+ if (gasPrice >= GAS_THRESHOLDS.MODERATE) return "moderate"
+ return "low"
+}
+
+export const useGasPrice = () => {
+ const [state, setState] = useState
({
+ data: null,
+ loading: true,
+ error: null,
+ })
+
+ const publicClient = usePublicClient()
+
+ useEffect(() => {
+ const fetchGasPrice = async () => {
+ if (!publicClient) return
+
+ try {
+ setState((prev) => ({ ...prev, loading: true, error: null }))
+
+ // Get current gas price from the network
+ const gasPrice = await publicClient.getGasPrice()
+
+ // Convert from wei to gwei
+ const gasPriceGwei = Number(gasPrice) / 1e9
+
+ // For simplicity, we'll use the network gas price as standard
+ // and calculate fast/instant as multiples
+ const gasPriceData: GasPriceData = {
+ standard: Math.round(gasPriceGwei),
+ fast: Math.round(gasPriceGwei * 1.2),
+ instant: Math.round(gasPriceGwei * 1.5),
+ timestamp: Date.now(),
+ }
+
+ setState({
+ data: gasPriceData,
+ loading: false,
+ error: null,
+ })
+ } catch (error) {
+ console.error("Failed to fetch gas price:", error)
+ setState({
+ data: null,
+ loading: false,
+ error:
+ error instanceof Error
+ ? error
+ : new Error("Failed to fetch gas price"),
+ })
+ }
+ }
+
+ fetchGasPrice()
+
+ // Refresh gas price every 30 seconds
+ const interval = setInterval(fetchGasPrice, 30000)
+
+ return () => clearInterval(interval)
+ }, [publicClient])
+
+ const gasLevel = state.data ? getGasPriceLevel(state.data.standard) : null
+ const shouldWarn = gasLevel === "high" || gasLevel === "very_high"
+
+ return {
+ ...state,
+ gasLevel,
+ shouldWarn,
+ refresh: () => setState((prev) => ({ ...prev, loading: true })),
+ }
+}
diff --git a/src/hooks/useNetworkContract.ts b/src/hooks/useNetworkContract.ts
new file mode 100644
index 00000000000..401596dbcd8
--- /dev/null
+++ b/src/hooks/useNetworkContract.ts
@@ -0,0 +1,45 @@
+import { useAccount, useChainId } from "wagmi"
+import { hardhat, mainnet, sepolia } from "wagmi/chains"
+
+import { getTenYearsNFTContract } from "@/data/contracts/TenYearsNFT"
+
+import { getTargetChains } from "@/config/rainbow-kit"
+
+export const useNetworkContract = () => {
+ const { chainId: accountChainId } = useAccount()
+ const chainId = useChainId()
+
+ const getContractData = () => {
+ const contractData = getTenYearsNFTContract(chainId)
+
+ if (!contractData) {
+ throw new Error(`Contract not deployed on chain ${chainId}`)
+ }
+
+ return contractData
+ }
+
+ const isSupportedNetwork = () => {
+ return getTargetChains().some((chain) => chain.id === accountChainId)
+ }
+
+ const getNetworkName = () => {
+ switch (chainId) {
+ case hardhat.id:
+ return "Hardhat (Local)"
+ case sepolia.id:
+ return "Sepolia"
+ case mainnet.id:
+ return "Mainnet"
+ default:
+ return "Unsupported Network"
+ }
+ }
+
+ return {
+ chainId,
+ contractData: getContractData(),
+ isSupportedNetwork: isSupportedNetwork(),
+ networkName: getNetworkName(),
+ }
+}
diff --git a/src/intl/en/common.json b/src/intl/en/common.json
index 9f7b0a15275..a348d3514e3 100644
--- a/src/intl/en/common.json
+++ b/src/intl/en/common.json
@@ -432,6 +432,7 @@
"statelessness": "Statelessness",
"style-guide": "Style guide",
"support": "Support",
+ "terms-and-conditions": "Terms & Conditions",
"terms-of-use": "Terms of use",
"translation-banner-body-new": "You’re viewing this page in English because we haven’t translated it yet. Help us translate this content.",
"translation-banner-body-update": "There’s a new version of this page but it’s only in English right now. Help us translate the latest version.",
diff --git a/src/lib/torch/config.ts b/src/lib/torch/config.ts
index 5ea93327418..3a8eb0c14e0 100644
--- a/src/lib/torch/config.ts
+++ b/src/lib/torch/config.ts
@@ -5,10 +5,10 @@ export const config = createConfig({
chains: [mainnet],
transports: {
[mainnet.id]: http(
- `https://eth-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`
+ `https://eth-mainnet.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_API_KEY}`
),
// [sepolia.id]: http(
- // `https://eth-sepolia.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`
+ // `https://eth-sepolia.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_API_KEY}`
// ),
// [hardhat.id]: http("http://127.0.0.1:8545"),
},
diff --git a/src/lib/torch/index.ts b/src/lib/torch/index.ts
index 98e2d8be3bf..51b606ccd0f 100644
--- a/src/lib/torch/index.ts
+++ b/src/lib/torch/index.ts
@@ -154,7 +154,7 @@ export const extractTwitterHandle = (twitterUrl: string): string | null => {
}
export const formatAddress = (address: Address) => {
- return `${address.slice(0, 6)}...${address.slice(-4)}`
+ return `${address.slice(0, 7)}...${address.slice(-5)}`
}
export const formatDate = (timestamp: number) => {
@@ -198,3 +198,20 @@ export async function resolveEnsName(
return null
}
}
+
+export const getErrorMessage = (error: Error) => {
+ if (error.message.includes("insufficient funds")) {
+ return "Insufficient funds"
+ }
+ if (error.message.includes("not enough ETH")) {
+ return "Not enough ETH"
+ }
+ if (error.message.includes("EnforcedPause")) {
+ return "Contract is paused"
+ }
+ if (error.message.includes("already minted")) {
+ return "You have already minted an NFT"
+ }
+
+ return "An error occurred during minting"
+}