Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 12 additions & 18 deletions app/[locale]/10years/_components/CurrentTorchHolderCard.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
}
Expand Down Expand Up @@ -78,15 +75,12 @@ const CurrentTorchHolderCard = ({
<CardContent className="p-6">
{currentHolder ? (
<div className="flex items-start gap-4">
<Avatar className="h-19 w-19 !shadow-none">
<AvatarImage
src={getAvatarImage(currentHolder)}
alt={`Avatar for ${currentHolder.name || currentHolder.address}`}
/>
<AvatarFallback>
{currentHolder.name || formatAddress(currentHolder.address)}
</AvatarFallback>
</Avatar>
<Avatar
className="h-19 w-19 !shadow-none"
src={getAvatarImage(currentHolder)}
href={`https://x.com/${extractTwitterHandle(currentHolder.twitter)}`}
name={currentHolder.name || formatAddress(currentHolder.address)}
/>

<div className="flex flex-col">
{/* Name */}
Expand All @@ -98,7 +92,7 @@ const CurrentTorchHolderCard = ({
{/* Verify onchain link */}
<BaseLink
className="mt-2 text-xs"
href={getAddressEtherscanUrl(currentHolder.address)}
href={getTxEtherscanUrl(currentHolder.event.transactionHash)}
>
View on Etherscan
</BaseLink>
Expand Down
24 changes: 14 additions & 10 deletions app/[locale]/10years/_components/TorchHistoryCard.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -29,6 +30,7 @@ const TorchHistoryCard: React.FC<TorchHistoryCardProps> = ({
name,
role,
avatar,
twitter,
from,
to,
transactionHash,
Expand All @@ -48,10 +50,12 @@ const TorchHistoryCard: React.FC<TorchHistoryCardProps> = ({
>
<CardHeader className="flex flex-col p-0">
<div className="mb-4 flex flex-col items-center">
<Avatar className="h-32 w-32 border-2 border-gray-100/50 !shadow-none">
<AvatarImage src={avatar} alt={`Avatar for ${name}`} />
<AvatarFallback>{name}</AvatarFallback>
</Avatar>
<Avatar
className="h-32 w-32 border-2 border-gray-100/50 !shadow-none"
src={avatar}
href={`https://x.com/${extractTwitterHandle(twitter)}`}
name={name}
/>
</div>

{isCurrentHolder && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
22 changes: 12 additions & 10 deletions app/[locale]/10years/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -101,24 +101,26 @@ const Page = async ({ params }: { params: Promise<{ locale: Lang }> }) => {
{} as Record<string, (typeof allTorchHolders)[0]>
)

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(
Expand Down
4 changes: 4 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ module.exports = (phase, { defaultConfig }) => {
protocol: "https",
hostname: "coin-images.coingecko.com",
},
{
protocol: "https",
hostname: "unavatar.io",
},
],
},
async headers() {
Expand Down
8 changes: 4 additions & 4 deletions src/components/ui/avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ const Avatar = React.forwardRef<
{src ? (
<Image
className="object-fill"
width={64}
height={64}
width={128}
height={128}
sizes="4rem"
src={src}
alt={name}
Expand All @@ -189,8 +189,8 @@ const Avatar = React.forwardRef<
{src ? (
<Image
className="object-fill"
width={64}
height={64}
width={128}
height={128}
sizes="4rem"
src={src}
alt={name}
Expand Down
66 changes: 66 additions & 0 deletions src/lib/torch/etherscan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { type Address } from "viem"

import Torch from "@/data/Torch.json"

const TORCH_CONTRACT_ADDRESS = Torch.address as Address
const TORCH_BLOCK_NUMBER = Torch.blockNumber

export type TransferEvent = {
from: Address
to: Address
blockNumber: number
transactionHash: string
timestamp: number
}

// You'll need to get an API key from https://etherscan.io/apis
const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY || ""

export const fetchTorchTransfersFromEtherscan = async (): Promise<
TransferEvent[]
> => {
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 []
}
}
53 changes: 3 additions & 50 deletions src/lib/torch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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
Expand Down
Loading