export interface CoinOptions { decimals: number symbol: string weiSymbol: string } export class Coin { public readonly decimals: number public readonly symbol: string public readonly weiSymbol: string public readonly oneCoinInWei: bigint // half of decimals wei // for 18 decimals will be gigawei or gwei public readonly hwei: bigint constructor(options: CoinOptions) { this.symbol = options.symbol this.weiSymbol = options.weiSymbol this.decimals = options.decimals const decimalsBigInt = BigInt(options.decimals) this.oneCoinInWei = 10n ** decimalsBigInt this.hwei = 10n ** (decimalsBigInt / 2n) } static createUnknown18DecimalsCoin() { return new Coin({ symbol: 'Unit', weiSymbol: 'wei', decimals: 18 }) } getOptions() { return { symbol: this.symbol, decimals: this.decimals, weiSymbol: this.weiSymbol, } } getFractionalPart(wei: bigint | string): bigint { const str = wei.toString() if (str.length <= this.decimals) return this.bigintFromWei(wei) return BigInt(str.slice(str.length - this.decimals)) } getIntegerPart(wei: bigint | string): bigint { const str = wei.toString() if (str.length <= this.decimals) return 0n return BigInt(str.slice(0, str.length - this.decimals)) } bigintFromWei(wei: string | bigint): bigint { if (typeof wei === "bigint") { return wei } else if (typeof wei === 'string') { if (!wei.match(/^\d+$/)) { throw new Error('wei should be string of digits') } return BigInt(wei) } else { throw new Error('wei should be bigint or string') } } format(wei: string | bigint, decimalPoints: number = 6): string { return this.formatWithoutCurrency(wei, decimalPoints) + ` ${this.symbol}` } formatFullLength(wei: string | bigint): string { return this.format(wei, this.decimals) } // always rounds down insignificant part // because it's better to show '5 UNQ' on balance 5.09 UNQ instead of '5.1 UNQ' formatWithoutCurrency(wei: string | bigint, decimalPoints: number = 6): string { if (typeof decimalPoints !== 'number') { throw new Error(`decimalPoints should be number`) } if (!(Math.round(decimalPoints) === decimalPoints)) { throw new Error('decimalPoints should be integer number') } if (decimalPoints < 0) { throw new Error(`decimal points should be in range GTE 0`) } if (decimalPoints > this.decimals) { decimalPoints = this.decimals } const intPart = this.getIntegerPart(wei) const fracPart = this.getFractionalPart(wei) const nonSignificantPartLength = this.decimals - decimalPoints const fracPartIsTooSmall = fracPart < (10n ** BigInt(nonSignificantPartLength)) if (!decimalPoints || fracPartIsTooSmall) { return intPart.toString() } const fracPartStr = fracPart.toString() .padStart(18, '0') .slice(0, decimalPoints) .replace(/\.?0*$/, '') return `${intPart}.${fracPartStr}` } /** * @description Dangerously because for the lossless conversion, strings should be used, not numbers. * Please use the `coinsToWei` method instead. */ dangerouslyCoinsToWei(coins: number): bigint { if (coins < 0) { throw new Error(`coins to wei: coins should be >= 0, received ${coins}`) } const intPart = Math.floor(coins) if (intPart !== 0) { return this.coinsToWei(coins.toString()) } else { return this.coinsToWei((coins + 1).toString()) - this.oneCoinInWei } } coinsToWei(coins: string): bigint { const parts = coins.trim().match(/^(\d*(\.\d*)?)/) if (!parts || !parts[1]) { throw new Error(`coinsToWei: could not parse input: ${coins}`) } const [intPart, fracPart] = parts[1].split('.') const finalFracPart = fracPart ? BigInt(fracPart.padEnd(18, '0')) : 0n return BigInt(intPart) * this.oneCoinInWei + finalFracPart } }