decred.org/dcrdex@v1.0.3/client/webserver/site/src/js/orderutil.ts (about)

     1  import * as intl from './locales'
     2  import {
     3    app,
     4    Order,
     5    TradeForm,
     6    OrderOption,
     7    Match
     8  } from './registry'
     9  import { BooleanOption, XYRangeOption } from './opts'
    10  import Doc from './doc'
    11  
    12  export const Limit = 1 // TODO: Delete for the versions below
    13  export const Market = 2 // TODO: Delete for the versions below
    14  export const Cancel = 3 // TODO: Delete for the versions below
    15  
    16  export const OrderTypeLimit = 1
    17  export const OrderTypeMarket = 2
    18  export const OrderTypeCancel = 3
    19  
    20  /* The time-in-force specifiers are a mirror of dex/order.TimeInForce. */
    21  export const ImmediateTiF = 0
    22  export const StandingTiF = 1
    23  
    24  /* The order statuses are a mirror of dex/order.OrderStatus. */
    25  export const StatusUnknown = 0
    26  export const StatusEpoch = 1
    27  export const StatusBooked = 2
    28  export const StatusExecuted = 3
    29  export const StatusCanceled = 4
    30  export const StatusRevoked = 5
    31  
    32  /* The match statuses are a mirror of dex/order.MatchStatus. */
    33  export const NewlyMatched = 0
    34  export const MakerSwapCast = 1
    35  export const TakerSwapCast = 2
    36  export const MakerRedeemed = 3
    37  export const MatchComplete = 4
    38  export const MatchConfirmed = 5
    39  
    40  /* The match sides are a mirror of dex/order.MatchSide. */
    41  export const Maker = 0 // TODO: Delete for the versions below
    42  export const Taker = 1 // TODO: Delete for the versions below
    43  
    44  export const MatchSideMaker = 0
    45  export const MatchSideTaker = 1
    46  
    47  /*
    48   * RateEncodingFactor is used when encoding an atomic exchange rate as an
    49   * integer. See docs on message-rate encoding @
    50   * https://github.com/decred/dcrdex/blob/master/spec/comm.mediawiki#Rate_Encoding
    51   */
    52  export const RateEncodingFactor = 1e8
    53  
    54  export function sellString (ord: Order) {
    55    const key = ord.sell ? intl.ID_SELL : intl.ID_BUY
    56    const lang = document.documentElement.lang.toLowerCase()
    57    return intl.prep(key).toLocaleLowerCase(lang)
    58  }
    59  
    60  export function typeString (ord: Order) {
    61    return ord.type === Limit ? (ord.tif === ImmediateTiF ? intl.prep(intl.ID_LIMIT_ORDER_IMMEDIATE_TIF) : intl.prep(intl.ID_LIMIT_ORDER)) : intl.prep(intl.ID_MARKET_ORDER)
    62  }
    63  
    64  /* isMarketBuy will return true if the order is a market buy order. */
    65  export function isMarketBuy (ord: Order) {
    66    return ord.type === Market && !ord.sell
    67  }
    68  
    69  /*
    70   * hasActiveMatches returns true if the order has matches that have not completed
    71   * settlement yet.
    72   */
    73  export function hasActiveMatches (order: Order) {
    74    if (!order.matches) return false
    75    for (const match of order.matches) {
    76      if (match.active) return true
    77    }
    78    return false
    79  }
    80  
    81  /**
    82   * statusString converts the order status to a string.
    83   *
    84   * IMPORTANT: we have similar function in Golang, it must match this one exactly,
    85   * when updating make sure to update both!
    86   */
    87  export function statusString (order: Order): string {
    88    if (!order.id) return intl.prep(intl.ID_ORDER_SUBMITTING) // order ID is empty.
    89    const isLive = hasActiveMatches(order)
    90    switch (order.status) {
    91      case StatusUnknown: return intl.prep(intl.ID_UNKNOWN)
    92      case StatusEpoch: return intl.prep(intl.ID_EPOCH)
    93      case StatusBooked:
    94        if (order.cancelling) return intl.prep(intl.ID_CANCELING)
    95        return isLive ? `${intl.prep(intl.ID_BOOKED)}/${intl.prep(intl.ID_SETTLING)}` : intl.prep(intl.ID_BOOKED)
    96      case StatusExecuted:
    97        if (isLive) return intl.prep(intl.ID_SETTLING)
    98        if (order.filled === 0 && order.type !== Cancel) return intl.prep(intl.ID_NO_MATCH)
    99        return intl.prep(intl.ID_EXECUTED)
   100      case StatusCanceled:
   101        return isLive ? `${intl.prep(intl.ID_CANCELED)}/${intl.prep(intl.ID_SETTLING)}` : intl.prep(intl.ID_CANCELED)
   102      case StatusRevoked:
   103        return isLive ? `${intl.prep(intl.ID_REVOKED)}/${intl.prep(intl.ID_SETTLING)}` : intl.prep(intl.ID_REVOKED)
   104    }
   105    return intl.prep(intl.ID_UNKNOWN)
   106  }
   107  
   108  /* filled sums the quantities of non-cancel matches available. */
   109  export function filled (order: Order) {
   110    if (!order.matches) return 0
   111    const qty = isMarketBuy(order) ? (m: Match) => m.qty * m.rate / RateEncodingFactor : (m: Match) => m.qty
   112    return order.matches.reduce((filled, match) => {
   113      if (match.isCancel) return filled
   114      return filled + qty(match)
   115    }, 0)
   116  }
   117  
   118  /* settled sums the quantities of the matches that have completed. */
   119  export function settled (order: Order) {
   120    if (!order.matches) return 0
   121    const qty = isMarketBuy(order) ? (m: Match) => m.qty * m.rate / RateEncodingFactor : (m: Match) => m.qty
   122    return order.matches.reduce((settled, match) => {
   123      if (match.isCancel) return settled
   124      const redeemed = (match.side === Maker && match.status >= MakerRedeemed) ||
   125        (match.side === Taker && match.status >= MatchComplete)
   126      return redeemed ? settled + qty(match) : settled
   127    }, 0)
   128  }
   129  
   130  /* averageRateString returns a formatting string containing the average rate of
   131  the matches that have been filled for a market order. */
   132  export function averageMarketOrderRateString (ord: Order): string {
   133    if (!ord.matches?.length) return intl.prep(intl.ID_MARKET_ORDER)
   134    let rateStr = Doc.formatCoinValue(app().conventionalRate(ord.baseID, ord.quoteID, averageRate(ord)))
   135    if (ord.matches.length > 1) rateStr = '~ ' + rateStr // "~" only makes sense if the order has more than one match.
   136    return rateStr
   137  }
   138  
   139  /* averageRate returns a the average rate of the matches that have been filled
   140  in an order. */
   141  export function averageRate (ord: Order): number {
   142    if (!ord.matches?.length) return 0
   143    let rateProduct = 0
   144    let baseQty = 0
   145    for (const m of ord.matches) {
   146      baseQty += m.qty
   147      rateProduct += (m.rate * m.qty) // order ~ 1e16
   148    }
   149    return rateProduct / baseQty
   150  }
   151  
   152  /* baseToQuote returns the quantity of the quote asset. */
   153  export function baseToQuote (rate: number, base: number) : number {
   154    return rate * base / RateEncodingFactor
   155  }
   156  
   157  /* orderPortion returns a string stating the percentage of the order a match
   158     makes up. */
   159  export function orderPortion (order: Order, match: Match) : string {
   160    let matchQty = match.qty
   161    if (isMarketBuy(order)) {
   162      matchQty = baseToQuote(match.rate, match.qty)
   163    }
   164    return ((matchQty / order.qty) * 100).toFixed(1) + ' %'
   165  }
   166  
   167  /*
   168   * matchStatusString is a string used to create a displayable string describing
   169   * describing the match status.
   170   */
   171  export function matchStatusString (m: Match) {
   172    if (m.revoked) {
   173      // When revoked, match status is less important than pending action if still
   174      // active, or the outcome if inactive.
   175      if (m.active) {
   176        if (m.redeem) return revokedMatchStatus(intl.ID_MATCH_STATUS_REDEMPTION_SENT) // must require confirmation if active
   177        // If maker and we have not redeemed, waiting to refund, assuming it's not
   178        // revoked while waiting for confs on an unspent/unexpired taker swap.
   179        if (m.side === Maker) return revokedMatchStatus(intl.ID_MATCH_STATUS_REFUND_PENDING)
   180        // As taker, resolution depends on maker's actions while waiting to refund.
   181        if (m.counterRedeem) return revokedMatchStatus(intl.ID_MATCH_STATUS_REDEEM_PENDING) // this should be very brief if we see the maker's redeem
   182        return revokedMatchStatus(intl.ID_MATCH_STATUS_REFUND_PENDING) // may switch to redeem if maker redeems on the sly
   183      }
   184      if (m.refund) {
   185        return revokedMatchStatus(intl.ID_MATCH_STATUS_REFUNDED)
   186      }
   187      if (m.redeem) {
   188        return revokedMatchStatus(intl.ID_MATCH_STATUS_REDEMPTION_CONFIRMED)
   189      }
   190      return revokedMatchStatus(intl.ID_MATCH_STATUS_COMPLETE) // i.e. we sent no swap
   191    }
   192  
   193    switch (m.status) {
   194      case NewlyMatched:
   195        return intl.prep(intl.ID_MATCH_STATUS_NEWLY_MATCHED)
   196      case MakerSwapCast:
   197        return intl.prep(intl.ID_MATCH_STATUS_MAKER_SWAP_CAST)
   198      case TakerSwapCast:
   199        return intl.prep(intl.ID_MATCH_STATUS_TAKER_SWAP_CAST)
   200      case MakerRedeemed:
   201        if (m.side === Maker) {
   202          return intl.prep(intl.ID_MATCH_STATUS_REDEMPTION_SENT)
   203        }
   204        return intl.prep(intl.ID_MATCH_STATUS_MAKER_REDEEMED)
   205      case MatchComplete:
   206        return intl.prep(intl.ID_MATCH_STATUS_REDEMPTION_SENT)
   207      case MatchConfirmed:
   208        return intl.prep(intl.ID_MATCH_STATUS_REDEMPTION_CONFIRMED)
   209    }
   210    return intl.prep(intl.ID_UNKNOWN)
   211  }
   212  
   213  // revokedMatchStatus is a helper function that returns the revoked match status
   214  // string.
   215  function revokedMatchStatus (matchStatus: string): string {
   216    return intl.prep(intl.ID_MATCH_STATUS_REVOKED, { status: intl.prep(matchStatus) })
   217  }
   218  
   219  /*
   220   * optionElement is a getter for an element matching the *OrderOption from
   221   * client/asset. change is a function with no arguments that is called when the
   222   * returned option's value has changed.
   223   */
   224  export function optionElement (opt: OrderOption, order: TradeForm, change: () => void, isSwap: boolean): HTMLElement {
   225    const isBaseChain = (isSwap && order.sell) || (!isSwap && !order.sell)
   226    const symbol = isBaseChain ? dexAssetSymbol(order.host, order.base) : dexAssetSymbol(order.host, order.quote)
   227  
   228    switch (true) {
   229      case !!opt.boolean:
   230        return new BooleanOption(opt, symbol, order.options, change).node
   231      case !!opt.xyRange:
   232        return new XYRangeOption(opt, symbol, order.options, change).node
   233      default:
   234        console.error('no option type specified', opt)
   235    }
   236    console.error('unknown option type', opt)
   237    return document.createElement('div')
   238  }
   239  
   240  function dexAssetSymbol (host: string, assetID: number): string {
   241    return app().exchanges[host].assets[assetID].symbol
   242  }
   243  
   244  export function isCancellable (ord: Order): boolean {
   245    return ord.type === Limit && ord.tif === StandingTiF && ord.status < StatusExecuted
   246  }
   247  
   248  export function orderTypeText (ordType: number): string {
   249    switch (ordType) {
   250      case OrderTypeLimit:
   251        return intl.prep(intl.ID_LIMIT_ORDER)
   252      case OrderTypeMarket:
   253        return intl.prep(intl.ID_MARKET_ORDER)
   254      default: // OrderTypeCancel
   255        return intl.prep(intl.ID_CANCEL_ORDER)
   256    }
   257  }