Skip to content

Commit

Permalink
mmui: Fix bugs related to cex connection and balance (#3060)
Browse files Browse the repository at this point in the history
- When CEX not connected, show error message when clicking allocate or
  reconfigure buttons.
- Add button to remove bot config from mm page.
- Unwrap cex connection errors.
- Add ablity to modify testbinance balances.
  • Loading branch information
martonp authored Nov 11, 2024
1 parent b7a3c33 commit 4d8aeca
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 66 deletions.
98 changes: 80 additions & 18 deletions client/cmd/testbinance/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"encoding/json"
"flag"
"fmt"
"io"
"math"
"math/rand"
"net/http"
Expand Down Expand Up @@ -150,15 +151,50 @@ func makeCoinpapAsset(assetID uint32, symbol, name string) *fiatrates.Coinpaprik
}
}

// sendBalanceUpdateRequest sends a balance update request to the testbinance server
// running in another process.
func sendBalanceUpdateRequest(coin string, balanceUpdate float64) {
if coin == "" || balanceUpdate == 0 {
fmt.Printf("Invalid balance update request: coin = %q, balanceUpdate = %f\n", coin, balanceUpdate)
return
}

url := fmt.Sprintf("http://localhost:37346/testbinance/updatebalance?coin=%s&amt=%f",
coin, balanceUpdate)
resp, err := http.Get(url)
if err != nil {
log.Errorf("Error sending balance update request: %v", err)
return
}

defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
fmt.Println("Balance update request failed:", string(body))
return
}

fmt.Println("Balance update request sent")
}

