Source

models/Sablier/Sablier.js

import { sablier } from '../../interfaces';
import IContract from '../IContract';
import Numbers from '../../utils/Numbers';

/**
 * Sablier Object for decentralized escrow payments
 * @class Sablier
 * @param {Sablier~Options} options
 */
export default class Sablier extends IContract {
  constructor(params) {
    super({ abi: sablier, ...params });
  }

  /**
   * Add pauser role to given address.
   * @param {Object} params
   * @param {address} params.account Address to assign pauser role to.
   * @returns {Promise<void>}
   */
  addPauser({ account }, options) {
    return this.__sendTx(
      this.getContract().methods.addPauser(account),
      options,
    );
  }

  /**
   * Pause contract.
   * @returns {Promise<void>}
   */
  pause(options) {
    return this.__sendTx(
      this.getContract().methods.pause(),
      options,
    );
  }

  /**
   * Unpause/resume contract.
   * @returns {Promise<void>}
   */
  unpause(options) {
    return this.__sendTx(
      this.getContract().methods.unpause(),
      options,
    );
  }

  /**
   * Check if the given address has pauser role.
   * @param {Object} params
   * @param {address} params.account Address to check.
   * @returns {Promise<bool>}
   */
  isPauser({ account }) {
    return this.getContract().methods.isPauser(account).call();
  }

  /**
   * Counter for new stream ids.
   * @returns {Promise<uint256>}
   */
  nextStreamId() {
    return this.getContract().methods.nextStreamId().call();
  }

  /**
   * The percentage fee charged by the contract on the accrued interest. For example 75
   * @returns {Promise<uint256>} mantissa
   */
  async fee() {
    const res = await this.getContract().methods.fee().call();
    // 1e16 is 1% of 1e18, fee is stored with 16 decimals as one hundred percent.
    return Numbers.fromDecimalsToBN(res, 16);
  }

  /**
   * Updates the Sablier fee.
   *  Throws if the caller is not the owner of the contract.
   * @param {Object} params
   * @param {uint256} params.feePercentage The new fee as a percentage, for example 75 means 75%.
   * @returns {Promise<void>}
   */
  updateFee({ feePercentage }, options) {
    return this.__sendTx(
      this.getContract().methods.updateFee(feePercentage),
      options,
    );
  }

  /**
   * Withdraws the earnings for the given token address.
   * Throws if `amount` exceeds the available balance.
   * @param {Object} params
   * @param {address} params.tokenAddress The address of the token to withdraw earnings for.
   * @param {uint256} params.amount The amount of tokens to withdraw.
   * @returns {Promise<void>}
   */
  async takeEarnings({ tokenAddress, amount }, options) {
    const decimals = await this.getContract().methods.getTokenDecimals(tokenAddress).call();
    const amountWithDecimals = Numbers.fromBNToDecimals(
      amount,
      decimals,
    );
    return this.__sendTx(
      this.getContract().methods.takeEarnings(tokenAddress, amountWithDecimals),
      options,
    );
  }

  /**
   * @typedef {Object} Sablier~getStreamType
   * @property {address} sender
   * @property {address} recipient
   * @property {uint256} deposit
   * @property {address} tokenAddress
   * @property {uint256} startTime
   * @property {uint256} stopTime
   * @property {uint256} remainingBalance
   * @property {uint256} ratePerSecond
   */

  /**
   * Returns the compounding stream with all its properties.
   *  Throws if the id does not point to a valid stream.
   * @param {Object} params
   * @param {uint256} params.streamId The id of the stream to query.
   * @returns {Promise<Sablier~getStream>} The stream object.
   */
  async getStream({ streamId }) {
    const res = await this.getContract().methods.getStream(streamId).call();

    const tokenAddress = res[3];
    const decimals = await this.getContract().methods.getTokenDecimals(tokenAddress).call();
    return {
      sender: res[0],
      recipient: res[1],
      deposit: Numbers.fromDecimalsToBN(res[2], decimals),
      tokenAddress, // res[3]
      startTime: res[4], // Numbers.fromSmartContractTimeToMinutes(res[4]),
      stopTime: res[5], // Numbers.fromSmartContractTimeToMinutes(res[5]),
      remainingBalance: Numbers.fromDecimalsToBN(res[6], decimals),
      ratePerSecond: Numbers.fromDecimalsToBN(res[7], decimals),
    };
  }

  /**
   * Returns either the delta in seconds between `block.timestamp` and `startTime` or
   *  between `stopTime` and `startTime, whichever is smaller. If `block.timestamp` is before
   *  `startTime`, it returns 0.
   *  Throws if the id does not point to a valid stream.
   * @param {Object} params
   * @param {uint256} params.streamId The id of the stream for which to query the delta.
   * @returns {Promise<uint256>} delta The time delta in seconds.
   */
  deltaOf({ streamId }) {
    return this.getContract().methods.deltaOf(streamId).call();
  }

