util/index.js

import { utils, Interface, Wallet } from 'ethers';
import abis from '../../lib/abis/index';
import Depot from '../contracts/Depot';
import Synth from '../contracts/Synth';
import Synthetix from '../contracts/Synthetix';
const GWEI = 1000000000;
const DEFAULT_GAS_LIMIT = 200000;

class Util {
  /**
   * set of helper functions
   * @param contractSettings
   */
  constructor(contractSettings) {
    this.contractSettings = contractSettings;
    this.depot = new Depot(contractSettings);
    this.synth = new Synth(contractSettings);
    this.synthetix = new Synthetix(contractSettings);
    this.depotInterface = new Interface(abis.Depot);
    this.synthInterface = new Interface(abis.Synth);

    this.signAndSendTransaction = this.signAndSendTransaction.bind(this);
    this.getEventLogs = this.getEventLogs.bind(this);
    this.getLatestConversions = this.getLatestConversions.bind(this);
    this.getGasAndSpeedInfo = this.getGasAndSpeedInfo.bind(this);
    this.waitForTransaction = this.waitForTransaction.bind(this);
    this.getGasEstimate = this.getGasEstimate.bind(this);
  }

  /**
   * converts number (as a string) to a BigNumber
   * @param value {String}
   * @returns {BigNumber}
   */
  parseEther(value) {
    return utils.parseEther(value);
  }

  /**
   * converts BigNumber to number (as a string)
   * @param value {BigNunber}
   * @returns {String}
   */
  formatEther(value) {
    return utils.formatEther(value);
  }

  /**
   * converts string to bytes
   * @param stringValue
   * @returns {Utf8Bytes}
   */
  toUtf8Bytes(stringValue) {
    return utils.toUtf8Bytes(stringValue);
  }

  /**
   * Manually sign any transaction with custom signer
   * @param transaction
   * @param fromAddress
   * @returns {Promise<void>}
   */
  async signAndSendTransaction({ transaction, fromAddress }) {
    transaction.nonce = await this.contractSettings.provider.getTransactionCount(fromAddress);
    transaction.gasLimit = 200000;
    transaction.chainId = this.contractSettings.networkId;

    const signedTx = await this.contractSettings.signer.sign(transaction);
    const signedSerialziedTx = '0x' + signedTx.serialize().toString('hex');
    return await this.contractSettings.provider.sendTransaction(signedSerialziedTx);
  }

  /**
   * Returns event logs for a specific contract event and fetches block timestamp for each transaction
   * @param contractAddress {String} in format "0x1234567890abcdef"
   * @param event - {Object<ethers.Interface>}ethers.js event interface
   * @param fromBlock
   * @returns {Promise<*>}
   */
  async getEventLogs(contractAddress, event, fromBlock) {
    const blockTimestampMap = {};
    try {
      const logs = await this.contractSettings.provider.getLogs({
        fromBlock: fromBlock,
        address: contractAddress,
        topics: event.topics,
      });
      const events = logs.map(log => ({
        ...log,
        parsedData: event.parse(log.topics, log.data),
      }));
      const blocks = await Promise.all(
        events.map(event => this.contractSettings.provider.getBlock(event.blockNumber))
      );
      blocks.forEach(block => {
        blockTimestampMap[block.number] = new Date(block.timestamp * 1000);
      });
      events.forEach(event => {
        event.timestamp = blockTimestampMap[event.blockNumber];
      });
      return events;
    } catch (err) {
      console.log(err);
    }
  }

  async getLatestConversions() {
    const latestBlockNumber = await this.contractSettings.provider.getBlockNumber();
    const contractAddr = this.contractSettings.addressList.Depot;

    const ExchangeEvent = this.depotInterface.events.Exchange;
    let events = await this.getEventLogs(contractAddr, ExchangeEvent, latestBlockNumber - 10000);
    if (events.length < 5) {
      events = await this.getEventLogs(contractAddr, ExchangeEvent, latestBlockNumber - 100000);
    }
    if (!events || !events.length) {
      return [];
    }
    return events.reverse().slice(0, 20);
  }