func main() {
var logDebug, logTrace bool
var coin string
var balanceUpdate float64
flag.Float64Var(&walkingSpeedAdj, "walkspeed", 1.0, "scale the maximum walking speed. default scale of 1.0 is about 3%")
flag.Float64Var(&gapRange, "gaprange", 0.04, "a ratio of how much the gap can vary. default is 0.04 => 4%")
flag.BoolVar(&logDebug, "debug", false, "use debug logging")
flag.BoolVar(&logTrace, "trace", false, "use trace logging")
flag.BoolVar(&flappyWS, "flappyws", false, "periodically drop websocket clients and delete subscriptions")
flag.Float64Var(&balanceUpdate, "balupdate", 0, "update the balance of an asset on a testbinance server running as another process")
flag.StringVar(&coin, "coin", "", "coin for testbinance admin update")
flag.Parse()

if balanceUpdate != 0 {
sendBalanceUpdateRequest(coin, balanceUpdate)
return
}

switch {
case logTrace:
log = dex.StdOutLogger("TB", dex.LevelTrace)
Expand Down Expand Up @@ -312,10 +348,32 @@ func newFakeBinanceServer(ctx context.Context) (*fakeBinance, error) {

mux.Get("/ws/{listenKey}", f.handleAccountSubscription)
mux.Get("/stream", f.handleMarketStream)
mux.Route("/testbinance", func(r chi.Router) {
r.Get("/updatebalance", f.handleUpdateBalance)
})

return f, nil
}

func (f *fakeBinance) handleUpdateBalance(w http.ResponseWriter, r *http.Request) {
coin := r.URL.Query().Get("coin")
amtStr := r.URL.Query().Get("amt")
amt, err := strconv.ParseFloat(amtStr, 64)
if err != nil {
http.Error(w, fmt.Sprintf("invalid amt %q: %v", amtStr, err), http.StatusBadRequest)
return
}

balUpdate := f.updateBalance(coin, amt)
if balUpdate == nil {
http.Error(w, fmt.Sprintf("no balance to update for %q", coin), http.StatusBadRequest)
return
}

f.sendBalanceUpdates([]*bntypes.WSBalance{balUpdate})
w.WriteHeader(http.StatusOK)
}

func (f *fakeBinance) run(ctx context.Context) {
// Start a ticker to do book shuffles.

Expand Down Expand Up @@ -779,6 +837,27 @@ func (f *fakeBinance) handleGetDepositAddress(w http.ResponseWriter, r *http.Req
writeJSONWithStatus(w, resp, http.StatusOK)
}

func (f *fakeBinance) updateBalance(coin string, amt float64) *bntypes.WSBalance {
f.balancesMtx.Lock()
defer f.balancesMtx.Unlock()

var balUpdate *bntypes.WSBalance

for _, b := range f.balances {
if b.Asset == coin {
if amt+b.Free < 0 {
b.Free = 0
} else {
b.Free += amt
}
balUpdate = (*bntypes.WSBalance)(b)
break
}
}

return balUpdate
}

func (f *fakeBinance) handleWithdrawal(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
apiKey := extractAPIKey(r)
Expand Down Expand Up @@ -814,24 +893,7 @@ func (f *fakeBinance) handleWithdrawal(w http.ResponseWriter, r *http.Request) {
}
f.withdrawalHistoryMtx.Unlock()

var balUpdate *bntypes.WSBalance
debitBalance := func(coin string, amt float64) {
for _, b := range f.balances {
if b.Asset == coin {
if amt > b.Free {
b.Free = 0
} else {
b.Free -= amt
}
}
balUpdate = (*bntypes.WSBalance)(b)
}
}

f.balancesMtx.Lock()
debitBalance(coin, amt)
f.balancesMtx.Unlock()

balUpdate := f.updateBalance(coin, -amt)
f.sendBalanceUpdates([]*bntypes.WSBalance{balUpdate})

resp := struct {
Expand Down
4 changes: 2 additions & 2 deletions client/mm/libxc/binance.go
Original file line number Diff line number Diff line change
Expand Up @@ -663,11 +663,11 @@ func (bnc *binance) Connect(ctx context.Context) (*sync.WaitGroup, error) {
wg := new(sync.WaitGroup)

if err := bnc.getCoinInfo(ctx); err != nil {
return nil, fmt.Errorf("error getting coin info: %v", err)
return nil, fmt.Errorf("error getting coin info: %w", err)
}

if _, err := bnc.getMarkets(ctx); err != nil {
return nil, fmt.Errorf("error getting markets: %v", err)
return nil, fmt.Errorf("error getting markets: %w", err)
}

if err := bnc.setBalances(ctx); err != nil {
Expand Down
18 changes: 9 additions & 9 deletions client/mm/mm.go
Original file line number Diff line number Diff line change
Expand Up @@ -510,21 +510,21 @@ func (m *MarketMaker) connectCEX(ctx context.Context, c *centralizedExchange) er
if !cm.On() {
c.connectErr = ""
if err := cm.ConnectOnce(ctx); err != nil {
c.connectErr = err.Error()
return fmt.Errorf("failed to connect to CEX: %v", err)
c.connectErr = core.UnwrapErr(err).Error()
return fmt.Errorf("failed to connect to CEX: %w", err)
}
mkts, err := c.Markets(ctx)
if err != nil {
// Probably can't get here if we didn't error on connect, but
// checking anyway.
c.connectErr = err.Error()
return fmt.Errorf("error refreshing markets: %v", err)
c.connectErr = core.UnwrapErr(err).Error()
return fmt.Errorf("error refreshing markets: %w", err)
}
c.mkts = mkts
bals, err := c.Balances(ctx)
if err != nil {
c.connectErr = err.Error()
return fmt.Errorf("error getting balances: %v", err)
c.connectErr = core.UnwrapErr(err).Error()
return fmt.Errorf("error getting balances: %w", err)
}
c.balances = bals
}
Expand Down Expand Up @@ -590,12 +590,12 @@ func (m *MarketMaker) loadCEX(ctx context.Context, cfg *CEXConfig) (*centralized
if err != nil {
m.log.Errorf("Failed to get markets for %s: %v", cfg.Name, err)
c.mkts = make(map[string]*libxc.Market)
c.connectErr = err.Error()
c.connectErr = core.UnwrapErr(err).Error()
}
if c.balances, err = c.Balances(ctx); err != nil {
m.log.Errorf("Failed to get balances for %s: %v", cfg.Name, err)
c.balances = make(map[uint32]*libxc.ExchangeBalance)
c.connectErr = err.Error()
c.connectErr = core.UnwrapErr(err).Error()
}
m.cexes[cfg.Name] = c
success = true
Expand Down Expand Up @@ -1390,7 +1390,7 @@ func (m *MarketMaker) connectedCEX(cexName string) (*centralizedExchange, error)

err := m.connectCEX(m.ctx, cex)
if err != nil {
return nil, fmt.Errorf("error connecting to CEX: %v", err)
return nil, fmt.Errorf("error connecting to CEX: %w", err)
}

return cex, nil
Expand Down
4 changes: 4 additions & 0 deletions client/webserver/jsintl.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ const (
idOrderReportTitle = "ORDER_REPORT_TITLE"
idCEXBalances = "CEX_BALANCES"
idCausesSelfMatch = "CAUSES_SELF_MATCH"
idCexNotConnected = "CEX_NOT_CONNECTED"
idRemovingBotConfig = "REMOVING_BOT_CONFIG"
)

var enUS = map[string]*intl.Translation{
Expand Down Expand Up @@ -433,6 +435,8 @@ var enUS = map[string]*intl.Translation{
idOrderReportTitle: {T: "{{ side }} orders report for epoch #{{ epochNum }}"},
idCEXBalances: {T: "{{ cexName }} Balances"},
idCausesSelfMatch: {T: "This order would cause a self-match"},
idCexNotConnected: {T: "{{ cexName }} not connected"},
idRemovingBotConfig: {T: "Are you sure you want to remove the config for the {{ baseSymbol }}-{{ quoteSymbol }} market on {{ host }}?"},
}

var ptBR = map[string]*intl.Translation{
Expand Down
1 change: 1 addition & 0 deletions client/webserver/locales/en-us.go
Original file line number Diff line number Diff line change
Expand Up @@ -678,4 +678,5 @@ var EnUS = map[string]*intl.Translation{
"Priority": {T: "Priority"},
"Wallet Balances": {T: "Wallet Balances"},
"Placements": {T: "Placements"},
"removing_bot_config": {T: "Removing Bot Config"},
}
16 changes: 16 additions & 0 deletions client/webserver/site/src/html/mm.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,12 @@
<button data-tmpl="reconfigureBttn">
<span class="ico-settings fs17"></span>
</button>
<button class="ms-1 danger" data-tmpl="removeBttn">
<span class="ico-cross fs17"></span>
</button>
</div>
</div>
<span data-tmpl="offError" class="fs16 px-3 text-center text-warning d-hide"></span>

{{- /* PROJECTED ALLOCATIONS */ -}}
<div class="flex-stretch-column px-3 pt-2 mt-2 border-top">
Expand Down Expand Up @@ -609,6 +613,18 @@
<form class="position-relative d-hide" id="orderReportForm" autocomplete="off">
{{template "orderReportForm"}}
</form>

<form class="position-relative mw-425 d-hide" id="confirmRemoveForm" autocomplete="off">
<div class="form-closer"><span class="ico-cross"></span></div>
<div>
<header>[[[removing_bot_config]]]</header>
<div>
<span id="confirmRemoveCfgMsg"></span>
</div>
<button id="confirmRemoveConfigBttn" type="button" class="feature mt-2">Confirm</button>
<div id="removeCfgErr" class="fs15 text-center d-hide text-danger text-break"></div>
</div>
</form>
</div> {{- /* END FORMS */ -}}
</div>
{{template "bottom"}}
Expand Down
11 changes: 11 additions & 0 deletions client/webserver/site/src/js/doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,17 @@ export default class Doc {
for (const el of els) el.classList.remove('d-hide')
}

/*
* showTemporarily shows the specified elements for the specified time, then
* hides it again.
*/
static showTemporarily (timeout: number, ...els: Element[]) {
this.show(...els)
setTimeout(() => {
this.hide(...els)
}, timeout)
}

/*
* show or hide the specified elements, based on value of the truthiness of
* vis.
Expand Down
12 changes: 5 additions & 7 deletions client/webserver/site/src/js/forms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2151,14 +2151,13 @@ export class TokenApprovalForm {
export class CEXConfigurationForm {
form: PageElement
page: Record<string, PageElement>
success: (cexName: string) => void
updated: (cexName: string, success: boolean) => void
cexName: string

constructor (form: PageElement, success: (cexName: string) => void) {
constructor (form: PageElement, updated: (cexName: string, success: boolean) => void) {
this.form = form
this.success = success
this.updated = updated
this.page = Doc.parseTemplate(form)

Doc.bind(this.page.cexSubmit, 'click', () => this.submit())
}

Expand Down Expand Up @@ -2202,12 +2201,11 @@ export class CEXConfigurationForm {
apiSecret: apiSecret
})
if (!app().checkResponse(res)) throw res
await app().fetchMMStatus()
this.success(cexName)
this.updated(cexName, true)
} catch (e) {
Doc.show(page.cexFormErr)
page.cexFormErr.textContent = intl.prep(intl.ID_API_ERROR, { msg: e.msg ?? String(e) })
return
this.updated(cexName, false)
} finally {
loaded()
}
Expand Down
2 changes: 2 additions & 0 deletions client/webserver/site/src/js/locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ export const ID_CEX_TRADE_ERROR = 'CEX_TRADE_ERROR'
export const ID_ORDER_REPORT_TITLE = 'ORDER_REPORT_TITLE'
export const ID_CEX_BALANCES = 'CEX_BALANCES'
export const ID_CAUSES_SELF_MATCH = 'CAUSES_SELF_MATCH'
export const ID_CEX_NOT_CONNECTED = 'CEX_NOT_CONNECTED'
export const ID_REMOVING_BOT_CONFIG = 'REMOVING_BOT_CONFIG'

let locale: Locale

Expand Down
Loading

0 comments on commit 4d8aeca

Please sign in to comment.