diff --git a/app/[locale]/10years/_components/CurrentTorchHolderCard.tsx b/app/[locale]/10years/_components/CurrentTorchHolderCard.tsx index 7dd73eb2b74..fb282e9284f 100644 --- a/app/[locale]/10years/_components/CurrentTorchHolderCard.tsx +++ b/app/[locale]/10years/_components/CurrentTorchHolderCard.tsx @@ -1,8 +1,4 @@ -import { - AvatarBase as Avatar, - AvatarFallback, - AvatarImage, -} from "@/components/ui/avatar" +import { Avatar } from "@/components/ui/avatar" import { ButtonLink } from "@/components/ui/buttons/Button" import { Card, @@ -18,14 +14,15 @@ import { cn } from "@/lib/utils/cn" import Curved10YearsText from "./10y.svg" import { + extractTwitterHandle, formatAddress, - getAddressEtherscanUrl, getAvatarImage, - type TorchHolderMetadata, + getTxEtherscanUrl, + type TorchHolderEvent, } from "@/lib/torch" interface CurrentTorchHolderCardProps { - currentHolder: TorchHolderMetadata | null + currentHolder: TorchHolderEvent | null isBurned?: boolean className?: string } @@ -78,15 +75,12 @@ const CurrentTorchHolderCard = ({ {currentHolder ? (
- - - - {currentHolder.name || formatAddress(currentHolder.address)} - - +
{/* Name */} @@ -98,7 +92,7 @@ const CurrentTorchHolderCard = ({ {/* Verify onchain link */} View on Etherscan diff --git a/app/[locale]/10years/_components/TorchHistoryCard.tsx b/app/[locale]/10years/_components/TorchHistoryCard.tsx index 6b8c259a245..9419b1fca3b 100644 --- a/app/[locale]/10years/_components/TorchHistoryCard.tsx +++ b/app/[locale]/10years/_components/TorchHistoryCard.tsx @@ -1,22 +1,23 @@ import React from "react" -import { - AvatarBase as Avatar, - AvatarFallback, - AvatarImage, -} from "@/components/ui/avatar" +import { Avatar } from "@/components/ui/avatar" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { BaseLink } from "@/components/ui/Link" import { Tag } from "@/components/ui/tag" import { cn } from "@/lib/utils/cn" -import { formatDate, getTxEtherscanUrl } from "@/lib/torch" +import { + extractTwitterHandle, + formatDate, + getTxEtherscanUrl, +} from "@/lib/torch" interface TorchHistoryCardProps { name: string role: string avatar: string + twitter: string from: number to: number transactionHash: string @@ -29,6 +30,7 @@ const TorchHistoryCard: React.FC = ({ name, role, avatar, + twitter, from, to, transactionHash, @@ -48,10 +50,12 @@ const TorchHistoryCard: React.FC = ({ >
- - - {name} - +
{isCurrentHolder && ( diff --git a/app/[locale]/10years/_components/TorchHistorySwiper/index.tsx b/app/[locale]/10years/_components/TorchHistorySwiper/index.tsx index 441086dac9b..7528e7dcfcd 100644 --- a/app/[locale]/10years/_components/TorchHistorySwiper/index.tsx +++ b/app/[locale]/10years/_components/TorchHistorySwiper/index.tsx @@ -89,6 +89,7 @@ const TorchHistorySwiper = ({ ? "/images/10-year-anniversary/torch-cover.webp" : getAvatarImage(card) } + twitter={card.twitter} from={card.event.timestamp} to={card.event.timestamp} transactionHash={card.event.transactionHash} diff --git a/app/[locale]/10years/page.tsx b/app/[locale]/10years/page.tsx index c4b3a337465..e19fcd2c2cd 100644 --- a/app/[locale]/10years/page.tsx +++ b/app/[locale]/10years/page.tsx @@ -49,7 +49,7 @@ import { getTransferEvents, isAddressFiltered, isTorchBurned, - TorchHolder, + TorchHolderEvent, } from "@/lib/torch" import TenYearLogo from "@/public/images/10-year-anniversary/10-year-logo.png" @@ -101,24 +101,26 @@ const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => { {} as Record ) + const transferEvents = await getTransferEvents() + const torchHoldersEvents = await getHolderEvents( + torchHolderMap, + transferEvents + ) + let isBurned = false - let currentHolder: TorchHolder | null = null + let currentHolder: TorchHolderEvent | null = null try { isBurned = await isTorchBurned() const currentHolderAddress = await getCurrentHolderAddress() const isFiltered = isAddressFiltered(currentHolderAddress) + const currentHolderEvent = torchHoldersEvents.find( + (holder) => holder.address === currentHolderAddress.toLowerCase() + ) - currentHolder = isFiltered - ? null - : torchHolderMap[currentHolderAddress.toLowerCase()] + currentHolder = !isFiltered ? (currentHolderEvent ?? null) : null } catch (error) { console.error("Error fetching torch data:", error) } - const transferEvents = await getTransferEvents() - const torchHoldersEvents = await getHolderEvents( - torchHolderMap, - transferEvents - ) // Filter out events where the address is in the filtered list const torchHolders = torchHoldersEvents.filter( diff --git a/next.config.js b/next.config.js index 5c39cb7693e..1182cd32334 100644 --- a/next.config.js +++ b/next.config.js @@ -93,6 +93,10 @@ module.exports = (phase, { defaultConfig }) => { protocol: "https", hostname: "coin-images.coingecko.com", }, + { + protocol: "https", + hostname: "unavatar.io", + }, ], }, async headers() { diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx index 8196a2b6c81..123cdc3b912 100644 --- a/src/components/ui/avatar.tsx +++ b/src/components/ui/avatar.tsx @@ -167,8 +167,8 @@ const Avatar = React.forwardRef< {src ? ( {name} => { + if (!ETHERSCAN_API_KEY) { + throw new Error("ETHERSCAN_API_KEY environment variable is required") + } + + try { + // Get contract events from Etherscan + const response = await fetch( + [ + "https://api.etherscan.io/api", + "?module=logs", + "&action=getLogs", + `&address=${TORCH_CONTRACT_ADDRESS}`, + `&fromBlock=${TORCH_BLOCK_NUMBER}`, + "&toBlock=latest", + // ERC721 Transfer event signature + "&topic0=0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + `&apikey=${ETHERSCAN_API_KEY}`, + ].join("") + ) + + const data = await response.json() + + if (data.status !== "1") { + throw new Error(`Etherscan API error: ${data.message}`) + } + + return data.result.map( + (log: { + topics: string[] + blockNumber: string + transactionHash: string + timeStamp: string + }) => ({ + from: `0x${log.topics[1].slice(26)}` as Address, // Remove padding from topic1 + to: `0x${log.topics[2].slice(26)}` as Address, // Remove padding from topic2 + blockNumber: parseInt(log.blockNumber, 16), + transactionHash: log.transactionHash, + timestamp: parseInt(log.timeStamp, 16), + }) + ) + } catch (error) { + console.error("Failed to fetch torch transfers from Etherscan:", error) + return [] + } +} diff --git a/src/lib/torch/index.ts b/src/lib/torch/index.ts index c661398c037..4da2214e9de 100644 --- a/src/lib/torch/index.ts +++ b/src/lib/torch/index.ts @@ -6,10 +6,10 @@ import { getPublicClient } from "@wagmi/core" import Torch from "@/data/Torch.json" import { config } from "./config" +import { fetchTorchTransfersFromEtherscan } from "./etherscan" const TORCH_CONTRACT_ADDRESS = Torch.address as Address const TORCH_ABI = Torch.abi -const TORCH_BLOCK_NUMBER = Torch.blockNumber // Addresses to filter from the UI (show as "Unknown Holder") const FILTERED_ADDRESSES: string[] = [ @@ -50,54 +50,7 @@ export type TorchHolderEvent = TorchHolder & { export const getTransferEvents = cache( async () => { - const publicClient = getPublicClient(config) - - // Get the current block number to ensure consistent results - const currentBlock = await publicClient.getBlockNumber() - - // Get Transfer events from the contract - // ERC721 Transfer event signature: Transfer(address indexed from, address indexed to, uint256 indexed tokenId) - const logs = await publicClient.getLogs({ - address: TORCH_CONTRACT_ADDRESS, - event: { - type: "event", - name: "Transfer", - inputs: [ - { name: "from", type: "address", indexed: true }, - { name: "to", type: "address", indexed: true }, - { name: "tokenId", type: "uint256", indexed: true }, - ], - }, - args: { - tokenId: BigInt(1), // Torch NFT token ID is always 1 - }, - fromBlock: BigInt(TORCH_BLOCK_NUMBER) || "earliest", - toBlock: currentBlock, - }) - - // Process logs and get timestamps - const transferEvents: TransferEvent[] = [] - - for (const log of logs) { - if (log.args?.from && log.args?.to) { - // Get block details to get timestamp - const block = await publicClient.getBlock({ - blockNumber: log.blockNumber, - }) - - transferEvents.push({ - from: log.args.from as Address, - to: log.args.to as Address, - blockNumber: Number(log.blockNumber), - transactionHash: log.transactionHash, - timestamp: Number(block.timestamp), - }) - } - } - - // Sort by block number (oldest first) - transferEvents.sort((a, b) => a.blockNumber - b.blockNumber) - + const transferEvents = await fetchTorchTransfersFromEtherscan() return transferEvents }, ["torch-transfer-events"], @@ -195,7 +148,7 @@ export const getAvatarImage = (holder: TorchHolderMetadata | null) => { return getBlockieImage(holder.address) } -const extractTwitterHandle = (twitterUrl: string): string | null => { +export const extractTwitterHandle = (twitterUrl: string): string | null => { // Handle various Twitter URL formats const patterns = [ /twitter\.com\/([^/?]+)/, // twitter.com/username