Source

models/IContract.js

import Contract from '../utils/Contract';
import Web3Connection from '../Web3Connection';

/**
 * @typedef {Object} IContract~Options
 * @property {boolean} test
 * @property {boolean} localtest ganache local blockchain
 * @property {ABI} abi
 * @property {string} tokenAddress
 * @property {Web3Connection} [web3Connection=Web3Connection] created from params: 'test', 'localtest' and optional 'web3Connection' string and 'privateKey'
 * @property {string} [contractAddress]
 */

/**
 * @typedef {Object} IContract~TxOptions
 * @property {boolean} call
 * @property {*} value
 * @property {function()} callback
 * @property {string} from
 * @property {number} gasAmount
 * @property {number} gasFactor
 * @property {number} gasPrice
 */

/**
 * Contract Object Interface
 * @class IContract
 * @param {IContract~Options} options
 */
class IContract {
  constructor({
    abi,
    contractAddress = null, // If not deployed
    gasFactor = 1, // multiplier for gas estimations, may avoid out-of-gas
    tokenAddress,
    web3Connection = null, // Web3Connection if exists, otherwise create one from the rest of params
    ...params
  }) {
    if (!abi) {
      throw new Error('No ABI Interface provided');
    }

    this.web3Connection = web3Connection || new Web3Connection(params);

    this.params = {
      abi,
      contract: this.web3Connection.web3 ? new Contract(this.web3Connection, abi, contractAddress) : null,
      contractAddress,
      gasFactor,
      tokenAddress,
      web3Connection: this.web3Connection,
    };

    if (this.web3Connection.test) {
      this._loadDataFromWeb3Connection();
    }
  }

  /**
   * Initialize by awaiting {@link IContract.__assert}
   * @function
   * @return {Promise<void>}
   * @throws {Error} if no {@link IContract.getAddress}, Please add a Contract Address
   */
  __init__ = async () => {
    if (!this.getAddress()) {
      throw new Error('Please add a Contract Address');
    }

    await this.__assert();
  };

  /**
   * @function
   * @params {*} f
   * @params {IContract~TxOptions} options
   * @return {Promise<*>}
   */
  __sendTx = async (method, options) => {
    const { call, value } = options || {};

    if (!this.account && !call) {
      const {
        callback, from, gasAmount, gasFactor, gasPrice,
      } = options || {};
      const { params, web3Connection } = this;

      const txFrom = from || await web3Connection.getAddress();
      const txGasPrice = gasPrice || await web3Connection.web3.eth.getGasPrice();
      const txGasAmount = gasAmount || await method.estimateGas({
        from: txFrom,
        value,
      });
      const txGasFactor = gasFactor || params.gasFactor || 1;

      return new Promise((resolve, reject) => {
        method.send({
          from: txFrom,
          gas: Math.round(txGasAmount * txGasFactor),
          gasPrice: txGasPrice,
          value,
        })
          .on('confirmation', (confirmationNumber, receipt) => {
            if (callback) {
              callback(confirmationNumber);
            }

            if (confirmationNumber > 0) {
              resolve(receipt);
            }
          })
          .on('error', err => {
            reject(err);
          });
      });
    }

    if (this.account && !call) {
      const data = method.encodeABI();
      return this.params.contract
        .send(this.account.getAccount(), data, value)
        .catch(err => {
          throw err;
        });
    }

    if (this.account && call) {
      return method.call({ from: this.account.getAddress() }).catch(err => {
        throw err;
      });
    }

    return method.call().catch(err => {
      throw err;
    });
  };

  /**
   * Deploy current contract
   * @function
   * @param {*} args
   * @param {IContract~TxOptions} options
   * @return {Promise<*|undefined>}
   */
  __deploy = (args, options) => this.params.contract.deploy(
    this.params.contract.getABI(),
    this.params.contract.getJSON().bytecode,
    {
      account: this.account,
      args,
      ...options,
    },
  );

  /**
   * Asserts and uses {@link IContract.params.contract} with {@link IContract.params.abi}
   * @function
   * @void
   * @throws {Error} Contract is not deployed, first deploy it and provide a contract address
   */
  __assert = () => {
    if (!this.getAddress()) {
      throw new Error(
        'Contract is not deployed, first deploy it and provide a contract address',
      );
    }

    // Use ABI
    this.params.contract.use(this.params.abi, this.getAddress());
  };

  /**
   * Updates this contract's params.
   * @function
   * @param {Object} params
   * @void
   */
  updateParams = params => {
    if (!params || typeof params !== 'object' || Array.isArray(params)) {
      throw new Error('Supplied params should be a valid object');
    }

    this.params = {
      ...this.params,
      ...params,
    };
  };

  /**
   * Deploy {@link IContract.params.contract} and call {@link IContract.__assert}
   * @function
   * @param {IContract~TxOptions} options
   * @return {Promise<*|undefined>}
   */
  deploy = async options => {
    const params = [];
    const res = await this.__deploy(params, options);
    this.params.contractAddress = res.contractAddress;
    /* Call to Backend API */
    await this.__assert();
    return res;
  };

  /**
   * Get Web3 Contract to interact directly with the web3 library functions like events (https://web3js.readthedocs.io/en/v1.2.11/web3-eth-contract.html?highlight=events#contract-events)
   * @function
   */
  getContract() {
    return this.params.contract.getContract();
  }

