diff --git a/client/mm/event_log.go b/client/mm/event_log.go index 9871af4ee3..13d523ec1f 100644 --- a/client/mm/event_log.go +++ b/client/mm/event_log.go @@ -117,7 +117,7 @@ type eventLogDB interface { // all of the events will be returned. If refID is not nil, the events // including and after the event with the ID will be returned. If // pendingOnly is true, only pending events will be returned. - runEvents(startTime int64, mkt *MarketWithHost, n uint64, refID *uint64, pendingOnly bool) ([]*MarketMakingEvent, error) + runEvents(startTime int64, mkt *MarketWithHost, n uint64, refID *uint64, pendingOnly bool, filters *RunLogFilters) ([]*MarketMakingEvent, error) } // eventUpdate is used to asynchronously add events to the event log. @@ -623,7 +623,7 @@ func decodeMarketMakingEvent(eventB []byte) (*MarketMakingEvent, error) { // events will be returned. If refID is not nil, the events including and after the // event with the ID will be returned. If pendingOnly is true, only pending events // will be returned. -func (db *boltEventLogDB) runEvents(startTime int64, mkt *MarketWithHost, n uint64, refID *uint64, pendingOnly bool) ([]*MarketMakingEvent, error) { +func (db *boltEventLogDB) runEvents(startTime int64, mkt *MarketWithHost, n uint64, refID *uint64, pendingOnly bool, filter *RunLogFilters) ([]*MarketMakingEvent, error) { events := make([]*MarketMakingEvent, 0, 32) return events, db.View(func(tx *bbolt.Tx) error { @@ -667,6 +667,10 @@ func (db *boltEventLogDB) runEvents(startTime int64, mkt *MarketWithHost, n uint continue } + if !filter.filter(e) { + continue + } + events = append(events, e) if n > 0 && uint64(len(events)) >= n { break diff --git a/client/mm/exchange_adaptor.go b/client/mm/exchange_adaptor.go index bc42233e59..ff19bac19d 100644 --- a/client/mm/exchange_adaptor.go +++ b/client/mm/exchange_adaptor.go @@ -3236,6 +3236,7 @@ type ProfitLoss struct { ModsUSD float64 `json:"modsUSD"` Final map[uint32]*Amount `json:"final"` FinalUSD float64 `json:"finalUSD"` + Diffs map[uint32]*Amount `json:"diffs"` Profit float64 `json:"profit"` ProfitRatio float64 `json:"profitRatio"` } @@ -3250,6 +3251,7 @@ func newProfitLoss( pl := &ProfitLoss{ Initial: make(map[uint32]*Amount, len(initialBalances)), Mods: make(map[uint32]*Amount, len(mods)), + Diffs: make(map[uint32]*Amount, len(initialBalances)), Final: make(map[uint32]*Amount, len(finalBalances)), } for assetID, v := range initialBalances { @@ -3262,6 +3264,8 @@ func newProfitLoss( mod := NewAmount(assetID, mods[assetID], fiatRate) pl.InitialUSD += init.USD pl.ModsUSD += mod.USD + diff := int64(finalBalances[assetID]) - int64(initialBalances[assetID]) - mods[assetID] + pl.Diffs[assetID] = NewAmount(assetID, diff, fiatRate) } for assetID, v := range finalBalances { if v == 0 { diff --git a/client/mm/mm.go b/client/mm/mm.go index 08f49d8770..99da4c67a6 100644 --- a/client/mm/mm.go +++ b/client/mm/mm.go @@ -1237,25 +1237,72 @@ func (m *MarketMaker) updatePendingEvent(mkt *MarketWithHost, event *MarketMakin } } +type RunLogFilters struct { + DexBuys bool `json:"dexBuys"` + DexSells bool `json:"dexSells"` + CexBuys bool `json:"cexBuys"` + CexSells bool `json:"cexSells"` + Deposits bool `json:"deposits"` + Withdrawals bool `json:"withdrawals"` +} + +func (f *RunLogFilters) filter(event *MarketMakingEvent) bool { + switch { + case event.DEXOrderEvent != nil: + if event.DEXOrderEvent.Sell { + return f.DexSells + } + return f.DexBuys + case event.CEXOrderEvent != nil: + if event.CEXOrderEvent.Sell { + return f.CexSells + } + return f.CexBuys + case event.DepositEvent != nil: + return f.Deposits + case event.WithdrawalEvent != nil: + return f.Withdrawals + default: + return false + } +} + +var noFilters = &RunLogFilters{ + DexBuys: true, + DexSells: true, + CexBuys: true, + CexSells: true, + Deposits: true, + Withdrawals: true, +} + // RunLogs returns the event logs of a market making run. At most n events are // returned, if n == 0 then all events are returned. If refID is not nil, then // the events including and after refID are returned. -func (m *MarketMaker) RunLogs(startTime int64, mkt *MarketWithHost, n uint64, refID *uint64) ([]*MarketMakingEvent, *MarketMakingRunOverview, error) { +// Updated events are events that were updated from pending to confirmed during +// this call. For completed runs, on each call to RunLogs, all pending events are +// checked for updates, and anything that was updated is returned. +func (m *MarketMaker) RunLogs(startTime int64, mkt *MarketWithHost, n uint64, refID *uint64, filters *RunLogFilters) (events, updatedEvents []*MarketMakingEvent, overview *MarketMakingRunOverview, err error) { var running bool runningBotsLookup := m.runningBotsLookup() if bot, found := runningBotsLookup[*mkt]; found { running = bot.timeStart() == startTime } + if filters == nil { + filters = noFilters + } + if !running { - pendingEvents, err := m.eventLogDB.runEvents(startTime, mkt, 0, nil, true) + pendingEvents, err := m.eventLogDB.runEvents(startTime, mkt, 0, nil, true, noFilters) if err != nil { - return nil, nil, err + return nil, nil, nil, err } if len(pendingEvents) > 0 { + updatedEvents = make([]*MarketMakingEvent, 0, len(pendingEvents)) overview, err := m.eventLogDB.runOverview(startTime, mkt) if err != nil { - return nil, nil, err + return nil, nil, nil, err } for _, event := range pendingEvents { if event.Pending { @@ -1264,23 +1311,24 @@ func (m *MarketMaker) RunLogs(startTime int64, mkt *MarketWithHost, n uint64, re m.log.Errorf("Error updating pending event: %v", err) continue } + updatedEvents = append(updatedEvents, updatedEvent) m.eventLogDB.storeEvent(startTime, mkt, updatedEvent, nil) } } } } - events, err := m.eventLogDB.runEvents(startTime, mkt, n, refID, false) + events, err = m.eventLogDB.runEvents(startTime, mkt, n, refID, false, filters) if err != nil { - return nil, nil, err + return nil, nil, nil, err } - overview, err := m.eventLogDB.runOverview(startTime, mkt) + overview, err = m.eventLogDB.runOverview(startTime, mkt) if err != nil { - return nil, nil, err + return nil, nil, nil, err } - return events, overview, nil + return events, updatedEvents, overview, nil } // CEXBook generates a snapshot of the specified CEX order book. diff --git a/client/webserver/api.go b/client/webserver/api.go index 3ebd696cc3..9979eff4bb 100644 --- a/client/webserver/api.go +++ b/client/webserver/api.go @@ -1745,25 +1745,28 @@ func (s *WebServer) apiRunLogs(w http.ResponseWriter, r *http.Request) { Market *mm.MarketWithHost `json:"market"` N uint64 `json:"n"` RefID *uint64 `json:"refID,omitempty"` + Filters *mm.RunLogFilters `json:"filters,omitempty"` } if !readPost(w, r, &req) { return } - logs, overview, err := s.mm.RunLogs(req.StartTime, req.Market, req.N, req.RefID) + logs, updatedLogs, overview, err := s.mm.RunLogs(req.StartTime, req.Market, req.N, req.RefID, req.Filters) if err != nil { s.writeAPIError(w, fmt.Errorf("error getting run logs: %w", err)) return } writeJSON(w, &struct { - OK bool `json:"ok"` - Overview *mm.MarketMakingRunOverview `json:"overview"` - Logs []*mm.MarketMakingEvent `json:"logs"` + OK bool `json:"ok"` + Overview *mm.MarketMakingRunOverview `json:"overview"` + Logs []*mm.MarketMakingEvent `json:"logs"` + UpdatedLogs []*mm.MarketMakingEvent `json:"updatedLogs"` }{ - OK: true, - Overview: overview, - Logs: logs, + OK: true, + Overview: overview, + Logs: logs, + UpdatedLogs: updatedLogs, }) } diff --git a/client/webserver/locales/en-us.go b/client/webserver/locales/en-us.go index 831796a7ae..73b627f802 100644 --- a/client/webserver/locales/en-us.go +++ b/client/webserver/locales/en-us.go @@ -622,4 +622,15 @@ var EnUS = map[string]*intl.Translation{ "remotegap_tooltip": {T: "The buy-sell spread on the linked cex market"}, "max_zero_no_fees": {T: ` balance < min fees ~`}, "max_zero_no_bal": {T: `low balance`}, + "balance_diff": {T: "Balance Diff"}, + "usd_diff": {T: "USD Diff"}, + "usd_rate": {T: "USD Rate"}, + "dex_sell": {T: "DEX Sell"}, + "dex_buy": {T: "DEX Buy"}, + "cex_sell": {T: "CEX Sell"}, + "cex_buy": {T: "CEX Buy"}, + "deposit": {T: "Deposit"}, + "withdrawal": {T: "Withdrawal"}, + "filters": {T: "Filters"}, + "Apply": {T: "Apply"}, } diff --git a/client/webserver/site/src/css/components.scss b/client/webserver/site/src/css/components.scss index 44b2e7998d..af2a3b3d3e 100644 --- a/client/webserver/site/src/css/components.scss +++ b/client/webserver/site/src/css/components.scss @@ -291,6 +291,7 @@ div[data-handler=order], div[data-handler=orders], div[data-handler=register], div[data-handler=settings], +div[data-handler=mmlogs], div[data-handler=wallets] { #forms>form:not(.plain) { border-radius: 5px; diff --git a/client/webserver/site/src/css/mm.scss b/client/webserver/site/src/css/mm.scss index 81c621b7d1..7aefedaaaf 100644 --- a/client/webserver/site/src/css/mm.scss +++ b/client/webserver/site/src/css/mm.scss @@ -6,6 +6,15 @@ div[data-handler=mm] { width: 300px; } + #eventsTablePanel { + & > div { + @include stylish-overflow; + + height: 100%; + overflow-x: hidden; + } + } + .gap-factor-input, .lots-input { max-width: 75px; diff --git a/client/webserver/site/src/html/mmarchives.tmpl b/client/webserver/site/src/html/mmarchives.tmpl index 33997ef552..902a15ccb3 100644 --- a/client/webserver/site/src/html/mmarchives.tmpl +++ b/client/webserver/site/src/html/mmarchives.tmpl @@ -8,29 +8,31 @@
[[[previous_mm_runs]]]
- - - - - - - - - - - - - - - - - -
[[[start_time]]][[[Host]]][[[Market]]][[[logs]]][[[Settings]]]
- - - - - -
+
+ + + + + + + + + + + + + + + + + +
[[[start_time]]][[[Host]]][[[Market]]][[[logs]]][[[Settings]]]
+ + - + + +
+
{{template "bottom"}} diff --git a/client/webserver/site/src/html/mmlogs.tmpl b/client/webserver/site/src/html/mmlogs.tmpl index 28a8354eb9..9795beb50a 100644 --- a/client/webserver/site/src/html/mmlogs.tmpl +++ b/client/webserver/site/src/html/mmlogs.tmpl @@ -1,6 +1,7 @@ {{define "mmlogs"}} {{template "top" .}} -
+
+
@@ -8,68 +9,129 @@
[[[logs_for]]] - - @ + @
-
-
-
-
[[[start_time]]]
-
-
-
-
[[[end_time]]]
-
-
-
-
[[[profit_loss]]]
-
-
+ +
+
+
+ + + + + + + + + + + +
[[[start_time]]]
[[[end_time]]]
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
[[[Asset]]][[[balance_diff]]][[[usd_rate]]][[[usd_diff]]]
[[[profit_loss]]]
+
+ +
+
[[[filters]]]
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
- - - - - - - - - - - - - - - - - -
[[[Time]]][[[Type]]][[[ID]]]Sum USD[[[Details]]]
+
+ + + + + + + + + + + + + + + + + +
[[[Time]]][[[Type]]][[[ID]]]Sum USD[[[Details]]]
+
- +
-
+
-
- [[[dex_order_details]]] -
-
- - [[[ID]]] - - - - [[[Rate]]] - - - - [[[Quantity]]] - - - - [[[Side]]] - - +
[[[dex_order_details]]]
+
+ + + + + + + + + + + + + + + + + +
[[[ID]]]
[[[Rate]]]
[[[Quantity]]]
[[[Side]]]
@@ -79,7 +141,7 @@ - + @@ -89,94 +151,98 @@ - +
-
- [[[cex_order_details]]] -
-
- - [[[ID]]] - - - - [[[Rate]]] - - - - [[[Quantity]]] - - - - [[[Side]]] - - - - [[[base_filled]]] - - - - [[[quote_filled]]] - - +
[[[cex_order_details]]]
+
+
[[[ID]]]
+ + + + + + + + + + + + + + + + + + + + + + + + +
[[[ID]]]
[[[Rate]]]
[[[Quantity]]]
[[[Side]]]
[[[base_filled]]]
[[[quote_filled]]]
-
+
[[[deposit_details]]]
-
- - [[[ID]]] - - - - [[[Amount]]] - - - - [[[Fees]]] - - - - [[[Status]]] - - - - [[[credited_amt]]] - - +
+ + + + + + + + + + + + + + + + + + + + + +
[[[ID]]]
[[[Amount]]]
[[[Fees]]]
[[[Status]]]
[[[credited_amt]]]
-
+
[[[withdrawal_details]]]
-
- - [[[ID]]] - - - - [[[Amount]]] - - - - [[[Status]]] - - - - [[[tx_id]]] - - - - [[[amt_received]]] - - +
+ + + + + + + + + + + + + + + + + + + + + +
[[[ID]]]
[[[Amount]]]
[[[Status]]]
[[[tx_id]]]
[[[amt_received]]]
diff --git a/client/webserver/site/src/js/doc.ts b/client/webserver/site/src/js/doc.ts index 7495df7243..8c7372fc2b 100644 --- a/client/webserver/site/src/js/doc.ts +++ b/client/webserver/site/src/js/doc.ts @@ -1117,3 +1117,21 @@ export function clamp (v: number, min: number, max: number): number { if (v > max) return max return v } + +export async function setupCopyBtn (txt: string, textEl: PageElement, btnEl: PageElement, color: string) { + console.log('copying txt:', txt) + try { + await navigator.clipboard.writeText(txt) + } catch (err) { + console.error('Unable to copy: ', err) + } + const textOriginalColor = textEl.style.color + const btnOriginalColor = btnEl.style.color + console.log('original colors', textOriginalColor, btnOriginalColor) + textEl.style.color = color + btnEl.style.color = color + setTimeout(() => { + textEl.style.color = textOriginalColor + btnEl.style.color = btnOriginalColor + }, 350) +} diff --git a/client/webserver/site/src/js/mmlogs.ts b/client/webserver/site/src/js/mmlogs.ts index 2051b3abef..93527279d3 100644 --- a/client/webserver/site/src/js/mmlogs.ts +++ b/client/webserver/site/src/js/mmlogs.ts @@ -1,7 +1,6 @@ import { app, PageElement, - UnitInfo, MarketMakingEvent, DEXOrderEvent, CEXOrderEvent, @@ -13,15 +12,17 @@ import { MarketMakingRunOverview, SupportedAsset, BalanceEffects, - MarketWithHost + MarketWithHost, + ProfitLoss } from './registry' import { Forms } from './forms' import { postJSON } from './http' -import Doc from './doc' +import Doc, { setupCopyBtn } from './doc' import BasePage from './basepage' import { setMarketElements, liveBotStatus } from './mmutil' import * as intl from './locales' import * as wallets from './wallets' +import { CoinExplorers } from './coinexplorers' interface LogsPageParams { host: string @@ -30,6 +31,33 @@ interface LogsPageParams { startTime: number } +let net = 0 + +const logsBatchSize = 50 + +interface logFilters { + dexSells: boolean + dexBuys: boolean + cexSells: boolean + cexBuys: boolean + deposits: boolean + withdrawals: boolean +} + +function eventPassesFilter (e: MarketMakingEvent, filters: logFilters): boolean { + if (e.dexOrderEvent) { + if (e.dexOrderEvent.sell) return filters.dexSells + return filters.dexBuys + } + if (e.cexOrderEvent) { + if (e.cexOrderEvent.sell) return filters.cexSells + return filters.cexBuys + } + if (e.depositEvent) return filters.deposits + if (e.withdrawalEvent) return filters.withdrawals + return false +} + export default class MarketMakerLogsPage extends BasePage { page: Record mkt: MarketWithHost @@ -37,14 +65,25 @@ export default class MarketMakerLogsPage extends BasePage { fiatRates: Record runStats: RunStats overview: MarketMakingRunOverview - events: Record + events: Record forms: Forms + dexOrderIDCopyListener: () => void | undefined + cexOrderIDCopyListener: () => void | undefined + depositIDCopyListener: () => void | undefined + withdrawalIDCopyListener: () => void | undefined + filters: logFilters + loading: boolean + refID: number | undefined + doneScrolling: boolean + statsRows: Record constructor (main: HTMLElement, params: LogsPageParams) { super() const page = this.page = Doc.idDescendants(main) - Doc.cleanTemplates(page.eventTableRowTmpl, page.dexOrderTxRowTmpl) + net = app().user.net + Doc.cleanTemplates(page.eventTableRowTmpl, page.dexOrderTxRowTmpl, page.performanceTableRowTmpl) Doc.bind(this.page.backButton, 'click', () => { app().loadPage(this.runStats ? 'mm' : 'mmarchives') }) + Doc.bind(this.page.filterButton, 'click', () => { this.applyFilters() }) if (params?.host) { const url = new URL(window.location.href) url.searchParams.set('host', params.host) @@ -64,37 +103,113 @@ export default class MarketMakerLogsPage extends BasePage { this.startTime = startTime this.forms = new Forms(page.forms) this.events = {} + this.statsRows = {} this.mkt = { base: baseID, quote: quoteID, host } setMarketElements(main, baseID, quoteID, host) + Doc.bind(main, 'scroll', () => { + if (this.loading) return + if (this.doneScrolling) return + const belowBottom = page.eventsTable.offsetHeight - main.offsetHeight - main.scrollTop + console.log(page.eventsTable.offsetHeight, main.offsetHeight, main.scrollTop) + if (belowBottom < 0) { + this.nextPage() + } + }) this.setup(host, baseID, quoteID) } - async getRunLogs (): Promise<[MarketMakingEvent[], MarketMakingRunOverview]> { + async nextPage () { + this.loading = true + const [events, updatedLogs, overview] = await this.getRunLogs() + const assets = this.mktAssets() + for (const event of events) { + if (this.events[event.id]) continue + const row = this.newEventRow(event, false, assets) + this.events[event.id] = [event, row] + } + this.populateStats(overview.profitLoss, overview.endTime) + this.updateExistingRows(updatedLogs) + this.loading = false + } + + async getRunLogs (): Promise<[MarketMakingEvent[], MarketMakingEvent[], MarketMakingRunOverview]> { const { mkt, startTime } = this - const req: any = { market: mkt, startTime } + const req: any = { market: mkt, startTime, n: logsBatchSize, filters: this.filters, refID: this.refID } const res = await postJSON('/api/mmrunlogs', req) if (!app().checkResponse(res)) { console.error('failed to get bot logs', res) } - return [res.logs, res.overview] + if (res.logs.length <= 1) { + this.doneScrolling = true + } + if (res.logs.length > 0) { + this.refID = res.logs[res.logs.length - 1].id + } + return [res.logs, res.updatedLogs || [], res.overview] + } + + async applyFilters () { + const page = this.page + this.filters = { + dexSells: !!page.dexSellsCheckbox.checked, + dexBuys: !!page.dexBuysCheckbox.checked, + cexSells: !!page.cexSellsCheckbox.checked, + cexBuys: !!page.cexBuysCheckbox.checked, + deposits: !!page.depositsCheckbox.checked, + withdrawals: !!page.withdrawalsCheckbox.checked + } + this.refID = undefined + const [events, , overview] = await this.getRunLogs() + this.populateTable(events) + this.populateStats(overview.profitLoss, overview.endTime) + } + + setFilters () { + const page = this.page + page.dexSellsCheckbox.checked = true + page.dexBuysCheckbox.checked = true + page.cexSellsCheckbox.checked = true + page.cexBuysCheckbox.checked = true + page.depositsCheckbox.checked = true + page.withdrawalsCheckbox.checked = true + this.filters = { + dexSells: true, + dexBuys: true, + cexSells: true, + cexBuys: true, + deposits: true, + withdrawals: true + } } async setup (host: string, baseID: number, quoteID: number) { + const page = this.page + this.setFilters() const { startTime } = this - let profit = 0 + let profitLoss: ProfitLoss let endTime = 0 const botStatus = liveBotStatus(host, baseID, quoteID) - const [events, overview] = await this.getRunLogs() + const [events, , overview] = await this.getRunLogs() if (botStatus?.runStats?.startTime === startTime) { this.fiatRates = app().fiatRatesMap - profit = botStatus.runStats.profitLoss.profit + profitLoss = botStatus.runStats.profitLoss } else { this.fiatRates = overview.finalState.fiatRates - profit = overview.profitLoss.profit + profitLoss = overview.profitLoss endTime = overview.endTime } - this.populateStats(profit, endTime) + this.populateStats(profitLoss, endTime) + const assets = this.mktAssets() + const parentHeader = page.sumUSDHeader.parentElement + for (const asset of assets) { + const th = document.createElement('th') as PageElement + th.textContent = `${asset.symbol.toUpperCase()} Delta` + if (parentHeader) { + parentHeader.insertBefore(th, page.sumUSDHeader) + } + } this.populateTable(events) + app().registerNoteFeeder({ runevent: (note: RunEventNote) => { this.handleRunEventNote(note) }, runstats: (note: RunStatsNote) => { this.handleRunStatsNote(note) } @@ -104,17 +219,16 @@ export default class MarketMakerLogsPage extends BasePage { handleRunEventNote (note: RunEventNote) { const { base, quote, host } = this.mkt if (note.host !== host || note.base !== base || note.quote !== quote) return - const page = this.page + if (!eventPassesFilter(note.event, this.filters)) return const event = note.event - this.events[event.id] = event - for (let i = 0; i < page.eventsTableBody.children.length; i++) { - const row = page.eventsTableBody.children[i] as HTMLElement - if (row.id === event.id.toString()) { - this.setRowContents(row, event, this.mktAssets()) - return - } + const cachedEvent = this.events[event.id] + if (cachedEvent) { + this.setRowContents(cachedEvent[1], event, this.mktAssets()) + cachedEvent[0] = event + return } - this.newEventRow(event, true, this.mktAssets()) + const row = this.newEventRow(event, true, this.mktAssets()) + this.events[event.id] = [event, row] } handleRunStatsNote (note: RunStatsNote) { @@ -123,18 +237,35 @@ export default class MarketMakerLogsPage extends BasePage { note.baseID !== base || note.quoteID !== quote) return if (!note.stats || note.stats.startTime !== startTime) return - this.page.profitLoss.textContent = `$${Doc.formatFiatValue(note.stats.profitLoss.profit)}` + this.populateStats(note.stats.profitLoss, 0) } - populateStats (profitLoss: number, endTime: number) { + populateStats (pl: ProfitLoss, endTime: number) { const page = this.page page.startTime.textContent = new Date(this.startTime * 1000).toLocaleString() if (endTime === 0) { - Doc.hide(page.endTimeBlock) + Doc.hide(page.endTimeRow) } else { page.endTime.textContent = new Date(endTime * 1000).toLocaleString() } - page.profitLoss.textContent = `$${Doc.formatFiatValue(profitLoss)}` + for (const assetID in pl.diffs) { + const asset = app().assets[parseInt(assetID)] + let row = this.statsRows[assetID] + if (!row) { + row = page.performanceTableRowTmpl.cloneNode(true) as HTMLElement + const tmpl = Doc.parseTemplate(row) + tmpl.logo.src = Doc.logoPath(asset.symbol) + tmpl.ticker.textContent = asset.symbol.toUpperCase() + this.statsRows[assetID] = row + page.performanceTableBody.appendChild(row) + } + const diff = pl.diffs[assetID] + const tmpl = Doc.parseTemplate(row) + tmpl.diff.textContent = diff.fmt + tmpl.usdDiff.textContent = diff.fmtUSD + tmpl.fiatRate.textContent = `${Doc.formatFiatValue(this.fiatRates[asset.id])} USD` + } + page.profitLoss.textContent = `${Doc.formatFiatValue(pl.profit)} USD` } mktAssets () : SupportedAsset[] { @@ -158,25 +289,24 @@ export default class MarketMakerLogsPage extends BasePage { return assets } + updateExistingRows (updatedLogs: MarketMakingEvent[]) { + for (const event of updatedLogs) { + const cachedEvent = this.events[event.id] + if (!cachedEvent) continue + this.setRowContents(cachedEvent[1], event, this.mktAssets()) + cachedEvent[0] = event + } + } + populateTable (events: MarketMakingEvent[]) { const page = this.page Doc.empty(page.eventsTableBody) - + this.events = {} + this.doneScrolling = false const assets = this.mktAssets() - - const parentHeader = page.sumUSDHeader.parentElement - for (const asset of assets) { - const th = document.createElement('th') as PageElement - th.textContent = `${asset.symbol.toUpperCase()} Delta` - console.log(parentHeader, page.sumUSDHeader) - if (parentHeader) { - parentHeader.insertBefore(th, page.sumUSDHeader) - } - } - for (const event of events) { - this.events[event.id] = event - this.newEventRow(event, false, assets) + const row = this.newEventRow(event, false, assets) + this.events[event.id] = [event, row] } } @@ -222,7 +352,7 @@ export default class MarketMakerLogsPage extends BasePage { Doc.bind(tmpl.details, 'click', () => { this.showEventDetails(event.id) }) } - newEventRow (event: MarketMakingEvent, prepend: boolean, assets: SupportedAsset[]) { + newEventRow (event: MarketMakingEvent, prepend: boolean, assets: SupportedAsset[]) : HTMLElement { const page = this.page const row = page.eventTableRowTmpl.cloneNode(true) as HTMLElement row.id = event.id.toString() @@ -232,6 +362,7 @@ export default class MarketMakerLogsPage extends BasePage { } else { page.eventsTableBody.appendChild(row) } + return row } eventType (event: MarketMakingEvent) : string { @@ -240,9 +371,9 @@ export default class MarketMakerLogsPage extends BasePage { } else if (event.withdrawalEvent) { return 'Withdrawal' } else if (event.dexOrderEvent) { - return 'DEX Order' + return event.dexOrderEvent.sell ? 'DEX Sell' : 'DEX Buy' } else if (event.cexOrderEvent) { - return 'CEX Order' + return event.cexOrderEvent.sell ? 'CEX Sell' : 'CEX Buy' } return '' @@ -254,7 +385,11 @@ export default class MarketMakerLogsPage extends BasePage { const quoteAsset = app().assets[quote] const [bui, qui] = [baseAsset.unitInfo, quoteAsset.unitInfo] const [baseTicker, quoteTicker] = [bui.conventional.unit, qui.conventional.unit] - + if (this.dexOrderIDCopyListener !== undefined) { + page.copyDexOrderID.removeEventListener('click', this.dexOrderIDCopyListener) + } + this.dexOrderIDCopyListener = () => { setupCopyBtn(event.id, page.dexOrderID, page.copyDexOrderID, '#1e7d11') } + page.copyDexOrderID.addEventListener('click', this.dexOrderIDCopyListener) page.dexOrderID.textContent = trimStringWithEllipsis(event.id, 20) page.dexOrderID.setAttribute('title', event.id) const rate = app().conventionalRate(base, quote, event.rate) @@ -268,16 +403,17 @@ export default class MarketMakerLogsPage extends BasePage { } Doc.empty(page.dexOrderTxsTableBody) Doc.setVis(event.transactions && event.transactions.length > 0, page.dexOrderTxsTable) - const txUnits = (txType: number, sell: boolean) : UnitInfo | undefined => { + const txAsset = (txType: number, sell: boolean) : SupportedAsset | undefined => { switch (txType) { case wallets.txTypeSwap: case wallets.txTypeRefund: case wallets.txTypeSplit: - return sell ? bui : qui + return sell ? baseAsset : quoteAsset case wallets.txTypeRedeem: - return sell ? qui : bui + return sell ? quoteAsset : baseAsset } } + for (let i = 0; event.transactions && i < event.transactions.length; i++) { const tx = event.transactions[i] const row = page.dexOrderTxRowTmpl.cloneNode(true) as HTMLElement @@ -285,13 +421,17 @@ export default class MarketMakerLogsPage extends BasePage { tmpl.id.textContent = trimStringWithEllipsis(tx.id, 20) tmpl.id.setAttribute('title', tx.id) tmpl.type.textContent = wallets.txTypeString(tx.type) - const unitInfo = txUnits(tx.type, event.sell) - if (!unitInfo) { + const asset = txAsset(tx.type, event.sell) + if (!asset) { console.error('unexpected tx type in dex order event', tx.type) continue } - tmpl.amt.textContent = `${Doc.formatCoinValue(tx.amount, unitInfo)} ${unitInfo.conventional.unit.toLowerCase()}` - tmpl.fees.textContent = `${Doc.formatCoinValue(tx.fees, unitInfo)} ${unitInfo.conventional.unit.toLowerCase()}` + const assetExplorer = CoinExplorers[asset.id] + if (assetExplorer && assetExplorer[net]) { + tmpl.explorerLink.href = assetExplorer[net](tx.id) + } + tmpl.amt.textContent = `${Doc.formatCoinValue(tx.amount, asset.unitInfo)} ${asset.unitInfo.conventional.unit.toLowerCase()}` + tmpl.fees.textContent = `${Doc.formatCoinValue(tx.fees, asset.unitInfo)} ${asset.unitInfo.conventional.unit.toLowerCase()}` page.dexOrderTxsTableBody.appendChild(row) } this.forms.show(page.dexOrderDetailsForm) @@ -305,6 +445,11 @@ export default class MarketMakerLogsPage extends BasePage { const [baseTicker, quoteTicker] = [bui.conventional.unit, qui.conventional.unit] page.cexOrderID.textContent = trimStringWithEllipsis(event.id, 20) + if (this.cexOrderIDCopyListener !== undefined) { + page.copyCexOrderID.removeEventListener('click', this.cexOrderIDCopyListener) + } + this.cexOrderIDCopyListener = () => { setupCopyBtn(event.id, page.cexOrderID, page.copyCexOrderID, '#1e7d11') } + page.copyCexOrderID.addEventListener('click', this.cexOrderIDCopyListener) page.cexOrderID.setAttribute('title', event.id) const rate = app().conventionalRate(base, quote, event.rate) page.cexOrderRate.textContent = `${rate} ${baseTicker}/${quoteTicker}` @@ -322,6 +467,11 @@ export default class MarketMakerLogsPage extends BasePage { showDepositEventDetails (event: DepositEvent, pending: boolean) { const page = this.page page.depositID.textContent = trimStringWithEllipsis(event.transaction.id, 20) + if (this.depositIDCopyListener !== undefined) { + page.copyDepositID.removeEventListener('click', this.depositIDCopyListener) + } + this.depositIDCopyListener = () => { setupCopyBtn(event.transaction.id, page.depositID, page.copyDepositID, '#1e7d11') } + page.copyDepositID.addEventListener('click', this.depositIDCopyListener) page.depositID.setAttribute('title', event.transaction.id) const unitInfo = app().assets[event.assetID].unitInfo const unit = unitInfo.conventional.unit @@ -338,6 +488,11 @@ export default class MarketMakerLogsPage extends BasePage { showWithdrawalEventDetails (event: WithdrawalEvent, pending: boolean) { const page = this.page page.withdrawalID.textContent = trimStringWithEllipsis(event.id, 20) + if (this.withdrawalIDCopyListener !== undefined) { + page.copyWithdrawalID.removeEventListener('click', this.withdrawalIDCopyListener) + } + this.withdrawalIDCopyListener = () => { setupCopyBtn(event.id, page.withdrawalID, page.copyWithdrawalID, '#1e7d11') } + page.copyWithdrawalID.addEventListener('click', this.withdrawalIDCopyListener) page.withdrawalID.setAttribute('title', event.id) const unitInfo = app().assets[event.assetID].unitInfo const unit = unitInfo.conventional.unit @@ -352,7 +507,7 @@ export default class MarketMakerLogsPage extends BasePage { } showEventDetails (eventID: number) { - const event = this.events[eventID] + const [event] = this.events[eventID] if (event.dexOrderEvent) this.showDexOrderEventDetails(event.dexOrderEvent) if (event.cexOrderEvent) this.showCexOrderEventDetails(event.cexOrderEvent) if (event.depositEvent) this.showDepositEventDetails(event.depositEvent, event.pending) diff --git a/client/webserver/site/src/js/registry.ts b/client/webserver/site/src/js/registry.ts index 6478c7bd24..68980dac1d 100644 --- a/client/webserver/site/src/js/registry.ts +++ b/client/webserver/site/src/js/registry.ts @@ -1021,6 +1021,7 @@ export interface ProfitLoss { modsUSD: number final: Record finalUSD: number + diffs: Record profit: number profitRatio: number } diff --git a/client/webserver/site/src/js/wallets.ts b/client/webserver/site/src/js/wallets.ts index 545f9f8f23..dae1030ab6 100644 --- a/client/webserver/site/src/js/wallets.ts +++ b/client/webserver/site/src/js/wallets.ts @@ -1,4 +1,4 @@ -import Doc, { Animation, AniToggle, parseFloatDefault } from './doc' +import Doc, { Animation, AniToggle, parseFloatDefault, setupCopyBtn } from './doc' import BasePage from './basepage' import { postJSON, Errors } from './http' import { @@ -287,10 +287,10 @@ export default class WalletsPage extends BasePage { Doc.bind(page.rescanWallet, 'click', () => this.rescanWallet(this.selectedAssetID)) Doc.bind(page.earlierTxs, 'click', () => this.loadEarlierTxs()) - Doc.bind(page.copyTxIDBtn, 'click', () => { this.copyTxDetail(this.currTx?.id || '', page.txDetailsID, page.copyTxIDBtn) }) - Doc.bind(page.copyRecipientBtn, 'click', () => { this.copyTxDetail(this.currTx?.recipient || '', page.txDetailsRecipient, page.copyRecipientBtn) }) - Doc.bind(page.copyBondIDBtn, 'click', () => { this.copyTxDetail(this.currTx?.bondInfo?.bondID || '', page.txDetailsBondID, page.copyBondIDBtn) }) - Doc.bind(page.copyBondAccountIDBtn, 'click', () => { this.copyTxDetail(this.currTx?.bondInfo?.accountID || '', page.txDetailsBondAccountID, page.copyBondAccountIDBtn) }) + Doc.bind(page.copyTxIDBtn, 'click', () => { setupCopyBtn(this.currTx?.id || '', page.txDetailsID, page.copyTxIDBtn, '#1e7d11') }) + Doc.bind(page.copyRecipientBtn, 'click', () => { setupCopyBtn(this.currTx?.recipient || '', page.txDetailsRecipient, page.copyRecipientBtn, '#1e7d11') }) + Doc.bind(page.copyBondIDBtn, 'click', () => { setupCopyBtn(this.currTx?.bondInfo?.bondID || '', page.txDetailsBondID, page.copyBondIDBtn, '#1e7d11') }) + Doc.bind(page.copyBondAccountIDBtn, 'click', () => { setupCopyBtn(this.currTx?.bondInfo?.accountID || '', page.txDetailsBondAccountID, page.copyBondAccountIDBtn, '#1e7d11') }) // Bind the new wallet form. this.newWalletForm = new NewWalletForm(page.newWalletForm, (assetID: number) => { @@ -1674,21 +1674,6 @@ export default class WalletsPage extends BasePage { return row } - async copyTxDetail (detail: string, textEl: PageElement, btnEl: PageElement) { - try { - await navigator.clipboard.writeText(detail) - } catch (err) { - console.error('Unable to copy: ', err) - } - const originalColor = textEl.style.color - textEl.style.color = '#1e7d11' - btnEl.style.color = '#1e7d11' - setTimeout(() => { - textEl.style.color = originalColor - btnEl.style.color = originalColor - }, 350) - } - setTxDetailsPopupElements (tx: WalletTransaction) { const page = this.page diff --git a/client/webserver/webserver.go b/client/webserver/webserver.go index 554b164c85..619e44046e 100644 --- a/client/webserver/webserver.go +++ b/client/webserver/webserver.go @@ -184,7 +184,7 @@ type MMCore interface { Status() *mm.Status ArchivedRuns() ([]*mm.MarketMakingRun, error) RunOverview(startTime int64, mkt *mm.MarketWithHost) (*mm.MarketMakingRunOverview, error) - RunLogs(startTime int64, mkt *mm.MarketWithHost, n uint64, refID *uint64) ([]*mm.MarketMakingEvent, *mm.MarketMakingRunOverview, error) + RunLogs(startTime int64, mkt *mm.MarketWithHost, n uint64, refID *uint64, filter *mm.RunLogFilters) (events, updatedEvents []*mm.MarketMakingEvent, overview *mm.MarketMakingRunOverview, err error) CEXBook(host string, baseID, quoteID uint32) (buys, sells []*core.MiniOrder, _ error) }