import Web3 from "web3"; import { abiERC1155 } from "../abis/abiERC1155"; import { timeoutFetch } from "../util/net.util"; import { getFormattedIpfsUrl } from "../util/wallet.util"; export const ERC1155 = "ERC1155"; export const ERC1155_INTERFACE_ID = "0xd9b67a26"; export const ERC1155_METADATA_URI_INTERFACE_ID = "0x0e89341c"; export const ERC1155_TOKEN_RECEIVER_INTERFACE_ID = "0x4e2312e0"; export class ERC1155Standard { private web3: Web3; constructor(web3: Web3) { this.web3 = web3; } /** * Query if contract implements ERC1155 URI Metadata interface. * * @param address - ERC1155 asset contract address. * @returns Promise resolving to whether the contract implements ERC1155 URI Metadata interface. */ contractSupportsURIMetadataInterface = async ( address: string ): Promise => { return this.contractSupportsInterface( address, ERC1155_METADATA_URI_INTERFACE_ID ); }; /** * Query if contract implements ERC1155 Token Receiver interface. * * @param address - ERC1155 asset contract address. * @returns Promise resolving to whether the contract implements ERC1155 Token Receiver interface. */ contractSupportsTokenReceiverInterface = async ( address: string ): Promise => { return this.contractSupportsInterface( address, ERC1155_TOKEN_RECEIVER_INTERFACE_ID ); }; /** * Query if contract implements ERC1155 interface. * * @param address - ERC1155 asset contract address. * @returns Promise resolving to whether the contract implements the base ERC1155 interface. */ contractSupportsBase1155Interface = async ( address: string ): Promise => { return this.contractSupportsInterface(address, ERC1155_INTERFACE_ID); }; /** * Query for tokenURI for a given asset. * * @param address - ERC1155 asset contract address. * @param tokenId - ERC1155 asset identifier. * @returns Promise resolving to the 'tokenURI'. */ getTokenURI = async (address: string, tokenId: string): Promise => { const contract = new this.web3.eth.Contract(abiERC1155, address); 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 balance of a given ERC1155 token. * * @param contractAddress - ERC1155 asset contract address. * @param address - Wallet public address. * @param tokenId - ERC1155 asset identifier. * @returns Promise resolving to the 'balanceOf'. */ getBalanceOf = async ( contractAddress: string, address: string, tokenId: string ): Promise => { const contract = new this.web3.eth.Contract(abiERC1155, contractAddress); return new Promise((resolve, reject) => { contract.methods.balanceOf( address, tokenId, (error: Error, result: number) => { /* istanbul ignore if */ if (error) { reject(error); return; } resolve(result); } ); }); }; /** * Transfer single ERC1155 token. * When minting/creating tokens, the from arg MUST be set to 0x0 (i.e. zero address). * When burning/destroying tokens, the to arg MUST be set to 0x0 (i.e. zero address). * * @param operator - ERC1155 token address. * @param from - ERC1155 token holder. * @param to - ERC1155 token recipient. * @param id - ERC1155 token id. * @param value - Number of tokens to be sent. * @returns Promise resolving to the 'transferSingle'. */ transferSingle = async ( operator: string, from: string, to: string, id: string, value: string ): Promise => { const contract = new this.web3.eth.Contract(abiERC1155, operator); return new Promise((resolve, reject) => { contract.methods.transferSingle( operator, from, to, id, value, (error: Error, result: void) => { /* istanbul ignore if */ if (error) { reject(error); return; } resolve(result); } ); }); }; /** * Query if a contract implements an interface. * * @param address - ERC1155 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(abiERC1155, address); return new Promise((resolve, reject) => { contract.methods.supportsInterface( interfaceId, (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; image: string | undefined; }> => { const isERC1155 = await this.contractSupportsBase1155Interface(address); if (!isERC1155) { throw new Error("This isn't a valid ERC1155 contract"); } let tokenURI, image; if (tokenId) { tokenURI = await this.getTokenURI(address, tokenId); if (tokenURI.startsWith("ipfs://")) { tokenURI = getFormattedIpfsUrl(ipfsGateway, tokenURI, true); } try { const response = await timeoutFetch(tokenURI); const object = await response.json(); image = object?.image; if (image?.startsWith("ipfs://")) { image = getFormattedIpfsUrl(ipfsGateway, image, true); } } catch { // ignore } } // TODO consider querying to the metadata to get name. return { standard: ERC1155, tokenURI, image, }; }; async transferBatch({ address, from, to, tokenIds, amounts, gas, estimate, }: { address: string; from: string; to: string; tokenIds: string[]; amounts: string[]; gas?: number; estimate: number; }) { const contract = new this.web3.eth.Contract(abiERC1155, address); if (!gas) { gas = await contract.methods .safeBatchTransferFrom(from, to, tokenIds, amounts, []) .estimateGas({ gas: 1000000 }); } gas = (gas * 1.1) | 1; if (estimate) { return jc.wallet.generateGasShow(gas); } return contract.methods .safeBatchTransferFrom(from, to, tokenIds, amounts, []) .send({ from, gas, }); } }