Skip to content

Commit

Permalink
Fix nft address (#143)
Browse files Browse the repository at this point in the history
* feat: Add to wallet button

* refactor: useCommunity hook
  • Loading branch information
shenshin authored Aug 28, 2024
1 parent 56e9245 commit 99c945e
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 149 deletions.
122 changes: 107 additions & 15 deletions src/pages/communities/nft/[address].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,83 @@ import { Button } from '@/components/Button'
import { Chip } from '@/components/Chip/Chip'
import { MainContainer } from '@/components/MainContainer/MainContainer'
import { Paragraph, Span, Typography } from '@/components/Typography'
import { cn, truncateMiddle, shortAddress } from '@/lib/utils'
import { cn, truncateMiddle } from '@/lib/utils'
import Image from 'next/image'
import { useRouter } from 'next/router'
import { ReactNode, useState } from 'react'
import { ReactNode, useState, useEffect } from 'react'
import { BsTwitterX } from 'react-icons/bs'
import { FaDiscord, FaLink } from 'react-icons/fa'
import { Address } from 'viem'
import { useAccount } from 'wagmi'
import { useCommunity } from '@/shared/hooks/useCommunity'
import { useMintNFT } from '@/shared/hooks/useMintNFT'
import { useNftMeta } from '@/shared/hooks/useNFTMeta'
import { CopyButton } from '@/components/CopyButton'

/**
* Name of the local storage variable with information about whether the token was added to the wallet
*/
const IS_IN_WALLET = 'isInWallet'
/**
* Type representing localStorage and state variable for tracking if an NFT token is added to the wallet
* The structure of the variable is the following:
* ```json
* {
* "0x123456...123456": {
* 1: true,
* 2: true
* }
* }
* ```
*/
type IsInWallet = Record<Address, Record<number, boolean>>

/**
* Early Adopter community page
*/
export default function Page() {
const router = useRouter()
const nftAddress = router.query.address as Address | undefined
const { address } = useAccount()
const { tokensAvailable, isMember, tokenId, membersCount, nftName } = useCommunity(nftAddress)
const { onMintNFT, isPending: isClaiming } = useMintNFT(nftAddress)
const {
tokensAvailable,
isMember,
tokenId,
membersCount,
nftName,
nftSymbol,
mint: { onMintNFT, isPending: isClaiming },
nftMeta,
} = useCommunity(nftAddress)

const [message, setMessage] = useState('')
// reset message after few seconds
useEffect(() => {
if (!message) return
const timeout = setTimeout(() => setMessage(''), 5000)
return () => clearTimeout(timeout)
}, [message])
// read from local storage if the NFT was added to the wallet
const [isNftInWallet, setIsNftInWallet] = useState<IsInWallet>(() => {
// prevent from server execution
if (typeof window !== 'undefined') {
const storedValue = window.localStorage.getItem(IS_IN_WALLET)
return storedValue !== null ? JSON.parse(storedValue) : {}
} else {
return {}
}
})

const { meta } = useNftMeta(nftAddress)
// synchronize local storage variable
useEffect(() => {
// prevent from server execution
if (typeof window !== 'undefined') {
window.localStorage.setItem(IS_IN_WALLET, JSON.stringify(isNftInWallet))
}
}, [isNftInWallet])

const handleMinting = () => {
if (!address) return
onMintNFT()
.then(txHash => {
console.log('SUCCESS', txHash)
setMessage(
'Request transaction sent. Your claim is in process. It will be visible when the transaction is confirmed.',
)
Expand All @@ -45,6 +94,40 @@ export default function Page() {
})
}

/**
* Adds NFT to wallet collection
*/
const addToWallet = async () => {
try {
if (typeof window === 'undefined' || !window.ethereum) throw new Error('Wallet is not installed')
if (!nftAddress || !tokenId) throw new Error('Unknown NFT')
// connect wallet in case it was disconnected
await window.ethereum.request({
method: 'eth_requestAccounts',
})
const wasAdded = await window.ethereum.request({
method: 'wallet_watchAsset',
params: {
type: 'ERC721',
options: {
address: nftAddress,
symbol: nftSymbol,
image: nftMeta?.image,
tokenId: String(tokenId),
},
},
})
if (!wasAdded) throw new Error('Unable to add NFT to wallet')
setIsNftInWallet(old => ({ ...old, [nftAddress]: { ...old[nftAddress], [tokenId]: true } }))
setMessage(`NFT#${tokenId} was added to wallet`)
} catch (error) {
// don't show error message if user has closed the wallet prompt
if ((error as { message?: string }).message?.includes('User rejected the request')) return
console.error('ERROR', error)
setMessage(`Error adding NFT#${tokenId} to wallet`)
}
}

if (!nftAddress) return null
return (
<MainContainer notProtected>
Expand Down Expand Up @@ -95,9 +178,11 @@ export default function Page() {
{/* pioneer, holders, followers */}
<div>
<DivWithBorderTop
firstParagraph={`${nftName} NFT`}
firstParagraph={`${nftName ?? ''} NFT`}
secondParagraph={
<CopyButton copyText={address as string}>{truncateMiddle(nftAddress as string)}</CopyButton>
<CopyButton copyText={address as string}>
{truncateMiddle(nftAddress as string, 4, 4)}
</CopyButton>
}
/>
<DivWithBorderTop firstParagraph="Holders" secondParagraph={membersCount} />
Expand All @@ -109,13 +194,13 @@ export default function Page() {
<Span className="mb-6 font-bold inline-block">Membership NFT</Span>
<div className="flex gap-6">
<Image
alt={meta?.name ?? 'NFT'}
src={meta?.image ?? '/images/Early-Adopters-Collection-Cover.png'}
alt={nftMeta?.name ?? 'NFT'}
src={nftMeta?.image || '/images/Early-Adopters-Collection-Cover.png'}
className="w-full self-center max-w-56 rounded-md"
width={500}
height={500}
/>
{isMember ? (
{isMember && tokenId ? (
<div>
<Paragraph variant="semibold" className="text-[18px]">
Early Adopter #{tokenId}
Expand All @@ -126,13 +211,20 @@ export default function Page() {
<Typography tagVariant="span">Owned{address && ' by '}</Typography>
{address && (
<Typography tagVariant="span" className="text-primary">
{shortAddress(address, 3)}
{truncateMiddle(address, 4, 3)}
</Typography>
)}
</div>

{/* `Add to wallet button` */}
{!isNftInWallet?.[nftAddress]?.[tokenId] && (
<Button onClick={addToWallet} className="mb-4">
Add to wallet
</Button>
)}

<Span className="inline-block text-[14px] tracking-wide font-light">
{meta?.description}
{nftMeta?.description}
</Span>
</div>
) : (
Expand Down
143 changes: 87 additions & 56 deletions src/shared/hooks/useCommunity.ts
Original file line number Diff line number Diff line change
@@ -1,85 +1,116 @@
import { useMemo } from 'react'
import { useMemo, useCallback, useEffect, useState } from 'react'
import { abiContractsMap } from '@/lib/contracts'
import { Address } from 'viem'
import { useReadContracts, useAccount } from 'wagmi'
import { useReadContracts, useAccount, useWaitForTransactionReceipt, useWriteContract } from 'wagmi'
import { fetchIpfsUri } from '@/app/user/Balances/actions'
import { NftMeta, CommunityData } from '../types'

/**
* useCommunity hook return properties
* Hook for loading NFT metadata from IPFS
*/
interface CommunityData {
/**
* The remaining number of tokens that can be minted
*/
tokensAvailable: number
/**
* Number of community members who received tokens
*/
membersCount: number
/**
* Tells whether the user is a member of the community
*/
isMember: boolean
/**
* Serial number of the token minted for the user
*/
tokenId: number | undefined
/**
* NFT smart contract name
*/
nftName: string | undefined
const useNftMeta = (nftUri?: string) => {
const [nftMeta, setNftMeta] = useState<NftMeta>()

useEffect(() => {
if (!nftUri) return setNftMeta(undefined)
fetchIpfsUri(nftUri).then(async nftMeta => {
const response = await fetchIpfsUri(nftMeta.image, 'blob')
const url = URL.createObjectURL(response)
setNftMeta({ ...nftMeta, image: url })
})
}, [nftUri])

return nftMeta
}

/**
* Reads different Early Adopters NFT contract functions
* @param nftAddress deployed contract address
* @returns community NFT view functions call results
* Hook for reading NFT contract view functions
*/
export const useCommunity = (nftAddress?: Address): CommunityData => {
export const useContractData = (nftAddress?: Address) => {
const { address } = useAccount()
const contract = {
// verifying `nftAddress` later in `useReadContracts` params
abi: abiContractsMap[nftAddress!],
address: nftAddress,
}
const { data } = useReadContracts(

// load data from the contract view functions
const { data, refetch } = useReadContracts(
address &&
nftAddress && {
contracts: [
{
...contract,
functionName: 'totalSupply',
},
{
...contract,
functionName: 'tokensAvailable',
},
{
...contract,
functionName: 'balanceOf',
args: [address],
},
{
...contract,
functionName: 'tokenIdByOwner',
args: [address],
},
{
...contract,
functionName: 'name',
},
{ ...contract, functionName: 'totalSupply' },
{ ...contract, functionName: 'tokensAvailable' },
{ ...contract, functionName: 'balanceOf', args: [address] },
{ ...contract, functionName: 'tokenIdByOwner', args: [address] },
{ ...contract, functionName: 'name' },
{ ...contract, functionName: 'symbol' },
{ ...contract, functionName: 'tokenUriByOwner', args: [address] },
],
},
)

return useMemo(() => {
const [membersCount, tokensAvailable, balanceOf, tokenIdByOwner, nftName] = data ?? []
const communityData: CommunityData = {
tokensAvailable: Number(tokensAvailable?.result ?? 0n),
const [membersCount, tokensAvailable, balanceOf, tokenIdByOwner, nftName, symbol, nftUri] = data ?? []
return {
refetch,
membersCount: Number(membersCount?.result ?? 0n),
tokensAvailable: Number(tokensAvailable?.result ?? 0n),
isMember: (balanceOf?.result ?? 0n) > 0n,
tokenId: typeof tokenIdByOwner?.result === 'bigint' ? Number(tokenIdByOwner.result) : undefined,
nftName: nftName?.result,
nftSymbol: symbol?.result,
nftUri: nftUri?.result,
}
return communityData
}, [data])
}, [data, refetch])
}

/**
* Hook for executing and watching NFT mint transaction
*/
const useMintNFT = (nftAddress?: Address, tokensAvailable?: number) => {
const { writeContractAsync: mint, isPending, data: hash } = useWriteContract()
const { isLoading, isSuccess } = useWaitForTransactionReceipt({ hash })

const onMintNFT = useCallback(async () => {
if (!nftAddress) throw new Error('Unknown NFT address')
if (!tokensAvailable) throw new Error('No NFTs available to mint')
return await mint({
abi: abiContractsMap[nftAddress],
address: nftAddress || '0x0',
functionName: 'mint',
args: [],
})
}, [mint, nftAddress, tokensAvailable])

return useMemo(
() => ({ onMintNFT, isPending: isLoading || isPending, isSuccess }),
[isLoading, isPending, isSuccess, onMintNFT],
)
}

/**
* Hook returning all information about Early Adopters community
*/
export const useCommunity = (nftAddress?: Address): CommunityData => {
const { refetch, ...data } = useContractData(nftAddress)
const { onMintNFT, isPending, isSuccess } = useMintNFT(nftAddress, data.tokensAvailable)
const nftMeta = useNftMeta(data.nftUri)

useEffect(() => {
if (isSuccess) refetch()
}, [isSuccess, refetch])

return useMemo(
() =>
({
...data,
mint: {
onMintNFT,
isPending,
},
nftMeta,
}) satisfies CommunityData,
[data, isPending, nftMeta, onMintNFT],
)
}
22 changes: 0 additions & 22 deletions src/shared/hooks/useMintNFT.ts

This file was deleted.

Loading

0 comments on commit 99c945e

Please sign in to comment.