Skip to content

Commit

Permalink
client/multi: Consider refunds in market making (#2479)
Browse files Browse the repository at this point in the history
* client/multi: Consider refunds in market making

The possibility of refunds was previously ignored in the market making
balance handling code.

- `core.SingleLotFees` now returns refund fees in addition to swap and
   redeem. This is needed because ETH reserves refund fees when funding
   a swap, so the market maker `MaxSell`/`MaxBuy` implementations need
   to know about this.
- `asset.SingleLotSwapFees` is now `asset.SingleLotSwapRefundFees`
- `core.FeeBreakdown` now includes Refund fees.
- The market maker balance handling code can now handle revoked matches
  and refunds.
  • Loading branch information
martonp authored Aug 28, 2023
1 parent b06d986 commit c34c89e
Show file tree
Hide file tree
Showing 11 changed files with 1,917 additions and 334 deletions.
22 changes: 16 additions & 6 deletions client/asset/btc/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -2001,8 +2001,9 @@ func (btc *baseWallet) PreSwap(req *asset.PreSwapForm) (*asset.PreSwap, error) {
}, nil
}

// SingleLotSwapFees returns the fees for a swap transaction for a single lot.
func (btc *baseWallet) SingleLotSwapFees(_ uint32, feeSuggestion uint64, useSafeTxSize bool) (fees uint64, err error) {
// SingleLotSwapRefundFees returns the fees for a swap and refund transaction
// for a single lot.
func (btc *baseWallet) SingleLotSwapRefundFees(_ uint32, feeSuggestion uint64, useSafeTxSize bool) (swapFees uint64, redeemFees uint64, err error) {
var numInputs uint64
if useSafeTxSize {
numInputs = 12
Expand All @@ -2013,14 +2014,23 @@ func (btc *baseWallet) SingleLotSwapFees(_ uint32, feeSuggestion uint64, useSafe
// TODO: The following is not correct for all BTC clones. e.g. Zcash has
// a different MinimumTxOverhead (29).

var txSize uint64
var swapTxSize uint64
if btc.segwit {
txSize = dexbtc.MinimumTxOverhead + (numInputs * dexbtc.RedeemP2WPKHInputSize) + dexbtc.P2WSHOutputSize + dexbtc.P2WPKHOutputSize
swapTxSize = dexbtc.MinimumTxOverhead + (numInputs * dexbtc.RedeemP2WPKHInputSize) + dexbtc.P2WSHOutputSize + dexbtc.P2WPKHOutputSize
} else {
txSize = dexbtc.MinimumTxOverhead + (numInputs * dexbtc.RedeemP2PKHInputSize) + dexbtc.P2SHOutputSize + dexbtc.P2PKHOutputSize
swapTxSize = dexbtc.MinimumTxOverhead + (numInputs * dexbtc.RedeemP2PKHInputSize) + dexbtc.P2SHOutputSize + dexbtc.P2PKHOutputSize
}

return txSize * feeSuggestion, nil
var refundTxSize uint64
if btc.segwit {
witnessVBytes := uint64((dexbtc.RefundSigScriptSize + 2 + 3) / 4)
refundTxSize = dexbtc.MinimumTxOverhead + dexbtc.TxInOverhead + witnessVBytes + dexbtc.P2WPKHOutputSize
} else {
inputSize := uint64(dexbtc.TxInOverhead + dexbtc.RefundSigScriptSize)
refundTxSize = dexbtc.MinimumTxOverhead + inputSize + dexbtc.P2PKHOutputSize
}

return swapTxSize * feeSuggestion, refundTxSize * feeSuggestion, nil
}

// splitOption constructs an *asset.OrderOption with customized text based on the
Expand Down
14 changes: 6 additions & 8 deletions client/asset/dcr/dcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -1717,20 +1717,18 @@ func (dcr *ExchangeWallet) PreSwap(req *asset.PreSwapForm) (*asset.PreSwap, erro
}, nil
}

// SingleLotSwapFees returns the fees for a swap transaction for a single lot.
func (dcr *ExchangeWallet) SingleLotSwapFees(_ uint32, feeSuggestion uint64, useSafeTxSize bool) (fees uint64, err error) {
// SingleLotSwapRefundFees returns the fees for a swap and refund transaction
// for a single lot.
func (dcr *ExchangeWallet) SingleLotSwapRefundFees(_ uint32, feeSuggestion uint64, useSafeTxSize bool) (swapFees uint64, refundFees uint64, err error) {
var numInputs uint64
if useSafeTxSize {
numInputs = 12
} else {
numInputs = 2
}

var txSize uint64 = dexdcr.InitTxSizeBase + (numInputs * dexdcr.P2PKHInputSize)

dcr.log.Infof("SingleLotSwapFees: txSize = %d, feeSuggestion = %d", txSize, feeSuggestion)

return txSize * feeSuggestion, nil
swapTxSize := dexdcr.InitTxSizeBase + (numInputs * dexdcr.P2PKHInputSize)
refundTxSize := dexdcr.MsgTxOverhead + dexdcr.TxInOverhead + dexdcr.RefundSigScriptSize + dexdcr.P2PKHOutputSize
return swapTxSize * feeSuggestion, uint64(refundTxSize) * feeSuggestion, nil
}

// MaxFundingFees returns the maximum funding fees for an order/multi-order.
Expand Down
9 changes: 4 additions & 5 deletions client/asset/eth/eth.go
Original file line number Diff line number Diff line change
Expand Up @@ -1291,14 +1291,13 @@ func (w *baseWallet) MaxFundingFees(_ uint32, _ uint64, _ map[string]string) uin
return 0
}

// SingleLotSwapFees returns the fees for a swap transaction for a single lot.
func (w *assetWallet) SingleLotSwapFees(version uint32, feeSuggestion uint64, _ bool) (fees uint64, err error) {
// SingleLotSwapRefundFees returns the fees for a swap transaction for a single lot.
func (w *assetWallet) SingleLotSwapRefundFees(version uint32, feeSuggestion uint64, _ bool) (swapFees uint64, refundFees uint64, err error) {
g := w.gases(version)
if g == nil {
return 0, fmt.Errorf("no gases known for %d version %d", w.assetID, version)
return 0, 0, fmt.Errorf("no gases known for %d version %d", w.assetID, version)
}

return g.Swap * feeSuggestion, nil
return g.Swap * feeSuggestion, g.Refund * feeSuggestion, nil
}

// estimateSwap prepares an *asset.SwapEstimate. The estimate does not include
Expand Down
4 changes: 2 additions & 2 deletions client/asset/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,8 +518,8 @@ type Wallet interface {
// different CoinID in the returned asset.ConfirmRedemptionStatus as was
// used to call the function.
ConfirmRedemption(coinID dex.Bytes, redemption *Redemption, feeSuggestion uint64) (*ConfirmRedemptionStatus, error)
// SingleLotSwapFees returns the fees for a swap transaction for a single lot.
SingleLotSwapFees(version uint32, feeRate uint64, useSafeTxSize bool) (uint64, error)
// SingleLotSwapRefundFees returns the fees for a swap and refund transaction for a single lot.
SingleLotSwapRefundFees(version uint32, feeRate uint64, useSafeTxSize bool) (uint64, uint64, error)
// SingleLotRedeemFees returns the fees for a redeem transaction for a single lot.
SingleLotRedeemFees(version uint32, feeRate uint64) (uint64, error)
// FundMultiOrder funds multiple orders at once. The return values will
Expand Down
30 changes: 15 additions & 15 deletions client/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -5511,26 +5511,26 @@ func (c *Core) EstimateSendTxFee(address string, assetID uint32, amount uint64,
return estimator.EstimateSendTxFee(address, amount, c.feeSuggestionAny(assetID), subtract)
}

// SingleLotFees returns the estimated swap and redeem fees for a single lot
// SingleLotFees returns the estimated swap, refund, and redeem fees for a single lot
// trade.
func (c *Core) SingleLotFees(form *SingleLotFeesForm) (uint64, uint64, error) {
func (c *Core) SingleLotFees(form *SingleLotFeesForm) (swapFees, redeemFees, refundFees uint64, err error) {
dc, err := c.registeredDEX(form.Host)
if err != nil {
return 0, 0, err
return 0, 0, 0, err
}

mktID := marketName(form.Base, form.Quote)
mktConf := dc.marketConfig(mktID)
if mktConf == nil {
return 0, 0, newError(marketErr, "unknown market %q", mktID)
return 0, 0, 0, newError(marketErr, "unknown market %q", mktID)
}

wallets, assetConfigs, versCompat, err := c.walletSet(dc, form.Base, form.Quote, form.Sell)
if err != nil {
return 0, 0, err
return 0, 0, 0, err
}
if !versCompat { // covers missing asset config, but that's unlikely since there is a market config
return 0, 0, fmt.Errorf("client and server asset versions are incompatible for %v", form.Host)
return 0, 0, 0, fmt.Errorf("client and server asset versions are incompatible for %v", form.Host)
}

var swapFeeRate, redeemFeeRate uint64
Expand All @@ -5540,34 +5540,34 @@ func (c *Core) SingleLotFees(form *SingleLotFeesForm) (uint64, uint64, error) {
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)
return 0, 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)
return 0, 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)
return 0, 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)
return 0, 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, swapFeeRate, form.UseSafeTxSize)
swapFees, refundFees, err = wallets.fromWallet.SingleLotSwapRefundFees(assetConfigs.fromAsset.Version, swapFeeRate, form.UseSafeTxSize)
if err != nil {
return 0, 0, fmt.Errorf("error calculating swap fees: %w", err)
return 0, 0, 0, fmt.Errorf("error calculating swap/refund fees: %w", err)
}

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

return swapFees, redeemFees, nil
return swapFees, redeemFees, refundFees, nil
}

// MaxFundingFees gives the max fees required to fund a Trade or MultiTrade.
Expand Down
4 changes: 2 additions & 2 deletions client/core/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1030,8 +1030,8 @@ func (w *TXCWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, chan
return w.preAccelerateSwapRate, &w.preAccelerateSuggestedRange, nil, nil
}

func (w *TXCWallet) SingleLotSwapFees(version uint32, feeRate uint64, useSafeTxSize bool) (uint64, error) {
return 0, nil
func (w *TXCWallet) SingleLotSwapRefundFees(version uint32, feeRate uint64, useSafeTxSize bool) (uint64, uint64, error) {
return 0, 0, nil
}

func (w *TXCWallet) SingleLotRedeemFees(version uint32, feeRate uint64) (uint64, error) {
Expand Down
2 changes: 2 additions & 0 deletions client/core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,8 @@ type FeeBreakdown struct {
Swap uint64 `json:"swap"`
Redemption uint64 `json:"redemption"`
Funding uint64 `json:"funding"` // split fees
// TODO: Refund is not yet being populated.
Refund uint64 `json:"refund"`
}

// coreOrderFromTrade constructs an *Order from the supplied limit or market
Expand Down
Loading

0 comments on commit c34c89e

Please sign in to comment.