  formatBigNumber(amount, decimals) {
    if (!amount) return '-';

    const amountString = utils.formatEther(amount, { commify: true });

    if (typeof decimals === 'undefined') {
      return amountString;
    } else {
      const [first, remainder] = amountString.split('.');
      let joined = `${first}.${remainder.substring(0, decimals)}`;

      if (joined.endsWith('.')) return joined.substring(0, joined.length - 1);

      return joined;
    }
  }

  formatNumber(amount, decimal) {
    if (amount === '' || amount === null) {
      return '';
    }
    return parseFloat(amount).toFixed(decimal);
  }

  formatNumberMaxDecimal(amount, decimal) {
    return Math.round(amount * Math.pow(10, decimal)) / Math.pow(10, decimal);
  }

  async getTransactionInformation(transactionHash) {
    if (typeof transactionHash !== 'string') {
      throw new Error('transactionHash must be a string');
    }
    return await this.contractSettings.provider.getTransaction(transactionHash);
  }

  /**
   * Estimates gas for a transaction
   * @param toAddress - where to send transaction
   * @param ethValue - optional - if function requires ETH to be sent
   * @param data - optional if function requires data to be sent
   * example  (new Interface(CONTRACT_ABIS.Depot).functions.exchangeEtherForSynths()).data
   * example2 synthInterface.functions.approve(MAINNET_ADDRESSES.Depot, utils.parseEther("2")).data;
   * @returns {Promise<String>}
   */
  async getGasEstimate(toAddress, ethValue, data) {
    // to get the gas estimate, the contract needs to be
    // initialized with a wallet or a customSigner
    const privateKey = '0x0123456789012345678901234567890123456789012345678901234567890123';
    const wallet = new Wallet(privateKey, this.provider);
    const tx = { to: toAddress };
    if (ethValue) {
      tx.value = ethValue;
    }
    if (data) {
      tx.data = data;
    }
    const estimate = await wallet.estimateGas(tx);
    return estimate.toString();
  }

  /**
   * Waits for ethereum transaction to succeed or fail. Checks the status every second.
   * @param transactionHash
   * @returns {Promise<*>}
   */
  async waitForTransaction(transactionHash) {
    return new Promise(resolve => {
      const check = async () => {
        const transactionInformation = await this.getTransactionInformation(transactionHash);
        if (transactionInformation && transactionInformation.blockHash) {
          resolve(true);
        } else {
          setTimeout(check, 1000);
        }
      };
      check();
    });
  }

  async getEtherPrice() {
    return await this.depot.usdToEthPrice();
  }

  async getSynthetixPrice() {
    return await this.depot.usdToSnxPrice();
  }

  /**
   * Returns the object with estimates for slow, average and fast gas prices and approximate waiting times
   * @returns {Promise<{gasFastGwei: number, gasAverageGwei: number, gasSlowGwei: number, timeFastMinutes: *, timeAverageMinutes: *, timeSlowMinutes: *}>}
   */
  async getGasAndSpeedInfo() {
    // ethToSynth uses approx 80,000, synthToHav 40,000 but approve 70,000; 100,000 is safe average
    const convetorTxGasPrice = DEFAULT_GAS_LIMIT;
    let [egsData, ethPrice] = await Promise.all([
      fetch('https://ethgasstation.info/json/ethgasAPI.json'),
      this.getEtherPrice(),
    ]);
    egsData = await egsData.json();
    ethPrice = Number(utils.formatEther(ethPrice));
    const data = {
      gasFastGwei: egsData.fast / 10,
      gasAverageGwei: egsData.average / 10,
      gasSlowGwei: egsData.safeLow / 10,
      timeFastMinutes: egsData.fastWait,
      timeAverageMinutes: egsData.avgWait,
      timeSlowMinutes: egsData.safeLowWait,
    };
    data.priceFastUsd =
      Math.round(((data.gasFastGwei * ethPrice * convetorTxGasPrice) / GWEI) * 1000) / 1000;
    data.priceAverageUsd =
      Math.round(((data.gasAverageGwei * ethPrice * convetorTxGasPrice) / GWEI) * 1000) / 1000;
    data.priceSlowUsd =
      Math.round(((data.gasSlowGwei * ethPrice * convetorTxGasPrice) / GWEI) * 1000) / 1000;
    return data;
  }
}

export default Util;