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

     1  import {
     2    app,
     3    PageElement,
     4    MarketMakingEvent,
     5    DEXOrderEvent,
     6    CEXOrderEvent,
     7    RunEventNote,
     8    RunStatsNote,
     9    DepositEvent,
    10    WithdrawalEvent,
    11    MarketMakingRunOverview,
    12    SupportedAsset,
    13    BalanceEffects,
    14    MarketWithHost,
    15    ProfitLoss
    16  } from './registry'
    17  import { Forms } from './forms'
    18  import { postJSON } from './http'
    19  import Doc, { setupCopyBtn } from './doc'
    20  import BasePage from './basepage'
    21  import { setMarketElements, liveBotStatus } from './mmutil'
    22  import * as intl from './locales'
    23  import * as wallets from './wallets'
    24  import { CoinExplorers } from './coinexplorers'
    25  
    26  interface LogsPageParams {
    27    host: string
    28    quoteID: number
    29    baseID: number
    30    startTime: number
    31    returnPage: string
    32  }
    33  
    34  let net = 0
    35  
    36  const logsBatchSize = 50
    37  
    38  interface logFilters {
    39    dexSells: boolean
    40    dexBuys: boolean
    41    cexSells: boolean
    42    cexBuys: boolean
    43    deposits: boolean
    44    withdrawals: boolean
    45  }
    46  
    47  function eventPassesFilter (e: MarketMakingEvent, filters: logFilters): boolean {
    48    if (e.dexOrderEvent) {
    49      if (e.dexOrderEvent.sell) return filters.dexSells
    50      return filters.dexBuys
    51    }
    52    if (e.cexOrderEvent) {
    53      if (e.cexOrderEvent.sell) return filters.cexSells
    54      return filters.cexBuys
    55    }
    56    if (e.depositEvent) return filters.deposits
    57    if (e.withdrawalEvent) return filters.withdrawals
    58    return false
    59  }
    60  
    61  export default class MarketMakerLogsPage extends BasePage {
    62    page: Record<string, PageElement>
    63    mkt: MarketWithHost
    64    startTime: number
    65    fiatRates: Record<number, number>
    66    liveBot: boolean
    67    overview: MarketMakingRunOverview
    68    events: Record<number, [MarketMakingEvent, HTMLElement]>
    69    forms: Forms
    70    dexOrderIDCopyListener: () => void | undefined
    71    cexOrderIDCopyListener: () => void | undefined
    72    depositIDCopyListener: () => void | undefined
    73    withdrawalIDCopyListener: () => void | undefined
    74    filters: logFilters
    75    loading: boolean
    76    refID: number | undefined
    77    doneScrolling: boolean
    78    statsRows: Record<number, HTMLElement>
    79  
    80    constructor (main: HTMLElement, params: LogsPageParams) {
    81      super()
    82      const page = this.page = Doc.idDescendants(main)
    83      net = app().user.net
    84      Doc.cleanTemplates(page.eventTableRowTmpl, page.dexOrderTxRowTmpl, page.performanceTableRowTmpl)
    85      Doc.bind(this.page.backButton, 'click', () => { app().loadPage(params.returnPage ?? 'mm') })
    86      Doc.bind(this.page.filterButton, 'click', () => { this.applyFilters() })
    87      if (params?.host) {
    88        const url = new URL(window.location.href)
    89        url.searchParams.set('host', params.host)
    90        url.searchParams.set('baseID', String(params.baseID))
    91        url.searchParams.set('quoteID', String(params.quoteID))
    92        url.searchParams.set('startTime', String(params.startTime))
    93        window.history.replaceState({ page: 'mmsettings', ...params }, '', url)
    94      } else {
    95        const urlParams = new URLSearchParams(window.location.search)
    96        if (!params) params = {} as LogsPageParams
    97        params.host = urlParams.get('host') || ''
    98        params.baseID = parseInt(urlParams.get('baseID') || '0')
    99        params.quoteID = parseInt(urlParams.get('quoteID') || '0')
   100        params.startTime = parseInt(urlParams.get('startTime') || '0')
   101      }
   102      const { baseID, quoteID, host, startTime } = params
   103      this.startTime = startTime
   104      this.forms = new Forms(page.forms)
   105      this.events = {}
   106      this.statsRows = {}
   107      this.mkt = { baseID: baseID, quoteID: quoteID, host }
   108      setMarketElements(main, baseID, quoteID, host)
   109      Doc.bind(main, 'scroll', () => {
   110        if (this.loading) return
   111        if (this.doneScrolling) return
   112        const belowBottom = page.eventsTable.offsetHeight - main.offsetHeight - main.scrollTop
   113        if (belowBottom < 0) {
   114          this.nextPage()
   115        }
   116      })
   117      this.setup(host, baseID, quoteID)
   118    }
   119  
   120    async nextPage () {
   121      this.loading = true
   122      const [events, updatedLogs, overview] = await this.getRunLogs()
   123      const assets = this.mktAssets()
   124      for (const event of events) {
   125        if (this.events[event.id]) continue
   126        const row = this.newEventRow(event, false, assets)
   127        this.events[event.id] = [event, row]
   128      }
   129      this.populateStats(overview.profitLoss, overview.endTime)
   130      this.updateExistingRows(updatedLogs)
   131      this.loading = false
   132    }
   133  
   134    async getRunLogs (): Promise<[MarketMakingEvent[], MarketMakingEvent[], MarketMakingRunOverview]> {
   135      const { mkt, startTime } = this
   136      const req: any = { market: mkt, startTime, n: logsBatchSize, filters: this.filters, refID: this.refID }
   137      const res = await postJSON('/api/mmrunlogs', req)
   138      if (!app().checkResponse(res)) {
   139        console.error('failed to get bot logs', res)
   140      }
   141      if (res.logs.length <= 1) {
   142        this.doneScrolling = true
   143      }
   144      if (res.logs.length > 0) {
   145        this.refID = res.logs[res.logs.length - 1].id
   146      }
   147      return [res.logs, res.updatedLogs || [], res.overview]
   148    }
   149  
   150    async applyFilters () {
   151      const page = this.page
   152      this.filters = {
   153        dexSells: !!page.dexSellsCheckbox.checked,
   154        dexBuys: !!page.dexBuysCheckbox.checked,
   155        cexSells: !!page.cexSellsCheckbox.checked,
   156        cexBuys: !!page.cexBuysCheckbox.checked,
   157        deposits: !!page.depositsCheckbox.checked,
   158        withdrawals: !!page.withdrawalsCheckbox.checked
   159      }
   160      this.refID = undefined
   161      const [events, , overview] = await this.getRunLogs()
   162      this.populateTable(events)
   163      this.populateStats(overview.profitLoss, overview.endTime)
   164    }
   165  
   166    setFilters () {
   167      const page = this.page
   168      page.dexSellsCheckbox.checked = true
   169      page.dexBuysCheckbox.checked = true
   170      page.cexSellsCheckbox.checked = true
   171      page.cexBuysCheckbox.checked = true
   172      page.depositsCheckbox.checked = true
   173      page.withdrawalsCheckbox.checked = true
   174      this.filters = {
   175        dexSells: true,
   176        dexBuys: true,
   177        cexSells: true,
   178        cexBuys: true,
   179        deposits: true,
   180        withdrawals: true
   181      }
   182    }
   183  
   184    async setup (host: string, baseID: number, quoteID: number) {
   185      const page = this.page
   186      this.setFilters()
   187      const { startTime } = this
   188      let profitLoss: ProfitLoss
   189      let endTime = 0
   190      const botStatus = liveBotStatus(host, baseID, quoteID)
   191      const [events, , overview] = await this.getRunLogs()
   192      if (botStatus?.runStats?.startTime === startTime) {
   193        this.liveBot = true
   194        this.fiatRates = app().fiatRatesMap
   195        profitLoss = botStatus.runStats.profitLoss
   196      } else {
   197        this.fiatRates = overview.finalState.fiatRates
   198        profitLoss = overview.profitLoss
   199        endTime = overview.endTime
   200      }
   201      this.populateStats(profitLoss, endTime)
   202      const assets = this.mktAssets()
   203      const parentHeader = page.sumUSDHeader.parentElement
   204      for (const asset of assets) {
   205        const th = document.createElement('th') as PageElement
   206        th.textContent = `${asset.symbol.toUpperCase()} Delta`
   207        if (parentHeader) {
   208          parentHeader.insertBefore(th, page.sumUSDHeader)
   209        }
   210      }
   211      this.populateTable(events)
   212  
   213      app().registerNoteFeeder({
   214        runevent: (note: RunEventNote) => { this.handleRunEventNote(note) },
   215        runstats: (note: RunStatsNote) => { this.handleRunStatsNote(note) }
   216      })
   217    }
   218  
   219    handleRunEventNote (note: RunEventNote) {
   220      const { baseID, quoteID, host } = this.mkt
   221      if (note.host !== host || note.baseID !== baseID || note.quoteID !== quoteID) return
   222      if (!eventPassesFilter(note.event, this.filters)) return
   223      const event = note.event
   224      const cachedEvent = this.events[event.id]
   225      if (cachedEvent) {
   226        this.setRowContents(cachedEvent[1], event, this.mktAssets())
   227        cachedEvent[0] = event
   228        return
   229      }
   230      const row = this.newEventRow(event, true, this.mktAssets())
   231      this.events[event.id] = [event, row]
   232    }
   233  
   234    handleRunStatsNote (note: RunStatsNote) {
   235      const { mkt: { baseID, quoteID, host }, startTime } = this
   236      if (note.host !== host ||
   237        note.baseID !== baseID ||
   238        note.quoteID !== quoteID) return
   239      if (!note.stats || note.stats.startTime !== startTime) return
   240      this.populateStats(note.stats.profitLoss, 0)
   241    }
   242  
   243    populateStats (pl: ProfitLoss, endTime: number) {
   244      const page = this.page
   245      page.startTime.textContent = new Date(this.startTime * 1000).toLocaleString()
   246      if (endTime === 0) {
   247        Doc.hide(page.endTimeRow)
   248      } else {
   249        page.endTime.textContent = new Date(endTime * 1000).toLocaleString()
   250      }
   251      for (const assetID in pl.diffs) {
   252        const asset = app().assets[parseInt(assetID)]
   253        let row = this.statsRows[assetID]
   254        if (!row) {
   255          row = page.performanceTableRowTmpl.cloneNode(true) as HTMLElement
   256          const tmpl = Doc.parseTemplate(row)
   257          tmpl.logo.src = Doc.logoPath(asset.symbol)
   258          tmpl.ticker.textContent = asset.symbol.toUpperCase()
   259          this.statsRows[assetID] = row
   260          page.performanceTableBody.appendChild(row)
   261        }
   262        const diff = pl.diffs[assetID]
   263        const tmpl = Doc.parseTemplate(row)
   264        tmpl.diff.textContent = diff.fmt
   265        tmpl.usdDiff.textContent = diff.fmtUSD
   266        tmpl.fiatRate.textContent = `${Doc.formatFiatValue(this.fiatRates[asset.id])} USD`
   267      }
   268      page.profitLoss.textContent = `${Doc.formatFiatValue(pl.profit)} USD`
   269    }
   270  
   271    mktAssets () : SupportedAsset[] {
   272      const baseAsset = app().assets[this.mkt.baseID]
   273      const quoteAsset = app().assets[this.mkt.quoteID]
   274  
   275      const assets = [baseAsset, quoteAsset]
   276      const assetIDs = { [baseAsset.id]: true, [quoteAsset.id]: true }
   277  
   278      if (baseAsset.token && !assetIDs[baseAsset.token.parentID]) {
   279        const baseTokenAsset = app().assets[baseAsset.token.parentID]
   280        assetIDs[baseTokenAsset.id] = true
   281        assets.push(baseTokenAsset)
   282      }
   283  
   284      if (quoteAsset.token && !assetIDs[quoteAsset.token.parentID]) {
   285        const quoteTokenAsset = app().assets[quoteAsset.token.parentID]
   286        assets.push(quoteTokenAsset)
   287      }
   288  
   289      return assets
   290    }
   291  
   292    updateExistingRows (updatedLogs: MarketMakingEvent[]) {
   293      for (const event of updatedLogs) {
   294        const cachedEvent = this.events[event.id]
   295        if (!cachedEvent) continue
   296        this.setRowContents(cachedEvent[1], event, this.mktAssets())
   297        cachedEvent[0] = event
   298      }
   299    }
   300  
   301    populateTable (events: MarketMakingEvent[]) {
   302      const page = this.page
   303      Doc.empty(page.eventsTableBody)
   304      this.events = {}
   305      this.doneScrolling = false
   306      const assets = this.mktAssets()
   307      for (const event of events) {
   308        const row = this.newEventRow(event, false, assets)
   309        this.events[event.id] = [event, row]
   310      }
   311    }
   312  
   313    setRowContents (row: HTMLElement, event: MarketMakingEvent, assets: SupportedAsset[]) {
   314      const tmpl = Doc.parseTemplate(row)
   315      tmpl.time.textContent = (new Date(event.timestamp * 1000)).toLocaleString()
   316      tmpl.eventType.textContent = this.eventType(event)
   317      let id
   318      if (event.depositEvent) {
   319        id = event.depositEvent.transaction.id
   320      } else if (event.withdrawalEvent) {
   321        id = event.withdrawalEvent.id
   322      } else if (event.dexOrderEvent) {
   323        id = event.dexOrderEvent.id
   324      } else if (event.cexOrderEvent) {
   325        id = event.cexOrderEvent.id
   326      }
   327      if (id) {
   328        tmpl.eventID.textContent = trimStringWithEllipsis(id, 30)
   329        tmpl.eventID.setAttribute('title', id)
   330      }
   331      let usd = 0
   332      for (const asset of assets) {
   333        const be = event.balanceEffects
   334        const sum = sumBalanceEffects(asset.id, be)
   335        const tmplID = `sum${asset.symbol.toUpperCase()}`
   336        let el : PageElement
   337        if (tmpl[tmplID]) {
   338          el = tmpl[tmplID]
   339        } else {
   340          el = document.createElement('td')
   341          el.dataset.tmpl = tmplID
   342          const parent = tmpl.sumUSD.parentElement
   343          if (parent) {
   344            parent.insertBefore(el, tmpl.sumUSD)
   345          }
   346        }
   347        el.textContent = Doc.formatCoinValue(sum, asset.unitInfo)
   348        const factor = asset.unitInfo.conventional.conversionFactor
   349        usd += sum / factor * this.fiatRates[asset.id] || 0
   350      }
   351      tmpl.sumUSD.textContent = Doc.formatFourSigFigs(usd)
   352      Doc.bind(tmpl.details, 'click', () => { this.showEventDetails(event.id) })
   353    }
   354  
   355    newEventRow (event: MarketMakingEvent, prepend: boolean, assets: SupportedAsset[]) : HTMLElement {
   356      const page = this.page
   357      const row = page.eventTableRowTmpl.cloneNode(true) as HTMLElement
   358      row.id = event.id.toString()
   359      this.setRowContents(row, event, assets)
   360      if (prepend) {
   361        page.eventsTableBody.insertBefore(row, page.eventsTableBody.firstChild)
   362      } else {
   363        page.eventsTableBody.appendChild(row)
   364      }
   365      return row
   366    }
   367  
   368    eventType (event: MarketMakingEvent) : string {
   369      if (event.depositEvent) {
   370        return 'Deposit'
   371      } else if (event.withdrawalEvent) {
   372        return 'Withdrawal'
   373      } else if (event.dexOrderEvent) {
   374        return event.dexOrderEvent.sell ? 'DEX Sell' : 'DEX Buy'
   375      } else if (event.cexOrderEvent) {
   376        return event.cexOrderEvent.sell ? 'CEX Sell' : 'CEX Buy'
   377      }
   378  
   379      return ''
   380    }
   381  
   382    showDexOrderEventDetails (event: DEXOrderEvent) {
   383      const { page, mkt: { baseID, quoteID } } = this
   384      const baseAsset = app().assets[baseID]
   385      const quoteAsset = app().assets[quoteID]
   386      const [bui, qui] = [baseAsset.unitInfo, quoteAsset.unitInfo]
   387      const [baseTicker, quoteTicker] = [bui.conventional.unit, qui.conventional.unit]
   388      if (this.dexOrderIDCopyListener !== undefined) {
   389        page.copyDexOrderID.removeEventListener('click', this.dexOrderIDCopyListener)
   390      }
   391      this.dexOrderIDCopyListener = () => { setupCopyBtn(event.id, page.dexOrderID, page.copyDexOrderID, '#1e7d11') }
   392      page.copyDexOrderID.addEventListener('click', this.dexOrderIDCopyListener)
   393      page.dexOrderID.textContent = trimStringWithEllipsis(event.id, 20)
   394      page.dexOrderID.setAttribute('title', event.id)
   395      const rate = app().conventionalRate(baseID, quoteID, event.rate)
   396  
   397      page.dexOrderRate.textContent = `${rate} ${baseTicker}/${quoteTicker}`
   398      page.dexOrderQty.textContent = `${event.qty / bui.conventional.conversionFactor} ${baseTicker}`
   399      if (event.sell) {
   400        page.dexOrderSide.textContent = intl.prep(intl.ID_SELL)
   401      } else {
   402        page.dexOrderSide.textContent = intl.prep(intl.ID_BUY)
   403      }
   404      Doc.empty(page.dexOrderTxsTableBody)
   405      Doc.setVis(event.transactions && event.transactions.length > 0, page.dexOrderTxsTable)
   406      const txAsset = (txType: number, sell: boolean) : SupportedAsset | undefined => {
   407        switch (txType) {
   408          case wallets.txTypeSwap:
   409          case wallets.txTypeRefund:
   410          case wallets.txTypeSplit:
   411            return sell ? baseAsset : quoteAsset
   412          case wallets.txTypeRedeem:
   413            return sell ? quoteAsset : baseAsset
   414        }
   415      }
   416  
   417      for (let i = 0; event.transactions && i < event.transactions.length; i++) {
   418        const tx = event.transactions[i]
   419        const row = page.dexOrderTxRowTmpl.cloneNode(true) as HTMLElement
   420        const tmpl = Doc.parseTemplate(row)
   421        tmpl.id.textContent = trimStringWithEllipsis(tx.id, 20)
   422        tmpl.id.setAttribute('title', tx.id)
   423        tmpl.type.textContent = wallets.txTypeString(tx.type)
   424        const asset = txAsset(tx.type, event.sell)
   425        if (!asset) {
   426          console.error('unexpected tx type in dex order event', tx.type)
   427          continue
   428        }
   429        const assetExplorer = CoinExplorers[asset.id]
   430        if (assetExplorer && assetExplorer[net]) {
   431          tmpl.explorerLink.href = assetExplorer[net](tx.id)
   432        }
   433        tmpl.amt.textContent = `${Doc.formatCoinValue(tx.amount, asset.unitInfo)} ${asset.unitInfo.conventional.unit.toLowerCase()}`
   434        tmpl.fees.textContent = `${Doc.formatCoinValue(tx.fees, asset.unitInfo)} ${asset.unitInfo.conventional.unit.toLowerCase()}`
   435        page.dexOrderTxsTableBody.appendChild(row)
   436      }
   437      this.forms.show(page.dexOrderDetailsForm)
   438    }
   439  
   440    showCexOrderEventDetails (event: CEXOrderEvent) {
   441      const { page, mkt: { baseID, quoteID } } = this
   442      const baseAsset = app().assets[baseID]
   443      const quoteAsset = app().assets[quoteID]
   444      const [bui, qui] = [baseAsset.unitInfo, quoteAsset.unitInfo]
   445      const [baseTicker, quoteTicker] = [bui.conventional.unit, qui.conventional.unit]
   446  
   447      page.cexOrderID.textContent = trimStringWithEllipsis(event.id, 20)
   448      if (this.cexOrderIDCopyListener !== undefined) {
   449        page.copyCexOrderID.removeEventListener('click', this.cexOrderIDCopyListener)
   450      }
   451      this.cexOrderIDCopyListener = () => { setupCopyBtn(event.id, page.cexOrderID, page.copyCexOrderID, '#1e7d11') }
   452      page.copyCexOrderID.addEventListener('click', this.cexOrderIDCopyListener)
   453      page.cexOrderID.setAttribute('title', event.id)
   454      const rate = app().conventionalRate(baseID, quoteID, event.rate)
   455      page.cexOrderRate.textContent = `${rate} ${baseTicker}/${quoteTicker}`
   456      page.cexOrderQty.textContent = `${event.qty / bui.conventional.conversionFactor} ${baseTicker}`
   457      if (event.sell) {
   458        page.cexOrderSide.textContent = intl.prep(intl.ID_SELL)
   459      } else {
   460        page.cexOrderSide.textContent = intl.prep(intl.ID_BUY)
   461      }
   462      page.cexOrderBaseFilled.textContent = `${event.baseFilled / bui.conventional.conversionFactor} ${baseTicker}`
   463      page.cexOrderQuoteFilled.textContent = `${event.quoteFilled / qui.conventional.conversionFactor} ${quoteTicker}`
   464      this.forms.show(page.cexOrderDetailsForm)
   465    }
   466  
   467    showDepositEventDetails (event: DepositEvent, pending: boolean) {
   468      const page = this.page
   469      page.depositID.textContent = trimStringWithEllipsis(event.transaction.id, 20)
   470      if (this.depositIDCopyListener !== undefined) {
   471        page.copyDepositID.removeEventListener('click', this.depositIDCopyListener)
   472      }
   473      this.depositIDCopyListener = () => { setupCopyBtn(event.transaction.id, page.depositID, page.copyDepositID, '#1e7d11') }
   474      page.copyDepositID.addEventListener('click', this.depositIDCopyListener)
   475      page.depositID.setAttribute('title', event.transaction.id)
   476      const unitInfo = app().assets[event.assetID].unitInfo
   477      const unit = unitInfo.conventional.unit
   478      page.depositAmt.textContent = `${Doc.formatCoinValue(event.transaction.amount, unitInfo)} ${unit}`
   479      page.depositFees.textContent = `${Doc.formatCoinValue(event.transaction.fees, unitInfo)} ${unit}`
   480      page.depositStatus.textContent = pending ? intl.prep(intl.ID_PENDING) : intl.prep(intl.ID_COMPLETE)
   481      Doc.setVis(!pending, page.depositCreditSection)
   482      if (!pending) {
   483        page.depositCredit.textContent = `${Doc.formatCoinValue(event.cexCredit, unitInfo)} ${unit}`
   484      }
   485      this.forms.show(page.depositDetailsForm)
   486    }
   487  
   488    showWithdrawalEventDetails (event: WithdrawalEvent, pending: boolean) {
   489      const page = this.page
   490      page.withdrawalID.textContent = trimStringWithEllipsis(event.id, 20)
   491      if (this.withdrawalIDCopyListener !== undefined) {
   492        page.copyWithdrawalID.removeEventListener('click', this.withdrawalIDCopyListener)
   493      }
   494      this.withdrawalIDCopyListener = () => { setupCopyBtn(event.id, page.withdrawalID, page.copyWithdrawalID, '#1e7d11') }
   495      page.copyWithdrawalID.addEventListener('click', this.withdrawalIDCopyListener)
   496      page.withdrawalID.setAttribute('title', event.id)
   497      const unitInfo = app().assets[event.assetID].unitInfo
   498      const unit = unitInfo.conventional.unit
   499      page.withdrawalAmt.textContent = `${Doc.formatCoinValue(event.cexDebit, unitInfo)} ${unit}`
   500      page.withdrawalStatus.textContent = pending ? intl.prep(intl.ID_PENDING) : intl.prep(intl.ID_COMPLETE)
   501      if (event.transaction) {
   502        page.withdrawalTxID.textContent = trimStringWithEllipsis(event.transaction.id, 20)
   503        page.withdrawalTxID.setAttribute('title', event.transaction.id)
   504        page.withdrawalReceived.textContent = `${Doc.formatCoinValue(event.transaction.amount, unitInfo)} ${unit}`
   505      }
   506      this.forms.show(page.withdrawalDetailsForm)
   507    }
   508  
   509    showEventDetails (eventID: number) {
   510      const [event] = this.events[eventID]
   511      if (event.dexOrderEvent) this.showDexOrderEventDetails(event.dexOrderEvent)
   512      if (event.cexOrderEvent) this.showCexOrderEventDetails(event.cexOrderEvent)
   513      if (event.depositEvent) this.showDepositEventDetails(event.depositEvent, event.pending)
   514      if (event.withdrawalEvent) this.showWithdrawalEventDetails(event.withdrawalEvent, event.pending)
   515    }
   516  }
   517  
   518  function trimStringWithEllipsis (str: string, maxLen: number): string {
   519    if (str.length <= maxLen) return str
   520    return `${str.substring(0, maxLen / 2)}...${str.substring(str.length - maxLen / 2)}`
   521  }
   522  
   523  function sumBalanceEffects (assetID: number, be: BalanceEffects): number {
   524    let sum = 0
   525    if (be.settled[assetID]) sum += be.settled[assetID]
   526    if (be.pending[assetID]) sum += be.pending[assetID]
   527    if (be.locked[assetID]) sum += be.locked[assetID]
   528    if (be.reserved[assetID]) sum += be.reserved[assetID]
   529    return sum
   530  }