chain-client/src/chain/ERC721Reactor.ts
2023-04-23 11:47:49 +08:00

380 lines
11 KiB
TypeScript

import { timeoutFetch } from 'utils/net.util'
import { getFormattedIpfsUrl } from 'utils/wallet.util'
import Web3 from 'web3'
import { Contract } from 'web3-eth-contract'
import { Account } from 'web3-core'
export const ERC721 = 'ERC721'
export const ERC721_INTERFACE_ID = '0x80ac58cd'
export const ERC721_METADATA_INTERFACE_ID = '0x5b5e139f'
export const ERC721_ENUMERABLE_INTERFACE_ID = '0x780e9d63'
const abiNft = require('abis/BEBadge.json').abi
export class ERC721Reactor {
private web3: Web3
private contract: Contract
private account: Account
constructor({ web3, address }: { web3: Web3; address: string }) {
this.web3 = web3
this.account = this.web3.eth.accounts.wallet[0]
this.contract = new this.web3.eth.Contract(abiNft, address, { from: this.account.address })
}
/**
* Query if contract implements ERC721Metadata interface.
*
* @param address - ERC721 asset contract address.
* @returns Promise resolving to whether the contract implements ERC721Metadata interface.
*/
contractSupportsMetadataInterface = async (address?: string): Promise<boolean> => {
return this.contractSupportsInterface({ address, interfaceId: ERC721_METADATA_INTERFACE_ID })
}
/**
* Query if contract implements ERC721Enumerable interface.
*
* @param address - ERC721 asset contract address.
* @returns Promise resolving to whether the contract implements ERC721Enumerable interface.
*/
contractSupportsEnumerableInterface = async (address?: string): Promise<boolean> => {
return this.contractSupportsInterface({ address, interfaceId: ERC721_ENUMERABLE_INTERFACE_ID })
}
/**
* Query if contract implements ERC721 interface.
*
* @param address - ERC721 asset contract address.
* @returns Promise resolving to whether the contract implements ERC721 interface.
*/
contractSupportsBase721Interface = async (address?: string): Promise<boolean> => {
return this.contractSupportsInterface({ address, interfaceId: ERC721_INTERFACE_ID })
}
/**
* Enumerate assets assigned to an owner.
*
* @param address - ERC721 asset contract address.
* @param selectedAddress - Current account public address.
* @param index - A collectible counter less than `balanceOf(selectedAddress)`.
* @returns Promise resolving to token identifier for the 'index'th asset assigned to 'selectedAddress'.
*/
getCollectibleTokenId = async ({
address,
selectedAddress,
index,
}: {
address?: string
selectedAddress: string
index: number
}): Promise<string> => {
const contract = address
? new this.web3.eth.Contract(abiNft, address, { from: this.account.address })
: this.contract
return new Promise<string>((resolve, reject) => {
contract.methods.tokenOfOwnerByIndex(selectedAddress, index).call((error: Error, result: string) => {
/* istanbul ignore if */
if (error) {
reject(error)
return
}
resolve(result)
})
})
}
getBalance = async ({ address, selectedAddress }: { address?: string; selectedAddress: string }): Promise<number> => {
const contract = address
? new this.web3.eth.Contract(abiNft, address, { from: this.account.address })
: this.contract
return new Promise<number>((resolve, reject) => {
contract.methods.balanceOf(selectedAddress).call((error: Error, result: number) => {
/* istanbul ignore if */
if (error) {
reject(error)
return
}
resolve(result)
})
})
}
/**
* Query for tokenURI for a given asset.
*
* @param address - ERC721 asset contract address.
* @param tokenId - ERC721 asset identifier.
* @returns Promise resolving to the 'tokenURI'.
*/
getTokenURI = async ({ address, tokenId }: { address?: string; tokenId: string }): Promise<string> => {
const contract = address
? new this.web3.eth.Contract(abiNft, address, { from: this.account.address })
: this.contract
const supportsMetadata = await this.contractSupportsMetadataInterface(address)
if (!supportsMetadata) {
throw new Error('Contract does not support ERC721 metadata interface.')
}
return new Promise<string>((resolve, reject) => {
contract.methods.tokenURI(tokenId).call((error: Error, result: string) => {
/* istanbul ignore if */
if (error) {
reject(error)
return
}
resolve(result)
})
})
}
/**
* Query for name for a given asset.
*
* @param address - ERC721 asset contract address.
* @returns Promise resolving to the 'name'.
*/
getAssetName = async (address?: string): Promise<string> => {
const contract = address
? new this.web3.eth.Contract(abiNft, address, { from: this.account.address })
: this.contract
return new Promise<string>((resolve, reject) => {
contract.methods.name().call((error: Error, result: string) => {
/* istanbul ignore if */
if (error) {
reject(error)
return
}
resolve(result)
})
})
}
/**
* Query for symbol for a given asset.
*
* @param address - ERC721 asset contract address.
* @returns Promise resolving to the 'symbol'.
*/
getAssetSymbol = async (address?: string): Promise<string> => {
const contract = address
? new this.web3.eth.Contract(abiNft, address, { from: this.account.address })
: this.contract
return new Promise<string>((resolve, reject) => {
contract.methods.symbol().call((error: Error, result: string) => {
/* istanbul ignore if */
if (error) {
reject(error)
return
}
resolve(result)
})
})
}
/**
* Query for owner for a given ERC721 asset.
*
* @param address - ERC721 asset contract address.
* @param tokenId - ERC721 asset identifier.
* @returns Promise resolving to the owner address.
*/
async getOwnerOf({ address, tokenId }: { address?: string; tokenId: string }): Promise<string> {
const contract = address
? new this.web3.eth.Contract(abiNft, address, { from: this.account.address })
: this.contract
return new Promise<string>((resolve, reject) => {
contract.methods.ownerOf(tokenId).call((error: Error, result: string) => {
/* istanbul ignore if */
if (error) {
reject(error)
return
}
resolve(result)
})
})
}
/**
* Query if a contract implements an interface.
*
* @param address - Asset contract address.
* @param interfaceId - Interface identifier.
* @returns Promise resolving to whether the contract implements `interfaceID`.
*/
private contractSupportsInterface = async ({
address,
interfaceId,
}: {
address?: string
interfaceId: string
}): Promise<boolean> => {
const contract = address
? new this.web3.eth.Contract(abiNft, address, { from: this.account.address })
: this.contract
return new Promise<boolean>((resolve, reject) => {
contract.methods.supportsInterface(interfaceId).call((error: Error, result: boolean) => {
/* istanbul ignore if */
if (error) {
reject(error)
return
}
resolve(result)
})
})
}
/**
* Query if a contract implements an interface.
*
* @param address - Asset contract address.
* @param ipfsGateway - The user's preferred IPFS gateway.
* @param tokenId - tokenId of a given token in the contract.
* @returns Promise resolving an object containing the standard, tokenURI, symbol and name of the given contract/tokenId pair.
*/
getDetails = async ({
address,
ipfsGateway,
tokenId,
}: {
address?: string
ipfsGateway: string
tokenId?: string
}): Promise<{
standard: string
tokenURI: string | undefined
symbol: string | undefined
name: string | undefined
image: string | undefined
}> => {
const isERC721 = await this.contractSupportsBase721Interface(address)
if (!isERC721) {
throw new Error("This isn't a valid ERC721 contract")
}
let tokenURI, image, symbol, name
// TODO upgrade to use Promise.allSettled for name/symbol when we can refactor to use es2020 in tsconfig
try {
symbol = await this.getAssetSymbol(address)
} catch {
// ignore
}
try {
name = await this.getAssetName(address)
} catch {
// ignore
}
if (tokenId) {
try {
tokenURI = await this.getTokenURI({ address, tokenId })
if (tokenURI.startsWith('ipfs://')) {
tokenURI = getFormattedIpfsUrl(ipfsGateway, tokenURI, true)
}
const response = await timeoutFetch(tokenURI)
const object = await response.json()
image = object ? object.image : ''
if (image.startsWith('ipfs://')) {
image = getFormattedIpfsUrl(ipfsGateway, image, true)
}
} catch {
// ignore
}
}
return {
standard: ERC721,
tokenURI,
symbol,
name,
image,
}
}
async transfer({
address,
from,
to,
tokenId,
account,
gas,
encodeABI = false,
}: {
address?: string
from: string
to: string
tokenId: string
account?: string
gas?: number
encodeABI?: boolean
}) {
const contract = address
? new this.web3.eth.Contract(abiNft, address, { from: account || this.account.address })
: this.contract
if (encodeABI) {
return contract.methods.safeTransferFrom(from, to, tokenId).encodeABI()
}
return contract.methods.safeTransferFrom(from, to, tokenId).send({
from,
gas: gas || 1000000,
})
}
async mint({
address,
to,
tokenId,
encodeABI = false,
}: {
address?: string
to: string
tokenId: string
encodeABI?: boolean
}) {
const contract = address
? new this.web3.eth.Contract(abiNft, address, { from: this.account.address })
: this.contract
if (encodeABI) {
return contract.methods.mint(to, tokenId).encodeABI()
}
let gas = await contract.methods.mint(to, tokenId).estimateGas({ gas: 1000000 })
return contract.methods.mint(to, tokenId).send({ gas: gas | 0 })
}
async batchMint({
account,
address,
to,
count,
encodeABI = false,
}: {
account?: string
address?: string
to: string
count: number
encodeABI?: boolean
}) {
const contract = address
? new this.web3.eth.Contract(abiNft, address, { from: account || this.account.address })
: this.contract
const countStr = count + ''
if (encodeABI) {
return contract.methods.batchMint(to, countStr).encodeABI()
}
let gas = await contract.methods.batchMint(to, countStr).estimateGas({ from: account || this.account.address })
return contract.methods.batchMint(to, countStr).send({ gas: gas | 0 })
}
async getPastEvents({ address, fromBlock }: { address?: string; fromBlock: number }) {
const contract = address
? new this.web3.eth.Contract(abiNft, address, { from: this.account.address })
: this.contract
return contract.getPastEvents('BatchMint', {
fromBlock,
toBlock: fromBlock + 50000,
})
}
}