decred.org/dcrdex@v1.0.5/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 }