Skip to content

Commit

Permalink
client/{mm,core}: Market maker balance segregation (#2332)
Browse files Browse the repository at this point in the history
* client/{mm,core}: Market maker balance segregation

New configurations are added to the market maker config specifying what
part of the wallet's balance each market maket will have control over.
Using this config, a `balanceHandler` object is created, which tracks
each of the orders made by the market makers, decreasing and increasing
their balances as needed over the lifecycle of the order. The
`balanceHandler` is not called directly by the market makers, instead a
`coreWithSegregatedBalance` is created which wraps the core methods so
that from the point of view of the bot, they behave as if the entire
balance of the wallet is only what has been allocated to that bot by
the `balanceHandler`.
  • Loading branch information
martonp authored Aug 6, 2023
1 parent fce2a65 commit de675a6
Show file tree
Hide file tree
Showing 16 changed files with 3,937 additions and 48 deletions.
30 changes: 30 additions & 0 deletions client/asset/btc/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -6148,6 +6148,36 @@ func (btc *baseWallet) FundMultiOrder(mo *asset.MultiOrder, maxLock uint64) ([]a
return btc.fundMulti(maxLock, mo.Values, mo.FeeSuggestion, mo.MaxFeeRate, useSplit, splitBuffer)
}

// MaxFundingFees returns the maximum funding fees for an order/multi-order.
func (btc *baseWallet) MaxFundingFees(numTrades uint32, options map[string]string) uint64 {
useSplit := btc.useSplitTx()
if options != nil {
if split, ok := options[splitKey]; ok {
useSplit, _ = strconv.ParseBool(split)
}
if split, ok := options[multiSplitKey]; ok {
useSplit, _ = strconv.ParseBool(split)
}
}
if !useSplit {
return 0
}

var inputSize, outputSize uint64
if btc.segwit {
inputSize = dexbtc.RedeemP2WPKHInputTotalSize
outputSize = dexbtc.P2WPKHOutputSize
} else {
inputSize = dexbtc.RedeemP2PKHInputSize
outputSize = dexbtc.P2PKHOutputSize
}

const numInputs = 12 // plan for lots of inputs to get a safe estimate

txSize := dexbtc.MinimumTxOverhead + numInputs*inputSize + uint64(numTrades+1)*outputSize
return btc.feeRateLimit() * txSize
}

type utxo struct {
txHash *chainhash.Hash
vout uint32
Expand Down
44 changes: 44 additions & 0 deletions client/asset/btc/btc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2018,6 +2018,50 @@ func testFundMultiOrder(t *testing.T, segwit bool, walletType string) {
}
}

func TestMaxFundingFees(t *testing.T) {
runRubric(t, testMaxFundingFees)
}

func testMaxFundingFees(t *testing.T, segwit bool, walletType string) {
wallet, _, shutdown := tNewWallet(segwit, walletType)
defer shutdown()

feeRateLimit := uint64(100)

wallet.cfgV.Store(&baseWalletConfig{
feeRateLimit: feeRateLimit,
})

useSplitOptions := map[string]string{
splitKey: "true",
}
noSplitOptions := map[string]string{
splitKey: "false",
}

var inputSize, outputSize uint64
if segwit {
inputSize = dexbtc.RedeemP2WPKHInputTotalSize
outputSize = dexbtc.P2WPKHOutputSize
} else {
inputSize = dexbtc.RedeemP2PKHInputSize
outputSize = dexbtc.P2PKHOutputSize
}

const maxSwaps = 3
const numInputs = 12
maxFundingFees := wallet.MaxFundingFees(maxSwaps, useSplitOptions)
expectedFees := feeRateLimit * (inputSize*numInputs + outputSize*(maxSwaps+1) + dexbtc.MinimumTxOverhead)
if maxFundingFees != expectedFees {
t.Fatalf("unexpected max funding fees. expected %d, got %d", expectedFees, maxFundingFees)
}

maxFundingFees = wallet.MaxFundingFees(maxSwaps, noSplitOptions)
if maxFundingFees != 0 {
t.Fatalf("unexpected max funding fees. expected 0, got %d", maxFundingFees)
}
}

func TestAvailableFund(t *testing.T) {
runRubric(t, testAvailableFund)
}
Expand Down
17 changes: 17 additions & 0 deletions client/asset/dcr/dcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -1695,6 +1695,23 @@ func (dcr *ExchangeWallet) SingleLotSwapFees(_ uint32, feeSuggestion uint64, opt
return totalTxSize * bumpedNetRate, nil
}

// MaxFundingFees returns the maximum funding fees for an order/multi-order.
func (dcr *ExchangeWallet) MaxFundingFees(numTrades uint32, options map[string]string) uint64 {
useSplit := dcr.config().useSplitTx
if options != nil {
if split, ok := options[splitKey]; ok {
useSplit, _ = strconv.ParseBool(split)
}
}
if !useSplit {
return 0
}

const numInputs = 12 // plan for lots of inputs to get a safe estimate
splitTxSize := dexdcr.MsgTxOverhead + (numInputs * dexdcr.P2PKHInputSize) + (uint64(numTrades+1) * dexdcr.P2PKHOutputSize)
return splitTxSize * dcr.config().feeRateLimit
}

// splitOption constructs an *asset.OrderOption with customized text based on the
// difference in fees between the configured and test split condition.
func (dcr *ExchangeWallet) splitOption(req *asset.PreSwapForm, utxos []*compositeUTXO, bump float64) *asset.OrderOption {
Expand Down
29 changes: 29 additions & 0 deletions client/asset/dcr/dcr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,35 @@ func TestMain(m *testing.M) {
os.Exit(doIt())
}

func TestMaxFundingFees(t *testing.T) {
wallet, _, shutdown := tNewWallet()
defer shutdown()

feeRateLimit := uint64(100)

wallet.cfgV.Store(&exchangeWalletConfig{
feeRateLimit: feeRateLimit,
})

useSplitOptions := map[string]string{
splitKey: "true",
}
noSplitOptions := map[string]string{
splitKey: "false",
}

maxFundingFees := wallet.MaxFundingFees(3, useSplitOptions)
expectedFees := feeRateLimit * (dexdcr.P2PKHInputSize*12 + dexdcr.P2PKHOutputSize*4 + dexdcr.MsgTxOverhead)
if maxFundingFees != expectedFees {
t.Fatalf("unexpected max funding fees. expected %d, got %d", expectedFees, maxFundingFees)
}

maxFundingFees = wallet.MaxFundingFees(3, noSplitOptions)
if maxFundingFees != 0 {
t.Fatalf("unexpected max funding fees. expected 0, got %d", maxFundingFees)
}
}

func TestAvailableFund(t *testing.T) {
wallet, node, shutdown := tNewWallet()
defer shutdown()
Expand Down
5 changes: 5 additions & 0 deletions client/asset/eth/eth.go
Original file line number Diff line number Diff line change
Expand Up @@ -1286,6 +1286,11 @@ func (w *assetWallet) preSwap(req *asset.PreSwapForm, feeWallet *assetWallet) (*
}, nil
}

// MaxFundingFees returns 0 because ETH does not have funding fees.
func (w *baseWallet) MaxFundingFees(_ uint32, _ map[string]string) uint64 {
return 0
}

// SingleLotSwapFees returns the fees for a swap transaction for a single lot.
func (w *assetWallet) SingleLotSwapFees(version uint32, feeSuggestion uint64, _ map[string]string) (fees uint64, err error) {
g := w.gases(version)
Expand Down
13 changes: 13 additions & 0 deletions client/asset/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const (
WalletTraitAuthenticator // The wallet require authentication.
WalletTraitShielded // The wallet is ShieldedWallet (e.g. ZCash)
WalletTraitTokenApprover // The wallet is a TokenApprover
WalletTraitAccountLocker // The wallet must have enough balance for redemptions before a trade.
)

// IsRescanner tests if the WalletTrait has the WalletTraitRescanner bit set.
Expand Down Expand Up @@ -116,6 +117,12 @@ func (wt WalletTrait) IsTokenApprover() bool {
return wt&WalletTraitTokenApprover != 0
}

// IsAccountLocker test if WalletTrait has WalletTraitAccountLocker bit set,
// which indicates the wallet implements the AccountLocker interface.
func (wt WalletTrait) IsAccountLocker() bool {
return wt&WalletTraitAccountLocker != 0
}

// DetermineWalletTraits returns the WalletTrait bitset for the provided Wallet.
func DetermineWalletTraits(w Wallet) (t WalletTrait) {
if _, is := w.(Rescanner); is {
Expand Down Expand Up @@ -160,6 +167,10 @@ func DetermineWalletTraits(w Wallet) (t WalletTrait) {
if _, is := w.(TokenApprover); is {
t |= WalletTraitTokenApprover
}
if _, is := w.(AccountLocker); is {
t |= WalletTraitAccountLocker
}

return t
}

Expand Down Expand Up @@ -504,6 +515,8 @@ type Wallet interface {
// than the number of orders, then the orders at the end of the list were
// not about to be funded.
FundMultiOrder(ord *MultiOrder, maxLock uint64) (coins []Coins, redeemScripts [][]dex.Bytes, fundingFees uint64, err error)
// MaxFundingFees returns the max fees that could be paid for funding a swap.
MaxFundingFees(numTrades uint32, options map[string]string) uint64
}

// Authenticator is a wallet implementation that require authentication.
Expand Down
42 changes: 33 additions & 9 deletions client/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -5513,29 +5513,53 @@ func (c *Core) SingleLotFees(form *SingleLotFeesForm) (uint64, uint64, error) {
return 0, 0, fmt.Errorf("client and server asset versions are incompatible for %v", form.Host)
}

swapFeeSuggestion := c.feeSuggestionAny(wallets.fromWallet.AssetID) // server rates only for the swap init
if swapFeeSuggestion == 0 {
return 0, 0, fmt.Errorf("failed to get swap fee suggestion for %s at %s", wallets.fromWallet.Symbol, form.Host)
}
var swapFeeRate, redeemFeeRate uint64

redeemFeeSuggestion := c.feeSuggestionAny(wallets.toWallet.AssetID) // wallet rate or server rate
if redeemFeeSuggestion == 0 {
return 0, 0, fmt.Errorf("failed to get redeem fee suggestion for %s at %s", wallets.toWallet.Symbol, form.Host)
if form.UseMaxFeeRate {
dc.assetsMtx.Lock()
swapAsset, redeemAsset := dc.assets[wallets.fromWallet.AssetID], dc.assets[wallets.toWallet.AssetID]
dc.assetsMtx.Unlock()
if swapAsset == nil {
return 0, 0, fmt.Errorf("no asset found for %d", wallets.fromWallet.AssetID)
}
if redeemAsset == nil {
return 0, 0, fmt.Errorf("no asset found for %d", wallets.toWallet.AssetID)
}
swapFeeRate, redeemFeeRate = swapAsset.MaxFeeRate, redeemAsset.MaxFeeRate
} else {
swapFeeRate = c.feeSuggestionAny(wallets.fromWallet.AssetID) // server rates only for the swap init
if swapFeeRate == 0 {
return 0, 0, fmt.Errorf("failed to get swap fee suggestion for %s at %s", wallets.fromWallet.Symbol, form.Host)
}
redeemFeeRate = c.feeSuggestionAny(wallets.toWallet.AssetID) // wallet rate or server rate
if redeemFeeRate == 0 {
return 0, 0, fmt.Errorf("failed to get redeem fee suggestion for %s at %s", wallets.toWallet.Symbol, form.Host)
}
}

swapFees, err := wallets.fromWallet.SingleLotSwapFees(assetConfigs.fromAsset.Version, swapFeeSuggestion, form.Options)
swapFees, err := wallets.fromWallet.SingleLotSwapFees(assetConfigs.fromAsset.Version, swapFeeRate, form.Options)
if err != nil {
return 0, 0, fmt.Errorf("error calculating swap fees: %w", err)
}

redeemFees, err := wallets.toWallet.SingleLotRedeemFees(assetConfigs.toAsset.Version, redeemFeeSuggestion, form.Options)
redeemFees, err := wallets.toWallet.SingleLotRedeemFees(assetConfigs.toAsset.Version, redeemFeeRate, form.Options)
if err != nil {
return 0, 0, fmt.Errorf("error calculating redeem fees: %w", err)
}

return swapFees, redeemFees, nil
}

// MaxFundingFees gives the max fees required to fund a Trade or MultiTrade.
func (c *Core) MaxFundingFees(fromAsset uint32, numTrades uint32, options map[string]string) (uint64, error) {
wallet, found := c.wallet(fromAsset)
if !found {
return 0, newError(missingWalletErr, "no wallet found for %s", unbip(fromAsset))
}

return wallet.MaxFundingFees(numTrades, options), nil
}

// PreOrder calculates fee estimates for a trade.
func (c *Core) PreOrder(form *TradeForm) (*OrderEstimate, error) {
dc, err := c.registeredDEX(form.Host)
Expand Down
4 changes: 3 additions & 1 deletion client/core/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1064,10 +1064,12 @@ func (w *TXCWallet) AccelerationEstimate(swapCoins, accelerationCoins []dex.Byte
func (w *TXCWallet) ReturnRedemptionAddress(addr string) {
w.returnedAddr = addr
}

func (w *TXCWallet) ReturnRefundContracts(contracts [][]byte) {
w.returnedContracts = contracts
}
func (w *TXCWallet) MaxFundingFees(_ uint32, _ map[string]string) uint64 {
return 0
}

func (*TXCWallet) FundMultiOrder(ord *asset.MultiOrder, maxLock uint64) (coins []asset.Coins, redeemScripts [][]dex.Bytes, fundingFees uint64, err error) {
return nil, nil, 0, nil
Expand Down
15 changes: 12 additions & 3 deletions client/core/trade.go
Original file line number Diff line number Diff line change
Expand Up @@ -645,14 +645,23 @@ func (t *trackedTrade) coreOrderInternal() *Order {
corder.Epoch = t.dc.marketEpoch(t.mktID, t.Prefix().ServerTime)
corder.LockedAmt = t.lockedAmount()
corder.ReadyToTick = t.readyToTick
corder.RedeemLockedAmt = t.redemptionLocked
corder.RefundLockedAmt = t.refundLocked

allFeesConfirmed := true
for _, mt := range t.matches {
if !mt.MetaData.Proof.SwapFeeConfirmed || !mt.MetaData.Proof.RedemptionFeeConfirmed {
allFeesConfirmed = false
}
swapConfs, counterConfs := mt.confirms()
corder.Matches = append(corder.Matches, matchFromMetaMatchWithConfs(t, &mt.MetaMatch,
swapConfs, int64(t.metaData.FromSwapConf),
counterConfs, int64(t.metaData.ToSwapConf),
int64(mt.redemptionConfs), int64(mt.redemptionConfsReq)))
}

corder.AllFeesConfirmed = allFeesConfirmed

return corder
}

Expand Down Expand Up @@ -1485,7 +1494,7 @@ func (t *trackedTrade) updateDynamicSwapOrRedemptionFeesPaid(ctx context.Context
if !isInit {
checkFees = feeChecker.DynamicRedemptionFeesPaid
}
actualSwapFees, secrets, err := checkFees(ctx, coinID, contractData)
actualFees, secrets, err := checkFees(ctx, coinID, contractData)
if err != nil {
if errors.Is(err, asset.CoinNotFoundError) || errors.Is(err, asset.ErrNotEnoughConfirms) {
return
Expand All @@ -1500,9 +1509,9 @@ func (t *trackedTrade) updateDynamicSwapOrRedemptionFeesPaid(ctx context.Context
return
}
if isInit {
t.metaData.SwapFeesPaid += actualSwapFees
t.metaData.SwapFeesPaid += actualFees
} else {
t.metaData.RedemptionFeesPaid += actualSwapFees
t.metaData.RedemptionFeesPaid += actualFees
}
stopChecks()
t.notify(newOrderNote(TopicOrderStatusUpdate, "", "", db.Data, t.coreOrderInternal()))
Expand Down
14 changes: 9 additions & 5 deletions client/core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,8 +376,11 @@ type Order struct {
Cancelling bool `json:"cancelling"`
Canceled bool `json:"canceled"`
FeesPaid *FeeBreakdown `json:"feesPaid"`
AllFeesConfirmed bool `json:"allFeesConfirmed"`
FundingCoins []*Coin `json:"fundingCoins"`
LockedAmt uint64 `json:"lockedamt"`
RedeemLockedAmt uint64 `json:"redeemLockedAmt"`
RefundLockedAmt uint64 `json:"refundLockedAmt"`
AccelerationCoins []*Coin `json:"accelerationCoins"`
Rate uint64 `json:"rate"` // limit only
TimeInForce order.TimeInForce `json:"tif"` // limit only
Expand Down Expand Up @@ -1009,11 +1012,12 @@ type MultiTradeForm struct {

// SingleLotFeesForm is used to determine the fees for a single lot trade.
type SingleLotFeesForm struct {
Host string `json:"host"`
Base uint32 `json:"base"`
Quote uint32 `json:"quote"`
Sell bool `json:"sell"`
Options map[string]string `json:"options"`
Host string `json:"host"`
Base uint32 `json:"base"`
Quote uint32 `json:"quote"`
Sell bool `json:"sell"`
Options map[string]string `json:"options"`
UseMaxFeeRate bool `json:"useMaxFeeRate"`
}

// marketName is a string ID constructed from the asset IDs.
Expand Down
Loading

0 comments on commit de675a6

Please sign in to comment.