From 62dad158f2eca23fb41ee8c9bb2d029664774403 Mon Sep 17 00:00:00 2001 From: norwnd Date: Mon, 30 Dec 2024 14:17:55 +0200 Subject: [PATCH] refactor code around recent matches & open orders (called active orders previuosly); add completed orders views (and extend order filtering API to support the use-case of fetching trully relevant orders); format quantities on open/completed views to lot size; finally, minor adjustments to relevant html/css have been made --- client/core/core.go | 14 +- client/core/types.go | 14 +- client/db/bolt/db.go | 78 +++- client/db/types.go | 6 + client/webserver/http.go | 22 ++ client/webserver/locales/en-us.go | 3 +- client/webserver/site/src/css/colors.scss | 4 + client/webserver/site/src/css/forms_dark.scss | 4 - client/webserver/site/src/css/market.scss | 34 +- client/webserver/site/src/css/mm.scss | 8 +- client/webserver/site/src/css/utilities.scss | 2 +- .../webserver/site/src/html/bodybuilder.tmpl | 2 +- client/webserver/site/src/html/forms.tmpl | 2 +- client/webserver/site/src/html/markets.tmpl | 150 ++++---- client/webserver/site/src/html/mm.tmpl | 2 +- .../webserver/site/src/html/mmsettings.tmpl | 6 +- client/webserver/site/src/html/wallets.tmpl | 8 +- client/webserver/site/src/js/app.ts | 8 +- client/webserver/site/src/js/doc.ts | 4 +- client/webserver/site/src/js/markets.ts | 356 ++++++++++++++---- client/webserver/site/src/js/order.ts | 6 +- client/webserver/site/src/js/orders.ts | 2 +- client/webserver/site/src/js/orderutil.ts | 6 +- client/webserver/site/src/js/registry.ts | 4 +- client/webserver/site/src/js/wallets.ts | 10 +- 25 files changed, 558 insertions(+), 197 deletions(-) diff --git a/client/core/core.go b/client/core/core.go index 9448d6219e..6424642296 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -4933,12 +4933,14 @@ func (c *Core) Orders(filter *OrderFilter) ([]*Order, error) { } ords, err := c.db.Orders(&db.OrderFilter{ - N: filter.N, - Offset: oid, - Hosts: filter.Hosts, - Assets: filter.Assets, - Market: mkt, - Statuses: filter.Statuses, + N: filter.N, + Offset: oid, + Hosts: filter.Hosts, + Assets: filter.Assets, + Market: mkt, + Statuses: filter.Statuses, + FilledOnly: filter.FilledOnly, + FresherThanUnixMs: filter.FresherThanUnixMs, }) if err != nil { return nil, fmt.Errorf("UserOrders error: %w", err) diff --git a/client/core/types.go b/client/core/types.go index 021a587177..23fbe74543 100644 --- a/client/core/types.go +++ b/client/core/types.go @@ -1121,12 +1121,14 @@ type PostBondResult struct { // OrderFilter is almost the same as db.OrderFilter, except the Offset order ID // is a dex.Bytes instead of a order.OrderID. type OrderFilter struct { - N int `json:"n"` - Offset dex.Bytes `json:"offset"` - Hosts []string `json:"hosts"` - Assets []uint32 `json:"assets"` - Statuses []order.OrderStatus `json:"statuses"` - Market *struct { + N int `json:"n"` + Offset dex.Bytes `json:"offset"` + FresherThanUnixMs uint64 `json:"fresherThanUnixMs"` + Hosts []string `json:"hosts"` + Assets []uint32 `json:"assets"` + Statuses []order.OrderStatus `json:"statuses"` + FilledOnly bool `json:"filledOnly"` + Market *struct { Base uint32 `json:"baseID"` Quote uint32 `json:"quoteID"` } `json:"market"` diff --git a/client/db/bolt/db.go b/client/db/bolt/db.go index 65b60f8f12..61cc313bfd 100644 --- a/client/db/bolt/db.go +++ b/client/db/bolt/db.go @@ -1120,6 +1120,29 @@ func (db *BoltDB) Orders(orderFilter *dexdb.OrderFilter) (ords []*dexdb.MetaOrde } } + if orderFilter.FilledOnly { + filledOrders, err := db.filledOrders() + if err != nil { + return nil, fmt.Errorf("filledOrders: %w", err) + } + filters = append(filters, func(oidB []byte, oBkt *bbolt.Bucket) bool { + oid, err := order.IDFromBytes(oidB) + if err != nil { + db.log.Error("couldn't parse order ID bytes: %x", oidB) + return false + } + _, ok := filledOrders[oid] + return ok + }) + } + + if orderFilter.FresherThanUnixMs > 0 { + filters = append(filters, func(oidB []byte, oBkt *bbolt.Bucket) bool { + stamp := intCoder.Uint64(oBkt.Get(updateTimeKey)) + return stamp >= orderFilter.FresherThanUnixMs + }) + } + if orderFilter.Market != nil { filters = append(filters, func(_ []byte, oBkt *bbolt.Bucket) bool { baseID, quoteID := intCoder.Uint32(oBkt.Get(baseKey)), intCoder.Uint32(oBkt.Get(quoteKey)) @@ -1541,7 +1564,6 @@ func (db *BoltDB) DEXOrdersWithActiveMatches(dex string) ([]order.OrderID, error ids = append(ids, id) } return ids, nil - } // MatchesForOrder retrieves the matches for the specified order ID. @@ -1553,6 +1575,28 @@ func (db *BoltDB) MatchesForOrder(oid order.OrderID, excludeCancels bool) ([]*de }, excludeCancels, true) // include archived matches } +// filledOrders returns a set of fully filled or partially filled orders. Order is partially +// filled if it has at least 1 non-cancel match with non-zero quantity. +func (db *BoltDB) filledOrders() (map[order.OrderID]struct{}, error) { + matches, err := db.filteredMatchesDecoded(func(match *dexdb.MetaMatch) bool { + // cancel order matches have an empty Address field, we don't want to count these + // hence skip + if match.Address == "" { + return false + } + return match.Quantity > 0 // means corresponding order is filled at least partially + }, false, true) // include archived matches + if err != nil { + return nil, fmt.Errorf("filteredMatchesDecoded: %w", err) + } + + result := make(map[order.OrderID]struct{}, len(matches)) + for _, m := range matches { + result[m.OrderID] = struct{}{} + } + return result, nil +} + // filteredMatches gets all matches that pass the provided filter function. Each // match's bucket is provided to the filter, and a boolean true return value // indicates the match should be decoded and returned. Matches with cancel @@ -1591,6 +1635,38 @@ func (db *BoltDB) filteredMatches(filter func(*bbolt.Bucket) bool, excludeCancel }) } +// filteredMatchesDecoded is same as filteredMatches but applies filter to decoded match. +func (db *BoltDB) filteredMatchesDecoded(filter func(*dexdb.MetaMatch) bool, excludeCancels, includeArchived bool) ([]*dexdb.MetaMatch, error) { + var matches []*dexdb.MetaMatch + return matches, db.matchesView(func(mb, archivedMB *bbolt.Bucket) error { + buckets := []*bbolt.Bucket{mb} + if includeArchived { + buckets = append(buckets, archivedMB) + } + for _, master := range buckets { + err := master.ForEach(func(k, _ []byte) error { + mBkt := master.Bucket(k) + if mBkt == nil { + return fmt.Errorf("match %x bucket is not a bucket", k) + } + match, err := loadMatchBucket(mBkt, excludeCancels) + if err != nil { + return fmt.Errorf("loading match %x bucket: %w", k, err) + } + if match == nil || !filter(match) { + return nil + } + matches = append(matches, match) + return nil + }) + if err != nil { + return err + } + } + return nil + }) +} + func loadMatchBucket(mBkt *bbolt.Bucket, excludeCancels bool) (*dexdb.MetaMatch, error) { var proof *dexdb.MatchProof matchB := getCopy(mBkt, matchKey) diff --git a/client/db/types.go b/client/db/types.go index ecce43c75b..644e70bde6 100644 --- a/client/db/types.go +++ b/client/db/types.go @@ -1218,6 +1218,12 @@ type OrderFilter struct { // Statuses is a list of acceptable statuses. A zero-length Statuses means // all statuses are accepted. Statuses []order.OrderStatus + // FilledOnly is a flag that when specified limits results to only those orders + // that have been fully filled or partially filled. + FilledOnly bool + // FresherThanUnixMs is a unix millisecond timestamp used to filter out orders that are + // older than its value. + FresherThanUnixMs uint64 } // noteKeySize must be <= 32. diff --git a/client/webserver/http.go b/client/webserver/http.go index 45ca372e66..92946e74f0 100644 --- a/client/webserver/http.go +++ b/client/webserver/http.go @@ -360,6 +360,27 @@ func (s *WebServer) handleExportOrders(w http.ResponseWriter, r *http.Request) { return } + nStr := r.Form.Get("n") + if nStr != "" { + n, err := strconv.ParseInt(nStr, 10, 32) + if err != nil { + log.Errorf("error parsing N: %v", err) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + filter.N = int(n) + } + fresherThanUnixMsStr := r.Form.Get("fresherThanUnixMs") + if fresherThanUnixMsStr != "" { + fresherThanUnixMs, err := strconv.ParseUint(fresherThanUnixMsStr, 10, 64) + if err != nil { + log.Errorf("error parsing fresherThanUnixMs: %v", err) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + filter.FresherThanUnixMs = fresherThanUnixMs + } + filter.Hosts = r.Form["hosts"] assets := r.Form["assets"] filter.Assets = make([]uint32, len(assets)) @@ -383,6 +404,7 @@ func (s *WebServer) handleExportOrders(w http.ResponseWriter, r *http.Request) { } filter.Statuses[k] = order.OrderStatus(statusNumID) } + filter.FilledOnly = r.Form.Get("filledOnly") == "true" ords, err := s.core.Orders(filter) if err != nil { diff --git a/client/webserver/locales/en-us.go b/client/webserver/locales/en-us.go index ea268974a1..a8273a5bf9 100644 --- a/client/webserver/locales/en-us.go +++ b/client/webserver/locales/en-us.go @@ -103,7 +103,8 @@ var EnUS = map[string]*intl.Translation{ "immature": {T: "immature"}, "fee balance": {T: "fee balance"}, "Sell Orders": {T: "Sell Orders"}, - "Your Orders": {T: "Your Orders"}, + "Open Orders": {T: "Open Orders"}, + "Completed Orders": {T: "Completed Orders"}, "sweep_orders": {T: "Hide fully executed orders"}, "sweep_order": {T: "Hide this fully executed order"}, "Recent Matches": {T: "Recent Matches"}, diff --git a/client/webserver/site/src/css/colors.scss b/client/webserver/site/src/css/colors.scss index 7af973dab3..5bb900d7e6 100644 --- a/client/webserver/site/src/css/colors.scss +++ b/client/webserver/site/src/css/colors.scss @@ -153,6 +153,10 @@ body.dark { background-color: var(--section-bg); } +.section-bg-strong { + background-color: var(--section-bg-strong); +} + .text-good { color: var(--indicator-good); } diff --git a/client/webserver/site/src/css/forms_dark.scss b/client/webserver/site/src/css/forms_dark.scss index 3dd2bc6bbe..6309e881af 100644 --- a/client/webserver/site/src/css/forms_dark.scss +++ b/client/webserver/site/src/css/forms_dark.scss @@ -50,10 +50,6 @@ body.dark { background-color: #263846; } } - - .bordertop { - border-top: solid 1px #999; - } } .selectable.selected { diff --git a/client/webserver/site/src/css/market.scss b/client/webserver/site/src/css/market.scss index ece950a8d3..17ee2d4976 100644 --- a/client/webserver/site/src/css/market.scss +++ b/client/webserver/site/src/css/market.scss @@ -87,7 +87,7 @@ div[data-handler=markets] { button { opacity: 0.85; padding: 5px 25px; - border-radius: 3px; + border-radius: 6px; background-color: var(--section-bg); color: var(--market-btn-selected-color); @@ -312,7 +312,7 @@ div[data-handler=markets] { & > section { &:first-child { // leftmost section order: 1; - flex-basis: 22%; /* width/height - depending on flex-direction */ + flex-basis: 18%; /* width/height - depending on flex-direction */ } &:nth-child(2) { // middle section @@ -346,7 +346,7 @@ div[data-handler=markets] { &:last-child { // user orders, recent matches order: 3; - flex-basis: 22%; /* width/height - depending on flex-direction */ + flex-basis: 26%; /* width/height - depending on flex-direction */ } } @@ -361,7 +361,7 @@ div[data-handler=markets] { } } - #durBttnBox { + #candleDurBttnBox { position: absolute; left: 65px; top: 5px; @@ -370,13 +370,14 @@ div[data-handler=markets] { .candle-dur-bttn { background-color: var(--section-bg); + border: 1px solid var(--btn-border-color); padding: 2px 4px; font-size: 14px; line-height: 1; margin: 0 2px; &:hover { - background-color: #7777; + background-color: var(--section-bg-strong); } &:hover, @@ -388,7 +389,7 @@ div[data-handler=markets] { } #loaderMsg { - color: #777; + color: var(--text-grey); } #bondCreationPending { @@ -515,6 +516,27 @@ div[data-handler=markets] { } } +#completedOrderHistoryDurBttnBox { + background-color: var(--section-bg); + z-index: 1; + + .completed-order-dur-bttn { + background-color: var(--section-bg); + border: 1px solid var(--btn-border-color); + line-height: 1; + + &:hover { + background-color: var(--section-bg-strong); + } + + &:hover, + &.selected { + border-color: var(--text-warning); + color: var(--text-warning); + } + } +} + @include media-breakpoint-up(xl) { #marketStats { display: none; diff --git a/client/webserver/site/src/css/mm.scss b/client/webserver/site/src/css/mm.scss index 5e88ab4ed7..1b4dd2fb82 100644 --- a/client/webserver/site/src/css/mm.scss +++ b/client/webserver/site/src/css/mm.scss @@ -2,12 +2,6 @@ div[data-handler=mmsettings], div[data-handler=mmarchives], div[data-handler=mmlogs], div[data-handler=mm] { - #overview { - section { - background-color: var(--section-bg-strong); - } - } - #gapStrategySelect { width: 300px; } @@ -35,7 +29,7 @@ div[data-handler=mm] { .bot-type-selector { @include border; - @extend .rounded3; + @extend .border-rounded3; display: flex; flex-direction: column; diff --git a/client/webserver/site/src/css/utilities.scss b/client/webserver/site/src/css/utilities.scss index 06cad05908..d058f7a6e8 100644 --- a/client/webserver/site/src/css/utilities.scss +++ b/client/webserver/site/src/css/utilities.scss @@ -105,7 +105,7 @@ color: var(--text-grey); } -.rounded3 { +.border-rounded3 { border-radius: 3px; } diff --git a/client/webserver/site/src/html/bodybuilder.tmpl b/client/webserver/site/src/html/bodybuilder.tmpl index 26f869e4d1..9f57f1b4c7 100644 --- a/client/webserver/site/src/html/bodybuilder.tmpl +++ b/client/webserver/site/src/html/bodybuilder.tmpl @@ -150,7 +150,7 @@ 4 -
+
diff --git a/client/webserver/site/src/html/forms.tmpl b/client/webserver/site/src/html/forms.tmpl index 24982ae155..0b0a7d33e9 100644 --- a/client/webserver/site/src/html/forms.tmpl +++ b/client/webserver/site/src/html/forms.tmpl @@ -237,7 +237,7 @@
-
+
diff --git a/client/webserver/site/src/html/markets.tmpl b/client/webserver/site/src/html/markets.tmpl index 3d3788b90a..3143cba4b1 100644 --- a/client/webserver/site/src/html/markets.tmpl +++ b/client/webserver/site/src/html/markets.tmpl @@ -62,7 +62,7 @@ - - + -
-
@@ -78,7 +78,7 @@
-
+
@@ -143,7 +143,7 @@
- {{/* textContent set by script */}} + {{/* textContent set by script */}}
@@ -192,7 +192,7 @@
- {{/* textContent set by script */}} + {{/* textContent set by script */}}
@@ -327,82 +327,84 @@
- {{- /* USER ORDERS */ -}} -
- [[[Your Orders]]] -
-
[[[unready_wallets_msg]]]
-
no active orders
-
-
-
-
- - - - - - - - - + {{- /* USER ORDER TEMPLATE */ -}} +
+
+
+ + + + + + + + +
+
+
+ +
-
-
- - -
-
- [[[Type]]] - -
-
- [[[Side]]] - -
-
- [[[Status]]] - -
-
- [[[Age]]] - -
-
- [[[Quantity]]] - -
-
- [[[Rate]]] - -
-
- [[[Filled]]] - -
-
- [[[Settled]]] - -
+
+ [[[Type]]] + +
+
+ [[[Side]]] + +
+
+ [[[Status]]] + +
+
+ [[[Age]]] + +
+
+ [[[Quantity]]] + +
+
+ [[[Rate]]] + +
+
+ [[[Filled]]] + +
+
+ [[[Settled]]] +
-
{{- /* END USER ORDERS */ -}} +
+ {{- /* END USER ORDER TEMPLATE */ -}} + + {{- /* OPEN ORDERS */ -}} +
+
[[[Open Orders]]]
+
[[[unready_wallets_msg]]]
+
no recent activity
+
+
+ {{- /* END OPEN ORDERS */ -}} {{- /* RECENT MATCHES */ -}} -
-
[[[Recent Matches]]]
- +
+
[[[Recent Matches]]]
+
- - - @@ -416,7 +418,19 @@
+ + +
-
{{- /* END RECENT MATCHES */ -}} +
+ {{- /* END RECENT MATCHES */ -}} + + {{- /* COMPLETED ORDERS */ -}} +
+
[[[Completed Orders]]]
+
+ +
+
no past history
+
+
+ {{- /* END COMPLETED ORDERS */ -}}
diff --git a/client/webserver/site/src/html/mm.tmpl b/client/webserver/site/src/html/mm.tmpl index 738c381473..217844ce3b 100644 --- a/client/webserver/site/src/html/mm.tmpl +++ b/client/webserver/site/src/html/mm.tmpl @@ -10,7 +10,7 @@ {{- /* MARKET MAKING OVERVIEW */ -}}
-
+
Market Making
-
+
[[[Quick Placements]]]
@@ -573,7 +573,7 @@ [[[Wallet Options]]]
-
no settings available
+
no settings available
@@ -766,7 +766,7 @@
-
+
diff --git a/client/webserver/site/src/html/wallets.tmpl b/client/webserver/site/src/html/wallets.tmpl index 2df2b554f1..3e8fc9eb9f 100644 --- a/client/webserver/site/src/html/wallets.tmpl +++ b/client/webserver/site/src/html/wallets.tmpl @@ -394,7 +394,7 @@ [[[load_earlier_transactions]]]
-
+
[[[no_tx_history]]]
@@ -471,7 +471,7 @@
-
+
[[[No Recent Activity]]]
@@ -858,7 +858,7 @@
[[[select_vsp_from_list]]]
- +
@@ -932,7 +932,7 @@ [[[Ticket History]]]
-
[[[URL]]]
+
diff --git a/client/webserver/site/src/js/app.ts b/client/webserver/site/src/js/app.ts index 3ef44ce313..83bae9b1cd 100644 --- a/client/webserver/site/src/js/app.ts +++ b/client/webserver/site/src/js/app.ts @@ -898,7 +898,7 @@ export default class Application { setNoteTimes (noteList: HTMLElement) { for (const el of (Array.from(noteList.children) as NoteElement[])) { - Doc.safeSelector(el, 'span.note-time').textContent = Doc.timeSinceFromMs(el.note.stamp) + Doc.safeSelector(el, 'span.note-time').textContent = Doc.ageSinceFromMs(el.note.stamp) } } @@ -1397,10 +1397,10 @@ export default class Application { return () => { loader.remove() } } - /* orders retrieves a list of orders for the specified dex and market - * including inflight orders. + /* orders returns a list of recent user orders for the specified dex and market + * including inflight/canceled/revoked orders. */ - orders (host: string, mktID: string): Order[] { + recentOrders (host: string, mktID: string): Order[] { let orders: Order[] = [] const mkt = this.user.exchanges[host].markets[mktID] if (mkt.orders) orders = orders.concat(mkt.orders) diff --git a/client/webserver/site/src/js/doc.ts b/client/webserver/site/src/js/doc.ts index 4b44d9b339..842f47139f 100644 --- a/client/webserver/site/src/js/doc.ts +++ b/client/webserver/site/src/js/doc.ts @@ -668,10 +668,10 @@ export default class Doc { } /* - * timeSinceFromMs returns a string representation of the duration since the + * ageSinceFromMs returns a string representation of the duration since the * specified unix timestamp (milliseconds). */ - static timeSinceFromMs (ms: number): string { + static ageSinceFromMs (ms: number): string { return Doc.formatDuration((new Date().getTime()) - ms) } diff --git a/client/webserver/site/src/js/markets.ts b/client/webserver/site/src/js/markets.ts index f29562b44b..cf794e8f43 100644 --- a/client/webserver/site/src/js/markets.ts +++ b/client/webserver/site/src/js/markets.ts @@ -62,7 +62,9 @@ const unmarketRoute = 'unmarket' const epochMatchSummaryRoute = 'epoch_match_summary' const animationLength = 500 -const maxUserOrdersShown = 10 + +const maxRecentlyActiveUserOrdersShown = 8 +const maxCompletedUserOrdersShown = 100 // orderBookSideMaxCapacity defines how many orders in the book side will be displayed const orderBookSideMaxCapacity = 14 @@ -73,6 +75,12 @@ const sellBtnClass = 'sellred-bg' const candleBinKey5m = '5m' const candleBinKey24h = '24h' +const completedOrderHistoryDurationHide = 'hide' +const completedOrderHistoryDuration1d = '1 day' +const completedOrderHistoryDuration1w = '1 week' +const completedOrderHistoryDuration1m = '1 month' +const completedOrderHistoryDuration3m = '3 month' + interface MetaOrder { div: HTMLElement header: Record @@ -155,9 +163,10 @@ export default class MarketsPage extends BasePage { currentCreate: SupportedAsset book: OrderBook cancelData: CancelData - // userOrders contains the latest snapshot of known user orders which is kept up-to-date - // by various events - userOrders: Record + recentlyActiveUserOrders: Record + // actionInflightCompletedOrderHistory helps coordinate which completed order history (what + // duration) will be displayed in UI with respect to what user actually chose + actionInflightCompletedOrderHistory: boolean hovers: HTMLElement[] ogTitle: string candleChart: CandleChart // reused across different markets @@ -189,7 +198,7 @@ export default class MarketsPage extends BasePage { if (!this.main.parentElement) return // Not gonna happen, but TypeScript cares. this.maxBuyLastReqID = 0 this.maxSellLastReqID = 0 - this.userOrders = {} + this.recentlyActiveUserOrders = {} this.recentMatches = [] this.hovers = [] // 'Recent Matches' list sort key and direction. @@ -223,7 +232,8 @@ export default class MarketsPage extends BasePage { setOptionTemplates(page) Doc.cleanTemplates( - page.orderRowTmpl, page.durBttnTemplate, page.userOrderTmpl, page.recentMatchesTemplate + page.orderRowTmpl, page.candleDurBttnTemplate, page.userOrderTmpl, page.recentMatchesTemplate, + page.completedOrderDurBttnTemplate ) // Buttons to show token approval form @@ -389,11 +399,11 @@ export default class MarketsPage extends BasePage { if (page.leftMarketDock.clientWidth === 0) openMarketsList() else closeMarketsList() }) - const initMarket = (mkt: ExchangeMarket) => { + const initMarket = async (mkt: ExchangeMarket) => { // nothing to do if this market is already set/chosen const { quoteid: quoteID, baseid: baseID, xc: { host } } = mkt if (this.market?.base?.id === baseID && this.market?.quote?.id === quoteID) return - this.switchToMarket(host, baseID, quoteID) + await this.switchToMarket(host, baseID, quoteID) } // Prepare the list of markets. this.marketList = new MarketList(page.marketListV1) @@ -438,8 +448,8 @@ export default class MarketsPage extends BasePage { // Start a ticker to update time-since values. this.secondTicker = window.setInterval(() => { - for (const mord of Object.values(this.userOrders)) { - mord.details.age.textContent = Doc.timeSinceFromMs(mord.ord.submitTime) + for (const mord of Object.values(this.recentlyActiveUserOrders)) { + mord.details.age.textContent = Doc.ageSinceFromMs(mord.ord.submitTime) } for (const td of Doc.applySelector(page.recentMatchesLiveList, '[data-tmpl=time]')) { td.textContent = Doc.timeFromMs(parseFloat(td.dataset.timestampMs ?? '0')) @@ -1087,13 +1097,13 @@ export default class MarketsPage extends BasePage { } } - setCandleDurBttns () { + setCandleDurationBttns () { const { page, market } = this - Doc.empty(page.durBttnBox) + Doc.empty(page.candleDurBttnBox) for (const dur of market.dex.candleDurs) { - const bttn = page.durBttnTemplate.cloneNode(true) + const bttn = page.candleDurBttnTemplate.cloneNode(true) bttn.textContent = dur bind(bttn, 'click', () => { const dur = bttn.textContent @@ -1104,13 +1114,60 @@ export default class MarketsPage extends BasePage { this.selectCandleDurationElem(dur) this.loadCandles(dur) }) - page.durBttnBox.appendChild(bttn) + page.candleDurBttnBox.appendChild(bttn) } } // selectCandleDurationElem draws in UI which candle duration was chosen. selectCandleDurationElem (dur: string) { - for (const bttn of Doc.kids(this.page.durBttnBox)) { + for (const bttn of Doc.kids(this.page.candleDurBttnBox)) { + if (bttn.textContent === dur) { + bttn.classList.add('selected') + continue + } + bttn.classList.remove('selected') + } + } + + setCompletedOrderHistoryDurationBttns () { + const { page } = this + + Doc.empty(page.completedOrderHistoryDurBttnBox) + + const completedOrderHistoryDurations = [ + completedOrderHistoryDurationHide, + completedOrderHistoryDuration1d, + completedOrderHistoryDuration1w, + completedOrderHistoryDuration1m, + completedOrderHistoryDuration3m + ] + for (const dur of completedOrderHistoryDurations) { + const bttn = page.completedOrderDurBttnTemplate.cloneNode(true) + bttn.textContent = dur + bind(bttn, 'click', () => { + if (this.actionInflightCompletedOrderHistory) { + return // let the older request to finish to avoid races + } + + this.actionInflightCompletedOrderHistory = true + + const dur = bttn.textContent + if (!dur) { + return // should never happen since we are initializing button textContent guaranteed + } + this.selectCompletedOrderHistoryDurationElem(dur) + this.reloadCompletedUserOrders(dur).then(() => { + this.actionInflightCompletedOrderHistory = false + }) + }) + page.completedOrderHistoryDurBttnBox.appendChild(bttn) + } + } + + // selectCompletedOrderHistoryDurationElem draws in UI which completed order history + // duration was chosen. + selectCompletedOrderHistoryDurationElem (dur: string) { + for (const bttn of Doc.kids(this.page.completedOrderHistoryDurBttnBox)) { if (bttn.textContent === dur) { bttn.classList.add('selected') continue @@ -1196,7 +1253,7 @@ export default class MarketsPage extends BasePage { this.setMarketDetails() this.setCurrMarketPrice() - this.setCandleDurBttns() + this.setCandleDurationBttns() // use user's last known candle duration (or 24h) as "initial default" const candleDur = State.fetchLocal(State.lastCandleDurationLK) || candleBinKey24h this.selectCandleDurationElem(candleDur) @@ -1217,7 +1274,11 @@ export default class MarketsPage extends BasePage { this.updateTitle() this.reputationMeter.setHost(dex.host) this.updateReputation() - this.loadUserOrders() + await this.reloadRecentlyActiveUserOrders() + + this.setCompletedOrderHistoryDurationBttns() + this.selectCompletedOrderHistoryDurationElem(completedOrderHistoryDurationHide) + await this.reloadCompletedUserOrders(completedOrderHistoryDurationHide) // update header for "matches" section page.priceHdr.textContent = `Price (${Doc.shortSymbol(this.market.quote.symbol)})` @@ -1700,20 +1761,23 @@ export default class MarketsPage extends BasePage { return 0 } - maxUserOrderCount (): number { - const { dex: { host }, cfg: { name: mktID } } = this.market - return Math.max(maxUserOrdersShown, app().orders(host, mktID).length) - } + // reloadRecentlyActiveUserOrders completely redraws recently active user orders section on + // markets page. + async reloadRecentlyActiveUserOrders () { + // erase all previously drawn recently active user orders + for (const oid in this.recentlyActiveUserOrders) { + delete this.recentlyActiveUserOrders[oid] + } - // loadUserOrders draws user orders section on markets page. - async loadUserOrders () { const { base: b, quote: q, dex: { host }, cfg: { name: mktID } } = this.market - for (const oid in this.userOrders) delete this.userOrders[oid] - if (!b || !q) return this.resolveUserOrders([]) // unsupported asset + if (!b || !q) { + // unsupported asset, show empty list + return this.drawRecentlyActiveUserOrders([]) + } - const activeOrders = app().orders(host, mktID) - if (activeOrders.length !== 0) { - this.resolveUserOrders(activeOrders) + let recentOrders = app().recentOrders(host, mktID) + if (recentOrders.length !== 0) { + this.drawRecentlyActiveUserOrders(recentOrders) return } @@ -1722,46 +1786,68 @@ export default class MarketsPage extends BasePage { const filter: OrderFilter = { hosts: [host], market: { baseID: b.id, quoteID: q.id }, - statuses: [0, 1, 2], // interested in active orders only - n: this.maxUserOrderCount() + n: maxRecentlyActiveUserOrdersShown } const res = await postJSON('/api/orders', filter) - this.resolveUserOrders(res.orders || []) + if (!res.orders) { + this.drawRecentlyActiveUserOrders([]) // we have not even 1 order for this market, show empty list + } + recentOrders = res.orders.filter((ord: Order): boolean => { + const orderIsActive = ord.status < OrderUtil.StatusExecuted || OrderUtil.hasActiveMatches(ord) + if (orderIsActive) { + return true // currently active order + } + const now = new Date().getTime() + const minute = 60 * 1000 + if (now - ord.stamp <= 10 * minute) { + return true // inactive but recent order + } + return false + }) + this.drawRecentlyActiveUserOrders(recentOrders) } - /* refreshActiveOrders refreshes the user's active order list. */ - refreshActiveOrders () { - const orders = app().orders(this.market.dex.host, marketID(this.market.baseCfg.symbol, this.market.quoteCfg.symbol)) - return this.resolveUserOrders(orders) + /* refreshRecentlyActiveOrders refreshes the user's active order list based on notifications feed */ + refreshRecentlyActiveOrders () { + const orders = app().recentOrders(this.market.dex.host, marketID(this.market.baseCfg.symbol, this.market.quoteCfg.symbol)) + this.drawRecentlyActiveUserOrders(orders) } - resolveUserOrders (orders: Order[]) { - const { page, userOrders, market } = this - const cfg = market.cfg + drawRecentlyActiveUserOrders (orders: Order[]) { + const { page, recentlyActiveUserOrders, market } = this - const orderIsActive = (ord: Order) => ord.status < OrderUtil.StatusExecuted || OrderUtil.hasActiveMatches(ord) + // enrich recently active user order list as necessary + for (const ord of orders) { + recentlyActiveUserOrders[ord.id] = { ord: ord } as MetaOrder + } - for (const ord of orders) userOrders[ord.id] = { ord: ord } as MetaOrder - let sortedOrders = Object.keys(userOrders).map((oid: string) => userOrders[oid]) + // we have to cap how many orders we can show in UI + const orderIsActive = (ord: Order) => ord.status < OrderUtil.StatusExecuted || OrderUtil.hasActiveMatches(ord) + let sortedOrders = Object.keys(recentlyActiveUserOrders).map((oid: string) => recentlyActiveUserOrders[oid]) sortedOrders.sort((a: MetaOrder, b: MetaOrder) => { const [aActive, bActive] = [orderIsActive(a.ord), orderIsActive(b.ord)] if (aActive && !bActive) return -1 else if (!aActive && bActive) return 1 return b.ord.submitTime - a.ord.submitTime }) - const n = this.maxUserOrderCount() - if (sortedOrders.length > n) { sortedOrders = sortedOrders.slice(0, n) } + if (sortedOrders.length > maxRecentlyActiveUserOrdersShown) { + sortedOrders = sortedOrders.slice(0, maxRecentlyActiveUserOrdersShown) + } - for (const oid in userOrders) delete userOrders[oid] + // empty recently active user order list as necessary, we'll re-populate it down below + // since some orders might not make it in UI (because we cap how many orderw we show) + for (const oid in recentlyActiveUserOrders) { + delete recentlyActiveUserOrders[oid] + } - Doc.empty(page.userOrders) - Doc.setVis(sortedOrders?.length, page.userOrders) - Doc.setVis(!sortedOrders?.length, page.userNoOrders) + Doc.empty(page.recentlyActiveUserOrders) + Doc.setVis(sortedOrders?.length, page.recentlyActiveUserOrders) + Doc.setVis(!sortedOrders?.length, page.recentlyActiveNoUserOrders) let unreadyOrders = false for (const mord of sortedOrders) { const div = page.userOrderTmpl.cloneNode(true) as HTMLElement - page.userOrders.appendChild(div) + page.recentlyActiveUserOrders.appendChild(div) const tmpl = Doc.parseTemplate(div) const header = Doc.parseTemplate(tmpl.header) const details = Doc.parseTemplate(tmpl.details) @@ -1770,13 +1856,13 @@ export default class MarketsPage extends BasePage { mord.header = header mord.details = details const ord = mord.ord - const orderID = ord.id const isActive = orderIsActive(ord) - // No need to track in-flight orders here. We've already added it to - // display. - if (orderID) userOrders[orderID] = mord + // No need to track in-flight orders here. We've already added it to display. + if (orderID) { + recentlyActiveUserOrders[orderID] = mord + } if (!ord.readyToTick && OrderUtil.hasActiveMatches(ord)) { tmpl.header.classList.add('unready-user-order') @@ -1787,9 +1873,15 @@ export default class MarketsPage extends BasePage { details.side.textContent = mord.header.side.textContent = OrderUtil.sellString(ord) details.side.classList.add(ord.sell ? 'sellcolor' : 'buycolor') header.side.classList.add(ord.sell ? 'sellcolor' : 'buycolor') - details.qty.textContent = mord.header.qty.textContent = Doc.formatCoinAtom(ord.qty, market.baseUnitInfo) - let headerRateStr = Doc.formatRateAtomToRateStep(ord.rate, market.baseUnitInfo, market.quoteUnitInfo, cfg.ratestep) - let detailsRateStr = Doc.formatRateAtomToRateStep(ord.rate, market.baseUnitInfo, market.quoteUnitInfo, cfg.ratestep) + const unfilledFormatted = Doc.formatCoinAtomToLotSizeBaseCurrency( + ord.qty - OrderUtil.filled(ord), + market.baseUnitInfo, + market.cfg.lotsize + ) + mord.header.qty.textContent = `${unfilledFormatted}` + details.qty.textContent = Doc.formatCoinAtomToLotSizeBaseCurrency(ord.qty, market.baseUnitInfo, market.cfg.lotsize) + let headerRateStr = Doc.formatRateAtomToRateStep(ord.rate, market.baseUnitInfo, market.quoteUnitInfo, market.cfg.ratestep) + let detailsRateStr = Doc.formatRateAtomToRateStep(ord.rate, market.baseUnitInfo, market.quoteUnitInfo, market.cfg.ratestep) if (ord.type === OrderUtil.Market) { headerRateStr = this.marketOrderHeaderRateString(ord, market) detailsRateStr = this.marketOrderDetailsRateString(ord, market) @@ -1845,7 +1937,7 @@ export default class MarketsPage extends BasePage { floater.style.top = `${y - 1}px` // - 1 to hide border on header div floater.style.left = `${m.bodyLeft}px` // Get the updated version of the order - const mord = this.userOrders[orderID] + const mord = this.recentlyActiveUserOrders[orderID] // if the order isn't among user orders it means we are still showing it in UI, yet it's // no longer relevant - do nothing in that case (it will get removed from UI eventually) if (!mord) { @@ -1884,6 +1976,131 @@ export default class MarketsPage extends BasePage { Doc.setVis(unreadyOrders, page.unreadyOrdersMsg) } + async reloadCompletedUserOrders (period: string) { + const { page, market } = this + const { base: b, quote: q, dex: { host } } = market + const now = new Date() + + let completedUserOrders = [] + let fresherThanUnixMs = 0 // default, means not showing completed orders history + if (period === completedOrderHistoryDuration1w) { + const day = 24 * 60 * 60 * 1000 + fresherThanUnixMs = now.getTime() - day + } + if (period === completedOrderHistoryDuration1w) { + const week = 7 * 24 * 60 * 60 * 1000 + fresherThanUnixMs = now.getTime() - week + } + if (period === completedOrderHistoryDuration1m) { + fresherThanUnixMs = new Date().setMonth(now.getMonth() - 1) // already returns unix ms timestamp + } + if (period === completedOrderHistoryDuration3m) { + fresherThanUnixMs = new Date().setMonth(now.getMonth() - 3) // already returns unix ms timestamp + } + if (fresherThanUnixMs !== 0) { + const filter: OrderFilter = { + n: maxCompletedUserOrdersShown, + fresherThanUnixMs: fresherThanUnixMs, + hosts: [host], + market: { baseID: b.id, quoteID: q.id }, + statuses: [OrderUtil.StatusUnknown, OrderUtil.StatusExecuted, OrderUtil.StatusCanceled, OrderUtil.StatusRevoked], + filledOnly: true + } + const res = await postJSON('/api/orders', filter) + completedUserOrders = res.orders || [] + } + + Doc.empty(page.completedUserOrders) + Doc.setVis(completedUserOrders?.length, page.completedUserOrders) + Doc.setVis(!completedUserOrders?.length, page.completedNoUserOrders) + + for (const ord of completedUserOrders) { + const div = page.userOrderTmpl.cloneNode(true) as HTMLElement + page.completedUserOrders.appendChild(div) + const tmpl = Doc.parseTemplate(div) + const header = Doc.parseTemplate(tmpl.header) + const details = Doc.parseTemplate(tmpl.details) + + header.sideLight.classList.add(ord.sell ? 'sell' : 'buy') + header.side.textContent = ord.sell ? 'sold' : 'bought' + details.side.textContent = OrderUtil.sellString(ord) + details.side.classList.add(ord.sell ? 'sellcolor' : 'buycolor') + header.side.classList.add(ord.sell ? 'sellcolor' : 'buycolor') + const settledFormatted = Doc.formatCoinAtomToLotSizeBaseCurrency(OrderUtil.settled(ord), market.baseUnitInfo, market.cfg.lotsize) + const totalQtyFormatted = Doc.formatCoinAtomToLotSizeBaseCurrency(ord.qty, market.baseUnitInfo, market.cfg.lotsize) + header.qty.textContent = `[ ${settledFormatted} / ${totalQtyFormatted} ]` + details.qty.textContent = Doc.formatCoinAtomToLotSizeBaseCurrency(ord.qty, market.baseUnitInfo, market.cfg.lotsize) + let headerRateStr = Doc.formatRateAtomToRateStep(ord.rate, market.baseUnitInfo, market.quoteUnitInfo, market.cfg.ratestep) + let detailsRateStr = Doc.formatRateAtomToRateStep(ord.rate, market.baseUnitInfo, market.quoteUnitInfo, market.cfg.ratestep) + if (ord.type === OrderUtil.Market) { + headerRateStr = this.marketOrderHeaderRateString(ord, market) + detailsRateStr = this.marketOrderDetailsRateString(ord, market) + } + header.rate.textContent = `@ ${headerRateStr}` + details.rate.textContent = detailsRateStr + header.baseSymbol.textContent = market.baseUnitInfo.conventional.unit + details.type.textContent = OrderUtil.orderTypeText(ord.type) + header.status.textContent = Doc.ageSinceFromMs(ord.stamp) + details.status.textContent = OrderUtil.statusString(ord) + details.age.textContent = Doc.ageSinceFromMs(ord.stamp) + details.filled.textContent = `${(OrderUtil.filled(ord) / ord.qty * 100).toFixed(1)}%` + details.settled.textContent = `${(OrderUtil.settled(ord) / ord.qty * 100).toFixed(1)}%` + + if (!ord.id) { + Doc.hide(details.link) + } else { + details.link.href = `order/${ord.id}` + app().bindInternalNavigation(div) + } + let currentFloater: (PageElement | null) + bind(tmpl.header, 'click', () => { + if (Doc.isDisplayed(tmpl.details)) { + Doc.hide(tmpl.details) + return + } + Doc.show(tmpl.details) + if (currentFloater) currentFloater.remove() + }) + /** + * We'll show the button menu when they hover over the header. To avoid + * pushing the layout around, we'll show the buttons as an absolutely + * positioned copy of the button menu. + */ + bind(tmpl.header, 'mouseenter', () => { + // Don't show the copy if the details are already displayed. + if (Doc.isDisplayed(tmpl.details)) return + if (currentFloater) currentFloater.remove() + // Create and position the element based on the position of the header. + const floater = document.createElement('div') + currentFloater = floater + document.body.appendChild(floater) + floater.className = 'user-order-floaty-menu' + const m = Doc.layoutMetrics(tmpl.header) + const y = m.bodyTop + m.height + floater.style.top = `${y - 1}px` // - 1 to hide border on header div + floater.style.left = `${m.bodyLeft}px` + floater.appendChild(details.link.cloneNode(true)) + + const ogScrollY = page.orderScroller.scrollTop + // Set up the hover interactions. + const moved = (e: MouseEvent) => { + // If the user scrolled, reposition the float menu. This keeps the + // menu from following us around, which can prevent removal below. + const yShift = page.orderScroller.scrollTop - ogScrollY + floater.style.top = `${y + yShift}px` + if (Doc.mouseInElement(e, floater) || Doc.mouseInElement(e, div)) return + floater.remove() + currentFloater = null + document.removeEventListener('mousemove', moved) + page.orderScroller.removeEventListener('scroll', moved) + } + document.addEventListener('mousemove', moved) + page.orderScroller.addEventListener('scroll', moved) + }) + app().bindTooltips(div) + } + } + marketOrderHeaderRateString (ord: Order, mkt: CurrentMarket): string { if (!ord.matches?.length) return intl.prep(intl.ID_MARKET_ORDER) let rateStr = Doc.formatRateAtomToRateStep(OrderUtil.averageRate(ord), mkt.baseUnitInfo, mkt.quoteUnitInfo, mkt.cfg.ratestep) @@ -1903,10 +2120,8 @@ export default class MarketsPage extends BasePage { */ updateMetaOrder (mord: MetaOrder) { const { header, details, ord } = mord - if (ord.status <= OrderUtil.StatusBooked || OrderUtil.hasActiveMatches(ord)) header.activeLight.classList.add('active') - else header.activeLight.classList.remove('active') details.status.textContent = header.status.textContent = OrderUtil.statusString(ord) - details.age.textContent = Doc.timeSinceFromMs(ord.submitTime) + details.age.textContent = Doc.ageSinceFromMs(ord.submitTime) details.filled.textContent = `${(OrderUtil.filled(ord) / ord.qty * 100).toFixed(1)}%` details.settled.textContent = `${(OrderUtil.settled(ord) / ord.qty * 100).toFixed(1)}%` } @@ -2219,7 +2434,7 @@ export default class MarketsPage extends BasePage { /* showCancel shows a form to confirm submission of a cancel order. */ showCancel (row: HTMLElement, orderID: string) { - const ord = this.userOrders[orderID].ord + const ord = this.recentlyActiveUserOrders[orderID].ord const page = this.page const remaining = ord.qty - ord.filled const asset = OrderUtil.isMarketBuy(ord) ? this.market.quote : this.market.base @@ -2407,9 +2622,9 @@ export default class MarketsPage extends BasePage { } handleMatchNote (note: MatchNote) { - const mord = this.userOrders[note.orderID] + const mord = this.recentlyActiveUserOrders[note.orderID] const match = note.match - if (!mord) return this.refreshActiveOrders() + if (!mord) return this.refreshRecentlyActiveOrders() else if (mord.ord.type === OrderUtil.Market && match.status === OrderUtil.NewlyMatched) { // Update the average market rate display. // Fetch and use the updated order. const ord = app().order(note.orderID) @@ -2430,17 +2645,17 @@ export default class MarketsPage extends BasePage { */ handleOrderNote (note: OrderNote) { const ord = note.order - const mord = this.userOrders[ord.id] + const mord = this.recentlyActiveUserOrders[ord.id] // - If metaOrder doesn't exist for the given order it means it was created // via bwctl and the GUI isn't aware of it, or it was an inflight order. - // refreshActiveOrders must be called to grab this order. + // refreshRecentlyActiveOrders must be called to grab this order. // - If an OrderLoaded notification is received, it means an order that was // previously not "ready to tick" (due to its wallets not being connected // and unlocked) has now become ready to tick. The active orders section // needs to be refreshed. const wasInflight = note.topic === 'AsyncOrderFailure' || note.topic === 'AsyncOrderSubmitted' if (!mord || wasInflight || (note.topic === 'OrderLoaded' && ord.readyToTick)) { - return this.refreshActiveOrders() + return this.refreshRecentlyActiveOrders() } const oldStatus = mord.ord.status mord.ord = ord @@ -2468,7 +2683,7 @@ export default class MarketsPage extends BasePage { } this.clearOrderTableEpochs() - for (const { ord, details, header } of Object.values(this.userOrders)) { + for (const { ord, details, header } of Object.values(this.recentlyActiveUserOrders)) { const alreadyMatched = note.epoch > ord.epoch switch (true) { case ord.type === OrderUtil.Limit && ord.status === OrderUtil.StatusEpoch && alreadyMatched: { @@ -2586,7 +2801,12 @@ export default class MarketsPage extends BasePage { } // Hide confirmation modal only on success. Doc.hide(page.forms) - this.refreshActiveOrders() + // refreshing UI orders with delay as a work-around for the fact that application + // notifications handling code doesn't provide any callback mechanism we can hook + // into to execute this exactly when we need to + setTimeout(() => { + this.refreshRecentlyActiveOrders() + }, 1000) // 1000ms delay } /* @@ -3540,7 +3760,7 @@ class OrderTableRowManager { // caused by races that might/will happen between different setTimeout calls since // every call executes as atomic unit with respect to other similar calls. const markUnmarkOwnOrders = () => { - const userOrders = app().orders(market.dex.host, marketID(market.baseCfg.symbol, market.quoteCfg.symbol)) + const userOrders = app().recentOrders(market.dex.host, marketID(market.baseCfg.symbol, market.quoteCfg.symbol)) let ownOrderSpotted = false for (const bin of orderBin) { for (const userOrder of userOrders) { diff --git a/client/webserver/site/src/js/order.ts b/client/webserver/site/src/js/order.ts index 324ce713e2..caa1001c97 100644 --- a/client/webserver/site/src/js/order.ts +++ b/client/webserver/site/src/js/order.ts @@ -46,7 +46,7 @@ export default class OrderPage extends BasePage { const setStamp = () => { for (const span of this.stampers) { - span.textContent = Doc.timeSinceFromMs(parseInt(span.dataset.stamp || '')) + span.textContent = Doc.ageSinceFromMs(parseInt(span.dataset.stamp || '')) } } setStamp() @@ -159,7 +159,7 @@ export default class OrderPage extends BasePage { }) tmpl.matchTimeAgo.dataset.stamp = match.stamp.toString() - tmpl.matchTimeAgo.textContent = Doc.timeSinceFromMs(match.stamp) + tmpl.matchTimeAgo.textContent = Doc.ageSinceFromMs(match.stamp) this.stampers.push(tmpl.matchTimeAgo) const orderPortion = OrderUtil.orderPortion(this.order, match) @@ -457,7 +457,7 @@ export default class OrderPage extends BasePage { const loaded = app().loading(this.page.accelerateBttn) await this.accelerateOrderForm.refresh(this.order) loaded() - this.showForm(this.page.accelerateForm) + await this.showForm(this.page.accelerateForm) } /* diff --git a/client/webserver/site/src/js/orders.ts b/client/webserver/site/src/js/orders.ts index 7276de2cc8..60c709e250 100644 --- a/client/webserver/site/src/js/orders.ts +++ b/client/webserver/site/src/js/orders.ts @@ -206,7 +206,7 @@ export default class OrdersPage extends BasePage { tmpl.filled.textContent = `${(OrderUtil.filled(ord) / ord.qty * 100).toFixed(1)}%` tmpl.settled.textContent = `${(OrderUtil.settled(ord) / ord.qty * 100).toFixed(1)}%` const dateTime = new Date(ord.submitTime).toLocaleString() - tmpl.timeAgo.textContent = `${Doc.timeSinceFromMs(ord.submitTime)} ago` + tmpl.timeAgo.textContent = `${Doc.ageSinceFromMs(ord.submitTime)} ago` tmpl.time.textContent = dateTime const link = Doc.tmplElement(tr, 'link') link.href = `order/${ord.id}` diff --git a/client/webserver/site/src/js/orderutil.ts b/client/webserver/site/src/js/orderutil.ts index aa818f8312..402dd27e7a 100644 --- a/client/webserver/site/src/js/orderutil.ts +++ b/client/webserver/site/src/js/orderutil.ts @@ -106,7 +106,7 @@ export function statusString (order: Order): string { } /* filled sums the quantities of non-cancel matches available. */ -export function filled (order: Order) { +export function filled (order: Order): number { if (!order.matches) return 0 const qty = isMarketBuy(order) ? (m: Match) => m.qty * m.rate / RateEncodingFactor : (m: Match) => m.qty return order.matches.reduce((filled, match) => { @@ -116,7 +116,7 @@ export function filled (order: Order) { } /* settled sums the quantities of the matches that have completed. */ -export function settled (order: Order) { +export function settled (order: Order): number { if (!order.matches) return 0 const qty = isMarketBuy(order) ? (m: Match) => m.qty * m.rate / RateEncodingFactor : (m: Match) => m.qty return order.matches.reduce((settled, match) => { @@ -168,7 +168,7 @@ export function orderPortion (order: Order, match: Match) : string { * matchStatusString is a string used to create a displayable string describing * describing the match status. */ -export function matchStatusString (m: Match) { +export function matchStatusString (m: Match): string { if (m.revoked) { // When revoked, match status is less important than pending action if still // active, or the outcome if inactive. diff --git a/client/webserver/site/src/js/registry.ts b/client/webserver/site/src/js/registry.ts index 8316642e13..c04f859163 100644 --- a/client/webserver/site/src/js/registry.ts +++ b/client/webserver/site/src/js/registry.ts @@ -745,11 +745,13 @@ export interface OrderFilterMarket { export interface OrderFilter { n?: number + fresherThanUnixMs?: number offset?: string hosts?: string[] assets?: number[] market?: OrderFilterMarket statuses?: number[] + filledOnly?: boolean } export interface OrderPlacement { @@ -1306,7 +1308,7 @@ export interface Application { prependNoteElement (note: CoreNote, skipSave?: boolean): void prependListElement (noteList: HTMLElement, note: CoreNote, el: NoteElement): void loading (el: HTMLElement): () => void - orders (host: string, mktID: string): Order[] + recentOrders (host: string, mktID: string): Order[] haveActiveOrders (assetID: number): boolean order (oid: string): Order | null canAccelerateOrder(order: Order): boolean diff --git a/client/webserver/site/src/js/wallets.ts b/client/webserver/site/src/js/wallets.ts index 8cd1b5a9e5..878de0db92 100644 --- a/client/webserver/site/src/js/wallets.ts +++ b/client/webserver/site/src/js/wallets.ts @@ -255,7 +255,7 @@ export default class WalletsPage extends BasePage { const setStamp = () => { for (const span of this.stampers) { if (span.dataset.stamp) { - span.textContent = Doc.timeSinceFromMs(parseInt(span.dataset.stamp || '') * 1000) + span.textContent = Doc.ageSinceFromMs(parseInt(span.dataset.stamp || '') * 1000) } } } @@ -425,7 +425,7 @@ export default class WalletsPage extends BasePage { setInterval(() => { for (const row of this.page.txHistoryTableBody.children) { const age = Doc.tmplElement(row as PageElement, 'age') - age.textContent = Doc.timeSinceFromMs(parseInt(age.dataset.timestamp as string)) + age.textContent = Doc.ageSinceFromMs(parseInt(age.dataset.timestamp as string)) } }, 5000) } @@ -1288,7 +1288,7 @@ export default class WalletsPage extends BasePage { page.ticketHistoryRows.appendChild(tr) app().bindUrlHandlers(tr) const tmpl = Doc.parseTemplate(tr) - tmpl.age.textContent = Doc.timeSinceFromMs(tx.stamp * 1000) + tmpl.age.textContent = Doc.ageSinceFromMs(tx.stamp * 1000) tmpl.price.textContent = Doc.formatFullPrecision(tx.ticketPrice, ui) tmpl.status.textContent = intl.prep(ticketStatusTranslationKeys[status]) tmpl.hashStart.textContent = tx.hash.slice(0, 6) @@ -1659,7 +1659,7 @@ export default class WalletsPage extends BasePage { tmpl.toSymbol.appendChild(Doc.symbolize(to, true)) tmpl.status.textContent = OrderUtil.statusString(ord) tmpl.filled.textContent = `${(OrderUtil.filled(ord) / ord.qty * 100).toFixed(1)}%` - tmpl.age.textContent = Doc.timeSinceFromMs(ord.submitTime) + tmpl.age.textContent = Doc.ageSinceFromMs(ord.submitTime) tmpl.link.href = `order/${ord.id}` app().bindInternalNavigation(row) } @@ -1681,7 +1681,7 @@ export default class WalletsPage extends BasePage { } const amtAssetUI = app().unitInfo(amtAssetID) const feesAssetUI = app().unitInfo(feesAssetID) - tmpl.age.textContent = Doc.timeSinceFromMs(tx.timestamp * 1000) + tmpl.age.textContent = Doc.ageSinceFromMs(tx.timestamp * 1000) tmpl.age.dataset.timestamp = String(tx.timestamp * 1000) Doc.setVis(tx.timestamp === 0, tmpl.pending) Doc.setVis(tx.timestamp !== 0, tmpl.age)
[[[Age]]]