336 lines
9.1 KiB
TypeScript
336 lines
9.1 KiB
TypeScript
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<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,
|
|
};
|
|
};
|
|
|
|
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,
|
|
})
|
|
);
|
|
}
|
|
}
|