({
export async function GET() {
// Preview mode: Show menu with original default
- if (IS_PREVIEW_DEPLOY) return NextResponse.json(getPreviewConfig())
+ if (!IS_PROD || IS_PREVIEW_DEPLOY)
+ return NextResponse.json(getPreviewConfig())
try {
const matomoUrl = process.env.NEXT_PUBLIC_MATOMO_URL
diff --git a/next.config.js b/next.config.js
index 1182cd32334..5c39cb7693e 100644
--- a/next.config.js
+++ b/next.config.js
@@ -93,10 +93,6 @@ module.exports = (phase, { defaultConfig }) => {
protocol: "https",
hostname: "coin-images.coingecko.com",
},
- {
- protocol: "https",
- hostname: "unavatar.io",
- },
],
},
async headers() {
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/images/10-year-anniversary/torchbearers/0x0c004944e16e9065Da1c7dB49F9964E2a3ac8892.jpg b/public/images/10-year-anniversary/torchbearers/0x0c004944e16e9065Da1c7dB49F9964E2a3ac8892.jpg
new file mode 100644
index 00000000000..0a03b820a43
Binary files /dev/null and b/public/images/10-year-anniversary/torchbearers/0x0c004944e16e9065Da1c7dB49F9964E2a3ac8892.jpg differ
diff --git a/public/images/10-year-anniversary/torchbearers/0x11adBC1B3fd5cb5F29B0052b4AfFe725645b5e4C.jpg b/public/images/10-year-anniversary/torchbearers/0x11adBC1B3fd5cb5F29B0052b4AfFe725645b5e4C.jpg
new file mode 100644
index 00000000000..1b15a7da6a3
Binary files /dev/null and b/public/images/10-year-anniversary/torchbearers/0x11adBC1B3fd5cb5F29B0052b4AfFe725645b5e4C.jpg differ
diff --git a/public/images/10-year-anniversary/torchbearers/0x36ACC9E5248f33B030d3eA3465AC1f99E55868Ec.jpg b/public/images/10-year-anniversary/torchbearers/0x36ACC9E5248f33B030d3eA3465AC1f99E55868Ec.jpg
new file mode 100644
index 00000000000..9b9ea20c6f0
Binary files /dev/null and b/public/images/10-year-anniversary/torchbearers/0x36ACC9E5248f33B030d3eA3465AC1f99E55868Ec.jpg differ
diff --git a/public/images/10-year-anniversary/torchbearers/0x54bae63e59B422Dd7C047E375f051D60C37cb60F.jpg b/public/images/10-year-anniversary/torchbearers/0x54bae63e59B422Dd7C047E375f051D60C37cb60F.jpg
new file mode 100644
index 00000000000..78d3a8c497d
Binary files /dev/null and b/public/images/10-year-anniversary/torchbearers/0x54bae63e59B422Dd7C047E375f051D60C37cb60F.jpg differ
diff --git a/public/images/10-year-anniversary/torchbearers/0x648aA14e4424e0825A5cE739C8C68610e143FB79.jpg b/public/images/10-year-anniversary/torchbearers/0x648aA14e4424e0825A5cE739C8C68610e143FB79.jpg
new file mode 100644
index 00000000000..579ff40312a
Binary files /dev/null and b/public/images/10-year-anniversary/torchbearers/0x648aA14e4424e0825A5cE739C8C68610e143FB79.jpg differ
diff --git a/public/images/10-year-anniversary/torchbearers/0x7a16fF8270133F063aAb6C9977183D9e72835428.jpg b/public/images/10-year-anniversary/torchbearers/0x7a16fF8270133F063aAb6C9977183D9e72835428.jpg
new file mode 100644
index 00000000000..172a905f938
Binary files /dev/null and b/public/images/10-year-anniversary/torchbearers/0x7a16fF8270133F063aAb6C9977183D9e72835428.jpg differ
diff --git a/public/images/10-year-anniversary/torchbearers/0x88C2C3C9E64a1299e6417C24Fa2ae773c6cEa47c.jpg b/public/images/10-year-anniversary/torchbearers/0x88C2C3C9E64a1299e6417C24Fa2ae773c6cEa47c.jpg
new file mode 100644
index 00000000000..baa436665a4
Binary files /dev/null and b/public/images/10-year-anniversary/torchbearers/0x88C2C3C9E64a1299e6417C24Fa2ae773c6cEa47c.jpg differ
diff --git a/public/images/10-year-anniversary/torchbearers/0xA307A15d113D9763C6fc84768AC34909438bB2EE.jpg b/public/images/10-year-anniversary/torchbearers/0xA307A15d113D9763C6fc84768AC34909438bB2EE.jpg
new file mode 100644
index 00000000000..56f93421537
Binary files /dev/null and b/public/images/10-year-anniversary/torchbearers/0xA307A15d113D9763C6fc84768AC34909438bB2EE.jpg differ
diff --git a/public/images/10-year-anniversary/torchbearers/0xcc2047a4108033Cb48727B8C69914F40cC0bBC1B.jpg b/public/images/10-year-anniversary/torchbearers/0xcc2047a4108033Cb48727B8C69914F40cC0bBC1B.jpg
new file mode 100644
index 00000000000..554cdfb2cd8
Binary files /dev/null and b/public/images/10-year-anniversary/torchbearers/0xcc2047a4108033Cb48727B8C69914F40cC0bBC1B.jpg differ
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/components/YouTube.tsx b/src/components/YouTube.tsx
index 3b5bb23a5cc..58e4e26128b 100644
--- a/src/components/YouTube.tsx
+++ b/src/components/YouTube.tsx
@@ -3,6 +3,8 @@
import React from "react"
import LiteYouTubeEmbed from "react-lite-youtube-embed"
+import { cn } from "@/lib/utils/cn"
+
import "react-lite-youtube-embed/dist/LiteYouTubeEmbed.css"
/**
@@ -19,13 +21,20 @@ type YouTubeProps = {
id: string
start?: string
title?: string
-}
+ className?: string
+} & React.ComponentProps
-const YouTube = ({ id, start = "0", title = "YouTube" }: YouTubeProps) => {
+const YouTube = ({
+ id,
+ start = "0",
+ title = "YouTube",
+ className,
+ ...props
+}: YouTubeProps) => {
const params = new URLSearchParams()
;+start > 0 && params.set("start", start)
return (
-
+
{
title={title}
params={params.toString()}
noCookie
+ {...props}
/>
)
diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx
index 5e5c20a09d9..fa2fb3494ab 100644
--- a/src/components/ui/avatar.tsx
+++ b/src/components/ui/avatar.tsx
@@ -172,7 +172,7 @@ const Avatar = React.forwardRef<
sizes="4rem"
src={src}
alt={name}
- quality={90}
+ quality={100}
/>
) : (
@@ -195,7 +195,7 @@ const Avatar = React.forwardRef<
sizes="4rem"
src={src}
alt={name}
- quality={90}
+ quality={100}
/>
) : (
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..9d18376af14
--- /dev/null
+++ b/src/data/contracts/TenYearsNFT.json
@@ -0,0 +1,662 @@
+{
+ "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"
+ },
+ {
+ "internalType": "uint256",
+ "name": "_startTime",
+ "type": "uint256"
+ },
+ {
+ "internalType": "uint256",
+ "name": "_endTime",
+ "type": "uint256"
+ }
+ ],
+ "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": [
+ {
+ "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": 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"
+ },
+ {
+ "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": [],
+ "name": "endTime",
+ "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": "isMintingActive",
+ "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": "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": [],
+ "name": "startTime",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "stateMutability": "view",
+ "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"
+ }
+ ]
+}
diff --git a/src/data/contracts/TenYearsNFT.ts b/src/data/contracts/TenYearsNFT.ts
new file mode 100644
index 00000000000..a9112e88f61
--- /dev/null
+++ b/src/data/contracts/TenYearsNFT.ts
@@ -0,0 +1,30 @@
+import { hardhat, mainnet, 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,
+ },
+ [mainnet.id]: {
+ address: "0x26d85a13212433fe6a8381969c2b0db390a0b0ae",
+ blockNumber: 23023215,
+ 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/data/torchHolders.json b/src/data/torchHolders.json
new file mode 100644
index 00000000000..9e6d03fc525
--- /dev/null
+++ b/src/data/torchHolders.json
@@ -0,0 +1,62 @@
+[
+ {
+ "address": "0x88C2C3C9E64a1299e6417C24Fa2ae773c6cEa47c",
+ "name": "Joseph Lubin",
+ "role": "Co-founder of Ethereum",
+ "twitter": "https://x.com/ethereumJoseph"
+ },
+ {
+ "address": "0x11adBC1B3fd5cb5F29B0052b4AfFe725645b5e4C",
+ "name": "Audrey Tang",
+ "role": "Cyber Ambassador, 1st Digital Minister of Taiwan (2016-2024)",
+ "twitter": "https://x.com/audreyt"
+ },
+ {
+ "address": "0xcc2047a4108033Cb48727B8C69914F40cC0bBC1B",
+ "name": "Manoj Gorle",
+ "role": "Co-president 0xblocsoc, Undergrad IIT Delhi",
+ "twitter": "https://x.com/manojkgorle"
+ },
+ {
+ "address": "0x5F19021618AF1cEB5De7Ca112B505F51f813aE18",
+ "name": "Roman and Alexey Defense Fund",
+ "role": "",
+ "twitter": ""
+ },
+ {
+ "address": "0x7a16fF8270133F063aAb6C9977183D9e72835428",
+ "name": "Michael Egorov",
+ "role": "Founder of Curve Finance",
+ "twitter": "https://x.com/newmichwill"
+ },
+ {
+ "address": "0x0c004944e16e9065Da1c7dB49F9964E2a3ac8892",
+ "name": "LetĂcia Pires",
+ "role": "CEO Pomodoki",
+ "twitter": "https://x.com/letispires"
+ },
+ {
+ "address": "0x54bae63e59B422Dd7C047E375f051D60C37cb60F",
+ "name": "Ayodeje Ebunayo",
+ "role": "Founder of Web3Bridge",
+ "twitter": "https://x.com/Ebunayo08"
+ },
+ {
+ "address": "0xA307A15d113D9763C6fc84768AC34909438bB2EE",
+ "name": "Alex Bornyakov",
+ "role": "The Deputy Minister of Digital Transformation of Ukraine",
+ "twitter": "https://x.com/abornyakov"
+ },
+ {
+ "address": "0x648aA14e4424e0825A5cE739C8C68610e143FB79",
+ "name": "Anthony Sassano",
+ "role": "Founder of The Daily Gwei",
+ "twitter": "https://x.com/sassal0x"
+ },
+ {
+ "address": "0x36ACC9E5248f33B030d3eA3465AC1f99E55868Ec",
+ "name": "Candela Fassano",
+ "role": "Builder SEED Latam",
+ "twitter": "https://x.com/candufaz"
+ }
+]
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 06ff2f39dad..595efcfd0cd 100644
--- a/src/intl/en/common.json
+++ b/src/intl/en/common.json
@@ -431,6 +431,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/ab-testing/server.ts b/src/lib/ab-testing/server.ts
index 42df90fb1d6..8dd9032787f 100644
--- a/src/lib/ab-testing/server.ts
+++ b/src/lib/ab-testing/server.ts
@@ -24,12 +24,26 @@ export const getABTestAssignment = async (
if (!testConfig || !testConfig.enabled) return null
- // Create deterministic assignment using IP + User-Agent fingerprint
+ // Create deterministic assignment using enhanced fingerprint
const headers = await import("next/headers").then((m) => m.headers())
- const userAgent = headers.get("user-agent") || ""
+
+ // Get IP and user agent (primary identifier)
const forwardedFor =
headers.get("x-forwarded-for") || headers.get("x-real-ip") || "unknown"
- const fingerprint = `${forwardedFor}-${userAgent}`
+ const userAgent = headers.get("user-agent") || ""
+
+ // Add privacy-preserving entropy sources
+ const acceptLanguage = headers.get("accept-language") || ""
+ const acceptEncoding = headers.get("accept-encoding") || ""
+
+ // Create enhanced fingerprint with more entropy
+ const fingerprint = [
+ forwardedFor,
+ userAgent,
+ acceptLanguage,
+ acceptEncoding,
+ testKey, // Include test key to ensure different tests get different distributions
+ ].join("|")
const variantIndex = assignVariantIndexDeterministic(testConfig, fingerprint)
const variant = testConfig.variants[variantIndex]
@@ -56,23 +70,22 @@ const assignVariantIndexDeterministic = (
// Handle case where total weight is 0
if (totalWeight === 0) return 0
- // Use a better hash function for more uniform distribution
- // This is a simple implementation of djb2 hash algorithm
- let hash = 5381
+ // Hash function to evenly distribute fingerprints amongst assignments
+ // Implementation of FNV-1a hash algorithm
+ let hash = 2166136261 // FNV offset basis
for (let i = 0; i < fingerprint.length; i++) {
- hash = (hash << 5) + hash + fingerprint.charCodeAt(i)
+ hash ^= fingerprint.charCodeAt(i) // XOR
+ hash = (hash * 16777619) >>> 0 // FNV prime, ensure 32-bit unsigned
}
- // Ensure positive value and create uniform distribution
- const normalized = Math.abs(hash) / 0x7fffffff // Max 32-bit signed int
+ // Convert to uniform distribution [0, 1)
+ const normalized = hash / 0x100000000 // 2^32 for full 32-bit range
const weighted = normalized * totalWeight
let cumulativeWeight = 0
for (let i = 0; i < config.variants.length; i++) {
cumulativeWeight += config.variants[i].weight
- if (weighted <= cumulativeWeight) {
- return i
- }
+ if (weighted <= cumulativeWeight) return i
}
return 0
diff --git a/src/lib/api/fetchTorchHolders.ts b/src/lib/api/fetchTorchHolders.ts
deleted file mode 100644
index 92e39945868..00000000000
--- a/src/lib/api/fetchTorchHolders.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { resolveEnsName, type TorchHolder } from "@/lib/torch"
-
-export async function fetchTorchHolders(): Promise {
- const googleApiKey = process.env.GOOGLE_API_KEY
- const sheetId = process.env.GOOGLE_SHEET_ID_TORCH_HOLDERS
-
- if (!googleApiKey) {
- console.warn("Google API key not set")
- return []
- }
-
- if (!sheetId) {
- console.warn("Google Sheet ID for torch holders not set")
- return []
- }
-
- try {
- const url = `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}/values/Website%20Info!A:E?majorDimension=ROWS&key=${googleApiKey}`
- const response = await fetch(url)
-
- if (!response.ok) {
- const errorText = await response.text()
- console.error("Google Sheets API Error:", {
- status: response.status,
- statusText: response.statusText,
- error: errorText,
- })
- throw new Error(
- `Google Sheets API responded with ${response.status}: ${response.statusText}`
- )
- }
-
- const data = await response.json()
- // data.values[0] is the header row
- const rows = data.values.slice(1) || []
-
- // Map rows to TorchHolder objects with ENS resolution
- const holders: TorchHolder[] = []
-
- for (const row of rows) {
- if (!row[0]) continue // must have address or ENS name
-
- const addressOrEns = row[0].trim()
- const resolvedAddress = await resolveEnsName(addressOrEns)
-
- if (resolvedAddress) {
- holders.push({
- address: resolvedAddress,
- name: row[1] || "",
- role: row[2] || "",
- twitter: row[3] || "",
- })
- } else {
- console.warn(`Could not resolve address or ENS name: ${addressOrEns}`)
- }
- }
-
- return holders
- } catch (error) {
- console.error("Error fetching torch holders from Google Sheets:", error)
- return []
- }
-}
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..9390bd7c307 100644
--- a/src/lib/torch/index.ts
+++ b/src/lib/torch/index.ts
@@ -125,10 +125,8 @@ export const getAvatarImage = (holder: TorchHolderMetadata | null) => {
// If there's a Twitter handle, use Twitter profile image
if (holder.twitter && holder.twitter.trim() !== "") {
- const twitterHandle = extractTwitterHandle(holder.twitter)
- if (twitterHandle) {
- return `https://unavatar.io/x/${twitterHandle}`
- }
+ const address = holder.address
+ return `/images/10-year-anniversary/torchbearers/${address}.jpg`
}
// Otherwise, fall back to blockie
@@ -154,7 +152,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 +196,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"
+}