  /**
   * Returns the available funds for the given stream id and address.
   *  Throws if the id does not point to a valid stream.
   * @param {Object} params
   * @param {uint256} params.streamId The id of the stream for which to query the balance.
   * @param {address} params.who The address for which to query the balance.
   * @returns {Promise<uint256>} balance The total funds allocated to `who` as uint256.
   */
  async balanceOf({ streamId, who }) {
    const decimals = await this.getContract().methods.getTokenDecimalsFromStream(streamId).call();
    const balance = await this.getContract().methods.balanceOf(streamId, who).call();
    return Numbers.fromDecimalsToBN(balance, decimals);
  }

  /**
   * Checks if the provided id points to a valid compounding stream.
   * @param {Object} params
   * @param {uint256} params.streamId The id of the compounding stream to check.
   * @returns {Promise<bool>} True if it is a compounding stream, otherwise false.
   */
  isCompoundingStream({ streamId }) {
    return this.getContract().methods.isCompoundingStream(streamId).call();
  }

  /**
   * @typedef {Object} Sablier~getCompoundingStreamType
   * @property {address} sender
   * @property {address} recipient
   * @property {uint256} deposit
   * @property {address} tokenAddress
   * @property {uint256} startTime
   * @property {uint256} stopTime
   * @property {uint256} remainingBalance
   * @property {uint256} ratePerSecond
   * @property {uint256} exchangeRateInitial
   * @property {uint256} senderSharePercentage
   * @property {uint256} recipientSharePercentage
   */

  /**
   * Returns the compounding stream object with all its properties.
   *  Throws if the id does not point to a valid compounding stream.
   * @param {Object} params
   * @param {uint256} params.streamId The id of the compounding stream to query.
   * @returns {Promise<Sablier~getCompoundingStream>} The compounding stream object.
   */
  async getCompoundingStream({ streamId }) {
    const res = await this.getContract().methods.getCompoundingStream(streamId).call();

    const tokenAddress = res[3];
    const decimals = await this.getContract().methods.getTokenDecimals(tokenAddress).call();
    return {
      sender: res[0],
      recipient: res[1],
      deposit: Numbers.fromDecimalsToBN(res[2], decimals),
      tokenAddress, // res[3]
      startTime: res[4], // Numbers.fromSmartContractTimeToMinutes(res[4]),
      stopTime: res[5], // Numbers.fromSmartContractTimeToMinutes(res[5]),
      remainingBalance: Numbers.fromDecimalsToBN(res[6], decimals),
      ratePerSecond: Numbers.fromDecimalsToBN(res[7], decimals),
      exchangeRateInitial: Numbers.fromDecimalsToBN(res[8], decimals), // uint256, TODO ??? any conversion needed
      senderSharePercentage: Numbers.fromDecimalsToBN(res[9], 16), // comes scaled up to 1e16, for example 75*1e16 is 75%
      recipientSharePercentage: Numbers.fromDecimalsToBN(res[10], 16),
    };
  }

  /** @typedef {Object} Sablier~interestOfType
   * @property {uint256} senderInterest
   * @property {uint256} recipientInterest
   * @property {uint256} sablierInterest
   */

  /**
   * Computes the interest accrued while the money has been streamed. Returns (0, 0, 0) if
   *  the stream is either not a compounding stream or it does not exist.
   *  Throws if there is a math error. We do not assert the calculations which involve the current
   *  exchange rate, because we can't know what value we'll get back from the cToken contract.
   * @param {Object} params
   * @param {uint256} params.streamId The id of the compounding stream for which to calculate the interest.
   * @param {uint256} params.amount The amount of money with respect to which to calculate the interest.
   * @returns {Promise<Sablier~interestOf>} The interest accrued by the sender, the recipient and sablier, respectively, as uint256s.
   */
  async interestOf({ streamId, amount }) {
    const decimals = await this.getContract().methods.getTokenDecimalsFromStream(streamId).call();
    const amountWithDecimals = Numbers.fromBNToDecimals(amount, decimals);
    const res = await this.getContract().methods.interestOf(streamId, amountWithDecimals).call();
    return {
      senderInterest: Numbers.fromDecimalsToBN(res[0], decimals),
      recipientInterest: Numbers.fromDecimalsToBN(res[1], decimals),
      sablierInterest: Numbers.fromDecimalsToBN(res[2], decimals),
    };
  }

  /**
   * Returns the amount of interest that has been accrued for the given token address.
   * @param {Object} params
   * @param {address} params.tokenAddress The address of the token to get the earnings for.
   * @returns {Promise<uint256>} The amount of interest as uint256.
   */
  async getEarnings({ tokenAddress }) {
    const earnings = await this.getContract().methods.getEarnings(tokenAddress).call();
    const decimals = await this.getContract().methods.getTokenDecimals(tokenAddress).call();
    return Numbers.fromDecimalsToBN(earnings, decimals);
  }

