Rainforest Cheng
返回作品集

案例研究

Hashgreen DEX

前端工程師 · Hashgreen Labs · 2022 – 至今

以 Next.js 打造的掛單簿交易所,用於交易 Chia CAT 代幣,具備各市場獨立路由與即時掛單簿。

Hashgreen DEX trading interface with the price chart, order book and trade history
1 / 3

Trading — chart, order book & trade history

500+ 個 CAT,一份可搜尋的清單

分頁、可模糊搜尋的市場清單,能捲動五百多個資產,卻只掛載螢幕上看得到的列。

前端由我主導,這份清單也由我負責。我拒絕在渲染速度和可搜尋性之間二選一,所以疊了三個彼此獨立的機制 —— 視窗化、延遲抓取、模糊評分 —— 各自解決一個面向。Fuse 的 threshold 刻意設為 0.1:代號很短、容易碰撞。

TableVirtuoso 只掛載視野內的列;MarketStoreonBecomeObserved/onBecomeUnobserved,讓某個市場在有人觀察前先不抓資料;Fusethreshold: 0.1 對貨幣代號與名稱評分。

HGNHGN / USDC
HGN2HGN2 / USDC
HGN3HGN3 / USDC
HGNXHGNX / USDC
HGNPROHGNPRO / USDC
HGNLPHGNLP / USDC
HGNMINIHGNMINI / USDC
HGNMAXHGNMAX / USDC

Mirrors src/components/Markets/MarketList.tsx · src/stores/MarketStore.ts · src/utils/fuse.ts

讀盤,但不對已消失的價位下手

即時的買/賣掛單簿,點某個價位會開出摘要面板,而當該價位移出盤面時會立刻關閉。

我守住一條不變式:交易者絕不能對一個已經移動的價位下手。錨定在某列的 tooltip,當那列被取代時,是一個一級事件、會關掉 popover —— 而不是渲染時的事後補救。

一個對帳的 useEffectkey(market, price, amount, total) 為每個價位編鍵,一旦這個 key 不再對應到當前盤面中的任何項目,就立刻清掉選取 —— 這正是正式介面裡那個分辨買賣方向的 react-popper tooltip 所依賴的同一條不變式,以免錨定到一列早已不存在的資料。

HGN-USDC
priceamounttotal
1.2431mid

Mirrors src/components/Trade/OrderBook.tsx · src/components/Trade/OfferTooltip.tsx

先抓一次,然後串流

統計數據與成交流水先用 REST 載入一次,再透過單一訂閱轉為即時 —— 還有一個會反映 socket 狀態的小圓點。

我把串流做成一份可重用的契約。每個即時畫面都以同樣方式 hydrate —— 一次 REST、再一個 hook —— 每個訂閱在 unmount 時拆除,頻道改變時再乾淨地重新訂閱。

一個 useAbly({ channelName, events, callback }) hook 掌管整個訂閱生命週期;ablyIndicator 讀取 Ably 的連線狀態來驅動那個小圓點。

idle
Live
last price
1.2431
24h high
1.3129
24h low
1.1904
  • 14:02:11buy 1.243142.50
  • 14:02:13sell 1.242818.20
  • 14:02:16buy 1.243365.70
useAbly lifecycle
  • idle — flip Live to subscribe; switch market to see the channel swap.

Mirrors src/hooks/useAbly.ts · src/components/Navbar/ablyIndicator.tsx

一個狀態機,三個錢包

透過 WalletConnect 連接 Goby、Hoogii 或 Chia —— 全都收斂成同一個縮短的地址與同一個已連線狀態。

我以單一事實來源 —— WalletStageEnum —— 來架構這一切,而不是一堆散落的布林值。WalletConnect 的配對是它自己明確的一個階段,因為配對可能被拒絕或逾時,而這些必須是一級狀態,不是事後補上的錯誤處理。

一個小小的階段機 —— Unspecified → Initial → Pairing → Connected —— 疊在各錢包的 Wallet 類別之上;已連線的帳戶對外提供 shortenAddressiconUrl

Stage is Unspecified until the user opts in.

stage machine
  1. Unspecified
  2. Initial
  3. Pairing
  4. Connected

Mirrors src/stores/WalletStore.ts · src/utils/wallets/*.ts

修補一列,還是重抓整頁?

即時的委託歷史 —— 可分頁、可篩選 —— 會逐一事件決定要重抓整頁還是修補一列。

同步策略由我負責。真正的問題是:一筆即時更新,是否還屬於你正在看的這一頁。只有當事件改變了頁面歸屬時才重抓;否則就原地修補那一列,其餘一概不動。

一個以頁為鍵的快取,由一個 scope 在 auth.id 的 Ably 頻道餵資料。每個事件會跑 needRefetch() = checkMarket && checkStatus && checkDateRange;當它為 false 時,checkUserOrderCached() 只把那一列原地替換掉。

ordermarketpriceamountstatusage
ORD-101HGN-USDCbuy 1.2401120Active2m
ORD-102HGN-USDCsell 1.248960Active4h
ORD-103XCH-USDCbuy 18.25Filled9m
ORD-104SBX-USDCbuy 0.0414000Active5d
ORD-105HGN-USDCsell 1.2630Cancelled14m
page 1 / 2
Fills a random Active order, then decides the branch against your filters.

Mirrors src/stores/OrderStore.ts · src/hooks/useUserOrders.ts