From 762b3621c87203a272969da14997c4e01c2e48a8 Mon Sep 17 00:00:00 2001 From: hellno Date: Fri, 25 Jul 2025 16:22:20 +0200 Subject: [PATCH 1/4] wip --- CLAUDE.md | 2 +- README.md | 2 +- components/nft-card-examples.tsx | 9 + components/nft-mint-examples.tsx | 15 +- public/r/daimo-pay-transfer-button.json | 2 +- public/r/manifold-utils.json | 12 +- public/r/nft-card.json | 10 +- public/r/nft-metadata-utils.json | 26 ++ public/r/nft-mint-flow.json | 18 +- public/r/nft-standards.json | 2 +- public/r/registry.json | 22 +- registry.json | 22 +- .../components/wagmi-provider.tsx | 5 +- .../blocks/manifold-nft-mint/config.ts | 2 +- .../mini-app/blocks/nft-card/nft-card.tsx | 8 +- .../nft-mint-flow/lib/price-optimizer.ts | 131 +++++++ .../nft-mint-flow/lib/provider-configs.ts | 83 +++- .../nft-mint-flow/lib/provider-detector.ts | 44 ++- .../nft-mint-flow/lib/test-contracts.ts | 36 +- .../blocks/nft-mint-flow/lib/types.ts | 11 +- .../blocks/nft-mint-flow/nft-mint-button.tsx | 6 +- registry/mini-app/lib/manifold-utils.ts | 183 +-------- registry/mini-app/lib/nft-metadata-utils.ts | 359 ++++++++++++++++++ registry/mini-app/lib/nft-standards.ts | 83 ++++ 24 files changed, 862 insertions(+), 231 deletions(-) create mode 100644 public/r/nft-metadata-utils.json create mode 100644 registry/mini-app/lib/nft-metadata-utils.ts diff --git a/CLAUDE.md b/CLAUDE.md index 0571f3c0..61eb8a04 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,7 +64,7 @@ export function NFTCard(props: NFTCardProps) { Always use these shared libraries instead of creating duplicates: - **Chains**: `/registry/mini-app/lib/chains.ts` - RPC configuration, chain detection - **NFT Standards**: `/registry/mini-app/lib/nft-standards.ts` - ABIs and constants -- **Manifold Utils**: `/registry/mini-app/lib/manifold-utils.ts` - Manifold-specific logic +- **NFT Metadata Utils**: `/registry/mini-app/lib/nft-metadata-utils.ts` - NFT metadata fetching with comprehensive fallbacks ### Component Organization Pattern diff --git a/README.md b/README.md index 52f6d51c..fe3ccad3 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ pnpm dlx shadcn@latest add http://localhost:3000/r/nft-card.json --yes - Create shared libraries for common patterns - `/lib/chains.ts` - Chain configurations - `/lib/nft-standards.ts` - ABIs and utilities - - `/lib/manifold-utils.ts` - Contract-specific logic + - `/lib/nft-metadata-utils.ts` - NFT metadata fetching with comprehensive fallbacks ### Testing with Real Contracts diff --git a/components/nft-card-examples.tsx b/components/nft-card-examples.tsx index bf37eb45..5a9802bb 100644 --- a/components/nft-card-examples.tsx +++ b/components/nft-card-examples.tsx @@ -21,6 +21,15 @@ interface NFTExample { } const nftExamples: NFTExample[] = [ + { + title: "Thirdweb OpenEdition", + description: "Thirdweb OpenEditionERC721", + contractAddress: "0xD2Ede6B7b1B08B2A8bB36118fBC0F76409719070", + tokenId: "1", + network: "celo", + titlePosition: "outside", + networkPosition: "outside", + }, { title: "Zora NFT", description: "NFT on Zora Network", diff --git a/components/nft-mint-examples.tsx b/components/nft-mint-examples.tsx index 38ff808e..449f9948 100644 --- a/components/nft-mint-examples.tsx +++ b/components/nft-mint-examples.tsx @@ -10,9 +10,19 @@ interface NFTMintExample { instanceId: string; tokenId: string; buttonText?: string; + chainId?: number; } const nftExamples: NFTMintExample[] = [ + { + title: "Thirdweb OpenEdition", + description: "Thirdweb OpenEditionERC721 contract on Celo", + contractAddress: "0xD2Ede6B7b1B08B2A8bB36118fBC0F76409719070", + instanceId: "", + tokenId: "", + buttonText: "Mint Thirdweb NFT", + chainId: 42220, // Celo + }, { title: "Test NFT - No Image", description: "Testing NFT processing for contract without metadata", @@ -101,11 +111,14 @@ export function NFTMintExamples({ {example.instanceId && (
Instance: {example.instanceId}
)} + {example.chainId && example.chainId !== 8453 && ( +
Chain: {example.chainId === 42220 ? "Celo" : `ID ${example.chainId}`}
+ )} \n \n {children}\n \n \n );\n}\n", + "content": "\"use client\";\n\nimport { createConfig, http, injected, WagmiProvider } from \"wagmi\";\nimport { base, degen, mainnet, optimism, celo } from \"wagmi/chains\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { farcasterFrame } from \"@farcaster/miniapp-wagmi-connector\";\nimport { DaimoPayProvider, getDefaultConfig } from \"@daimo/pay\";\n\nconst alchemyApiKey = process.env.NEXT_PUBLIC_ALCHEMY_KEY;\n\nexport const config = createConfig(\n getDefaultConfig({\n appName: \"hi\",\n chains: [base, degen, mainnet, optimism, celo],\n additionalConnectors: [farcasterFrame(), injected()],\n transports: {\n [base.id]: http(\n alchemyApiKey\n ? `https://base-mainnet.g.alchemy.com/v2/${alchemyApiKey}`\n : undefined,\n ),\n [celo.id]: http(), // Use public RPC for Celo\n },\n }),\n);\n\nconst queryClient = new QueryClient();\n\nexport default function OnchainProvider({\n children,\n}: {\n children: React.ReactNode;\n}) {\n return (\n \n \n {children}\n \n \n );\n}\n", "type": "registry:component" }, { diff --git a/public/r/manifold-utils.json b/public/r/manifold-utils.json index fc879e6c..3859dae4 100644 --- a/public/r/manifold-utils.json +++ b/public/r/manifold-utils.json @@ -3,24 +3,18 @@ "name": "manifold-utils", "type": "registry:lib", "title": "manifoldUtils", - "description": "Manifold contract detection and token URI utilities", + "description": "DEPRECATED: Use nft-metadata-utils instead. Kept for backward compatibility.", "dependencies": [ "viem" ], "registryDependencies": [ - "https://hellno-mini-app-ui.vercel.app/r/nft-standards.json" + "https://hellno-mini-app-ui.vercel.app/r/nft-metadata-utils.json" ], "files": [ { "path": "registry/mini-app/lib/manifold-utils.ts", - "content": "import { type Address, type PublicClient, getAddress } from \"viem\";\nimport { MANIFOLD_DETECTION_ABI, MANIFOLD_EXTENSION_ABI, KNOWN_CONTRACTS, ERC721_ABI } from \"@/registry/mini-app/lib/nft-standards\";\n\n/**\n * Manifold contract utilities\n */\n\nexport interface ManifoldDetectionResult {\n isManifold: boolean;\n extensionAddress?: Address;\n extensions?: Address[];\n}\n\nexport interface ManifoldClaim {\n total: number;\n totalMax: number;\n walletMax: number;\n startDate: bigint;\n endDate: bigint;\n storageProtocol: number;\n merkleRoot: `0x${string}`;\n location: string;\n tokenId: bigint;\n cost: bigint;\n paymentReceiver: Address;\n erc20: Address;\n signingAddress: Address;\n}\n\n/**\n * Detect if a contract is a Manifold contract with extensions\n */\nexport async function detectManifoldContract(\n client: PublicClient,\n contractAddress: Address\n): Promise {\n try {\n const extensions = await client.readContract({\n address: getAddress(contractAddress),\n abi: MANIFOLD_DETECTION_ABI,\n functionName: \"getExtensions\",\n }) as Address[];\n \n if (!extensions || extensions.length === 0) {\n return { isManifold: false };\n }\n \n // Check if it has the known Manifold extension\n const knownExtension = extensions.find(\n ext => ext.toLowerCase() === KNOWN_CONTRACTS.manifoldExtension.toLowerCase()\n );\n \n return {\n isManifold: true,\n extensionAddress: knownExtension || extensions[0],\n extensions,\n };\n } catch {\n return { isManifold: false };\n }\n}\n\n/**\n * Get token URI for a Manifold NFT\n */\nexport async function getManifoldTokenURI(\n client: PublicClient,\n contractAddress: Address,\n tokenId: string,\n extensionAddress?: Address\n): Promise {\n const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension;\n \n return await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"tokenURI\",\n args: [getAddress(contractAddress), BigInt(tokenId)],\n }) as string;\n}\n\n/**\n * Get token URI with automatic Manifold detection\n */\nexport async function getTokenURIWithManifoldSupport(\n client: PublicClient,\n contractAddress: Address,\n tokenId: string\n): Promise {\n // Try Manifold first\n const manifoldInfo = await detectManifoldContract(client, contractAddress);\n \n if (manifoldInfo.isManifold && manifoldInfo.extensionAddress) {\n try {\n return await getManifoldTokenURI(\n client,\n contractAddress,\n tokenId,\n manifoldInfo.extensionAddress\n );\n } catch (error) {\n console.warn(\"Failed to get Manifold tokenURI, falling back to standard\", error);\n }\n }\n \n // Fallback to standard ERC721 tokenURI\n return await client.readContract({\n address: getAddress(contractAddress),\n abi: ERC721_ABI.tokenURI,\n functionName: \"tokenURI\",\n args: [BigInt(tokenId)],\n }) as string;\n}\n\n/**\n * Get Manifold claim information\n */\nexport async function getManifoldClaim(\n client: PublicClient,\n contractAddress: Address,\n instanceId: string,\n extensionAddress?: Address\n): Promise {\n try {\n const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension;\n \n const claim = await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"getClaim\",\n args: [getAddress(contractAddress), BigInt(instanceId)],\n });\n \n return claim as unknown as ManifoldClaim;\n } catch {\n return null;\n }\n}\n\n/**\n * Get Manifold mint fee\n */\nexport async function getManifoldMintFee(\n client: PublicClient,\n extensionAddress?: Address\n): Promise {\n const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension;\n \n try {\n return await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"MINT_FEE\",\n }) as bigint;\n } catch {\n // Try MINT_FEE_MERKLE as fallback\n try {\n return await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"MINT_FEE_MERKLE\",\n }) as bigint;\n } catch {\n return BigInt(0);\n }\n }\n}\n\n/**\n * Check if an address is the zero address\n */\nexport function isZeroAddress(address: string): boolean {\n return address === \"0x0000000000000000000000000000000000000000\";\n}\n\n/**\n * Format instance ID and token ID for display\n */\nexport function formatManifoldTokenId(instanceId: string, tokenId: string): string {\n return `${instanceId}-${tokenId}`;\n}", + "content": "/**\n * @deprecated This file is kept for backward compatibility. \n * Please use nft-metadata-utils.ts instead which provides the same functionality plus enhanced metadata support.\n */\nexport * from \"@/registry/mini-app/lib/nft-metadata-utils\";\n\n// Re-export the deprecated function name for backward compatibility\nexport { getTokenMetadataURL as getTokenURIWithManifoldSupport } from \"@/registry/mini-app/lib/nft-metadata-utils\";", "type": "registry:lib" - }, - { - "path": "registry/mini-app/lib/nft-standards.ts", - "content": "import { parseAbi, type Address } from \"viem\";\n\n/**\n * Common NFT contract ABIs and standards\n */\n\n// Standard ERC721 functions\nexport const ERC721_ABI = {\n full: parseAbi([\n \"function tokenURI(uint256 tokenId) view returns (string)\",\n \"function name() view returns (string)\",\n \"function symbol() view returns (string)\",\n \"function ownerOf(uint256 tokenId) view returns (address)\",\n \"function balanceOf(address owner) view returns (uint256)\",\n \"function totalSupply() view returns (uint256)\",\n ]),\n \n // Individual functions for specific use cases\n tokenURI: parseAbi([\"function tokenURI(uint256 tokenId) view returns (string)\"]),\n name: parseAbi([\"function name() view returns (string)\"]),\n symbol: parseAbi([\"function symbol() view returns (string)\"]),\n ownerOf: parseAbi([\"function ownerOf(uint256 tokenId) view returns (address)\"]),\n};\n\n// Common price discovery functions across NFT contracts\nexport const PRICE_DISCOVERY_ABI = parseAbi([\n \"function mintPrice() view returns (uint256)\",\n \"function price() view returns (uint256)\",\n \"function MINT_PRICE() view returns (uint256)\",\n \"function getMintPrice() view returns (uint256)\",\n \"function publicMintPrice() view returns (uint256)\",\n]);\n\n// Common mint functions\nexport const MINT_ABI = parseAbi([\n \"function mint(uint256 amount) payable\",\n \"function mint(address to, uint256 amount) payable\",\n \"function publicMint(uint256 amount) payable\",\n \"function mintTo(address to, uint256 amount) payable\",\n]);\n\n// ERC20 ABI for token interactions\nexport const ERC20_ABI = parseAbi([\n \"function decimals() view returns (uint8)\",\n \"function symbol() view returns (string)\",\n \"function name() view returns (string)\",\n \"function totalSupply() view returns (uint256)\",\n \"function balanceOf(address owner) view returns (uint256)\",\n \"function allowance(address owner, address spender) view returns (uint256)\",\n \"function approve(address spender, uint256 value) returns (bool)\",\n \"function transfer(address to, uint256 value) returns (bool)\",\n \"function transferFrom(address from, address to, uint256 value) returns (bool)\",\n]);\n\n// Manifold contract detection ABI (kept separate as it's used on the main contract)\nexport const MANIFOLD_DETECTION_ABI = parseAbi([\n \"function getExtensions() view returns (address[])\",\n]);\n\n// Manifold extension contract full ABI\nexport const MANIFOLD_EXTENSION_ABI = [\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"tokenId\", type: \"uint256\" }\n ],\n name: \"tokenURI\",\n outputs: [{ name: \"uri\", type: \"string\" }],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"instanceId\", type: \"uint256\" }\n ],\n name: \"getClaim\",\n outputs: [\n {\n components: [\n { name: \"total\", type: \"uint32\" },\n { name: \"totalMax\", type: \"uint32\" },\n { name: \"walletMax\", type: \"uint32\" },\n { name: \"startDate\", type: \"uint48\" },\n { name: \"endDate\", type: \"uint48\" },\n { name: \"storageProtocol\", type: \"uint8\" },\n { name: \"merkleRoot\", type: \"bytes32\" },\n { name: \"location\", type: \"string\" },\n { name: \"tokenId\", type: \"uint256\" },\n { name: \"cost\", type: \"uint256\" },\n { name: \"paymentReceiver\", type: \"address\" },\n { name: \"erc20\", type: \"address\" },\n { name: \"signingAddress\", type: \"address\" }\n ],\n name: \"claim\",\n type: \"tuple\"\n }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"tokenId\", type: \"uint256\" }\n ],\n name: \"getClaimForToken\",\n outputs: [\n { name: \"instanceId\", type: \"uint256\" },\n {\n components: [\n { name: \"total\", type: \"uint32\" },\n { name: \"totalMax\", type: \"uint32\" },\n { name: \"walletMax\", type: \"uint32\" },\n { name: \"startDate\", type: \"uint48\" },\n { name: \"endDate\", type: \"uint48\" },\n { name: \"storageProtocol\", type: \"uint8\" },\n { name: \"merkleRoot\", type: \"bytes32\" },\n { name: \"location\", type: \"string\" },\n { name: \"tokenId\", type: \"uint256\" },\n { name: \"cost\", type: \"uint256\" },\n { name: \"paymentReceiver\", type: \"address\" },\n { name: \"erc20\", type: \"address\" },\n { name: \"signingAddress\", type: \"address\" }\n ],\n name: \"claim\",\n type: \"tuple\"\n }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"instanceId\", type: \"uint256\" },\n { name: \"mintIndex\", type: \"uint32\" },\n { name: \"merkleProof\", type: \"bytes32[]\" },\n { name: \"mintFor\", type: \"address\" }\n ],\n name: \"mint\",\n outputs: [],\n stateMutability: \"payable\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"MINT_FEE\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"MINT_FEE_MERKLE\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }\n] as const;\n\n// ERC165 interface detection\nexport const ERC165_ABI = parseAbi([\n \"function supportsInterface(bytes4 interfaceId) view returns (bool)\",\n]);\n\n// Known contract addresses\nexport const KNOWN_CONTRACTS = {\n // Manifold extension contracts\n manifoldExtension: \"0x26BBEA7803DcAc346D5F5f135b57Cf2c752A02bE\" as Address,\n \n // Add other known contracts here as needed\n} as const;\n\n// Interface IDs for contract detection\nexport const INTERFACE_IDS = {\n ERC165: \"0x01ffc9a7\",\n ERC721: \"0x80ac58cd\",\n ERC1155: \"0xd9b67a26\",\n ERC721Metadata: \"0x5b5e139f\",\n} as const;\n\n// IPFS Gateway configuration\nexport const IPFS_GATEWAYS = {\n default: \"https://ipfs.io/ipfs/\",\n cloudflare: \"https://cloudflare-ipfs.com/ipfs/\",\n pinata: \"https://gateway.pinata.cloud/ipfs/\",\n} as const;\n\n/**\n * Convert IPFS URL to HTTP gateway URL\n */\nexport function ipfsToHttp(url: string, gateway?: keyof typeof IPFS_GATEWAYS): string {\n const selectedGateway = gateway || \"default\";\n if (!url || !url.startsWith(\"ipfs://\")) {\n return url;\n }\n \n return url.replace(\"ipfs://\", IPFS_GATEWAYS[selectedGateway]);\n}\n\n/**\n * Check if a contract is likely an NFT contract by checking interface support\n */\nexport async function isNFTContract(\n client: any,\n contractAddress: Address\n): Promise<{ isNFT: boolean; type?: 'ERC721' | 'ERC1155' }> {\n try {\n // Try ERC165 supportsInterface\n const [supportsERC721, supportsERC1155] = await Promise.all([\n client.readContract({\n address: contractAddress,\n abi: ERC165_ABI,\n functionName: 'supportsInterface',\n args: [INTERFACE_IDS.ERC721],\n }).catch(() => false),\n client.readContract({\n address: contractAddress,\n abi: ERC165_ABI,\n functionName: 'supportsInterface',\n args: [INTERFACE_IDS.ERC1155],\n }).catch(() => false),\n ]);\n \n if (supportsERC721) return { isNFT: true, type: 'ERC721' };\n if (supportsERC1155) return { isNFT: true, type: 'ERC1155' };\n \n // Fallback: try to call name() function\n const name = await client.readContract({\n address: contractAddress,\n abi: ERC721_ABI.name,\n functionName: 'name',\n }).catch(() => null);\n \n return { isNFT: !!name };\n } catch {\n return { isNFT: false };\n }\n}", - "type": "registry:lib", - "target": "" } ] } \ No newline at end of file diff --git a/public/r/nft-card.json b/public/r/nft-card.json index 1483228d..99d50887 100644 --- a/public/r/nft-card.json +++ b/public/r/nft-card.json @@ -13,12 +13,12 @@ "https://hellno-mini-app-ui.vercel.app/r/utils.json", "https://hellno-mini-app-ui.vercel.app/r/chains.json", "https://hellno-mini-app-ui.vercel.app/r/nft-standards.json", - "https://hellno-mini-app-ui.vercel.app/r/manifold-utils.json" + "https://hellno-mini-app-ui.vercel.app/r/nft-metadata-utils.json" ], "files": [ { "path": "registry/mini-app/blocks/nft-card/nft-card.tsx", - "content": "\"use client\";\n\nimport { cn } from \"@/registry/mini-app/lib/utils\";\nimport Image from \"next/image\";\nimport { useState, useEffect, useRef } from \"react\";\n\ninterface NFTMetadata {\n name?: string;\n description?: string;\n image?: string;\n image_url?: string;\n external_url?: string;\n attributes?: Array<{\n trait_type: string;\n value: string | number;\n display_type?: string;\n }>;\n image_details?: {\n bytes?: number;\n format?: string;\n sha256?: string;\n width?: number;\n height?: number;\n };\n [key: string]: unknown;\n}\nimport { getAddress, type Address } from \"viem\";\nimport { \n findChainByName, \n getPublicClient \n} from \"@/registry/mini-app/lib/chains\";\nimport { \n ERC721_ABI, \n ipfsToHttp \n} from \"@/registry/mini-app/lib/nft-standards\";\nimport { \n getTokenURIWithManifoldSupport \n} from \"@/registry/mini-app/lib/manifold-utils\";\n\n// Base64 placeholder image\nconst PLACEHOLDER_IMAGE =\n \"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjMwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMzAwIiBoZWlnaHQ9IjMwMCIgZmlsbD0iI2YxZjFmMSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMjQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiIGZpbGw9IiM5OTkiPk5GVCBJbWFnZTwvdGV4dD48L3N2Zz4=\";\n\n\ntype NFTCardProps = {\n contractAddress: string;\n tokenId: string;\n network?: string;\n alt?: string;\n className?: string;\n width?: number | string;\n height?: number | string;\n rounded?: \"none\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n shadow?: boolean;\n objectFit?: \"contain\" | \"cover\" | \"fill\";\n fallbackImageUrl?: string;\n showTitle?: boolean;\n showNetwork?: boolean;\n titlePosition?: \"top\" | \"bottom\" | \"outside\";\n networkPosition?:\n | \"top-left\"\n | \"top-right\"\n | \"bottom-left\"\n | \"bottom-right\"\n | \"outside\";\n customTitle?: string;\n customNetworkName?: string;\n loadingComponent?: React.ReactNode;\n errorComponent?: React.ReactNode;\n imageProps?: React.ComponentProps;\n titleClassName?: string;\n networkClassName?: string;\n showOwner?: boolean;\n onLoad?: (metadata: NFTMetadata) => void;\n onError?: (error: Error) => void;\n layout?: \"compact\" | \"card\" | \"detailed\";\n containerClassName?: string;\n};\n\nexport function NFTCard({\n contractAddress,\n tokenId,\n network = \"ethereum\", // Default to Ethereum mainnet\n alt = \"NFT Image\",\n className = \"\",\n width = 300,\n height = 300,\n rounded = \"md\",\n shadow = true,\n objectFit = \"cover\",\n fallbackImageUrl = PLACEHOLDER_IMAGE,\n showTitle = true,\n showNetwork = true,\n titlePosition = \"outside\",\n networkPosition = \"top-right\",\n customTitle,\n customNetworkName,\n loadingComponent,\n errorComponent,\n imageProps,\n titleClassName = \"\",\n networkClassName = \"\",\n showOwner = false,\n onLoad,\n onError,\n layout = \"card\",\n containerClassName = \"\",\n}: NFTCardProps) {\n const [imageUrl, setImageUrl] = useState(fallbackImageUrl);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState(null);\n const [title, setTitle] = useState(customTitle || null);\n const [networkName, setNetworkName] = useState(\n customNetworkName || \"\",\n );\n const [owner, setOwner] = useState(null);\n const [metadata, setMetadata] = useState(null);\n const abortControllerRef = useRef(null);\n\n const roundedClasses = {\n none: \"rounded-none\",\n sm: \"rounded-sm\",\n md: \"rounded-md\",\n lg: \"rounded-lg\",\n xl: \"rounded-xl\",\n full: \"rounded-full\",\n };\n\n const networkPositionClasses = {\n \"top-left\": \"top-0 left-0 rounded-br-md\",\n \"top-right\": \"top-0 right-0 rounded-bl-md\",\n \"bottom-left\": \"bottom-0 left-0 rounded-tr-md\",\n \"bottom-right\": \"bottom-0 right-0 rounded-tl-md\",\n outside: \"\",\n };\n\n useEffect(() => {\n if (customTitle) {\n setTitle(customTitle);\n }\n\n if (customNetworkName) {\n setNetworkName(customNetworkName);\n }\n }, [customTitle, customNetworkName]);\n\n useEffect(() => {\n const fetchNFTData = async () => {\n if (!contractAddress || !tokenId) return;\n \n // Cancel any previous request\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n }\n \n // Create new AbortController for this request\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n setIsLoading(true);\n setError(null);\n\n try {\n // Skip chain setup if we have customNetworkName\n if (!customNetworkName) {\n // Find the chain by name using shared utility\n const selectedChain = findChainByName(network || \"ethereum\");\n \n if (!selectedChain) {\n console.warn(\n `Chain \"${network}\" not found, defaulting to Ethereum mainnet`,\n );\n setNetworkName(\"Ethereum\");\n } else {\n setNetworkName(selectedChain.name);\n }\n\n // Create public client using shared utility\n const client = getPublicClient(selectedChain?.id || 1);\n\n console.log(\n `Fetching NFT data from ${selectedChain?.name || 'Ethereum'} for contract ${contractAddress} token ${tokenId}`,\n );\n\n // Skip title setup if we have customTitle\n if (!customTitle) {\n try {\n // Get contract name\n const name = (await client.readContract({\n address: getAddress(contractAddress),\n abi: ERC721_ABI.name,\n functionName: \"name\",\n })) as string;\n\n // Set title\n setTitle(`${name} #${tokenId}`);\n } catch (nameError) {\n console.warn(\"Could not fetch NFT name:\", nameError);\n setTitle(`NFT #${tokenId}`);\n }\n }\n\n // Get owner if requested\n if (showOwner) {\n try {\n const ownerAddress = (await client.readContract({\n address: getAddress(contractAddress),\n abi: ERC721_ABI.ownerOf,\n functionName: \"ownerOf\",\n args: [BigInt(tokenId)],\n })) as string;\n\n setOwner(ownerAddress);\n } catch (ownerError) {\n console.warn(\"Could not fetch NFT owner:\", ownerError);\n }\n }\n\n // Get tokenURI with automatic Manifold support\n let metadataUrl = await getTokenURIWithManifoldSupport(\n client,\n getAddress(contractAddress) as Address,\n tokenId\n );\n\n // Handle IPFS URLs using shared utility\n metadataUrl = ipfsToHttp(metadataUrl);\n\n // Fetch metadata with abort signal\n const response = await fetch(metadataUrl, {\n signal: abortController.signal\n });\n \n if (!response.ok) {\n throw new Error(`Failed to fetch metadata: ${response.status}`);\n }\n \n const fetchedMetadata = await response.json();\n console.log(\"NFT metadata:\", fetchedMetadata);\n \n // Store metadata in state\n setMetadata(fetchedMetadata);\n\n // Call onLoad callback if provided\n if (onLoad) {\n onLoad(fetchedMetadata);\n }\n\n // Get image URL from metadata\n let nftImageUrl = fetchedMetadata.image || fetchedMetadata.image_url;\n\n // Handle IPFS URLs for image using shared utility\n if (nftImageUrl) {\n nftImageUrl = ipfsToHttp(nftImageUrl);\n }\n\n if (nftImageUrl) {\n setImageUrl(nftImageUrl);\n } else {\n // If no image URL found, use placeholder\n setImageUrl(fallbackImageUrl);\n }\n }\n } catch (err) {\n // Don't update state if request was aborted\n if (err instanceof Error && err.name === 'AbortError') {\n console.log('NFT data fetch was cancelled');\n return;\n }\n \n console.error(\"Error fetching NFT:\", err);\n const error = err instanceof Error ? err : new Error(String(err));\n setError(`Failed to load NFT data: ${error.message}`);\n setImageUrl(fallbackImageUrl);\n\n // Call onError callback if provided\n if (onError) {\n onError(error);\n }\n } finally {\n // Only update loading state if this request wasn't aborted\n if (!abortController.signal.aborted) {\n setIsLoading(false);\n }\n }\n };\n\n fetchNFTData();\n \n // Cleanup function to abort request if component unmounts or deps change\n return () => {\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n }\n };\n }, [\n contractAddress,\n tokenId,\n network,\n fallbackImageUrl,\n customTitle,\n customNetworkName,\n showOwner,\n onLoad,\n onError,\n ]);\n\n const defaultLoadingComponent = (\n
\n
\n
\n );\n\n const defaultErrorComponent = (\n
\n

{error}

\n
\n );\n\n // Render network badge inside the image\n const renderNetworkBadge = () => {\n if (!showNetwork || !networkName || networkPosition === \"outside\")\n return null;\n\n return (\n \n {networkName}\n \n );\n };\n\n // Render title inside the image\n const renderInnerTitle = () => {\n if (!showTitle || !title || titlePosition === \"outside\") return null;\n\n return (\n \n {title}\n {showOwner && owner && (\n
\n Owner: {owner.substring(0, 6)}...{owner.substring(owner.length - 4)}\n
\n )}\n \n );\n };\n\n // Render outside information (title, network, owner)\n const renderOutsideInfo = () => {\n if (\n (!showTitle || !title) &&\n (!showNetwork || !networkName || networkPosition !== \"outside\") &&\n (!showOwner || !owner || titlePosition !== \"outside\")\n ) {\n return null;\n }\n\n return (\n
\n {showTitle && title && titlePosition === \"outside\" && (\n
\n {title}\n
\n )}\n\n {showNetwork && networkName && networkPosition === \"outside\" && (\n \n Network: {networkName}\n
\n )}\n\n {showOwner && owner && titlePosition === \"outside\" && (\n
\n Owner: {owner.substring(0, 6)}...{owner.substring(owner.length - 4)}\n
\n )}\n \n );\n };\n\n // Apply different layouts\n const getContainerClasses = () => {\n switch (layout) {\n case \"compact\":\n return \"inline-block\";\n case \"detailed\":\n return \"flex flex-col overflow-hidden\";\n case \"card\":\n default:\n return \"\";\n }\n };\n\n // Calculate display dimensions that preserve aspect ratio\n const getDisplayDimensions = () => {\n // Handle percentage values\n const isPercentageWidth = typeof width === \"string\" && width.includes(\"%\");\n const isPercentageHeight = typeof height === \"string\" && height.includes(\"%\");\n \n if (isPercentageWidth || isPercentageHeight) {\n return { \n width: width || '100%', \n height: height || '100%', \n useContain: false,\n isPercentage: true\n };\n }\n \n const maxWidth = typeof width === \"number\" ? width : 300;\n const maxHeight = typeof height === \"number\" ? height : 300;\n \n // Check if we have image_details with dimensions\n if (metadata?.image_details?.width && metadata?.image_details?.height) {\n const originalAspectRatio = metadata.image_details.width / metadata.image_details.height;\n \n // Scale to fit within bounds while preserving aspect ratio\n const widthBasedHeight = maxWidth / originalAspectRatio;\n const heightBasedWidth = maxHeight * originalAspectRatio;\n \n if (widthBasedHeight <= maxHeight) {\n // Width is the limiting factor\n return { \n width: maxWidth, \n height: Math.round(widthBasedHeight),\n useContain: true, // Use contain to show full image\n isPercentage: false\n };\n } else {\n // Height is the limiting factor\n return { \n width: Math.round(heightBasedWidth), \n height: maxHeight,\n useContain: true,\n isPercentage: false\n };\n }\n }\n \n // No image_details, use provided dimensions\n return { width: maxWidth, height: maxHeight, useContain: false, isPercentage: false };\n };\n\n const displayDimensions = getDisplayDimensions();\n\n return (\n
\n \n {isLoading && (loadingComponent || defaultLoadingComponent)}\n\n {error && (errorComponent || defaultErrorComponent)}\n\n setImageUrl(PLACEHOLDER_IMAGE)}\n {...imageProps}\n />\n\n {renderInnerTitle()}\n {renderNetworkBadge()}\n
\n\n {renderOutsideInfo()}\n \n );\n}\n", + "content": "\"use client\";\n\nimport { cn } from \"@/registry/mini-app/lib/utils\";\nimport Image from \"next/image\";\nimport { useState, useEffect, useRef } from \"react\";\n\ninterface NFTMetadata {\n name?: string;\n description?: string;\n image?: string;\n image_url?: string;\n external_url?: string;\n attributes?: Array<{\n trait_type: string;\n value: string | number;\n display_type?: string;\n }>;\n image_details?: {\n bytes?: number;\n format?: string;\n sha256?: string;\n width?: number;\n height?: number;\n };\n [key: string]: unknown;\n}\nimport { getAddress, type Address } from \"viem\";\nimport { \n findChainByName, \n getPublicClient \n} from \"@/registry/mini-app/lib/chains\";\nimport { \n ERC721_ABI, \n ipfsToHttp \n} from \"@/registry/mini-app/lib/nft-standards\";\nimport { \n getTokenMetadataURL \n} from \"@/registry/mini-app/lib/nft-metadata-utils\";\n\n// Base64 placeholder image\nconst PLACEHOLDER_IMAGE =\n \"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjMwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMzAwIiBoZWlnaHQ9IjMwMCIgZmlsbD0iI2YxZjFmMSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMjQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiIGZpbGw9IiM5OTkiPk5GVCBJbWFnZTwvdGV4dD48L3N2Zz4=\";\n\n\ntype NFTCardProps = {\n contractAddress: string;\n tokenId: string;\n network?: string;\n alt?: string;\n className?: string;\n width?: number | string;\n height?: number | string;\n rounded?: \"none\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n shadow?: boolean;\n objectFit?: \"contain\" | \"cover\" | \"fill\";\n fallbackImageUrl?: string;\n showTitle?: boolean;\n showNetwork?: boolean;\n titlePosition?: \"top\" | \"bottom\" | \"outside\";\n networkPosition?:\n | \"top-left\"\n | \"top-right\"\n | \"bottom-left\"\n | \"bottom-right\"\n | \"outside\";\n customTitle?: string;\n customNetworkName?: string;\n loadingComponent?: React.ReactNode;\n errorComponent?: React.ReactNode;\n imageProps?: React.ComponentProps;\n titleClassName?: string;\n networkClassName?: string;\n showOwner?: boolean;\n onLoad?: (metadata: NFTMetadata) => void;\n onError?: (error: Error) => void;\n layout?: \"compact\" | \"card\" | \"detailed\";\n containerClassName?: string;\n};\n\nexport function NFTCard({\n contractAddress,\n tokenId,\n network = \"ethereum\", // Default to Ethereum mainnet\n alt = \"NFT Image\",\n className = \"\",\n width = 300,\n height = 300,\n rounded = \"md\",\n shadow = true,\n objectFit = \"cover\",\n fallbackImageUrl = PLACEHOLDER_IMAGE,\n showTitle = true,\n showNetwork = true,\n titlePosition = \"outside\",\n networkPosition = \"top-right\",\n customTitle,\n customNetworkName,\n loadingComponent,\n errorComponent,\n imageProps,\n titleClassName = \"\",\n networkClassName = \"\",\n showOwner = false,\n onLoad,\n onError,\n layout = \"card\",\n containerClassName = \"\",\n}: NFTCardProps) {\n const [imageUrl, setImageUrl] = useState(fallbackImageUrl);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState(null);\n const [title, setTitle] = useState(customTitle || null);\n const [networkName, setNetworkName] = useState(\n customNetworkName || \"\",\n );\n const [owner, setOwner] = useState(null);\n const [metadata, setMetadata] = useState(null);\n const abortControllerRef = useRef(null);\n\n const roundedClasses = {\n none: \"rounded-none\",\n sm: \"rounded-sm\",\n md: \"rounded-md\",\n lg: \"rounded-lg\",\n xl: \"rounded-xl\",\n full: \"rounded-full\",\n };\n\n const networkPositionClasses = {\n \"top-left\": \"top-0 left-0 rounded-br-md\",\n \"top-right\": \"top-0 right-0 rounded-bl-md\",\n \"bottom-left\": \"bottom-0 left-0 rounded-tr-md\",\n \"bottom-right\": \"bottom-0 right-0 rounded-tl-md\",\n outside: \"\",\n };\n\n useEffect(() => {\n if (customTitle) {\n setTitle(customTitle);\n }\n\n if (customNetworkName) {\n setNetworkName(customNetworkName);\n }\n }, [customTitle, customNetworkName]);\n\n useEffect(() => {\n const fetchNFTData = async () => {\n if (!contractAddress || !tokenId) return;\n \n // Cancel any previous request\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n }\n \n // Create new AbortController for this request\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n setIsLoading(true);\n setError(null);\n\n try {\n // Skip chain setup if we have customNetworkName\n if (!customNetworkName) {\n // Find the chain by name using shared utility\n const selectedChain = findChainByName(network || \"ethereum\");\n \n if (!selectedChain) {\n console.warn(\n `Chain \"${network}\" not found, defaulting to Ethereum mainnet`,\n );\n setNetworkName(\"Ethereum\");\n } else {\n setNetworkName(selectedChain.name);\n }\n\n // Create public client using shared utility\n const client = getPublicClient(selectedChain?.id || 1);\n\n console.log(\n `Fetching NFT data from ${selectedChain?.name || 'Ethereum'} for contract ${contractAddress} token ${tokenId}`,\n );\n\n // Skip title setup if we have customTitle\n if (!customTitle) {\n try {\n // Get contract name\n const name = (await client.readContract({\n address: getAddress(contractAddress),\n abi: ERC721_ABI.name,\n functionName: \"name\",\n })) as string;\n\n // Set title\n setTitle(`${name} #${tokenId}`);\n } catch (nameError) {\n console.warn(\"Could not fetch NFT name:\", nameError);\n setTitle(`NFT #${tokenId}`);\n }\n }\n\n // Get owner if requested\n if (showOwner) {\n try {\n const ownerAddress = (await client.readContract({\n address: getAddress(contractAddress),\n abi: ERC721_ABI.ownerOf,\n functionName: \"ownerOf\",\n args: [BigInt(tokenId)],\n })) as string;\n\n setOwner(ownerAddress);\n } catch (ownerError) {\n console.warn(\"Could not fetch NFT owner:\", ownerError);\n }\n }\n\n // Get tokenURI with comprehensive metadata support\n let metadataUrl = await getTokenMetadataURL(\n client,\n getAddress(contractAddress) as Address,\n tokenId\n );\n\n // Handle IPFS URLs using shared utility\n metadataUrl = ipfsToHttp(metadataUrl);\n\n // Fetch metadata with abort signal\n const response = await fetch(metadataUrl, {\n signal: abortController.signal\n });\n \n if (!response.ok) {\n throw new Error(`Failed to fetch metadata: ${response.status}`);\n }\n \n const fetchedMetadata = await response.json();\n console.log(\"NFT metadata:\", fetchedMetadata);\n \n // Store metadata in state\n setMetadata(fetchedMetadata);\n\n // Call onLoad callback if provided\n if (onLoad) {\n onLoad(fetchedMetadata);\n }\n\n // Get image URL from metadata\n let nftImageUrl = fetchedMetadata.image || fetchedMetadata.image_url;\n\n // Handle IPFS URLs for image using shared utility\n if (nftImageUrl) {\n nftImageUrl = ipfsToHttp(nftImageUrl);\n }\n\n if (nftImageUrl) {\n setImageUrl(nftImageUrl);\n } else {\n // If no image URL found, use placeholder\n setImageUrl(fallbackImageUrl);\n }\n }\n } catch (err) {\n // Don't update state if request was aborted\n if (err instanceof Error && err.name === 'AbortError') {\n console.log('NFT data fetch was cancelled');\n return;\n }\n \n console.error(\"Error fetching NFT:\", err);\n const error = err instanceof Error ? err : new Error(String(err));\n setError(`Failed to load NFT data: ${error.message}`);\n setImageUrl(fallbackImageUrl);\n\n // Call onError callback if provided\n if (onError) {\n onError(error);\n }\n } finally {\n // Only update loading state if this request wasn't aborted\n if (!abortController.signal.aborted) {\n setIsLoading(false);\n }\n }\n };\n\n fetchNFTData();\n \n // Cleanup function to abort request if component unmounts or deps change\n return () => {\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n }\n };\n }, [\n contractAddress,\n tokenId,\n network,\n fallbackImageUrl,\n customTitle,\n customNetworkName,\n showOwner,\n onLoad,\n onError,\n ]);\n\n const defaultLoadingComponent = (\n
\n
\n
\n );\n\n const defaultErrorComponent = (\n
\n

{error}

\n
\n );\n\n // Render network badge inside the image\n const renderNetworkBadge = () => {\n if (!showNetwork || !networkName || networkPosition === \"outside\")\n return null;\n\n return (\n \n {networkName}\n \n );\n };\n\n // Render title inside the image\n const renderInnerTitle = () => {\n if (!showTitle || !title || titlePosition === \"outside\") return null;\n\n return (\n \n {title}\n {showOwner && owner && (\n
\n Owner: {owner.substring(0, 6)}...{owner.substring(owner.length - 4)}\n
\n )}\n \n );\n };\n\n // Render outside information (title, network, owner)\n const renderOutsideInfo = () => {\n if (\n (!showTitle || !title) &&\n (!showNetwork || !networkName || networkPosition !== \"outside\") &&\n (!showOwner || !owner || titlePosition !== \"outside\")\n ) {\n return null;\n }\n\n return (\n
\n {showTitle && title && titlePosition === \"outside\" && (\n
\n {title}\n
\n )}\n\n {showNetwork && networkName && networkPosition === \"outside\" && (\n \n Network: {networkName}\n
\n )}\n\n {showOwner && owner && titlePosition === \"outside\" && (\n
\n Owner: {owner.substring(0, 6)}...{owner.substring(owner.length - 4)}\n
\n )}\n \n );\n };\n\n // Apply different layouts\n const getContainerClasses = () => {\n switch (layout) {\n case \"compact\":\n return \"inline-block\";\n case \"detailed\":\n return \"flex flex-col overflow-hidden\";\n case \"card\":\n default:\n return \"\";\n }\n };\n\n // Calculate display dimensions that preserve aspect ratio\n const getDisplayDimensions = () => {\n // Handle percentage values\n const isPercentageWidth = typeof width === \"string\" && width.includes(\"%\");\n const isPercentageHeight = typeof height === \"string\" && height.includes(\"%\");\n \n if (isPercentageWidth || isPercentageHeight) {\n return { \n width: width || '100%', \n height: height || '100%', \n useContain: false,\n isPercentage: true\n };\n }\n \n const maxWidth = typeof width === \"number\" ? width : 300;\n const maxHeight = typeof height === \"number\" ? height : 300;\n \n // Check if we have image_details with dimensions\n if (metadata?.image_details?.width && metadata?.image_details?.height) {\n const originalAspectRatio = metadata.image_details.width / metadata.image_details.height;\n \n // Scale to fit within bounds while preserving aspect ratio\n const widthBasedHeight = maxWidth / originalAspectRatio;\n const heightBasedWidth = maxHeight * originalAspectRatio;\n \n if (widthBasedHeight <= maxHeight) {\n // Width is the limiting factor\n return { \n width: maxWidth, \n height: Math.round(widthBasedHeight),\n useContain: true, // Use contain to show full image\n isPercentage: false\n };\n } else {\n // Height is the limiting factor\n return { \n width: Math.round(heightBasedWidth), \n height: maxHeight,\n useContain: true,\n isPercentage: false\n };\n }\n }\n \n // No image_details, use provided dimensions\n return { width: maxWidth, height: maxHeight, useContain: false, isPercentage: false };\n };\n\n const displayDimensions = getDisplayDimensions();\n\n return (\n
\n \n {isLoading && (loadingComponent || defaultLoadingComponent)}\n\n {error && (errorComponent || defaultErrorComponent)}\n\n setImageUrl(PLACEHOLDER_IMAGE)}\n {...imageProps}\n />\n\n {renderInnerTitle()}\n {renderNetworkBadge()}\n
\n\n {renderOutsideInfo()}\n \n );\n}\n", "type": "registry:component" }, { @@ -35,13 +35,13 @@ }, { "path": "registry/mini-app/lib/nft-standards.ts", - "content": "import { parseAbi, type Address } from \"viem\";\n\n/**\n * Common NFT contract ABIs and standards\n */\n\n// Standard ERC721 functions\nexport const ERC721_ABI = {\n full: parseAbi([\n \"function tokenURI(uint256 tokenId) view returns (string)\",\n \"function name() view returns (string)\",\n \"function symbol() view returns (string)\",\n \"function ownerOf(uint256 tokenId) view returns (address)\",\n \"function balanceOf(address owner) view returns (uint256)\",\n \"function totalSupply() view returns (uint256)\",\n ]),\n \n // Individual functions for specific use cases\n tokenURI: parseAbi([\"function tokenURI(uint256 tokenId) view returns (string)\"]),\n name: parseAbi([\"function name() view returns (string)\"]),\n symbol: parseAbi([\"function symbol() view returns (string)\"]),\n ownerOf: parseAbi([\"function ownerOf(uint256 tokenId) view returns (address)\"]),\n};\n\n// Common price discovery functions across NFT contracts\nexport const PRICE_DISCOVERY_ABI = parseAbi([\n \"function mintPrice() view returns (uint256)\",\n \"function price() view returns (uint256)\",\n \"function MINT_PRICE() view returns (uint256)\",\n \"function getMintPrice() view returns (uint256)\",\n \"function publicMintPrice() view returns (uint256)\",\n]);\n\n// Common mint functions\nexport const MINT_ABI = parseAbi([\n \"function mint(uint256 amount) payable\",\n \"function mint(address to, uint256 amount) payable\",\n \"function publicMint(uint256 amount) payable\",\n \"function mintTo(address to, uint256 amount) payable\",\n]);\n\n// ERC20 ABI for token interactions\nexport const ERC20_ABI = parseAbi([\n \"function decimals() view returns (uint8)\",\n \"function symbol() view returns (string)\",\n \"function name() view returns (string)\",\n \"function totalSupply() view returns (uint256)\",\n \"function balanceOf(address owner) view returns (uint256)\",\n \"function allowance(address owner, address spender) view returns (uint256)\",\n \"function approve(address spender, uint256 value) returns (bool)\",\n \"function transfer(address to, uint256 value) returns (bool)\",\n \"function transferFrom(address from, address to, uint256 value) returns (bool)\",\n]);\n\n// Manifold contract detection ABI (kept separate as it's used on the main contract)\nexport const MANIFOLD_DETECTION_ABI = parseAbi([\n \"function getExtensions() view returns (address[])\",\n]);\n\n// Manifold extension contract full ABI\nexport const MANIFOLD_EXTENSION_ABI = [\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"tokenId\", type: \"uint256\" }\n ],\n name: \"tokenURI\",\n outputs: [{ name: \"uri\", type: \"string\" }],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"instanceId\", type: \"uint256\" }\n ],\n name: \"getClaim\",\n outputs: [\n {\n components: [\n { name: \"total\", type: \"uint32\" },\n { name: \"totalMax\", type: \"uint32\" },\n { name: \"walletMax\", type: \"uint32\" },\n { name: \"startDate\", type: \"uint48\" },\n { name: \"endDate\", type: \"uint48\" },\n { name: \"storageProtocol\", type: \"uint8\" },\n { name: \"merkleRoot\", type: \"bytes32\" },\n { name: \"location\", type: \"string\" },\n { name: \"tokenId\", type: \"uint256\" },\n { name: \"cost\", type: \"uint256\" },\n { name: \"paymentReceiver\", type: \"address\" },\n { name: \"erc20\", type: \"address\" },\n { name: \"signingAddress\", type: \"address\" }\n ],\n name: \"claim\",\n type: \"tuple\"\n }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"tokenId\", type: \"uint256\" }\n ],\n name: \"getClaimForToken\",\n outputs: [\n { name: \"instanceId\", type: \"uint256\" },\n {\n components: [\n { name: \"total\", type: \"uint32\" },\n { name: \"totalMax\", type: \"uint32\" },\n { name: \"walletMax\", type: \"uint32\" },\n { name: \"startDate\", type: \"uint48\" },\n { name: \"endDate\", type: \"uint48\" },\n { name: \"storageProtocol\", type: \"uint8\" },\n { name: \"merkleRoot\", type: \"bytes32\" },\n { name: \"location\", type: \"string\" },\n { name: \"tokenId\", type: \"uint256\" },\n { name: \"cost\", type: \"uint256\" },\n { name: \"paymentReceiver\", type: \"address\" },\n { name: \"erc20\", type: \"address\" },\n { name: \"signingAddress\", type: \"address\" }\n ],\n name: \"claim\",\n type: \"tuple\"\n }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"instanceId\", type: \"uint256\" },\n { name: \"mintIndex\", type: \"uint32\" },\n { name: \"merkleProof\", type: \"bytes32[]\" },\n { name: \"mintFor\", type: \"address\" }\n ],\n name: \"mint\",\n outputs: [],\n stateMutability: \"payable\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"MINT_FEE\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"MINT_FEE_MERKLE\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }\n] as const;\n\n// ERC165 interface detection\nexport const ERC165_ABI = parseAbi([\n \"function supportsInterface(bytes4 interfaceId) view returns (bool)\",\n]);\n\n// Known contract addresses\nexport const KNOWN_CONTRACTS = {\n // Manifold extension contracts\n manifoldExtension: \"0x26BBEA7803DcAc346D5F5f135b57Cf2c752A02bE\" as Address,\n \n // Add other known contracts here as needed\n} as const;\n\n// Interface IDs for contract detection\nexport const INTERFACE_IDS = {\n ERC165: \"0x01ffc9a7\",\n ERC721: \"0x80ac58cd\",\n ERC1155: \"0xd9b67a26\",\n ERC721Metadata: \"0x5b5e139f\",\n} as const;\n\n// IPFS Gateway configuration\nexport const IPFS_GATEWAYS = {\n default: \"https://ipfs.io/ipfs/\",\n cloudflare: \"https://cloudflare-ipfs.com/ipfs/\",\n pinata: \"https://gateway.pinata.cloud/ipfs/\",\n} as const;\n\n/**\n * Convert IPFS URL to HTTP gateway URL\n */\nexport function ipfsToHttp(url: string, gateway?: keyof typeof IPFS_GATEWAYS): string {\n const selectedGateway = gateway || \"default\";\n if (!url || !url.startsWith(\"ipfs://\")) {\n return url;\n }\n \n return url.replace(\"ipfs://\", IPFS_GATEWAYS[selectedGateway]);\n}\n\n/**\n * Check if a contract is likely an NFT contract by checking interface support\n */\nexport async function isNFTContract(\n client: any,\n contractAddress: Address\n): Promise<{ isNFT: boolean; type?: 'ERC721' | 'ERC1155' }> {\n try {\n // Try ERC165 supportsInterface\n const [supportsERC721, supportsERC1155] = await Promise.all([\n client.readContract({\n address: contractAddress,\n abi: ERC165_ABI,\n functionName: 'supportsInterface',\n args: [INTERFACE_IDS.ERC721],\n }).catch(() => false),\n client.readContract({\n address: contractAddress,\n abi: ERC165_ABI,\n functionName: 'supportsInterface',\n args: [INTERFACE_IDS.ERC1155],\n }).catch(() => false),\n ]);\n \n if (supportsERC721) return { isNFT: true, type: 'ERC721' };\n if (supportsERC1155) return { isNFT: true, type: 'ERC1155' };\n \n // Fallback: try to call name() function\n const name = await client.readContract({\n address: contractAddress,\n abi: ERC721_ABI.name,\n functionName: 'name',\n }).catch(() => null);\n \n return { isNFT: !!name };\n } catch {\n return { isNFT: false };\n }\n}", + "content": "import { parseAbi, type Address } from \"viem\";\n\n/**\n * Common NFT contract ABIs and standards\n */\n\n// Standard ERC721 functions\nexport const ERC721_ABI = {\n full: parseAbi([\n \"function tokenURI(uint256 tokenId) view returns (string)\",\n \"function name() view returns (string)\",\n \"function symbol() view returns (string)\",\n \"function ownerOf(uint256 tokenId) view returns (address)\",\n \"function balanceOf(address owner) view returns (uint256)\",\n \"function totalSupply() view returns (uint256)\",\n \"function baseURI() view returns (string)\",\n \"function contractURI() view returns (string)\",\n ]),\n \n // Individual functions for specific use cases\n tokenURI: parseAbi([\"function tokenURI(uint256 tokenId) view returns (string)\"]),\n name: parseAbi([\"function name() view returns (string)\"]),\n symbol: parseAbi([\"function symbol() view returns (string)\"]),\n ownerOf: parseAbi([\"function ownerOf(uint256 tokenId) view returns (address)\"]),\n baseURI: parseAbi([\"function baseURI() view returns (string)\"]),\n contractURI: parseAbi([\"function contractURI() view returns (string)\"]),\n};\n\n// Common price discovery functions across NFT contracts\nexport const PRICE_DISCOVERY_ABI = parseAbi([\n \"function mintPrice() view returns (uint256)\",\n \"function price() view returns (uint256)\",\n \"function MINT_PRICE() view returns (uint256)\",\n \"function getMintPrice() view returns (uint256)\",\n \"function publicMintPrice() view returns (uint256)\",\n]);\n\n// Common mint functions\nexport const MINT_ABI = parseAbi([\n \"function mint(uint256 amount) payable\",\n \"function mint(address to, uint256 amount) payable\",\n \"function publicMint(uint256 amount) payable\",\n \"function mintTo(address to, uint256 amount) payable\",\n]);\n\n// ERC1155 metadata function\nexport const ERC1155_ABI = {\n uri: parseAbi([\"function uri(uint256 tokenId) view returns (string)\"]),\n};\n\n// ERC20 ABI for token interactions\nexport const ERC20_ABI = parseAbi([\n \"function decimals() view returns (uint8)\",\n \"function symbol() view returns (string)\",\n \"function name() view returns (string)\",\n \"function totalSupply() view returns (uint256)\",\n \"function balanceOf(address owner) view returns (uint256)\",\n \"function allowance(address owner, address spender) view returns (uint256)\",\n \"function approve(address spender, uint256 value) returns (bool)\",\n \"function transfer(address to, uint256 value) returns (bool)\",\n \"function transferFrom(address from, address to, uint256 value) returns (bool)\",\n]);\n\n// Manifold contract detection ABI (kept separate as it's used on the main contract)\nexport const MANIFOLD_DETECTION_ABI = parseAbi([\n \"function getExtensions() view returns (address[])\",\n]);\n\n// Manifold extension contract full ABI\nexport const MANIFOLD_EXTENSION_ABI = [\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"tokenId\", type: \"uint256\" }\n ],\n name: \"tokenURI\",\n outputs: [{ name: \"uri\", type: \"string\" }],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"instanceId\", type: \"uint256\" }\n ],\n name: \"getClaim\",\n outputs: [\n {\n components: [\n { name: \"total\", type: \"uint32\" },\n { name: \"totalMax\", type: \"uint32\" },\n { name: \"walletMax\", type: \"uint32\" },\n { name: \"startDate\", type: \"uint48\" },\n { name: \"endDate\", type: \"uint48\" },\n { name: \"storageProtocol\", type: \"uint8\" },\n { name: \"merkleRoot\", type: \"bytes32\" },\n { name: \"location\", type: \"string\" },\n { name: \"tokenId\", type: \"uint256\" },\n { name: \"cost\", type: \"uint256\" },\n { name: \"paymentReceiver\", type: \"address\" },\n { name: \"erc20\", type: \"address\" },\n { name: \"signingAddress\", type: \"address\" }\n ],\n name: \"claim\",\n type: \"tuple\"\n }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"tokenId\", type: \"uint256\" }\n ],\n name: \"getClaimForToken\",\n outputs: [\n { name: \"instanceId\", type: \"uint256\" },\n {\n components: [\n { name: \"total\", type: \"uint32\" },\n { name: \"totalMax\", type: \"uint32\" },\n { name: \"walletMax\", type: \"uint32\" },\n { name: \"startDate\", type: \"uint48\" },\n { name: \"endDate\", type: \"uint48\" },\n { name: \"storageProtocol\", type: \"uint8\" },\n { name: \"merkleRoot\", type: \"bytes32\" },\n { name: \"location\", type: \"string\" },\n { name: \"tokenId\", type: \"uint256\" },\n { name: \"cost\", type: \"uint256\" },\n { name: \"paymentReceiver\", type: \"address\" },\n { name: \"erc20\", type: \"address\" },\n { name: \"signingAddress\", type: \"address\" }\n ],\n name: \"claim\",\n type: \"tuple\"\n }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"instanceId\", type: \"uint256\" },\n { name: \"mintIndex\", type: \"uint32\" },\n { name: \"merkleProof\", type: \"bytes32[]\" },\n { name: \"mintFor\", type: \"address\" }\n ],\n name: \"mint\",\n outputs: [],\n stateMutability: \"payable\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"MINT_FEE\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"MINT_FEE_MERKLE\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }\n] as const;\n\n// ERC165 interface detection\nexport const ERC165_ABI = parseAbi([\n \"function supportsInterface(bytes4 interfaceId) view returns (bool)\",\n]);\n\n// Known contract addresses\nexport const KNOWN_CONTRACTS = {\n // Manifold extension contracts\n manifoldExtension: \"0x26BBEA7803DcAc346D5F5f135b57Cf2c752A02bE\" as Address,\n \n // Add other known contracts here as needed\n} as const;\n\n// thirdweb OpenEditionERC721 ABI\nexport const THIRDWEB_OPENEDITONERC721_ABI = [\n {\n inputs: [\n { name: \"_receiver\", type: \"address\" },\n { name: \"_quantity\", type: \"uint256\" },\n { name: \"_currency\", type: \"address\" },\n { name: \"_pricePerToken\", type: \"uint256\" },\n {\n components: [\n { name: \"proof\", type: \"bytes32[]\" },\n { name: \"quantityLimitPerWallet\", type: \"uint256\" },\n { name: \"pricePerToken\", type: \"uint256\" },\n { name: \"currency\", type: \"address\" }\n ],\n name: \"_allowlistProof\",\n type: \"tuple\"\n },\n { name: \"_data\", type: \"bytes\" }\n ],\n name: \"claim\",\n outputs: [],\n stateMutability: \"payable\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"claimCondition\",\n outputs: [\n { name: \"currentStartId\", type: \"uint256\" },\n { name: \"count\", type: \"uint256\" }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [{ name: \"_conditionId\", type: \"uint256\" }],\n name: \"getClaimConditionById\",\n outputs: [\n {\n components: [\n { name: \"startTimestamp\", type: \"uint256\" },\n { name: \"maxClaimableSupply\", type: \"uint256\" },\n { name: \"supplyClaimed\", type: \"uint256\" },\n { name: \"quantityLimitPerWallet\", type: \"uint256\" },\n { name: \"merkleRoot\", type: \"bytes32\" },\n { name: \"pricePerToken\", type: \"uint256\" },\n { name: \"currency\", type: \"address\" },\n { name: \"metadata\", type: \"string\" }\n ],\n name: \"condition\",\n type: \"tuple\"\n }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"sharedMetadata\",\n outputs: [\n { name: \"name\", type: \"string\" },\n { name: \"description\", type: \"string\" },\n { name: \"image\", type: \"string\" },\n { name: \"animationUrl\", type: \"string\" }\n ],\n stateMutability: \"view\",\n type: \"function\"\n }\n] as const;\n\n// Native ETH address for thirdweb contracts\nexport const THIRDWEB_NATIVE_TOKEN = \"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE\" as Address;\n\n// Interface IDs for contract detection\nexport const INTERFACE_IDS = {\n ERC165: \"0x01ffc9a7\",\n ERC721: \"0x80ac58cd\",\n ERC1155: \"0xd9b67a26\",\n ERC721Metadata: \"0x5b5e139f\",\n} as const;\n\n// IPFS Gateway configuration\nexport const IPFS_GATEWAYS = {\n default: \"https://ipfs.io/ipfs/\",\n cloudflare: \"https://cloudflare-ipfs.com/ipfs/\",\n pinata: \"https://gateway.pinata.cloud/ipfs/\",\n} as const;\n\n/**\n * Convert IPFS URL to HTTP gateway URL\n */\nexport function ipfsToHttp(url: string, gateway?: keyof typeof IPFS_GATEWAYS): string {\n const selectedGateway = gateway || \"default\";\n if (!url || !url.startsWith(\"ipfs://\")) {\n return url;\n }\n \n return url.replace(\"ipfs://\", IPFS_GATEWAYS[selectedGateway]);\n}\n\n/**\n * Check if a contract is likely an NFT contract by checking interface support\n */\nexport async function isNFTContract(\n client: any,\n contractAddress: Address\n): Promise<{ isNFT: boolean; type?: 'ERC721' | 'ERC1155' }> {\n try {\n // Try ERC165 supportsInterface\n const [supportsERC721, supportsERC1155] = await Promise.all([\n client.readContract({\n address: contractAddress,\n abi: ERC165_ABI,\n functionName: 'supportsInterface',\n args: [INTERFACE_IDS.ERC721],\n }).catch(() => false),\n client.readContract({\n address: contractAddress,\n abi: ERC165_ABI,\n functionName: 'supportsInterface',\n args: [INTERFACE_IDS.ERC1155],\n }).catch(() => false),\n ]);\n \n if (supportsERC721) return { isNFT: true, type: 'ERC721' };\n if (supportsERC1155) return { isNFT: true, type: 'ERC1155' };\n \n // Fallback: try to call name() function\n const name = await client.readContract({\n address: contractAddress,\n abi: ERC721_ABI.name,\n functionName: 'name',\n }).catch(() => null);\n \n return { isNFT: !!name };\n } catch {\n return { isNFT: false };\n }\n}", "type": "registry:lib", "target": "" }, { - "path": "registry/mini-app/lib/manifold-utils.ts", - "content": "import { type Address, type PublicClient, getAddress } from \"viem\";\nimport { MANIFOLD_DETECTION_ABI, MANIFOLD_EXTENSION_ABI, KNOWN_CONTRACTS, ERC721_ABI } from \"@/registry/mini-app/lib/nft-standards\";\n\n/**\n * Manifold contract utilities\n */\n\nexport interface ManifoldDetectionResult {\n isManifold: boolean;\n extensionAddress?: Address;\n extensions?: Address[];\n}\n\nexport interface ManifoldClaim {\n total: number;\n totalMax: number;\n walletMax: number;\n startDate: bigint;\n endDate: bigint;\n storageProtocol: number;\n merkleRoot: `0x${string}`;\n location: string;\n tokenId: bigint;\n cost: bigint;\n paymentReceiver: Address;\n erc20: Address;\n signingAddress: Address;\n}\n\n/**\n * Detect if a contract is a Manifold contract with extensions\n */\nexport async function detectManifoldContract(\n client: PublicClient,\n contractAddress: Address\n): Promise {\n try {\n const extensions = await client.readContract({\n address: getAddress(contractAddress),\n abi: MANIFOLD_DETECTION_ABI,\n functionName: \"getExtensions\",\n }) as Address[];\n \n if (!extensions || extensions.length === 0) {\n return { isManifold: false };\n }\n \n // Check if it has the known Manifold extension\n const knownExtension = extensions.find(\n ext => ext.toLowerCase() === KNOWN_CONTRACTS.manifoldExtension.toLowerCase()\n );\n \n return {\n isManifold: true,\n extensionAddress: knownExtension || extensions[0],\n extensions,\n };\n } catch {\n return { isManifold: false };\n }\n}\n\n/**\n * Get token URI for a Manifold NFT\n */\nexport async function getManifoldTokenURI(\n client: PublicClient,\n contractAddress: Address,\n tokenId: string,\n extensionAddress?: Address\n): Promise {\n const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension;\n \n return await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"tokenURI\",\n args: [getAddress(contractAddress), BigInt(tokenId)],\n }) as string;\n}\n\n/**\n * Get token URI with automatic Manifold detection\n */\nexport async function getTokenURIWithManifoldSupport(\n client: PublicClient,\n contractAddress: Address,\n tokenId: string\n): Promise {\n // Try Manifold first\n const manifoldInfo = await detectManifoldContract(client, contractAddress);\n \n if (manifoldInfo.isManifold && manifoldInfo.extensionAddress) {\n try {\n return await getManifoldTokenURI(\n client,\n contractAddress,\n tokenId,\n manifoldInfo.extensionAddress\n );\n } catch (error) {\n console.warn(\"Failed to get Manifold tokenURI, falling back to standard\", error);\n }\n }\n \n // Fallback to standard ERC721 tokenURI\n return await client.readContract({\n address: getAddress(contractAddress),\n abi: ERC721_ABI.tokenURI,\n functionName: \"tokenURI\",\n args: [BigInt(tokenId)],\n }) as string;\n}\n\n/**\n * Get Manifold claim information\n */\nexport async function getManifoldClaim(\n client: PublicClient,\n contractAddress: Address,\n instanceId: string,\n extensionAddress?: Address\n): Promise {\n try {\n const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension;\n \n const claim = await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"getClaim\",\n args: [getAddress(contractAddress), BigInt(instanceId)],\n });\n \n return claim as unknown as ManifoldClaim;\n } catch {\n return null;\n }\n}\n\n/**\n * Get Manifold mint fee\n */\nexport async function getManifoldMintFee(\n client: PublicClient,\n extensionAddress?: Address\n): Promise {\n const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension;\n \n try {\n return await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"MINT_FEE\",\n }) as bigint;\n } catch {\n // Try MINT_FEE_MERKLE as fallback\n try {\n return await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"MINT_FEE_MERKLE\",\n }) as bigint;\n } catch {\n return BigInt(0);\n }\n }\n}\n\n/**\n * Check if an address is the zero address\n */\nexport function isZeroAddress(address: string): boolean {\n return address === \"0x0000000000000000000000000000000000000000\";\n}\n\n/**\n * Format instance ID and token ID for display\n */\nexport function formatManifoldTokenId(instanceId: string, tokenId: string): string {\n return `${instanceId}-${tokenId}`;\n}", + "path": "registry/mini-app/lib/nft-metadata-utils.ts", + "content": "import { type Address, type PublicClient, getAddress } from \"viem\";\nimport { \n MANIFOLD_DETECTION_ABI, \n MANIFOLD_EXTENSION_ABI, \n KNOWN_CONTRACTS, \n ERC721_ABI,\n ERC1155_ABI,\n THIRDWEB_OPENEDITONERC721_ABI\n} from \"@/registry/mini-app/lib/nft-standards\";\n\n/**\n * NFT metadata utilities with support for multiple standards and fallback mechanisms\n */\n\nexport type ProviderHint = \"manifold\" | \"thirdweb\" | \"standard\" | \"erc1155\";\n\nexport interface ManifoldDetectionResult {\n isManifold: boolean;\n extensionAddress?: Address;\n extensions?: Address[];\n}\n\nexport interface ManifoldClaim {\n total: number;\n totalMax: number;\n walletMax: number;\n startDate: bigint;\n endDate: bigint;\n storageProtocol: number;\n merkleRoot: `0x${string}`;\n location: string;\n tokenId: bigint;\n cost: bigint;\n paymentReceiver: Address;\n erc20: Address;\n signingAddress: Address;\n}\n\n/**\n * Detect if a contract is a Manifold contract with extensions\n */\nexport async function detectManifoldContract(\n client: PublicClient,\n contractAddress: Address\n): Promise {\n try {\n const extensions = await client.readContract({\n address: getAddress(contractAddress),\n abi: MANIFOLD_DETECTION_ABI,\n functionName: \"getExtensions\",\n }) as Address[];\n \n if (!extensions || extensions.length === 0) {\n return { isManifold: false };\n }\n \n // Check if it has the known Manifold extension\n const knownExtension = extensions.find(\n ext => ext.toLowerCase() === KNOWN_CONTRACTS.manifoldExtension.toLowerCase()\n );\n \n return {\n isManifold: true,\n extensionAddress: knownExtension || extensions[0],\n extensions,\n };\n } catch {\n return { isManifold: false };\n }\n}\n\n/**\n * Get token URI for a Manifold NFT\n */\nexport async function getManifoldTokenURI(\n client: PublicClient,\n contractAddress: Address,\n tokenId: string,\n extensionAddress?: Address\n): Promise {\n const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension;\n \n return await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"tokenURI\",\n args: [getAddress(contractAddress), BigInt(tokenId)],\n }) as string;\n}\n\n/**\n * Get token URI with automatic Manifold detection\n * @deprecated Use getTokenMetadataURL instead for more comprehensive metadata support\n */\nexport async function getTokenURIWithManifoldSupport(\n client: PublicClient,\n contractAddress: Address,\n tokenId: string\n): Promise {\n // Try Manifold first\n const manifoldInfo = await detectManifoldContract(client, contractAddress);\n \n if (manifoldInfo.isManifold && manifoldInfo.extensionAddress) {\n try {\n return await getManifoldTokenURI(\n client,\n contractAddress,\n tokenId,\n manifoldInfo.extensionAddress\n );\n } catch (error) {\n console.warn(\"Failed to get Manifold tokenURI, falling back to standard\", error);\n }\n }\n \n // Fallback to standard ERC721 tokenURI\n return await client.readContract({\n address: getAddress(contractAddress),\n abi: ERC721_ABI.tokenURI,\n functionName: \"tokenURI\",\n args: [BigInt(tokenId)],\n }) as string;\n}\n\n/**\n * Get token metadata URL with comprehensive fallback chain\n * Supports multiple NFT standards including ERC721, ERC1155, Manifold, and Thirdweb OpenEditions\n * \n * @param client - Public client for blockchain interactions\n * @param contractAddress - NFT contract address\n * @param tokenId - Token ID to fetch metadata for\n * @param providerHint - Optional hint about the contract type for optimization\n * @returns Token metadata URL or empty string if not found\n */\nexport async function getTokenMetadataURL(\n client: PublicClient,\n contractAddress: Address,\n tokenId: string,\n providerHint?: ProviderHint\n): Promise {\n const address = getAddress(contractAddress);\n const tokenIdBigInt = BigInt(tokenId);\n \n // If provider hint is given, try that first\n if (providerHint) {\n try {\n switch (providerHint) {\n case \"manifold\": {\n const manifoldInfo = await detectManifoldContract(client, address);\n if (manifoldInfo.isManifold && manifoldInfo.extensionAddress) {\n return await getManifoldTokenURI(client, address, tokenId, manifoldInfo.extensionAddress);\n }\n break;\n }\n case \"erc1155\": {\n const uri = await client.readContract({\n address,\n abi: ERC1155_ABI.uri,\n functionName: \"uri\",\n args: [tokenIdBigInt],\n }) as string;\n // ERC1155 URIs often have {id} placeholder that needs to be replaced\n return uri.replace(\"{id}\", tokenId.padStart(64, \"0\"));\n }\n case \"thirdweb\": {\n // Try sharedMetadata first for OpenEditions\n const metadata = await client.readContract({\n address,\n abi: THIRDWEB_OPENEDITONERC721_ABI,\n functionName: \"sharedMetadata\",\n }) as any;\n if (metadata && metadata.image) {\n // Construct metadata JSON from sharedMetadata\n const metadataJson = {\n name: metadata.name || \"\",\n description: metadata.description || \"\",\n image: metadata.image,\n animation_url: metadata.animationUrl || undefined,\n };\n // Return as data URI\n return `data:application/json;base64,${btoa(JSON.stringify(metadataJson))}`;\n }\n break;\n }\n }\n } catch (error) {\n console.debug(`Provider hint ${providerHint} failed, trying other methods`, error);\n }\n }\n \n // Comprehensive fallback chain\n const fallbackMethods = [\n // 1. Standard ERC721 tokenURI\n async () => {\n return await client.readContract({\n address,\n abi: ERC721_ABI.tokenURI,\n functionName: \"tokenURI\",\n args: [tokenIdBigInt],\n }) as string;\n },\n \n // 2. ERC1155 uri\n async () => {\n const uri = await client.readContract({\n address,\n abi: ERC1155_ABI.uri,\n functionName: \"uri\",\n args: [tokenIdBigInt],\n }) as string;\n // Replace {id} placeholder if present\n return uri.replace(\"{id}\", tokenId.padStart(64, \"0\"));\n },\n \n // 3. Manifold detection and tokenURI\n async () => {\n const manifoldInfo = await detectManifoldContract(client, address);\n if (manifoldInfo.isManifold && manifoldInfo.extensionAddress) {\n return await getManifoldTokenURI(client, address, tokenId, manifoldInfo.extensionAddress);\n }\n throw new Error(\"Not a Manifold contract\");\n },\n \n // 4. contractURI (for contracts with shared metadata)\n async () => {\n const contractURI = await client.readContract({\n address,\n abi: ERC721_ABI.contractURI,\n functionName: \"contractURI\",\n }) as string;\n // Note: contractURI typically contains collection-level metadata, not token-specific\n // This is a last resort fallback\n return contractURI;\n },\n \n // 5. Thirdweb sharedMetadata (for OpenEditions)\n async () => {\n const metadata = await client.readContract({\n address,\n abi: THIRDWEB_OPENEDITONERC721_ABI,\n functionName: \"sharedMetadata\",\n }) as any;\n \n if (metadata && metadata.image) {\n // Construct metadata JSON from sharedMetadata\n const metadataJson = {\n name: metadata.name || `Token #${tokenId}`,\n description: metadata.description || \"\",\n image: metadata.image,\n animation_url: metadata.animationUrl || undefined,\n };\n // Return as data URI\n return `data:application/json;base64,${btoa(JSON.stringify(metadataJson))}`;\n }\n throw new Error(\"No shared metadata found\");\n },\n \n // 6. baseURI + tokenId concatenation\n async () => {\n const baseURI = await client.readContract({\n address,\n abi: ERC721_ABI.baseURI,\n functionName: \"baseURI\",\n }) as string;\n \n if (baseURI) {\n // Ensure proper URL joining\n const separator = baseURI.endsWith(\"/\") ? \"\" : \"/\";\n return `${baseURI}${separator}${tokenId}`;\n }\n throw new Error(\"No baseURI found\");\n },\n ];\n \n // Try each method in order\n for (const method of fallbackMethods) {\n try {\n const result = await method();\n if (result && typeof result === \"string\" && result.length > 0) {\n return result;\n }\n } catch (error) {\n // Continue to next method\n continue;\n }\n }\n \n // If all methods fail, return empty string\n console.warn(`Could not fetch metadata for token ${tokenId} at ${contractAddress}`);\n return \"\";\n}\n\n/**\n * Get Manifold claim information\n */\nexport async function getManifoldClaim(\n client: PublicClient,\n contractAddress: Address,\n instanceId: string,\n extensionAddress?: Address\n): Promise {\n try {\n const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension;\n \n const claim = await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"getClaim\",\n args: [getAddress(contractAddress), BigInt(instanceId)],\n });\n \n return claim as unknown as ManifoldClaim;\n } catch {\n return null;\n }\n}\n\n/**\n * Get Manifold mint fee\n */\nexport async function getManifoldMintFee(\n client: PublicClient,\n extensionAddress?: Address\n): Promise {\n const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension;\n \n try {\n return await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"MINT_FEE\",\n }) as bigint;\n } catch {\n // Try MINT_FEE_MERKLE as fallback\n try {\n return await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"MINT_FEE_MERKLE\",\n }) as bigint;\n } catch {\n return BigInt(0);\n }\n }\n}\n\n/**\n * Check if an address is the zero address\n */\nexport function isZeroAddress(address: string): boolean {\n return address === \"0x0000000000000000000000000000000000000000\";\n}\n\n/**\n * Format instance ID and token ID for display\n */\nexport function formatManifoldTokenId(instanceId: string, tokenId: string): string {\n return `${instanceId}-${tokenId}`;\n}", "type": "registry:lib", "target": "" } diff --git a/public/r/nft-metadata-utils.json b/public/r/nft-metadata-utils.json new file mode 100644 index 00000000..022c9802 --- /dev/null +++ b/public/r/nft-metadata-utils.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "nft-metadata-utils", + "type": "registry:lib", + "title": "nftMetadataUtils", + "description": "NFT metadata utilities with support for multiple standards including Manifold, Thirdweb, ERC721, and ERC1155", + "dependencies": [ + "viem" + ], + "registryDependencies": [ + "https://hellno-mini-app-ui.vercel.app/r/nft-standards.json" + ], + "files": [ + { + "path": "registry/mini-app/lib/nft-metadata-utils.ts", + "content": "import { type Address, type PublicClient, getAddress } from \"viem\";\nimport { \n MANIFOLD_DETECTION_ABI, \n MANIFOLD_EXTENSION_ABI, \n KNOWN_CONTRACTS, \n ERC721_ABI,\n ERC1155_ABI,\n THIRDWEB_OPENEDITONERC721_ABI\n} from \"@/registry/mini-app/lib/nft-standards\";\n\n/**\n * NFT metadata utilities with support for multiple standards and fallback mechanisms\n */\n\nexport type ProviderHint = \"manifold\" | \"thirdweb\" | \"standard\" | \"erc1155\";\n\nexport interface ManifoldDetectionResult {\n isManifold: boolean;\n extensionAddress?: Address;\n extensions?: Address[];\n}\n\nexport interface ManifoldClaim {\n total: number;\n totalMax: number;\n walletMax: number;\n startDate: bigint;\n endDate: bigint;\n storageProtocol: number;\n merkleRoot: `0x${string}`;\n location: string;\n tokenId: bigint;\n cost: bigint;\n paymentReceiver: Address;\n erc20: Address;\n signingAddress: Address;\n}\n\n/**\n * Detect if a contract is a Manifold contract with extensions\n */\nexport async function detectManifoldContract(\n client: PublicClient,\n contractAddress: Address\n): Promise {\n try {\n const extensions = await client.readContract({\n address: getAddress(contractAddress),\n abi: MANIFOLD_DETECTION_ABI,\n functionName: \"getExtensions\",\n }) as Address[];\n \n if (!extensions || extensions.length === 0) {\n return { isManifold: false };\n }\n \n // Check if it has the known Manifold extension\n const knownExtension = extensions.find(\n ext => ext.toLowerCase() === KNOWN_CONTRACTS.manifoldExtension.toLowerCase()\n );\n \n return {\n isManifold: true,\n extensionAddress: knownExtension || extensions[0],\n extensions,\n };\n } catch {\n return { isManifold: false };\n }\n}\n\n/**\n * Get token URI for a Manifold NFT\n */\nexport async function getManifoldTokenURI(\n client: PublicClient,\n contractAddress: Address,\n tokenId: string,\n extensionAddress?: Address\n): Promise {\n const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension;\n \n return await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"tokenURI\",\n args: [getAddress(contractAddress), BigInt(tokenId)],\n }) as string;\n}\n\n/**\n * Get token URI with automatic Manifold detection\n * @deprecated Use getTokenMetadataURL instead for more comprehensive metadata support\n */\nexport async function getTokenURIWithManifoldSupport(\n client: PublicClient,\n contractAddress: Address,\n tokenId: string\n): Promise {\n // Try Manifold first\n const manifoldInfo = await detectManifoldContract(client, contractAddress);\n \n if (manifoldInfo.isManifold && manifoldInfo.extensionAddress) {\n try {\n return await getManifoldTokenURI(\n client,\n contractAddress,\n tokenId,\n manifoldInfo.extensionAddress\n );\n } catch (error) {\n console.warn(\"Failed to get Manifold tokenURI, falling back to standard\", error);\n }\n }\n \n // Fallback to standard ERC721 tokenURI\n return await client.readContract({\n address: getAddress(contractAddress),\n abi: ERC721_ABI.tokenURI,\n functionName: \"tokenURI\",\n args: [BigInt(tokenId)],\n }) as string;\n}\n\n/**\n * Get token metadata URL with comprehensive fallback chain\n * Supports multiple NFT standards including ERC721, ERC1155, Manifold, and Thirdweb OpenEditions\n * \n * @param client - Public client for blockchain interactions\n * @param contractAddress - NFT contract address\n * @param tokenId - Token ID to fetch metadata for\n * @param providerHint - Optional hint about the contract type for optimization\n * @returns Token metadata URL or empty string if not found\n */\nexport async function getTokenMetadataURL(\n client: PublicClient,\n contractAddress: Address,\n tokenId: string,\n providerHint?: ProviderHint\n): Promise {\n const address = getAddress(contractAddress);\n const tokenIdBigInt = BigInt(tokenId);\n \n // If provider hint is given, try that first\n if (providerHint) {\n try {\n switch (providerHint) {\n case \"manifold\": {\n const manifoldInfo = await detectManifoldContract(client, address);\n if (manifoldInfo.isManifold && manifoldInfo.extensionAddress) {\n return await getManifoldTokenURI(client, address, tokenId, manifoldInfo.extensionAddress);\n }\n break;\n }\n case \"erc1155\": {\n const uri = await client.readContract({\n address,\n abi: ERC1155_ABI.uri,\n functionName: \"uri\",\n args: [tokenIdBigInt],\n }) as string;\n // ERC1155 URIs often have {id} placeholder that needs to be replaced\n return uri.replace(\"{id}\", tokenId.padStart(64, \"0\"));\n }\n case \"thirdweb\": {\n // Try sharedMetadata first for OpenEditions\n const metadata = await client.readContract({\n address,\n abi: THIRDWEB_OPENEDITONERC721_ABI,\n functionName: \"sharedMetadata\",\n }) as any;\n if (metadata && metadata.image) {\n // Construct metadata JSON from sharedMetadata\n const metadataJson = {\n name: metadata.name || \"\",\n description: metadata.description || \"\",\n image: metadata.image,\n animation_url: metadata.animationUrl || undefined,\n };\n // Return as data URI\n return `data:application/json;base64,${btoa(JSON.stringify(metadataJson))}`;\n }\n break;\n }\n }\n } catch (error) {\n console.debug(`Provider hint ${providerHint} failed, trying other methods`, error);\n }\n }\n \n // Comprehensive fallback chain\n const fallbackMethods = [\n // 1. Standard ERC721 tokenURI\n async () => {\n return await client.readContract({\n address,\n abi: ERC721_ABI.tokenURI,\n functionName: \"tokenURI\",\n args: [tokenIdBigInt],\n }) as string;\n },\n \n // 2. ERC1155 uri\n async () => {\n const uri = await client.readContract({\n address,\n abi: ERC1155_ABI.uri,\n functionName: \"uri\",\n args: [tokenIdBigInt],\n }) as string;\n // Replace {id} placeholder if present\n return uri.replace(\"{id}\", tokenId.padStart(64, \"0\"));\n },\n \n // 3. Manifold detection and tokenURI\n async () => {\n const manifoldInfo = await detectManifoldContract(client, address);\n if (manifoldInfo.isManifold && manifoldInfo.extensionAddress) {\n return await getManifoldTokenURI(client, address, tokenId, manifoldInfo.extensionAddress);\n }\n throw new Error(\"Not a Manifold contract\");\n },\n \n // 4. contractURI (for contracts with shared metadata)\n async () => {\n const contractURI = await client.readContract({\n address,\n abi: ERC721_ABI.contractURI,\n functionName: \"contractURI\",\n }) as string;\n // Note: contractURI typically contains collection-level metadata, not token-specific\n // This is a last resort fallback\n return contractURI;\n },\n \n // 5. Thirdweb sharedMetadata (for OpenEditions)\n async () => {\n const metadata = await client.readContract({\n address,\n abi: THIRDWEB_OPENEDITONERC721_ABI,\n functionName: \"sharedMetadata\",\n }) as any;\n \n if (metadata && metadata.image) {\n // Construct metadata JSON from sharedMetadata\n const metadataJson = {\n name: metadata.name || `Token #${tokenId}`,\n description: metadata.description || \"\",\n image: metadata.image,\n animation_url: metadata.animationUrl || undefined,\n };\n // Return as data URI\n return `data:application/json;base64,${btoa(JSON.stringify(metadataJson))}`;\n }\n throw new Error(\"No shared metadata found\");\n },\n \n // 6. baseURI + tokenId concatenation\n async () => {\n const baseURI = await client.readContract({\n address,\n abi: ERC721_ABI.baseURI,\n functionName: \"baseURI\",\n }) as string;\n \n if (baseURI) {\n // Ensure proper URL joining\n const separator = baseURI.endsWith(\"/\") ? \"\" : \"/\";\n return `${baseURI}${separator}${tokenId}`;\n }\n throw new Error(\"No baseURI found\");\n },\n ];\n \n // Try each method in order\n for (const method of fallbackMethods) {\n try {\n const result = await method();\n if (result && typeof result === \"string\" && result.length > 0) {\n return result;\n }\n } catch (error) {\n // Continue to next method\n continue;\n }\n }\n \n // If all methods fail, return empty string\n console.warn(`Could not fetch metadata for token ${tokenId} at ${contractAddress}`);\n return \"\";\n}\n\n/**\n * Get Manifold claim information\n */\nexport async function getManifoldClaim(\n client: PublicClient,\n contractAddress: Address,\n instanceId: string,\n extensionAddress?: Address\n): Promise {\n try {\n const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension;\n \n const claim = await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"getClaim\",\n args: [getAddress(contractAddress), BigInt(instanceId)],\n });\n \n return claim as unknown as ManifoldClaim;\n } catch {\n return null;\n }\n}\n\n/**\n * Get Manifold mint fee\n */\nexport async function getManifoldMintFee(\n client: PublicClient,\n extensionAddress?: Address\n): Promise {\n const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension;\n \n try {\n return await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"MINT_FEE\",\n }) as bigint;\n } catch {\n // Try MINT_FEE_MERKLE as fallback\n try {\n return await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"MINT_FEE_MERKLE\",\n }) as bigint;\n } catch {\n return BigInt(0);\n }\n }\n}\n\n/**\n * Check if an address is the zero address\n */\nexport function isZeroAddress(address: string): boolean {\n return address === \"0x0000000000000000000000000000000000000000\";\n}\n\n/**\n * Format instance ID and token ID for display\n */\nexport function formatManifoldTokenId(instanceId: string, tokenId: string): string {\n return `${instanceId}-${tokenId}`;\n}", + "type": "registry:lib" + }, + { + "path": "registry/mini-app/lib/nft-standards.ts", + "content": "import { parseAbi, type Address } from \"viem\";\n\n/**\n * Common NFT contract ABIs and standards\n */\n\n// Standard ERC721 functions\nexport const ERC721_ABI = {\n full: parseAbi([\n \"function tokenURI(uint256 tokenId) view returns (string)\",\n \"function name() view returns (string)\",\n \"function symbol() view returns (string)\",\n \"function ownerOf(uint256 tokenId) view returns (address)\",\n \"function balanceOf(address owner) view returns (uint256)\",\n \"function totalSupply() view returns (uint256)\",\n \"function baseURI() view returns (string)\",\n \"function contractURI() view returns (string)\",\n ]),\n \n // Individual functions for specific use cases\n tokenURI: parseAbi([\"function tokenURI(uint256 tokenId) view returns (string)\"]),\n name: parseAbi([\"function name() view returns (string)\"]),\n symbol: parseAbi([\"function symbol() view returns (string)\"]),\n ownerOf: parseAbi([\"function ownerOf(uint256 tokenId) view returns (address)\"]),\n baseURI: parseAbi([\"function baseURI() view returns (string)\"]),\n contractURI: parseAbi([\"function contractURI() view returns (string)\"]),\n};\n\n// Common price discovery functions across NFT contracts\nexport const PRICE_DISCOVERY_ABI = parseAbi([\n \"function mintPrice() view returns (uint256)\",\n \"function price() view returns (uint256)\",\n \"function MINT_PRICE() view returns (uint256)\",\n \"function getMintPrice() view returns (uint256)\",\n \"function publicMintPrice() view returns (uint256)\",\n]);\n\n// Common mint functions\nexport const MINT_ABI = parseAbi([\n \"function mint(uint256 amount) payable\",\n \"function mint(address to, uint256 amount) payable\",\n \"function publicMint(uint256 amount) payable\",\n \"function mintTo(address to, uint256 amount) payable\",\n]);\n\n// ERC1155 metadata function\nexport const ERC1155_ABI = {\n uri: parseAbi([\"function uri(uint256 tokenId) view returns (string)\"]),\n};\n\n// ERC20 ABI for token interactions\nexport const ERC20_ABI = parseAbi([\n \"function decimals() view returns (uint8)\",\n \"function symbol() view returns (string)\",\n \"function name() view returns (string)\",\n \"function totalSupply() view returns (uint256)\",\n \"function balanceOf(address owner) view returns (uint256)\",\n \"function allowance(address owner, address spender) view returns (uint256)\",\n \"function approve(address spender, uint256 value) returns (bool)\",\n \"function transfer(address to, uint256 value) returns (bool)\",\n \"function transferFrom(address from, address to, uint256 value) returns (bool)\",\n]);\n\n// Manifold contract detection ABI (kept separate as it's used on the main contract)\nexport const MANIFOLD_DETECTION_ABI = parseAbi([\n \"function getExtensions() view returns (address[])\",\n]);\n\n// Manifold extension contract full ABI\nexport const MANIFOLD_EXTENSION_ABI = [\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"tokenId\", type: \"uint256\" }\n ],\n name: \"tokenURI\",\n outputs: [{ name: \"uri\", type: \"string\" }],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"instanceId\", type: \"uint256\" }\n ],\n name: \"getClaim\",\n outputs: [\n {\n components: [\n { name: \"total\", type: \"uint32\" },\n { name: \"totalMax\", type: \"uint32\" },\n { name: \"walletMax\", type: \"uint32\" },\n { name: \"startDate\", type: \"uint48\" },\n { name: \"endDate\", type: \"uint48\" },\n { name: \"storageProtocol\", type: \"uint8\" },\n { name: \"merkleRoot\", type: \"bytes32\" },\n { name: \"location\", type: \"string\" },\n { name: \"tokenId\", type: \"uint256\" },\n { name: \"cost\", type: \"uint256\" },\n { name: \"paymentReceiver\", type: \"address\" },\n { name: \"erc20\", type: \"address\" },\n { name: \"signingAddress\", type: \"address\" }\n ],\n name: \"claim\",\n type: \"tuple\"\n }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"tokenId\", type: \"uint256\" }\n ],\n name: \"getClaimForToken\",\n outputs: [\n { name: \"instanceId\", type: \"uint256\" },\n {\n components: [\n { name: \"total\", type: \"uint32\" },\n { name: \"totalMax\", type: \"uint32\" },\n { name: \"walletMax\", type: \"uint32\" },\n { name: \"startDate\", type: \"uint48\" },\n { name: \"endDate\", type: \"uint48\" },\n { name: \"storageProtocol\", type: \"uint8\" },\n { name: \"merkleRoot\", type: \"bytes32\" },\n { name: \"location\", type: \"string\" },\n { name: \"tokenId\", type: \"uint256\" },\n { name: \"cost\", type: \"uint256\" },\n { name: \"paymentReceiver\", type: \"address\" },\n { name: \"erc20\", type: \"address\" },\n { name: \"signingAddress\", type: \"address\" }\n ],\n name: \"claim\",\n type: \"tuple\"\n }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"instanceId\", type: \"uint256\" },\n { name: \"mintIndex\", type: \"uint32\" },\n { name: \"merkleProof\", type: \"bytes32[]\" },\n { name: \"mintFor\", type: \"address\" }\n ],\n name: \"mint\",\n outputs: [],\n stateMutability: \"payable\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"MINT_FEE\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"MINT_FEE_MERKLE\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }\n] as const;\n\n// ERC165 interface detection\nexport const ERC165_ABI = parseAbi([\n \"function supportsInterface(bytes4 interfaceId) view returns (bool)\",\n]);\n\n// Known contract addresses\nexport const KNOWN_CONTRACTS = {\n // Manifold extension contracts\n manifoldExtension: \"0x26BBEA7803DcAc346D5F5f135b57Cf2c752A02bE\" as Address,\n \n // Add other known contracts here as needed\n} as const;\n\n// thirdweb OpenEditionERC721 ABI\nexport const THIRDWEB_OPENEDITONERC721_ABI = [\n {\n inputs: [\n { name: \"_receiver\", type: \"address\" },\n { name: \"_quantity\", type: \"uint256\" },\n { name: \"_currency\", type: \"address\" },\n { name: \"_pricePerToken\", type: \"uint256\" },\n {\n components: [\n { name: \"proof\", type: \"bytes32[]\" },\n { name: \"quantityLimitPerWallet\", type: \"uint256\" },\n { name: \"pricePerToken\", type: \"uint256\" },\n { name: \"currency\", type: \"address\" }\n ],\n name: \"_allowlistProof\",\n type: \"tuple\"\n },\n { name: \"_data\", type: \"bytes\" }\n ],\n name: \"claim\",\n outputs: [],\n stateMutability: \"payable\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"claimCondition\",\n outputs: [\n { name: \"currentStartId\", type: \"uint256\" },\n { name: \"count\", type: \"uint256\" }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [{ name: \"_conditionId\", type: \"uint256\" }],\n name: \"getClaimConditionById\",\n outputs: [\n {\n components: [\n { name: \"startTimestamp\", type: \"uint256\" },\n { name: \"maxClaimableSupply\", type: \"uint256\" },\n { name: \"supplyClaimed\", type: \"uint256\" },\n { name: \"quantityLimitPerWallet\", type: \"uint256\" },\n { name: \"merkleRoot\", type: \"bytes32\" },\n { name: \"pricePerToken\", type: \"uint256\" },\n { name: \"currency\", type: \"address\" },\n { name: \"metadata\", type: \"string\" }\n ],\n name: \"condition\",\n type: \"tuple\"\n }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"sharedMetadata\",\n outputs: [\n { name: \"name\", type: \"string\" },\n { name: \"description\", type: \"string\" },\n { name: \"image\", type: \"string\" },\n { name: \"animationUrl\", type: \"string\" }\n ],\n stateMutability: \"view\",\n type: \"function\"\n }\n] as const;\n\n// Native ETH address for thirdweb contracts\nexport const THIRDWEB_NATIVE_TOKEN = \"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE\" as Address;\n\n// Interface IDs for contract detection\nexport const INTERFACE_IDS = {\n ERC165: \"0x01ffc9a7\",\n ERC721: \"0x80ac58cd\",\n ERC1155: \"0xd9b67a26\",\n ERC721Metadata: \"0x5b5e139f\",\n} as const;\n\n// IPFS Gateway configuration\nexport const IPFS_GATEWAYS = {\n default: \"https://ipfs.io/ipfs/\",\n cloudflare: \"https://cloudflare-ipfs.com/ipfs/\",\n pinata: \"https://gateway.pinata.cloud/ipfs/\",\n} as const;\n\n/**\n * Convert IPFS URL to HTTP gateway URL\n */\nexport function ipfsToHttp(url: string, gateway?: keyof typeof IPFS_GATEWAYS): string {\n const selectedGateway = gateway || \"default\";\n if (!url || !url.startsWith(\"ipfs://\")) {\n return url;\n }\n \n return url.replace(\"ipfs://\", IPFS_GATEWAYS[selectedGateway]);\n}\n\n/**\n * Check if a contract is likely an NFT contract by checking interface support\n */\nexport async function isNFTContract(\n client: any,\n contractAddress: Address\n): Promise<{ isNFT: boolean; type?: 'ERC721' | 'ERC1155' }> {\n try {\n // Try ERC165 supportsInterface\n const [supportsERC721, supportsERC1155] = await Promise.all([\n client.readContract({\n address: contractAddress,\n abi: ERC165_ABI,\n functionName: 'supportsInterface',\n args: [INTERFACE_IDS.ERC721],\n }).catch(() => false),\n client.readContract({\n address: contractAddress,\n abi: ERC165_ABI,\n functionName: 'supportsInterface',\n args: [INTERFACE_IDS.ERC1155],\n }).catch(() => false),\n ]);\n \n if (supportsERC721) return { isNFT: true, type: 'ERC721' };\n if (supportsERC1155) return { isNFT: true, type: 'ERC1155' };\n \n // Fallback: try to call name() function\n const name = await client.readContract({\n address: contractAddress,\n abi: ERC721_ABI.name,\n functionName: 'name',\n }).catch(() => null);\n \n return { isNFT: !!name };\n } catch {\n return { isNFT: false };\n }\n}", + "type": "registry:lib", + "target": "" + } + ] +} \ No newline at end of file diff --git a/public/r/nft-mint-flow.json b/public/r/nft-mint-flow.json index aac05902..ab17ee12 100644 --- a/public/r/nft-mint-flow.json +++ b/public/r/nft-mint-flow.json @@ -33,27 +33,27 @@ }, { "path": "registry/mini-app/blocks/nft-mint-flow/nft-mint-button.tsx", - "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Button } from \"@/registry/mini-app/ui/button\";\nimport {\n Sheet,\n SheetContent,\n SheetHeader,\n SheetTitle,\n} from \"@/registry/mini-app/ui/sheet\";\nimport { useMiniAppSdk } from \"@/registry/mini-app/hooks/use-miniapp-sdk\";\nimport {\n useAccount,\n useConnect,\n useWaitForTransactionReceipt,\n useWriteContract,\n useSwitchChain,\n} from \"wagmi\";\nimport { formatEther, type Address } from \"viem\";\nimport { farcasterFrame } from \"@farcaster/miniapp-wagmi-connector\";\nimport {\n Coins,\n CheckCircle,\n AlertCircle,\n Loader2,\n Info,\n ExternalLink,\n RefreshCw,\n Wallet,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n detectNFTProvider,\n validateParameters,\n getClientForChain,\n} from \"@/registry/mini-app/blocks/nft-mint-flow/lib/provider-detector\";\nimport { getProviderConfig } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/provider-configs\";\nimport { fetchPriceData } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/price-optimizer\";\nimport { mintReducer, initialState, type MintStep } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/mint-reducer\";\nimport type { MintParams } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/types\";\nimport { parseError, type ParsedError } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/error-parser\";\n\n/**\n * NFTMintButton - Universal NFT minting button with automatic provider detection and ERC20 approval handling\n *\n * @example\n * ```tsx\n * // Basic ETH mint (auto-detects provider)\n * \n *\n * // Manifold NFT with ERC20 payment (HIGHER token)\n * \n *\n * // Multiple NFTs with custom button\n * console.log('Minted!', txHash)}\n * />\n * ```\n */\ntype NFTMintFlowProps = {\n /**\n * NFT contract address (0x...). This should be the main NFT contract, not the minting contract.\n * For Manifold, this is the creator contract, not the extension.\n */\n contractAddress: Address;\n\n /**\n * Blockchain network ID\n * - 1 = Ethereum mainnet\n * - 8453 = Base mainnet\n */\n chainId: 1 | 8453;\n\n /**\n * Optional provider hint. Use when:\n * - Auto-detection is failing\n * - You know the provider and want faster loading\n * - Testing specific provider flows\n *\n * Leave undefined for automatic detection.\n */\n provider?: \"manifold\" | \"opensea\" | \"zora\" | \"generic\";\n\n /**\n * Number of NFTs to mint. Defaults to 1.\n * Note: For ERC20 payments, the total cost is multiplied by this amount.\n */\n amount?: number;\n\n /**\n * Manifold-specific parameters. Required when provider=\"manifold\".\n * - instanceId: The claim instance ID from Manifold (required for most Manifold NFTs)\n * - tokenId: The specific token ID (required for some editions)\n *\n * Find these in the Manifold claim page URL or contract details.\n */\n manifoldParams?: {\n instanceId?: string;\n tokenId?: string;\n };\n\n // UI customization\n /** Additional CSS classes */\n className?: string;\n /** Button style variant */\n variant?: \"default\" | \"destructive\" | \"secondary\" | \"ghost\" | \"outline\";\n /** Button size */\n size?: \"default\" | \"sm\" | \"lg\" | \"icon\";\n /** Custom button text. Defaults to \"Mint NFT\" */\n buttonText?: string;\n /** Disable the mint button */\n disabled?: boolean;\n\n /**\n * Called when NFT minting succeeds (not on approval success)\n * @param txHash - The mint transaction hash (not approval tx)\n */\n onMintSuccess?: (txHash: string) => void;\n\n /**\n * Called when NFT minting fails (not on approval failure)\n * @param error - Human-readable error message\n */\n onMintError?: (error: string) => void;\n};\n\nexport function NFTMintButton({\n contractAddress,\n chainId,\n provider,\n amount = 1,\n manifoldParams,\n className,\n variant = \"default\",\n size = \"default\",\n buttonText = \"Mint NFT\",\n disabled = false,\n onMintSuccess,\n onMintError,\n}: NFTMintFlowProps) {\n const [state, dispatch] = React.useReducer(mintReducer, initialState);\n const [isSheetOpen, setIsSheetOpen] = React.useState(false);\n const [parsedError, setParsedError] = React.useState(null);\n\n // Prop validation with helpful errors\n React.useEffect(() => {\n if (\n provider === \"manifold\" &&\n !manifoldParams?.instanceId &&\n !manifoldParams?.tokenId\n ) {\n console.error(\n \"NFTMintFlow: When provider='manifold', you must provide manifoldParams with either instanceId or tokenId. \" +\n \"Example: manifoldParams={{ instanceId: '4293509360' }}\",\n );\n }\n\n if (manifoldParams && provider !== \"manifold\") {\n console.warn(\n \"NFTMintFlow: manifoldParams provided but provider is not 'manifold'. \" +\n \"Did you forget to set provider='manifold'?\",\n );\n }\n\n if (chainId !== 1 && chainId !== 8453) {\n console.warn(\n `NFTMintFlow: Chain ID ${chainId} may not be supported. ` +\n \"Currently tested chains: 1 (Ethereum), 8453 (Base)\",\n );\n }\n\n if (!contractAddress || !contractAddress.match(/^0x[a-fA-F0-9]{40}$/)) {\n console.error(\n \"NFTMintFlow: Invalid contract address. Must be a valid Ethereum address (0x...)\",\n );\n }\n }, [provider, manifoldParams, chainId, contractAddress]);\n\n // Destructure commonly used values\n const {\n step,\n contractInfo,\n priceData,\n error,\n txHash,\n txType,\n isLoading,\n validationErrors,\n } = state;\n const { erc20Details } = priceData;\n\n const { isSDKLoaded } = useMiniAppSdk();\n const { isConnected, address, chain } = useAccount();\n const { connect } = useConnect();\n const { switchChain } = useSwitchChain();\n const {\n writeContract,\n isPending: isWritePending,\n data: writeData,\n error: writeError,\n } = useWriteContract();\n\n // Build mint params\n const mintParams: MintParams = React.useMemo(\n () => ({\n contractAddress,\n chainId,\n provider,\n amount,\n instanceId: manifoldParams?.instanceId,\n tokenId: manifoldParams?.tokenId,\n recipient: address,\n }),\n [contractAddress, chainId, provider, amount, manifoldParams, address],\n );\n\n // Watch for transaction completion\n const {\n isSuccess: isTxSuccess,\n isError: isTxError,\n error: txError,\n } = useWaitForTransactionReceipt({\n hash: writeData,\n });\n\n // Get provider config\n const providerConfig = contractInfo\n ? getProviderConfig(contractInfo.provider)\n : null;\n\n // Check if user is on the correct network\n const isCorrectNetwork = chain?.id === chainId;\n const networkName = chainId === 1 ? \"Ethereum\" : chainId === 8453 ? \"Base\" : \"Unknown\";\n\n // Handle transaction status updates\n React.useEffect(() => {\n if (writeError) {\n const parsed = parseError(writeError, txType || \"mint\");\n \n // Show retry option for user rejections\n if (parsed.type === \"user-rejected\") {\n setParsedError(parsed);\n dispatch({ type: \"TX_ERROR\", payload: \"Transaction cancelled by user\" });\n return;\n }\n \n setParsedError(parsed);\n dispatch({ type: \"TX_ERROR\", payload: writeError.message });\n if (txType === \"mint\") {\n onMintError?.(writeError.message);\n }\n }\n if (isTxError && txError) {\n const parsed = parseError(txError, txType || \"mint\");\n setParsedError(parsed);\n dispatch({ type: \"TX_ERROR\", payload: txError.message });\n if (txType === \"mint\") {\n onMintError?.(txError.message);\n }\n }\n if (writeData && !isTxSuccess && !isTxError) {\n // Transaction submitted, waiting for confirmation\n if (txType === \"approval\") {\n dispatch({ type: \"APPROVE_TX_SUBMITTED\", payload: writeData });\n } else if (txType === \"mint\") {\n dispatch({ type: \"MINT_TX_SUBMITTED\", payload: writeData });\n }\n }\n if (isTxSuccess && writeData) {\n if (txType === \"approval\") {\n dispatch({ type: \"APPROVE_SUCCESS\" });\n } else if (txType === \"mint\") {\n dispatch({ type: \"TX_SUCCESS\", payload: writeData });\n onMintSuccess?.(writeData);\n }\n }\n }, [\n isTxSuccess,\n writeData,\n onMintSuccess,\n isTxError,\n txError,\n onMintError,\n writeError,\n txType,\n ]);\n\n const handleClose = React.useCallback(() => {\n setIsSheetOpen(false);\n dispatch({ type: \"RESET\" });\n setParsedError(null);\n }, []);\n\n // Auto-close on success after 10 seconds\n React.useEffect(() => {\n if (step === \"success\") {\n const timer = setTimeout(() => {\n handleClose();\n }, 10000);\n return () => clearTimeout(timer);\n }\n }, [step, handleClose]);\n\n const handleSwitchNetwork = async () => {\n try {\n await switchChain({ chainId });\n } catch (err) {\n // Network switch failed - user likely rejected or wallet doesn't support it\n }\n };\n\n // Detect NFT provider and validate\n const detectAndValidate = async () => {\n dispatch({ type: \"DETECT_START\" });\n\n try {\n // Detect provider\n const info = await detectNFTProvider(mintParams);\n\n // Validate parameters\n const validation = validateParameters(mintParams, info);\n\n if (!validation.isValid) {\n dispatch({ type: \"VALIDATION_ERROR\", payload: validation.errors });\n return;\n }\n\n // Fetch optimized price data\n const client = getClientForChain(chainId);\n const fetchedPriceData = await fetchPriceData(client, mintParams, info);\n\n // Update contract info with ERC20 details and claim data\n if (fetchedPriceData.erc20Details) {\n info.erc20Token = fetchedPriceData.erc20Details\n .address as `0x${string}`;\n info.erc20Symbol = fetchedPriceData.erc20Details.symbol;\n info.erc20Decimals = fetchedPriceData.erc20Details.decimals;\n }\n\n // Add claim data if available\n if (fetchedPriceData.claim) {\n info.claim = fetchedPriceData.claim;\n }\n\n dispatch({\n type: \"DETECT_SUCCESS\",\n payload: {\n contractInfo: info,\n priceData: {\n mintPrice: fetchedPriceData.mintPrice,\n totalCost: fetchedPriceData.totalCost,\n erc20Details: fetchedPriceData.erc20Details,\n },\n },\n });\n } catch (err) {\n dispatch({\n type: \"DETECT_ERROR\",\n payload: \"Failed to detect NFT contract type\",\n });\n }\n };\n\n // Check allowance only (without re-detecting everything)\n const checkAllowanceOnly = React.useCallback(async () => {\n if (!contractInfo || !erc20Details || !address) return;\n\n try {\n const client = getClientForChain(chainId);\n const spenderAddress =\n contractInfo.provider === \"manifold\" && contractInfo.extensionAddress\n ? contractInfo.extensionAddress\n : contractAddress;\n\n const allowance = await client.readContract({\n address: erc20Details.address as `0x${string}`,\n abi: [\n {\n name: \"allowance\",\n type: \"function\",\n inputs: [\n { name: \"owner\", type: \"address\" },\n { name: \"spender\", type: \"address\" },\n ],\n outputs: [{ type: \"uint256\" }],\n stateMutability: \"view\",\n },\n ],\n functionName: \"allowance\",\n args: [address, spenderAddress],\n });\n\n dispatch({ type: \"UPDATE_ALLOWANCE\", payload: allowance as bigint });\n } catch (err) {\n // Allowance check failed - will proceed without pre-checked allowance\n }\n }, [contractInfo, erc20Details, address, chainId, contractAddress]);\n\n // Re-check allowance after wallet connection\n React.useEffect(() => {\n if (\n isConnected &&\n address &&\n erc20Details &&\n erc20Details.allowance === undefined\n ) {\n checkAllowanceOnly();\n }\n }, [isConnected, address, erc20Details, checkAllowanceOnly]);\n\n const handleInitialMint = async () => {\n if (!isSDKLoaded) {\n dispatch({ type: \"TX_ERROR\", payload: \"Farcaster SDK not loaded\" });\n setIsSheetOpen(true);\n return;\n }\n\n setIsSheetOpen(true);\n await detectAndValidate();\n };\n\n const handleConnectWallet = async () => {\n try {\n dispatch({ type: \"CONNECT_START\" });\n const connector = farcasterFrame();\n connect({ connector });\n } catch (err) {\n handleError(err, \"Failed to connect wallet\");\n }\n };\n\n const handleApprove = async () => {\n if (!isConnected || !erc20Details || !contractInfo?.claim) {\n dispatch({\n type: \"TX_ERROR\",\n payload: \"Missing required information for approval\",\n });\n return;\n }\n\n try {\n dispatch({ type: \"APPROVE_START\" });\n\n // For Manifold, approve the extension contract, not the NFT contract\n const spenderAddress =\n contractInfo.provider === \"manifold\" && contractInfo.extensionAddress\n ? contractInfo.extensionAddress\n : contractAddress;\n\n // Approve exact amount needed\n await writeContract({\n address: erc20Details.address as `0x${string}`,\n abi: [\n {\n name: \"approve\",\n type: \"function\",\n inputs: [\n { name: \"spender\", type: \"address\" },\n { name: \"amount\", type: \"uint256\" },\n ],\n outputs: [{ type: \"bool\" }],\n stateMutability: \"nonpayable\",\n },\n ],\n functionName: \"approve\",\n args: [spenderAddress, contractInfo.claim.cost],\n chainId,\n });\n \n // The transaction has been initiated - we'll track it via writeData in the effect\n } catch (err) {\n handleError(err, \"Approval failed\", \"approval\");\n }\n };\n\n const handleMint = async () => {\n if (!isConnected) {\n await handleConnectWallet();\n return;\n }\n\n if (!contractInfo || !providerConfig) {\n dispatch({\n type: \"TX_ERROR\",\n payload: \"Contract information not available\",\n });\n return;\n }\n\n try {\n dispatch({ type: \"MINT_START\" });\n\n const args = providerConfig.mintConfig.buildArgs(mintParams);\n \n const value = priceData.totalCost || BigInt(0);\n\n // Handle Manifold's special case\n const mintAddress =\n contractInfo.provider === \"manifold\" && contractInfo.extensionAddress\n ? contractInfo.extensionAddress\n : contractAddress;\n\n \n // Prepare contract config based on provider type\n let contractConfig: any;\n \n contractConfig = {\n address: mintAddress,\n abi: providerConfig.mintConfig.abi,\n functionName: providerConfig.mintConfig.functionName,\n args,\n value,\n chainId,\n };\n\n // Execute the transaction\n await writeContract(contractConfig);\n \n // The transaction has been initiated - we'll track it via writeData in the effect\n } catch (err) {\n handleError(err, \"Mint transaction failed\", \"mint\");\n }\n };\n\n // Centralized error handler\n const handleError = (\n error: unknown,\n context: string,\n transactionType?: \"approval\" | \"mint\",\n ) => {\n console.error(`${context}:`, error);\n const message = error instanceof Error ? error.message : `${context}`;\n \n // Parse the error for better UX\n const parsed = parseError(error, transactionType || \"mint\");\n setParsedError(parsed);\n \n dispatch({ type: \"TX_ERROR\", payload: message });\n // Use explicit transaction type if provided, otherwise fall back to state\n if ((transactionType || txType) === \"mint\") {\n onMintError?.(message);\n }\n };\n\n const handleRetry = () => {\n dispatch({ type: \"RESET\" });\n detectAndValidate();\n };\n\n // Display helpers (quick win: centralized formatting)\n const formatPrice = (amount: bigint, decimals: number, symbol: string) => {\n if (amount === BigInt(0)) return \"Free\";\n return `${Number(amount) / 10 ** decimals} ${symbol}`;\n };\n\n const displayPrice = () => {\n if (erc20Details && contractInfo?.claim) {\n return formatPrice(\n contractInfo.claim.cost || BigInt(0),\n erc20Details.decimals || 18,\n erc20Details.symbol,\n );\n }\n return priceData.mintPrice\n ? `${formatEther(priceData.mintPrice)} ETH`\n : \"Free\";\n };\n\n const displayTotalCost = () => {\n if (erc20Details && contractInfo?.claim) {\n // For Manifold, cost is per claim, not per NFT amount\n return formatPrice(\n contractInfo.claim.cost || BigInt(0),\n erc20Details.decimals || 18,\n erc20Details.symbol,\n );\n }\n return priceData.totalCost\n ? `${formatEther(priceData.totalCost)} ETH`\n : \"Free\";\n };\n\n const displayMintFee = () => {\n const fee = priceData.mintPrice || BigInt(0);\n return fee > BigInt(0) ? `${formatEther(fee)} ETH` : \"0 ETH\";\n };\n\n const providerName = contractInfo?.provider\n ? contractInfo.provider.charAt(0).toUpperCase() +\n contractInfo.provider.slice(1)\n : \"Unknown\";\n\n // Quick win: validation helper\n const isReadyToMint = () => {\n return (\n isConnected &&\n contractInfo &&\n !isLoading &&\n step === \"sheet\" &&\n (!erc20Details || !erc20Details.needsApproval)\n );\n };\n\n return (\n {\n setIsSheetOpen(open);\n if (!open) {\n handleClose();\n }\n }}\n >\n \n \n {buttonText}\n \n\n \n \n \n {step === \"detecting\" && \"Detecting NFT Type\"}\n {step === \"sheet\" && \"Mint NFT\"}\n {step === \"connecting\" && \"Connecting Wallet\"}\n {step === \"approve\" && \"Approve Token\"}\n {step === \"approving\" && \"Approving...\"}\n {step === \"minting\" && \"Preparing Mint\"}\n {step === \"waiting\" &&\n (txType === \"approval\" ? \"Approving...\" : \"Minting...\")}\n {step === \"success\" && \"Mint Successful!\"}\n {step === \"error\" && (parsedError?.type === \"user-rejected\" ? \"Transaction Cancelled\" : \"Transaction Failed\")}\n {step === \"validation-error\" && \"Missing Information\"}\n \n \n\n {/* Detecting Provider */}\n {step === \"detecting\" && (\n
\n
\n \n
\n

\n Detecting NFT contract type...\n

\n
\n )}\n\n {/* Validation Error */}\n {step === \"validation-error\" && (\n
\n
\n \n
\n
\n

\n Missing Required Information\n

\n {validationErrors.map((err, idx) => (\n \n {err}\n

\n ))}\n
\n \n
\n )}\n\n {/* Approve Step */}\n {step === \"approve\" && erc20Details && (\n
\n
\n

Approval Required

\n

\n This NFT requires payment in {erc20Details.symbol}. You need to\n approve the contract to spend your tokens.\n

\n
\n
\n
\n Token\n {erc20Details.symbol}\n
\n
\n Amount to Approve\n \n {contractInfo?.claim\n ? Number(contractInfo.claim.cost) /\n 10 ** erc20Details.decimals\n : 0}{\" \"}\n {erc20Details.symbol}\n \n
\n
\n \n \n Approve {erc20Details.symbol}\n \n
\n )}\n\n {/* Main Sheet Content */}\n {step === \"sheet\" && contractInfo && (\n
\n {/* Network warning */}\n {!isCorrectNetwork && isConnected && (\n
\n
\n
\n \n

Wrong network

\n
\n \n Switch to {networkName}\n \n
\n
\n )}\n \n
\n
\n Provider\n {providerName}\n
\n
\n Contract\n \n {contractAddress.slice(0, 6)}...{contractAddress.slice(-4)}\n \n
\n
\n Quantity\n {amount}\n
\n
\n Price per NFT\n {displayPrice()}\n
\n {erc20Details && (\n
\n Mint Fee\n {displayMintFee()}\n
\n )}\n
\n Total Cost\n {displayTotalCost()}\n
\n
\n\n \n {isConnected ? (\n !isCorrectNetwork ? (\n <>\n \n Switch Network\n Switch Network to Mint\n \n ) : (\n <>\n \n Mint {amount} NFT{amount > 1 ? \"s\" : \"\"}\n \n )\n ) : (\n <>\n \n Connect\n Connect Wallet to Mint\n \n )}\n \n
\n )}\n\n {/* Connecting */}\n {step === \"connecting\" && (\n
\n
\n \n
\n

\n Connecting to your Farcaster wallet...\n

\n
\n )}\n\n {/* Minting/Approving */}\n {(step === \"minting\" || step === \"approving\") && (\n
\n
\n \n
\n
\n

\n {step === \"approving\"\n ? \"Preparing approval\"\n : \"Preparing mint transaction\"}\n

\n

\n Please approve the transaction in your wallet\n

\n
\n
\n )}\n\n {/* Waiting for Transaction */}\n {step === \"waiting\" && (\n
\n
\n \n
\n
\n

\n {txType === \"approval\"\n ? \"Approval submitted\"\n : \"Transaction submitted\"}\n

\n

\n Waiting for confirmation on the blockchain...\n

\n {txHash && (\n

\n {txHash.slice(0, 10)}...{txHash.slice(-8)}\n

\n )}\n
\n
\n )}\n\n {/* Success */}\n {step === \"success\" && (\n
\n
\n \n
\n
\n

Minted! 🎉

\n

\n {amount} NFT{amount > 1 ? \"s\" : \"\"} successfully minted\n

\n
\n {txHash && (\n
\n \n
\n )}\n \n
\n )}\n\n {/* Error State */}\n {step === \"error\" && (\n
\n
\n
\n
\n {parsedError?.type === \"user-rejected\" ? (\n \n ) : (\n \n )}\n
\n
\n
\n

\n {parsedError?.message || \"Transaction Failed\"}\n

\n {parsedError?.details && (\n

\n {parsedError.details}\n

\n )}\n
\n
\n\n {/* Special handling for wrong network */}\n {!isCorrectNetwork && isConnected && (\n
\n
\n \n
\n

Wrong Network

\n

\n Please switch to {networkName} to continue\n

\n
\n
\n \n Switch to {networkName}\n \n
\n )}\n\n {/* Specific error actions */}\n {parsedError?.type === \"insufficient-funds\" && (\n
\n
\n \n
\n

Insufficient Balance

\n

\n Make sure you have enough:\n

\n
    \n {erc20Details ? (\n <>\n
  • {erc20Details.symbol} for the NFT price
  • \n
  • ETH for gas fees
  • \n \n ) : (\n
  • ETH for both NFT price and gas fees
  • \n )}\n
\n
\n
\n
\n )}\n\n {/* Action buttons */}\n
\n \n Close\n \n \n
\n
\n )}\n \n \n );\n}\n\n/**\n * Preset builders for common NFT minting scenarios.\n * These provide type-safe, self-documenting ways to create NFTMintButton components.\n */\nNFTMintButton.presets = {\n /**\n * Create a basic ETH-based NFT mint\n * @example\n * ```tsx\n * \n * ```\n */\n generic: (props: {\n contractAddress: Address;\n chainId: 1 | 8453;\n amount?: number;\n buttonText?: string;\n onMintSuccess?: (txHash: string) => void;\n onMintError?: (error: string) => void;\n }): NFTMintFlowProps => ({\n ...props,\n provider: \"generic\",\n amount: props.amount || 1,\n }),\n\n /**\n * Create a Manifold NFT mint with proper configuration\n * @example\n * ```tsx\n * \n * ```\n */\n manifold: (props: {\n contractAddress: Address;\n chainId: 1 | 8453;\n instanceId: string;\n tokenId?: string;\n amount?: number;\n buttonText?: string;\n onMintSuccess?: (txHash: string) => void;\n onMintError?: (error: string) => void;\n }): NFTMintFlowProps => ({\n contractAddress: props.contractAddress,\n chainId: props.chainId,\n provider: \"manifold\",\n manifoldParams: {\n instanceId: props.instanceId,\n tokenId: props.tokenId,\n },\n amount: props.amount || 1,\n buttonText: props.buttonText,\n onMintSuccess: props.onMintSuccess,\n onMintError: props.onMintError,\n }),\n\n /**\n * Create an auto-detecting NFT mint (tries to figure out the provider)\n * @example\n * ```tsx\n * \n * ```\n */\n auto: (props: {\n contractAddress: Address;\n chainId: 1 | 8453;\n amount?: number;\n buttonText?: string;\n onMintSuccess?: (txHash: string) => void;\n onMintError?: (error: string) => void;\n }): NFTMintFlowProps => ({\n ...props,\n amount: props.amount || 1,\n }),\n};\n", + "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Button } from \"@/registry/mini-app/ui/button\";\nimport {\n Sheet,\n SheetContent,\n SheetHeader,\n SheetTitle,\n} from \"@/registry/mini-app/ui/sheet\";\nimport { useMiniAppSdk } from \"@/registry/mini-app/hooks/use-miniapp-sdk\";\nimport {\n useAccount,\n useConnect,\n useWaitForTransactionReceipt,\n useWriteContract,\n useSwitchChain,\n} from \"wagmi\";\nimport { formatEther, type Address } from \"viem\";\nimport { farcasterFrame } from \"@farcaster/miniapp-wagmi-connector\";\nimport {\n Coins,\n CheckCircle,\n AlertCircle,\n Loader2,\n Info,\n ExternalLink,\n RefreshCw,\n Wallet,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n detectNFTProvider,\n validateParameters,\n getClientForChain,\n} from \"@/registry/mini-app/blocks/nft-mint-flow/lib/provider-detector\";\nimport { getChainById } from \"@/registry/mini-app/lib/chains\";\nimport { getProviderConfig } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/provider-configs\";\nimport { fetchPriceData } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/price-optimizer\";\nimport { mintReducer, initialState, type MintStep } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/mint-reducer\";\nimport type { MintParams } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/types\";\nimport { parseError, type ParsedError } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/error-parser\";\n\n/**\n * NFTMintButton - Universal NFT minting button with automatic provider detection and ERC20 approval handling\n *\n * @example\n * ```tsx\n * // Basic ETH mint (auto-detects provider)\n * \n *\n * // Manifold NFT with ERC20 payment (HIGHER token)\n * \n *\n * // Multiple NFTs with custom button\n * console.log('Minted!', txHash)}\n * />\n * ```\n */\ntype NFTMintFlowProps = {\n /**\n * NFT contract address (0x...). This should be the main NFT contract, not the minting contract.\n * For Manifold, this is the creator contract, not the extension.\n */\n contractAddress: Address;\n\n /**\n * Blockchain network ID\n * - 1 = Ethereum mainnet\n * - 8453 = Base mainnet\n */\n chainId: 1 | 8453;\n\n /**\n * Optional provider hint. Use when:\n * - Auto-detection is failing\n * - You know the provider and want faster loading\n * - Testing specific provider flows\n *\n * Leave undefined for automatic detection.\n */\n provider?: \"manifold\" | \"opensea\" | \"zora\" | \"generic\";\n\n /**\n * Number of NFTs to mint. Defaults to 1.\n * Note: For ERC20 payments, the total cost is multiplied by this amount.\n */\n amount?: number;\n\n /**\n * Manifold-specific parameters. Required when provider=\"manifold\".\n * - instanceId: The claim instance ID from Manifold (required for most Manifold NFTs)\n * - tokenId: The specific token ID (required for some editions)\n *\n * Find these in the Manifold claim page URL or contract details.\n */\n manifoldParams?: {\n instanceId?: string;\n tokenId?: string;\n };\n\n // UI customization\n /** Additional CSS classes */\n className?: string;\n /** Button style variant */\n variant?: \"default\" | \"destructive\" | \"secondary\" | \"ghost\" | \"outline\";\n /** Button size */\n size?: \"default\" | \"sm\" | \"lg\" | \"icon\";\n /** Custom button text. Defaults to \"Mint NFT\" */\n buttonText?: string;\n /** Disable the mint button */\n disabled?: boolean;\n\n /**\n * Called when NFT minting succeeds (not on approval success)\n * @param txHash - The mint transaction hash (not approval tx)\n */\n onMintSuccess?: (txHash: string) => void;\n\n /**\n * Called when NFT minting fails (not on approval failure)\n * @param error - Human-readable error message\n */\n onMintError?: (error: string) => void;\n};\n\nexport function NFTMintButton({\n contractAddress,\n chainId,\n provider,\n amount = 1,\n manifoldParams,\n className,\n variant = \"default\",\n size = \"default\",\n buttonText = \"Mint NFT\",\n disabled = false,\n onMintSuccess,\n onMintError,\n}: NFTMintFlowProps) {\n const [state, dispatch] = React.useReducer(mintReducer, initialState);\n const [isSheetOpen, setIsSheetOpen] = React.useState(false);\n const [parsedError, setParsedError] = React.useState(null);\n\n // Prop validation with helpful errors\n React.useEffect(() => {\n if (\n provider === \"manifold\" &&\n !manifoldParams?.instanceId &&\n !manifoldParams?.tokenId\n ) {\n console.error(\n \"NFTMintFlow: When provider='manifold', you must provide manifoldParams with either instanceId or tokenId. \" +\n \"Example: manifoldParams={{ instanceId: '4293509360' }}\",\n );\n }\n\n if (manifoldParams && provider !== \"manifold\") {\n console.warn(\n \"NFTMintFlow: manifoldParams provided but provider is not 'manifold'. \" +\n \"Did you forget to set provider='manifold'?\",\n );\n }\n\n if (chainId !== 1 && chainId !== 8453) {\n console.warn(\n `NFTMintFlow: Chain ID ${chainId} may not be supported. ` +\n \"Currently tested chains: 1 (Ethereum), 8453 (Base)\",\n );\n }\n\n if (!contractAddress || !contractAddress.match(/^0x[a-fA-F0-9]{40}$/)) {\n console.error(\n \"NFTMintFlow: Invalid contract address. Must be a valid Ethereum address (0x...)\",\n );\n }\n }, [provider, manifoldParams, chainId, contractAddress]);\n\n // Destructure commonly used values\n const {\n step,\n contractInfo,\n priceData,\n error,\n txHash,\n txType,\n isLoading,\n validationErrors,\n } = state;\n const { erc20Details } = priceData;\n\n const { isSDKLoaded } = useMiniAppSdk();\n const { isConnected, address, chain } = useAccount();\n const { connect } = useConnect();\n const { switchChain } = useSwitchChain();\n const {\n writeContract,\n isPending: isWritePending,\n data: writeData,\n error: writeError,\n } = useWriteContract();\n\n // Build mint params\n const mintParams: MintParams = React.useMemo(\n () => ({\n contractAddress,\n chainId,\n provider,\n amount,\n instanceId: manifoldParams?.instanceId,\n tokenId: manifoldParams?.tokenId,\n recipient: address,\n }),\n [contractAddress, chainId, provider, amount, manifoldParams, address],\n );\n\n // Watch for transaction completion\n const {\n isSuccess: isTxSuccess,\n isError: isTxError,\n error: txError,\n } = useWaitForTransactionReceipt({\n hash: writeData,\n });\n\n // Get provider config\n const providerConfig = contractInfo\n ? getProviderConfig(contractInfo.provider, contractInfo)\n : null;\n\n // Check if user is on the correct network\n const isCorrectNetwork = chain?.id === chainId;\n const targetChain = getChainById(chainId);\n const networkName = targetChain.name || \"Unknown\";\n\n // Handle transaction status updates\n React.useEffect(() => {\n if (writeError) {\n const parsed = parseError(writeError, txType || \"mint\");\n \n // Show retry option for user rejections\n if (parsed.type === \"user-rejected\") {\n setParsedError(parsed);\n dispatch({ type: \"TX_ERROR\", payload: \"Transaction cancelled by user\" });\n return;\n }\n \n setParsedError(parsed);\n dispatch({ type: \"TX_ERROR\", payload: writeError.message });\n if (txType === \"mint\") {\n onMintError?.(writeError.message);\n }\n }\n if (isTxError && txError) {\n const parsed = parseError(txError, txType || \"mint\");\n setParsedError(parsed);\n dispatch({ type: \"TX_ERROR\", payload: txError.message });\n if (txType === \"mint\") {\n onMintError?.(txError.message);\n }\n }\n if (writeData && !isTxSuccess && !isTxError) {\n // Transaction submitted, waiting for confirmation\n if (txType === \"approval\") {\n dispatch({ type: \"APPROVE_TX_SUBMITTED\", payload: writeData });\n } else if (txType === \"mint\") {\n dispatch({ type: \"MINT_TX_SUBMITTED\", payload: writeData });\n }\n }\n if (isTxSuccess && writeData) {\n if (txType === \"approval\") {\n dispatch({ type: \"APPROVE_SUCCESS\" });\n } else if (txType === \"mint\") {\n dispatch({ type: \"TX_SUCCESS\", payload: writeData });\n onMintSuccess?.(writeData);\n }\n }\n }, [\n isTxSuccess,\n writeData,\n onMintSuccess,\n isTxError,\n txError,\n onMintError,\n writeError,\n txType,\n ]);\n\n const handleClose = React.useCallback(() => {\n setIsSheetOpen(false);\n dispatch({ type: \"RESET\" });\n setParsedError(null);\n }, []);\n\n // Auto-close on success after 10 seconds\n React.useEffect(() => {\n if (step === \"success\") {\n const timer = setTimeout(() => {\n handleClose();\n }, 10000);\n return () => clearTimeout(timer);\n }\n }, [step, handleClose]);\n\n const handleSwitchNetwork = async () => {\n try {\n await switchChain({ chainId });\n } catch (err) {\n // Network switch failed - user likely rejected or wallet doesn't support it\n }\n };\n\n // Detect NFT provider and validate\n const detectAndValidate = async () => {\n dispatch({ type: \"DETECT_START\" });\n\n try {\n // Detect provider\n const info = await detectNFTProvider(mintParams);\n\n // Validate parameters\n const validation = validateParameters(mintParams, info);\n\n if (!validation.isValid) {\n dispatch({ type: \"VALIDATION_ERROR\", payload: validation.errors });\n return;\n }\n\n // Fetch optimized price data\n const client = getClientForChain(chainId);\n const fetchedPriceData = await fetchPriceData(client, mintParams, info);\n\n // Update contract info with ERC20 details and claim data\n if (fetchedPriceData.erc20Details) {\n info.erc20Token = fetchedPriceData.erc20Details\n .address as `0x${string}`;\n info.erc20Symbol = fetchedPriceData.erc20Details.symbol;\n info.erc20Decimals = fetchedPriceData.erc20Details.decimals;\n }\n\n // Add claim data if available\n if (fetchedPriceData.claim) {\n info.claim = fetchedPriceData.claim;\n }\n\n dispatch({\n type: \"DETECT_SUCCESS\",\n payload: {\n contractInfo: info,\n priceData: {\n mintPrice: fetchedPriceData.mintPrice,\n totalCost: fetchedPriceData.totalCost,\n erc20Details: fetchedPriceData.erc20Details,\n },\n },\n });\n } catch (err) {\n dispatch({\n type: \"DETECT_ERROR\",\n payload: \"Failed to detect NFT contract type\",\n });\n }\n };\n\n // Check allowance only (without re-detecting everything)\n const checkAllowanceOnly = React.useCallback(async () => {\n if (!contractInfo || !erc20Details || !address) return;\n\n try {\n const client = getClientForChain(chainId);\n const spenderAddress =\n contractInfo.provider === \"manifold\" && contractInfo.extensionAddress\n ? contractInfo.extensionAddress\n : contractAddress;\n\n const allowance = await client.readContract({\n address: erc20Details.address as `0x${string}`,\n abi: [\n {\n name: \"allowance\",\n type: \"function\",\n inputs: [\n { name: \"owner\", type: \"address\" },\n { name: \"spender\", type: \"address\" },\n ],\n outputs: [{ type: \"uint256\" }],\n stateMutability: \"view\",\n },\n ],\n functionName: \"allowance\",\n args: [address, spenderAddress],\n });\n\n dispatch({ type: \"UPDATE_ALLOWANCE\", payload: allowance as bigint });\n } catch (err) {\n // Allowance check failed - will proceed without pre-checked allowance\n }\n }, [contractInfo, erc20Details, address, chainId, contractAddress]);\n\n // Re-check allowance after wallet connection\n React.useEffect(() => {\n if (\n isConnected &&\n address &&\n erc20Details &&\n erc20Details.allowance === undefined\n ) {\n checkAllowanceOnly();\n }\n }, [isConnected, address, erc20Details, checkAllowanceOnly]);\n\n const handleInitialMint = async () => {\n if (!isSDKLoaded) {\n dispatch({ type: \"TX_ERROR\", payload: \"Farcaster SDK not loaded\" });\n setIsSheetOpen(true);\n return;\n }\n\n setIsSheetOpen(true);\n await detectAndValidate();\n };\n\n const handleConnectWallet = async () => {\n try {\n dispatch({ type: \"CONNECT_START\" });\n const connector = farcasterFrame();\n connect({ connector });\n } catch (err) {\n handleError(err, \"Failed to connect wallet\");\n }\n };\n\n const handleApprove = async () => {\n if (!isConnected || !erc20Details || !contractInfo?.claim) {\n dispatch({\n type: \"TX_ERROR\",\n payload: \"Missing required information for approval\",\n });\n return;\n }\n\n try {\n dispatch({ type: \"APPROVE_START\" });\n\n // For Manifold, approve the extension contract, not the NFT contract\n const spenderAddress =\n contractInfo.provider === \"manifold\" && contractInfo.extensionAddress\n ? contractInfo.extensionAddress\n : contractAddress;\n\n // Approve exact amount needed\n await writeContract({\n address: erc20Details.address as `0x${string}`,\n abi: [\n {\n name: \"approve\",\n type: \"function\",\n inputs: [\n { name: \"spender\", type: \"address\" },\n { name: \"amount\", type: \"uint256\" },\n ],\n outputs: [{ type: \"bool\" }],\n stateMutability: \"nonpayable\",\n },\n ],\n functionName: \"approve\",\n args: [spenderAddress, contractInfo.claim.cost],\n chainId,\n });\n \n // The transaction has been initiated - we'll track it via writeData in the effect\n } catch (err) {\n handleError(err, \"Approval failed\", \"approval\");\n }\n };\n\n const handleMint = async () => {\n if (!isConnected) {\n await handleConnectWallet();\n return;\n }\n\n if (!contractInfo || !providerConfig) {\n dispatch({\n type: \"TX_ERROR\",\n payload: \"Contract information not available\",\n });\n return;\n }\n\n try {\n dispatch({ type: \"MINT_START\" });\n\n const args = providerConfig.mintConfig.buildArgs(mintParams);\n \n const value = priceData.totalCost || BigInt(0);\n\n // Handle Manifold's special case\n const mintAddress =\n contractInfo.provider === \"manifold\" && contractInfo.extensionAddress\n ? contractInfo.extensionAddress\n : contractAddress;\n\n \n // Prepare contract config based on provider type\n let contractConfig: any;\n \n contractConfig = {\n address: mintAddress,\n abi: providerConfig.mintConfig.abi,\n functionName: providerConfig.mintConfig.functionName,\n args,\n value,\n chainId,\n };\n\n // Execute the transaction\n await writeContract(contractConfig);\n \n // The transaction has been initiated - we'll track it via writeData in the effect\n } catch (err) {\n handleError(err, \"Mint transaction failed\", \"mint\");\n }\n };\n\n // Centralized error handler\n const handleError = (\n error: unknown,\n context: string,\n transactionType?: \"approval\" | \"mint\",\n ) => {\n console.error(`${context}:`, error);\n const message = error instanceof Error ? error.message : `${context}`;\n \n // Parse the error for better UX\n const parsed = parseError(error, transactionType || \"mint\");\n setParsedError(parsed);\n \n dispatch({ type: \"TX_ERROR\", payload: message });\n // Use explicit transaction type if provided, otherwise fall back to state\n if ((transactionType || txType) === \"mint\") {\n onMintError?.(message);\n }\n };\n\n const handleRetry = () => {\n dispatch({ type: \"RESET\" });\n detectAndValidate();\n };\n\n // Display helpers (quick win: centralized formatting)\n const formatPrice = (amount: bigint, decimals: number, symbol: string) => {\n if (amount === BigInt(0)) return \"Free\";\n return `${Number(amount) / 10 ** decimals} ${symbol}`;\n };\n\n const displayPrice = () => {\n if (erc20Details && contractInfo?.claim) {\n return formatPrice(\n contractInfo.claim.cost || BigInt(0),\n erc20Details.decimals || 18,\n erc20Details.symbol,\n );\n }\n return priceData.mintPrice\n ? `${formatEther(priceData.mintPrice)} ETH`\n : \"Free\";\n };\n\n const displayTotalCost = () => {\n if (erc20Details && contractInfo?.claim) {\n // For Manifold, cost is per claim, not per NFT amount\n return formatPrice(\n contractInfo.claim.cost || BigInt(0),\n erc20Details.decimals || 18,\n erc20Details.symbol,\n );\n }\n return priceData.totalCost\n ? `${formatEther(priceData.totalCost)} ETH`\n : \"Free\";\n };\n\n const displayMintFee = () => {\n const fee = priceData.mintPrice || BigInt(0);\n return fee > BigInt(0) ? `${formatEther(fee)} ETH` : \"0 ETH\";\n };\n\n const providerName = contractInfo?.provider\n ? contractInfo.provider.charAt(0).toUpperCase() +\n contractInfo.provider.slice(1)\n : \"Unknown\";\n\n // Quick win: validation helper\n const isReadyToMint = () => {\n return (\n isConnected &&\n contractInfo &&\n !isLoading &&\n step === \"sheet\" &&\n (!erc20Details || !erc20Details.needsApproval)\n );\n };\n\n return (\n {\n setIsSheetOpen(open);\n if (!open) {\n handleClose();\n }\n }}\n >\n \n \n {buttonText}\n \n\n \n \n \n {step === \"detecting\" && \"Detecting NFT Type\"}\n {step === \"sheet\" && \"Mint NFT\"}\n {step === \"connecting\" && \"Connecting Wallet\"}\n {step === \"approve\" && \"Approve Token\"}\n {step === \"approving\" && \"Approving...\"}\n {step === \"minting\" && \"Preparing Mint\"}\n {step === \"waiting\" &&\n (txType === \"approval\" ? \"Approving...\" : \"Minting...\")}\n {step === \"success\" && \"Mint Successful!\"}\n {step === \"error\" && (parsedError?.type === \"user-rejected\" ? \"Transaction Cancelled\" : \"Transaction Failed\")}\n {step === \"validation-error\" && \"Missing Information\"}\n \n \n\n {/* Detecting Provider */}\n {step === \"detecting\" && (\n
\n
\n \n
\n

\n Detecting NFT contract type...\n

\n
\n )}\n\n {/* Validation Error */}\n {step === \"validation-error\" && (\n
\n
\n \n
\n
\n

\n Missing Required Information\n

\n {validationErrors.map((err, idx) => (\n \n {err}\n

\n ))}\n
\n \n
\n )}\n\n {/* Approve Step */}\n {step === \"approve\" && erc20Details && (\n
\n
\n

Approval Required

\n

\n This NFT requires payment in {erc20Details.symbol}. You need to\n approve the contract to spend your tokens.\n

\n
\n
\n
\n Token\n {erc20Details.symbol}\n
\n
\n Amount to Approve\n \n {contractInfo?.claim\n ? Number(contractInfo.claim.cost) /\n 10 ** erc20Details.decimals\n : 0}{\" \"}\n {erc20Details.symbol}\n \n
\n
\n \n \n Approve {erc20Details.symbol}\n \n
\n )}\n\n {/* Main Sheet Content */}\n {step === \"sheet\" && contractInfo && (\n
\n {/* Network warning */}\n {!isCorrectNetwork && isConnected && (\n
\n
\n
\n \n

Wrong network

\n
\n \n Switch to {networkName}\n \n
\n
\n )}\n \n
\n
\n Provider\n {providerName}\n
\n
\n Contract\n \n {contractAddress.slice(0, 6)}...{contractAddress.slice(-4)}\n \n
\n
\n Quantity\n {amount}\n
\n
\n Price per NFT\n {displayPrice()}\n
\n {erc20Details && (\n
\n Mint Fee\n {displayMintFee()}\n
\n )}\n
\n Total Cost\n {displayTotalCost()}\n
\n
\n\n \n {isConnected ? (\n !isCorrectNetwork ? (\n <>\n \n Switch Network\n Switch Network to Mint\n \n ) : (\n <>\n \n Mint {amount} NFT{amount > 1 ? \"s\" : \"\"}\n \n )\n ) : (\n <>\n \n Connect\n Connect Wallet to Mint\n \n )}\n \n
\n )}\n\n {/* Connecting */}\n {step === \"connecting\" && (\n
\n
\n \n
\n

\n Connecting to your Farcaster wallet...\n

\n
\n )}\n\n {/* Minting/Approving */}\n {(step === \"minting\" || step === \"approving\") && (\n
\n
\n \n
\n
\n

\n {step === \"approving\"\n ? \"Preparing approval\"\n : \"Preparing mint transaction\"}\n

\n

\n Please approve the transaction in your wallet\n

\n
\n
\n )}\n\n {/* Waiting for Transaction */}\n {step === \"waiting\" && (\n
\n
\n \n
\n
\n

\n {txType === \"approval\"\n ? \"Approval submitted\"\n : \"Transaction submitted\"}\n

\n

\n Waiting for confirmation on the blockchain...\n

\n {txHash && (\n

\n {txHash.slice(0, 10)}...{txHash.slice(-8)}\n

\n )}\n
\n
\n )}\n\n {/* Success */}\n {step === \"success\" && (\n
\n
\n \n
\n
\n

Minted! 🎉

\n

\n {amount} NFT{amount > 1 ? \"s\" : \"\"} successfully minted\n

\n
\n {txHash && (\n
\n \n
\n )}\n \n
\n )}\n\n {/* Error State */}\n {step === \"error\" && (\n
\n
\n
\n
\n {parsedError?.type === \"user-rejected\" ? (\n \n ) : (\n \n )}\n
\n
\n
\n

\n {parsedError?.message || \"Transaction Failed\"}\n

\n {parsedError?.details && (\n

\n {parsedError.details}\n

\n )}\n
\n
\n\n {/* Special handling for wrong network */}\n {!isCorrectNetwork && isConnected && (\n
\n
\n \n
\n

Wrong Network

\n

\n Please switch to {networkName} to continue\n

\n
\n
\n \n Switch to {networkName}\n \n
\n )}\n\n {/* Specific error actions */}\n {parsedError?.type === \"insufficient-funds\" && (\n
\n
\n \n
\n

Insufficient Balance

\n

\n Make sure you have enough:\n

\n
    \n {erc20Details ? (\n <>\n
  • {erc20Details.symbol} for the NFT price
  • \n
  • ETH for gas fees
  • \n \n ) : (\n
  • ETH for both NFT price and gas fees
  • \n )}\n
\n
\n
\n
\n )}\n\n {/* Action buttons */}\n
\n \n Close\n \n \n
\n
\n )}\n \n \n );\n}\n\n/**\n * Preset builders for common NFT minting scenarios.\n * These provide type-safe, self-documenting ways to create NFTMintButton components.\n */\nNFTMintButton.presets = {\n /**\n * Create a basic ETH-based NFT mint\n * @example\n * ```tsx\n * \n * ```\n */\n generic: (props: {\n contractAddress: Address;\n chainId: 1 | 8453;\n amount?: number;\n buttonText?: string;\n onMintSuccess?: (txHash: string) => void;\n onMintError?: (error: string) => void;\n }): NFTMintFlowProps => ({\n ...props,\n provider: \"generic\",\n amount: props.amount || 1,\n }),\n\n /**\n * Create a Manifold NFT mint with proper configuration\n * @example\n * ```tsx\n * \n * ```\n */\n manifold: (props: {\n contractAddress: Address;\n chainId: 1 | 8453;\n instanceId: string;\n tokenId?: string;\n amount?: number;\n buttonText?: string;\n onMintSuccess?: (txHash: string) => void;\n onMintError?: (error: string) => void;\n }): NFTMintFlowProps => ({\n contractAddress: props.contractAddress,\n chainId: props.chainId,\n provider: \"manifold\",\n manifoldParams: {\n instanceId: props.instanceId,\n tokenId: props.tokenId,\n },\n amount: props.amount || 1,\n buttonText: props.buttonText,\n onMintSuccess: props.onMintSuccess,\n onMintError: props.onMintError,\n }),\n\n /**\n * Create an auto-detecting NFT mint (tries to figure out the provider)\n * @example\n * ```tsx\n * \n * ```\n */\n auto: (props: {\n contractAddress: Address;\n chainId: 1 | 8453;\n amount?: number;\n buttonText?: string;\n onMintSuccess?: (txHash: string) => void;\n onMintError?: (error: string) => void;\n }): NFTMintFlowProps => ({\n ...props,\n amount: props.amount || 1,\n }),\n};\n", "type": "registry:component" }, { "path": "registry/mini-app/blocks/nft-mint-flow/lib/types.ts", - "content": "import type { Address } from \"viem\";\n\nexport type NFTProvider = \"manifold\" | \"opensea\" | \"zora\" | \"generic\" | \"nfts2me\";\n\nexport interface ProviderConfig {\n name: NFTProvider;\n detectPattern?: RegExp;\n extensionAddresses?: Address[];\n priceDiscovery: PriceDiscoveryConfig;\n mintConfig: MintConfig;\n requiredParams: string[];\n supportsERC20: boolean;\n}\n\nexport interface PriceDiscoveryConfig {\n abis: any[];\n functionNames: string[];\n requiresInstanceId?: boolean;\n requiresAmountParam?: boolean;\n}\n\nexport interface MintConfig {\n abi: any;\n functionName: string;\n buildArgs: (params: MintParams) => any[];\n calculateValue: (price: bigint, params: MintParams) => bigint;\n}\n\nexport interface MintParams {\n contractAddress: Address;\n chainId: number;\n provider?: NFTProvider;\n amount?: number;\n instanceId?: string;\n tokenId?: string;\n recipient?: Address;\n merkleProof?: string[];\n}\n\nexport interface NFTContractInfo {\n provider: NFTProvider;\n isERC1155: boolean;\n isERC721: boolean;\n extensionAddress?: Address;\n hasManifoldExtension?: boolean;\n mintPrice?: bigint;\n erc20Token?: Address;\n erc20Symbol?: string;\n erc20Decimals?: number;\n claim?: {\n cost: bigint;\n merkleRoot: `0x${string}`;\n erc20: Address;\n startDate: number;\n endDate: number;\n walletMax: number;\n };\n}\n\nexport interface ValidationResult {\n isValid: boolean;\n missingParams: string[];\n errors: string[];\n warnings: string[];\n}", + "content": "import type { Address } from \"viem\";\n\nexport type NFTProvider = \"manifold\" | \"opensea\" | \"zora\" | \"generic\" | \"nfts2me\" | \"thirdweb\";\n\nexport interface ProviderConfig {\n name: NFTProvider;\n detectPattern?: RegExp;\n extensionAddresses?: Address[];\n priceDiscovery: PriceDiscoveryConfig;\n mintConfig: MintConfig;\n requiredParams: string[];\n supportsERC20: boolean;\n}\n\nexport interface PriceDiscoveryConfig {\n abis: any[];\n functionNames: string[];\n requiresInstanceId?: boolean;\n requiresAmountParam?: boolean;\n}\n\nexport interface MintConfig {\n abi: any;\n functionName: string;\n buildArgs: (params: MintParams) => any[];\n calculateValue: (price: bigint, params: MintParams) => bigint;\n}\n\nexport interface MintParams {\n contractAddress: Address;\n chainId: number;\n provider?: NFTProvider;\n amount?: number;\n instanceId?: string;\n tokenId?: string;\n recipient?: Address;\n merkleProof?: string[];\n}\n\nexport interface NFTContractInfo {\n provider: NFTProvider;\n isERC1155: boolean;\n isERC721: boolean;\n extensionAddress?: Address;\n hasManifoldExtension?: boolean;\n mintPrice?: bigint;\n erc20Token?: Address;\n erc20Symbol?: string;\n erc20Decimals?: number;\n claim?: {\n cost: bigint;\n merkleRoot: `0x${string}`;\n erc20: Address;\n startDate: number;\n endDate: number;\n walletMax: number;\n };\n claimCondition?: {\n id: number;\n pricePerToken: bigint;\n currency: Address;\n maxClaimableSupply: bigint;\n merkleRoot: `0x${string}`;\n startTimestamp: number;\n quantityLimitPerWallet: bigint;\n };\n}\n\nexport interface ValidationResult {\n isValid: boolean;\n missingParams: string[];\n errors: string[];\n warnings: string[];\n}", "type": "registry:lib" }, { "path": "registry/mini-app/blocks/nft-mint-flow/lib/provider-configs.ts", - "content": "import { type Address } from \"viem\";\nimport type { ProviderConfig } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/types\";\nimport { MANIFOLD_EXTENSION_ABI, KNOWN_CONTRACTS, PRICE_DISCOVERY_ABI, MINT_ABI } from \"@/registry/mini-app/lib/nft-standards\";\n\nexport const PROVIDER_CONFIGS: Record = {\n manifold: {\n name: \"manifold\",\n extensionAddresses: [\n KNOWN_CONTRACTS.manifoldExtension, // Known Manifold extension\n ],\n priceDiscovery: {\n abis: [MANIFOLD_EXTENSION_ABI],\n functionNames: [\"MINT_FEE\"],\n requiresInstanceId: true\n },\n mintConfig: {\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"mint\",\n buildArgs: (params) => [\n params.contractAddress,\n BigInt(params.instanceId || \"0\"),\n Number(params.tokenId || \"0\"),\n params.merkleProof || [],\n params.recipient\n ],\n calculateValue: (mintFee, params) => {\n // For Manifold, value is just the mint fee\n // The actual NFT cost might be in ERC20\n return mintFee;\n }\n },\n requiredParams: [\"contractAddress\", \"chainId\"],\n supportsERC20: true\n },\n \n opensea: {\n name: \"opensea\",\n priceDiscovery: {\n abis: [PRICE_DISCOVERY_ABI],\n functionNames: [\"mintPrice\", \"price\", \"publicMintPrice\"]\n },\n mintConfig: {\n abi: MINT_ABI,\n functionName: \"mint\",\n buildArgs: (params) => [BigInt(params.amount || 1)],\n calculateValue: (price, params) => price * BigInt(params.amount || 1)\n },\n requiredParams: [\"contractAddress\", \"chainId\"],\n supportsERC20: false\n },\n\n zora: {\n name: \"zora\",\n priceDiscovery: {\n abis: [PRICE_DISCOVERY_ABI],\n functionNames: [\"mintPrice\", \"price\"]\n },\n mintConfig: {\n abi: MINT_ABI,\n functionName: \"mint\",\n buildArgs: (params) => [params.recipient, BigInt(params.amount || 1)],\n calculateValue: (price, params) => price * BigInt(params.amount || 1)\n },\n requiredParams: [\"contractAddress\", \"chainId\"],\n supportsERC20: false\n },\n\n generic: {\n name: \"generic\",\n priceDiscovery: {\n abis: [PRICE_DISCOVERY_ABI],\n functionNames: [\"mintPrice\", \"price\", \"MINT_PRICE\", \"getMintPrice\"]\n },\n mintConfig: {\n abi: MINT_ABI,\n functionName: \"mint\",\n buildArgs: (params) => [BigInt(params.amount || 1)],\n calculateValue: (price, params) => price * BigInt(params.amount || 1)\n },\n requiredParams: [\"contractAddress\", \"chainId\"],\n supportsERC20: false\n },\n\n nfts2me: {\n name: \"nfts2me\",\n priceDiscovery: {\n // For nfts2me, we need a custom ABI with mintFee function\n abis: [[{\n inputs: [{ name: \"amount\", type: \"uint256\" }],\n name: \"mintFee\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }]],\n functionNames: [\"mintFee\"],\n // Custom logic to handle mintFee with amount parameter\n requiresAmountParam: true\n },\n mintConfig: {\n // NFTs2Me mint(amount) expects the number of NFTs to mint\n // For minting 1 NFT, pass 1. Payment is via msg.value.\n abi: [{\n inputs: [{ name: \"amount\", type: \"uint256\" }],\n name: \"mint\",\n outputs: [],\n stateMutability: \"payable\",\n type: \"function\"\n }],\n functionName: \"mint\",\n buildArgs: (params) => [BigInt(params.amount || 1)],\n calculateValue: (price, params) => price * BigInt(params.amount || 1)\n },\n requiredParams: [\"contractAddress\", \"chainId\"],\n supportsERC20: false\n }\n};\n\n// Helper to get config by provider name\nexport function getProviderConfig(provider: string): ProviderConfig {\n return PROVIDER_CONFIGS[provider] || PROVIDER_CONFIGS.generic;\n}", + "content": "import { type Address, maxUint256 } from \"viem\";\nimport type { ProviderConfig } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/types\";\nimport { MANIFOLD_EXTENSION_ABI, KNOWN_CONTRACTS, PRICE_DISCOVERY_ABI, MINT_ABI, THIRDWEB_OPENEDITONERC721_ABI, THIRDWEB_NATIVE_TOKEN } from \"@/registry/mini-app/lib/nft-standards\";\n\nexport const PROVIDER_CONFIGS: Record = {\n manifold: {\n name: \"manifold\",\n extensionAddresses: [\n KNOWN_CONTRACTS.manifoldExtension, // Known Manifold extension\n ],\n priceDiscovery: {\n abis: [MANIFOLD_EXTENSION_ABI],\n functionNames: [\"MINT_FEE\"],\n requiresInstanceId: true\n },\n mintConfig: {\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"mint\",\n buildArgs: (params) => [\n params.contractAddress,\n BigInt(params.instanceId || \"0\"),\n Number(params.tokenId || \"0\"),\n params.merkleProof || [],\n params.recipient\n ],\n calculateValue: (mintFee, params) => {\n // For Manifold, value is just the mint fee\n // The actual NFT cost might be in ERC20\n return mintFee;\n }\n },\n requiredParams: [\"contractAddress\", \"chainId\"],\n supportsERC20: true\n },\n \n opensea: {\n name: \"opensea\",\n priceDiscovery: {\n abis: [PRICE_DISCOVERY_ABI],\n functionNames: [\"mintPrice\", \"price\", \"publicMintPrice\"]\n },\n mintConfig: {\n abi: MINT_ABI,\n functionName: \"mint\",\n buildArgs: (params) => [BigInt(params.amount || 1)],\n calculateValue: (price, params) => price * BigInt(params.amount || 1)\n },\n requiredParams: [\"contractAddress\", \"chainId\"],\n supportsERC20: false\n },\n\n zora: {\n name: \"zora\",\n priceDiscovery: {\n abis: [PRICE_DISCOVERY_ABI],\n functionNames: [\"mintPrice\", \"price\"]\n },\n mintConfig: {\n abi: MINT_ABI,\n functionName: \"mint\",\n buildArgs: (params) => [params.recipient, BigInt(params.amount || 1)],\n calculateValue: (price, params) => price * BigInt(params.amount || 1)\n },\n requiredParams: [\"contractAddress\", \"chainId\"],\n supportsERC20: false\n },\n\n generic: {\n name: \"generic\",\n priceDiscovery: {\n abis: [PRICE_DISCOVERY_ABI],\n functionNames: [\"mintPrice\", \"price\", \"MINT_PRICE\", \"getMintPrice\"]\n },\n mintConfig: {\n abi: MINT_ABI,\n functionName: \"mint\",\n buildArgs: (params) => [BigInt(params.amount || 1)],\n calculateValue: (price, params) => price * BigInt(params.amount || 1)\n },\n requiredParams: [\"contractAddress\", \"chainId\"],\n supportsERC20: false\n },\n\n nfts2me: {\n name: \"nfts2me\",\n priceDiscovery: {\n // For nfts2me, we need a custom ABI with mintFee function\n abis: [[{\n inputs: [{ name: \"amount\", type: \"uint256\" }],\n name: \"mintFee\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }]],\n functionNames: [\"mintFee\"],\n // Custom logic to handle mintFee with amount parameter\n requiresAmountParam: true\n },\n mintConfig: {\n // NFTs2Me mint(amount) expects the number of NFTs to mint\n // For minting 1 NFT, pass 1. Payment is via msg.value.\n abi: [{\n inputs: [{ name: \"amount\", type: \"uint256\" }],\n name: \"mint\",\n outputs: [],\n stateMutability: \"payable\",\n type: \"function\"\n }],\n functionName: \"mint\",\n buildArgs: (params) => [BigInt(params.amount || 1)],\n calculateValue: (price, params) => price * BigInt(params.amount || 1)\n },\n requiredParams: [\"contractAddress\", \"chainId\"],\n supportsERC20: false\n },\n\n thirdweb: {\n name: \"thirdweb\",\n priceDiscovery: {\n abis: [THIRDWEB_OPENEDITONERC721_ABI],\n functionNames: [\"claimCondition\", \"getClaimConditionById\"],\n requiresInstanceId: false\n },\n mintConfig: {\n abi: THIRDWEB_OPENEDITONERC721_ABI,\n functionName: \"claim\",\n buildArgs: (params) => {\n // This will be overridden by getProviderConfig for thirdweb\n // when contractInfo with claimCondition is available\n return [\n params.recipient || params.contractAddress, // _receiver\n BigInt(params.amount || 1), // _quantity\n THIRDWEB_NATIVE_TOKEN, // _currency (default to ETH)\n BigInt(0), // _pricePerToken (default to 0)\n {\n proof: params.merkleProof || [],\n quantityLimitPerWallet: maxUint256,\n pricePerToken: maxUint256,\n currency: \"0x0000000000000000000000000000000000000000\"\n }, // _allowlistProof\n \"0x\" // _data\n ];\n },\n calculateValue: (price, params) => {\n // This will be overridden by getProviderConfig for thirdweb\n // Default assumes native token payment\n return price * BigInt(params.amount || 1);\n }\n },\n requiredParams: [\"contractAddress\", \"chainId\"],\n supportsERC20: true\n }\n};\n\n// Helper to get config by provider name\nexport function getProviderConfig(provider: string, contractInfo?: any): ProviderConfig {\n const baseConfig = PROVIDER_CONFIGS[provider] || PROVIDER_CONFIGS.generic;\n \n // For thirdweb, we need to inject the claim condition data\n if (provider === \"thirdweb\" && contractInfo?.claimCondition) {\n return {\n ...baseConfig,\n mintConfig: {\n ...baseConfig.mintConfig,\n buildArgs: (params) => {\n const pricePerToken = contractInfo.claimCondition.pricePerToken || BigInt(0);\n const currency = contractInfo.claimCondition.currency || THIRDWEB_NATIVE_TOKEN;\n \n return [\n params.recipient || params.contractAddress, // _receiver\n BigInt(params.amount || 1), // _quantity\n currency, // _currency\n pricePerToken, // _pricePerToken\n {\n proof: params.merkleProof || [],\n quantityLimitPerWallet: maxUint256,\n pricePerToken: maxUint256,\n currency: \"0x0000000000000000000000000000000000000000\"\n }, // _allowlistProof\n \"0x\" // _data\n ];\n },\n calculateValue: (price, params) => {\n const currency = contractInfo.claimCondition?.currency;\n \n if (!currency || currency.toLowerCase() === THIRDWEB_NATIVE_TOKEN.toLowerCase()) {\n return price * BigInt(params.amount || 1);\n }\n return BigInt(0);\n }\n }\n };\n }\n \n return baseConfig;\n}", "type": "registry:lib" }, { "path": "registry/mini-app/blocks/nft-mint-flow/lib/provider-detector.ts", - "content": "import { type Address, type PublicClient } from \"viem\";\nimport type { NFTProvider, NFTContractInfo, MintParams } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/types\";\nimport { PROVIDER_CONFIGS } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/provider-configs\";\nimport { getPublicClient } from \"@/registry/mini-app/lib/chains\";\nimport { \n ERC165_ABI, \n INTERFACE_IDS, \n MANIFOLD_DETECTION_ABI \n} from \"@/registry/mini-app/lib/nft-standards\";\n\n// Re-export from shared library for backward compatibility\nexport const getClientForChain = getPublicClient;\n\n/**\n * Detects NFT provider and contract info with minimal RPC calls\n * Uses multicall where possible to batch requests\n */\nexport async function detectNFTProvider(params: MintParams): Promise {\n const { contractAddress, chainId, provider: specifiedProvider } = params;\n const client = getClientForChain(chainId);\n\n // If provider is specified, use known configuration\n if (specifiedProvider) {\n const config = PROVIDER_CONFIGS[specifiedProvider];\n \n // For Manifold, we know the extension address\n if (specifiedProvider === \"manifold\" && config.extensionAddresses?.[0]) {\n return {\n provider: \"manifold\",\n isERC1155: true, // Manifold contracts are typically ERC1155\n isERC721: false,\n extensionAddress: config.extensionAddresses[0],\n hasManifoldExtension: true\n };\n }\n \n // For other providers, return basic info\n return {\n provider: specifiedProvider,\n isERC1155: false,\n isERC721: false\n };\n }\n\n try {\n // Batch 1: Check interfaces and Manifold extensions in parallel\n const [isERC721, isERC1155, extensions] = await Promise.all([\n client.readContract({\n address: contractAddress,\n abi: ERC165_ABI,\n functionName: \"supportsInterface\",\n args: [INTERFACE_IDS.ERC721]\n }).catch(() => false),\n \n client.readContract({\n address: contractAddress,\n abi: ERC165_ABI,\n functionName: \"supportsInterface\",\n args: [INTERFACE_IDS.ERC1155]\n }).catch(() => false),\n \n client.readContract({\n address: contractAddress,\n abi: MANIFOLD_DETECTION_ABI,\n functionName: \"getExtensions\"\n }).catch(() => null)\n ]);\n\n // Check if it's a Manifold contract\n if (extensions && extensions.length > 0) {\n const knownManifoldExtension = extensions.find(ext => \n PROVIDER_CONFIGS.manifold.extensionAddresses?.includes(ext)\n );\n \n if (knownManifoldExtension || extensions.length > 0) {\n return {\n provider: \"manifold\",\n isERC1155: isERC1155 as boolean,\n isERC721: isERC721 as boolean,\n extensionAddress: knownManifoldExtension || extensions[0],\n hasManifoldExtension: true\n };\n }\n }\n\n // Check if it's an NFTs2Me contract by looking for unique functions\n try {\n // Try to call n2mVersion - this is unique to NFTs2Me contracts\n const version = await client.readContract({\n address: contractAddress,\n abi: [{\n inputs: [],\n name: \"n2mVersion\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"pure\",\n type: \"function\"\n }],\n functionName: \"n2mVersion\"\n });\n \n // If n2mVersion exists, it's an NFTs2Me contract\n if (version !== undefined) {\n return {\n provider: \"nfts2me\",\n isERC1155: isERC1155 as boolean,\n isERC721: isERC721 as boolean\n };\n }\n } catch {\n // Not an NFTs2Me contract, continue detection\n }\n\n // TODO: Add detection for OpenSea, Zora, etc.\n // For now, return generic\n return {\n provider: \"generic\",\n isERC1155: isERC1155 as boolean,\n isERC721: isERC721 as boolean\n };\n\n } catch (error) {\n console.error(\"Error detecting NFT provider:\", error);\n // Default to generic provider\n return {\n provider: \"generic\",\n isERC1155: false,\n isERC721: false\n };\n }\n}\n\n/**\n * Validates parameters based on detected provider\n */\nexport function validateParameters(params: MintParams, contractInfo: NFTContractInfo): {\n isValid: boolean;\n missingParams: string[];\n errors: string[];\n} {\n const config = PROVIDER_CONFIGS[contractInfo.provider];\n const missingParams: string[] = [];\n const errors: string[] = [];\n\n // Check required params for the provider\n for (const param of config.requiredParams) {\n if (!params[param as keyof MintParams]) {\n missingParams.push(param);\n }\n }\n\n // Provider-specific validation\n if (contractInfo.provider === \"manifold\") {\n if (!params.instanceId && !params.tokenId) {\n errors.push(\"Manifold NFTs require either instanceId or tokenId\");\n missingParams.push(\"instanceId or tokenId\");\n }\n \n if (contractInfo.claim?.merkleRoot && contractInfo.claim.merkleRoot !== \"0x0000000000000000000000000000000000000000000000000000000000000000\") {\n errors.push(\"This NFT requires a merkle proof for minting - not supported yet\");\n }\n }\n\n return {\n isValid: missingParams.length === 0 && errors.length === 0,\n missingParams,\n errors\n };\n}", + "content": "import { type Address, type PublicClient } from \"viem\";\nimport type { NFTProvider, NFTContractInfo, MintParams } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/types\";\nimport { PROVIDER_CONFIGS } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/provider-configs\";\nimport { getPublicClient } from \"@/registry/mini-app/lib/chains\";\nimport { \n ERC165_ABI, \n INTERFACE_IDS, \n MANIFOLD_DETECTION_ABI,\n THIRDWEB_OPENEDITONERC721_ABI\n} from \"@/registry/mini-app/lib/nft-standards\";\n\n// Re-export from shared library for backward compatibility\nexport const getClientForChain = getPublicClient;\n\n/**\n * Detects NFT provider and contract info with minimal RPC calls\n * Uses multicall where possible to batch requests\n */\nexport async function detectNFTProvider(params: MintParams): Promise {\n const { contractAddress, chainId, provider: specifiedProvider } = params;\n const client = getClientForChain(chainId);\n\n // If provider is specified, use known configuration\n if (specifiedProvider) {\n const config = PROVIDER_CONFIGS[specifiedProvider];\n \n // For Manifold, we know the extension address\n if (specifiedProvider === \"manifold\" && config.extensionAddresses?.[0]) {\n return {\n provider: \"manifold\",\n isERC1155: true, // Manifold contracts are typically ERC1155\n isERC721: false,\n extensionAddress: config.extensionAddresses[0],\n hasManifoldExtension: true\n };\n }\n \n // For other providers, return basic info\n return {\n provider: specifiedProvider,\n isERC1155: false,\n isERC721: false\n };\n }\n\n try {\n // Batch 1: Check interfaces and Manifold extensions in parallel\n const [isERC721, isERC1155, extensions] = await Promise.all([\n client.readContract({\n address: contractAddress,\n abi: ERC165_ABI,\n functionName: \"supportsInterface\",\n args: [INTERFACE_IDS.ERC721]\n }).catch(() => false),\n \n client.readContract({\n address: contractAddress,\n abi: ERC165_ABI,\n functionName: \"supportsInterface\",\n args: [INTERFACE_IDS.ERC1155]\n }).catch(() => false),\n \n client.readContract({\n address: contractAddress,\n abi: MANIFOLD_DETECTION_ABI,\n functionName: \"getExtensions\"\n }).catch(() => null)\n ]);\n\n // Check if it's a Manifold contract\n if (extensions && extensions.length > 0) {\n const knownManifoldExtension = extensions.find(ext => \n PROVIDER_CONFIGS.manifold.extensionAddresses?.includes(ext)\n );\n \n if (knownManifoldExtension || extensions.length > 0) {\n return {\n provider: \"manifold\",\n isERC1155: isERC1155 as boolean,\n isERC721: isERC721 as boolean,\n extensionAddress: knownManifoldExtension || extensions[0],\n hasManifoldExtension: true\n };\n }\n }\n\n // Check if it's an NFTs2Me contract by looking for unique functions\n try {\n // Try to call n2mVersion - this is unique to NFTs2Me contracts\n const version = await client.readContract({\n address: contractAddress,\n abi: [{\n inputs: [],\n name: \"n2mVersion\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"pure\",\n type: \"function\"\n }],\n functionName: \"n2mVersion\"\n });\n \n // If n2mVersion exists, it's an NFTs2Me contract\n if (version !== undefined) {\n return {\n provider: \"nfts2me\",\n isERC1155: isERC1155 as boolean,\n isERC721: isERC721 as boolean\n };\n }\n } catch {\n // Not an NFTs2Me contract, continue detection\n }\n\n // Check if it's a thirdweb OpenEditionERC721 contract\n try {\n // Try to call claimCondition and sharedMetadata - unique to thirdweb\n const [claimConditionResult, sharedMetadataResult] = await Promise.all([\n client.readContract({\n address: contractAddress,\n abi: THIRDWEB_OPENEDITONERC721_ABI,\n functionName: \"claimCondition\"\n }).catch(() => null),\n client.readContract({\n address: contractAddress,\n abi: THIRDWEB_OPENEDITONERC721_ABI,\n functionName: \"sharedMetadata\"\n }).catch(() => null)\n ]);\n \n // If both functions exist, it's a thirdweb contract\n if (claimConditionResult !== null && sharedMetadataResult !== null) {\n return {\n provider: \"thirdweb\",\n isERC1155: isERC1155 as boolean,\n isERC721: isERC721 as boolean\n };\n }\n } catch {\n // Not a thirdweb contract, continue detection\n }\n\n // TODO: Add detection for OpenSea, Zora, etc.\n // For now, return generic\n return {\n provider: \"generic\",\n isERC1155: isERC1155 as boolean,\n isERC721: isERC721 as boolean\n };\n\n } catch (error) {\n console.error(\"Error detecting NFT provider:\", error);\n // Default to generic provider\n return {\n provider: \"generic\",\n isERC1155: false,\n isERC721: false\n };\n }\n}\n\n/**\n * Validates parameters based on detected provider\n */\nexport function validateParameters(params: MintParams, contractInfo: NFTContractInfo): {\n isValid: boolean;\n missingParams: string[];\n errors: string[];\n} {\n const config = PROVIDER_CONFIGS[contractInfo.provider];\n const missingParams: string[] = [];\n const errors: string[] = [];\n\n // Check required params for the provider\n for (const param of config.requiredParams) {\n if (!params[param as keyof MintParams]) {\n missingParams.push(param);\n }\n }\n\n // Provider-specific validation\n if (contractInfo.provider === \"manifold\") {\n if (!params.instanceId && !params.tokenId) {\n errors.push(\"Manifold NFTs require either instanceId or tokenId\");\n missingParams.push(\"instanceId or tokenId\");\n }\n \n if (contractInfo.claim?.merkleRoot && contractInfo.claim.merkleRoot !== \"0x0000000000000000000000000000000000000000000000000000000000000000\") {\n errors.push(\"This NFT requires a merkle proof for minting - not supported yet\");\n }\n }\n\n if (contractInfo.provider === \"thirdweb\") {\n if (contractInfo.claimCondition?.merkleRoot && contractInfo.claimCondition.merkleRoot !== \"0x0000000000000000000000000000000000000000000000000000000000000000\") {\n errors.push(\"This NFT requires a merkle proof for minting - not supported yet\");\n }\n \n if (contractInfo.claimCondition?.startTimestamp) {\n const now = Math.floor(Date.now() / 1000);\n if (now < contractInfo.claimCondition.startTimestamp) {\n errors.push(\"Claim has not started yet\");\n }\n }\n }\n\n return {\n isValid: missingParams.length === 0 && errors.length === 0,\n missingParams,\n errors\n };\n}", "type": "registry:lib" }, { "path": "registry/mini-app/blocks/nft-mint-flow/lib/price-optimizer.ts", - "content": "import type { PublicClient } from \"viem\";\nimport type { NFTContractInfo, MintParams } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/types\";\nimport { getProviderConfig } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/provider-configs\";\n\n/**\n * Optimized price discovery that batches RPC calls where possible\n */\nexport async function fetchPriceData(\n client: PublicClient,\n params: MintParams,\n contractInfo: NFTContractInfo\n): Promise<{\n mintPrice?: bigint;\n erc20Details?: {\n address: string;\n symbol: string;\n decimals: number;\n allowance?: bigint;\n balance?: bigint;\n };\n totalCost: bigint;\n claim?: NFTContractInfo[\"claim\"];\n}> {\n const config = getProviderConfig(contractInfo.provider);\n \n if (contractInfo.provider === \"manifold\" && contractInfo.extensionAddress) {\n // For Manifold, we need extension fee + claim cost\n const calls = [\n // Get MINT_FEE from extension\n {\n address: contractInfo.extensionAddress,\n abi: config.mintConfig.abi,\n functionName: \"MINT_FEE\",\n args: []\n }\n ];\n \n // Add claim fetch if we have instanceId\n if (params.instanceId) {\n calls.push({\n address: contractInfo.extensionAddress,\n abi: config.mintConfig.abi,\n functionName: \"getClaim\",\n args: [params.contractAddress, BigInt(params.instanceId)]\n } as any);\n }\n \n try {\n const results = await Promise.all(\n calls.map(call => \n client.readContract(call as any).catch(err => {\n console.error(\"RPC call failed:\", err);\n return null;\n })\n )\n );\n \n const mintFee = results[0] as bigint | null;\n const claim = results[1] as any;\n \n let totalCost = mintFee || BigInt(0);\n let erc20Details = undefined;\n \n if (claim) {\n // Store claim data in contractInfo for later use\n contractInfo.claim = {\n cost: claim.cost,\n merkleRoot: claim.merkleRoot,\n erc20: claim.erc20,\n startDate: claim.startDate,\n endDate: claim.endDate,\n walletMax: claim.walletMax\n };\n \n // Check if ERC20 payment\n if (claim.erc20 && claim.erc20 !== \"0x0000000000000000000000000000000000000000\") {\n // Batch ERC20 details fetch\n const [symbol, decimals, allowance, balance] = await Promise.all([\n client.readContract({\n address: claim.erc20,\n abi: [{ name: \"symbol\", type: \"function\", inputs: [], outputs: [{ type: \"string\" }], stateMutability: \"view\" }],\n functionName: \"symbol\"\n }),\n client.readContract({\n address: claim.erc20,\n abi: [{ name: \"decimals\", type: \"function\", inputs: [], outputs: [{ type: \"uint8\" }], stateMutability: \"view\" }],\n functionName: \"decimals\"\n }),\n params.recipient ? client.readContract({\n address: claim.erc20,\n abi: [{ \n name: \"allowance\", \n type: \"function\", \n inputs: [{ name: \"owner\", type: \"address\" }, { name: \"spender\", type: \"address\" }], \n outputs: [{ type: \"uint256\" }], \n stateMutability: \"view\" \n }],\n functionName: \"allowance\",\n args: [params.recipient, contractInfo.extensionAddress || params.contractAddress]\n }).catch(() => BigInt(0)) : Promise.resolve(undefined), // Return undefined when no recipient, not 0\n params.recipient ? client.readContract({\n address: claim.erc20,\n abi: [{ \n name: \"balanceOf\", \n type: \"function\", \n inputs: [{ name: \"owner\", type: \"address\" }], \n outputs: [{ type: \"uint256\" }], \n stateMutability: \"view\" \n }],\n functionName: \"balanceOf\",\n args: [params.recipient]\n }).catch(() => BigInt(0)) : Promise.resolve(undefined)\n ]);\n \n // Validate decimals\n const validatedDecimals = Number(decimals);\n if (isNaN(validatedDecimals) || validatedDecimals < 0 || validatedDecimals > 255) {\n console.error(`Invalid ERC20 decimals for ${claim.erc20}:`, decimals);\n throw new Error(`Invalid ERC20 decimals: ${decimals}`);\n }\n \n erc20Details = {\n address: claim.erc20,\n symbol: symbol as string,\n decimals: validatedDecimals,\n allowance: allowance as bigint,\n balance: balance as bigint | undefined\n };\n \n // For ERC20, total cost in ETH is just the mint fee\n totalCost = mintFee || BigInt(0);\n } else {\n // ETH payment - add claim cost to mint fee\n totalCost = (mintFee || BigInt(0)) + (claim.cost || BigInt(0));\n }\n }\n \n return {\n mintPrice: mintFee || BigInt(0),\n erc20Details,\n totalCost,\n claim: claim ? contractInfo.claim : undefined\n };\n } catch (err) {\n console.error(\"Failed to fetch Manifold price data:\", err);\n return { totalCost: BigInt(0) };\n }\n } else if (contractInfo.provider === \"nfts2me\") {\n // Special handling for nfts2me - try different pricing patterns\n \n // Pattern 1: Try mintPrice() first - for simple/free NFTs2Me contracts\n try {\n const mintPrice = await client.readContract({\n address: params.contractAddress,\n abi: [{\n inputs: [],\n name: \"mintPrice\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }],\n functionName: \"mintPrice\",\n args: []\n });\n \n if (mintPrice !== undefined) {\n // mintPrice() returns the price per NFT\n const pricePerNFT = mintPrice as bigint;\n const totalCost = pricePerNFT * BigInt(params.amount || 1);\n \n // Handle free mints (mintPrice = 0)\n return {\n mintPrice: pricePerNFT,\n totalCost: totalCost\n };\n }\n } catch (err) {\n // mintPrice() doesn't exist, try next pattern\n console.log(\"mintPrice() not found, trying mintFee pattern\");\n }\n \n // Pattern 2: Try mintFee(amount) + protocolFee() - for more complex pricing\n try {\n // Fetch both fees in parallel\n const [mintFee, protocolFee] = await Promise.all([\n // mintFee is the creator's revenue per NFT\n client.readContract({\n address: params.contractAddress,\n abi: [{\n inputs: [{ name: \"amount\", type: \"uint256\" }],\n name: \"mintFee\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }],\n functionName: \"mintFee\",\n args: [BigInt(params.amount || 1)]\n }),\n // protocolFee is the platform fee (0.0001 ETH unless disabled)\n client.readContract({\n address: params.contractAddress,\n abi: [{\n inputs: [],\n name: \"protocolFee\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }],\n functionName: \"protocolFee\",\n args: []\n })\n ]);\n \n if (mintFee !== undefined && protocolFee !== undefined) {\n // Total cost = creator revenue (mintFee) + platform fee (protocolFee * amount)\n const totalCost = (mintFee as bigint) + ((protocolFee as bigint) * BigInt(params.amount || 1));\n return {\n mintPrice: mintFee as bigint,\n totalCost: totalCost\n };\n }\n } catch (err) {\n console.error(\"Failed to fetch nfts2me fees:\", err);\n }\n \n // Pattern 3: Fallback to default fees if all patterns fail\n const amount = BigInt(params.amount || 1);\n const creatorFeePerNFT = BigInt(\"100000000000000\"); // 0.0001 ETH creator fee per NFT\n const protocolFeePerNFT = BigInt(\"100000000000000\"); // 0.0001 ETH protocol fee per NFT\n return { \n mintPrice: creatorFeePerNFT * amount,\n totalCost: (creatorFeePerNFT + protocolFeePerNFT) * amount\n };\n } else {\n // Generic price discovery - try multiple function names\n const functionNames = config.priceDiscovery.functionNames;\n \n for (const functionName of functionNames) {\n try {\n // Check if this function requires an amount parameter\n const args = config.priceDiscovery.requiresAmountParam \n ? [BigInt(params.amount || 1)]\n : [];\n \n const price = await client.readContract({\n address: params.contractAddress,\n abi: config.priceDiscovery.abis[0],\n functionName: functionName as any,\n args\n });\n \n if (price !== undefined) {\n // Calculate total cost based on provider's custom logic\n const totalCost = config.mintConfig.calculateValue(price as bigint, params);\n return {\n mintPrice: price as bigint,\n totalCost\n };\n }\n } catch {\n // Try next function name\n continue;\n }\n }\n \n // No price found, assume free mint\n return { mintPrice: BigInt(0), totalCost: BigInt(0) };\n }\n}", + "content": "import type { PublicClient } from \"viem\";\nimport type { NFTContractInfo, MintParams } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/types\";\nimport { getProviderConfig } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/provider-configs\";\nimport { THIRDWEB_OPENEDITONERC721_ABI, THIRDWEB_NATIVE_TOKEN } from \"@/registry/mini-app/lib/nft-standards\";\n\n/**\n * Optimized price discovery that batches RPC calls where possible\n */\nexport async function fetchPriceData(\n client: PublicClient,\n params: MintParams,\n contractInfo: NFTContractInfo\n): Promise<{\n mintPrice?: bigint;\n erc20Details?: {\n address: string;\n symbol: string;\n decimals: number;\n allowance?: bigint;\n balance?: bigint;\n };\n totalCost: bigint;\n claim?: NFTContractInfo[\"claim\"];\n}> {\n const config = getProviderConfig(contractInfo.provider);\n \n if (contractInfo.provider === \"manifold\" && contractInfo.extensionAddress) {\n // For Manifold, we need extension fee + claim cost\n const calls = [\n // Get MINT_FEE from extension\n {\n address: contractInfo.extensionAddress,\n abi: config.mintConfig.abi,\n functionName: \"MINT_FEE\",\n args: []\n }\n ];\n \n // Add claim fetch if we have instanceId\n if (params.instanceId) {\n calls.push({\n address: contractInfo.extensionAddress,\n abi: config.mintConfig.abi,\n functionName: \"getClaim\",\n args: [params.contractAddress, BigInt(params.instanceId)]\n } as any);\n }\n \n try {\n const results = await Promise.all(\n calls.map(call => \n client.readContract(call as any).catch(err => {\n console.error(\"RPC call failed:\", err);\n return null;\n })\n )\n );\n \n const mintFee = results[0] as bigint | null;\n const claim = results[1] as any;\n \n let totalCost = mintFee || BigInt(0);\n let erc20Details = undefined;\n \n if (claim) {\n // Store claim data in contractInfo for later use\n contractInfo.claim = {\n cost: claim.cost,\n merkleRoot: claim.merkleRoot,\n erc20: claim.erc20,\n startDate: claim.startDate,\n endDate: claim.endDate,\n walletMax: claim.walletMax\n };\n \n // Check if ERC20 payment\n if (claim.erc20 && claim.erc20 !== \"0x0000000000000000000000000000000000000000\") {\n // Batch ERC20 details fetch\n const [symbol, decimals, allowance, balance] = await Promise.all([\n client.readContract({\n address: claim.erc20,\n abi: [{ name: \"symbol\", type: \"function\", inputs: [], outputs: [{ type: \"string\" }], stateMutability: \"view\" }],\n functionName: \"symbol\"\n }),\n client.readContract({\n address: claim.erc20,\n abi: [{ name: \"decimals\", type: \"function\", inputs: [], outputs: [{ type: \"uint8\" }], stateMutability: \"view\" }],\n functionName: \"decimals\"\n }),\n params.recipient ? client.readContract({\n address: claim.erc20,\n abi: [{ \n name: \"allowance\", \n type: \"function\", \n inputs: [{ name: \"owner\", type: \"address\" }, { name: \"spender\", type: \"address\" }], \n outputs: [{ type: \"uint256\" }], \n stateMutability: \"view\" \n }],\n functionName: \"allowance\",\n args: [params.recipient, contractInfo.extensionAddress || params.contractAddress]\n }).catch(() => BigInt(0)) : Promise.resolve(undefined), // Return undefined when no recipient, not 0\n params.recipient ? client.readContract({\n address: claim.erc20,\n abi: [{ \n name: \"balanceOf\", \n type: \"function\", \n inputs: [{ name: \"owner\", type: \"address\" }], \n outputs: [{ type: \"uint256\" }], \n stateMutability: \"view\" \n }],\n functionName: \"balanceOf\",\n args: [params.recipient]\n }).catch(() => BigInt(0)) : Promise.resolve(undefined)\n ]);\n \n // Validate decimals\n const validatedDecimals = Number(decimals);\n if (isNaN(validatedDecimals) || validatedDecimals < 0 || validatedDecimals > 255) {\n console.error(`Invalid ERC20 decimals for ${claim.erc20}:`, decimals);\n throw new Error(`Invalid ERC20 decimals: ${decimals}`);\n }\n \n erc20Details = {\n address: claim.erc20,\n symbol: symbol as string,\n decimals: validatedDecimals,\n allowance: allowance as bigint,\n balance: balance as bigint | undefined\n };\n \n // For ERC20, total cost in ETH is just the mint fee\n totalCost = mintFee || BigInt(0);\n } else {\n // ETH payment - add claim cost to mint fee\n totalCost = (mintFee || BigInt(0)) + (claim.cost || BigInt(0));\n }\n }\n \n return {\n mintPrice: mintFee || BigInt(0),\n erc20Details,\n totalCost,\n claim: claim ? contractInfo.claim : undefined\n };\n } catch (err) {\n console.error(\"Failed to fetch Manifold price data:\", err);\n return { totalCost: BigInt(0) };\n }\n } else if (contractInfo.provider === \"nfts2me\") {\n // Special handling for nfts2me - try different pricing patterns\n \n // Pattern 1: Try mintPrice() first - for simple/free NFTs2Me contracts\n try {\n const mintPrice = await client.readContract({\n address: params.contractAddress,\n abi: [{\n inputs: [],\n name: \"mintPrice\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }],\n functionName: \"mintPrice\",\n args: []\n });\n \n if (mintPrice !== undefined) {\n // mintPrice() returns the price per NFT\n const pricePerNFT = mintPrice as bigint;\n const totalCost = pricePerNFT * BigInt(params.amount || 1);\n \n // Handle free mints (mintPrice = 0)\n return {\n mintPrice: pricePerNFT,\n totalCost: totalCost\n };\n }\n } catch (err) {\n // mintPrice() doesn't exist, try next pattern\n console.log(\"mintPrice() not found, trying mintFee pattern\");\n }\n \n // Pattern 2: Try mintFee(amount) + protocolFee() - for more complex pricing\n try {\n // Fetch both fees in parallel\n const [mintFee, protocolFee] = await Promise.all([\n // mintFee is the creator's revenue per NFT\n client.readContract({\n address: params.contractAddress,\n abi: [{\n inputs: [{ name: \"amount\", type: \"uint256\" }],\n name: \"mintFee\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }],\n functionName: \"mintFee\",\n args: [BigInt(params.amount || 1)]\n }),\n // protocolFee is the platform fee (0.0001 ETH unless disabled)\n client.readContract({\n address: params.contractAddress,\n abi: [{\n inputs: [],\n name: \"protocolFee\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }],\n functionName: \"protocolFee\",\n args: []\n })\n ]);\n \n if (mintFee !== undefined && protocolFee !== undefined) {\n // Total cost = creator revenue (mintFee) + platform fee (protocolFee * amount)\n const totalCost = (mintFee as bigint) + ((protocolFee as bigint) * BigInt(params.amount || 1));\n return {\n mintPrice: mintFee as bigint,\n totalCost: totalCost\n };\n }\n } catch (err) {\n console.error(\"Failed to fetch nfts2me fees:\", err);\n }\n \n // Pattern 3: Fallback to default fees if all patterns fail\n const amount = BigInt(params.amount || 1);\n const creatorFeePerNFT = BigInt(\"100000000000000\"); // 0.0001 ETH creator fee per NFT\n const protocolFeePerNFT = BigInt(\"100000000000000\"); // 0.0001 ETH protocol fee per NFT\n return { \n mintPrice: creatorFeePerNFT * amount,\n totalCost: (creatorFeePerNFT + protocolFeePerNFT) * amount\n };\n } else if (contractInfo.provider === \"thirdweb\") {\n // thirdweb OpenEditionERC721 price discovery\n try {\n // First get the active claim condition ID\n const claimCondition = await client.readContract({\n address: params.contractAddress,\n abi: THIRDWEB_OPENEDITONERC721_ABI,\n functionName: \"claimCondition\"\n });\n \n // Validate the response is an array with expected values\n if (!Array.isArray(claimCondition) || claimCondition.length !== 2) {\n throw new Error(\"Invalid claim condition response from contract\");\n }\n \n const [currentStartId, count] = claimCondition as [bigint, bigint];\n \n if (count === BigInt(0)) {\n // No claim conditions set\n return { mintPrice: BigInt(0), totalCost: BigInt(0) };\n }\n \n // Get the active claim condition (last one)\n const activeConditionId = currentStartId + count - BigInt(1);\n \n const condition = await client.readContract({\n address: params.contractAddress,\n abi: THIRDWEB_OPENEDITONERC721_ABI,\n functionName: \"getClaimConditionById\",\n args: [activeConditionId]\n });\n \n if (!condition || typeof condition !== \"object\") {\n throw new Error(\"Invalid claim condition response\");\n }\n \n const {\n startTimestamp,\n maxClaimableSupply,\n supplyClaimed,\n quantityLimitPerWallet,\n merkleRoot,\n pricePerToken,\n currency,\n metadata\n } = condition as any;\n \n // Store claim condition in contractInfo for later use\n contractInfo.claimCondition = {\n id: Number(activeConditionId),\n pricePerToken: pricePerToken as bigint,\n currency: currency as string,\n maxClaimableSupply: maxClaimableSupply as bigint,\n merkleRoot: merkleRoot as `0x${string}`,\n startTimestamp: Number(startTimestamp),\n quantityLimitPerWallet: quantityLimitPerWallet as bigint\n };\n \n // Check if it's ERC20 payment\n if (currency && currency.toLowerCase() !== THIRDWEB_NATIVE_TOKEN.toLowerCase()) {\n // ERC20 payment\n const [symbol, decimals, allowance, balance] = await Promise.all([\n client.readContract({\n address: currency,\n abi: [{ name: \"symbol\", type: \"function\", inputs: [], outputs: [{ type: \"string\" }], stateMutability: \"view\" }],\n functionName: \"symbol\"\n }),\n client.readContract({\n address: currency,\n abi: [{ name: \"decimals\", type: \"function\", inputs: [], outputs: [{ type: \"uint8\" }], stateMutability: \"view\" }],\n functionName: \"decimals\"\n }),\n params.recipient ? client.readContract({\n address: currency,\n abi: [{ \n name: \"allowance\", \n type: \"function\", \n inputs: [{ name: \"owner\", type: \"address\" }, { name: \"spender\", type: \"address\" }], \n outputs: [{ type: \"uint256\" }], \n stateMutability: \"view\" \n }],\n functionName: \"allowance\",\n args: [params.recipient, params.contractAddress]\n }).catch(() => BigInt(0)) : Promise.resolve(undefined),\n params.recipient ? client.readContract({\n address: currency,\n abi: [{ \n name: \"balanceOf\", \n type: \"function\", \n inputs: [{ name: \"owner\", type: \"address\" }], \n outputs: [{ type: \"uint256\" }], \n stateMutability: \"view\" \n }],\n functionName: \"balanceOf\",\n args: [params.recipient]\n }).catch(() => BigInt(0)) : Promise.resolve(undefined)\n ]);\n \n // Validate decimals\n const validatedDecimals = Number(decimals);\n if (isNaN(validatedDecimals) || validatedDecimals < 0 || validatedDecimals > 255) {\n console.error(`Invalid ERC20 decimals for ${currency}:`, decimals);\n throw new Error(`Invalid ERC20 decimals: ${decimals}`);\n }\n \n return {\n mintPrice: pricePerToken as bigint,\n erc20Details: {\n address: currency,\n symbol: symbol as string,\n decimals: validatedDecimals,\n allowance: allowance as bigint,\n balance: balance as bigint | undefined\n },\n totalCost: BigInt(0), // No ETH needed for ERC20 payment\n claim: contractInfo.claimCondition\n };\n } else {\n // ETH payment\n const totalCost = (pricePerToken as bigint) * BigInt(params.amount || 1);\n return {\n mintPrice: pricePerToken as bigint,\n totalCost,\n claim: contractInfo.claimCondition\n };\n }\n } catch (err) {\n console.error(\"Failed to fetch thirdweb price data:\", err);\n return { totalCost: BigInt(0) };\n }\n } else {\n // Generic price discovery - try multiple function names\n const functionNames = config.priceDiscovery.functionNames;\n \n for (const functionName of functionNames) {\n try {\n // Check if this function requires an amount parameter\n const args = config.priceDiscovery.requiresAmountParam \n ? [BigInt(params.amount || 1)]\n : [];\n \n const price = await client.readContract({\n address: params.contractAddress,\n abi: config.priceDiscovery.abis[0],\n functionName: functionName as any,\n args\n });\n \n if (price !== undefined) {\n // Calculate total cost based on provider's custom logic\n const totalCost = config.mintConfig.calculateValue(price as bigint, params);\n return {\n mintPrice: price as bigint,\n totalCost\n };\n }\n } catch {\n // Try next function name\n continue;\n }\n }\n \n // No price found, assume free mint\n return { mintPrice: BigInt(0), totalCost: BigInt(0) };\n }\n}", "type": "registry:lib" }, { @@ -68,7 +68,7 @@ }, { "path": "registry/mini-app/blocks/nft-card/nft-card.tsx", - "content": "\"use client\";\n\nimport { cn } from \"@/registry/mini-app/lib/utils\";\nimport Image from \"next/image\";\nimport { useState, useEffect, useRef } from \"react\";\n\ninterface NFTMetadata {\n name?: string;\n description?: string;\n image?: string;\n image_url?: string;\n external_url?: string;\n attributes?: Array<{\n trait_type: string;\n value: string | number;\n display_type?: string;\n }>;\n image_details?: {\n bytes?: number;\n format?: string;\n sha256?: string;\n width?: number;\n height?: number;\n };\n [key: string]: unknown;\n}\nimport { getAddress, type Address } from \"viem\";\nimport { \n findChainByName, \n getPublicClient \n} from \"@/registry/mini-app/lib/chains\";\nimport { \n ERC721_ABI, \n ipfsToHttp \n} from \"@/registry/mini-app/lib/nft-standards\";\nimport { \n getTokenURIWithManifoldSupport \n} from \"@/registry/mini-app/lib/manifold-utils\";\n\n// Base64 placeholder image\nconst PLACEHOLDER_IMAGE =\n \"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjMwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMzAwIiBoZWlnaHQ9IjMwMCIgZmlsbD0iI2YxZjFmMSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMjQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiIGZpbGw9IiM5OTkiPk5GVCBJbWFnZTwvdGV4dD48L3N2Zz4=\";\n\n\ntype NFTCardProps = {\n contractAddress: string;\n tokenId: string;\n network?: string;\n alt?: string;\n className?: string;\n width?: number | string;\n height?: number | string;\n rounded?: \"none\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n shadow?: boolean;\n objectFit?: \"contain\" | \"cover\" | \"fill\";\n fallbackImageUrl?: string;\n showTitle?: boolean;\n showNetwork?: boolean;\n titlePosition?: \"top\" | \"bottom\" | \"outside\";\n networkPosition?:\n | \"top-left\"\n | \"top-right\"\n | \"bottom-left\"\n | \"bottom-right\"\n | \"outside\";\n customTitle?: string;\n customNetworkName?: string;\n loadingComponent?: React.ReactNode;\n errorComponent?: React.ReactNode;\n imageProps?: React.ComponentProps;\n titleClassName?: string;\n networkClassName?: string;\n showOwner?: boolean;\n onLoad?: (metadata: NFTMetadata) => void;\n onError?: (error: Error) => void;\n layout?: \"compact\" | \"card\" | \"detailed\";\n containerClassName?: string;\n};\n\nexport function NFTCard({\n contractAddress,\n tokenId,\n network = \"ethereum\", // Default to Ethereum mainnet\n alt = \"NFT Image\",\n className = \"\",\n width = 300,\n height = 300,\n rounded = \"md\",\n shadow = true,\n objectFit = \"cover\",\n fallbackImageUrl = PLACEHOLDER_IMAGE,\n showTitle = true,\n showNetwork = true,\n titlePosition = \"outside\",\n networkPosition = \"top-right\",\n customTitle,\n customNetworkName,\n loadingComponent,\n errorComponent,\n imageProps,\n titleClassName = \"\",\n networkClassName = \"\",\n showOwner = false,\n onLoad,\n onError,\n layout = \"card\",\n containerClassName = \"\",\n}: NFTCardProps) {\n const [imageUrl, setImageUrl] = useState(fallbackImageUrl);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState(null);\n const [title, setTitle] = useState(customTitle || null);\n const [networkName, setNetworkName] = useState(\n customNetworkName || \"\",\n );\n const [owner, setOwner] = useState(null);\n const [metadata, setMetadata] = useState(null);\n const abortControllerRef = useRef(null);\n\n const roundedClasses = {\n none: \"rounded-none\",\n sm: \"rounded-sm\",\n md: \"rounded-md\",\n lg: \"rounded-lg\",\n xl: \"rounded-xl\",\n full: \"rounded-full\",\n };\n\n const networkPositionClasses = {\n \"top-left\": \"top-0 left-0 rounded-br-md\",\n \"top-right\": \"top-0 right-0 rounded-bl-md\",\n \"bottom-left\": \"bottom-0 left-0 rounded-tr-md\",\n \"bottom-right\": \"bottom-0 right-0 rounded-tl-md\",\n outside: \"\",\n };\n\n useEffect(() => {\n if (customTitle) {\n setTitle(customTitle);\n }\n\n if (customNetworkName) {\n setNetworkName(customNetworkName);\n }\n }, [customTitle, customNetworkName]);\n\n useEffect(() => {\n const fetchNFTData = async () => {\n if (!contractAddress || !tokenId) return;\n \n // Cancel any previous request\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n }\n \n // Create new AbortController for this request\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n setIsLoading(true);\n setError(null);\n\n try {\n // Skip chain setup if we have customNetworkName\n if (!customNetworkName) {\n // Find the chain by name using shared utility\n const selectedChain = findChainByName(network || \"ethereum\");\n \n if (!selectedChain) {\n console.warn(\n `Chain \"${network}\" not found, defaulting to Ethereum mainnet`,\n );\n setNetworkName(\"Ethereum\");\n } else {\n setNetworkName(selectedChain.name);\n }\n\n // Create public client using shared utility\n const client = getPublicClient(selectedChain?.id || 1);\n\n console.log(\n `Fetching NFT data from ${selectedChain?.name || 'Ethereum'} for contract ${contractAddress} token ${tokenId}`,\n );\n\n // Skip title setup if we have customTitle\n if (!customTitle) {\n try {\n // Get contract name\n const name = (await client.readContract({\n address: getAddress(contractAddress),\n abi: ERC721_ABI.name,\n functionName: \"name\",\n })) as string;\n\n // Set title\n setTitle(`${name} #${tokenId}`);\n } catch (nameError) {\n console.warn(\"Could not fetch NFT name:\", nameError);\n setTitle(`NFT #${tokenId}`);\n }\n }\n\n // Get owner if requested\n if (showOwner) {\n try {\n const ownerAddress = (await client.readContract({\n address: getAddress(contractAddress),\n abi: ERC721_ABI.ownerOf,\n functionName: \"ownerOf\",\n args: [BigInt(tokenId)],\n })) as string;\n\n setOwner(ownerAddress);\n } catch (ownerError) {\n console.warn(\"Could not fetch NFT owner:\", ownerError);\n }\n }\n\n // Get tokenURI with automatic Manifold support\n let metadataUrl = await getTokenURIWithManifoldSupport(\n client,\n getAddress(contractAddress) as Address,\n tokenId\n );\n\n // Handle IPFS URLs using shared utility\n metadataUrl = ipfsToHttp(metadataUrl);\n\n // Fetch metadata with abort signal\n const response = await fetch(metadataUrl, {\n signal: abortController.signal\n });\n \n if (!response.ok) {\n throw new Error(`Failed to fetch metadata: ${response.status}`);\n }\n \n const fetchedMetadata = await response.json();\n console.log(\"NFT metadata:\", fetchedMetadata);\n \n // Store metadata in state\n setMetadata(fetchedMetadata);\n\n // Call onLoad callback if provided\n if (onLoad) {\n onLoad(fetchedMetadata);\n }\n\n // Get image URL from metadata\n let nftImageUrl = fetchedMetadata.image || fetchedMetadata.image_url;\n\n // Handle IPFS URLs for image using shared utility\n if (nftImageUrl) {\n nftImageUrl = ipfsToHttp(nftImageUrl);\n }\n\n if (nftImageUrl) {\n setImageUrl(nftImageUrl);\n } else {\n // If no image URL found, use placeholder\n setImageUrl(fallbackImageUrl);\n }\n }\n } catch (err) {\n // Don't update state if request was aborted\n if (err instanceof Error && err.name === 'AbortError') {\n console.log('NFT data fetch was cancelled');\n return;\n }\n \n console.error(\"Error fetching NFT:\", err);\n const error = err instanceof Error ? err : new Error(String(err));\n setError(`Failed to load NFT data: ${error.message}`);\n setImageUrl(fallbackImageUrl);\n\n // Call onError callback if provided\n if (onError) {\n onError(error);\n }\n } finally {\n // Only update loading state if this request wasn't aborted\n if (!abortController.signal.aborted) {\n setIsLoading(false);\n }\n }\n };\n\n fetchNFTData();\n \n // Cleanup function to abort request if component unmounts or deps change\n return () => {\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n }\n };\n }, [\n contractAddress,\n tokenId,\n network,\n fallbackImageUrl,\n customTitle,\n customNetworkName,\n showOwner,\n onLoad,\n onError,\n ]);\n\n const defaultLoadingComponent = (\n
\n
\n
\n );\n\n const defaultErrorComponent = (\n
\n

{error}

\n
\n );\n\n // Render network badge inside the image\n const renderNetworkBadge = () => {\n if (!showNetwork || !networkName || networkPosition === \"outside\")\n return null;\n\n return (\n \n {networkName}\n \n );\n };\n\n // Render title inside the image\n const renderInnerTitle = () => {\n if (!showTitle || !title || titlePosition === \"outside\") return null;\n\n return (\n \n {title}\n {showOwner && owner && (\n
\n Owner: {owner.substring(0, 6)}...{owner.substring(owner.length - 4)}\n
\n )}\n \n );\n };\n\n // Render outside information (title, network, owner)\n const renderOutsideInfo = () => {\n if (\n (!showTitle || !title) &&\n (!showNetwork || !networkName || networkPosition !== \"outside\") &&\n (!showOwner || !owner || titlePosition !== \"outside\")\n ) {\n return null;\n }\n\n return (\n
\n {showTitle && title && titlePosition === \"outside\" && (\n
\n {title}\n
\n )}\n\n {showNetwork && networkName && networkPosition === \"outside\" && (\n \n Network: {networkName}\n
\n )}\n\n {showOwner && owner && titlePosition === \"outside\" && (\n
\n Owner: {owner.substring(0, 6)}...{owner.substring(owner.length - 4)}\n
\n )}\n \n );\n };\n\n // Apply different layouts\n const getContainerClasses = () => {\n switch (layout) {\n case \"compact\":\n return \"inline-block\";\n case \"detailed\":\n return \"flex flex-col overflow-hidden\";\n case \"card\":\n default:\n return \"\";\n }\n };\n\n // Calculate display dimensions that preserve aspect ratio\n const getDisplayDimensions = () => {\n // Handle percentage values\n const isPercentageWidth = typeof width === \"string\" && width.includes(\"%\");\n const isPercentageHeight = typeof height === \"string\" && height.includes(\"%\");\n \n if (isPercentageWidth || isPercentageHeight) {\n return { \n width: width || '100%', \n height: height || '100%', \n useContain: false,\n isPercentage: true\n };\n }\n \n const maxWidth = typeof width === \"number\" ? width : 300;\n const maxHeight = typeof height === \"number\" ? height : 300;\n \n // Check if we have image_details with dimensions\n if (metadata?.image_details?.width && metadata?.image_details?.height) {\n const originalAspectRatio = metadata.image_details.width / metadata.image_details.height;\n \n // Scale to fit within bounds while preserving aspect ratio\n const widthBasedHeight = maxWidth / originalAspectRatio;\n const heightBasedWidth = maxHeight * originalAspectRatio;\n \n if (widthBasedHeight <= maxHeight) {\n // Width is the limiting factor\n return { \n width: maxWidth, \n height: Math.round(widthBasedHeight),\n useContain: true, // Use contain to show full image\n isPercentage: false\n };\n } else {\n // Height is the limiting factor\n return { \n width: Math.round(heightBasedWidth), \n height: maxHeight,\n useContain: true,\n isPercentage: false\n };\n }\n }\n \n // No image_details, use provided dimensions\n return { width: maxWidth, height: maxHeight, useContain: false, isPercentage: false };\n };\n\n const displayDimensions = getDisplayDimensions();\n\n return (\n
\n \n {isLoading && (loadingComponent || defaultLoadingComponent)}\n\n {error && (errorComponent || defaultErrorComponent)}\n\n setImageUrl(PLACEHOLDER_IMAGE)}\n {...imageProps}\n />\n\n {renderInnerTitle()}\n {renderNetworkBadge()}\n
\n\n {renderOutsideInfo()}\n \n );\n}\n", + "content": "\"use client\";\n\nimport { cn } from \"@/registry/mini-app/lib/utils\";\nimport Image from \"next/image\";\nimport { useState, useEffect, useRef } from \"react\";\n\ninterface NFTMetadata {\n name?: string;\n description?: string;\n image?: string;\n image_url?: string;\n external_url?: string;\n attributes?: Array<{\n trait_type: string;\n value: string | number;\n display_type?: string;\n }>;\n image_details?: {\n bytes?: number;\n format?: string;\n sha256?: string;\n width?: number;\n height?: number;\n };\n [key: string]: unknown;\n}\nimport { getAddress, type Address } from \"viem\";\nimport { \n findChainByName, \n getPublicClient \n} from \"@/registry/mini-app/lib/chains\";\nimport { \n ERC721_ABI, \n ipfsToHttp \n} from \"@/registry/mini-app/lib/nft-standards\";\nimport { \n getTokenMetadataURL \n} from \"@/registry/mini-app/lib/nft-metadata-utils\";\n\n// Base64 placeholder image\nconst PLACEHOLDER_IMAGE =\n \"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjMwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMzAwIiBoZWlnaHQ9IjMwMCIgZmlsbD0iI2YxZjFmMSIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMjQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGRvbWluYW50LWJhc2VsaW5lPSJtaWRkbGUiIGZpbGw9IiM5OTkiPk5GVCBJbWFnZTwvdGV4dD48L3N2Zz4=\";\n\n\ntype NFTCardProps = {\n contractAddress: string;\n tokenId: string;\n network?: string;\n alt?: string;\n className?: string;\n width?: number | string;\n height?: number | string;\n rounded?: \"none\" | \"sm\" | \"md\" | \"lg\" | \"xl\" | \"full\";\n shadow?: boolean;\n objectFit?: \"contain\" | \"cover\" | \"fill\";\n fallbackImageUrl?: string;\n showTitle?: boolean;\n showNetwork?: boolean;\n titlePosition?: \"top\" | \"bottom\" | \"outside\";\n networkPosition?:\n | \"top-left\"\n | \"top-right\"\n | \"bottom-left\"\n | \"bottom-right\"\n | \"outside\";\n customTitle?: string;\n customNetworkName?: string;\n loadingComponent?: React.ReactNode;\n errorComponent?: React.ReactNode;\n imageProps?: React.ComponentProps;\n titleClassName?: string;\n networkClassName?: string;\n showOwner?: boolean;\n onLoad?: (metadata: NFTMetadata) => void;\n onError?: (error: Error) => void;\n layout?: \"compact\" | \"card\" | \"detailed\";\n containerClassName?: string;\n};\n\nexport function NFTCard({\n contractAddress,\n tokenId,\n network = \"ethereum\", // Default to Ethereum mainnet\n alt = \"NFT Image\",\n className = \"\",\n width = 300,\n height = 300,\n rounded = \"md\",\n shadow = true,\n objectFit = \"cover\",\n fallbackImageUrl = PLACEHOLDER_IMAGE,\n showTitle = true,\n showNetwork = true,\n titlePosition = \"outside\",\n networkPosition = \"top-right\",\n customTitle,\n customNetworkName,\n loadingComponent,\n errorComponent,\n imageProps,\n titleClassName = \"\",\n networkClassName = \"\",\n showOwner = false,\n onLoad,\n onError,\n layout = \"card\",\n containerClassName = \"\",\n}: NFTCardProps) {\n const [imageUrl, setImageUrl] = useState(fallbackImageUrl);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState(null);\n const [title, setTitle] = useState(customTitle || null);\n const [networkName, setNetworkName] = useState(\n customNetworkName || \"\",\n );\n const [owner, setOwner] = useState(null);\n const [metadata, setMetadata] = useState(null);\n const abortControllerRef = useRef(null);\n\n const roundedClasses = {\n none: \"rounded-none\",\n sm: \"rounded-sm\",\n md: \"rounded-md\",\n lg: \"rounded-lg\",\n xl: \"rounded-xl\",\n full: \"rounded-full\",\n };\n\n const networkPositionClasses = {\n \"top-left\": \"top-0 left-0 rounded-br-md\",\n \"top-right\": \"top-0 right-0 rounded-bl-md\",\n \"bottom-left\": \"bottom-0 left-0 rounded-tr-md\",\n \"bottom-right\": \"bottom-0 right-0 rounded-tl-md\",\n outside: \"\",\n };\n\n useEffect(() => {\n if (customTitle) {\n setTitle(customTitle);\n }\n\n if (customNetworkName) {\n setNetworkName(customNetworkName);\n }\n }, [customTitle, customNetworkName]);\n\n useEffect(() => {\n const fetchNFTData = async () => {\n if (!contractAddress || !tokenId) return;\n \n // Cancel any previous request\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n }\n \n // Create new AbortController for this request\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n setIsLoading(true);\n setError(null);\n\n try {\n // Skip chain setup if we have customNetworkName\n if (!customNetworkName) {\n // Find the chain by name using shared utility\n const selectedChain = findChainByName(network || \"ethereum\");\n \n if (!selectedChain) {\n console.warn(\n `Chain \"${network}\" not found, defaulting to Ethereum mainnet`,\n );\n setNetworkName(\"Ethereum\");\n } else {\n setNetworkName(selectedChain.name);\n }\n\n // Create public client using shared utility\n const client = getPublicClient(selectedChain?.id || 1);\n\n console.log(\n `Fetching NFT data from ${selectedChain?.name || 'Ethereum'} for contract ${contractAddress} token ${tokenId}`,\n );\n\n // Skip title setup if we have customTitle\n if (!customTitle) {\n try {\n // Get contract name\n const name = (await client.readContract({\n address: getAddress(contractAddress),\n abi: ERC721_ABI.name,\n functionName: \"name\",\n })) as string;\n\n // Set title\n setTitle(`${name} #${tokenId}`);\n } catch (nameError) {\n console.warn(\"Could not fetch NFT name:\", nameError);\n setTitle(`NFT #${tokenId}`);\n }\n }\n\n // Get owner if requested\n if (showOwner) {\n try {\n const ownerAddress = (await client.readContract({\n address: getAddress(contractAddress),\n abi: ERC721_ABI.ownerOf,\n functionName: \"ownerOf\",\n args: [BigInt(tokenId)],\n })) as string;\n\n setOwner(ownerAddress);\n } catch (ownerError) {\n console.warn(\"Could not fetch NFT owner:\", ownerError);\n }\n }\n\n // Get tokenURI with comprehensive metadata support\n let metadataUrl = await getTokenMetadataURL(\n client,\n getAddress(contractAddress) as Address,\n tokenId\n );\n\n // Handle IPFS URLs using shared utility\n metadataUrl = ipfsToHttp(metadataUrl);\n\n // Fetch metadata with abort signal\n const response = await fetch(metadataUrl, {\n signal: abortController.signal\n });\n \n if (!response.ok) {\n throw new Error(`Failed to fetch metadata: ${response.status}`);\n }\n \n const fetchedMetadata = await response.json();\n console.log(\"NFT metadata:\", fetchedMetadata);\n \n // Store metadata in state\n setMetadata(fetchedMetadata);\n\n // Call onLoad callback if provided\n if (onLoad) {\n onLoad(fetchedMetadata);\n }\n\n // Get image URL from metadata\n let nftImageUrl = fetchedMetadata.image || fetchedMetadata.image_url;\n\n // Handle IPFS URLs for image using shared utility\n if (nftImageUrl) {\n nftImageUrl = ipfsToHttp(nftImageUrl);\n }\n\n if (nftImageUrl) {\n setImageUrl(nftImageUrl);\n } else {\n // If no image URL found, use placeholder\n setImageUrl(fallbackImageUrl);\n }\n }\n } catch (err) {\n // Don't update state if request was aborted\n if (err instanceof Error && err.name === 'AbortError') {\n console.log('NFT data fetch was cancelled');\n return;\n }\n \n console.error(\"Error fetching NFT:\", err);\n const error = err instanceof Error ? err : new Error(String(err));\n setError(`Failed to load NFT data: ${error.message}`);\n setImageUrl(fallbackImageUrl);\n\n // Call onError callback if provided\n if (onError) {\n onError(error);\n }\n } finally {\n // Only update loading state if this request wasn't aborted\n if (!abortController.signal.aborted) {\n setIsLoading(false);\n }\n }\n };\n\n fetchNFTData();\n \n // Cleanup function to abort request if component unmounts or deps change\n return () => {\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n }\n };\n }, [\n contractAddress,\n tokenId,\n network,\n fallbackImageUrl,\n customTitle,\n customNetworkName,\n showOwner,\n onLoad,\n onError,\n ]);\n\n const defaultLoadingComponent = (\n
\n
\n
\n );\n\n const defaultErrorComponent = (\n
\n

{error}

\n
\n );\n\n // Render network badge inside the image\n const renderNetworkBadge = () => {\n if (!showNetwork || !networkName || networkPosition === \"outside\")\n return null;\n\n return (\n \n {networkName}\n \n );\n };\n\n // Render title inside the image\n const renderInnerTitle = () => {\n if (!showTitle || !title || titlePosition === \"outside\") return null;\n\n return (\n \n {title}\n {showOwner && owner && (\n
\n Owner: {owner.substring(0, 6)}...{owner.substring(owner.length - 4)}\n
\n )}\n \n );\n };\n\n // Render outside information (title, network, owner)\n const renderOutsideInfo = () => {\n if (\n (!showTitle || !title) &&\n (!showNetwork || !networkName || networkPosition !== \"outside\") &&\n (!showOwner || !owner || titlePosition !== \"outside\")\n ) {\n return null;\n }\n\n return (\n
\n {showTitle && title && titlePosition === \"outside\" && (\n
\n {title}\n
\n )}\n\n {showNetwork && networkName && networkPosition === \"outside\" && (\n \n Network: {networkName}\n
\n )}\n\n {showOwner && owner && titlePosition === \"outside\" && (\n
\n Owner: {owner.substring(0, 6)}...{owner.substring(owner.length - 4)}\n
\n )}\n \n );\n };\n\n // Apply different layouts\n const getContainerClasses = () => {\n switch (layout) {\n case \"compact\":\n return \"inline-block\";\n case \"detailed\":\n return \"flex flex-col overflow-hidden\";\n case \"card\":\n default:\n return \"\";\n }\n };\n\n // Calculate display dimensions that preserve aspect ratio\n const getDisplayDimensions = () => {\n // Handle percentage values\n const isPercentageWidth = typeof width === \"string\" && width.includes(\"%\");\n const isPercentageHeight = typeof height === \"string\" && height.includes(\"%\");\n \n if (isPercentageWidth || isPercentageHeight) {\n return { \n width: width || '100%', \n height: height || '100%', \n useContain: false,\n isPercentage: true\n };\n }\n \n const maxWidth = typeof width === \"number\" ? width : 300;\n const maxHeight = typeof height === \"number\" ? height : 300;\n \n // Check if we have image_details with dimensions\n if (metadata?.image_details?.width && metadata?.image_details?.height) {\n const originalAspectRatio = metadata.image_details.width / metadata.image_details.height;\n \n // Scale to fit within bounds while preserving aspect ratio\n const widthBasedHeight = maxWidth / originalAspectRatio;\n const heightBasedWidth = maxHeight * originalAspectRatio;\n \n if (widthBasedHeight <= maxHeight) {\n // Width is the limiting factor\n return { \n width: maxWidth, \n height: Math.round(widthBasedHeight),\n useContain: true, // Use contain to show full image\n isPercentage: false\n };\n } else {\n // Height is the limiting factor\n return { \n width: Math.round(heightBasedWidth), \n height: maxHeight,\n useContain: true,\n isPercentage: false\n };\n }\n }\n \n // No image_details, use provided dimensions\n return { width: maxWidth, height: maxHeight, useContain: false, isPercentage: false };\n };\n\n const displayDimensions = getDisplayDimensions();\n\n return (\n
\n \n {isLoading && (loadingComponent || defaultLoadingComponent)}\n\n {error && (errorComponent || defaultErrorComponent)}\n\n setImageUrl(PLACEHOLDER_IMAGE)}\n {...imageProps}\n />\n\n {renderInnerTitle()}\n {renderNetworkBadge()}\n
\n\n {renderOutsideInfo()}\n \n );\n}\n", "type": "registry:component", "target": "" }, @@ -86,13 +86,13 @@ }, { "path": "registry/mini-app/lib/nft-standards.ts", - "content": "import { parseAbi, type Address } from \"viem\";\n\n/**\n * Common NFT contract ABIs and standards\n */\n\n// Standard ERC721 functions\nexport const ERC721_ABI = {\n full: parseAbi([\n \"function tokenURI(uint256 tokenId) view returns (string)\",\n \"function name() view returns (string)\",\n \"function symbol() view returns (string)\",\n \"function ownerOf(uint256 tokenId) view returns (address)\",\n \"function balanceOf(address owner) view returns (uint256)\",\n \"function totalSupply() view returns (uint256)\",\n ]),\n \n // Individual functions for specific use cases\n tokenURI: parseAbi([\"function tokenURI(uint256 tokenId) view returns (string)\"]),\n name: parseAbi([\"function name() view returns (string)\"]),\n symbol: parseAbi([\"function symbol() view returns (string)\"]),\n ownerOf: parseAbi([\"function ownerOf(uint256 tokenId) view returns (address)\"]),\n};\n\n// Common price discovery functions across NFT contracts\nexport const PRICE_DISCOVERY_ABI = parseAbi([\n \"function mintPrice() view returns (uint256)\",\n \"function price() view returns (uint256)\",\n \"function MINT_PRICE() view returns (uint256)\",\n \"function getMintPrice() view returns (uint256)\",\n \"function publicMintPrice() view returns (uint256)\",\n]);\n\n// Common mint functions\nexport const MINT_ABI = parseAbi([\n \"function mint(uint256 amount) payable\",\n \"function mint(address to, uint256 amount) payable\",\n \"function publicMint(uint256 amount) payable\",\n \"function mintTo(address to, uint256 amount) payable\",\n]);\n\n// ERC20 ABI for token interactions\nexport const ERC20_ABI = parseAbi([\n \"function decimals() view returns (uint8)\",\n \"function symbol() view returns (string)\",\n \"function name() view returns (string)\",\n \"function totalSupply() view returns (uint256)\",\n \"function balanceOf(address owner) view returns (uint256)\",\n \"function allowance(address owner, address spender) view returns (uint256)\",\n \"function approve(address spender, uint256 value) returns (bool)\",\n \"function transfer(address to, uint256 value) returns (bool)\",\n \"function transferFrom(address from, address to, uint256 value) returns (bool)\",\n]);\n\n// Manifold contract detection ABI (kept separate as it's used on the main contract)\nexport const MANIFOLD_DETECTION_ABI = parseAbi([\n \"function getExtensions() view returns (address[])\",\n]);\n\n// Manifold extension contract full ABI\nexport const MANIFOLD_EXTENSION_ABI = [\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"tokenId\", type: \"uint256\" }\n ],\n name: \"tokenURI\",\n outputs: [{ name: \"uri\", type: \"string\" }],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"instanceId\", type: \"uint256\" }\n ],\n name: \"getClaim\",\n outputs: [\n {\n components: [\n { name: \"total\", type: \"uint32\" },\n { name: \"totalMax\", type: \"uint32\" },\n { name: \"walletMax\", type: \"uint32\" },\n { name: \"startDate\", type: \"uint48\" },\n { name: \"endDate\", type: \"uint48\" },\n { name: \"storageProtocol\", type: \"uint8\" },\n { name: \"merkleRoot\", type: \"bytes32\" },\n { name: \"location\", type: \"string\" },\n { name: \"tokenId\", type: \"uint256\" },\n { name: \"cost\", type: \"uint256\" },\n { name: \"paymentReceiver\", type: \"address\" },\n { name: \"erc20\", type: \"address\" },\n { name: \"signingAddress\", type: \"address\" }\n ],\n name: \"claim\",\n type: \"tuple\"\n }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"tokenId\", type: \"uint256\" }\n ],\n name: \"getClaimForToken\",\n outputs: [\n { name: \"instanceId\", type: \"uint256\" },\n {\n components: [\n { name: \"total\", type: \"uint32\" },\n { name: \"totalMax\", type: \"uint32\" },\n { name: \"walletMax\", type: \"uint32\" },\n { name: \"startDate\", type: \"uint48\" },\n { name: \"endDate\", type: \"uint48\" },\n { name: \"storageProtocol\", type: \"uint8\" },\n { name: \"merkleRoot\", type: \"bytes32\" },\n { name: \"location\", type: \"string\" },\n { name: \"tokenId\", type: \"uint256\" },\n { name: \"cost\", type: \"uint256\" },\n { name: \"paymentReceiver\", type: \"address\" },\n { name: \"erc20\", type: \"address\" },\n { name: \"signingAddress\", type: \"address\" }\n ],\n name: \"claim\",\n type: \"tuple\"\n }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"instanceId\", type: \"uint256\" },\n { name: \"mintIndex\", type: \"uint32\" },\n { name: \"merkleProof\", type: \"bytes32[]\" },\n { name: \"mintFor\", type: \"address\" }\n ],\n name: \"mint\",\n outputs: [],\n stateMutability: \"payable\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"MINT_FEE\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"MINT_FEE_MERKLE\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }\n] as const;\n\n// ERC165 interface detection\nexport const ERC165_ABI = parseAbi([\n \"function supportsInterface(bytes4 interfaceId) view returns (bool)\",\n]);\n\n// Known contract addresses\nexport const KNOWN_CONTRACTS = {\n // Manifold extension contracts\n manifoldExtension: \"0x26BBEA7803DcAc346D5F5f135b57Cf2c752A02bE\" as Address,\n \n // Add other known contracts here as needed\n} as const;\n\n// Interface IDs for contract detection\nexport const INTERFACE_IDS = {\n ERC165: \"0x01ffc9a7\",\n ERC721: \"0x80ac58cd\",\n ERC1155: \"0xd9b67a26\",\n ERC721Metadata: \"0x5b5e139f\",\n} as const;\n\n// IPFS Gateway configuration\nexport const IPFS_GATEWAYS = {\n default: \"https://ipfs.io/ipfs/\",\n cloudflare: \"https://cloudflare-ipfs.com/ipfs/\",\n pinata: \"https://gateway.pinata.cloud/ipfs/\",\n} as const;\n\n/**\n * Convert IPFS URL to HTTP gateway URL\n */\nexport function ipfsToHttp(url: string, gateway?: keyof typeof IPFS_GATEWAYS): string {\n const selectedGateway = gateway || \"default\";\n if (!url || !url.startsWith(\"ipfs://\")) {\n return url;\n }\n \n return url.replace(\"ipfs://\", IPFS_GATEWAYS[selectedGateway]);\n}\n\n/**\n * Check if a contract is likely an NFT contract by checking interface support\n */\nexport async function isNFTContract(\n client: any,\n contractAddress: Address\n): Promise<{ isNFT: boolean; type?: 'ERC721' | 'ERC1155' }> {\n try {\n // Try ERC165 supportsInterface\n const [supportsERC721, supportsERC1155] = await Promise.all([\n client.readContract({\n address: contractAddress,\n abi: ERC165_ABI,\n functionName: 'supportsInterface',\n args: [INTERFACE_IDS.ERC721],\n }).catch(() => false),\n client.readContract({\n address: contractAddress,\n abi: ERC165_ABI,\n functionName: 'supportsInterface',\n args: [INTERFACE_IDS.ERC1155],\n }).catch(() => false),\n ]);\n \n if (supportsERC721) return { isNFT: true, type: 'ERC721' };\n if (supportsERC1155) return { isNFT: true, type: 'ERC1155' };\n \n // Fallback: try to call name() function\n const name = await client.readContract({\n address: contractAddress,\n abi: ERC721_ABI.name,\n functionName: 'name',\n }).catch(() => null);\n \n return { isNFT: !!name };\n } catch {\n return { isNFT: false };\n }\n}", + "content": "import { parseAbi, type Address } from \"viem\";\n\n/**\n * Common NFT contract ABIs and standards\n */\n\n// Standard ERC721 functions\nexport const ERC721_ABI = {\n full: parseAbi([\n \"function tokenURI(uint256 tokenId) view returns (string)\",\n \"function name() view returns (string)\",\n \"function symbol() view returns (string)\",\n \"function ownerOf(uint256 tokenId) view returns (address)\",\n \"function balanceOf(address owner) view returns (uint256)\",\n \"function totalSupply() view returns (uint256)\",\n \"function baseURI() view returns (string)\",\n \"function contractURI() view returns (string)\",\n ]),\n \n // Individual functions for specific use cases\n tokenURI: parseAbi([\"function tokenURI(uint256 tokenId) view returns (string)\"]),\n name: parseAbi([\"function name() view returns (string)\"]),\n symbol: parseAbi([\"function symbol() view returns (string)\"]),\n ownerOf: parseAbi([\"function ownerOf(uint256 tokenId) view returns (address)\"]),\n baseURI: parseAbi([\"function baseURI() view returns (string)\"]),\n contractURI: parseAbi([\"function contractURI() view returns (string)\"]),\n};\n\n// Common price discovery functions across NFT contracts\nexport const PRICE_DISCOVERY_ABI = parseAbi([\n \"function mintPrice() view returns (uint256)\",\n \"function price() view returns (uint256)\",\n \"function MINT_PRICE() view returns (uint256)\",\n \"function getMintPrice() view returns (uint256)\",\n \"function publicMintPrice() view returns (uint256)\",\n]);\n\n// Common mint functions\nexport const MINT_ABI = parseAbi([\n \"function mint(uint256 amount) payable\",\n \"function mint(address to, uint256 amount) payable\",\n \"function publicMint(uint256 amount) payable\",\n \"function mintTo(address to, uint256 amount) payable\",\n]);\n\n// ERC1155 metadata function\nexport const ERC1155_ABI = {\n uri: parseAbi([\"function uri(uint256 tokenId) view returns (string)\"]),\n};\n\n// ERC20 ABI for token interactions\nexport const ERC20_ABI = parseAbi([\n \"function decimals() view returns (uint8)\",\n \"function symbol() view returns (string)\",\n \"function name() view returns (string)\",\n \"function totalSupply() view returns (uint256)\",\n \"function balanceOf(address owner) view returns (uint256)\",\n \"function allowance(address owner, address spender) view returns (uint256)\",\n \"function approve(address spender, uint256 value) returns (bool)\",\n \"function transfer(address to, uint256 value) returns (bool)\",\n \"function transferFrom(address from, address to, uint256 value) returns (bool)\",\n]);\n\n// Manifold contract detection ABI (kept separate as it's used on the main contract)\nexport const MANIFOLD_DETECTION_ABI = parseAbi([\n \"function getExtensions() view returns (address[])\",\n]);\n\n// Manifold extension contract full ABI\nexport const MANIFOLD_EXTENSION_ABI = [\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"tokenId\", type: \"uint256\" }\n ],\n name: \"tokenURI\",\n outputs: [{ name: \"uri\", type: \"string\" }],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"instanceId\", type: \"uint256\" }\n ],\n name: \"getClaim\",\n outputs: [\n {\n components: [\n { name: \"total\", type: \"uint32\" },\n { name: \"totalMax\", type: \"uint32\" },\n { name: \"walletMax\", type: \"uint32\" },\n { name: \"startDate\", type: \"uint48\" },\n { name: \"endDate\", type: \"uint48\" },\n { name: \"storageProtocol\", type: \"uint8\" },\n { name: \"merkleRoot\", type: \"bytes32\" },\n { name: \"location\", type: \"string\" },\n { name: \"tokenId\", type: \"uint256\" },\n { name: \"cost\", type: \"uint256\" },\n { name: \"paymentReceiver\", type: \"address\" },\n { name: \"erc20\", type: \"address\" },\n { name: \"signingAddress\", type: \"address\" }\n ],\n name: \"claim\",\n type: \"tuple\"\n }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"tokenId\", type: \"uint256\" }\n ],\n name: \"getClaimForToken\",\n outputs: [\n { name: \"instanceId\", type: \"uint256\" },\n {\n components: [\n { name: \"total\", type: \"uint32\" },\n { name: \"totalMax\", type: \"uint32\" },\n { name: \"walletMax\", type: \"uint32\" },\n { name: \"startDate\", type: \"uint48\" },\n { name: \"endDate\", type: \"uint48\" },\n { name: \"storageProtocol\", type: \"uint8\" },\n { name: \"merkleRoot\", type: \"bytes32\" },\n { name: \"location\", type: \"string\" },\n { name: \"tokenId\", type: \"uint256\" },\n { name: \"cost\", type: \"uint256\" },\n { name: \"paymentReceiver\", type: \"address\" },\n { name: \"erc20\", type: \"address\" },\n { name: \"signingAddress\", type: \"address\" }\n ],\n name: \"claim\",\n type: \"tuple\"\n }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"instanceId\", type: \"uint256\" },\n { name: \"mintIndex\", type: \"uint32\" },\n { name: \"merkleProof\", type: \"bytes32[]\" },\n { name: \"mintFor\", type: \"address\" }\n ],\n name: \"mint\",\n outputs: [],\n stateMutability: \"payable\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"MINT_FEE\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"MINT_FEE_MERKLE\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }\n] as const;\n\n// ERC165 interface detection\nexport const ERC165_ABI = parseAbi([\n \"function supportsInterface(bytes4 interfaceId) view returns (bool)\",\n]);\n\n// Known contract addresses\nexport const KNOWN_CONTRACTS = {\n // Manifold extension contracts\n manifoldExtension: \"0x26BBEA7803DcAc346D5F5f135b57Cf2c752A02bE\" as Address,\n \n // Add other known contracts here as needed\n} as const;\n\n// thirdweb OpenEditionERC721 ABI\nexport const THIRDWEB_OPENEDITONERC721_ABI = [\n {\n inputs: [\n { name: \"_receiver\", type: \"address\" },\n { name: \"_quantity\", type: \"uint256\" },\n { name: \"_currency\", type: \"address\" },\n { name: \"_pricePerToken\", type: \"uint256\" },\n {\n components: [\n { name: \"proof\", type: \"bytes32[]\" },\n { name: \"quantityLimitPerWallet\", type: \"uint256\" },\n { name: \"pricePerToken\", type: \"uint256\" },\n { name: \"currency\", type: \"address\" }\n ],\n name: \"_allowlistProof\",\n type: \"tuple\"\n },\n { name: \"_data\", type: \"bytes\" }\n ],\n name: \"claim\",\n outputs: [],\n stateMutability: \"payable\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"claimCondition\",\n outputs: [\n { name: \"currentStartId\", type: \"uint256\" },\n { name: \"count\", type: \"uint256\" }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [{ name: \"_conditionId\", type: \"uint256\" }],\n name: \"getClaimConditionById\",\n outputs: [\n {\n components: [\n { name: \"startTimestamp\", type: \"uint256\" },\n { name: \"maxClaimableSupply\", type: \"uint256\" },\n { name: \"supplyClaimed\", type: \"uint256\" },\n { name: \"quantityLimitPerWallet\", type: \"uint256\" },\n { name: \"merkleRoot\", type: \"bytes32\" },\n { name: \"pricePerToken\", type: \"uint256\" },\n { name: \"currency\", type: \"address\" },\n { name: \"metadata\", type: \"string\" }\n ],\n name: \"condition\",\n type: \"tuple\"\n }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"sharedMetadata\",\n outputs: [\n { name: \"name\", type: \"string\" },\n { name: \"description\", type: \"string\" },\n { name: \"image\", type: \"string\" },\n { name: \"animationUrl\", type: \"string\" }\n ],\n stateMutability: \"view\",\n type: \"function\"\n }\n] as const;\n\n// Native ETH address for thirdweb contracts\nexport const THIRDWEB_NATIVE_TOKEN = \"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE\" as Address;\n\n// Interface IDs for contract detection\nexport const INTERFACE_IDS = {\n ERC165: \"0x01ffc9a7\",\n ERC721: \"0x80ac58cd\",\n ERC1155: \"0xd9b67a26\",\n ERC721Metadata: \"0x5b5e139f\",\n} as const;\n\n// IPFS Gateway configuration\nexport const IPFS_GATEWAYS = {\n default: \"https://ipfs.io/ipfs/\",\n cloudflare: \"https://cloudflare-ipfs.com/ipfs/\",\n pinata: \"https://gateway.pinata.cloud/ipfs/\",\n} as const;\n\n/**\n * Convert IPFS URL to HTTP gateway URL\n */\nexport function ipfsToHttp(url: string, gateway?: keyof typeof IPFS_GATEWAYS): string {\n const selectedGateway = gateway || \"default\";\n if (!url || !url.startsWith(\"ipfs://\")) {\n return url;\n }\n \n return url.replace(\"ipfs://\", IPFS_GATEWAYS[selectedGateway]);\n}\n\n/**\n * Check if a contract is likely an NFT contract by checking interface support\n */\nexport async function isNFTContract(\n client: any,\n contractAddress: Address\n): Promise<{ isNFT: boolean; type?: 'ERC721' | 'ERC1155' }> {\n try {\n // Try ERC165 supportsInterface\n const [supportsERC721, supportsERC1155] = await Promise.all([\n client.readContract({\n address: contractAddress,\n abi: ERC165_ABI,\n functionName: 'supportsInterface',\n args: [INTERFACE_IDS.ERC721],\n }).catch(() => false),\n client.readContract({\n address: contractAddress,\n abi: ERC165_ABI,\n functionName: 'supportsInterface',\n args: [INTERFACE_IDS.ERC1155],\n }).catch(() => false),\n ]);\n \n if (supportsERC721) return { isNFT: true, type: 'ERC721' };\n if (supportsERC1155) return { isNFT: true, type: 'ERC1155' };\n \n // Fallback: try to call name() function\n const name = await client.readContract({\n address: contractAddress,\n abi: ERC721_ABI.name,\n functionName: 'name',\n }).catch(() => null);\n \n return { isNFT: !!name };\n } catch {\n return { isNFT: false };\n }\n}", "type": "registry:lib", "target": "" }, { - "path": "registry/mini-app/lib/manifold-utils.ts", - "content": "import { type Address, type PublicClient, getAddress } from \"viem\";\nimport { MANIFOLD_DETECTION_ABI, MANIFOLD_EXTENSION_ABI, KNOWN_CONTRACTS, ERC721_ABI } from \"@/registry/mini-app/lib/nft-standards\";\n\n/**\n * Manifold contract utilities\n */\n\nexport interface ManifoldDetectionResult {\n isManifold: boolean;\n extensionAddress?: Address;\n extensions?: Address[];\n}\n\nexport interface ManifoldClaim {\n total: number;\n totalMax: number;\n walletMax: number;\n startDate: bigint;\n endDate: bigint;\n storageProtocol: number;\n merkleRoot: `0x${string}`;\n location: string;\n tokenId: bigint;\n cost: bigint;\n paymentReceiver: Address;\n erc20: Address;\n signingAddress: Address;\n}\n\n/**\n * Detect if a contract is a Manifold contract with extensions\n */\nexport async function detectManifoldContract(\n client: PublicClient,\n contractAddress: Address\n): Promise {\n try {\n const extensions = await client.readContract({\n address: getAddress(contractAddress),\n abi: MANIFOLD_DETECTION_ABI,\n functionName: \"getExtensions\",\n }) as Address[];\n \n if (!extensions || extensions.length === 0) {\n return { isManifold: false };\n }\n \n // Check if it has the known Manifold extension\n const knownExtension = extensions.find(\n ext => ext.toLowerCase() === KNOWN_CONTRACTS.manifoldExtension.toLowerCase()\n );\n \n return {\n isManifold: true,\n extensionAddress: knownExtension || extensions[0],\n extensions,\n };\n } catch {\n return { isManifold: false };\n }\n}\n\n/**\n * Get token URI for a Manifold NFT\n */\nexport async function getManifoldTokenURI(\n client: PublicClient,\n contractAddress: Address,\n tokenId: string,\n extensionAddress?: Address\n): Promise {\n const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension;\n \n return await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"tokenURI\",\n args: [getAddress(contractAddress), BigInt(tokenId)],\n }) as string;\n}\n\n/**\n * Get token URI with automatic Manifold detection\n */\nexport async function getTokenURIWithManifoldSupport(\n client: PublicClient,\n contractAddress: Address,\n tokenId: string\n): Promise {\n // Try Manifold first\n const manifoldInfo = await detectManifoldContract(client, contractAddress);\n \n if (manifoldInfo.isManifold && manifoldInfo.extensionAddress) {\n try {\n return await getManifoldTokenURI(\n client,\n contractAddress,\n tokenId,\n manifoldInfo.extensionAddress\n );\n } catch (error) {\n console.warn(\"Failed to get Manifold tokenURI, falling back to standard\", error);\n }\n }\n \n // Fallback to standard ERC721 tokenURI\n return await client.readContract({\n address: getAddress(contractAddress),\n abi: ERC721_ABI.tokenURI,\n functionName: \"tokenURI\",\n args: [BigInt(tokenId)],\n }) as string;\n}\n\n/**\n * Get Manifold claim information\n */\nexport async function getManifoldClaim(\n client: PublicClient,\n contractAddress: Address,\n instanceId: string,\n extensionAddress?: Address\n): Promise {\n try {\n const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension;\n \n const claim = await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"getClaim\",\n args: [getAddress(contractAddress), BigInt(instanceId)],\n });\n \n return claim as unknown as ManifoldClaim;\n } catch {\n return null;\n }\n}\n\n/**\n * Get Manifold mint fee\n */\nexport async function getManifoldMintFee(\n client: PublicClient,\n extensionAddress?: Address\n): Promise {\n const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension;\n \n try {\n return await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"MINT_FEE\",\n }) as bigint;\n } catch {\n // Try MINT_FEE_MERKLE as fallback\n try {\n return await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"MINT_FEE_MERKLE\",\n }) as bigint;\n } catch {\n return BigInt(0);\n }\n }\n}\n\n/**\n * Check if an address is the zero address\n */\nexport function isZeroAddress(address: string): boolean {\n return address === \"0x0000000000000000000000000000000000000000\";\n}\n\n/**\n * Format instance ID and token ID for display\n */\nexport function formatManifoldTokenId(instanceId: string, tokenId: string): string {\n return `${instanceId}-${tokenId}`;\n}", + "path": "registry/mini-app/lib/nft-metadata-utils.ts", + "content": "import { type Address, type PublicClient, getAddress } from \"viem\";\nimport { \n MANIFOLD_DETECTION_ABI, \n MANIFOLD_EXTENSION_ABI, \n KNOWN_CONTRACTS, \n ERC721_ABI,\n ERC1155_ABI,\n THIRDWEB_OPENEDITONERC721_ABI\n} from \"@/registry/mini-app/lib/nft-standards\";\n\n/**\n * NFT metadata utilities with support for multiple standards and fallback mechanisms\n */\n\nexport type ProviderHint = \"manifold\" | \"thirdweb\" | \"standard\" | \"erc1155\";\n\nexport interface ManifoldDetectionResult {\n isManifold: boolean;\n extensionAddress?: Address;\n extensions?: Address[];\n}\n\nexport interface ManifoldClaim {\n total: number;\n totalMax: number;\n walletMax: number;\n startDate: bigint;\n endDate: bigint;\n storageProtocol: number;\n merkleRoot: `0x${string}`;\n location: string;\n tokenId: bigint;\n cost: bigint;\n paymentReceiver: Address;\n erc20: Address;\n signingAddress: Address;\n}\n\n/**\n * Detect if a contract is a Manifold contract with extensions\n */\nexport async function detectManifoldContract(\n client: PublicClient,\n contractAddress: Address\n): Promise {\n try {\n const extensions = await client.readContract({\n address: getAddress(contractAddress),\n abi: MANIFOLD_DETECTION_ABI,\n functionName: \"getExtensions\",\n }) as Address[];\n \n if (!extensions || extensions.length === 0) {\n return { isManifold: false };\n }\n \n // Check if it has the known Manifold extension\n const knownExtension = extensions.find(\n ext => ext.toLowerCase() === KNOWN_CONTRACTS.manifoldExtension.toLowerCase()\n );\n \n return {\n isManifold: true,\n extensionAddress: knownExtension || extensions[0],\n extensions,\n };\n } catch {\n return { isManifold: false };\n }\n}\n\n/**\n * Get token URI for a Manifold NFT\n */\nexport async function getManifoldTokenURI(\n client: PublicClient,\n contractAddress: Address,\n tokenId: string,\n extensionAddress?: Address\n): Promise {\n const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension;\n \n return await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"tokenURI\",\n args: [getAddress(contractAddress), BigInt(tokenId)],\n }) as string;\n}\n\n/**\n * Get token URI with automatic Manifold detection\n * @deprecated Use getTokenMetadataURL instead for more comprehensive metadata support\n */\nexport async function getTokenURIWithManifoldSupport(\n client: PublicClient,\n contractAddress: Address,\n tokenId: string\n): Promise {\n // Try Manifold first\n const manifoldInfo = await detectManifoldContract(client, contractAddress);\n \n if (manifoldInfo.isManifold && manifoldInfo.extensionAddress) {\n try {\n return await getManifoldTokenURI(\n client,\n contractAddress,\n tokenId,\n manifoldInfo.extensionAddress\n );\n } catch (error) {\n console.warn(\"Failed to get Manifold tokenURI, falling back to standard\", error);\n }\n }\n \n // Fallback to standard ERC721 tokenURI\n return await client.readContract({\n address: getAddress(contractAddress),\n abi: ERC721_ABI.tokenURI,\n functionName: \"tokenURI\",\n args: [BigInt(tokenId)],\n }) as string;\n}\n\n/**\n * Get token metadata URL with comprehensive fallback chain\n * Supports multiple NFT standards including ERC721, ERC1155, Manifold, and Thirdweb OpenEditions\n * \n * @param client - Public client for blockchain interactions\n * @param contractAddress - NFT contract address\n * @param tokenId - Token ID to fetch metadata for\n * @param providerHint - Optional hint about the contract type for optimization\n * @returns Token metadata URL or empty string if not found\n */\nexport async function getTokenMetadataURL(\n client: PublicClient,\n contractAddress: Address,\n tokenId: string,\n providerHint?: ProviderHint\n): Promise {\n const address = getAddress(contractAddress);\n const tokenIdBigInt = BigInt(tokenId);\n \n // If provider hint is given, try that first\n if (providerHint) {\n try {\n switch (providerHint) {\n case \"manifold\": {\n const manifoldInfo = await detectManifoldContract(client, address);\n if (manifoldInfo.isManifold && manifoldInfo.extensionAddress) {\n return await getManifoldTokenURI(client, address, tokenId, manifoldInfo.extensionAddress);\n }\n break;\n }\n case \"erc1155\": {\n const uri = await client.readContract({\n address,\n abi: ERC1155_ABI.uri,\n functionName: \"uri\",\n args: [tokenIdBigInt],\n }) as string;\n // ERC1155 URIs often have {id} placeholder that needs to be replaced\n return uri.replace(\"{id}\", tokenId.padStart(64, \"0\"));\n }\n case \"thirdweb\": {\n // Try sharedMetadata first for OpenEditions\n const metadata = await client.readContract({\n address,\n abi: THIRDWEB_OPENEDITONERC721_ABI,\n functionName: \"sharedMetadata\",\n }) as any;\n if (metadata && metadata.image) {\n // Construct metadata JSON from sharedMetadata\n const metadataJson = {\n name: metadata.name || \"\",\n description: metadata.description || \"\",\n image: metadata.image,\n animation_url: metadata.animationUrl || undefined,\n };\n // Return as data URI\n return `data:application/json;base64,${btoa(JSON.stringify(metadataJson))}`;\n }\n break;\n }\n }\n } catch (error) {\n console.debug(`Provider hint ${providerHint} failed, trying other methods`, error);\n }\n }\n \n // Comprehensive fallback chain\n const fallbackMethods = [\n // 1. Standard ERC721 tokenURI\n async () => {\n return await client.readContract({\n address,\n abi: ERC721_ABI.tokenURI,\n functionName: \"tokenURI\",\n args: [tokenIdBigInt],\n }) as string;\n },\n \n // 2. ERC1155 uri\n async () => {\n const uri = await client.readContract({\n address,\n abi: ERC1155_ABI.uri,\n functionName: \"uri\",\n args: [tokenIdBigInt],\n }) as string;\n // Replace {id} placeholder if present\n return uri.replace(\"{id}\", tokenId.padStart(64, \"0\"));\n },\n \n // 3. Manifold detection and tokenURI\n async () => {\n const manifoldInfo = await detectManifoldContract(client, address);\n if (manifoldInfo.isManifold && manifoldInfo.extensionAddress) {\n return await getManifoldTokenURI(client, address, tokenId, manifoldInfo.extensionAddress);\n }\n throw new Error(\"Not a Manifold contract\");\n },\n \n // 4. contractURI (for contracts with shared metadata)\n async () => {\n const contractURI = await client.readContract({\n address,\n abi: ERC721_ABI.contractURI,\n functionName: \"contractURI\",\n }) as string;\n // Note: contractURI typically contains collection-level metadata, not token-specific\n // This is a last resort fallback\n return contractURI;\n },\n \n // 5. Thirdweb sharedMetadata (for OpenEditions)\n async () => {\n const metadata = await client.readContract({\n address,\n abi: THIRDWEB_OPENEDITONERC721_ABI,\n functionName: \"sharedMetadata\",\n }) as any;\n \n if (metadata && metadata.image) {\n // Construct metadata JSON from sharedMetadata\n const metadataJson = {\n name: metadata.name || `Token #${tokenId}`,\n description: metadata.description || \"\",\n image: metadata.image,\n animation_url: metadata.animationUrl || undefined,\n };\n // Return as data URI\n return `data:application/json;base64,${btoa(JSON.stringify(metadataJson))}`;\n }\n throw new Error(\"No shared metadata found\");\n },\n \n // 6. baseURI + tokenId concatenation\n async () => {\n const baseURI = await client.readContract({\n address,\n abi: ERC721_ABI.baseURI,\n functionName: \"baseURI\",\n }) as string;\n \n if (baseURI) {\n // Ensure proper URL joining\n const separator = baseURI.endsWith(\"/\") ? \"\" : \"/\";\n return `${baseURI}${separator}${tokenId}`;\n }\n throw new Error(\"No baseURI found\");\n },\n ];\n \n // Try each method in order\n for (const method of fallbackMethods) {\n try {\n const result = await method();\n if (result && typeof result === \"string\" && result.length > 0) {\n return result;\n }\n } catch (error) {\n // Continue to next method\n continue;\n }\n }\n \n // If all methods fail, return empty string\n console.warn(`Could not fetch metadata for token ${tokenId} at ${contractAddress}`);\n return \"\";\n}\n\n/**\n * Get Manifold claim information\n */\nexport async function getManifoldClaim(\n client: PublicClient,\n contractAddress: Address,\n instanceId: string,\n extensionAddress?: Address\n): Promise {\n try {\n const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension;\n \n const claim = await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"getClaim\",\n args: [getAddress(contractAddress), BigInt(instanceId)],\n });\n \n return claim as unknown as ManifoldClaim;\n } catch {\n return null;\n }\n}\n\n/**\n * Get Manifold mint fee\n */\nexport async function getManifoldMintFee(\n client: PublicClient,\n extensionAddress?: Address\n): Promise {\n const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension;\n \n try {\n return await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"MINT_FEE\",\n }) as bigint;\n } catch {\n // Try MINT_FEE_MERKLE as fallback\n try {\n return await client.readContract({\n address: getAddress(extension),\n abi: MANIFOLD_EXTENSION_ABI,\n functionName: \"MINT_FEE_MERKLE\",\n }) as bigint;\n } catch {\n return BigInt(0);\n }\n }\n}\n\n/**\n * Check if an address is the zero address\n */\nexport function isZeroAddress(address: string): boolean {\n return address === \"0x0000000000000000000000000000000000000000\";\n}\n\n/**\n * Format instance ID and token ID for display\n */\nexport function formatManifoldTokenId(instanceId: string, tokenId: string): string {\n return `${instanceId}-${tokenId}`;\n}", "type": "registry:lib", "target": "" }, diff --git a/public/r/nft-standards.json b/public/r/nft-standards.json index 430fad34..7c7e5344 100644 --- a/public/r/nft-standards.json +++ b/public/r/nft-standards.json @@ -11,7 +11,7 @@ "files": [ { "path": "registry/mini-app/lib/nft-standards.ts", - "content": "import { parseAbi, type Address } from \"viem\";\n\n/**\n * Common NFT contract ABIs and standards\n */\n\n// Standard ERC721 functions\nexport const ERC721_ABI = {\n full: parseAbi([\n \"function tokenURI(uint256 tokenId) view returns (string)\",\n \"function name() view returns (string)\",\n \"function symbol() view returns (string)\",\n \"function ownerOf(uint256 tokenId) view returns (address)\",\n \"function balanceOf(address owner) view returns (uint256)\",\n \"function totalSupply() view returns (uint256)\",\n ]),\n \n // Individual functions for specific use cases\n tokenURI: parseAbi([\"function tokenURI(uint256 tokenId) view returns (string)\"]),\n name: parseAbi([\"function name() view returns (string)\"]),\n symbol: parseAbi([\"function symbol() view returns (string)\"]),\n ownerOf: parseAbi([\"function ownerOf(uint256 tokenId) view returns (address)\"]),\n};\n\n// Common price discovery functions across NFT contracts\nexport const PRICE_DISCOVERY_ABI = parseAbi([\n \"function mintPrice() view returns (uint256)\",\n \"function price() view returns (uint256)\",\n \"function MINT_PRICE() view returns (uint256)\",\n \"function getMintPrice() view returns (uint256)\",\n \"function publicMintPrice() view returns (uint256)\",\n]);\n\n// Common mint functions\nexport const MINT_ABI = parseAbi([\n \"function mint(uint256 amount) payable\",\n \"function mint(address to, uint256 amount) payable\",\n \"function publicMint(uint256 amount) payable\",\n \"function mintTo(address to, uint256 amount) payable\",\n]);\n\n// ERC20 ABI for token interactions\nexport const ERC20_ABI = parseAbi([\n \"function decimals() view returns (uint8)\",\n \"function symbol() view returns (string)\",\n \"function name() view returns (string)\",\n \"function totalSupply() view returns (uint256)\",\n \"function balanceOf(address owner) view returns (uint256)\",\n \"function allowance(address owner, address spender) view returns (uint256)\",\n \"function approve(address spender, uint256 value) returns (bool)\",\n \"function transfer(address to, uint256 value) returns (bool)\",\n \"function transferFrom(address from, address to, uint256 value) returns (bool)\",\n]);\n\n// Manifold contract detection ABI (kept separate as it's used on the main contract)\nexport const MANIFOLD_DETECTION_ABI = parseAbi([\n \"function getExtensions() view returns (address[])\",\n]);\n\n// Manifold extension contract full ABI\nexport const MANIFOLD_EXTENSION_ABI = [\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"tokenId\", type: \"uint256\" }\n ],\n name: \"tokenURI\",\n outputs: [{ name: \"uri\", type: \"string\" }],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"instanceId\", type: \"uint256\" }\n ],\n name: \"getClaim\",\n outputs: [\n {\n components: [\n { name: \"total\", type: \"uint32\" },\n { name: \"totalMax\", type: \"uint32\" },\n { name: \"walletMax\", type: \"uint32\" },\n { name: \"startDate\", type: \"uint48\" },\n { name: \"endDate\", type: \"uint48\" },\n { name: \"storageProtocol\", type: \"uint8\" },\n { name: \"merkleRoot\", type: \"bytes32\" },\n { name: \"location\", type: \"string\" },\n { name: \"tokenId\", type: \"uint256\" },\n { name: \"cost\", type: \"uint256\" },\n { name: \"paymentReceiver\", type: \"address\" },\n { name: \"erc20\", type: \"address\" },\n { name: \"signingAddress\", type: \"address\" }\n ],\n name: \"claim\",\n type: \"tuple\"\n }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"tokenId\", type: \"uint256\" }\n ],\n name: \"getClaimForToken\",\n outputs: [\n { name: \"instanceId\", type: \"uint256\" },\n {\n components: [\n { name: \"total\", type: \"uint32\" },\n { name: \"totalMax\", type: \"uint32\" },\n { name: \"walletMax\", type: \"uint32\" },\n { name: \"startDate\", type: \"uint48\" },\n { name: \"endDate\", type: \"uint48\" },\n { name: \"storageProtocol\", type: \"uint8\" },\n { name: \"merkleRoot\", type: \"bytes32\" },\n { name: \"location\", type: \"string\" },\n { name: \"tokenId\", type: \"uint256\" },\n { name: \"cost\", type: \"uint256\" },\n { name: \"paymentReceiver\", type: \"address\" },\n { name: \"erc20\", type: \"address\" },\n { name: \"signingAddress\", type: \"address\" }\n ],\n name: \"claim\",\n type: \"tuple\"\n }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"instanceId\", type: \"uint256\" },\n { name: \"mintIndex\", type: \"uint32\" },\n { name: \"merkleProof\", type: \"bytes32[]\" },\n { name: \"mintFor\", type: \"address\" }\n ],\n name: \"mint\",\n outputs: [],\n stateMutability: \"payable\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"MINT_FEE\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"MINT_FEE_MERKLE\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }\n] as const;\n\n// ERC165 interface detection\nexport const ERC165_ABI = parseAbi([\n \"function supportsInterface(bytes4 interfaceId) view returns (bool)\",\n]);\n\n// Known contract addresses\nexport const KNOWN_CONTRACTS = {\n // Manifold extension contracts\n manifoldExtension: \"0x26BBEA7803DcAc346D5F5f135b57Cf2c752A02bE\" as Address,\n \n // Add other known contracts here as needed\n} as const;\n\n// Interface IDs for contract detection\nexport const INTERFACE_IDS = {\n ERC165: \"0x01ffc9a7\",\n ERC721: \"0x80ac58cd\",\n ERC1155: \"0xd9b67a26\",\n ERC721Metadata: \"0x5b5e139f\",\n} as const;\n\n// IPFS Gateway configuration\nexport const IPFS_GATEWAYS = {\n default: \"https://ipfs.io/ipfs/\",\n cloudflare: \"https://cloudflare-ipfs.com/ipfs/\",\n pinata: \"https://gateway.pinata.cloud/ipfs/\",\n} as const;\n\n/**\n * Convert IPFS URL to HTTP gateway URL\n */\nexport function ipfsToHttp(url: string, gateway?: keyof typeof IPFS_GATEWAYS): string {\n const selectedGateway = gateway || \"default\";\n if (!url || !url.startsWith(\"ipfs://\")) {\n return url;\n }\n \n return url.replace(\"ipfs://\", IPFS_GATEWAYS[selectedGateway]);\n}\n\n/**\n * Check if a contract is likely an NFT contract by checking interface support\n */\nexport async function isNFTContract(\n client: any,\n contractAddress: Address\n): Promise<{ isNFT: boolean; type?: 'ERC721' | 'ERC1155' }> {\n try {\n // Try ERC165 supportsInterface\n const [supportsERC721, supportsERC1155] = await Promise.all([\n client.readContract({\n address: contractAddress,\n abi: ERC165_ABI,\n functionName: 'supportsInterface',\n args: [INTERFACE_IDS.ERC721],\n }).catch(() => false),\n client.readContract({\n address: contractAddress,\n abi: ERC165_ABI,\n functionName: 'supportsInterface',\n args: [INTERFACE_IDS.ERC1155],\n }).catch(() => false),\n ]);\n \n if (supportsERC721) return { isNFT: true, type: 'ERC721' };\n if (supportsERC1155) return { isNFT: true, type: 'ERC1155' };\n \n // Fallback: try to call name() function\n const name = await client.readContract({\n address: contractAddress,\n abi: ERC721_ABI.name,\n functionName: 'name',\n }).catch(() => null);\n \n return { isNFT: !!name };\n } catch {\n return { isNFT: false };\n }\n}", + "content": "import { parseAbi, type Address } from \"viem\";\n\n/**\n * Common NFT contract ABIs and standards\n */\n\n// Standard ERC721 functions\nexport const ERC721_ABI = {\n full: parseAbi([\n \"function tokenURI(uint256 tokenId) view returns (string)\",\n \"function name() view returns (string)\",\n \"function symbol() view returns (string)\",\n \"function ownerOf(uint256 tokenId) view returns (address)\",\n \"function balanceOf(address owner) view returns (uint256)\",\n \"function totalSupply() view returns (uint256)\",\n \"function baseURI() view returns (string)\",\n \"function contractURI() view returns (string)\",\n ]),\n \n // Individual functions for specific use cases\n tokenURI: parseAbi([\"function tokenURI(uint256 tokenId) view returns (string)\"]),\n name: parseAbi([\"function name() view returns (string)\"]),\n symbol: parseAbi([\"function symbol() view returns (string)\"]),\n ownerOf: parseAbi([\"function ownerOf(uint256 tokenId) view returns (address)\"]),\n baseURI: parseAbi([\"function baseURI() view returns (string)\"]),\n contractURI: parseAbi([\"function contractURI() view returns (string)\"]),\n};\n\n// Common price discovery functions across NFT contracts\nexport const PRICE_DISCOVERY_ABI = parseAbi([\n \"function mintPrice() view returns (uint256)\",\n \"function price() view returns (uint256)\",\n \"function MINT_PRICE() view returns (uint256)\",\n \"function getMintPrice() view returns (uint256)\",\n \"function publicMintPrice() view returns (uint256)\",\n]);\n\n// Common mint functions\nexport const MINT_ABI = parseAbi([\n \"function mint(uint256 amount) payable\",\n \"function mint(address to, uint256 amount) payable\",\n \"function publicMint(uint256 amount) payable\",\n \"function mintTo(address to, uint256 amount) payable\",\n]);\n\n// ERC1155 metadata function\nexport const ERC1155_ABI = {\n uri: parseAbi([\"function uri(uint256 tokenId) view returns (string)\"]),\n};\n\n// ERC20 ABI for token interactions\nexport const ERC20_ABI = parseAbi([\n \"function decimals() view returns (uint8)\",\n \"function symbol() view returns (string)\",\n \"function name() view returns (string)\",\n \"function totalSupply() view returns (uint256)\",\n \"function balanceOf(address owner) view returns (uint256)\",\n \"function allowance(address owner, address spender) view returns (uint256)\",\n \"function approve(address spender, uint256 value) returns (bool)\",\n \"function transfer(address to, uint256 value) returns (bool)\",\n \"function transferFrom(address from, address to, uint256 value) returns (bool)\",\n]);\n\n// Manifold contract detection ABI (kept separate as it's used on the main contract)\nexport const MANIFOLD_DETECTION_ABI = parseAbi([\n \"function getExtensions() view returns (address[])\",\n]);\n\n// Manifold extension contract full ABI\nexport const MANIFOLD_EXTENSION_ABI = [\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"tokenId\", type: \"uint256\" }\n ],\n name: \"tokenURI\",\n outputs: [{ name: \"uri\", type: \"string\" }],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"instanceId\", type: \"uint256\" }\n ],\n name: \"getClaim\",\n outputs: [\n {\n components: [\n { name: \"total\", type: \"uint32\" },\n { name: \"totalMax\", type: \"uint32\" },\n { name: \"walletMax\", type: \"uint32\" },\n { name: \"startDate\", type: \"uint48\" },\n { name: \"endDate\", type: \"uint48\" },\n { name: \"storageProtocol\", type: \"uint8\" },\n { name: \"merkleRoot\", type: \"bytes32\" },\n { name: \"location\", type: \"string\" },\n { name: \"tokenId\", type: \"uint256\" },\n { name: \"cost\", type: \"uint256\" },\n { name: \"paymentReceiver\", type: \"address\" },\n { name: \"erc20\", type: \"address\" },\n { name: \"signingAddress\", type: \"address\" }\n ],\n name: \"claim\",\n type: \"tuple\"\n }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"tokenId\", type: \"uint256\" }\n ],\n name: \"getClaimForToken\",\n outputs: [\n { name: \"instanceId\", type: \"uint256\" },\n {\n components: [\n { name: \"total\", type: \"uint32\" },\n { name: \"totalMax\", type: \"uint32\" },\n { name: \"walletMax\", type: \"uint32\" },\n { name: \"startDate\", type: \"uint48\" },\n { name: \"endDate\", type: \"uint48\" },\n { name: \"storageProtocol\", type: \"uint8\" },\n { name: \"merkleRoot\", type: \"bytes32\" },\n { name: \"location\", type: \"string\" },\n { name: \"tokenId\", type: \"uint256\" },\n { name: \"cost\", type: \"uint256\" },\n { name: \"paymentReceiver\", type: \"address\" },\n { name: \"erc20\", type: \"address\" },\n { name: \"signingAddress\", type: \"address\" }\n ],\n name: \"claim\",\n type: \"tuple\"\n }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [\n { name: \"creatorContractAddress\", type: \"address\" },\n { name: \"instanceId\", type: \"uint256\" },\n { name: \"mintIndex\", type: \"uint32\" },\n { name: \"merkleProof\", type: \"bytes32[]\" },\n { name: \"mintFor\", type: \"address\" }\n ],\n name: \"mint\",\n outputs: [],\n stateMutability: \"payable\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"MINT_FEE\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"MINT_FEE_MERKLE\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }\n] as const;\n\n// ERC165 interface detection\nexport const ERC165_ABI = parseAbi([\n \"function supportsInterface(bytes4 interfaceId) view returns (bool)\",\n]);\n\n// Known contract addresses\nexport const KNOWN_CONTRACTS = {\n // Manifold extension contracts\n manifoldExtension: \"0x26BBEA7803DcAc346D5F5f135b57Cf2c752A02bE\" as Address,\n \n // Add other known contracts here as needed\n} as const;\n\n// thirdweb OpenEditionERC721 ABI\nexport const THIRDWEB_OPENEDITONERC721_ABI = [\n {\n inputs: [\n { name: \"_receiver\", type: \"address\" },\n { name: \"_quantity\", type: \"uint256\" },\n { name: \"_currency\", type: \"address\" },\n { name: \"_pricePerToken\", type: \"uint256\" },\n {\n components: [\n { name: \"proof\", type: \"bytes32[]\" },\n { name: \"quantityLimitPerWallet\", type: \"uint256\" },\n { name: \"pricePerToken\", type: \"uint256\" },\n { name: \"currency\", type: \"address\" }\n ],\n name: \"_allowlistProof\",\n type: \"tuple\"\n },\n { name: \"_data\", type: \"bytes\" }\n ],\n name: \"claim\",\n outputs: [],\n stateMutability: \"payable\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"claimCondition\",\n outputs: [\n { name: \"currentStartId\", type: \"uint256\" },\n { name: \"count\", type: \"uint256\" }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [{ name: \"_conditionId\", type: \"uint256\" }],\n name: \"getClaimConditionById\",\n outputs: [\n {\n components: [\n { name: \"startTimestamp\", type: \"uint256\" },\n { name: \"maxClaimableSupply\", type: \"uint256\" },\n { name: \"supplyClaimed\", type: \"uint256\" },\n { name: \"quantityLimitPerWallet\", type: \"uint256\" },\n { name: \"merkleRoot\", type: \"bytes32\" },\n { name: \"pricePerToken\", type: \"uint256\" },\n { name: \"currency\", type: \"address\" },\n { name: \"metadata\", type: \"string\" }\n ],\n name: \"condition\",\n type: \"tuple\"\n }\n ],\n stateMutability: \"view\",\n type: \"function\"\n },\n {\n inputs: [],\n name: \"sharedMetadata\",\n outputs: [\n { name: \"name\", type: \"string\" },\n { name: \"description\", type: \"string\" },\n { name: \"image\", type: \"string\" },\n { name: \"animationUrl\", type: \"string\" }\n ],\n stateMutability: \"view\",\n type: \"function\"\n }\n] as const;\n\n// Native ETH address for thirdweb contracts\nexport const THIRDWEB_NATIVE_TOKEN = \"0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE\" as Address;\n\n// Interface IDs for contract detection\nexport const INTERFACE_IDS = {\n ERC165: \"0x01ffc9a7\",\n ERC721: \"0x80ac58cd\",\n ERC1155: \"0xd9b67a26\",\n ERC721Metadata: \"0x5b5e139f\",\n} as const;\n\n// IPFS Gateway configuration\nexport const IPFS_GATEWAYS = {\n default: \"https://ipfs.io/ipfs/\",\n cloudflare: \"https://cloudflare-ipfs.com/ipfs/\",\n pinata: \"https://gateway.pinata.cloud/ipfs/\",\n} as const;\n\n/**\n * Convert IPFS URL to HTTP gateway URL\n */\nexport function ipfsToHttp(url: string, gateway?: keyof typeof IPFS_GATEWAYS): string {\n const selectedGateway = gateway || \"default\";\n if (!url || !url.startsWith(\"ipfs://\")) {\n return url;\n }\n \n return url.replace(\"ipfs://\", IPFS_GATEWAYS[selectedGateway]);\n}\n\n/**\n * Check if a contract is likely an NFT contract by checking interface support\n */\nexport async function isNFTContract(\n client: any,\n contractAddress: Address\n): Promise<{ isNFT: boolean; type?: 'ERC721' | 'ERC1155' }> {\n try {\n // Try ERC165 supportsInterface\n const [supportsERC721, supportsERC1155] = await Promise.all([\n client.readContract({\n address: contractAddress,\n abi: ERC165_ABI,\n functionName: 'supportsInterface',\n args: [INTERFACE_IDS.ERC721],\n }).catch(() => false),\n client.readContract({\n address: contractAddress,\n abi: ERC165_ABI,\n functionName: 'supportsInterface',\n args: [INTERFACE_IDS.ERC1155],\n }).catch(() => false),\n ]);\n \n if (supportsERC721) return { isNFT: true, type: 'ERC721' };\n if (supportsERC1155) return { isNFT: true, type: 'ERC1155' };\n \n // Fallback: try to call name() function\n const name = await client.readContract({\n address: contractAddress,\n abi: ERC721_ABI.name,\n functionName: 'name',\n }).catch(() => null);\n \n return { isNFT: !!name };\n } catch {\n return { isNFT: false };\n }\n}", "type": "registry:lib" } ] diff --git a/public/r/registry.json b/public/r/registry.json index 9b4dedec..f267171e 100644 --- a/public/r/registry.json +++ b/public/r/registry.json @@ -222,7 +222,7 @@ "https://hellno-mini-app-ui.vercel.app/r/utils.json", "https://hellno-mini-app-ui.vercel.app/r/chains.json", "https://hellno-mini-app-ui.vercel.app/r/nft-standards.json", - "https://hellno-mini-app-ui.vercel.app/r/manifold-utils.json" + "https://hellno-mini-app-ui.vercel.app/r/nft-metadata-utils.json" ], "meta": { "use_cases": ["NFT Showcase", "Digital Collectibles Display"], @@ -343,11 +343,27 @@ "dependencies": ["viem"], "registryDependencies": [] }, + { + "name": "nft-metadata-utils", + "type": "registry:lib", + "title": "nftMetadataUtils", + "description": "NFT metadata utilities with support for multiple standards including Manifold, Thirdweb, ERC721, and ERC1155", + "files": [ + { + "path": "registry/mini-app/lib/nft-metadata-utils.ts", + "type": "registry:lib" + } + ], + "dependencies": ["viem"], + "registryDependencies": [ + "https://hellno-mini-app-ui.vercel.app/r/nft-standards.json" + ] + }, { "name": "manifold-utils", "type": "registry:lib", "title": "manifoldUtils", - "description": "Manifold contract detection and token URI utilities", + "description": "DEPRECATED: Use nft-metadata-utils instead. Kept for backward compatibility.", "files": [ { "path": "registry/mini-app/lib/manifold-utils.ts", @@ -356,7 +372,7 @@ ], "dependencies": ["viem"], "registryDependencies": [ - "https://hellno-mini-app-ui.vercel.app/r/nft-standards.json" + "https://hellno-mini-app-ui.vercel.app/r/nft-metadata-utils.json" ] }, { diff --git a/registry.json b/registry.json index 9b4dedec..f267171e 100644 --- a/registry.json +++ b/registry.json @@ -222,7 +222,7 @@ "https://hellno-mini-app-ui.vercel.app/r/utils.json", "https://hellno-mini-app-ui.vercel.app/r/chains.json", "https://hellno-mini-app-ui.vercel.app/r/nft-standards.json", - "https://hellno-mini-app-ui.vercel.app/r/manifold-utils.json" + "https://hellno-mini-app-ui.vercel.app/r/nft-metadata-utils.json" ], "meta": { "use_cases": ["NFT Showcase", "Digital Collectibles Display"], @@ -343,11 +343,27 @@ "dependencies": ["viem"], "registryDependencies": [] }, + { + "name": "nft-metadata-utils", + "type": "registry:lib", + "title": "nftMetadataUtils", + "description": "NFT metadata utilities with support for multiple standards including Manifold, Thirdweb, ERC721, and ERC1155", + "files": [ + { + "path": "registry/mini-app/lib/nft-metadata-utils.ts", + "type": "registry:lib" + } + ], + "dependencies": ["viem"], + "registryDependencies": [ + "https://hellno-mini-app-ui.vercel.app/r/nft-standards.json" + ] + }, { "name": "manifold-utils", "type": "registry:lib", "title": "manifoldUtils", - "description": "Manifold contract detection and token URI utilities", + "description": "DEPRECATED: Use nft-metadata-utils instead. Kept for backward compatibility.", "files": [ { "path": "registry/mini-app/lib/manifold-utils.ts", @@ -356,7 +372,7 @@ ], "dependencies": ["viem"], "registryDependencies": [ - "https://hellno-mini-app-ui.vercel.app/r/nft-standards.json" + "https://hellno-mini-app-ui.vercel.app/r/nft-metadata-utils.json" ] }, { diff --git a/registry/mini-app/blocks/daimo-pay-transfer/components/wagmi-provider.tsx b/registry/mini-app/blocks/daimo-pay-transfer/components/wagmi-provider.tsx index acd6d66e..51704476 100644 --- a/registry/mini-app/blocks/daimo-pay-transfer/components/wagmi-provider.tsx +++ b/registry/mini-app/blocks/daimo-pay-transfer/components/wagmi-provider.tsx @@ -1,7 +1,7 @@ "use client"; import { createConfig, http, injected, WagmiProvider } from "wagmi"; -import { base, degen, mainnet, optimism } from "wagmi/chains"; +import { base, degen, mainnet, optimism, celo } from "wagmi/chains"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { farcasterFrame } from "@farcaster/miniapp-wagmi-connector"; import { DaimoPayProvider, getDefaultConfig } from "@daimo/pay"; @@ -11,7 +11,7 @@ const alchemyApiKey = process.env.NEXT_PUBLIC_ALCHEMY_KEY; export const config = createConfig( getDefaultConfig({ appName: "hi", - chains: [base, degen, mainnet, optimism], + chains: [base, degen, mainnet, optimism, celo], additionalConnectors: [farcasterFrame(), injected()], transports: { [base.id]: http( @@ -19,6 +19,7 @@ export const config = createConfig( ? `https://base-mainnet.g.alchemy.com/v2/${alchemyApiKey}` : undefined, ), + [celo.id]: http(), // Use public RPC for Celo }, }), ); diff --git a/registry/mini-app/blocks/manifold-nft-mint/config.ts b/registry/mini-app/blocks/manifold-nft-mint/config.ts index 3b6dc3c7..3d79dfd6 100644 --- a/registry/mini-app/blocks/manifold-nft-mint/config.ts +++ b/registry/mini-app/blocks/manifold-nft-mint/config.ts @@ -2,7 +2,7 @@ import { type Address } from "viem"; import { Config } from "./manifold-nft-mint"; import { getPublicClient } from "@/registry/mini-app/lib/chains"; import { MANIFOLD_DETECTION_ABI, MANIFOLD_EXTENSION_ABI, KNOWN_CONTRACTS, ERC20_ABI } from "@/registry/mini-app/lib/nft-standards"; -import { isZeroAddress } from "@/registry/mini-app/lib/manifold-utils"; +import { isZeroAddress } from "@/registry/mini-app/lib/nft-metadata-utils"; export type Claim = { total: number; diff --git a/registry/mini-app/blocks/nft-card/nft-card.tsx b/registry/mini-app/blocks/nft-card/nft-card.tsx index 6e2559e2..6ef77aea 100644 --- a/registry/mini-app/blocks/nft-card/nft-card.tsx +++ b/registry/mini-app/blocks/nft-card/nft-card.tsx @@ -34,8 +34,8 @@ import { ipfsToHttp } from "@/registry/mini-app/lib/nft-standards"; import { - getTokenURIWithManifoldSupport -} from "@/registry/mini-app/lib/manifold-utils"; + getTokenMetadataURL +} from "@/registry/mini-app/lib/nft-metadata-utils"; // Base64 placeholder image const PLACEHOLDER_IMAGE = @@ -216,8 +216,8 @@ export function NFTCard({ } } - // Get tokenURI with automatic Manifold support - let metadataUrl = await getTokenURIWithManifoldSupport( + // Get tokenURI with comprehensive metadata support + let metadataUrl = await getTokenMetadataURL( client, getAddress(contractAddress) as Address, tokenId diff --git a/registry/mini-app/blocks/nft-mint-flow/lib/price-optimizer.ts b/registry/mini-app/blocks/nft-mint-flow/lib/price-optimizer.ts index 1be3cd98..d245bbda 100644 --- a/registry/mini-app/blocks/nft-mint-flow/lib/price-optimizer.ts +++ b/registry/mini-app/blocks/nft-mint-flow/lib/price-optimizer.ts @@ -1,6 +1,7 @@ import type { PublicClient } from "viem"; import type { NFTContractInfo, MintParams } from "@/registry/mini-app/blocks/nft-mint-flow/lib/types"; import { getProviderConfig } from "@/registry/mini-app/blocks/nft-mint-flow/lib/provider-configs"; +import { THIRDWEB_OPENEDITONERC721_ABI, THIRDWEB_NATIVE_TOKEN } from "@/registry/mini-app/lib/nft-standards"; /** * Optimized price discovery that batches RPC calls where possible @@ -231,6 +232,136 @@ export async function fetchPriceData( mintPrice: creatorFeePerNFT * amount, totalCost: (creatorFeePerNFT + protocolFeePerNFT) * amount }; + } else if (contractInfo.provider === "thirdweb") { + // thirdweb OpenEditionERC721 price discovery + try { + // First get the active claim condition ID + const claimCondition = await client.readContract({ + address: params.contractAddress, + abi: THIRDWEB_OPENEDITONERC721_ABI, + functionName: "claimCondition" + }); + + // Validate the response is an array with expected values + if (!Array.isArray(claimCondition) || claimCondition.length !== 2) { + throw new Error("Invalid claim condition response from contract"); + } + + const [currentStartId, count] = claimCondition as [bigint, bigint]; + + if (count === BigInt(0)) { + // No claim conditions set + return { mintPrice: BigInt(0), totalCost: BigInt(0) }; + } + + // Get the active claim condition (last one) + const activeConditionId = currentStartId + count - BigInt(1); + + const condition = await client.readContract({ + address: params.contractAddress, + abi: THIRDWEB_OPENEDITONERC721_ABI, + functionName: "getClaimConditionById", + args: [activeConditionId] + }); + + if (!condition || typeof condition !== "object") { + throw new Error("Invalid claim condition response"); + } + + const { + startTimestamp, + maxClaimableSupply, + supplyClaimed, + quantityLimitPerWallet, + merkleRoot, + pricePerToken, + currency, + metadata + } = condition as any; + + // Store claim condition in contractInfo for later use + contractInfo.claimCondition = { + id: Number(activeConditionId), + pricePerToken: pricePerToken as bigint, + currency: currency as string, + maxClaimableSupply: maxClaimableSupply as bigint, + merkleRoot: merkleRoot as `0x${string}`, + startTimestamp: Number(startTimestamp), + quantityLimitPerWallet: quantityLimitPerWallet as bigint + }; + + // Check if it's ERC20 payment + if (currency && currency.toLowerCase() !== THIRDWEB_NATIVE_TOKEN.toLowerCase()) { + // ERC20 payment + const [symbol, decimals, allowance, balance] = await Promise.all([ + client.readContract({ + address: currency, + abi: [{ name: "symbol", type: "function", inputs: [], outputs: [{ type: "string" }], stateMutability: "view" }], + functionName: "symbol" + }), + client.readContract({ + address: currency, + abi: [{ name: "decimals", type: "function", inputs: [], outputs: [{ type: "uint8" }], stateMutability: "view" }], + functionName: "decimals" + }), + params.recipient ? client.readContract({ + address: currency, + abi: [{ + name: "allowance", + type: "function", + inputs: [{ name: "owner", type: "address" }, { name: "spender", type: "address" }], + outputs: [{ type: "uint256" }], + stateMutability: "view" + }], + functionName: "allowance", + args: [params.recipient, params.contractAddress] + }).catch(() => BigInt(0)) : Promise.resolve(undefined), + params.recipient ? client.readContract({ + address: currency, + abi: [{ + name: "balanceOf", + type: "function", + inputs: [{ name: "owner", type: "address" }], + outputs: [{ type: "uint256" }], + stateMutability: "view" + }], + functionName: "balanceOf", + args: [params.recipient] + }).catch(() => BigInt(0)) : Promise.resolve(undefined) + ]); + + // Validate decimals + const validatedDecimals = Number(decimals); + if (isNaN(validatedDecimals) || validatedDecimals < 0 || validatedDecimals > 255) { + console.error(`Invalid ERC20 decimals for ${currency}:`, decimals); + throw new Error(`Invalid ERC20 decimals: ${decimals}`); + } + + return { + mintPrice: pricePerToken as bigint, + erc20Details: { + address: currency, + symbol: symbol as string, + decimals: validatedDecimals, + allowance: allowance as bigint, + balance: balance as bigint | undefined + }, + totalCost: BigInt(0), // No ETH needed for ERC20 payment + claim: contractInfo.claimCondition + }; + } else { + // ETH payment + const totalCost = (pricePerToken as bigint) * BigInt(params.amount || 1); + return { + mintPrice: pricePerToken as bigint, + totalCost, + claim: contractInfo.claimCondition + }; + } + } catch (err) { + console.error("Failed to fetch thirdweb price data:", err); + return { totalCost: BigInt(0) }; + } } else { // Generic price discovery - try multiple function names const functionNames = config.priceDiscovery.functionNames; diff --git a/registry/mini-app/blocks/nft-mint-flow/lib/provider-configs.ts b/registry/mini-app/blocks/nft-mint-flow/lib/provider-configs.ts index 912760b7..a18ea56a 100644 --- a/registry/mini-app/blocks/nft-mint-flow/lib/provider-configs.ts +++ b/registry/mini-app/blocks/nft-mint-flow/lib/provider-configs.ts @@ -1,6 +1,6 @@ -import { type Address } from "viem"; +import { type Address, maxUint256 } from "viem"; import type { ProviderConfig } from "@/registry/mini-app/blocks/nft-mint-flow/lib/types"; -import { MANIFOLD_EXTENSION_ABI, KNOWN_CONTRACTS, PRICE_DISCOVERY_ABI, MINT_ABI } from "@/registry/mini-app/lib/nft-standards"; +import { MANIFOLD_EXTENSION_ABI, KNOWN_CONTRACTS, PRICE_DISCOVERY_ABI, MINT_ABI, THIRDWEB_OPENEDITONERC721_ABI, THIRDWEB_NATIVE_TOKEN } from "@/registry/mini-app/lib/nft-standards"; export const PROVIDER_CONFIGS: Record = { manifold: { @@ -112,10 +112,85 @@ export const PROVIDER_CONFIGS: Record = { }, requiredParams: ["contractAddress", "chainId"], supportsERC20: false + }, + + thirdweb: { + name: "thirdweb", + priceDiscovery: { + abis: [THIRDWEB_OPENEDITONERC721_ABI], + functionNames: ["claimCondition", "getClaimConditionById"], + requiresInstanceId: false + }, + mintConfig: { + abi: THIRDWEB_OPENEDITONERC721_ABI, + functionName: "claim", + buildArgs: (params) => { + // This will be overridden by getProviderConfig for thirdweb + // when contractInfo with claimCondition is available + return [ + params.recipient || params.contractAddress, // _receiver + BigInt(params.amount || 1), // _quantity + THIRDWEB_NATIVE_TOKEN, // _currency (default to ETH) + BigInt(0), // _pricePerToken (default to 0) + { + proof: params.merkleProof || [], + quantityLimitPerWallet: maxUint256, + pricePerToken: maxUint256, + currency: "0x0000000000000000000000000000000000000000" + }, // _allowlistProof + "0x" // _data + ]; + }, + calculateValue: (price, params) => { + // This will be overridden by getProviderConfig for thirdweb + // Default assumes native token payment + return price * BigInt(params.amount || 1); + } + }, + requiredParams: ["contractAddress", "chainId"], + supportsERC20: true } }; // Helper to get config by provider name -export function getProviderConfig(provider: string): ProviderConfig { - return PROVIDER_CONFIGS[provider] || PROVIDER_CONFIGS.generic; +export function getProviderConfig(provider: string, contractInfo?: any): ProviderConfig { + const baseConfig = PROVIDER_CONFIGS[provider] || PROVIDER_CONFIGS.generic; + + // For thirdweb, we need to inject the claim condition data + if (provider === "thirdweb" && contractInfo?.claimCondition) { + return { + ...baseConfig, + mintConfig: { + ...baseConfig.mintConfig, + buildArgs: (params) => { + const pricePerToken = contractInfo.claimCondition.pricePerToken || BigInt(0); + const currency = contractInfo.claimCondition.currency || THIRDWEB_NATIVE_TOKEN; + + return [ + params.recipient || params.contractAddress, // _receiver + BigInt(params.amount || 1), // _quantity + currency, // _currency + pricePerToken, // _pricePerToken + { + proof: params.merkleProof || [], + quantityLimitPerWallet: maxUint256, + pricePerToken: maxUint256, + currency: "0x0000000000000000000000000000000000000000" + }, // _allowlistProof + "0x" // _data + ]; + }, + calculateValue: (price, params) => { + const currency = contractInfo.claimCondition?.currency; + + if (!currency || currency.toLowerCase() === THIRDWEB_NATIVE_TOKEN.toLowerCase()) { + return price * BigInt(params.amount || 1); + } + return BigInt(0); + } + } + }; + } + + return baseConfig; } \ No newline at end of file diff --git a/registry/mini-app/blocks/nft-mint-flow/lib/provider-detector.ts b/registry/mini-app/blocks/nft-mint-flow/lib/provider-detector.ts index 4bc5e254..1304f633 100644 --- a/registry/mini-app/blocks/nft-mint-flow/lib/provider-detector.ts +++ b/registry/mini-app/blocks/nft-mint-flow/lib/provider-detector.ts @@ -5,7 +5,8 @@ import { getPublicClient } from "@/registry/mini-app/lib/chains"; import { ERC165_ABI, INTERFACE_IDS, - MANIFOLD_DETECTION_ABI + MANIFOLD_DETECTION_ABI, + THIRDWEB_OPENEDITONERC721_ABI } from "@/registry/mini-app/lib/nft-standards"; // Re-export from shared library for backward compatibility @@ -110,6 +111,34 @@ export async function detectNFTProvider(params: MintParams): Promise null), + client.readContract({ + address: contractAddress, + abi: THIRDWEB_OPENEDITONERC721_ABI, + functionName: "sharedMetadata" + }).catch(() => null) + ]); + + // If both functions exist, it's a thirdweb contract + if (claimConditionResult !== null && sharedMetadataResult !== null) { + return { + provider: "thirdweb", + isERC1155: isERC1155 as boolean, + isERC721: isERC721 as boolean + }; + } + } catch { + // Not a thirdweb contract, continue detection + } + // TODO: Add detection for OpenSea, Zora, etc. // For now, return generic return { @@ -160,6 +189,19 @@ export function validateParameters(params: MintParams, contractInfo: NFTContract } } + if (contractInfo.provider === "thirdweb") { + if (contractInfo.claimCondition?.merkleRoot && contractInfo.claimCondition.merkleRoot !== "0x0000000000000000000000000000000000000000000000000000000000000000") { + errors.push("This NFT requires a merkle proof for minting - not supported yet"); + } + + if (contractInfo.claimCondition?.startTimestamp) { + const now = Math.floor(Date.now() / 1000); + if (now < contractInfo.claimCondition.startTimestamp) { + errors.push("Claim has not started yet"); + } + } + } + return { isValid: missingParams.length === 0 && errors.length === 0, missingParams, diff --git a/registry/mini-app/blocks/nft-mint-flow/lib/test-contracts.ts b/registry/mini-app/blocks/nft-mint-flow/lib/test-contracts.ts index b90fd283..45fef403 100644 --- a/registry/mini-app/blocks/nft-mint-flow/lib/test-contracts.ts +++ b/registry/mini-app/blocks/nft-mint-flow/lib/test-contracts.ts @@ -67,6 +67,34 @@ export const testContracts: TestContract[] = [ mintFeeETH: "0.0005", // ETH platform fee nftCost: "1" // 1 USDC } + }, + { + name: "thirdweb Free Mint", + params: { + contractAddress: "0x9D7FEB3351c71D3E87b059FE088e2B73C951727A", // Example thirdweb contract + chainId: 8453, // Base + provider: "thirdweb" + }, + expected: { + provider: "thirdweb", + hasERC20: false, + mintFeeETH: "0", // Free mint + totalETH: "0" + } + }, + { + name: "thirdweb ETH Payment", + params: { + contractAddress: "0x1234567890123456789012345678901234567890", // Example with ETH payment + chainId: 8453, + provider: "thirdweb" + }, + expected: { + provider: "thirdweb", + hasERC20: false, + mintFeeETH: "0.001", // Example price + totalETH: "0.001" + } } ]; @@ -100,12 +128,12 @@ export async function runContractTests() { await delay(1000); try { - // Create contract info + // Create contract info based on provider const contractInfo: NFTContractInfo = { provider: test.params.provider as any, - isERC1155: true, - isERC721: false, - extensionAddress: KNOWN_CONTRACTS.manifoldExtension + isERC1155: test.params.provider === "manifold", + isERC721: test.params.provider === "thirdweb" || test.params.provider === "nfts2me", + extensionAddress: test.params.provider === "manifold" ? KNOWN_CONTRACTS.manifoldExtension : undefined }; // Fetch price data diff --git a/registry/mini-app/blocks/nft-mint-flow/lib/types.ts b/registry/mini-app/blocks/nft-mint-flow/lib/types.ts index b17eed3b..4214de55 100644 --- a/registry/mini-app/blocks/nft-mint-flow/lib/types.ts +++ b/registry/mini-app/blocks/nft-mint-flow/lib/types.ts @@ -1,6 +1,6 @@ import type { Address } from "viem"; -export type NFTProvider = "manifold" | "opensea" | "zora" | "generic" | "nfts2me"; +export type NFTProvider = "manifold" | "opensea" | "zora" | "generic" | "nfts2me" | "thirdweb"; export interface ProviderConfig { name: NFTProvider; @@ -55,6 +55,15 @@ export interface NFTContractInfo { endDate: number; walletMax: number; }; + claimCondition?: { + id: number; + pricePerToken: bigint; + currency: Address; + maxClaimableSupply: bigint; + merkleRoot: `0x${string}`; + startTimestamp: number; + quantityLimitPerWallet: bigint; + }; } export interface ValidationResult { diff --git a/registry/mini-app/blocks/nft-mint-flow/nft-mint-button.tsx b/registry/mini-app/blocks/nft-mint-flow/nft-mint-button.tsx index 3105e84c..f0ad7a8c 100644 --- a/registry/mini-app/blocks/nft-mint-flow/nft-mint-button.tsx +++ b/registry/mini-app/blocks/nft-mint-flow/nft-mint-button.tsx @@ -34,6 +34,7 @@ import { validateParameters, getClientForChain, } from "@/registry/mini-app/blocks/nft-mint-flow/lib/provider-detector"; +import { getChainById } from "@/registry/mini-app/lib/chains"; import { getProviderConfig } from "@/registry/mini-app/blocks/nft-mint-flow/lib/provider-configs"; import { fetchPriceData } from "@/registry/mini-app/blocks/nft-mint-flow/lib/price-optimizer"; import { mintReducer, initialState, type MintStep } from "@/registry/mini-app/blocks/nft-mint-flow/lib/mint-reducer"; @@ -237,12 +238,13 @@ export function NFTMintButton({ // Get provider config const providerConfig = contractInfo - ? getProviderConfig(contractInfo.provider) + ? getProviderConfig(contractInfo.provider, contractInfo) : null; // Check if user is on the correct network const isCorrectNetwork = chain?.id === chainId; - const networkName = chainId === 1 ? "Ethereum" : chainId === 8453 ? "Base" : "Unknown"; + const targetChain = getChainById(chainId); + const networkName = targetChain.name || "Unknown"; // Handle transaction status updates React.useEffect(() => { diff --git a/registry/mini-app/lib/manifold-utils.ts b/registry/mini-app/lib/manifold-utils.ts index 7b91baea..2319ac2b 100644 --- a/registry/mini-app/lib/manifold-utils.ts +++ b/registry/mini-app/lib/manifold-utils.ts @@ -1,181 +1,8 @@ -import { type Address, type PublicClient, getAddress } from "viem"; -import { MANIFOLD_DETECTION_ABI, MANIFOLD_EXTENSION_ABI, KNOWN_CONTRACTS, ERC721_ABI } from "@/registry/mini-app/lib/nft-standards"; - -/** - * Manifold contract utilities - */ - -export interface ManifoldDetectionResult { - isManifold: boolean; - extensionAddress?: Address; - extensions?: Address[]; -} - -export interface ManifoldClaim { - total: number; - totalMax: number; - walletMax: number; - startDate: bigint; - endDate: bigint; - storageProtocol: number; - merkleRoot: `0x${string}`; - location: string; - tokenId: bigint; - cost: bigint; - paymentReceiver: Address; - erc20: Address; - signingAddress: Address; -} - -/** - * Detect if a contract is a Manifold contract with extensions - */ -export async function detectManifoldContract( - client: PublicClient, - contractAddress: Address -): Promise { - try { - const extensions = await client.readContract({ - address: getAddress(contractAddress), - abi: MANIFOLD_DETECTION_ABI, - functionName: "getExtensions", - }) as Address[]; - - if (!extensions || extensions.length === 0) { - return { isManifold: false }; - } - - // Check if it has the known Manifold extension - const knownExtension = extensions.find( - ext => ext.toLowerCase() === KNOWN_CONTRACTS.manifoldExtension.toLowerCase() - ); - - return { - isManifold: true, - extensionAddress: knownExtension || extensions[0], - extensions, - }; - } catch { - return { isManifold: false }; - } -} - -/** - * Get token URI for a Manifold NFT - */ -export async function getManifoldTokenURI( - client: PublicClient, - contractAddress: Address, - tokenId: string, - extensionAddress?: Address -): Promise { - const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension; - - return await client.readContract({ - address: getAddress(extension), - abi: MANIFOLD_EXTENSION_ABI, - functionName: "tokenURI", - args: [getAddress(contractAddress), BigInt(tokenId)], - }) as string; -} - /** - * Get token URI with automatic Manifold detection + * @deprecated This file is kept for backward compatibility. + * Please use nft-metadata-utils.ts instead which provides the same functionality plus enhanced metadata support. */ -export async function getTokenURIWithManifoldSupport( - client: PublicClient, - contractAddress: Address, - tokenId: string -): Promise { - // Try Manifold first - const manifoldInfo = await detectManifoldContract(client, contractAddress); - - if (manifoldInfo.isManifold && manifoldInfo.extensionAddress) { - try { - return await getManifoldTokenURI( - client, - contractAddress, - tokenId, - manifoldInfo.extensionAddress - ); - } catch (error) { - console.warn("Failed to get Manifold tokenURI, falling back to standard", error); - } - } - - // Fallback to standard ERC721 tokenURI - return await client.readContract({ - address: getAddress(contractAddress), - abi: ERC721_ABI.tokenURI, - functionName: "tokenURI", - args: [BigInt(tokenId)], - }) as string; -} +export * from "@/registry/mini-app/lib/nft-metadata-utils"; -/** - * Get Manifold claim information - */ -export async function getManifoldClaim( - client: PublicClient, - contractAddress: Address, - instanceId: string, - extensionAddress?: Address -): Promise { - try { - const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension; - - const claim = await client.readContract({ - address: getAddress(extension), - abi: MANIFOLD_EXTENSION_ABI, - functionName: "getClaim", - args: [getAddress(contractAddress), BigInt(instanceId)], - }); - - return claim as unknown as ManifoldClaim; - } catch { - return null; - } -} - -/** - * Get Manifold mint fee - */ -export async function getManifoldMintFee( - client: PublicClient, - extensionAddress?: Address -): Promise { - const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension; - - try { - return await client.readContract({ - address: getAddress(extension), - abi: MANIFOLD_EXTENSION_ABI, - functionName: "MINT_FEE", - }) as bigint; - } catch { - // Try MINT_FEE_MERKLE as fallback - try { - return await client.readContract({ - address: getAddress(extension), - abi: MANIFOLD_EXTENSION_ABI, - functionName: "MINT_FEE_MERKLE", - }) as bigint; - } catch { - return BigInt(0); - } - } -} - -/** - * Check if an address is the zero address - */ -export function isZeroAddress(address: string): boolean { - return address === "0x0000000000000000000000000000000000000000"; -} - -/** - * Format instance ID and token ID for display - */ -export function formatManifoldTokenId(instanceId: string, tokenId: string): string { - return `${instanceId}-${tokenId}`; -} \ No newline at end of file +// Re-export the deprecated function name for backward compatibility +export { getTokenMetadataURL as getTokenURIWithManifoldSupport } from "@/registry/mini-app/lib/nft-metadata-utils"; \ No newline at end of file diff --git a/registry/mini-app/lib/nft-metadata-utils.ts b/registry/mini-app/lib/nft-metadata-utils.ts new file mode 100644 index 00000000..30378618 --- /dev/null +++ b/registry/mini-app/lib/nft-metadata-utils.ts @@ -0,0 +1,359 @@ +import { type Address, type PublicClient, getAddress } from "viem"; +import { + MANIFOLD_DETECTION_ABI, + MANIFOLD_EXTENSION_ABI, + KNOWN_CONTRACTS, + ERC721_ABI, + ERC1155_ABI, + THIRDWEB_OPENEDITONERC721_ABI +} from "@/registry/mini-app/lib/nft-standards"; + +/** + * NFT metadata utilities with support for multiple standards and fallback mechanisms + */ + +export type ProviderHint = "manifold" | "thirdweb" | "standard" | "erc1155"; + +export interface ManifoldDetectionResult { + isManifold: boolean; + extensionAddress?: Address; + extensions?: Address[]; +} + +export interface ManifoldClaim { + total: number; + totalMax: number; + walletMax: number; + startDate: bigint; + endDate: bigint; + storageProtocol: number; + merkleRoot: `0x${string}`; + location: string; + tokenId: bigint; + cost: bigint; + paymentReceiver: Address; + erc20: Address; + signingAddress: Address; +} + +/** + * Detect if a contract is a Manifold contract with extensions + */ +export async function detectManifoldContract( + client: PublicClient, + contractAddress: Address +): Promise { + try { + const extensions = await client.readContract({ + address: getAddress(contractAddress), + abi: MANIFOLD_DETECTION_ABI, + functionName: "getExtensions", + }) as Address[]; + + if (!extensions || extensions.length === 0) { + return { isManifold: false }; + } + + // Check if it has the known Manifold extension + const knownExtension = extensions.find( + ext => ext.toLowerCase() === KNOWN_CONTRACTS.manifoldExtension.toLowerCase() + ); + + return { + isManifold: true, + extensionAddress: knownExtension || extensions[0], + extensions, + }; + } catch { + return { isManifold: false }; + } +} + +/** + * Get token URI for a Manifold NFT + */ +export async function getManifoldTokenURI( + client: PublicClient, + contractAddress: Address, + tokenId: string, + extensionAddress?: Address +): Promise { + const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension; + + return await client.readContract({ + address: getAddress(extension), + abi: MANIFOLD_EXTENSION_ABI, + functionName: "tokenURI", + args: [getAddress(contractAddress), BigInt(tokenId)], + }) as string; +} + +/** + * Get token URI with automatic Manifold detection + * @deprecated Use getTokenMetadataURL instead for more comprehensive metadata support + */ +export async function getTokenURIWithManifoldSupport( + client: PublicClient, + contractAddress: Address, + tokenId: string +): Promise { + // Try Manifold first + const manifoldInfo = await detectManifoldContract(client, contractAddress); + + if (manifoldInfo.isManifold && manifoldInfo.extensionAddress) { + try { + return await getManifoldTokenURI( + client, + contractAddress, + tokenId, + manifoldInfo.extensionAddress + ); + } catch (error) { + console.warn("Failed to get Manifold tokenURI, falling back to standard", error); + } + } + + // Fallback to standard ERC721 tokenURI + return await client.readContract({ + address: getAddress(contractAddress), + abi: ERC721_ABI.tokenURI, + functionName: "tokenURI", + args: [BigInt(tokenId)], + }) as string; +} + +/** + * Get token metadata URL with comprehensive fallback chain + * Supports multiple NFT standards including ERC721, ERC1155, Manifold, and Thirdweb OpenEditions + * + * @param client - Public client for blockchain interactions + * @param contractAddress - NFT contract address + * @param tokenId - Token ID to fetch metadata for + * @param providerHint - Optional hint about the contract type for optimization + * @returns Token metadata URL or empty string if not found + */ +export async function getTokenMetadataURL( + client: PublicClient, + contractAddress: Address, + tokenId: string, + providerHint?: ProviderHint +): Promise { + const address = getAddress(contractAddress); + const tokenIdBigInt = BigInt(tokenId); + + // If provider hint is given, try that first + if (providerHint) { + try { + switch (providerHint) { + case "manifold": { + const manifoldInfo = await detectManifoldContract(client, address); + if (manifoldInfo.isManifold && manifoldInfo.extensionAddress) { + return await getManifoldTokenURI(client, address, tokenId, manifoldInfo.extensionAddress); + } + break; + } + case "erc1155": { + const uri = await client.readContract({ + address, + abi: ERC1155_ABI.uri, + functionName: "uri", + args: [tokenIdBigInt], + }) as string; + // ERC1155 URIs often have {id} placeholder that needs to be replaced + return uri.replace("{id}", tokenId.padStart(64, "0")); + } + case "thirdweb": { + // Try sharedMetadata first for OpenEditions + const metadata = await client.readContract({ + address, + abi: THIRDWEB_OPENEDITONERC721_ABI, + functionName: "sharedMetadata", + }) as any; + if (metadata && metadata.image) { + // Construct metadata JSON from sharedMetadata + const metadataJson = { + name: metadata.name || "", + description: metadata.description || "", + image: metadata.image, + animation_url: metadata.animationUrl || undefined, + }; + // Return as data URI + return `data:application/json;base64,${btoa(JSON.stringify(metadataJson))}`; + } + break; + } + } + } catch (error) { + console.debug(`Provider hint ${providerHint} failed, trying other methods`, error); + } + } + + // Comprehensive fallback chain + const fallbackMethods = [ + // 1. Standard ERC721 tokenURI + async () => { + return await client.readContract({ + address, + abi: ERC721_ABI.tokenURI, + functionName: "tokenURI", + args: [tokenIdBigInt], + }) as string; + }, + + // 2. ERC1155 uri + async () => { + const uri = await client.readContract({ + address, + abi: ERC1155_ABI.uri, + functionName: "uri", + args: [tokenIdBigInt], + }) as string; + // Replace {id} placeholder if present + return uri.replace("{id}", tokenId.padStart(64, "0")); + }, + + // 3. Manifold detection and tokenURI + async () => { + const manifoldInfo = await detectManifoldContract(client, address); + if (manifoldInfo.isManifold && manifoldInfo.extensionAddress) { + return await getManifoldTokenURI(client, address, tokenId, manifoldInfo.extensionAddress); + } + throw new Error("Not a Manifold contract"); + }, + + // 4. contractURI (for contracts with shared metadata) + async () => { + const contractURI = await client.readContract({ + address, + abi: ERC721_ABI.contractURI, + functionName: "contractURI", + }) as string; + // Note: contractURI typically contains collection-level metadata, not token-specific + // This is a last resort fallback + return contractURI; + }, + + // 5. Thirdweb sharedMetadata (for OpenEditions) + async () => { + const metadata = await client.readContract({ + address, + abi: THIRDWEB_OPENEDITONERC721_ABI, + functionName: "sharedMetadata", + }) as any; + + if (metadata && metadata.image) { + // Construct metadata JSON from sharedMetadata + const metadataJson = { + name: metadata.name || `Token #${tokenId}`, + description: metadata.description || "", + image: metadata.image, + animation_url: metadata.animationUrl || undefined, + }; + // Return as data URI + return `data:application/json;base64,${btoa(JSON.stringify(metadataJson))}`; + } + throw new Error("No shared metadata found"); + }, + + // 6. baseURI + tokenId concatenation + async () => { + const baseURI = await client.readContract({ + address, + abi: ERC721_ABI.baseURI, + functionName: "baseURI", + }) as string; + + if (baseURI) { + // Ensure proper URL joining + const separator = baseURI.endsWith("/") ? "" : "/"; + return `${baseURI}${separator}${tokenId}`; + } + throw new Error("No baseURI found"); + }, + ]; + + // Try each method in order + for (const method of fallbackMethods) { + try { + const result = await method(); + if (result && typeof result === "string" && result.length > 0) { + return result; + } + } catch (error) { + // Continue to next method + continue; + } + } + + // If all methods fail, return empty string + console.warn(`Could not fetch metadata for token ${tokenId} at ${contractAddress}`); + return ""; +} + +/** + * Get Manifold claim information + */ +export async function getManifoldClaim( + client: PublicClient, + contractAddress: Address, + instanceId: string, + extensionAddress?: Address +): Promise { + try { + const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension; + + const claim = await client.readContract({ + address: getAddress(extension), + abi: MANIFOLD_EXTENSION_ABI, + functionName: "getClaim", + args: [getAddress(contractAddress), BigInt(instanceId)], + }); + + return claim as unknown as ManifoldClaim; + } catch { + return null; + } +} + +/** + * Get Manifold mint fee + */ +export async function getManifoldMintFee( + client: PublicClient, + extensionAddress?: Address +): Promise { + const extension = extensionAddress || KNOWN_CONTRACTS.manifoldExtension; + + try { + return await client.readContract({ + address: getAddress(extension), + abi: MANIFOLD_EXTENSION_ABI, + functionName: "MINT_FEE", + }) as bigint; + } catch { + // Try MINT_FEE_MERKLE as fallback + try { + return await client.readContract({ + address: getAddress(extension), + abi: MANIFOLD_EXTENSION_ABI, + functionName: "MINT_FEE_MERKLE", + }) as bigint; + } catch { + return BigInt(0); + } + } +} + +/** + * Check if an address is the zero address + */ +export function isZeroAddress(address: string): boolean { + return address === "0x0000000000000000000000000000000000000000"; +} + +/** + * Format instance ID and token ID for display + */ +export function formatManifoldTokenId(instanceId: string, tokenId: string): string { + return `${instanceId}-${tokenId}`; +} \ No newline at end of file diff --git a/registry/mini-app/lib/nft-standards.ts b/registry/mini-app/lib/nft-standards.ts index f7ff80e0..aa192a8d 100644 --- a/registry/mini-app/lib/nft-standards.ts +++ b/registry/mini-app/lib/nft-standards.ts @@ -13,6 +13,8 @@ export const ERC721_ABI = { "function ownerOf(uint256 tokenId) view returns (address)", "function balanceOf(address owner) view returns (uint256)", "function totalSupply() view returns (uint256)", + "function baseURI() view returns (string)", + "function contractURI() view returns (string)", ]), // Individual functions for specific use cases @@ -20,6 +22,8 @@ export const ERC721_ABI = { name: parseAbi(["function name() view returns (string)"]), symbol: parseAbi(["function symbol() view returns (string)"]), ownerOf: parseAbi(["function ownerOf(uint256 tokenId) view returns (address)"]), + baseURI: parseAbi(["function baseURI() view returns (string)"]), + contractURI: parseAbi(["function contractURI() view returns (string)"]), }; // Common price discovery functions across NFT contracts @@ -39,6 +43,11 @@ export const MINT_ABI = parseAbi([ "function mintTo(address to, uint256 amount) payable", ]); +// ERC1155 metadata function +export const ERC1155_ABI = { + uri: parseAbi(["function uri(uint256 tokenId) view returns (string)"]), +}; + // ERC20 ABI for token interactions export const ERC20_ABI = parseAbi([ "function decimals() view returns (uint8)", @@ -172,6 +181,80 @@ export const KNOWN_CONTRACTS = { // Add other known contracts here as needed } as const; +// thirdweb OpenEditionERC721 ABI +export const THIRDWEB_OPENEDITONERC721_ABI = [ + { + inputs: [ + { name: "_receiver", type: "address" }, + { name: "_quantity", type: "uint256" }, + { name: "_currency", type: "address" }, + { name: "_pricePerToken", type: "uint256" }, + { + components: [ + { name: "proof", type: "bytes32[]" }, + { name: "quantityLimitPerWallet", type: "uint256" }, + { name: "pricePerToken", type: "uint256" }, + { name: "currency", type: "address" } + ], + name: "_allowlistProof", + type: "tuple" + }, + { name: "_data", type: "bytes" } + ], + name: "claim", + outputs: [], + stateMutability: "payable", + type: "function" + }, + { + inputs: [], + name: "claimCondition", + outputs: [ + { name: "currentStartId", type: "uint256" }, + { name: "count", type: "uint256" } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [{ name: "_conditionId", type: "uint256" }], + name: "getClaimConditionById", + outputs: [ + { + components: [ + { name: "startTimestamp", type: "uint256" }, + { name: "maxClaimableSupply", type: "uint256" }, + { name: "supplyClaimed", type: "uint256" }, + { name: "quantityLimitPerWallet", type: "uint256" }, + { name: "merkleRoot", type: "bytes32" }, + { name: "pricePerToken", type: "uint256" }, + { name: "currency", type: "address" }, + { name: "metadata", type: "string" } + ], + name: "condition", + type: "tuple" + } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [], + name: "sharedMetadata", + outputs: [ + { name: "name", type: "string" }, + { name: "description", type: "string" }, + { name: "image", type: "string" }, + { name: "animationUrl", type: "string" } + ], + stateMutability: "view", + type: "function" + } +] as const; + +// Native ETH address for thirdweb contracts +export const THIRDWEB_NATIVE_TOKEN = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" as Address; + // Interface IDs for contract detection export const INTERFACE_IDS = { ERC165: "0x01ffc9a7", From bb8bb40bf825f6697b9033963c71707666ba6dc0 Mon Sep 17 00:00:00 2001 From: hellno Date: Fri, 25 Jul 2025 16:29:20 +0200 Subject: [PATCH 2/4] fix build --- .../blocks/nft-mint-flow/lib/price-optimizer.ts | 8 +++----- .../mini-app/blocks/nft-mint-flow/nft-mint-button.tsx | 11 +++++------ 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/registry/mini-app/blocks/nft-mint-flow/lib/price-optimizer.ts b/registry/mini-app/blocks/nft-mint-flow/lib/price-optimizer.ts index d245bbda..2549ed14 100644 --- a/registry/mini-app/blocks/nft-mint-flow/lib/price-optimizer.ts +++ b/registry/mini-app/blocks/nft-mint-flow/lib/price-optimizer.ts @@ -283,7 +283,7 @@ export async function fetchPriceData( contractInfo.claimCondition = { id: Number(activeConditionId), pricePerToken: pricePerToken as bigint, - currency: currency as string, + currency: currency as `0x${string}`, maxClaimableSupply: maxClaimableSupply as bigint, merkleRoot: merkleRoot as `0x${string}`, startTimestamp: Number(startTimestamp), @@ -346,16 +346,14 @@ export async function fetchPriceData( allowance: allowance as bigint, balance: balance as bigint | undefined }, - totalCost: BigInt(0), // No ETH needed for ERC20 payment - claim: contractInfo.claimCondition + totalCost: BigInt(0) // No ETH needed for ERC20 payment }; } else { // ETH payment const totalCost = (pricePerToken as bigint) * BigInt(params.amount || 1); return { mintPrice: pricePerToken as bigint, - totalCost, - claim: contractInfo.claimCondition + totalCost }; } } catch (err) { diff --git a/registry/mini-app/blocks/nft-mint-flow/nft-mint-button.tsx b/registry/mini-app/blocks/nft-mint-flow/nft-mint-button.tsx index f0ad7a8c..e45590cf 100644 --- a/registry/mini-app/blocks/nft-mint-flow/nft-mint-button.tsx +++ b/registry/mini-app/blocks/nft-mint-flow/nft-mint-button.tsx @@ -79,10 +79,9 @@ type NFTMintFlowProps = { /** * Blockchain network ID - * - 1 = Ethereum mainnet - * - 8453 = Base mainnet + * Supports any valid chain ID */ - chainId: 1 | 8453; + chainId: number; /** * Optional provider hint. Use when: @@ -1021,7 +1020,7 @@ NFTMintButton.presets = { */ generic: (props: { contractAddress: Address; - chainId: 1 | 8453; + chainId: number; amount?: number; buttonText?: string; onMintSuccess?: (txHash: string) => void; @@ -1045,7 +1044,7 @@ NFTMintButton.presets = { */ manifold: (props: { contractAddress: Address; - chainId: 1 | 8453; + chainId: number; instanceId: string; tokenId?: string; amount?: number; @@ -1078,7 +1077,7 @@ NFTMintButton.presets = { */ auto: (props: { contractAddress: Address; - chainId: 1 | 8453; + chainId: number; amount?: number; buttonText?: string; onMintSuccess?: (txHash: string) => void; From f636604bd642e6c79d586ab3e08d8174ac48a4d3 Mon Sep 17 00:00:00 2001 From: hellno Date: Fri, 25 Jul 2025 16:29:26 +0200 Subject: [PATCH 3/4] Update nft-mint-flow.json --- public/r/nft-mint-flow.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/r/nft-mint-flow.json b/public/r/nft-mint-flow.json index ab17ee12..18db7af2 100644 --- a/public/r/nft-mint-flow.json +++ b/public/r/nft-mint-flow.json @@ -33,7 +33,7 @@ }, { "path": "registry/mini-app/blocks/nft-mint-flow/nft-mint-button.tsx", - "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Button } from \"@/registry/mini-app/ui/button\";\nimport {\n Sheet,\n SheetContent,\n SheetHeader,\n SheetTitle,\n} from \"@/registry/mini-app/ui/sheet\";\nimport { useMiniAppSdk } from \"@/registry/mini-app/hooks/use-miniapp-sdk\";\nimport {\n useAccount,\n useConnect,\n useWaitForTransactionReceipt,\n useWriteContract,\n useSwitchChain,\n} from \"wagmi\";\nimport { formatEther, type Address } from \"viem\";\nimport { farcasterFrame } from \"@farcaster/miniapp-wagmi-connector\";\nimport {\n Coins,\n CheckCircle,\n AlertCircle,\n Loader2,\n Info,\n ExternalLink,\n RefreshCw,\n Wallet,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n detectNFTProvider,\n validateParameters,\n getClientForChain,\n} from \"@/registry/mini-app/blocks/nft-mint-flow/lib/provider-detector\";\nimport { getChainById } from \"@/registry/mini-app/lib/chains\";\nimport { getProviderConfig } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/provider-configs\";\nimport { fetchPriceData } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/price-optimizer\";\nimport { mintReducer, initialState, type MintStep } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/mint-reducer\";\nimport type { MintParams } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/types\";\nimport { parseError, type ParsedError } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/error-parser\";\n\n/**\n * NFTMintButton - Universal NFT minting button with automatic provider detection and ERC20 approval handling\n *\n * @example\n * ```tsx\n * // Basic ETH mint (auto-detects provider)\n * \n *\n * // Manifold NFT with ERC20 payment (HIGHER token)\n * \n *\n * // Multiple NFTs with custom button\n * console.log('Minted!', txHash)}\n * />\n * ```\n */\ntype NFTMintFlowProps = {\n /**\n * NFT contract address (0x...). This should be the main NFT contract, not the minting contract.\n * For Manifold, this is the creator contract, not the extension.\n */\n contractAddress: Address;\n\n /**\n * Blockchain network ID\n * - 1 = Ethereum mainnet\n * - 8453 = Base mainnet\n */\n chainId: 1 | 8453;\n\n /**\n * Optional provider hint. Use when:\n * - Auto-detection is failing\n * - You know the provider and want faster loading\n * - Testing specific provider flows\n *\n * Leave undefined for automatic detection.\n */\n provider?: \"manifold\" | \"opensea\" | \"zora\" | \"generic\";\n\n /**\n * Number of NFTs to mint. Defaults to 1.\n * Note: For ERC20 payments, the total cost is multiplied by this amount.\n */\n amount?: number;\n\n /**\n * Manifold-specific parameters. Required when provider=\"manifold\".\n * - instanceId: The claim instance ID from Manifold (required for most Manifold NFTs)\n * - tokenId: The specific token ID (required for some editions)\n *\n * Find these in the Manifold claim page URL or contract details.\n */\n manifoldParams?: {\n instanceId?: string;\n tokenId?: string;\n };\n\n // UI customization\n /** Additional CSS classes */\n className?: string;\n /** Button style variant */\n variant?: \"default\" | \"destructive\" | \"secondary\" | \"ghost\" | \"outline\";\n /** Button size */\n size?: \"default\" | \"sm\" | \"lg\" | \"icon\";\n /** Custom button text. Defaults to \"Mint NFT\" */\n buttonText?: string;\n /** Disable the mint button */\n disabled?: boolean;\n\n /**\n * Called when NFT minting succeeds (not on approval success)\n * @param txHash - The mint transaction hash (not approval tx)\n */\n onMintSuccess?: (txHash: string) => void;\n\n /**\n * Called when NFT minting fails (not on approval failure)\n * @param error - Human-readable error message\n */\n onMintError?: (error: string) => void;\n};\n\nexport function NFTMintButton({\n contractAddress,\n chainId,\n provider,\n amount = 1,\n manifoldParams,\n className,\n variant = \"default\",\n size = \"default\",\n buttonText = \"Mint NFT\",\n disabled = false,\n onMintSuccess,\n onMintError,\n}: NFTMintFlowProps) {\n const [state, dispatch] = React.useReducer(mintReducer, initialState);\n const [isSheetOpen, setIsSheetOpen] = React.useState(false);\n const [parsedError, setParsedError] = React.useState(null);\n\n // Prop validation with helpful errors\n React.useEffect(() => {\n if (\n provider === \"manifold\" &&\n !manifoldParams?.instanceId &&\n !manifoldParams?.tokenId\n ) {\n console.error(\n \"NFTMintFlow: When provider='manifold', you must provide manifoldParams with either instanceId or tokenId. \" +\n \"Example: manifoldParams={{ instanceId: '4293509360' }}\",\n );\n }\n\n if (manifoldParams && provider !== \"manifold\") {\n console.warn(\n \"NFTMintFlow: manifoldParams provided but provider is not 'manifold'. \" +\n \"Did you forget to set provider='manifold'?\",\n );\n }\n\n if (chainId !== 1 && chainId !== 8453) {\n console.warn(\n `NFTMintFlow: Chain ID ${chainId} may not be supported. ` +\n \"Currently tested chains: 1 (Ethereum), 8453 (Base)\",\n );\n }\n\n if (!contractAddress || !contractAddress.match(/^0x[a-fA-F0-9]{40}$/)) {\n console.error(\n \"NFTMintFlow: Invalid contract address. Must be a valid Ethereum address (0x...)\",\n );\n }\n }, [provider, manifoldParams, chainId, contractAddress]);\n\n // Destructure commonly used values\n const {\n step,\n contractInfo,\n priceData,\n error,\n txHash,\n txType,\n isLoading,\n validationErrors,\n } = state;\n const { erc20Details } = priceData;\n\n const { isSDKLoaded } = useMiniAppSdk();\n const { isConnected, address, chain } = useAccount();\n const { connect } = useConnect();\n const { switchChain } = useSwitchChain();\n const {\n writeContract,\n isPending: isWritePending,\n data: writeData,\n error: writeError,\n } = useWriteContract();\n\n // Build mint params\n const mintParams: MintParams = React.useMemo(\n () => ({\n contractAddress,\n chainId,\n provider,\n amount,\n instanceId: manifoldParams?.instanceId,\n tokenId: manifoldParams?.tokenId,\n recipient: address,\n }),\n [contractAddress, chainId, provider, amount, manifoldParams, address],\n );\n\n // Watch for transaction completion\n const {\n isSuccess: isTxSuccess,\n isError: isTxError,\n error: txError,\n } = useWaitForTransactionReceipt({\n hash: writeData,\n });\n\n // Get provider config\n const providerConfig = contractInfo\n ? getProviderConfig(contractInfo.provider, contractInfo)\n : null;\n\n // Check if user is on the correct network\n const isCorrectNetwork = chain?.id === chainId;\n const targetChain = getChainById(chainId);\n const networkName = targetChain.name || \"Unknown\";\n\n // Handle transaction status updates\n React.useEffect(() => {\n if (writeError) {\n const parsed = parseError(writeError, txType || \"mint\");\n \n // Show retry option for user rejections\n if (parsed.type === \"user-rejected\") {\n setParsedError(parsed);\n dispatch({ type: \"TX_ERROR\", payload: \"Transaction cancelled by user\" });\n return;\n }\n \n setParsedError(parsed);\n dispatch({ type: \"TX_ERROR\", payload: writeError.message });\n if (txType === \"mint\") {\n onMintError?.(writeError.message);\n }\n }\n if (isTxError && txError) {\n const parsed = parseError(txError, txType || \"mint\");\n setParsedError(parsed);\n dispatch({ type: \"TX_ERROR\", payload: txError.message });\n if (txType === \"mint\") {\n onMintError?.(txError.message);\n }\n }\n if (writeData && !isTxSuccess && !isTxError) {\n // Transaction submitted, waiting for confirmation\n if (txType === \"approval\") {\n dispatch({ type: \"APPROVE_TX_SUBMITTED\", payload: writeData });\n } else if (txType === \"mint\") {\n dispatch({ type: \"MINT_TX_SUBMITTED\", payload: writeData });\n }\n }\n if (isTxSuccess && writeData) {\n if (txType === \"approval\") {\n dispatch({ type: \"APPROVE_SUCCESS\" });\n } else if (txType === \"mint\") {\n dispatch({ type: \"TX_SUCCESS\", payload: writeData });\n onMintSuccess?.(writeData);\n }\n }\n }, [\n isTxSuccess,\n writeData,\n onMintSuccess,\n isTxError,\n txError,\n onMintError,\n writeError,\n txType,\n ]);\n\n const handleClose = React.useCallback(() => {\n setIsSheetOpen(false);\n dispatch({ type: \"RESET\" });\n setParsedError(null);\n }, []);\n\n // Auto-close on success after 10 seconds\n React.useEffect(() => {\n if (step === \"success\") {\n const timer = setTimeout(() => {\n handleClose();\n }, 10000);\n return () => clearTimeout(timer);\n }\n }, [step, handleClose]);\n\n const handleSwitchNetwork = async () => {\n try {\n await switchChain({ chainId });\n } catch (err) {\n // Network switch failed - user likely rejected or wallet doesn't support it\n }\n };\n\n // Detect NFT provider and validate\n const detectAndValidate = async () => {\n dispatch({ type: \"DETECT_START\" });\n\n try {\n // Detect provider\n const info = await detectNFTProvider(mintParams);\n\n // Validate parameters\n const validation = validateParameters(mintParams, info);\n\n if (!validation.isValid) {\n dispatch({ type: \"VALIDATION_ERROR\", payload: validation.errors });\n return;\n }\n\n // Fetch optimized price data\n const client = getClientForChain(chainId);\n const fetchedPriceData = await fetchPriceData(client, mintParams, info);\n\n // Update contract info with ERC20 details and claim data\n if (fetchedPriceData.erc20Details) {\n info.erc20Token = fetchedPriceData.erc20Details\n .address as `0x${string}`;\n info.erc20Symbol = fetchedPriceData.erc20Details.symbol;\n info.erc20Decimals = fetchedPriceData.erc20Details.decimals;\n }\n\n // Add claim data if available\n if (fetchedPriceData.claim) {\n info.claim = fetchedPriceData.claim;\n }\n\n dispatch({\n type: \"DETECT_SUCCESS\",\n payload: {\n contractInfo: info,\n priceData: {\n mintPrice: fetchedPriceData.mintPrice,\n totalCost: fetchedPriceData.totalCost,\n erc20Details: fetchedPriceData.erc20Details,\n },\n },\n });\n } catch (err) {\n dispatch({\n type: \"DETECT_ERROR\",\n payload: \"Failed to detect NFT contract type\",\n });\n }\n };\n\n // Check allowance only (without re-detecting everything)\n const checkAllowanceOnly = React.useCallback(async () => {\n if (!contractInfo || !erc20Details || !address) return;\n\n try {\n const client = getClientForChain(chainId);\n const spenderAddress =\n contractInfo.provider === \"manifold\" && contractInfo.extensionAddress\n ? contractInfo.extensionAddress\n : contractAddress;\n\n const allowance = await client.readContract({\n address: erc20Details.address as `0x${string}`,\n abi: [\n {\n name: \"allowance\",\n type: \"function\",\n inputs: [\n { name: \"owner\", type: \"address\" },\n { name: \"spender\", type: \"address\" },\n ],\n outputs: [{ type: \"uint256\" }],\n stateMutability: \"view\",\n },\n ],\n functionName: \"allowance\",\n args: [address, spenderAddress],\n });\n\n dispatch({ type: \"UPDATE_ALLOWANCE\", payload: allowance as bigint });\n } catch (err) {\n // Allowance check failed - will proceed without pre-checked allowance\n }\n }, [contractInfo, erc20Details, address, chainId, contractAddress]);\n\n // Re-check allowance after wallet connection\n React.useEffect(() => {\n if (\n isConnected &&\n address &&\n erc20Details &&\n erc20Details.allowance === undefined\n ) {\n checkAllowanceOnly();\n }\n }, [isConnected, address, erc20Details, checkAllowanceOnly]);\n\n const handleInitialMint = async () => {\n if (!isSDKLoaded) {\n dispatch({ type: \"TX_ERROR\", payload: \"Farcaster SDK not loaded\" });\n setIsSheetOpen(true);\n return;\n }\n\n setIsSheetOpen(true);\n await detectAndValidate();\n };\n\n const handleConnectWallet = async () => {\n try {\n dispatch({ type: \"CONNECT_START\" });\n const connector = farcasterFrame();\n connect({ connector });\n } catch (err) {\n handleError(err, \"Failed to connect wallet\");\n }\n };\n\n const handleApprove = async () => {\n if (!isConnected || !erc20Details || !contractInfo?.claim) {\n dispatch({\n type: \"TX_ERROR\",\n payload: \"Missing required information for approval\",\n });\n return;\n }\n\n try {\n dispatch({ type: \"APPROVE_START\" });\n\n // For Manifold, approve the extension contract, not the NFT contract\n const spenderAddress =\n contractInfo.provider === \"manifold\" && contractInfo.extensionAddress\n ? contractInfo.extensionAddress\n : contractAddress;\n\n // Approve exact amount needed\n await writeContract({\n address: erc20Details.address as `0x${string}`,\n abi: [\n {\n name: \"approve\",\n type: \"function\",\n inputs: [\n { name: \"spender\", type: \"address\" },\n { name: \"amount\", type: \"uint256\" },\n ],\n outputs: [{ type: \"bool\" }],\n stateMutability: \"nonpayable\",\n },\n ],\n functionName: \"approve\",\n args: [spenderAddress, contractInfo.claim.cost],\n chainId,\n });\n \n // The transaction has been initiated - we'll track it via writeData in the effect\n } catch (err) {\n handleError(err, \"Approval failed\", \"approval\");\n }\n };\n\n const handleMint = async () => {\n if (!isConnected) {\n await handleConnectWallet();\n return;\n }\n\n if (!contractInfo || !providerConfig) {\n dispatch({\n type: \"TX_ERROR\",\n payload: \"Contract information not available\",\n });\n return;\n }\n\n try {\n dispatch({ type: \"MINT_START\" });\n\n const args = providerConfig.mintConfig.buildArgs(mintParams);\n \n const value = priceData.totalCost || BigInt(0);\n\n // Handle Manifold's special case\n const mintAddress =\n contractInfo.provider === \"manifold\" && contractInfo.extensionAddress\n ? contractInfo.extensionAddress\n : contractAddress;\n\n \n // Prepare contract config based on provider type\n let contractConfig: any;\n \n contractConfig = {\n address: mintAddress,\n abi: providerConfig.mintConfig.abi,\n functionName: providerConfig.mintConfig.functionName,\n args,\n value,\n chainId,\n };\n\n // Execute the transaction\n await writeContract(contractConfig);\n \n // The transaction has been initiated - we'll track it via writeData in the effect\n } catch (err) {\n handleError(err, \"Mint transaction failed\", \"mint\");\n }\n };\n\n // Centralized error handler\n const handleError = (\n error: unknown,\n context: string,\n transactionType?: \"approval\" | \"mint\",\n ) => {\n console.error(`${context}:`, error);\n const message = error instanceof Error ? error.message : `${context}`;\n \n // Parse the error for better UX\n const parsed = parseError(error, transactionType || \"mint\");\n setParsedError(parsed);\n \n dispatch({ type: \"TX_ERROR\", payload: message });\n // Use explicit transaction type if provided, otherwise fall back to state\n if ((transactionType || txType) === \"mint\") {\n onMintError?.(message);\n }\n };\n\n const handleRetry = () => {\n dispatch({ type: \"RESET\" });\n detectAndValidate();\n };\n\n // Display helpers (quick win: centralized formatting)\n const formatPrice = (amount: bigint, decimals: number, symbol: string) => {\n if (amount === BigInt(0)) return \"Free\";\n return `${Number(amount) / 10 ** decimals} ${symbol}`;\n };\n\n const displayPrice = () => {\n if (erc20Details && contractInfo?.claim) {\n return formatPrice(\n contractInfo.claim.cost || BigInt(0),\n erc20Details.decimals || 18,\n erc20Details.symbol,\n );\n }\n return priceData.mintPrice\n ? `${formatEther(priceData.mintPrice)} ETH`\n : \"Free\";\n };\n\n const displayTotalCost = () => {\n if (erc20Details && contractInfo?.claim) {\n // For Manifold, cost is per claim, not per NFT amount\n return formatPrice(\n contractInfo.claim.cost || BigInt(0),\n erc20Details.decimals || 18,\n erc20Details.symbol,\n );\n }\n return priceData.totalCost\n ? `${formatEther(priceData.totalCost)} ETH`\n : \"Free\";\n };\n\n const displayMintFee = () => {\n const fee = priceData.mintPrice || BigInt(0);\n return fee > BigInt(0) ? `${formatEther(fee)} ETH` : \"0 ETH\";\n };\n\n const providerName = contractInfo?.provider\n ? contractInfo.provider.charAt(0).toUpperCase() +\n contractInfo.provider.slice(1)\n : \"Unknown\";\n\n // Quick win: validation helper\n const isReadyToMint = () => {\n return (\n isConnected &&\n contractInfo &&\n !isLoading &&\n step === \"sheet\" &&\n (!erc20Details || !erc20Details.needsApproval)\n );\n };\n\n return (\n {\n setIsSheetOpen(open);\n if (!open) {\n handleClose();\n }\n }}\n >\n \n \n {buttonText}\n \n\n \n \n \n {step === \"detecting\" && \"Detecting NFT Type\"}\n {step === \"sheet\" && \"Mint NFT\"}\n {step === \"connecting\" && \"Connecting Wallet\"}\n {step === \"approve\" && \"Approve Token\"}\n {step === \"approving\" && \"Approving...\"}\n {step === \"minting\" && \"Preparing Mint\"}\n {step === \"waiting\" &&\n (txType === \"approval\" ? \"Approving...\" : \"Minting...\")}\n {step === \"success\" && \"Mint Successful!\"}\n {step === \"error\" && (parsedError?.type === \"user-rejected\" ? \"Transaction Cancelled\" : \"Transaction Failed\")}\n {step === \"validation-error\" && \"Missing Information\"}\n \n \n\n {/* Detecting Provider */}\n {step === \"detecting\" && (\n
\n
\n \n
\n

\n Detecting NFT contract type...\n

\n
\n )}\n\n {/* Validation Error */}\n {step === \"validation-error\" && (\n
\n
\n \n
\n
\n

\n Missing Required Information\n

\n {validationErrors.map((err, idx) => (\n \n {err}\n

\n ))}\n
\n \n
\n )}\n\n {/* Approve Step */}\n {step === \"approve\" && erc20Details && (\n
\n
\n

Approval Required

\n

\n This NFT requires payment in {erc20Details.symbol}. You need to\n approve the contract to spend your tokens.\n

\n
\n
\n
\n Token\n {erc20Details.symbol}\n
\n
\n Amount to Approve\n \n {contractInfo?.claim\n ? Number(contractInfo.claim.cost) /\n 10 ** erc20Details.decimals\n : 0}{\" \"}\n {erc20Details.symbol}\n \n
\n
\n \n \n Approve {erc20Details.symbol}\n \n
\n )}\n\n {/* Main Sheet Content */}\n {step === \"sheet\" && contractInfo && (\n
\n {/* Network warning */}\n {!isCorrectNetwork && isConnected && (\n
\n
\n
\n \n

Wrong network

\n
\n \n Switch to {networkName}\n \n
\n
\n )}\n \n
\n
\n Provider\n {providerName}\n
\n
\n Contract\n \n {contractAddress.slice(0, 6)}...{contractAddress.slice(-4)}\n \n
\n
\n Quantity\n {amount}\n
\n
\n Price per NFT\n {displayPrice()}\n
\n {erc20Details && (\n
\n Mint Fee\n {displayMintFee()}\n
\n )}\n
\n Total Cost\n {displayTotalCost()}\n
\n
\n\n \n {isConnected ? (\n !isCorrectNetwork ? (\n <>\n \n Switch Network\n Switch Network to Mint\n \n ) : (\n <>\n \n Mint {amount} NFT{amount > 1 ? \"s\" : \"\"}\n \n )\n ) : (\n <>\n \n Connect\n Connect Wallet to Mint\n \n )}\n \n
\n )}\n\n {/* Connecting */}\n {step === \"connecting\" && (\n
\n
\n \n
\n

\n Connecting to your Farcaster wallet...\n

\n
\n )}\n\n {/* Minting/Approving */}\n {(step === \"minting\" || step === \"approving\") && (\n
\n
\n \n
\n
\n

\n {step === \"approving\"\n ? \"Preparing approval\"\n : \"Preparing mint transaction\"}\n

\n

\n Please approve the transaction in your wallet\n

\n
\n
\n )}\n\n {/* Waiting for Transaction */}\n {step === \"waiting\" && (\n
\n
\n \n
\n
\n

\n {txType === \"approval\"\n ? \"Approval submitted\"\n : \"Transaction submitted\"}\n

\n

\n Waiting for confirmation on the blockchain...\n

\n {txHash && (\n

\n {txHash.slice(0, 10)}...{txHash.slice(-8)}\n

\n )}\n
\n
\n )}\n\n {/* Success */}\n {step === \"success\" && (\n
\n
\n \n
\n
\n

Minted! 🎉

\n

\n {amount} NFT{amount > 1 ? \"s\" : \"\"} successfully minted\n

\n
\n {txHash && (\n
\n \n
\n )}\n \n
\n )}\n\n {/* Error State */}\n {step === \"error\" && (\n
\n
\n
\n
\n {parsedError?.type === \"user-rejected\" ? (\n \n ) : (\n \n )}\n
\n
\n
\n

\n {parsedError?.message || \"Transaction Failed\"}\n

\n {parsedError?.details && (\n

\n {parsedError.details}\n

\n )}\n
\n
\n\n {/* Special handling for wrong network */}\n {!isCorrectNetwork && isConnected && (\n
\n
\n \n
\n

Wrong Network

\n

\n Please switch to {networkName} to continue\n

\n
\n
\n \n Switch to {networkName}\n \n
\n )}\n\n {/* Specific error actions */}\n {parsedError?.type === \"insufficient-funds\" && (\n
\n
\n \n
\n

Insufficient Balance

\n

\n Make sure you have enough:\n

\n
    \n {erc20Details ? (\n <>\n
  • {erc20Details.symbol} for the NFT price
  • \n
  • ETH for gas fees
  • \n \n ) : (\n
  • ETH for both NFT price and gas fees
  • \n )}\n
\n
\n
\n
\n )}\n\n {/* Action buttons */}\n
\n \n Close\n \n \n
\n
\n )}\n \n \n );\n}\n\n/**\n * Preset builders for common NFT minting scenarios.\n * These provide type-safe, self-documenting ways to create NFTMintButton components.\n */\nNFTMintButton.presets = {\n /**\n * Create a basic ETH-based NFT mint\n * @example\n * ```tsx\n * \n * ```\n */\n generic: (props: {\n contractAddress: Address;\n chainId: 1 | 8453;\n amount?: number;\n buttonText?: string;\n onMintSuccess?: (txHash: string) => void;\n onMintError?: (error: string) => void;\n }): NFTMintFlowProps => ({\n ...props,\n provider: \"generic\",\n amount: props.amount || 1,\n }),\n\n /**\n * Create a Manifold NFT mint with proper configuration\n * @example\n * ```tsx\n * \n * ```\n */\n manifold: (props: {\n contractAddress: Address;\n chainId: 1 | 8453;\n instanceId: string;\n tokenId?: string;\n amount?: number;\n buttonText?: string;\n onMintSuccess?: (txHash: string) => void;\n onMintError?: (error: string) => void;\n }): NFTMintFlowProps => ({\n contractAddress: props.contractAddress,\n chainId: props.chainId,\n provider: \"manifold\",\n manifoldParams: {\n instanceId: props.instanceId,\n tokenId: props.tokenId,\n },\n amount: props.amount || 1,\n buttonText: props.buttonText,\n onMintSuccess: props.onMintSuccess,\n onMintError: props.onMintError,\n }),\n\n /**\n * Create an auto-detecting NFT mint (tries to figure out the provider)\n * @example\n * ```tsx\n * \n * ```\n */\n auto: (props: {\n contractAddress: Address;\n chainId: 1 | 8453;\n amount?: number;\n buttonText?: string;\n onMintSuccess?: (txHash: string) => void;\n onMintError?: (error: string) => void;\n }): NFTMintFlowProps => ({\n ...props,\n amount: props.amount || 1,\n }),\n};\n", + "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Button } from \"@/registry/mini-app/ui/button\";\nimport {\n Sheet,\n SheetContent,\n SheetHeader,\n SheetTitle,\n} from \"@/registry/mini-app/ui/sheet\";\nimport { useMiniAppSdk } from \"@/registry/mini-app/hooks/use-miniapp-sdk\";\nimport {\n useAccount,\n useConnect,\n useWaitForTransactionReceipt,\n useWriteContract,\n useSwitchChain,\n} from \"wagmi\";\nimport { formatEther, type Address } from \"viem\";\nimport { farcasterFrame } from \"@farcaster/miniapp-wagmi-connector\";\nimport {\n Coins,\n CheckCircle,\n AlertCircle,\n Loader2,\n Info,\n ExternalLink,\n RefreshCw,\n Wallet,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n detectNFTProvider,\n validateParameters,\n getClientForChain,\n} from \"@/registry/mini-app/blocks/nft-mint-flow/lib/provider-detector\";\nimport { getChainById } from \"@/registry/mini-app/lib/chains\";\nimport { getProviderConfig } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/provider-configs\";\nimport { fetchPriceData } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/price-optimizer\";\nimport { mintReducer, initialState, type MintStep } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/mint-reducer\";\nimport type { MintParams } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/types\";\nimport { parseError, type ParsedError } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/error-parser\";\n\n/**\n * NFTMintButton - Universal NFT minting button with automatic provider detection and ERC20 approval handling\n *\n * @example\n * ```tsx\n * // Basic ETH mint (auto-detects provider)\n * \n *\n * // Manifold NFT with ERC20 payment (HIGHER token)\n * \n *\n * // Multiple NFTs with custom button\n * console.log('Minted!', txHash)}\n * />\n * ```\n */\ntype NFTMintFlowProps = {\n /**\n * NFT contract address (0x...). This should be the main NFT contract, not the minting contract.\n * For Manifold, this is the creator contract, not the extension.\n */\n contractAddress: Address;\n\n /**\n * Blockchain network ID\n * Supports any valid chain ID\n */\n chainId: number;\n\n /**\n * Optional provider hint. Use when:\n * - Auto-detection is failing\n * - You know the provider and want faster loading\n * - Testing specific provider flows\n *\n * Leave undefined for automatic detection.\n */\n provider?: \"manifold\" | \"opensea\" | \"zora\" | \"generic\";\n\n /**\n * Number of NFTs to mint. Defaults to 1.\n * Note: For ERC20 payments, the total cost is multiplied by this amount.\n */\n amount?: number;\n\n /**\n * Manifold-specific parameters. Required when provider=\"manifold\".\n * - instanceId: The claim instance ID from Manifold (required for most Manifold NFTs)\n * - tokenId: The specific token ID (required for some editions)\n *\n * Find these in the Manifold claim page URL or contract details.\n */\n manifoldParams?: {\n instanceId?: string;\n tokenId?: string;\n };\n\n // UI customization\n /** Additional CSS classes */\n className?: string;\n /** Button style variant */\n variant?: \"default\" | \"destructive\" | \"secondary\" | \"ghost\" | \"outline\";\n /** Button size */\n size?: \"default\" | \"sm\" | \"lg\" | \"icon\";\n /** Custom button text. Defaults to \"Mint NFT\" */\n buttonText?: string;\n /** Disable the mint button */\n disabled?: boolean;\n\n /**\n * Called when NFT minting succeeds (not on approval success)\n * @param txHash - The mint transaction hash (not approval tx)\n */\n onMintSuccess?: (txHash: string) => void;\n\n /**\n * Called when NFT minting fails (not on approval failure)\n * @param error - Human-readable error message\n */\n onMintError?: (error: string) => void;\n};\n\nexport function NFTMintButton({\n contractAddress,\n chainId,\n provider,\n amount = 1,\n manifoldParams,\n className,\n variant = \"default\",\n size = \"default\",\n buttonText = \"Mint NFT\",\n disabled = false,\n onMintSuccess,\n onMintError,\n}: NFTMintFlowProps) {\n const [state, dispatch] = React.useReducer(mintReducer, initialState);\n const [isSheetOpen, setIsSheetOpen] = React.useState(false);\n const [parsedError, setParsedError] = React.useState(null);\n\n // Prop validation with helpful errors\n React.useEffect(() => {\n if (\n provider === \"manifold\" &&\n !manifoldParams?.instanceId &&\n !manifoldParams?.tokenId\n ) {\n console.error(\n \"NFTMintFlow: When provider='manifold', you must provide manifoldParams with either instanceId or tokenId. \" +\n \"Example: manifoldParams={{ instanceId: '4293509360' }}\",\n );\n }\n\n if (manifoldParams && provider !== \"manifold\") {\n console.warn(\n \"NFTMintFlow: manifoldParams provided but provider is not 'manifold'. \" +\n \"Did you forget to set provider='manifold'?\",\n );\n }\n\n if (chainId !== 1 && chainId !== 8453) {\n console.warn(\n `NFTMintFlow: Chain ID ${chainId} may not be supported. ` +\n \"Currently tested chains: 1 (Ethereum), 8453 (Base)\",\n );\n }\n\n if (!contractAddress || !contractAddress.match(/^0x[a-fA-F0-9]{40}$/)) {\n console.error(\n \"NFTMintFlow: Invalid contract address. Must be a valid Ethereum address (0x...)\",\n );\n }\n }, [provider, manifoldParams, chainId, contractAddress]);\n\n // Destructure commonly used values\n const {\n step,\n contractInfo,\n priceData,\n error,\n txHash,\n txType,\n isLoading,\n validationErrors,\n } = state;\n const { erc20Details } = priceData;\n\n const { isSDKLoaded } = useMiniAppSdk();\n const { isConnected, address, chain } = useAccount();\n const { connect } = useConnect();\n const { switchChain } = useSwitchChain();\n const {\n writeContract,\n isPending: isWritePending,\n data: writeData,\n error: writeError,\n } = useWriteContract();\n\n // Build mint params\n const mintParams: MintParams = React.useMemo(\n () => ({\n contractAddress,\n chainId,\n provider,\n amount,\n instanceId: manifoldParams?.instanceId,\n tokenId: manifoldParams?.tokenId,\n recipient: address,\n }),\n [contractAddress, chainId, provider, amount, manifoldParams, address],\n );\n\n // Watch for transaction completion\n const {\n isSuccess: isTxSuccess,\n isError: isTxError,\n error: txError,\n } = useWaitForTransactionReceipt({\n hash: writeData,\n });\n\n // Get provider config\n const providerConfig = contractInfo\n ? getProviderConfig(contractInfo.provider, contractInfo)\n : null;\n\n // Check if user is on the correct network\n const isCorrectNetwork = chain?.id === chainId;\n const targetChain = getChainById(chainId);\n const networkName = targetChain.name || \"Unknown\";\n\n // Handle transaction status updates\n React.useEffect(() => {\n if (writeError) {\n const parsed = parseError(writeError, txType || \"mint\");\n \n // Show retry option for user rejections\n if (parsed.type === \"user-rejected\") {\n setParsedError(parsed);\n dispatch({ type: \"TX_ERROR\", payload: \"Transaction cancelled by user\" });\n return;\n }\n \n setParsedError(parsed);\n dispatch({ type: \"TX_ERROR\", payload: writeError.message });\n if (txType === \"mint\") {\n onMintError?.(writeError.message);\n }\n }\n if (isTxError && txError) {\n const parsed = parseError(txError, txType || \"mint\");\n setParsedError(parsed);\n dispatch({ type: \"TX_ERROR\", payload: txError.message });\n if (txType === \"mint\") {\n onMintError?.(txError.message);\n }\n }\n if (writeData && !isTxSuccess && !isTxError) {\n // Transaction submitted, waiting for confirmation\n if (txType === \"approval\") {\n dispatch({ type: \"APPROVE_TX_SUBMITTED\", payload: writeData });\n } else if (txType === \"mint\") {\n dispatch({ type: \"MINT_TX_SUBMITTED\", payload: writeData });\n }\n }\n if (isTxSuccess && writeData) {\n if (txType === \"approval\") {\n dispatch({ type: \"APPROVE_SUCCESS\" });\n } else if (txType === \"mint\") {\n dispatch({ type: \"TX_SUCCESS\", payload: writeData });\n onMintSuccess?.(writeData);\n }\n }\n }, [\n isTxSuccess,\n writeData,\n onMintSuccess,\n isTxError,\n txError,\n onMintError,\n writeError,\n txType,\n ]);\n\n const handleClose = React.useCallback(() => {\n setIsSheetOpen(false);\n dispatch({ type: \"RESET\" });\n setParsedError(null);\n }, []);\n\n // Auto-close on success after 10 seconds\n React.useEffect(() => {\n if (step === \"success\") {\n const timer = setTimeout(() => {\n handleClose();\n }, 10000);\n return () => clearTimeout(timer);\n }\n }, [step, handleClose]);\n\n const handleSwitchNetwork = async () => {\n try {\n await switchChain({ chainId });\n } catch (err) {\n // Network switch failed - user likely rejected or wallet doesn't support it\n }\n };\n\n // Detect NFT provider and validate\n const detectAndValidate = async () => {\n dispatch({ type: \"DETECT_START\" });\n\n try {\n // Detect provider\n const info = await detectNFTProvider(mintParams);\n\n // Validate parameters\n const validation = validateParameters(mintParams, info);\n\n if (!validation.isValid) {\n dispatch({ type: \"VALIDATION_ERROR\", payload: validation.errors });\n return;\n }\n\n // Fetch optimized price data\n const client = getClientForChain(chainId);\n const fetchedPriceData = await fetchPriceData(client, mintParams, info);\n\n // Update contract info with ERC20 details and claim data\n if (fetchedPriceData.erc20Details) {\n info.erc20Token = fetchedPriceData.erc20Details\n .address as `0x${string}`;\n info.erc20Symbol = fetchedPriceData.erc20Details.symbol;\n info.erc20Decimals = fetchedPriceData.erc20Details.decimals;\n }\n\n // Add claim data if available\n if (fetchedPriceData.claim) {\n info.claim = fetchedPriceData.claim;\n }\n\n dispatch({\n type: \"DETECT_SUCCESS\",\n payload: {\n contractInfo: info,\n priceData: {\n mintPrice: fetchedPriceData.mintPrice,\n totalCost: fetchedPriceData.totalCost,\n erc20Details: fetchedPriceData.erc20Details,\n },\n },\n });\n } catch (err) {\n dispatch({\n type: \"DETECT_ERROR\",\n payload: \"Failed to detect NFT contract type\",\n });\n }\n };\n\n // Check allowance only (without re-detecting everything)\n const checkAllowanceOnly = React.useCallback(async () => {\n if (!contractInfo || !erc20Details || !address) return;\n\n try {\n const client = getClientForChain(chainId);\n const spenderAddress =\n contractInfo.provider === \"manifold\" && contractInfo.extensionAddress\n ? contractInfo.extensionAddress\n : contractAddress;\n\n const allowance = await client.readContract({\n address: erc20Details.address as `0x${string}`,\n abi: [\n {\n name: \"allowance\",\n type: \"function\",\n inputs: [\n { name: \"owner\", type: \"address\" },\n { name: \"spender\", type: \"address\" },\n ],\n outputs: [{ type: \"uint256\" }],\n stateMutability: \"view\",\n },\n ],\n functionName: \"allowance\",\n args: [address, spenderAddress],\n });\n\n dispatch({ type: \"UPDATE_ALLOWANCE\", payload: allowance as bigint });\n } catch (err) {\n // Allowance check failed - will proceed without pre-checked allowance\n }\n }, [contractInfo, erc20Details, address, chainId, contractAddress]);\n\n // Re-check allowance after wallet connection\n React.useEffect(() => {\n if (\n isConnected &&\n address &&\n erc20Details &&\n erc20Details.allowance === undefined\n ) {\n checkAllowanceOnly();\n }\n }, [isConnected, address, erc20Details, checkAllowanceOnly]);\n\n const handleInitialMint = async () => {\n if (!isSDKLoaded) {\n dispatch({ type: \"TX_ERROR\", payload: \"Farcaster SDK not loaded\" });\n setIsSheetOpen(true);\n return;\n }\n\n setIsSheetOpen(true);\n await detectAndValidate();\n };\n\n const handleConnectWallet = async () => {\n try {\n dispatch({ type: \"CONNECT_START\" });\n const connector = farcasterFrame();\n connect({ connector });\n } catch (err) {\n handleError(err, \"Failed to connect wallet\");\n }\n };\n\n const handleApprove = async () => {\n if (!isConnected || !erc20Details || !contractInfo?.claim) {\n dispatch({\n type: \"TX_ERROR\",\n payload: \"Missing required information for approval\",\n });\n return;\n }\n\n try {\n dispatch({ type: \"APPROVE_START\" });\n\n // For Manifold, approve the extension contract, not the NFT contract\n const spenderAddress =\n contractInfo.provider === \"manifold\" && contractInfo.extensionAddress\n ? contractInfo.extensionAddress\n : contractAddress;\n\n // Approve exact amount needed\n await writeContract({\n address: erc20Details.address as `0x${string}`,\n abi: [\n {\n name: \"approve\",\n type: \"function\",\n inputs: [\n { name: \"spender\", type: \"address\" },\n { name: \"amount\", type: \"uint256\" },\n ],\n outputs: [{ type: \"bool\" }],\n stateMutability: \"nonpayable\",\n },\n ],\n functionName: \"approve\",\n args: [spenderAddress, contractInfo.claim.cost],\n chainId,\n });\n \n // The transaction has been initiated - we'll track it via writeData in the effect\n } catch (err) {\n handleError(err, \"Approval failed\", \"approval\");\n }\n };\n\n const handleMint = async () => {\n if (!isConnected) {\n await handleConnectWallet();\n return;\n }\n\n if (!contractInfo || !providerConfig) {\n dispatch({\n type: \"TX_ERROR\",\n payload: \"Contract information not available\",\n });\n return;\n }\n\n try {\n dispatch({ type: \"MINT_START\" });\n\n const args = providerConfig.mintConfig.buildArgs(mintParams);\n \n const value = priceData.totalCost || BigInt(0);\n\n // Handle Manifold's special case\n const mintAddress =\n contractInfo.provider === \"manifold\" && contractInfo.extensionAddress\n ? contractInfo.extensionAddress\n : contractAddress;\n\n \n // Prepare contract config based on provider type\n let contractConfig: any;\n \n contractConfig = {\n address: mintAddress,\n abi: providerConfig.mintConfig.abi,\n functionName: providerConfig.mintConfig.functionName,\n args,\n value,\n chainId,\n };\n\n // Execute the transaction\n await writeContract(contractConfig);\n \n // The transaction has been initiated - we'll track it via writeData in the effect\n } catch (err) {\n handleError(err, \"Mint transaction failed\", \"mint\");\n }\n };\n\n // Centralized error handler\n const handleError = (\n error: unknown,\n context: string,\n transactionType?: \"approval\" | \"mint\",\n ) => {\n console.error(`${context}:`, error);\n const message = error instanceof Error ? error.message : `${context}`;\n \n // Parse the error for better UX\n const parsed = parseError(error, transactionType || \"mint\");\n setParsedError(parsed);\n \n dispatch({ type: \"TX_ERROR\", payload: message });\n // Use explicit transaction type if provided, otherwise fall back to state\n if ((transactionType || txType) === \"mint\") {\n onMintError?.(message);\n }\n };\n\n const handleRetry = () => {\n dispatch({ type: \"RESET\" });\n detectAndValidate();\n };\n\n // Display helpers (quick win: centralized formatting)\n const formatPrice = (amount: bigint, decimals: number, symbol: string) => {\n if (amount === BigInt(0)) return \"Free\";\n return `${Number(amount) / 10 ** decimals} ${symbol}`;\n };\n\n const displayPrice = () => {\n if (erc20Details && contractInfo?.claim) {\n return formatPrice(\n contractInfo.claim.cost || BigInt(0),\n erc20Details.decimals || 18,\n erc20Details.symbol,\n );\n }\n return priceData.mintPrice\n ? `${formatEther(priceData.mintPrice)} ETH`\n : \"Free\";\n };\n\n const displayTotalCost = () => {\n if (erc20Details && contractInfo?.claim) {\n // For Manifold, cost is per claim, not per NFT amount\n return formatPrice(\n contractInfo.claim.cost || BigInt(0),\n erc20Details.decimals || 18,\n erc20Details.symbol,\n );\n }\n return priceData.totalCost\n ? `${formatEther(priceData.totalCost)} ETH`\n : \"Free\";\n };\n\n const displayMintFee = () => {\n const fee = priceData.mintPrice || BigInt(0);\n return fee > BigInt(0) ? `${formatEther(fee)} ETH` : \"0 ETH\";\n };\n\n const providerName = contractInfo?.provider\n ? contractInfo.provider.charAt(0).toUpperCase() +\n contractInfo.provider.slice(1)\n : \"Unknown\";\n\n // Quick win: validation helper\n const isReadyToMint = () => {\n return (\n isConnected &&\n contractInfo &&\n !isLoading &&\n step === \"sheet\" &&\n (!erc20Details || !erc20Details.needsApproval)\n );\n };\n\n return (\n {\n setIsSheetOpen(open);\n if (!open) {\n handleClose();\n }\n }}\n >\n \n \n {buttonText}\n \n\n \n \n \n {step === \"detecting\" && \"Detecting NFT Type\"}\n {step === \"sheet\" && \"Mint NFT\"}\n {step === \"connecting\" && \"Connecting Wallet\"}\n {step === \"approve\" && \"Approve Token\"}\n {step === \"approving\" && \"Approving...\"}\n {step === \"minting\" && \"Preparing Mint\"}\n {step === \"waiting\" &&\n (txType === \"approval\" ? \"Approving...\" : \"Minting...\")}\n {step === \"success\" && \"Mint Successful!\"}\n {step === \"error\" && (parsedError?.type === \"user-rejected\" ? \"Transaction Cancelled\" : \"Transaction Failed\")}\n {step === \"validation-error\" && \"Missing Information\"}\n \n \n\n {/* Detecting Provider */}\n {step === \"detecting\" && (\n
\n
\n \n
\n

\n Detecting NFT contract type...\n

\n
\n )}\n\n {/* Validation Error */}\n {step === \"validation-error\" && (\n
\n
\n \n
\n
\n

\n Missing Required Information\n

\n {validationErrors.map((err, idx) => (\n \n {err}\n

\n ))}\n
\n \n
\n )}\n\n {/* Approve Step */}\n {step === \"approve\" && erc20Details && (\n
\n
\n

Approval Required

\n

\n This NFT requires payment in {erc20Details.symbol}. You need to\n approve the contract to spend your tokens.\n

\n
\n
\n
\n Token\n {erc20Details.symbol}\n
\n
\n Amount to Approve\n \n {contractInfo?.claim\n ? Number(contractInfo.claim.cost) /\n 10 ** erc20Details.decimals\n : 0}{\" \"}\n {erc20Details.symbol}\n \n
\n
\n \n \n Approve {erc20Details.symbol}\n \n
\n )}\n\n {/* Main Sheet Content */}\n {step === \"sheet\" && contractInfo && (\n
\n {/* Network warning */}\n {!isCorrectNetwork && isConnected && (\n
\n
\n
\n \n

Wrong network

\n
\n \n Switch to {networkName}\n \n
\n
\n )}\n \n
\n
\n Provider\n {providerName}\n
\n
\n Contract\n \n {contractAddress.slice(0, 6)}...{contractAddress.slice(-4)}\n \n
\n
\n Quantity\n {amount}\n
\n
\n Price per NFT\n {displayPrice()}\n
\n {erc20Details && (\n
\n Mint Fee\n {displayMintFee()}\n
\n )}\n
\n Total Cost\n {displayTotalCost()}\n
\n
\n\n \n {isConnected ? (\n !isCorrectNetwork ? (\n <>\n \n Switch Network\n Switch Network to Mint\n \n ) : (\n <>\n \n Mint {amount} NFT{amount > 1 ? \"s\" : \"\"}\n \n )\n ) : (\n <>\n \n Connect\n Connect Wallet to Mint\n \n )}\n \n
\n )}\n\n {/* Connecting */}\n {step === \"connecting\" && (\n
\n
\n \n
\n

\n Connecting to your Farcaster wallet...\n

\n
\n )}\n\n {/* Minting/Approving */}\n {(step === \"minting\" || step === \"approving\") && (\n
\n
\n \n
\n
\n

\n {step === \"approving\"\n ? \"Preparing approval\"\n : \"Preparing mint transaction\"}\n

\n

\n Please approve the transaction in your wallet\n

\n
\n
\n )}\n\n {/* Waiting for Transaction */}\n {step === \"waiting\" && (\n
\n
\n \n
\n
\n

\n {txType === \"approval\"\n ? \"Approval submitted\"\n : \"Transaction submitted\"}\n

\n

\n Waiting for confirmation on the blockchain...\n

\n {txHash && (\n

\n {txHash.slice(0, 10)}...{txHash.slice(-8)}\n

\n )}\n
\n
\n )}\n\n {/* Success */}\n {step === \"success\" && (\n
\n
\n \n
\n
\n

Minted! 🎉

\n

\n {amount} NFT{amount > 1 ? \"s\" : \"\"} successfully minted\n

\n
\n {txHash && (\n
\n \n
\n )}\n \n
\n )}\n\n {/* Error State */}\n {step === \"error\" && (\n
\n
\n
\n
\n {parsedError?.type === \"user-rejected\" ? (\n \n ) : (\n \n )}\n
\n
\n
\n

\n {parsedError?.message || \"Transaction Failed\"}\n

\n {parsedError?.details && (\n

\n {parsedError.details}\n

\n )}\n
\n
\n\n {/* Special handling for wrong network */}\n {!isCorrectNetwork && isConnected && (\n
\n
\n \n
\n

Wrong Network

\n

\n Please switch to {networkName} to continue\n

\n
\n
\n \n Switch to {networkName}\n \n
\n )}\n\n {/* Specific error actions */}\n {parsedError?.type === \"insufficient-funds\" && (\n
\n
\n \n
\n

Insufficient Balance

\n

\n Make sure you have enough:\n

\n
    \n {erc20Details ? (\n <>\n
  • {erc20Details.symbol} for the NFT price
  • \n
  • ETH for gas fees
  • \n \n ) : (\n
  • ETH for both NFT price and gas fees
  • \n )}\n
\n
\n
\n
\n )}\n\n {/* Action buttons */}\n
\n \n Close\n \n \n
\n
\n )}\n \n \n );\n}\n\n/**\n * Preset builders for common NFT minting scenarios.\n * These provide type-safe, self-documenting ways to create NFTMintButton components.\n */\nNFTMintButton.presets = {\n /**\n * Create a basic ETH-based NFT mint\n * @example\n * ```tsx\n * \n * ```\n */\n generic: (props: {\n contractAddress: Address;\n chainId: number;\n amount?: number;\n buttonText?: string;\n onMintSuccess?: (txHash: string) => void;\n onMintError?: (error: string) => void;\n }): NFTMintFlowProps => ({\n ...props,\n provider: \"generic\",\n amount: props.amount || 1,\n }),\n\n /**\n * Create a Manifold NFT mint with proper configuration\n * @example\n * ```tsx\n * \n * ```\n */\n manifold: (props: {\n contractAddress: Address;\n chainId: number;\n instanceId: string;\n tokenId?: string;\n amount?: number;\n buttonText?: string;\n onMintSuccess?: (txHash: string) => void;\n onMintError?: (error: string) => void;\n }): NFTMintFlowProps => ({\n contractAddress: props.contractAddress,\n chainId: props.chainId,\n provider: \"manifold\",\n manifoldParams: {\n instanceId: props.instanceId,\n tokenId: props.tokenId,\n },\n amount: props.amount || 1,\n buttonText: props.buttonText,\n onMintSuccess: props.onMintSuccess,\n onMintError: props.onMintError,\n }),\n\n /**\n * Create an auto-detecting NFT mint (tries to figure out the provider)\n * @example\n * ```tsx\n * \n * ```\n */\n auto: (props: {\n contractAddress: Address;\n chainId: number;\n amount?: number;\n buttonText?: string;\n onMintSuccess?: (txHash: string) => void;\n onMintError?: (error: string) => void;\n }): NFTMintFlowProps => ({\n ...props,\n amount: props.amount || 1,\n }),\n};\n", "type": "registry:component" }, { @@ -53,7 +53,7 @@ }, { "path": "registry/mini-app/blocks/nft-mint-flow/lib/price-optimizer.ts", - "content": "import type { PublicClient } from \"viem\";\nimport type { NFTContractInfo, MintParams } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/types\";\nimport { getProviderConfig } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/provider-configs\";\nimport { THIRDWEB_OPENEDITONERC721_ABI, THIRDWEB_NATIVE_TOKEN } from \"@/registry/mini-app/lib/nft-standards\";\n\n/**\n * Optimized price discovery that batches RPC calls where possible\n */\nexport async function fetchPriceData(\n client: PublicClient,\n params: MintParams,\n contractInfo: NFTContractInfo\n): Promise<{\n mintPrice?: bigint;\n erc20Details?: {\n address: string;\n symbol: string;\n decimals: number;\n allowance?: bigint;\n balance?: bigint;\n };\n totalCost: bigint;\n claim?: NFTContractInfo[\"claim\"];\n}> {\n const config = getProviderConfig(contractInfo.provider);\n \n if (contractInfo.provider === \"manifold\" && contractInfo.extensionAddress) {\n // For Manifold, we need extension fee + claim cost\n const calls = [\n // Get MINT_FEE from extension\n {\n address: contractInfo.extensionAddress,\n abi: config.mintConfig.abi,\n functionName: \"MINT_FEE\",\n args: []\n }\n ];\n \n // Add claim fetch if we have instanceId\n if (params.instanceId) {\n calls.push({\n address: contractInfo.extensionAddress,\n abi: config.mintConfig.abi,\n functionName: \"getClaim\",\n args: [params.contractAddress, BigInt(params.instanceId)]\n } as any);\n }\n \n try {\n const results = await Promise.all(\n calls.map(call => \n client.readContract(call as any).catch(err => {\n console.error(\"RPC call failed:\", err);\n return null;\n })\n )\n );\n \n const mintFee = results[0] as bigint | null;\n const claim = results[1] as any;\n \n let totalCost = mintFee || BigInt(0);\n let erc20Details = undefined;\n \n if (claim) {\n // Store claim data in contractInfo for later use\n contractInfo.claim = {\n cost: claim.cost,\n merkleRoot: claim.merkleRoot,\n erc20: claim.erc20,\n startDate: claim.startDate,\n endDate: claim.endDate,\n walletMax: claim.walletMax\n };\n \n // Check if ERC20 payment\n if (claim.erc20 && claim.erc20 !== \"0x0000000000000000000000000000000000000000\") {\n // Batch ERC20 details fetch\n const [symbol, decimals, allowance, balance] = await Promise.all([\n client.readContract({\n address: claim.erc20,\n abi: [{ name: \"symbol\", type: \"function\", inputs: [], outputs: [{ type: \"string\" }], stateMutability: \"view\" }],\n functionName: \"symbol\"\n }),\n client.readContract({\n address: claim.erc20,\n abi: [{ name: \"decimals\", type: \"function\", inputs: [], outputs: [{ type: \"uint8\" }], stateMutability: \"view\" }],\n functionName: \"decimals\"\n }),\n params.recipient ? client.readContract({\n address: claim.erc20,\n abi: [{ \n name: \"allowance\", \n type: \"function\", \n inputs: [{ name: \"owner\", type: \"address\" }, { name: \"spender\", type: \"address\" }], \n outputs: [{ type: \"uint256\" }], \n stateMutability: \"view\" \n }],\n functionName: \"allowance\",\n args: [params.recipient, contractInfo.extensionAddress || params.contractAddress]\n }).catch(() => BigInt(0)) : Promise.resolve(undefined), // Return undefined when no recipient, not 0\n params.recipient ? client.readContract({\n address: claim.erc20,\n abi: [{ \n name: \"balanceOf\", \n type: \"function\", \n inputs: [{ name: \"owner\", type: \"address\" }], \n outputs: [{ type: \"uint256\" }], \n stateMutability: \"view\" \n }],\n functionName: \"balanceOf\",\n args: [params.recipient]\n }).catch(() => BigInt(0)) : Promise.resolve(undefined)\n ]);\n \n // Validate decimals\n const validatedDecimals = Number(decimals);\n if (isNaN(validatedDecimals) || validatedDecimals < 0 || validatedDecimals > 255) {\n console.error(`Invalid ERC20 decimals for ${claim.erc20}:`, decimals);\n throw new Error(`Invalid ERC20 decimals: ${decimals}`);\n }\n \n erc20Details = {\n address: claim.erc20,\n symbol: symbol as string,\n decimals: validatedDecimals,\n allowance: allowance as bigint,\n balance: balance as bigint | undefined\n };\n \n // For ERC20, total cost in ETH is just the mint fee\n totalCost = mintFee || BigInt(0);\n } else {\n // ETH payment - add claim cost to mint fee\n totalCost = (mintFee || BigInt(0)) + (claim.cost || BigInt(0));\n }\n }\n \n return {\n mintPrice: mintFee || BigInt(0),\n erc20Details,\n totalCost,\n claim: claim ? contractInfo.claim : undefined\n };\n } catch (err) {\n console.error(\"Failed to fetch Manifold price data:\", err);\n return { totalCost: BigInt(0) };\n }\n } else if (contractInfo.provider === \"nfts2me\") {\n // Special handling for nfts2me - try different pricing patterns\n \n // Pattern 1: Try mintPrice() first - for simple/free NFTs2Me contracts\n try {\n const mintPrice = await client.readContract({\n address: params.contractAddress,\n abi: [{\n inputs: [],\n name: \"mintPrice\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }],\n functionName: \"mintPrice\",\n args: []\n });\n \n if (mintPrice !== undefined) {\n // mintPrice() returns the price per NFT\n const pricePerNFT = mintPrice as bigint;\n const totalCost = pricePerNFT * BigInt(params.amount || 1);\n \n // Handle free mints (mintPrice = 0)\n return {\n mintPrice: pricePerNFT,\n totalCost: totalCost\n };\n }\n } catch (err) {\n // mintPrice() doesn't exist, try next pattern\n console.log(\"mintPrice() not found, trying mintFee pattern\");\n }\n \n // Pattern 2: Try mintFee(amount) + protocolFee() - for more complex pricing\n try {\n // Fetch both fees in parallel\n const [mintFee, protocolFee] = await Promise.all([\n // mintFee is the creator's revenue per NFT\n client.readContract({\n address: params.contractAddress,\n abi: [{\n inputs: [{ name: \"amount\", type: \"uint256\" }],\n name: \"mintFee\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }],\n functionName: \"mintFee\",\n args: [BigInt(params.amount || 1)]\n }),\n // protocolFee is the platform fee (0.0001 ETH unless disabled)\n client.readContract({\n address: params.contractAddress,\n abi: [{\n inputs: [],\n name: \"protocolFee\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }],\n functionName: \"protocolFee\",\n args: []\n })\n ]);\n \n if (mintFee !== undefined && protocolFee !== undefined) {\n // Total cost = creator revenue (mintFee) + platform fee (protocolFee * amount)\n const totalCost = (mintFee as bigint) + ((protocolFee as bigint) * BigInt(params.amount || 1));\n return {\n mintPrice: mintFee as bigint,\n totalCost: totalCost\n };\n }\n } catch (err) {\n console.error(\"Failed to fetch nfts2me fees:\", err);\n }\n \n // Pattern 3: Fallback to default fees if all patterns fail\n const amount = BigInt(params.amount || 1);\n const creatorFeePerNFT = BigInt(\"100000000000000\"); // 0.0001 ETH creator fee per NFT\n const protocolFeePerNFT = BigInt(\"100000000000000\"); // 0.0001 ETH protocol fee per NFT\n return { \n mintPrice: creatorFeePerNFT * amount,\n totalCost: (creatorFeePerNFT + protocolFeePerNFT) * amount\n };\n } else if (contractInfo.provider === \"thirdweb\") {\n // thirdweb OpenEditionERC721 price discovery\n try {\n // First get the active claim condition ID\n const claimCondition = await client.readContract({\n address: params.contractAddress,\n abi: THIRDWEB_OPENEDITONERC721_ABI,\n functionName: \"claimCondition\"\n });\n \n // Validate the response is an array with expected values\n if (!Array.isArray(claimCondition) || claimCondition.length !== 2) {\n throw new Error(\"Invalid claim condition response from contract\");\n }\n \n const [currentStartId, count] = claimCondition as [bigint, bigint];\n \n if (count === BigInt(0)) {\n // No claim conditions set\n return { mintPrice: BigInt(0), totalCost: BigInt(0) };\n }\n \n // Get the active claim condition (last one)\n const activeConditionId = currentStartId + count - BigInt(1);\n \n const condition = await client.readContract({\n address: params.contractAddress,\n abi: THIRDWEB_OPENEDITONERC721_ABI,\n functionName: \"getClaimConditionById\",\n args: [activeConditionId]\n });\n \n if (!condition || typeof condition !== \"object\") {\n throw new Error(\"Invalid claim condition response\");\n }\n \n const {\n startTimestamp,\n maxClaimableSupply,\n supplyClaimed,\n quantityLimitPerWallet,\n merkleRoot,\n pricePerToken,\n currency,\n metadata\n } = condition as any;\n \n // Store claim condition in contractInfo for later use\n contractInfo.claimCondition = {\n id: Number(activeConditionId),\n pricePerToken: pricePerToken as bigint,\n currency: currency as string,\n maxClaimableSupply: maxClaimableSupply as bigint,\n merkleRoot: merkleRoot as `0x${string}`,\n startTimestamp: Number(startTimestamp),\n quantityLimitPerWallet: quantityLimitPerWallet as bigint\n };\n \n // Check if it's ERC20 payment\n if (currency && currency.toLowerCase() !== THIRDWEB_NATIVE_TOKEN.toLowerCase()) {\n // ERC20 payment\n const [symbol, decimals, allowance, balance] = await Promise.all([\n client.readContract({\n address: currency,\n abi: [{ name: \"symbol\", type: \"function\", inputs: [], outputs: [{ type: \"string\" }], stateMutability: \"view\" }],\n functionName: \"symbol\"\n }),\n client.readContract({\n address: currency,\n abi: [{ name: \"decimals\", type: \"function\", inputs: [], outputs: [{ type: \"uint8\" }], stateMutability: \"view\" }],\n functionName: \"decimals\"\n }),\n params.recipient ? client.readContract({\n address: currency,\n abi: [{ \n name: \"allowance\", \n type: \"function\", \n inputs: [{ name: \"owner\", type: \"address\" }, { name: \"spender\", type: \"address\" }], \n outputs: [{ type: \"uint256\" }], \n stateMutability: \"view\" \n }],\n functionName: \"allowance\",\n args: [params.recipient, params.contractAddress]\n }).catch(() => BigInt(0)) : Promise.resolve(undefined),\n params.recipient ? client.readContract({\n address: currency,\n abi: [{ \n name: \"balanceOf\", \n type: \"function\", \n inputs: [{ name: \"owner\", type: \"address\" }], \n outputs: [{ type: \"uint256\" }], \n stateMutability: \"view\" \n }],\n functionName: \"balanceOf\",\n args: [params.recipient]\n }).catch(() => BigInt(0)) : Promise.resolve(undefined)\n ]);\n \n // Validate decimals\n const validatedDecimals = Number(decimals);\n if (isNaN(validatedDecimals) || validatedDecimals < 0 || validatedDecimals > 255) {\n console.error(`Invalid ERC20 decimals for ${currency}:`, decimals);\n throw new Error(`Invalid ERC20 decimals: ${decimals}`);\n }\n \n return {\n mintPrice: pricePerToken as bigint,\n erc20Details: {\n address: currency,\n symbol: symbol as string,\n decimals: validatedDecimals,\n allowance: allowance as bigint,\n balance: balance as bigint | undefined\n },\n totalCost: BigInt(0), // No ETH needed for ERC20 payment\n claim: contractInfo.claimCondition\n };\n } else {\n // ETH payment\n const totalCost = (pricePerToken as bigint) * BigInt(params.amount || 1);\n return {\n mintPrice: pricePerToken as bigint,\n totalCost,\n claim: contractInfo.claimCondition\n };\n }\n } catch (err) {\n console.error(\"Failed to fetch thirdweb price data:\", err);\n return { totalCost: BigInt(0) };\n }\n } else {\n // Generic price discovery - try multiple function names\n const functionNames = config.priceDiscovery.functionNames;\n \n for (const functionName of functionNames) {\n try {\n // Check if this function requires an amount parameter\n const args = config.priceDiscovery.requiresAmountParam \n ? [BigInt(params.amount || 1)]\n : [];\n \n const price = await client.readContract({\n address: params.contractAddress,\n abi: config.priceDiscovery.abis[0],\n functionName: functionName as any,\n args\n });\n \n if (price !== undefined) {\n // Calculate total cost based on provider's custom logic\n const totalCost = config.mintConfig.calculateValue(price as bigint, params);\n return {\n mintPrice: price as bigint,\n totalCost\n };\n }\n } catch {\n // Try next function name\n continue;\n }\n }\n \n // No price found, assume free mint\n return { mintPrice: BigInt(0), totalCost: BigInt(0) };\n }\n}", + "content": "import type { PublicClient } from \"viem\";\nimport type { NFTContractInfo, MintParams } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/types\";\nimport { getProviderConfig } from \"@/registry/mini-app/blocks/nft-mint-flow/lib/provider-configs\";\nimport { THIRDWEB_OPENEDITONERC721_ABI, THIRDWEB_NATIVE_TOKEN } from \"@/registry/mini-app/lib/nft-standards\";\n\n/**\n * Optimized price discovery that batches RPC calls where possible\n */\nexport async function fetchPriceData(\n client: PublicClient,\n params: MintParams,\n contractInfo: NFTContractInfo\n): Promise<{\n mintPrice?: bigint;\n erc20Details?: {\n address: string;\n symbol: string;\n decimals: number;\n allowance?: bigint;\n balance?: bigint;\n };\n totalCost: bigint;\n claim?: NFTContractInfo[\"claim\"];\n}> {\n const config = getProviderConfig(contractInfo.provider);\n \n if (contractInfo.provider === \"manifold\" && contractInfo.extensionAddress) {\n // For Manifold, we need extension fee + claim cost\n const calls = [\n // Get MINT_FEE from extension\n {\n address: contractInfo.extensionAddress,\n abi: config.mintConfig.abi,\n functionName: \"MINT_FEE\",\n args: []\n }\n ];\n \n // Add claim fetch if we have instanceId\n if (params.instanceId) {\n calls.push({\n address: contractInfo.extensionAddress,\n abi: config.mintConfig.abi,\n functionName: \"getClaim\",\n args: [params.contractAddress, BigInt(params.instanceId)]\n } as any);\n }\n \n try {\n const results = await Promise.all(\n calls.map(call => \n client.readContract(call as any).catch(err => {\n console.error(\"RPC call failed:\", err);\n return null;\n })\n )\n );\n \n const mintFee = results[0] as bigint | null;\n const claim = results[1] as any;\n \n let totalCost = mintFee || BigInt(0);\n let erc20Details = undefined;\n \n if (claim) {\n // Store claim data in contractInfo for later use\n contractInfo.claim = {\n cost: claim.cost,\n merkleRoot: claim.merkleRoot,\n erc20: claim.erc20,\n startDate: claim.startDate,\n endDate: claim.endDate,\n walletMax: claim.walletMax\n };\n \n // Check if ERC20 payment\n if (claim.erc20 && claim.erc20 !== \"0x0000000000000000000000000000000000000000\") {\n // Batch ERC20 details fetch\n const [symbol, decimals, allowance, balance] = await Promise.all([\n client.readContract({\n address: claim.erc20,\n abi: [{ name: \"symbol\", type: \"function\", inputs: [], outputs: [{ type: \"string\" }], stateMutability: \"view\" }],\n functionName: \"symbol\"\n }),\n client.readContract({\n address: claim.erc20,\n abi: [{ name: \"decimals\", type: \"function\", inputs: [], outputs: [{ type: \"uint8\" }], stateMutability: \"view\" }],\n functionName: \"decimals\"\n }),\n params.recipient ? client.readContract({\n address: claim.erc20,\n abi: [{ \n name: \"allowance\", \n type: \"function\", \n inputs: [{ name: \"owner\", type: \"address\" }, { name: \"spender\", type: \"address\" }], \n outputs: [{ type: \"uint256\" }], \n stateMutability: \"view\" \n }],\n functionName: \"allowance\",\n args: [params.recipient, contractInfo.extensionAddress || params.contractAddress]\n }).catch(() => BigInt(0)) : Promise.resolve(undefined), // Return undefined when no recipient, not 0\n params.recipient ? client.readContract({\n address: claim.erc20,\n abi: [{ \n name: \"balanceOf\", \n type: \"function\", \n inputs: [{ name: \"owner\", type: \"address\" }], \n outputs: [{ type: \"uint256\" }], \n stateMutability: \"view\" \n }],\n functionName: \"balanceOf\",\n args: [params.recipient]\n }).catch(() => BigInt(0)) : Promise.resolve(undefined)\n ]);\n \n // Validate decimals\n const validatedDecimals = Number(decimals);\n if (isNaN(validatedDecimals) || validatedDecimals < 0 || validatedDecimals > 255) {\n console.error(`Invalid ERC20 decimals for ${claim.erc20}:`, decimals);\n throw new Error(`Invalid ERC20 decimals: ${decimals}`);\n }\n \n erc20Details = {\n address: claim.erc20,\n symbol: symbol as string,\n decimals: validatedDecimals,\n allowance: allowance as bigint,\n balance: balance as bigint | undefined\n };\n \n // For ERC20, total cost in ETH is just the mint fee\n totalCost = mintFee || BigInt(0);\n } else {\n // ETH payment - add claim cost to mint fee\n totalCost = (mintFee || BigInt(0)) + (claim.cost || BigInt(0));\n }\n }\n \n return {\n mintPrice: mintFee || BigInt(0),\n erc20Details,\n totalCost,\n claim: claim ? contractInfo.claim : undefined\n };\n } catch (err) {\n console.error(\"Failed to fetch Manifold price data:\", err);\n return { totalCost: BigInt(0) };\n }\n } else if (contractInfo.provider === \"nfts2me\") {\n // Special handling for nfts2me - try different pricing patterns\n \n // Pattern 1: Try mintPrice() first - for simple/free NFTs2Me contracts\n try {\n const mintPrice = await client.readContract({\n address: params.contractAddress,\n abi: [{\n inputs: [],\n name: \"mintPrice\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }],\n functionName: \"mintPrice\",\n args: []\n });\n \n if (mintPrice !== undefined) {\n // mintPrice() returns the price per NFT\n const pricePerNFT = mintPrice as bigint;\n const totalCost = pricePerNFT * BigInt(params.amount || 1);\n \n // Handle free mints (mintPrice = 0)\n return {\n mintPrice: pricePerNFT,\n totalCost: totalCost\n };\n }\n } catch (err) {\n // mintPrice() doesn't exist, try next pattern\n console.log(\"mintPrice() not found, trying mintFee pattern\");\n }\n \n // Pattern 2: Try mintFee(amount) + protocolFee() - for more complex pricing\n try {\n // Fetch both fees in parallel\n const [mintFee, protocolFee] = await Promise.all([\n // mintFee is the creator's revenue per NFT\n client.readContract({\n address: params.contractAddress,\n abi: [{\n inputs: [{ name: \"amount\", type: \"uint256\" }],\n name: \"mintFee\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }],\n functionName: \"mintFee\",\n args: [BigInt(params.amount || 1)]\n }),\n // protocolFee is the platform fee (0.0001 ETH unless disabled)\n client.readContract({\n address: params.contractAddress,\n abi: [{\n inputs: [],\n name: \"protocolFee\",\n outputs: [{ name: \"\", type: \"uint256\" }],\n stateMutability: \"view\",\n type: \"function\"\n }],\n functionName: \"protocolFee\",\n args: []\n })\n ]);\n \n if (mintFee !== undefined && protocolFee !== undefined) {\n // Total cost = creator revenue (mintFee) + platform fee (protocolFee * amount)\n const totalCost = (mintFee as bigint) + ((protocolFee as bigint) * BigInt(params.amount || 1));\n return {\n mintPrice: mintFee as bigint,\n totalCost: totalCost\n };\n }\n } catch (err) {\n console.error(\"Failed to fetch nfts2me fees:\", err);\n }\n \n // Pattern 3: Fallback to default fees if all patterns fail\n const amount = BigInt(params.amount || 1);\n const creatorFeePerNFT = BigInt(\"100000000000000\"); // 0.0001 ETH creator fee per NFT\n const protocolFeePerNFT = BigInt(\"100000000000000\"); // 0.0001 ETH protocol fee per NFT\n return { \n mintPrice: creatorFeePerNFT * amount,\n totalCost: (creatorFeePerNFT + protocolFeePerNFT) * amount\n };\n } else if (contractInfo.provider === \"thirdweb\") {\n // thirdweb OpenEditionERC721 price discovery\n try {\n // First get the active claim condition ID\n const claimCondition = await client.readContract({\n address: params.contractAddress,\n abi: THIRDWEB_OPENEDITONERC721_ABI,\n functionName: \"claimCondition\"\n });\n \n // Validate the response is an array with expected values\n if (!Array.isArray(claimCondition) || claimCondition.length !== 2) {\n throw new Error(\"Invalid claim condition response from contract\");\n }\n \n const [currentStartId, count] = claimCondition as [bigint, bigint];\n \n if (count === BigInt(0)) {\n // No claim conditions set\n return { mintPrice: BigInt(0), totalCost: BigInt(0) };\n }\n \n // Get the active claim condition (last one)\n const activeConditionId = currentStartId + count - BigInt(1);\n \n const condition = await client.readContract({\n address: params.contractAddress,\n abi: THIRDWEB_OPENEDITONERC721_ABI,\n functionName: \"getClaimConditionById\",\n args: [activeConditionId]\n });\n \n if (!condition || typeof condition !== \"object\") {\n throw new Error(\"Invalid claim condition response\");\n }\n \n const {\n startTimestamp,\n maxClaimableSupply,\n supplyClaimed,\n quantityLimitPerWallet,\n merkleRoot,\n pricePerToken,\n currency,\n metadata\n } = condition as any;\n \n // Store claim condition in contractInfo for later use\n contractInfo.claimCondition = {\n id: Number(activeConditionId),\n pricePerToken: pricePerToken as bigint,\n currency: currency as `0x${string}`,\n maxClaimableSupply: maxClaimableSupply as bigint,\n merkleRoot: merkleRoot as `0x${string}`,\n startTimestamp: Number(startTimestamp),\n quantityLimitPerWallet: quantityLimitPerWallet as bigint\n };\n \n // Check if it's ERC20 payment\n if (currency && currency.toLowerCase() !== THIRDWEB_NATIVE_TOKEN.toLowerCase()) {\n // ERC20 payment\n const [symbol, decimals, allowance, balance] = await Promise.all([\n client.readContract({\n address: currency,\n abi: [{ name: \"symbol\", type: \"function\", inputs: [], outputs: [{ type: \"string\" }], stateMutability: \"view\" }],\n functionName: \"symbol\"\n }),\n client.readContract({\n address: currency,\n abi: [{ name: \"decimals\", type: \"function\", inputs: [], outputs: [{ type: \"uint8\" }], stateMutability: \"view\" }],\n functionName: \"decimals\"\n }),\n params.recipient ? client.readContract({\n address: currency,\n abi: [{ \n name: \"allowance\", \n type: \"function\", \n inputs: [{ name: \"owner\", type: \"address\" }, { name: \"spender\", type: \"address\" }], \n outputs: [{ type: \"uint256\" }], \n stateMutability: \"view\" \n }],\n functionName: \"allowance\",\n args: [params.recipient, params.contractAddress]\n }).catch(() => BigInt(0)) : Promise.resolve(undefined),\n params.recipient ? client.readContract({\n address: currency,\n abi: [{ \n name: \"balanceOf\", \n type: \"function\", \n inputs: [{ name: \"owner\", type: \"address\" }], \n outputs: [{ type: \"uint256\" }], \n stateMutability: \"view\" \n }],\n functionName: \"balanceOf\",\n args: [params.recipient]\n }).catch(() => BigInt(0)) : Promise.resolve(undefined)\n ]);\n \n // Validate decimals\n const validatedDecimals = Number(decimals);\n if (isNaN(validatedDecimals) || validatedDecimals < 0 || validatedDecimals > 255) {\n console.error(`Invalid ERC20 decimals for ${currency}:`, decimals);\n throw new Error(`Invalid ERC20 decimals: ${decimals}`);\n }\n \n return {\n mintPrice: pricePerToken as bigint,\n erc20Details: {\n address: currency,\n symbol: symbol as string,\n decimals: validatedDecimals,\n allowance: allowance as bigint,\n balance: balance as bigint | undefined\n },\n totalCost: BigInt(0) // No ETH needed for ERC20 payment\n };\n } else {\n // ETH payment\n const totalCost = (pricePerToken as bigint) * BigInt(params.amount || 1);\n return {\n mintPrice: pricePerToken as bigint,\n totalCost\n };\n }\n } catch (err) {\n console.error(\"Failed to fetch thirdweb price data:\", err);\n return { totalCost: BigInt(0) };\n }\n } else {\n // Generic price discovery - try multiple function names\n const functionNames = config.priceDiscovery.functionNames;\n \n for (const functionName of functionNames) {\n try {\n // Check if this function requires an amount parameter\n const args = config.priceDiscovery.requiresAmountParam \n ? [BigInt(params.amount || 1)]\n : [];\n \n const price = await client.readContract({\n address: params.contractAddress,\n abi: config.priceDiscovery.abis[0],\n functionName: functionName as any,\n args\n });\n \n if (price !== undefined) {\n // Calculate total cost based on provider's custom logic\n const totalCost = config.mintConfig.calculateValue(price as bigint, params);\n return {\n mintPrice: price as bigint,\n totalCost\n };\n }\n } catch {\n // Try next function name\n continue;\n }\n }\n \n // No price found, assume free mint\n return { mintPrice: BigInt(0), totalCost: BigInt(0) };\n }\n}", "type": "registry:lib" }, { From 16802f6beeb2536a4357996cfa8420589181754c Mon Sep 17 00:00:00 2001 From: hellno Date: Fri, 25 Jul 2025 16:32:54 +0200 Subject: [PATCH 4/4] Delete claude-code-review.yml --- .github/workflows/claude-code-review.yml | 78 ------------------------ 1 file changed, 78 deletions(-) delete mode 100644 .github/workflows/claude-code-review.yml diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml deleted file mode 100644 index 5bf8ce59..00000000 --- a/.github/workflows/claude-code-review.yml +++ /dev/null @@ -1,78 +0,0 @@ -name: Claude Code Review - -on: - pull_request: - types: [opened, synchronize] - # Optional: Only run on specific file changes - # paths: - # - "src/**/*.ts" - # - "src/**/*.tsx" - # - "src/**/*.js" - # - "src/**/*.jsx" - -jobs: - claude-review: - # Optional: Filter by PR author - # if: | - # github.event.pull_request.user.login == 'external-contributor' || - # github.event.pull_request.user.login == 'new-developer' || - # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - issues: read - id-token: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Run Claude Code Review - id: claude-review - uses: anthropics/claude-code-action@beta - with: - claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - - # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) - # model: "claude-opus-4-20250514" - - # Direct prompt for automated review (no @claude mention needed) - direct_prompt: | - Please review this pull request and provide feedback on: - - Code quality and best practices - - Potential bugs or issues - - Performance considerations - - Security concerns - - Test coverage - - Be constructive and helpful in your feedback. - - # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR - # use_sticky_comment: true - - # Optional: Customize review based on file types - # direct_prompt: | - # Review this PR focusing on: - # - For TypeScript files: Type safety and proper interface usage - # - For API endpoints: Security, input validation, and error handling - # - For React components: Performance, accessibility, and best practices - # - For tests: Coverage, edge cases, and test quality - - # Optional: Different prompts for different authors - # direct_prompt: | - # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && - # 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' || - # 'Please provide a thorough code review focusing on our coding standards and best practices.' }} - - # Optional: Add specific tools for running tests or linting - # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)" - - # Optional: Skip review for certain conditions - # if: | - # !contains(github.event.pull_request.title, '[skip-review]') && - # !contains(github.event.pull_request.title, '[WIP]') -