Source

models/ERC20/ERC20TokenLock.js

import moment from 'moment';
import { tokenlock } from '../../interfaces';
import ERC20Contract from './ERC20Contract';
import IContract from '../IContract';
import Numbers from '../../utils/Numbers';

const assert = require('assert');

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

/**
 * ERC20 Token Lock Contract Object
 * @class ERC20TokenLock
 * @param {ERC20TokenLock~Options} options
 */
class ERC20TokenLock extends IContract {
  constructor(params = {}) {
    super({ ...params, abi: tokenlock });
    if (params.tokenAddress) {
      this.params.ERC20Contract = new ERC20Contract({
        web3Connection: this.web3Connection,
        contractAddress: params.tokenAddress,
      });
    }
  }

  /**
   * Get ERC20 Address of the Token Contract managed
   * @returns {Promise<Address>}
   */
  erc20() {
    return this.getContract().methods.erc20().call();
  }

  /**
   * Get Token Amount of ERC20 Address
   * @function
   * @param {Object} params
   * @param {Address} params.address
   * @returns {Promise<number>} Token Amount
   */
  getTokenAmount = ({ address }) => this.getERC20Contract().getTokenAmount(address);

  /**
   * Get All Tokens staked/locked at that specific moment
   * @returns {Integer} Token Amount
   */
  async totalAmountStaked() {
    const res = await this.getContract()
      .methods.totalAmountStaked()
      .call();
    return Numbers.fromDecimals(res, this.getERC20Contract().getDecimals());
  }

  /**
   * Get minimum amount of tokens to lock per user
   * @returns {Promise<number>} Minimum Amount
   */
  async minAmountToLock() {
    const res = await this.getContract()
      .methods.minAmountToLock()
      .call();
    return Numbers.fromDecimals(res, this.getERC20Contract().getDecimals());
  }

  /**
   * Get maximum amount of tokens to lock per user
   * @returns {Promise<number>} Maximum Amount
   */
  async maxAmountToLock() {
    const res = await this.getContract()
      .methods.maxAmountToLock()
      .call();
    return Numbers.fromDecimals(res, this.getERC20Contract().getDecimals());
  }

  /**
   * Check if locked tokens release date has come and user can withdraw them
   * @function
   * @param {Object} params
   * @param {Address} params.address
   * @returns {Promise<boolean>} canRelease
   */
  canRelease = ({ address }) => this.getContract().methods.canRelease(address).call();

  /**
   * Get locked tokens amount for a given address
   * @function
   * @param {Object} params
   * @param {Address} params.address
   * @returns {Promise<number>} amount Locked token amount
   */
  getLockedTokens = async ({ address }) => {
    const res = await this.getContract()
      .methods.getLockedTokens(address)
      .call();
    return Numbers.fromDecimals(res, this.getERC20Contract().getDecimals());
  };

  /**
   * Get locked tokens info for a given address
   * @function
   * @param {Object} params
   * @param {Address} params.address
   * @returns {Date} startDate
   * @returns {Date} endDate
   * @returns {Promise<number>} amount Token amount
   */
  getLockedTokensInfo = async ({ address }) => {
    const res = await this.getContract()
      .methods.getLockedTokensInfo(address)
      .call();

    return {
      startDate: Numbers.fromSmartContractTimeToMinutes(res[0]),
      endDate: Numbers.fromSmartContractTimeToMinutes(res[1]),
      amount: Numbers.fromDecimals(
        res[2],
        this.getERC20Contract().getDecimals(),
      ),
    };
  };

  /**
   * (Admin only) sets maximum amount of tokens to lock per user
   * @function
   * @param {Object} params
   * @param {Address} params.tokenAmount Amount of Tokens
   * @returns {Promise<boolean>} Success True if operation was successful
   */
  setMaxAmountToLock = ({ tokenAmount }, options) => {
    this.onlyOwner(); // verify that user is admin

    /* Get Decimals of Amount */
    const amountWithDecimals = Numbers.toSmartContractDecimals(
      tokenAmount,
      this.getERC20Contract().getDecimals(),
    );

    return this.__sendTx(
      this.getContract()
        .methods.setMaxAmountToLock(amountWithDecimals),
      options,
    );
  };

  /**
   * (Admin only) sets minimum amount of tokens to lock per user
   * @function
   * @param {Object} params
   * @param {number} params.tokenAmount Minimum tokens amount
   * @returns {Promise<boolean>} Success True if operation was successful
   */
  setMinAmountToLock = ({ tokenAmount }, options) => {
    this.onlyOwner(); // verify that user is admin

    /* Get Decimals of Amount */
    const amountWithDecimals = Numbers.toSmartContractDecimals(
      tokenAmount,
      this.getERC20Contract().getDecimals(),
    );

    return this.__sendTx(
      this.getContract()
        .methods.setMinAmountToLock(amountWithDecimals),
      options,
    );
  };

