Skip to content

Commit

Permalink
multi: optimize MaxOrder and fix maxSell API hammering
Browse files Browse the repository at this point in the history
This optimizes MaxOrder by using a binary search for utxo selection
instead of a dumb brute force search, in addition to first checking if
the largest UTXO is not enough.

This also fixes the frontend hammering the /api/maxSell endpoint.
  • Loading branch information
buck54321 authored Jul 3, 2022
1 parent c68b82f commit add7978
Show file tree
Hide file tree
Showing 8 changed files with 49 additions and 46 deletions.
27 changes: 16 additions & 11 deletions client/asset/btc/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -1756,18 +1756,23 @@ func fund(utxos []*compositeUTXO, enough func(uint64, uint64) bool) (
if len(okUTXOs) == 0 {
return false
}
// On each loop, find the smallest UTXO that is enough.
for _, txout := range okUTXOs {
if isEnoughWith(txout) {
addUTXO(txout)
return true
}

// Check if the largest output is too small.
lastUTXO := okUTXOs[len(okUTXOs)-1]
if !isEnoughWith(lastUTXO) {
addUTXO(lastUTXO)
okUTXOs = okUTXOs[0 : len(okUTXOs)-1]
continue
}
// No single UTXO was large enough. Add the largest (the last
// output) and continue.
addUTXO(okUTXOs[len(okUTXOs)-1])
// Pop the utxo.
okUTXOs = okUTXOs[:len(okUTXOs)-1]

// We only need one then. Find it.
idx := sort.Search(len(okUTXOs), func(i int) bool {
return isEnoughWith(okUTXOs[i])
})
// No need to check idx == len(okUTXOs). We already verified that the last
// utxo passes above.
addUTXO(okUTXOs[idx])
return true
}
}

