AugurProject/augur-ui

View on GitHub
src/modules/transactions/actions/add-transactions.js

Summary

Maintainability
F
4 days
Test Coverage
import {
  MARKET_CREATION,
  PUBLIC_TRADE,
  TRANSFER,
  REPORTING,
  TRADE,
  OPEN_ORDER,
  BUY,
  SELL,
  COMPLETE_SETS_SOLD
} from "modules/transactions/constants/types";
import { SUCCESS, PENDING } from "modules/transactions/constants/statuses";
import { updateTransactionsData } from "modules/transactions/actions/update-transactions-data";
import { eachOf, each } from "async";
import { unfix } from "speedomatic";
import { isNull, orderBy } from "lodash";
import { createBigNumber } from "utils/create-big-number";
import { convertUnixToFormattedDate } from "src/utils/format-date";
import { YES_NO, CATEGORICAL } from "modules/markets/constants/market-types";
import {
  formatAttoRep,
  formatShares,
  formatEther,
  formatAttoEth
} from "utils/format-number";
import calculatePayoutNumeratorsValue from "utils/calculate-payout-numerators-value";

import { groupBy } from "lodash/fp";

function formatTransactionMessage(sumBuy, sumSell, txType) {
  const buys = sumBuy !== 0 ? `${sumBuy} ${BUY}` : "";
  const sells = sumSell !== 0 ? `${sumSell} ${SELL}` : "";
  return `${buys}${
    sumBuy !== 0 && sumSell !== 0 ? " & " : " "
  }${sells} ${txType}${sumBuy + sumSell > 1 ? "s" : ""}`;
}

function formatTransactionFillMessage(txType, amount, price, numTxs, outcome) {
  return `${txType === BUY ? "bought" : "sold"} ${amount} Share${
    amount === 1 ? "" : "s"
  } of ${outcome} at ${numTxs > 1 ? "an average price of " : ""}${price}`;
}

export function addTradeTransactions(trades) {
  return (dispatch, getState) => {
    const { marketsData } = getState();
    const transactions = {};
    const sorted = groupBy("tradeGroupId", trades);
    // todo: need fallback if groupBy fails and groups aren't created
    each(sorted, group => {
      if (group[0].tradeGroupId === undefined) {
        each(group, trade => {
          const header = buildTradeTransaction(trade, marketsData);
          transactions[header.hash] = header;
        });
      } else {
        const header = buildTradeTransactionGroup(group, marketsData);
        transactions[header.hash] = header;
      }
    });
    dispatch(updateTransactionsData(transactions));
  };
}

function buildTradeTransactionGroup(group, marketsData) {
  let header = {};
  let sumAmount = createBigNumber(0);
  let sumPrice = createBigNumber(0);
  let numTxs = 0;

  each(group, trade => {
    sumAmount = sumAmount.plus(createBigNumber(trade.amount));
    sumPrice = sumPrice.plus(
      createBigNumber(trade.price).times(createBigNumber(trade.amount))
    );
    numTxs += 1;
    const localHeader = buildTradeTransaction(trade, marketsData);
    if (Object.keys(header).length === 0) {
      header = localHeader;
    } else {
      header.transactions.push(localHeader.transactions[0]);
    }
  });

  header.message = formatTransactionFillMessage(
    header.transactions[0].type,
    formatShares(sumAmount.toNumber()).formatted,
    formatEther(sumPrice.dividedBy(sumAmount).toNumber()).full,
    numTxs,
    header.transactions[0].meta && header.transactions[0].meta.outcome
  );
  return header;
}

function buildTradeTransaction(trade, marketsData) {
  const market = marketsData[trade.marketId];
  const transaction = { ...trade, market };
  transaction.status = SUCCESS;
  transaction.id = `${transaction.transactionHash}-${transaction.orderId}`;
  const header = buildHeader(transaction, TRADE, SUCCESS);
  const meta = {};
  meta.type = TRADE + (transaction.maker ? " Filled" : " Placed");
  const outcomeName = getOutcome(market, transaction.outcome);
  if (outcomeName) meta.outcome = outcomeName;
  const formattedShares = formatShares(transaction.amount);
  meta.shares = formattedShares.formatted;
  meta.price = transaction.price;
  meta.fee = transaction.settlementFees;
  meta.txhash = transaction.transactionHash;
  meta.timestamp = transaction.timestamp;
  transaction.meta = meta;
  header.status = SUCCESS;
  if (transaction.market) {
    header.description = transaction.market.description;
  }
  transaction.message = `${transaction.type} ${
    formattedShares.formatted
  } Shares @ ${transaction.price} ETH`;
  header.transactions = [transaction];
  return header;
}

