ใช้งาน Wagmi เพื่อเช็ค NFT holding

เรามาต่อการคราวที่เเล้วกัน วันนี้ก็จะมาเขียนวิธีการแสดงข้อมูล 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 ค้นหากันได้เเล้ว

Aa

© 2023, All Rights Reserved, VulturePrime co., ltd.