  /**
   * Creates a new stream funded by `msg.sender` and paid towards `recipient`.
   *  Throws if paused.
   *  Throws if the recipient is the zero address, the contract itself or the caller.
   *  Throws if the deposit is 0.
   *  Throws if the start time is before `block.timestamp`.
   *  Throws if the stop time is before the start time.
   *  Throws if the duration calculation has a math error.
   *  Throws if the deposit is smaller than the duration.
   *  Throws if the deposit is not a multiple of the duration.
   *  Throws if the rate calculation has a math error.
   *  Throws if the next stream id calculation has a math error.
   *  Throws if the contract is not allowed to transfer enough tokens.
   *  Throws if there is a token transfer failure.
   * @param {Object} params
   * @param {address} params.recipient The address towards which the money is streamed.
   * @param {uint256} params.deposit The amount of money to be streamed.
   * @param {address} params.tokenAddress The ERC20 token to use as streaming currency.
   * @param {uint256} params.startTime The unix timestamp for when the stream starts.
   * @param {uint256} params.stopTime The unix timestamp for when the stream stops.
   * @returns {Promise<uint256>} The uint256 id of the newly created stream.
   */
  async createStream({
    recipient, deposit, tokenAddress, startTime, stopTime,
  }, options) {
    const decimals = await this.getContract().methods.getTokenDecimals(tokenAddress).call();
    const depositWithDecimals = Numbers.fromBNToDecimals(deposit, decimals);
    return this.__sendTx(
      this.getContract().methods.createStream(recipient, depositWithDecimals, tokenAddress, startTime, stopTime),
      options,
    );
  }

  /**
   * Creates a new compounding stream funded by `msg.sender` and paid towards `recipient`.
   * Inherits all security checks from `createStream`.
   *  Throws if the cToken is not whitelisted.
   *  Throws if the sender share percentage and the recipient share percentage do not sum up to 100.
   *  Throws if the the sender share mantissa calculation has a math error.
   *  Throws if the the recipient share mantissa calculation has a math error.
   * @param {Object} params
   * @param {address} params.recipient The address towards which the money is streamed.
   * @param {uint256} params.deposit The amount of money to be streamed.
   * @param {address} params.tokenAddress The ERC20 token to use as streaming currency.
   * @param {uint256} params.startTime The unix timestamp for when the stream starts.
   * @param {uint256} params.stopTime The unix timestamp for when the stream stops.
   * @param {uint256} params.senderSharePercentage The sender's share of the interest, as a percentage.
   * @param {uint256} params.recipientSharePercentage The recipient's share of the interest, as a percentage.
   * @returns {Promise<uint256>} The uint256 id of the newly created compounding stream.
   */
  async createCompoundingStream({
    recipient, deposit, tokenAddress, startTime, stopTime, senderSharePercentage, recipientSharePercentage,
  }, options) {
    const decimals = await this.getContract().methods.getTokenDecimals(tokenAddress).call();
    const depositWithDecimals = Numbers.fromBNToDecimals(deposit, decimals);
    return this.__sendTx(
      this.getContract().methods.createCompoundingStream(
        recipient,
        depositWithDecimals,
        tokenAddress,
        startTime,
        stopTime,
        senderSharePercentage,
        recipientSharePercentage,
      ),
      options,
    );
  }

  /**
   * Withdraws from the contract to the recipient's account.
   *  Throws if the id does not point to a valid stream.
   *  Throws if the caller is not the sender or the recipient of the stream.
   *  Throws if the amount exceeds the available balance.
   *  Throws if there is a token transfer failure.
   * @param {Object} params
   * @param {uint256} params.streamId The id of the stream to withdraw tokens from.
   * @param {uint256} params.amount The amount of tokens to withdraw.
   * @returns {Promise<bool>} True if success, otherwise false.
   */
  async withdrawFromStream({ streamId, amount }, options) {
    const decimals = await this.getContract().methods.getTokenDecimalsFromStream(streamId).call();
    const amountWithDecimals = Numbers.fromBNToDecimals(amount, decimals);
    return this.__sendTx(
      this.getContract().methods.withdrawFromStream(streamId, amountWithDecimals),
      options,
    );
  }

  /**
   * Cancels the stream and transfers the tokens back on a pro rata basis.
   *  Throws if the id does not point to a valid stream.
   *  Throws if the caller is not the sender or the recipient of the stream.
   *  Throws if there is a token transfer failure.
   * @param {Object} params
   * @param {uint256} params.streamId The id of the stream to cancel.
   * @returns {Promise<bool>} True if success, otherwise false.
   */
  cancelStream({ streamId }, options) {
    return this.__sendTx(
      this.getContract().methods.cancelStream(streamId),
      options,
    );
  }

  /**
   * Get token decimals given a stream id.
   * This is an utility function for DApp layer when converting to float numbers.
   * @param streamId The id of the stream.
   */
  getTokenDecimalsFromStream({ streamId }) {
    return this.getContract().methods.getTokenDecimalsFromStream(streamId).call();
  }

  /**
   *
   * @return {Promise<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(sablier, this.getAddress());
  };

  /**
   * Deploy the Sablier Contract
   * @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;
  };
}