export function addTransferTransactions(transfers) {
  return dispatch => {
    const transactions = {};
    each(transfers, transfer => {
      const transaction = { ...transfer };
      transaction.id = `${transaction.transactionHash}-${transaction.logIndex}`;
      const header = buildHeader(transaction, TRANSFER, SUCCESS);
      header.transactions = [transaction];
      const meta = {
        value: transaction.value
      };
      if (transaction.symbol === "ether") {
        meta.value = `${
          formatAttoEth(transaction.value, { decimals: 4, roundUp: true })
            .formatted
        }`;
        meta.block = transaction.blockNumber || transaction.creationBlockNumber;
        meta.sender = transaction.sender;
        transaction.symbol = "ETH";
        header.message = "ETH Transfer";
        header.description = `${meta.value} ${transaction.symbol} transferred`;
      } else if (transaction.symbol === "ParticipationToken") {
        meta.value = `${
          formatAttoRep(transaction.value, { decimals: 4, roundUp: true })
            .formatted
        }`;
        header.message = "Participation Tokens purchased";
        header.description = `${meta.value} Participation purchased`;
      } else if (
        transaction.symbol === "REP" ||
        (transaction.market === "0x0000000000000000000000000000000000000000" &&
          transaction.eventName === "TokensTransferred")
      ) {
        meta.value = `${
          formatAttoRep(transaction.value, { decimals: 4, roundUp: true })
            .formatted
        }`;
        meta.block = transaction.blockNumber || transaction.creationBlockNumber;
        meta.sender = transaction.sender;
        transaction.symbol = "REP";
        header.message = "Rep Transfer";
        header.description = `${meta.value} ${transaction.symbol} transferred`;
      } else {
        header.message = "Transfer";
        header.description = `${meta.value} ${
          transaction.symbol
        } transferred from ${transaction.sender} to ${transaction.recipient}`;
        meta.sender = transaction.sender;
        meta.block = transaction.blockNumber;
      }
      transaction.meta = Object.assign(meta, {
        txhash: transaction.transactionHash,
        recipient: transaction.recipient
      });
      if (transaction.meta.recipient !== null)
        transactions[transaction.id] = header;
    });
    dispatch(updateTransactionsData(transactions));
  };
}

export function addNewMarketCreationTransactions(market) {
  return (dispatch, getState) => {
    const marketCreationData = {};
    const { loginAccount } = getState();
    const transaction = {
      timestamp: market._endTime,
      createdBy: loginAccount.address,
      id: market.hash
    };
    const fee = unfix(market._feePerEthInWei, "number");
    const percentage = createBigNumber(fee.toString())
      .times(createBigNumber(100))
      .toNumber();
    const meta = {
      txhash: market.hash,
      "designated reporter": market._designatedReporterAddress,
      "settlement fee": `${percentage} %`
    };
    transaction.meta = meta;

    const header = {
      ...buildHeader(transaction, MARKET_CREATION, PENDING),
      message: "Market Creation",
      description: market._description,
      type: MARKET_CREATION,
      transactions: [transaction]
    };

    marketCreationData[transaction.id] = header;
    dispatch(updateTransactionsData(marketCreationData));
  };
}

