diff --git a/src/index.ts b/src/index.ts index 8d68ab4..6eb995b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,6 +31,7 @@ import { } from "./manage/WalletManage"; import { signLogin } from "./util/sign.util"; import { JazzIcon } from "./comp/JazzIcon"; +import { ERC1155Standard } from "./standards/ERC1155Standard"; var global = (typeof globalThis !== "undefined" && globalThis) || @@ -49,9 +50,6 @@ declare global { } } -// window.Buffer = require('buffer').Buffer; -// window.process = require('process'); -// window.Stream = require('stream-browserify'); export interface IChainData { name: string; @@ -71,6 +69,7 @@ export default class JCWallet { private _currentChain: IChainData; public erc20Standard: ERC20Standard; public erc721Standard: ERC721Standard; + public erc1155Standard: ERC1155Standard; public mainHandlers = createWalletEvents(); public data: IAccount[] = []; public iconType = "jazz"; @@ -91,6 +90,7 @@ export default class JCWallet { console.log(`init web3 cost: ${(Date.now() - start) / 1000}`); this.erc20Standard = new ERC20Standard(this.web3); this.erc721Standard = new ERC721Standard(this.web3); + this.erc1155Standard = new ERC1155Standard(this.web3); start = Date.now(); this.wallet = this.web3.eth.accounts.wallet.load( this.password, @@ -101,10 +101,6 @@ export default class JCWallet { this.data = loadData(); console.log(`init wallet ext data cost: ${(Date.now() - start) / 1000}`); window.jc = { wallet: this }; - // window.cc = window.cc || {}; - // window.cc.walletCallback = (dataStr: string) => { - // console.log('[Native CB]::' + dataStr) - // } this.init({ chains: [322, 97], password }); } @@ -137,7 +133,7 @@ export default class JCWallet { this.password = password; this.wallet.clear(); restoreWalletByMnemonic(mnemonic, this.password); - this.createAccount(); + return this.createAccount(); } get currentChain() { diff --git a/src/standards/ERC1155Standard.ts b/src/standards/ERC1155Standard.ts new file mode 100644 index 0000000..42f437c --- /dev/null +++ b/src/standards/ERC1155Standard.ts @@ -0,0 +1,224 @@ + +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, address); + 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, + }; + }; +}