Expand Down
1 change: 0 additions & 1 deletion client/asset/btc/btc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2813,7 +2813,6 @@ func testPreRedeem(t *testing.T, segwit bool, walletType string) {
defer shutdown()

preRedeem, err := wallet.PreRedeem(&asset.PreRedeemForm{
LotSize: 123456, // Doesn't actually matter
Lots: 5,
AssetConfig: tBTC,
})
Expand Down
34 changes: 22 additions & 12 deletions client/asset/dcr/dcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -1457,6 +1457,10 @@ func (dcr *ExchangeWallet) tryFund(utxos []*compositeUTXO, enough func(sum uint6
return nil
}

isEnoughWith := func(utxo *compositeUTXO) bool {
return enough(sum, size, utxo)
}

tryUTXOs := func(minconf int64) (ok bool, err error) {
sum, size = 0, 0
coins, spents, redeemScripts = nil, nil, nil
Expand All @@ -1473,22 +1477,27 @@ func (dcr *ExchangeWallet) tryFund(utxos []*compositeUTXO, enough func(sum uint6
if len(okUTXOs) == 0 {
return false, nil
}
// On each loop, find the smallest UTXO that is enough.
for _, txout := range okUTXOs {
if enough(sum, size, txout) {
if err = addUTXO(txout); err != nil {
return false, err
}
return true, nil

// Check if the largest output is too small.
lastUTXO := okUTXOs[len(okUTXOs)-1]
if !isEnoughWith(lastUTXO) {
if err = addUTXO(lastUTXO); err != nil {
return false, err
}
okUTXOs = okUTXOs[0 : len(okUTXOs)-1]
continue
}
// No single UTXO was large enough. Add the largest (the last
// output) and continue.
if err = addUTXO(okUTXOs[len(okUTXOs)-1]); err != nil {

// We only need one then. Find it.
idx := sort.Search(len(okUTXOs), func(i int) bool {
return isEnoughWith(okUTXOs[i])
})
// No need to check idx == -1. We already verified that the last
// utxo passes above.
if err = addUTXO(okUTXOs[idx]); err != nil {
return false, err
}
// Pop the utxo.
okUTXOs = okUTXOs[:len(okUTXOs)-1]
return true, nil
}
}

Expand All @@ -1497,6 +1506,7 @@ func (dcr *ExchangeWallet) tryFund(utxos []*compositeUTXO, enough func(sum uint6
if err != nil {
return 0, 0, nil, nil, nil, err
}

// Fallback to allowing 0-conf outputs.
if !ok {
ok, err = tryUTXOs(0)
Expand Down
1 change: 0 additions & 1 deletion client/asset/dcr/dcr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2508,7 +2508,6 @@ func TestPreRedeem(t *testing.T) {
defer shutdown()

preRedeem, err := wallet.PreRedeem(&asset.PreRedeemForm{
LotSize: 123456, // Doesn't actually matter
Lots: 5,
AssetConfig: tDCR,
})
Expand Down
4 changes: 0 additions & 4 deletions client/asset/estimation.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,6 @@ type PreSwap struct {

// PreRedeemForm can be used to get a redemption estimate.
type PreRedeemForm struct {
// LotSize is the lot size for the calculation. For quote assets, LotSize
// should be based on either the user's limit order rate, or some measure
// of the current market rate.
LotSize uint64
// Lots is the number of lots in the order.
Lots uint64
// FeeSuggestion is a suggested fee from the server.
Expand Down
1 change: 0 additions & 1 deletion client/asset/eth/eth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1684,7 +1684,6 @@ func TestPreRedeem(t *testing.T) {
defer shutdown()

form := &asset.PreRedeemForm{
LotSize: 123456,
Lots: 5,
FeeSuggestion: 100,
AssetConfig: tETH,
Expand Down
15 changes: 3 additions & 12 deletions client/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -3728,7 +3728,6 @@ func (c *Core) MaxBuy(host string, base, quote uint32, rate uint64) (*MaxOrderEs
}

preRedeem, err := baseWallet.PreRedeem(&asset.PreRedeemForm{
LotSize: lotSize,
Lots: maxBuy.Lots,
FeeSuggestion: redeemFeeSuggestion,
AssetConfig: baseAsset,
Expand Down Expand Up @@ -3791,14 +3790,7 @@ func (c *Core) MaxSell(host string, base, quote uint32) (*MaxOrderEstimate, erro
return nil, fmt.Errorf("%s wallet MaxOrder error: %v", unbip(base), err)
}

midGap, err := book.MidGap()
if err != nil {
return nil, fmt.Errorf("error calculating market rate for %s at %s: %v", mktID, host, err)
}
lotSize = calc.BaseToQuote(midGap, lotSize)

preRedeem, err := quoteWallet.PreRedeem(&asset.PreRedeemForm{
LotSize: lotSize,
Lots: maxSell.Lots,
FeeSuggestion: redeemFeeSuggestion,
})
Expand Down Expand Up @@ -4201,13 +4193,13 @@ func (c *Core) PreOrder(form *TradeForm) (*OrderEstimate, error) {
return nil, fmt.Errorf("failed to get redeem fee suggestion for %s at %s", unbip(wallets.toAsset.ID), form.Host)
}

fromLotSize, toLotSize := lotSize, calc.BaseToQuote(rate, lotSize)
swapLotSize := lotSize
if !form.Sell {
fromLotSize, toLotSize = toLotSize, fromLotSize
swapLotSize = calc.BaseToQuote(rate, lotSize)
}

swapEstimate, err := wallets.fromWallet.PreSwap(&asset.PreSwapForm{
LotSize: fromLotSize,
LotSize: swapLotSize,
Lots: lots,
AssetConfig: wallets.fromAsset,
RedeemConfig: wallets.toAsset,
Expand All @@ -4220,7 +4212,6 @@ func (c *Core) PreOrder(form *TradeForm) (*OrderEstimate, error) {
}

redeemEstimate, err := wallets.toWallet.PreRedeem(&asset.PreRedeemForm{
LotSize: toLotSize,
Lots: lots,
FeeSuggestion: redeemFeeSuggestion,
SelectedOptions: form.Options,
Expand Down
12 changes: 8 additions & 4 deletions client/webserver/site/src/js/markets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ interface CurrentMarket {
quote: SupportedAsset
baseUnitInfo: UnitInfo
quoteUnitInfo: UnitInfo
maxSellRequested: boolean
maxSell: MaxOrderEstimate | null
sellBalance: number
buyBalance: number
Expand Down Expand Up @@ -707,6 +708,7 @@ export default class MarketsPage extends BasePage {
quoteUnitInfo: qui,
maxSell: null,
maxBuys: {},
maxSellRequested: false,
candleCaches: {},
baseCfg,
quoteCfg,
Expand Down Expand Up @@ -872,7 +874,6 @@ export default class MarketsPage extends BasePage {
* preSell populates the max order message for the largest available sell.
*/
preSell () {
this.maxOrderUpdateCounter++
const mkt = this.market
const baseWallet = app().assets[mkt.base.id].wallet
if (baseWallet.balance.available < mkt.cfg.lotsize) {
Expand All @@ -883,8 +884,11 @@ export default class MarketsPage extends BasePage {
this.setMaxOrder(mkt.maxSell.swap)
return
}
if (mkt.maxSellRequested) return
mkt.maxSellRequested = true
// We only fetch pre-sell once per balance update, so don't delay.
this.scheduleMaxEstimate('/api/maxsell', {}, 0, (res: MaxSell) => {
mkt.maxSellRequested = false
mkt.maxSell = res.maxSell
mkt.sellBalance = baseWallet.balance.available
this.setMaxOrder(res.maxSell.swap)
Expand All @@ -895,7 +899,6 @@ export default class MarketsPage extends BasePage {
* preBuy populates the max order message for the largest available buy.
*/
preBuy () {
this.maxOrderUpdateCounter++
const mkt = this.market
const rate = this.adjustedRate()
const quoteWallet = app().assets[mkt.quote.id].wallet
Expand All @@ -910,8 +913,8 @@ export default class MarketsPage extends BasePage {
}
// 0 delay for first fetch after balance update or market change, otherwise
// meter these at 1 / sec.
const delay = mkt.maxBuys ? 1000 : 0
this.scheduleMaxEstimate('/api/maxbuy', { rate: rate }, delay, (res: MaxBuy) => {
const delay = Object.keys(mkt.maxBuys).length ? 350 : 0
this.scheduleMaxEstimate('/api/maxbuy', { rate }, delay, (res: MaxBuy) => {
mkt.maxBuys[rate] = res.maxBuy
mkt.buyBalance = app().assets[mkt.quote.id].wallet.balance.available
this.setMaxOrder(res.maxBuy.swap)
Expand All @@ -935,6 +938,7 @@ export default class MarketsPage extends BasePage {
Doc.hide(page.maxAboveZero)
page.maxFromLots.textContent = intl.prep(intl.ID_CALCULATING)
page.maxFromLotsLbl.textContent = ''
this.maxOrderUpdateCounter++
const counter = this.maxOrderUpdateCounter
this.maxEstimateTimer = window.setTimeout(async () => {
this.maxEstimateTimer = null
Expand Down

0 comments on commit add7978

Please sign in to comment.