export function addMarketCreationTransactions(marketsCreated) {
  return (dispatch, getState) => {
    const marketCreationData = {};
    const { loginAccount, marketsData } = getState();
    each(marketsCreated, marketIdOrObject => {
      const market = marketsData[marketIdOrObject];
      const marketId = marketIdOrObject;
      // we already get market creation from augur-node
      if (typeof marketIdOrObject === "object") return;
      const transaction = Object.assign(
        { marketId, market },
        {
          timestamp: market.creationTime,
          createdBy: loginAccount.address,
          id: marketId
        }
      );
      transaction.meta = {
        market: transaction.marketId,
        "market type": market.marketType,
        category: market.category,
        "reporting start": convertUnixToFormattedDate(market.endTime)
          .formattedLocal,
        "designated reporter": market.designatedReporter
      };
      const header = Object.assign(
        buildHeader(transaction, MARKET_CREATION, SUCCESS),
        {
          message: "Market Creation",
          description: market.description,
          transactions: [transaction]
        }
      );
      marketCreationData[transaction.id] = header;
    });
    dispatch(updateTransactionsData(marketCreationData));
  };
}

export function addCompleteSetsSoldLogs(completeSetsSoldLogs) {
  return (dispatch, getState) => {
    const { marketsData } = getState();
    const completeSetsData = {};
    each(completeSetsSoldLogs, completeSetLog => {
      const { marketId } = completeSetLog;
      const market = marketsData[marketId];
      const transaction = Object.assign(
        { marketId, ...completeSetLog },
        {
          id: completeSetLog.transactionHash
        }
      );
      transaction.meta = {
        market: transaction.marketId,
        amount: formatShares(transaction.numCompleteSets).full,
        block: completeSetLog.blockNumber,
        sender: completeSetLog.account
      };
      const header = Object.assign(
        buildHeader(transaction, COMPLETE_SETS_SOLD, SUCCESS),
        {
          message: "Complete Sets Sold",
          description: `${transaction.meta.amount} sold on Market: ${
            (market || {}).description
          }`,
          transactions: [transaction]
        }
      );
      completeSetsData[transaction.id] = header;
    });
    dispatch(updateTransactionsData(completeSetsData));
  };
}

export function addOpenOrderTransactions(openOrders) {
  return (dispatch, getState) => {
    const { marketsData, transactionsData } = getState();
    // flatten open orders
    const transactions = {};
    eachOf(openOrders, (value, marketId) => {
      const market = marketsData[marketId];
      let sumBuy = 0;
      let sumSell = 0;
      const marketHeader = {
        status: SUCCESS,
        marketId,
        sortOrder: getSortOrder(OPEN_ORDER)
      };
      if (market !== undefined) {
        marketHeader.description = market.description;
      }
      let creationTime = null;
      const marketTradeTransactions = [];
      eachOf(value, (value2, outcome) => {
        eachOf(value2, (value3, type) => {
          const sorted = orderBy(value3, ["creationTime"], ["desc"]);
          eachOf(sorted, (value4, hash) => {
            const transaction = { marketId, type, hash, ...value4 };
            transaction.id = transaction.transactionHash + transaction.logIndex;
            const meta = {};
            if (
              transaction.canceledTransactionHash &&
              transaction.canceledTime
            ) {
              const cancelationTime = convertUnixToFormattedDate(
                transaction.canceledTime
              );
              meta.canceledTransactionHash =
                transaction.canceledTransactionHash;
              meta.canceledTime = cancelationTime.full;
            }
            transaction.message = `${type} ${
              transaction.originalFullPrecisionAmount
            } Shares @ ${transaction.fullPrecisionPrice} ETH`;
            creationTime = convertUnixToFormattedDate(transaction.creationTime);
            meta.txhash = transaction.transactionHash;
            meta.timestamp = creationTime.full;
            const outcomeName = getOutcome(market, outcome);
            if (outcomeName) meta.outcome = outcomeName;
            meta.status = transaction.orderState.toLowerCase();
            meta.status =
              meta.status.charAt(0).toUpperCase() + meta.status.slice(1);
            meta.amount = formatShares(
              transaction.originalFullPrecisionAmount
            ).full;
            meta.filled = formatShares(
              createBigNumber(
                transaction.originalFullPrecisionAmount,
                10
              ).minus(transaction.fullPrecisionAmount, 10)
            ).full;
            meta.price = formatEther(transaction.fullPrecisionPrice).full;
            transaction.meta = meta;
            marketTradeTransactions.push(transaction);
            if (type === BUY) {
              sumBuy += 1;
            }
            if (type === SELL) {
              sumSell += 1;
            }
          });
        });
      });
      // TODO: last order creation time will be in header, earliest activite
      const currentTransactionsKeys = Object.keys(transactionsData);
      const leadOrder = marketTradeTransactions.find(marketTransaction =>
        currentTransactionsKeys.includes(marketTransaction.orderId)
      );
      marketHeader.timestamp = creationTime;
      marketHeader.message = formatTransactionMessage(sumBuy, sumSell, "Order");
      marketHeader.type = OPEN_ORDER;
      marketHeader.transactions = marketTradeTransactions;
      marketHeader.hash = leadOrder
        ? leadOrder.orderId
        : `${marketTradeTransactions[0].orderId}`;
      transactions[marketHeader.hash] = marketHeader;
    });
    dispatch(updateTransactionsData(transactions));
  };
}

