เรามาต่อการคราวที่เเล้วกัน วันนี้ก็จะมาเขียนวิธีการแสดงข้อมูล NFT ที่เราถืออยู่กัน
ก่อนอื่นสิ่งที่เราต้องมีคือ ABI ของ ERC721
https://docs.openzeppelin.com/contracts/2.x/api/token/erc721
[
{
"constant": true,
"inputs": [
{
"name": "interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "tokenId",
"type": "uint256"
}
],
"name": "getApproved",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "to",
"type": "address"
},
{
"name": "tokenId",
"type": "uint256"
}
],
"name": "approve",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "from",
"type": "address"
},
{
"name": "to",
"type": "address"
},
{
"name": "tokenId",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "tokenId",
"type": "uint256"
}
],
"name": "ownerOf",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "to",
"type": "address"
},
{
"name": "approved",
"type": "bool"
}
],
"name": "setApprovalForAll",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "owner",
"type": "address"
},
{
"name": "operator",
"type": "address"
}
],
"name": "isApprovedForAll",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "from",
"type": "address"
},
{
"indexed": true,
"name": "to",
"type": "address"
},
{
"indexed": true,
"name": "tokenId",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "owner",
"type": "address"
},
{
"indexed": true,
"name": "approved",
"type": "address"
},
{
"indexed": true,
"name": "tokenId",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "owner",
"type": "address"
},
{
"indexed": true,
"name": "operator",
"type": "address"
},
{
"indexed": false,
"name": "approved",
"type": "bool"
}
],
"name": "ApprovalForAll",
"type": "event"
},
{
"constant": true,
"inputs": [
{
"name": "owner",
"type": "address"
},
{
"name": "index",
"type": "uint256"
}
],
"name": "tokenOfOwnerByIndex",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "index",
"type": "uint256"
}
],
"name": "tokenByIndex",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "tokenId",
"type": "uint256"
}
],
"name": "tokenURI",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]
จากนั้นสิ่งที่เราจะเริ่มคือ สร้าง hooks ที่มีชื่อว่า useGetNFTCollection กัน
โดน hooks นี่จะทำการเรียก func 3 อย่าง คือ balanceOf, tokenOfOwnerByIndex และ tokenURI
โดยที่ func balanceOf จะดึง จำนวน nft ทั้งหมดที่เราถืออยู่ของ contractaddress นี้มา
จากนั้นทำการเรียก func tokenOfOwnerByIndex จะ ส่งค่า tokenID และสุดท้ายเราจะนำ tokenID ที่เราได้มาไป เรียก func tokenURI เพื่อนำค่า ipfs มานั้นเอง
โดย contractAddress ที่เราจะมาลองใช้ ก็คือ 0x7c230d7a7efbf17b2ebd2aac24a8fb5373e381b7
เป็น contractAddress ของ Optimistic Bunnies
https://qx.app/collection/opbunnies
เอาละเรามาดูตัวอย่าง เขียน Hooks ที่ใช้ Read Contract กัน
import { useEffect, useState } from 'react'
import Erc721ABI from '../assets/abi/erc721_full.json'
import { useChainId, useContractRead, useContractReads } from 'wagmi'
import axios from 'axios'
const useGetNFTCollection = (
contractAddress?: `0x${string}`,
walletAddress?: `0x${string}`
) => {
const [data, setData] = useState<any[]>()
const chainId = useChainId()
const { data: totalNFT } = useContractRead(
contractAddress
? {
abi: Erc721ABI,
address: contractAddress,
args: [walletAddress],
functionName: 'balanceOf',
watch: true,
enabled: !!contractAddress,
chainId,
select: (data) => data?.toString(),
}
: undefined
)
const newArray = totalNFT ? (Array(Number(totalNFT)).fill(0) as number[]) : []
const calls = newArray.map((_, index) => ({
abi: Erc721ABI,
functionName: 'tokenOfOwnerByIndex',
address: contractAddress,
args: [walletAddress, index],
chainId,
}))
const { data: tokenID } = useContractReads({
contracts: newArray.length > 0 ? calls : [],
watch: true,
enabled: newArray?.length > 0,
select: (value) => {
return value.map((value) => value?.result.toString())
},
})
const callsTokenURI = tokenID?.map((tokenId) => ({
abi: Erc721ABI,
functionName: 'tokenURI',
address: contractAddress,
args: [tokenId],
chainId,
}))
const { data: tokenURI } = useContractReads({
contracts: tokenID ? callsTokenURI : [],
watch: true,
enabled: !!tokenID,
select: (value) => {
return value.map((value) =>
value?.result.toString().replace('ipfs://', 'https://ipfs.io/ipfs/')
)
},
})
const fetchTokenURI = async (tokenURI: string[]) => {
const metaData = await Promise.all(
tokenURI.map(async (item) => {
try {
const { data } = await axios.get(item)
return data
} catch (error) {
return ''
}
})
)
return metaData
}
useEffect(() => {
if (tokenURI) {
fetchTokenURI(tokenURI)
.then((data) => {
setData(data)
})
.catch(console.log)
}
}, [tokenURI])
return {
data: data,
}
}
export default useGetNFTCollection
จากตัวอย่าง เราจะเรียก ใช้ useContractRead, useContractReads สองตัวนี้ ซึ่งสองตัวนี้มีการใช้ต่างกัน
useContractRead จะดึงค่า contract มาcall อันเดียว เเต่ useContractReads จะเป็นการทำ multicall ในกรณีนี้เราจะใช้ useContractReads เมื่อเราต้องเรียกค่ามาหลายค่ามาพร้อมกัน
ค่าที่เราได้จาก การเรียก function tokenURI จะได้ มาแบบนี้
ipfs/QmegSrDAZZRGKhEf4cwCje4ZRmXfxjTxwUMCwD2J5NqXE3/pixel3992.json
ซึ่งเราต้องมาทำการ fetch ค่าจาก ipfs มา ซึ่งเราสามารถเช็ค gateway ipfs ได้ว่า อันไหนสามารถใช้งานได้ ได้จาก https://ipfs.github.io/public-gateway-checker/
ซึ่งเราจะใช้ ipfs.io/ ดังนั้นจาก ตัวอย่าง hooks จะเห็นว่า จะทำการเปลี่ยน
ipfs/QmegSrDAZZRGKhEf4cwCje4ZRmXfxjTxwUMCwD2J5NqXE3/pixel3992.json
เป็น
https://ipfs.io/ipfs/QmegSrDAZZRGKhEf4cwCje4ZRmXfxjTxwUMCwD2J5NqXE3/pixel3992.json
เมื่อเราทำการ fetch ดึงค่าจาก IPFS
เราลองมา console.log ค่าจาก tokenURI เราจะได้หน้าตาประมาณนี่ ซึ่งสิ่งนี้เราจะเรียกว่า metadata
{
"dna": "67d93b8c03f14d3227aec326b75b83108841090a5d6dff1e79b5aadb341f4d4d",
"name": "pixel#3992",
"description": "These Optimistic Bunnies stepped through a pixelator and became Pixel Bunnies. Their journey on the blockchain has just begun. Follow these bunnies down the rabbit hole to find out what they are all about.",
"image": "ipfs://QmXGpq5ogcjnKji3ySGccXHhbCfTho2wgMxpcub1u9XiL6/pixel3992.png",
"imageHash": "267280d17360aaa7030e9957487791324dd8378962c6571c6b4a6571741bc32c",
"edition": 3992,
"date": 1642352450285,
"attributes": [
{
"trait_type": "Background",
"value": "Green"
},
{
"trait_type": "Body",
"value": "Grey"
},
{
"trait_type": "Personality",
"value": "Pessimistic"
},
{
"trait_type": "Clothes",
"value": "Orange Shirt"
},
{
"trait_type": "Mouth",
"value": "Mustache"
},
{
"trait_type": "Head",
"value": "Graduate's Hat"
}
],
"creator": "cryptofox_nft"
}
จากนั้น เราจะมาลองเรียกใช้กันเถอะมาดูว่าเป็นยังไง
import React from 'react'
import useGetNFTCollection from '../hooks/useGetNFTCollection'
export default function ViewNFT() {
const { data } = useGetNFTCollection(
'0x7c230d7a7efbf17b2ebd2aac24a8fb5373e381b7',
'0xc49e9d0ebA971990007B30D3052B243E45D3e7b0'
)
return (
<div>
{data?.map((item, index) => (
<div key={index}>
<img src={item?.image.replace('ipfs://', 'https://ipfs.io/ipfs/')} />
<p>{item?.name}</p>
<p>{item?.description}</p>
<p>{item?.creator}</p>
{item?.attributes.map((attribute) => (
<div key={attribute?.trait_type}>
{attribute?.trait_type} :{attribute?.value}
</div>
))}
</div>
))}
<>
)
}
เราจะได้หน้าตา UI เป็นแบบนี้ ซึ่งจะแสดงข้อมูลของ Optimistic Bunnies ที่ address นี้ได้ทำการถืออยู่นั้นเอง
ต่อมาเราก็จะมา ปรับปรุง hooks ของเรา โดยการ ใช้ zod มาช่วยในการ validate input กัน
import { useEffect, useState } from 'react'
import Erc721ABI from '../assets/abi/erc721_full.json'
import { useChainId, useContractRead, useContractReads } from 'wagmi'
import axios from 'axios'
import { z } from 'zod'
import { isAddress } from 'ethers'
export const zodAddress = z.custom<`0x${string}`>((value) => {
if (typeof value !== 'string') {
return false
}
if (!isAddress(value)) {
return false
}
return true
})
const validator = z.object({
walletAddress: zodAddress,
contractAddress: zodAddress,
})
const useGetNFTCollection = (input?: z.input<typeof validator>) => {
const result = validator.safeParse(input)
const [data, setData] = useState<any[]>()
const chainId = useChainId()
const { data: totalNFT } = useContractRead(
result.success
? {
abi: Erc721ABI,
address: result.data.contractAddress,
args: [result.data.walletAddress],
functionName: 'balanceOf',
watch: true,
enabled: !!result.data.contractAddress,
chainId,
select: (data) => data?.toString(),
}
: undefined
)
const newArray = totalNFT ? (Array(Number(totalNFT)).fill(0) as number[]) : []
const calls =
result.success && newArray.length > 0
? newArray.map((_, index) => ({
abi: Erc721ABI,
functionName: 'tokenOfOwnerByIndex',
address: result.data.contractAddress,
args: [result.data.walletAddress, index],
chainId,
}))
: []
const { data: tokenID } = useContractReads({
contracts: calls,
watch: true,
enabled: newArray.length > 0 && result.success,
select: (value) => {
return value.map((value) => value?.result.toString())
},
})
const callsTokenURI =
result.success && tokenID
? tokenID?.map((tokenId) => ({
abi: Erc721ABI,
functionName: 'tokenURI',
address: result.data.contractAddress,
args: [tokenId],
chainId,
}))
: []
const { data: tokenURI } = useContractReads({
contracts: callsTokenURI,
watch: true,
enabled: !!tokenID,
select: (value) => {
return value.map((value) =>
value?.result.toString().replace('ipfs://', 'https://ipfs.io/ipfs/')
)
},
})
const fetchTokenURI = async (tokenURI: string[]) => {
const metaData = await Promise.all(
tokenURI.map(async (item) => {
try {
const { data } = await axios.get(item)
return data
} catch (error) {
return ''
}
})
)
return metaData
}
useEffect(() => {
if (tokenURI) {
fetchTokenURI(tokenURI)
.then((data) => {
setData(data)
})
.catch(console.log)
}
}, [tokenURI])
return {
data: data,
}
}
export default useGetNFTCollection
เมื่อเราปรับปรุง hooks ของเรากันเสร็จเรียบร้อยเเล้ว
เราจะทำการเพิ่ม input ไปในหน้า UI ของเรากัน
import { useState } from 'react'
import useGetNFTCollection from '../hooks/useGetNFTCollection'
type Address = `0x${string}`
export default function ViewNFT() {
const [value, setValue] = useState<string>()
const { data } = useGetNFTCollection({
contractAddress: '0x7c230d7a7efbf17b2ebd2aac24a8fb5373e381b7',
walletAddress: value as Address,
})
return (
<div>
<input
placeholder='enter Address'
value={value}
onChange={(e) => setValue(e.target.value)}
/>
{data?.map((item, index) => (
<div key={index}>
<img src={item?.image.replace('ipfs://', 'https://ipfs.io/ipfs/')} />
<p>{item?.name}</p>
<p>{item?.description}</p>
<p>{item?.creator}</p>
{item?.attributes.map((attribute) => (
<div key={attribute?.trait_type}>
{attribute?.trait_type} :{attribute?.value}
</div>
))}
</div>
))}
</div>
)
}
จากนั้น เราก็มารถลองใส่ address แล้ว search ค้นหากันได้เเล้ว