  /**
   * Set new owner of {@link IContract.params.contract}
   * @param {Object} params
   * @param {Address} params.address
   * @return {Promise<*|undefined>}
   */
  setNewOwner({ address }, options) {
    return this.__sendTx(
      this.getContract().methods.transferOwnership(address),
      options,
    );
  }

  /**
   * Get Owner of {@link IContract.params.contract}
   * @returns {Promise<string>}
   */
  owner() {
    return this.getContract().methods.owner().call();
  }

  /**
   * Get the paused state of {@link IContract.params.contract}
   * @returns {Promise<boolean>}
   */
  isPaused() {
    return this.getContract().methods.paused().call();
  }

  /**
   * (Admins only) Pauses the Contract
   * @return {Promise<*|undefined>}
   */
  pauseContract(options) {
    return this.__sendTx(
      this.getContract().methods.pause(),
      options,
    );
  }

  /**
   * (Admins only) Unpause Contract
   * @return {Promise<*|undefined>}
   */
  unpauseContract(options) {
    return this.__sendTx(
      this.getContract().methods.unpause(),
      options,
    );
  }

  /**
   * Remove Tokens from other ERC20 Address (in case of accident)
   * @param {Object} params
   * @param {Address} params.tokenAddress
   * @param {Address} params.toAddress
   */
  removeOtherERC20Tokens({ tokenAddress, toAddress }, options) {
    return this.__sendTx(
      this.getContract()
        .methods.removeOtherERC20Tokens(tokenAddress, toAddress),
      options,
    );
  }

  /**
   * (Admins only) Safeguards all tokens from {@link IContract.params.contract}
   * @param {Object} params
   * @param {Address} params.toAddress
   * @return {Promise<*|undefined>}
   */
  safeGuardAllTokens({ toAddress }, options) {
    return this.__sendTx(
      this.getContract().methods.safeGuardAllTokens(toAddress),
      options,
    );
  }

  /**
   * Change token address of {@link IContract.params.contract}
   * @param {Object} params
   * @param {Address} params.newTokenAddress
   * @return {Promise<*|undefined>}
   */
  changeTokenAddress({ newTokenAddress }, options) {
    return this.__sendTx(
      this.getContract()
        .methods.changeTokenAddress(newTokenAddress),
      options,
    );
  }

  /**
   * Returns the contract address
   * @returns {string|null} Contract address
   */
  getAddress() {
    return this.params.contractAddress;
  }

  /**
   * Get the Ether balance for the current {@link IContract#getAddress} using `fromWei` util of {@link IContract#web3}
   * @returns {Promise<string>}
   */
  async getBalance() {
    const wei = await this.web3.eth.getBalance(this.getAddress());
    return this.web3.utils.fromWei(wei, 'ether');
  }

  /**
   * Verify that current user/sender is admin, throws an error otherwise
   * @async
   * @throws {Error} Only admin can perform this operation
   * @void
   */
  async onlyOwner() {
    /* Verify that sender is admin */
    const adminAddress = await this.owner();
    const userAddress = await this.getUserAddress();
    const isAdmin = adminAddress === userAddress;
    if (!isAdmin) {
      throw new Error('Only admin can perform this operation');
    }
  }

  /**
   * Verify that contract is not paused before sending a transaction, throws an error otherwise
   * @async
   * @throws {Error} Contract is paused
   * @void
   */
  async whenNotPaused() {
    /* Verify that contract is not paused */
    const paused = await this.isPaused();
    if (paused) {
      throw new Error('Contract is paused');
    }
  }

  /**
   * @function
   * @description Load data from Web3Connection object,
   * Called at start when testing or at login on MAINNET
   */
  _loadDataFromWeb3Connection() {
    this.web3 = this.web3Connection.getWeb3();
    this.account = this.web3Connection.account;

    // update some params properties with new values
    this.params = {
      ...this.params,
      web3: this.web3,
      contract: new Contract(
        this.web3Connection,
        this.params.abi,
        this.params.contractAddress,
      ),
    };
  }

  /** ***** */
  /** Web3Connection functions */
  /** ***** */

  /**
   * @function
   * @description Start the Web3Connection
   */
  start() {
    if (!this.web3Connection.web3) {
      this.web3Connection.start();
    }
    this._loadDataFromWeb3Connection();
  }

  /**
   * @function
   * @description Login with Metamask/Web3 Wallet - substitutes start()
   * @return {Promise<Boolean>} True is login was successful
   */
  async login() {
    const loginOk = await this.web3Connection.login();
    if (loginOk) {
      this._loadDataFromWeb3Connection();
    }
    return loginOk;
  }

  /**
   * @function
   * @description Get ETH Network
   * @return {Promise<string>} Network Name (Ex : Kovan)
   */
  getETHNetwork() {
    return this.web3Connection.getETHNetwork();
  }

  /**
   * Get current/selected user account in use if available,
   * or selected signer wallet/address otherwise.
   * @function
   * @return {Promise<string>} Account/Wallet in use
   */
  getUserCurrentAccount() {
    return this.web3Connection.getCurrentAccount();
  }

  /**
   * Get contract current user/sender address
   * @return {Promise<string>|string}
   */
  getUserAddress() {
    return this.web3Connection.getAddress();
  }

  /**
   * @function
   * @description Get user ETH Balance of Address connected via login()
   * @return {Promise<string>} User ETH Balance
   */
  getUserETHBalance() {
    return this.web3Connection.getETHBalance();
  }

  /**
   * @function
   * @description Get user wallets from current provider
   * @return {Promise<Array>}
   */
  getAccounts() {
    return this.web3Connection.getAccounts();
  }
}

export default IContract;