  /**
   * User locks his tokens until specified end date.
   * REQUIREMENTS:
   *  user must have approved this contract to spend the tokens "amount" he wants to lock before calling this function.
   * @function
   * @param {Object} params
   * @param {Address} params.address User Address
   * @param {number} params.amount Tokens amount to be locked
   * @param {Date} params.endDate Lock tokens until this end date
   * @returns {Promise<boolean>} Success True if operation was successful
   */
  lock = async ({ address, amount, endDate }, options) => {
    // / 'address' is current user address

    this.whenNotPaused(); // verify that contract is not paused

    assert(
      amount > 0
      && amount >= (await this.minAmountToLock())
      && amount <= (await this.maxAmountToLock()),
      'Invalid token amount',
    );
    assert(endDate > moment(), 'Invalid end date');

    // check if user can lock tokens
    const lockedAmount = await this.getLockedTokens({ address });
    assert(lockedAmount === '0', 'User already has locked tokens'); // otherwise user already locked tokens

    /* Verify if transfer is approved for this amount */
    const isApproved = await this.getERC20Contract().isApproved({
      address,
      amount,
      spenderAddress: this.getAddress(),
    });
    if (!isApproved) {
      throw new Error(
        "Has to Approve Token Transfer First, use the 'approve' Call",
      );
    }
    return this.__sendTx(
      this.getContract()
        .methods.lock(
          Numbers.toSmartContractDecimals(
            amount,
            this.getERC20Contract().getDecimals(),
          ),
          Numbers.timeToSmartContractTime(endDate),
        ),
      options,
    );
  };

  /**
   * User withdraws his locked tokens after specified end date
   * @function
   * @param {Object} params
   * @param {Address} params.address User Address
   * @return {Promise<boolean>} Success True if operation was successful
   */
  release = async ({ address }, options) => {
    // / 'address' is current user address

    // check if user has locked tokens and if he can unlock and withdraw them
    const { endDate, amount } = await this.getLockedTokensInfo({
      address,
    });
    const lockedAmount = amount;

    assert(lockedAmount > 0, 'ERC20TokenLock.user has no locked tokens');
    assert(
      moment() >= endDate,
      'ERC20TokenLock.tokens release date not reached',
    );

    return this.__sendTx(
      this.getContract().methods.release(),
      options,
    );
  };

  /**
   * Approve this contract to transfer tokens of the ERC20 token contract on behalf of user
   * @function
   * @return {Promise<boolean>} Success True if operation was successful
   */
  approveERC20Transfer = async () => {
    // let totalMaxAmount = await this.getERC20Contract().getTokenAmount(await this.getUserAddress());
    const totalMaxAmount = await this.getERC20Contract().totalSupply();
    return this.getERC20Contract().approve({
      address: this.getAddress(),
      amount: Numbers.toSmartContractDecimals(
        totalMaxAmount,
        this.getERC20Contract().getDecimals(),
      ),
    });
  };

  /**
   *
   * @return {Promise<void>}
   * @throws {Error} Contract is not deployed, first deploy it and provide a contract address
   */
  __assert = async () => {
    if (!this.getAddress()) {
      throw new Error(
        'Contract is not deployed, first deploy it and provide a contract address',
      );
    }

    /* Use ABI */
    this.params.contract.use(tokenlock, this.getAddress());

    /* Set Token Address Contract for easy access */
    if (!this.params.ERC20Contract) {
      this.params.ERC20Contract = new ERC20Contract({
        web3Connection: this.web3Connection,
        contractAddress: await this.erc20(),
      });
    }
    /* Assert Token Contract */
    await this.params.ERC20Contract.start();
    await this.params.ERC20Contract.__assert();
  };

  /**
   * Deploy the ERC20 Token Lock Contract
   * @function
   * @param {IContract~TxOptions} options
   * @return {Promise<*|undefined>}
   * @throws {Error} No Token Address Provided
   */
  deploy = async options => {
    if (!this.getERC20Contract()) {
      throw new Error('No Token Address Provided');
    }
    const params = [ this.getERC20Contract().getAddress() ];

    const res = await this.__deploy(params, options);
    this.params.contractAddress = res.contractAddress;
    /* Call to Backend API */
    await this.__assert();
    return res;
  };

  /**
   * @function
   * @return ERC20Contract|undefined
   */
  getERC20Contract = () => this.params.ERC20Contract;
}

export default ERC20TokenLock;