2022-06-19 22:32:52 +08:00

287 lines
8.2 KiB
TypeScript

import { abiERC721 } from '../abis/abiERC721';
import Web3 = require('../lib/web3.min');
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<boolean> => {
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<boolean> => {
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<boolean> => {
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<string> => {
const contract = new this.web3.eth.Contract(abiERC721, address);
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: string,
selectedAddress: string
): Promise<number> => {
const contract = new this.web3.eth.Contract(abiERC721, address);
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: string, tokenId: string): Promise<string> => {
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<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 = new this.web3.eth.Contract(abiERC721, address);
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 = new this.web3.eth.Contract(abiERC721, address);
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: string, tokenId: string): Promise<string> {
const contract = new this.web3.eth.Contract(abiERC721, address);
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: string,
interfaceId: string,
): Promise<boolean> => {
const contract = new this.web3.eth.Contract(abiERC721, address);
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: 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,
};
};
}