import BigNumber from "bignumber.js"; import invariant from "tiny-invariant"; import Web3 from "web3"; import { TransactionReceipt } from "web3-eth"; import { ERC20 } from "../../web3-types/ERC20"; import { Address, ChainId, CHAINS_TO_RPC, TransactionOptions, } from "../constants"; import { ERC20_INTERFACE, getErc20 } from "../constants/contracts"; import { validateAndParseAddress } from "../utils"; import { Wallet } from "../wallet/Wallet"; export interface SerializedToken { address: string; decimals: number; name?: string; symbol?: string; chainId: number; imageUri?: string; } /** * Represents an ERC20 token with a unique address and some metadata. */ export class Token { public decimals: number; public symbol?: string; public name?: string; public imageUri?: string; private contract?: ERC20; private attemptedLoadContract?: boolean; public readonly chainId: ChainId; public readonly address: string; /** * Constructs an instance of the base class `Currency`. * @param decimals decimals of the currency * @param symbol symbol of the currency * @param name of the currency */ public constructor( chainId: ChainId, address: string, decimals?: number, symbol?: string, name?: string, imageUri?: string ) { this.decimals = decimals ?? 18; this.symbol = symbol; this.name = name; this.imageUri = imageUri; this.chainId = chainId; this.address = validateAndParseAddress(address); } public async loadDetails() { if (!this.contract) { await this._loadContract(); } [this.name, this.symbol, this.decimals] = await Promise.all([ this.contract?.methods.name().call(), this.contract?.methods.symbol().call(), parseInt((await this.contract?.methods.decimals().call()) ?? "18"), ]); } public static isSerializedToken = ( obj?: Record ): obj is SerializedToken => obj?._type === "Erc20Token"; public static parseJson = ({ chainId, address, name, symbol, imageUri, decimals, }: SerializedToken) => new Token(chainId, address, decimals, symbol, name, imageUri); toJSON() { return { _type: "Erc20Token", address: this.address, chainId: this.chainId, decimals: this.decimals, symbol: this.symbol, name: this.name, imageUri: this.imageUri, }; } /** * Loads the ERC20 contract */ private async _loadContract() { invariant(!this.attemptedLoadContract, "Contract has already been loaded"); const web3 = new Web3( new Web3.providers.HttpProvider(CHAINS_TO_RPC[this.chainId].http) ); this.contract = getErc20(web3, this.address); } /** * * @param amount amount, in wei, of the token to send * @param from the wallet where the tokens will be sent from * @param to the address of the recipient of the tokens * @returns transaction receipt of the transfer */ public async send( amount: BigNumber, from: Wallet, to: Address, opt?: TransactionOptions ): Promise { return await from.signAndSendContractTransactions( [ { contractInterface: ERC20_INTERFACE, method: "transfer", params: [to, amount.toFixed(0)], target: this.address, }, ], opt ); } /** * Returns true if the two tokens are equivalent, i.e. have the same chainId and address. * @param other other token to compare */ public equals(other: Token): boolean { // short circuit on reference equality if (this === other) { return true; } return this.chainId === other.chainId && this.address === other.address; } /** * Returns true if the address of this token sorts before the address of the other token * @param other other token to compare * @throws if the tokens have the same address * @throws if the tokens are on different chains */ public sortsBefore(other: Token): boolean { invariant(this.chainId === other.chainId, "CHAIN_IDS"); invariant(this.address !== other.address, "ADDRESSES"); return this.address.toLowerCase() < other.address.toLowerCase(); } } /** * Compares two currencies for equality */ export function currencyEquals(currencyA: Token, currencyB: Token): boolean { return currencyA.equals(currencyB); }