export function addReportingTransactions(reports) {
  return (dispatch, getState) => {
    const { marketsData } = getState();
    const transactions = {};
    eachOf(reports, (reports, universe) => {
      eachOf(reports, (value1, marketId) => {
        eachOf(value1, (value2, reportType) => {
          const market = marketsData[marketId];
          if (reportType === "initialReporter" && !isNull(value2)) {
            const transaction = {
              universe,
              marketId,
              ...value2,
              market,
              reportType
            };
            transactions[transaction.transactionHash] = processReport(
              market,
              transaction
            );
          } else {
            eachOf(value2, (report, index) => {
              const transaction = {
                universe,
                marketId,
                ...report,
                market,
                reportType
              };
              transactions[transaction.transactionHash] = processReport(
                market,
                transaction
              );
            });
          }
        });
      });
    });
    dispatch(updateTransactionsData(transactions));
  };
}

function processReport(market, transaction) {
  const amountStaked = formatAttoRep(transaction.amountStaked, {
    decimals: 4,
    roundUp: true
  });
  const outcomeName = getOutcome(
    transaction.market,
    calculatePayoutNumeratorsValue(
      market,
      transaction.payoutNumerators,
      transaction.isInvalid
    )
  );
  transaction.id = transaction.transactionHash + transaction.logIndex;
  transaction.meta = {
    txhash: transaction.transactionHash,
    type:
      transaction.reportType === "initialReporter"
        ? "Initial Report"
        : "Dispute",
    staked: `${amountStaked.formatted} REP`,
    outcome: outcomeName,
    marketId: transaction.marketId
  };
  transaction.timestamp = transaction.creationTime;
  const header = Object.assign(buildHeader(transaction, REPORTING, SUCCESS), {
    transactions: [transaction],
    message: `${transaction.meta.type}`,
    description: `Staked ${amountStaked.formatted} REP on "${
      market.description
    }"`
  });
  return header;
}

export function getOutcome(
  market,
  outcome,
  alwaysReturnYesForBinaryMarket = true
) {
  let value = null;
  if (!market || isNaN(outcome)) return outcome;
  if (market.marketType === YES_NO) {
    value = "Yes";
    if (!alwaysReturnYesForBinaryMarket && outcome.toString() === "0") {
      value = "No";
    }
  } else if (market.marketType === CATEGORICAL) {
    value = market.outcomes[outcome] && market.outcomes[outcome].description;
  } else {
    value = outcome;
  }
  return value;
}

function buildHeader(item, type, status) {
  const header = {};
  header.status = status;
  header.hash = item.id;
  header.type = type;
  // TODO: need to sort by datetime in render
  if (item.timestamp) {
    header.timestamp = convertUnixToFormattedDate(item.timestamp);
  } else if (item.creationTime) {
    header.timestamp = convertUnixToFormattedDate(item.creationTime);
  }
  header.sortOrder = getSortOrder(type);
  return header;
}

// TODO: this should be dynamic by user control
export function getSortOrder(type) {
  if (type === OPEN_ORDER) {
    return 10;
  }
  if (type === TRADE || type === PUBLIC_TRADE) {
    return 20;
  }
  if (type === COMPLETE_SETS_SOLD) {
    return 40;
  }
  if (type === TRANSFER) {
    return 50;
  }
  if (type === MARKET_CREATION) {
    return 90;
  }
  if (type === REPORTING) {
    return 100;
  }
  return 1000;
}