import Web3 from "web3"; import { abiERC721 } from "../abis/abiERC721"; import { universalChainCb } from "../util/chain.util"; import { timeoutFetch } from "../util/net.util"; import { getFormattedIpfsUrl } from "../util/wallet.util"; export const ERC721 = "ERC721"; export const ERC721_INTERFACE_ID = "0x80ac58cd"; export const ERC721_METADATA_INTERFACE_ID = "0x5b5e139f"; export const ERC721_ENUMERABLE_INTERFACE_ID = "0x780e9d63"; export class ERC721Standard { private web3: Web3; constructor(web3: Web3) { this.web3 = web3; } /** * 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 => { return this.contractSupportsInterface( address, 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 => { return this.contractSupportsInterface( address, 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 => { return this.contractSupportsInterface(address, 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: string, selectedAddress: string, index: number ): Promise => { const contract = new this.web3.eth.Contract(abiERC721, address); return new Promise((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: string, selectedAddress: string ): Promise => { const contract = new this.web3.eth.Contract(abiERC721, address); return new Promise((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: string, tokenId: string): Promise => { const contract = new this.web3.eth.Contract(abiERC721, address); const supportsMetadata = await this.contractSupportsMetadataInterface( address ); if (!supportsMetadata) { throw new Error("Contract does not support ERC721 metadata interface."); } return new Promise((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 => { const contract = new this.web3.eth.Contract(abiERC721, address); return new Promise((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 => { const contract = new this.web3.eth.Contract(abiERC721, address); return new Promise((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: string, tokenId: string): Promise { const contract = new this.web3.eth.Contract(abiERC721, address); return new Promise((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: string, interfaceId: string ): Promise => { const contract = new this.web3.eth.Contract(abiERC721, address); return new Promise((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: 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, gas, estimate, }: { address: string; from: string; to: string; tokenId: string; gas?: number; estimate: number; }) { const contract = new this.web3.eth.Contract(abiERC721, address); if (!gas) { gas = await contract.methods .safeTransferFrom(from, to, tokenId) .estimateGas({ gas: 1000000 }); } gas = (gas * 1.1) | 1; if (estimate) { return jc.wallet.generateGasShow(gas); } const logData = { gas, title: "transfer", details: [ { address, from, to, id: tokenId, }, ], }; return universalChainCb( logData, contract.methods.safeTransferFrom(from, to, tokenId).send({ from, gas, }) ); } }