decred.org/dcrdex@v1.0.5/server/market/orderrouter.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package market
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"encoding/hex"
    10  	"errors"
    11  	"fmt"
    12  	"math"
    13  	"time"
    14  
    15  	"decred.org/dcrdex/dex"
    16  	"decred.org/dcrdex/dex/calc"
    17  	"decred.org/dcrdex/dex/msgjson"
    18  	"decred.org/dcrdex/dex/order"
    19  	"decred.org/dcrdex/dex/wait"
    20  	"decred.org/dcrdex/server/account"
    21  	"decred.org/dcrdex/server/asset"
    22  	"decred.org/dcrdex/server/comms"
    23  	"decred.org/dcrdex/server/matcher"
    24  )
    25  
    26  // The AuthManager handles client-related actions, including authorization and
    27  // communications.
    28  type AuthManager interface {
    29  	Route(route string, handler func(account.AccountID, *msgjson.Message) *msgjson.Error)
    30  	Auth(user account.AccountID, msg, sig []byte) error
    31  	AcctStatus(user account.AccountID) (connected bool, tier int64)
    32  	Sign(...msgjson.Signable)
    33  	Send(account.AccountID, *msgjson.Message) error
    34  	Request(account.AccountID, *msgjson.Message, func(comms.Link, *msgjson.Message)) error
    35  	RequestWithTimeout(account.AccountID, *msgjson.Message, func(comms.Link, *msgjson.Message), time.Duration, func()) error
    36  	PreimageSuccess(user account.AccountID, refTime time.Time, oid order.OrderID)
    37  	MissedPreimage(user account.AccountID, refTime time.Time, oid order.OrderID)
    38  	RecordCancel(user account.AccountID, oid, target order.OrderID, epochGap int32, t time.Time)
    39  	RecordCompletedOrder(user account.AccountID, oid order.OrderID, t time.Time)
    40  	UserReputation(user account.AccountID) (tier int64, score, maxScore int32, err error)
    41  }
    42  
    43  const (
    44  	maxClockOffset = 600_000 // milliseconds => 600 sec => 10 minutes
    45  	fundingTxWait  = time.Minute
    46  	// ZeroConfFeeRateThreshold is multiplied by the last known fee rate for an
    47  	// asset to attain a minimum fee rate acceptable for zero-conf funding
    48  	// coins.
    49  	ZeroConfFeeRateThreshold = 0.9
    50  )
    51  
    52  // MarketTunnel is a connection to a market.
    53  type MarketTunnel interface {
    54  	// SubmitOrder submits the order to the market for insertion into the epoch
    55  	// queue.
    56  	SubmitOrder(*orderRecord) error
    57  	// MidGap returns the mid-gap market rate, which is ths rate halfway between
    58  	// the best buy order and the best sell order in the order book.
    59  	MidGap() uint64
    60  	// MarketBuyBuffer is a coefficient that when multiplied by the market's lot
    61  	// size specifies the minimum required amount for a market buy order.
    62  	MarketBuyBuffer() float64
    63  	// LotSize is the market's lot size in units of the base asset.
    64  	LotSize() uint64
    65  	// RateStep is the market's rate step in units of the quote asset.
    66  	RateStep() uint64
    67  	// CoinLocked should return true if the CoinID is currently a funding Coin
    68  	// for an active DEX order. This is required for Coin validation to prevent
    69  	// a user from submitting multiple orders spending the same Coin. This
    70  	// method will likely need to check all orders currently in the epoch queue,
    71  	// the order book, and the swap monitor, since UTXOs will still be unspent
    72  	// according to the asset backends until the client broadcasts their
    73  	// initialization transaction.
    74  	//
    75  	// DRAFT NOTE: This function could also potentially be handled by persistent
    76  	// storage, since active orders and active matches are tracked there.
    77  	CoinLocked(assetID uint32, coinID order.CoinID) bool
    78  	// Cancelable determines whether an order is cancelable. A cancelable order
    79  	// is a limit order with time-in-force standing either in the epoch queue or
    80  	// in the order book.
    81  	Cancelable(order.OrderID) bool
    82  
    83  	// Suspend suspends the market as soon as a given time, returning the final
    84  	// epoch index and and time at which that epoch closes.
    85  	Suspend(asSoonAs time.Time, persistBook bool) (finalEpochIdx int64, finalEpochEnd time.Time)
    86  
    87  	// Running indicates is the market is accepting new orders. This will return
    88  	// false when suspended, but false does not necessarily mean Run has stopped
    89  	// since a start epoch may be set.
    90  	Running() bool
    91  
    92  	// CheckUnfilled checks a user's unfilled book orders that are funded by
    93  	// coins for a given asset to ensure that their funding coins are not spent.
    94  	// If any of an unfilled order's funding coins are spent, the order is
    95  	// unbooked (removed from the in-memory book, revoked in the DB, a
    96  	// cancellation marked against the user, coins unlocked, and orderbook
    97  	// subscribers notified). See Unbook for details.
    98  	CheckUnfilled(assetID uint32, user account.AccountID) (unbooked []*order.LimitOrder)
    99  
   100  	// Parcels calculates the number of active parcels for the market.
   101  	Parcels(user account.AccountID, settlingQty uint64) float64
   102  }
   103  
   104  type MarketParcelCalculator func(settlingQty uint64) (parcels float64)
   105  
   106  // orderRecord contains the information necessary to respond to an order
   107  // request.
   108  type orderRecord struct {
   109  	order order.Order
   110  	req   msgjson.Stampable
   111  	msgID uint64
   112  }
   113  
   114  // assetSet is pointers to two different assets, but with 4 ways of addressing
   115  // them.
   116  type assetSet struct {
   117  	funding   *asset.BackedAsset
   118  	receiving *asset.BackedAsset
   119  	base      *asset.BackedAsset
   120  	quote     *asset.BackedAsset
   121  }
   122  
   123  // newAssetSet is a constructor for an assetSet.
   124  func newAssetSet(base, quote *asset.BackedAsset, sell bool) *assetSet {
   125  	coins := &assetSet{
   126  		quote:     quote,
   127  		base:      base,
   128  		funding:   quote,
   129  		receiving: base,
   130  	}
   131  	if sell {
   132  		coins.funding, coins.receiving = base, quote
   133  	}
   134  	return coins
   135  }
   136  
   137  // FeeSource is a source of the last reported tx fee rate estimate for an asset.
   138  type FeeSource interface {
   139  	LastRate(assetID uint32) (feeRate uint64)
   140  }
   141  
   142  // MatchSwapper is a source for information about settling matches.
   143  type MatchSwapper interface {
   144  	UnsettledQuantity(user account.AccountID) map[[2]uint32]uint64
   145  }
   146  
   147  // OrderRouter handles the 'limit', 'market', and 'cancel' DEX routes. These
   148  // are authenticated routes used for placing and canceling orders.
   149  type OrderRouter struct {
   150  	auth        AuthManager
   151  	assets      map[uint32]*asset.BackedAsset
   152  	tunnels     map[string]MarketTunnel
   153  	latencyQ    *wait.TickerQueue
   154  	feeSource   FeeSource
   155  	dexBalancer *DEXBalancer
   156  	swapper     MatchSwapper
   157  }
   158  
   159  // OrderRouterConfig is the configuration settings for an OrderRouter.
   160  type OrderRouterConfig struct {
   161  	AuthManager  AuthManager
   162  	Assets       map[uint32]*asset.BackedAsset
   163  	Markets      map[string]MarketTunnel
   164  	FeeSource    FeeSource
   165  	DEXBalancer  *DEXBalancer
   166  	MatchSwapper MatchSwapper
   167  }
   168  
   169  // NewOrderRouter is a constructor for an OrderRouter.
   170  func NewOrderRouter(cfg *OrderRouterConfig) *OrderRouter {
   171  	router := &OrderRouter{
   172  		auth:        cfg.AuthManager,
   173  		assets:      cfg.Assets,
   174  		tunnels:     cfg.Markets,
   175  		latencyQ:    wait.NewTickerQueue(2 * time.Second),
   176  		feeSource:   cfg.FeeSource,
   177  		dexBalancer: cfg.DEXBalancer,
   178  		swapper:     cfg.MatchSwapper,
   179  	}
   180  	cfg.AuthManager.Route(msgjson.LimitRoute, router.handleLimit)
   181  	cfg.AuthManager.Route(msgjson.MarketRoute, router.handleMarket)
   182  	cfg.AuthManager.Route(msgjson.CancelRoute, router.handleCancel)
   183  	return router
   184  }
   185  
   186  func (r *OrderRouter) Run(ctx context.Context) {
   187  	r.latencyQ.Run(ctx)
   188  }
   189  
   190  func (r *OrderRouter) respondError(reqID uint64, user account.AccountID, msgErr *msgjson.Error) {
   191  	log.Debugf("Error going to user %v: %s", user, msgErr)
   192  	msg, err := msgjson.NewResponse(reqID, nil, msgErr)
   193  	if err != nil {
   194  		log.Errorf("Failed to create error response with message '%s': %v", msg, err)
   195  		return // this should not be possible, but don't pass nil msg to Send
   196  	}
   197  	if err := r.auth.Send(user, msg); err != nil {
   198  		log.Infof("Failed to send %s error response (msg = %s) to disconnected user %v: %q",
   199  			msg.Route, msgErr, user, err)
   200  	}
   201  }
   202  
   203  func fundingCoin(backend asset.Backend, coinID []byte, redeemScript []byte) (asset.FundingCoin, error) {
   204  	outputTracker, is := backend.(asset.OutputTracker)
   205  	if !is {
   206  		return nil, fmt.Errorf("fundingCoin requested for incapable asset")
   207  	}
   208  	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   209  	defer cancel()
   210  	return outputTracker.FundingCoin(ctx, coinID, redeemScript)
   211  }
   212  
   213  func coinConfirmations(coin asset.Coin) (int64, error) {
   214  	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   215  	defer cancel()
   216  	return coin.Confirmations(ctx)
   217  }
   218  
   219  // handleLimit is the handler for the 'limit' route. This route accepts a
   220  // msgjson.Limit payload, validates the information, constructs an
   221  // order.LimitOrder and submits it to the epoch queue.
   222  func (r *OrderRouter) handleLimit(user account.AccountID, msg *msgjson.Message) *msgjson.Error {
   223  	limit := new(msgjson.LimitOrder)
   224  	err := msg.Unmarshal(&limit)
   225  	if err != nil || limit == nil {
   226  		return msgjson.NewError(msgjson.RPCParseError, "error decoding 'limit' payload")
   227  	}
   228  
   229  	rpcErr := r.verifyAccount(user, limit.AccountID, limit)
   230  	if rpcErr != nil {
   231  		return rpcErr
   232  	}
   233  
   234  	if _, tier := r.auth.AcctStatus(user); tier < 1 {
   235  		return msgjson.NewError(msgjson.AccountClosedError, "account %v with tier %d may not submit trade orders", user, tier)
   236  	}
   237  
   238  	tunnel, assets, sell, rpcErr := r.extractMarketDetails(&limit.Prefix, &limit.Trade)
   239  	if rpcErr != nil {
   240  		return rpcErr
   241  	}
   242  
   243  	// Spare some resources if the market is closed now. Any orders that make it
   244  	// through to a closed market will receive a similar error from SubmitOrder.
   245  	if !tunnel.Running() {
   246  		return msgjson.NewError(msgjson.MarketNotRunningError, "market closed to new orders")
   247  	}
   248  
   249  	// Check that OrderType is set correctly
   250  	if limit.OrderType != msgjson.LimitOrderNum {
   251  		return msgjson.NewError(msgjson.OrderParameterError, "wrong order type set for limit order. wanted %d, got %d",
   252  			msgjson.LimitOrderNum, limit.OrderType)
   253  	}
   254  
   255  	// Check that the rate is non-zero and obeys the rate step interval.
   256  	if limit.Rate == 0 {
   257  		return msgjson.NewError(msgjson.OrderParameterError, "rate = 0 not allowed")
   258  	}
   259  	if rateStep := tunnel.RateStep(); limit.Rate%rateStep != 0 {
   260  		return msgjson.NewError(msgjson.OrderParameterError, "rate (%d) not a multiple of ratestep (%d)",
   261  			limit.Rate, rateStep)
   262  	}
   263  
   264  	// Check time-in-force
   265  	var force order.TimeInForce
   266  	switch limit.TiF {
   267  	case msgjson.StandingOrderNum:
   268  		force = order.StandingTiF
   269  	case msgjson.ImmediateOrderNum:
   270  		force = order.ImmediateTiF
   271  	default:
   272  		return msgjson.NewError(msgjson.OrderParameterError, "unknown time-in-force")
   273  	}
   274  
   275  	lotSize := tunnel.LotSize()
   276  	rpcErr = r.checkPrefixTrade(assets, lotSize, &limit.Prefix, &limit.Trade, true)
   277  	if rpcErr != nil {
   278  		return rpcErr
   279  	}
   280  
   281  	// Commitment
   282  	if len(limit.Commit) != order.CommitmentSize {
   283  		return msgjson.NewError(msgjson.OrderParameterError, "invalid commitment")
   284  	}
   285  	var commit order.Commitment
   286  	copy(commit[:], limit.Commit)
   287  
   288  	coinIDs := make([]order.CoinID, 0, len(limit.Trade.Coins))
   289  	for _, coin := range limit.Trade.Coins {
   290  		coinID := order.CoinID(coin.ID)
   291  		coinIDs = append(coinIDs, coinID)
   292  	}
   293  
   294  	// Create the limit order.
   295  	lo := &order.LimitOrder{
   296  		P: order.Prefix{
   297  			AccountID:  user,
   298  			BaseAsset:  limit.Base,
   299  			QuoteAsset: limit.Quote,
   300  			OrderType:  order.LimitOrderType,
   301  			ClientTime: time.UnixMilli(int64(limit.ClientTime)),
   302  			//ServerTime set in epoch queue processing pipeline.
   303  			Commit: commit,
   304  		},
   305  		T: order.Trade{
   306  			Coins:    coinIDs,
   307  			Sell:     sell,
   308  			Quantity: limit.Quantity,
   309  			Address:  limit.Address,
   310  		},
   311  		Rate:  limit.Rate,
   312  		Force: force,
   313  	}
   314  
   315  	// NOTE: ServerTime is not yet set, so the order's ID, which is computed
   316  	// from the serialized order, is not yet valid. The Market will stamp the
   317  	// order on receipt, and the order ID will be valid.
   318  
   319  	oRecord := &orderRecord{
   320  		order: lo,
   321  		req:   limit,
   322  		msgID: msg.ID,
   323  	}
   324  
   325  	return r.processTrade(oRecord, tunnel, assets, limit.Coins, sell, limit.Rate, limit.RedeemSig, limit.Serialize())
   326  }
   327  
   328  // handleMarket is the handler for the 'market' route. This route accepts a
   329  // msgjson.MarketOrder payload, validates the information, constructs an
   330  // order.MarketOrder and submits it to the epoch queue.
   331  func (r *OrderRouter) handleMarket(user account.AccountID, msg *msgjson.Message) *msgjson.Error {
   332  	market := new(msgjson.MarketOrder)
   333  	err := msg.Unmarshal(&market)
   334  	if err != nil || market == nil {
   335  		return msgjson.NewError(msgjson.RPCParseError, "error decoding 'market' payload")
   336  	}
   337  
   338  	rpcErr := r.verifyAccount(user, market.AccountID, market)
   339  	if rpcErr != nil {
   340  		return rpcErr
   341  	}
   342  
   343  	if _, tier := r.auth.AcctStatus(user); tier < 1 {
   344  		return msgjson.NewError(msgjson.AccountClosedError, "account %v with tier %d may not submit trade orders", user, tier)
   345  	}
   346  
   347  	tunnel, assets, sell, rpcErr := r.extractMarketDetails(&market.Prefix, &market.Trade)
   348  	if rpcErr != nil {
   349  		return rpcErr
   350  	}
   351  
   352  	if !tunnel.Running() {
   353  		mktName, _ := dex.MarketName(market.Base, market.Quote)
   354  		return msgjson.NewError(msgjson.MarketNotRunningError, "market %s closed to new orders", mktName)
   355  	}
   356  
   357  	// Check that OrderType is set correctly
   358  	if market.OrderType != msgjson.MarketOrderNum {
   359  		return msgjson.NewError(msgjson.OrderParameterError, "wrong order type set for market order")
   360  	}
   361  
   362  	// Passing sell as the checkLot parameter causes the lot size check to be
   363  	// ignored for market buy orders.
   364  	lotSize := tunnel.LotSize()
   365  	rpcErr = r.checkPrefixTrade(assets, lotSize, &market.Prefix, &market.Trade, sell)
   366  	if rpcErr != nil {
   367  		return rpcErr
   368  	}
   369  
   370  	// Commitment.
   371  	if len(market.Commit) != order.CommitmentSize {
   372  		return msgjson.NewError(msgjson.OrderParameterError, "invalid commitment")
   373  	}
   374  	var commit order.Commitment
   375  	copy(commit[:], market.Commit)
   376  
   377  	coinIDs := make([]order.CoinID, 0, len(market.Trade.Coins))
   378  	for _, coin := range market.Trade.Coins {
   379  		coinID := order.CoinID(coin.ID)
   380  		coinIDs = append(coinIDs, coinID)
   381  	}
   382  
   383  	// Create the market order
   384  	mo := &order.MarketOrder{
   385  		P: order.Prefix{
   386  			AccountID:  user,
   387  			BaseAsset:  market.Base,
   388  			QuoteAsset: market.Quote,
   389  			OrderType:  order.MarketOrderType,
   390  			ClientTime: time.UnixMilli(int64(market.ClientTime)),
   391  			//ServerTime set in epoch queue processing pipeline.
   392  			Commit: commit,
   393  		},
   394  		T: order.Trade{
   395  			Coins:    coinIDs,
   396  			Sell:     sell,
   397  			Quantity: market.Quantity,
   398  			Address:  market.Address,
   399  		},
   400  	}
   401  
   402  	// Send the order to the epoch queue.
   403  	oRecord := &orderRecord{
   404  		order: mo,
   405  		req:   market,
   406  		msgID: msg.ID,
   407  	}
   408  
   409  	return r.processTrade(oRecord, tunnel, assets, market.Coins, sell, 0, market.RedeemSig, market.Serialize())
   410  }
   411  
   412  // processTrade checks that the trade is valid and submits it to the market.
   413  func (r *OrderRouter) processTrade(oRecord *orderRecord, tunnel MarketTunnel, assets *assetSet,
   414  	coins []*msgjson.Coin, sell bool, rate uint64, redeemSig *msgjson.RedeemSig, sigMsg []byte) *msgjson.Error {
   415  
   416  	fundingAsset := assets.funding
   417  	user := oRecord.order.User()
   418  	trade := oRecord.order.Trade()
   419  
   420  	// If the receiving asset is account-based, we need to check that they can
   421  	// cover fees for the redemption, since they can't be subtracted from the
   422  	// received amount.
   423  	receivingBalancer, isToAccount := assets.receiving.Backend.(asset.AccountBalancer)
   424  	if isToAccount {
   425  		if redeemSig == nil {
   426  			log.Infof("user %s did not include a RedeemSig for received asset %s", user, assets.receiving.Symbol)
   427  			return msgjson.NewError(msgjson.OrderParameterError, "no redeem address verification included for asset %s", assets.receiving.Symbol)
   428  		}
   429  
   430  		acctAddr := trade.ToAccount()
   431  		if err := receivingBalancer.ValidateSignature(acctAddr, redeemSig.PubKey, sigMsg, redeemSig.Sig); err != nil {
   432  			log.Infof("user %s failed redeem signature validation for order: %v",
   433  				user, err)
   434  			return msgjson.NewError(msgjson.SignatureError, "redeem signature validation failed")
   435  		}
   436  
   437  		if !r.sufficientAccountBalance(acctAddr, oRecord.order, assets.receiving.Asset.ID, assets.receiving.ID, tunnel) {
   438  			return msgjson.NewError(msgjson.FundingError, "insufficient balance")
   439  		}
   440  	}
   441  
   442  	// If the funding asset is account-based, we'll check balance and submit the
   443  	// order immediately, since we don't need to find coins.
   444  	fundingBalancer, isAccountFunded := assets.funding.Backend.(asset.AccountBalancer)
   445  	if isAccountFunded {
   446  		// Validate that the coins are correct for an account-based-asset-funded
   447  		// order. There should be 1 coin, 1 sig, 1 pubkey, and no redeem script.
   448  		if len(coins) != 1 {
   449  			log.Infof("user %s submitted an %s-funded order with %d coin IDs", user, assets.funding.Symbol, len(coins))
   450  			return msgjson.NewError(msgjson.OrderParameterError, "account-type asset funding requires exactly one coin ID")
   451  		}
   452  		acctProof := coins[0]
   453  		if len(acctProof.PubKeys) != 1 || len(acctProof.Sigs) != 1 || len(acctProof.Redeem) > 0 {
   454  			log.Infof("user %s submitted an %s-funded order with %d pubkeys, %d sigs, redeem script length %d",
   455  				user, assets.funding.Symbol, len(acctProof.PubKeys), len(acctProof.Sigs), len(acctProof.Redeem))
   456  			return msgjson.NewError(msgjson.OrderParameterError, "account-type asset funding requires exactly one coin ID")
   457  		}
   458  
   459  		acctAddr := trade.FromAccount()
   460  		pubKey := acctProof.PubKeys[0]
   461  		sig := acctProof.Sigs[0]
   462  		if err := fundingBalancer.ValidateSignature(acctAddr, pubKey, sigMsg, sig); err != nil {
   463  			log.Infof("user %s failed signature validation for order: %v",
   464  				user, err)
   465  			return msgjson.NewError(msgjson.SignatureError, "signature validation failed")
   466  		}
   467  
   468  		if !r.sufficientAccountBalance(acctAddr, oRecord.order, assets.funding.Asset.ID, assets.receiving.ID, tunnel) {
   469  			return msgjson.NewError(msgjson.FundingError, "insufficient balance")
   470  		}
   471  		return r.submitOrderToMarket(tunnel, oRecord)
   472  	}
   473  
   474  	// Funding coins are from a utxo-based asset. Need to find them.
   475  
   476  	funder, is := assets.funding.Backend.(asset.OutputTracker)
   477  	if !is {
   478  		return msgjson.NewError(msgjson.RPCInternal, "internal error")
   479  	}
   480  
   481  	// Validate coin IDs and prepare some strings for debug logging.
   482  	coinStrs := make([]string, 0, len(coins))
   483  	for _, coinID := range trade.Coins {
   484  		coinStr, err := fundingAsset.Backend.ValidateCoinID(coinID)
   485  		if err != nil {
   486  			return msgjson.NewError(msgjson.FundingError, "invalid coin ID %v: %v", coinID, err)
   487  		}
   488  		// TODO: Check all markets here?
   489  		if tunnel.CoinLocked(assets.funding.ID, coinID) {
   490  			return msgjson.NewError(msgjson.FundingError, "coin %s is locked", fmtCoinID(assets.funding.ID, coinID))
   491  		}
   492  		coinStrs = append(coinStrs, coinStr)
   493  	}
   494  
   495  	// Use this as a chance to check user's existing market orders.
   496  	// TODO: check all markets?
   497  	for mktName, tunnel := range r.tunnels {
   498  		unbookedUnfunded := tunnel.CheckUnfilled(assets.funding.ID, oRecord.order.User())
   499  		for _, badLo := range unbookedUnfunded {
   500  			log.Infof("Unbooked unfunded order %v from market %s for user %v", badLo, mktName, oRecord.order.User())
   501  		}
   502  	}
   503  
   504  	lotSize := tunnel.LotSize()
   505  
   506  	midGap := tunnel.MidGap()
   507  	if midGap == 0 {
   508  		midGap = tunnel.RateStep()
   509  	}
   510  
   511  	lots := trade.Quantity / lotSize
   512  	if !sell && rate == 0 {
   513  		lots = matcher.QuoteToBase(midGap, trade.Quantity) / lotSize
   514  	}
   515  
   516  	var valSum uint64
   517  	var spendSize uint32
   518  	neededCoins := make(map[int]*msgjson.Coin, len(trade.Coins))
   519  	for i, coin := range coins {
   520  		neededCoins[i] = coin
   521  	}
   522  
   523  	checkCoins := func() (tryAgain bool, msgErr *msgjson.Error) {
   524  		for key, coin := range neededCoins {
   525  			// Get the coin from the backend and validate it.
   526  			dexCoin, err := fundingCoin(fundingAsset.Backend, coin.ID, coin.Redeem)
   527  			if err != nil {
   528  				if errors.Is(err, asset.CoinNotFoundError) {
   529  					return true, nil
   530  				}
   531  				if errors.Is(err, asset.ErrRequestTimeout) {
   532  					log.Errorf("Deadline exceeded attempting to verify funding coin %v (%s). Will try again.",
   533  						coin.ID, fundingAsset.Symbol)
   534  					return true, nil
   535  				}
   536  				log.Errorf("Error retrieving limit order funding coin ID %s. user = %s: %v", coin.ID, user, err)
   537  				return false, msgjson.NewError(msgjson.FundingError, "error retrieving coin ID %v", coin.ID)
   538  			}
   539  
   540  			// Verify that the user controls the funding coins.
   541  			err = dexCoin.Auth(msgBytesToBytes(coin.PubKeys), msgBytesToBytes(coin.Sigs), coin.ID)
   542  			if err != nil {
   543  				log.Debugf("Auth error for %s coin %s: %v", fundingAsset.Symbol, dexCoin, err)
   544  				return false, msgjson.NewError(msgjson.CoinAuthError, "failed to authorize coin %v", dexCoin)
   545  			}
   546  
   547  			msgErr := r.checkZeroConfs(dexCoin, fundingAsset)
   548  			if msgErr != nil {
   549  				return false, msgErr
   550  			}
   551  
   552  			delete(neededCoins, key) // don't check this coin again
   553  			valSum += dexCoin.Coin().Value()
   554  			// NOTE: Summing like this is actually not quite sufficient to
   555  			// estimate the size associated with the input, because if it's a
   556  			// BTC segwit output, we would also have to account for the marker
   557  			// and flag weight, but only once per tx. The weight would add
   558  			// either 0 or 1 byte to the tx virtual size, so we have a chance of
   559  			// under-estimating by 1 byte to the advantage of the client. It
   560  			// won't ever cause issues though, because we also require funding
   561  			// for a change output in the final swap, which is actually not
   562  			// needed, so there's some buffer.
   563  			spendSize += dexCoin.SpendSize()
   564  		}
   565  
   566  		if valSum == 0 {
   567  			return false, msgjson.NewError(msgjson.FundingError, "zero value funding coins not permitted")
   568  		}
   569  
   570  		// Calculate the fees and check that the utxo sum is enough.
   571  		var swapVal uint64
   572  		if sell {
   573  			swapVal = trade.Quantity
   574  		} else {
   575  			if rate > 0 { // limit buy
   576  				swapVal = calc.BaseToQuote(rate, trade.Quantity)
   577  			} else {
   578  				// This is a market buy order, so the quantity gets special handling.
   579  				// 1. The quantity is in units of the quote asset.
   580  				// 2. The quantity has to satisfy the market buy buffer.
   581  				midGap := tunnel.MidGap()
   582  				if midGap == 0 {
   583  					midGap = tunnel.RateStep()
   584  				}
   585  				buyBuffer := tunnel.MarketBuyBuffer()
   586  				lotWithBuffer := uint64(float64(lotSize) * buyBuffer)
   587  				swapVal = matcher.BaseToQuote(midGap, lotWithBuffer)
   588  				if trade.Quantity < swapVal {
   589  					return false, msgjson.NewError(msgjson.FundingError, "order quantity does not satisfy market buy buffer. %d < %d. midGap = %d",
   590  						trade.Quantity, swapVal, midGap)
   591  				}
   592  			}
   593  		}
   594  
   595  		if !funder.ValidateOrderFunding(swapVal, valSum, uint64(len(trade.Coins)), uint64(spendSize), lots, &assets.funding.Asset) {
   596  			return false, msgjson.NewError(msgjson.FundingError, "failed funding validation")
   597  		}
   598  
   599  		return false, nil
   600  	}
   601  
   602  	log.Tracef("Searching for %s coins %v for new order", fundingAsset.Symbol, coinStrs)
   603  	r.latencyQ.Wait(&wait.Waiter{
   604  		Expiration: time.Now().Add(fundingTxWait),
   605  		TryFunc: func() wait.TryDirective {
   606  			tryAgain, msgErr := checkCoins()
   607  			if tryAgain {
   608  				return wait.TryAgain
   609  			}
   610  			if msgErr != nil {
   611  				r.respondError(oRecord.msgID, user, msgErr)
   612  				return wait.DontTryAgain
   613  			}
   614  
   615  			// Send the order to the epoch queue where it will be time stamped.
   616  			log.Tracef("Found and validated %s coins %v for new order", fundingAsset.Symbol, coinStrs)
   617  			if msgErr := r.submitOrderToMarket(tunnel, oRecord); msgErr != nil {
   618  				r.respondError(oRecord.msgID, user, msgErr)
   619  			}
   620  			return wait.DontTryAgain
   621  		},
   622  		ExpireFunc: func() {
   623  			// Tell them to broadcast again or check their node before broadcast
   624  			// timeout is reached and the match is revoked.
   625  			r.respondError(oRecord.msgID, user, msgjson.NewError(msgjson.TransactionUndiscovered,
   626  				"failed to find funding coins %v", coinStrs))
   627  		},
   628  	})
   629  
   630  	return nil
   631  }
   632  
   633  // sufficientAccountBalance checks that the user's account-based asset balance
   634  // is sufficient to support the order, considering the user's other orders and
   635  // active matches across all DEX markets.
   636  func (r *OrderRouter) sufficientAccountBalance(accountAddr string, ord order.Order,
   637  	assetID, redeemAssetID uint32, tunnel MarketTunnel) bool {
   638  	trade := ord.Trade()
   639  
   640  	// This asset is funding an order when it is either:
   641  	//  - base asset in a sell order e.g. selling ETH in a ETH-LTC market
   642  	//  - quote asset in a buy order e.g. buying BTC in a BTC-ETH market
   643  	// This asset will be redeemed when it is either:
   644  	//  - base asset in a buy order e.g. buying ETH in a ETH-LTC market
   645  	//  - quote asset in a sell order e.g. selling in a BTC-ETH market
   646  
   647  	var fundingQty, fundingLots uint64 // when the asset is base in sell order, or quote in buy order
   648  	var redeems int                    // when the asset is base in buy order, or quote in sell order
   649  	if ord.Base() == assetID {
   650  		if trade.Sell {
   651  			fundingQty = trade.Quantity
   652  			fundingLots = trade.Quantity / tunnel.LotSize()
   653  		} else { // buying base asset
   654  			baseQty := trade.Quantity
   655  			if _, ok := ord.(*order.MarketOrder); ok {
   656  				// Market buy Quantity is in units of quote asset, so estimate
   657  				// how much of base asset that might be based on mid-gap rate.
   658  				baseQty = calc.QuoteToBase(safeMidGap(tunnel), trade.Quantity)
   659  			}
   660  			redeems = int(baseQty / tunnel.LotSize())
   661  		}
   662  	} else {
   663  		if trade.Sell {
   664  			redeems = int(trade.Quantity / tunnel.LotSize())
   665  		} else {
   666  			if lo, ok := ord.(*order.LimitOrder); ok {
   667  				fundingQty = calc.BaseToQuote(lo.Rate, trade.Quantity)
   668  				fundingLots = trade.Quantity / tunnel.LotSize()
   669  			} else { // market buy
   670  				fundingQty = trade.Quantity
   671  				fundingLots = fundingQty / tunnel.LotSize()
   672  			}
   673  		}
   674  	}
   675  
   676  	return r.dexBalancer.CheckBalance(accountAddr, assetID, redeemAssetID, fundingQty, fundingLots, redeems)
   677  }
   678  
   679  // calcParcelLimit computes the users score-scaled user parcel limit.
   680  func calcParcelLimit(tier int64, score, maxScore int32) uint32 {
   681  	// Users limit starts at 2 parcels per tier.
   682  	lowerLimit := tier * dex.PerTierBaseParcelLimit
   683  	// Limit can scale up to 3x with score.
   684  	upperLimit := lowerLimit * dex.ParcelLimitScoreMultiplier
   685  	limitRange := upperLimit - lowerLimit
   686  	var scaleFactor float64
   687  	if score > 0 {
   688  		scaleFactor = float64(score) / float64(maxScore)
   689  	}
   690  	return uint32(lowerLimit) + uint32(math.Round(scaleFactor*float64(limitRange)))
   691  }
   692  
   693  // CheckParcelLimit checks that the user does not exceed their parcel limit.
   694  // The calcParcels function must be provided by the order's targeted Market, and
   695  // calculate the number of parcels from that market when quantity from settling
   696  // matches is taken into consideration. CheckParcelLimit checks the global
   697  // parcel limit, based on the users tier and score and active orders for ALL
   698  // markets.
   699  func (r *OrderRouter) CheckParcelLimit(user account.AccountID, targetMarketName string, calcParcels MarketParcelCalculator) bool {
   700  	tier, score, maxScore, err := r.auth.UserReputation(user)
   701  	if err != nil {
   702  		log.Errorf("error getting user score for parcel limit check: %w", err)
   703  		return false
   704  	}
   705  	if tier <= 0 {
   706  		return false
   707  	}
   708  
   709  	roundParcels := func(parcels float64) uint32 {
   710  		// Rounding to 8 decimal places first should resolve any floating point
   711  		// error, then we take the floor. 1e8 is not completetly arbitrary. We
   712  		// need to choose a number of decimals of an order > the expected parcel
   713  		// size of a low-lot-size market, which I expect wouldn't be greater
   714  		// than 1e5.
   715  		return uint32(math.Round(parcels*1e8) / 1e8)
   716  	}
   717  
   718  	parcelLimit := calcParcelLimit(tier, score, maxScore)
   719  
   720  	settlingQuantities := make(map[string]uint64)
   721  	for bq, qty := range r.swapper.UnsettledQuantity(user) {
   722  		mktName, _ := dex.MarketName(bq[0], bq[1])
   723  		settlingQuantities[mktName] += qty
   724  	}
   725  
   726  	var otherMarketParcels float64
   727  	var settlingQty uint64
   728  	for mktName, mkt := range r.tunnels {
   729  		if mktName == targetMarketName {
   730  			settlingQty = settlingQuantities[mktName]
   731  			continue
   732  		}
   733  
   734  		otherMarketParcels += mkt.Parcels(user, settlingQuantities[mktName])
   735  		if roundParcels(otherMarketParcels) > parcelLimit {
   736  			return false
   737  		}
   738  	}
   739  	targetMarketParcels := calcParcels(settlingQty)
   740  
   741  	return roundParcels(otherMarketParcels+targetMarketParcels) <= parcelLimit
   742  }
   743  
   744  func (r *OrderRouter) submitOrderToMarket(tunnel MarketTunnel, oRecord *orderRecord) *msgjson.Error {
   745  	if err := tunnel.SubmitOrder(oRecord); err != nil {
   746  		code := msgjson.UnknownMarketError
   747  		switch {
   748  		case errors.Is(err, ErrInternalServer):
   749  			log.Errorf("Market failed to SubmitOrder: %v", err)
   750  		case errors.Is(err, ErrQuantityTooHigh):
   751  			code = msgjson.OrderQuantityTooHigh
   752  			fallthrough
   753  		default:
   754  			log.Debugf("Market failed to SubmitOrder: %v", err)
   755  		}
   756  		return msgjson.NewError(code, "%v", err)
   757  	}
   758  	return nil
   759  }
   760  
   761  // Check the FundingCoin confirmations, and if zero, ensure the tx fee rate
   762  // is sufficient, > 90% of our last recorded estimate for the asset.
   763  func (r *OrderRouter) checkZeroConfs(dexCoin asset.FundingCoin, fundingAsset *asset.BackedAsset) *msgjson.Error {
   764  	// Verify that zero-conf coins are within 10% of the last known fee
   765  	// rate.
   766  	confs, err := coinConfirmations(dexCoin.Coin())
   767  	if err != nil {
   768  		log.Debugf("Confirmations error for %s coin %s: %v", fundingAsset.Symbol, dexCoin, err)
   769  		return msgjson.NewError(msgjson.FundingError, "failed to verify coin %v", dexCoin)
   770  	}
   771  	if confs > 0 {
   772  		return nil
   773  	}
   774  	lastKnownFeeRate := r.feeSource.LastRate(fundingAsset.ID) // MaxFeeRate applied inside feeSource
   775  	feeMinimum := uint64(math.Round(float64(lastKnownFeeRate) * ZeroConfFeeRateThreshold))
   776  
   777  	if !fundingAsset.Backend.ValidateFeeRate(dexCoin.Coin(), feeMinimum) {
   778  		log.Debugf("Fees too low %s coin %s: fee mim %d", fundingAsset.Symbol, dexCoin, feeMinimum)
   779  		return msgjson.NewError(msgjson.FundingError,
   780  			"fee rate for %s is too low. fee min %d", dexCoin, feeMinimum)
   781  	}
   782  	return nil
   783  }
   784  
   785  // handleCancel is the handler for the 'cancel' route. This route accepts a
   786  // msgjson.Cancel payload, validates the information, constructs an
   787  // order.CancelOrder and submits it to the epoch queue.
   788  func (r *OrderRouter) handleCancel(user account.AccountID, msg *msgjson.Message) *msgjson.Error {
   789  	cancel := new(msgjson.CancelOrder)
   790  	err := msg.Unmarshal(&cancel)
   791  	if err != nil || cancel == nil {
   792  		return msgjson.NewError(msgjson.RPCParseError, "error decoding 'cancel' payload")
   793  	}
   794  
   795  	rpcErr := r.verifyAccount(user, cancel.AccountID, cancel)
   796  	if rpcErr != nil {
   797  		return rpcErr
   798  	}
   799  
   800  	// NOTE: Allow suspended accounts to submit cancel orders.
   801  
   802  	tunnel, rpcErr := r.extractMarket(&cancel.Prefix)
   803  	if rpcErr != nil {
   804  		return rpcErr
   805  	}
   806  
   807  	if len(cancel.TargetID) != order.OrderIDSize {
   808  		return msgjson.NewError(msgjson.OrderParameterError, "invalid target ID format")
   809  	}
   810  	var targetID order.OrderID
   811  	copy(targetID[:], cancel.TargetID)
   812  
   813  	if !tunnel.Cancelable(targetID) {
   814  		return msgjson.NewError(msgjson.OrderParameterError, "target order not known: %v", targetID)
   815  	}
   816  
   817  	// Check that OrderType is set correctly
   818  	if cancel.OrderType != msgjson.CancelOrderNum {
   819  		return msgjson.NewError(msgjson.OrderParameterError, "wrong order type set for cancel order")
   820  	}
   821  
   822  	rpcErr = checkTimes(&cancel.Prefix)
   823  	if rpcErr != nil {
   824  		return rpcErr
   825  	}
   826  
   827  	// Commitment.
   828  	if len(cancel.Commit) != order.CommitmentSize {
   829  		return msgjson.NewError(msgjson.OrderParameterError, "invalid commitment")
   830  	}
   831  	var commit order.Commitment
   832  	copy(commit[:], cancel.Commit)
   833  
   834  	// Create the cancel order
   835  	co := &order.CancelOrder{
   836  		P: order.Prefix{
   837  			AccountID:  user,
   838  			BaseAsset:  cancel.Base,
   839  			QuoteAsset: cancel.Quote,
   840  			OrderType:  order.CancelOrderType,
   841  			ClientTime: time.UnixMilli(int64(cancel.ClientTime)),
   842  			//ServerTime set in epoch queue processing pipeline.
   843  			Commit: commit,
   844  		},
   845  		TargetOrderID: targetID,
   846  	}
   847  
   848  	// Send the order to the epoch queue.
   849  	oRecord := &orderRecord{
   850  		order: co,
   851  		req:   cancel,
   852  		msgID: msg.ID,
   853  	}
   854  	if err := tunnel.SubmitOrder(oRecord); err != nil {
   855  		if errors.Is(err, ErrInternalServer) {
   856  			log.Errorf("Market failed to SubmitOrder: %v", err)
   857  		}
   858  		return msgjson.NewError(msgjson.UnknownMarketError, "%v", err)
   859  	}
   860  	return nil
   861  }
   862  
   863  // verifyAccount checks that the submitted order squares with the submitting user.
   864  func (r *OrderRouter) verifyAccount(user account.AccountID, msgAcct msgjson.Bytes, signable msgjson.Signable) *msgjson.Error {
   865  	// Verify account ID matches.
   866  	if !bytes.Equal(user[:], msgAcct) {
   867  		return msgjson.NewError(msgjson.OrderParameterError, "account ID mismatch")
   868  	}
   869  	// Check the clients signature of the order.
   870  	sigMsg := signable.Serialize()
   871  	err := r.auth.Auth(user, sigMsg, signable.SigBytes())
   872  	if err != nil {
   873  		return msgjson.NewError(msgjson.SignatureError, "signature error: %v", err.Error())
   874  	}
   875  	return nil
   876  }
   877  
   878  // extractMarket finds the MarketTunnel for the provided prefix.
   879  func (r *OrderRouter) extractMarket(prefix *msgjson.Prefix) (MarketTunnel, *msgjson.Error) {
   880  	mktName, err := dex.MarketName(prefix.Base, prefix.Quote)
   881  	if err != nil {
   882  		return nil, msgjson.NewError(msgjson.UnknownMarketError, "asset lookup error: %v", err.Error())
   883  	}
   884  	tunnel, found := r.tunnels[mktName]
   885  	if !found {
   886  		return nil, msgjson.NewError(msgjson.UnknownMarketError, "unknown market %s", mktName)
   887  	}
   888  	return tunnel, nil
   889  }
   890  
   891  // SuspendEpoch holds the index and end time of final epoch marking the
   892  // suspension of a market.
   893  type SuspendEpoch struct {
   894  	Idx int64
   895  	End time.Time
   896  }
   897  
   898  // SuspendMarket schedules a suspension of a given market, with the option to
   899  // persist the orders on the book (or purge the book automatically on market
   900  // shutdown). The scheduled final epoch and suspend time are returned. Note that
   901  // OrderRouter is a proxy for this request to the ultimate Market. This is done
   902  // because OrderRouter is the entry point for new orders into the market. TODO:
   903  // track running, suspended, and scheduled-suspended markets, appropriately
   904  // blocking order submission according to the schedule rather than just checking
   905  // Market.Running prior to submitting incoming orders to the Market.
   906  func (r *OrderRouter) SuspendMarket(mktName string, asSoonAs time.Time, persistBooks bool) *SuspendEpoch {
   907  	mkt, found := r.tunnels[mktName]
   908  	if !found {
   909  		return nil
   910  	}
   911  
   912  	idx, t := mkt.Suspend(asSoonAs, persistBooks)
   913  	return &SuspendEpoch{
   914  		Idx: idx,
   915  		End: t,
   916  	}
   917  }
   918  
   919  // Suspend is like SuspendMarket, but for all known markets.
   920  func (r *OrderRouter) Suspend(asSoonAs time.Time, persistBooks bool) map[string]*SuspendEpoch {
   921  
   922  	suspendTimes := make(map[string]*SuspendEpoch, len(r.tunnels))
   923  	for name, mkt := range r.tunnels {
   924  		idx, ts := mkt.Suspend(asSoonAs, persistBooks)
   925  		suspendTimes[name] = &SuspendEpoch{Idx: idx, End: ts}
   926  	}
   927  
   928  	// MarketTunnel.Running will return false when the market closes, and true
   929  	// when and if it opens again. Locking/blocking of the incoming order
   930  	// handlers is not necessary since any orders that sneak in to a Market will
   931  	// be rejected if there is no active epoch.
   932  
   933  	return suspendTimes
   934  }
   935  
   936  // extractMarketDetails finds the MarketTunnel, an assetSet, and market side for
   937  // the provided prefix.
   938  func (r *OrderRouter) extractMarketDetails(prefix *msgjson.Prefix, trade *msgjson.Trade) (MarketTunnel, *assetSet, bool, *msgjson.Error) {
   939  	// Check that assets are for a valid market.
   940  	tunnel, rpcErr := r.extractMarket(prefix)
   941  	if rpcErr != nil {
   942  		return nil, nil, false, rpcErr
   943  	}
   944  	// Side must be one of buy or sell
   945  	var sell bool
   946  	switch trade.Side {
   947  	case msgjson.BuyOrderNum:
   948  	case msgjson.SellOrderNum:
   949  		sell = true
   950  	default:
   951  		return nil, nil, false, msgjson.NewError(msgjson.OrderParameterError,
   952  			"invalid side value %d", trade.Side)
   953  	}
   954  	quote, found := r.assets[prefix.Quote]
   955  	if !found {
   956  		panic("missing quote asset for known market should be impossible")
   957  	}
   958  	base, found := r.assets[prefix.Base]
   959  	if !found {
   960  		panic("missing base asset for known market should be impossible")
   961  	}
   962  	return tunnel, newAssetSet(base, quote, sell), sell, nil
   963  }
   964  
   965  // checkTimes validates the timestamps in an order prefix.
   966  func checkTimes(prefix *msgjson.Prefix) *msgjson.Error {
   967  	offset := time.Now().UnixMilli() - int64(prefix.ClientTime)
   968  	if offset < 0 {
   969  		offset *= -1
   970  	}
   971  	if offset >= maxClockOffset {
   972  		return msgjson.NewError(msgjson.ClockRangeError,
   973  			"clock offset of %d ms is larger than maximum allowed, %d ms",
   974  			offset, maxClockOffset,
   975  		)
   976  	}
   977  	// Server time should be unset.
   978  	if prefix.ServerTime != 0 {
   979  		return msgjson.NewError(msgjson.OrderParameterError, "non-zero server time not allowed")
   980  	}
   981  	return nil
   982  }
   983  
   984  // checkPrefixTrade validates the information in the prefix and trade portions
   985  // of an order.
   986  func (r *OrderRouter) checkPrefixTrade(assets *assetSet, lotSize uint64, prefix *msgjson.Prefix,
   987  	trade *msgjson.Trade, checkLot bool) *msgjson.Error {
   988  	// Check that the client's timestamp is still valid.
   989  	rpcErr := checkTimes(prefix)
   990  	if rpcErr != nil {
   991  		return rpcErr
   992  	}
   993  	// Check that the address is valid.
   994  	if !assets.receiving.Backend.CheckSwapAddress(trade.Address) {
   995  		return msgjson.NewError(msgjson.OrderParameterError, "address doesn't check")
   996  	}
   997  	// Quantity cannot be zero, and must be an integral multiple of the lot size.
   998  	if trade.Quantity == 0 {
   999  		return msgjson.NewError(msgjson.OrderParameterError, "zero quantity not allowed")
  1000  	}
  1001  	if checkLot && trade.Quantity%lotSize != 0 {
  1002  		return msgjson.NewError(msgjson.OrderParameterError, "order quantity not a multiple of lot size")
  1003  	}
  1004  	// Validate UTXOs
  1005  	// Check that all required arrays are of equal length.
  1006  	if len(trade.Coins) == 0 {
  1007  		return msgjson.NewError(msgjson.FundingError, "order must specify utxos")
  1008  	}
  1009  
  1010  	for i, coin := range trade.Coins {
  1011  		sigCount := len(coin.Sigs)
  1012  		if sigCount == 0 {
  1013  			return msgjson.NewError(msgjson.SignatureError, "no signature for coin %d", i)
  1014  		}
  1015  		if len(coin.PubKeys) != sigCount {
  1016  			return msgjson.NewError(msgjson.OrderParameterError,
  1017  				"pubkey count %d not equal to signature count %d for coin %d",
  1018  				len(coin.PubKeys), sigCount, i,
  1019  			)
  1020  		}
  1021  	}
  1022  
  1023  	return nil
  1024  }
  1025  
  1026  // msgBytesToBytes converts a []msgjson.Byte to a [][]byte.
  1027  func msgBytesToBytes(msgBs []msgjson.Bytes) [][]byte {
  1028  	b := make([][]byte, 0, len(msgBs))
  1029  	for _, msgB := range msgBs {
  1030  		b = append(b, msgB)
  1031  	}
  1032  	return b
  1033  }
  1034  
  1035  // fmtCoinID formats the coin ID by asset. If an error is encountered, the
  1036  // coinID string returned hex-encoded and prepended with "unparsed:".
  1037  func fmtCoinID(assetID uint32, coinID []byte) string {
  1038  	strID, err := asset.DecodeCoinID(assetID, coinID)
  1039  	if err != nil {
  1040  		return "unparsed:" + hex.EncodeToString(coinID)
  1041  	}
  1042  	return strID
  1043  }
  1044  
  1045  // fmtCoinIDs is like fmtCoinID but for a slice of CoinIDs, printing with
  1046  // default Go slice formatting like "[coin1 coin2 ...]".
  1047  func fmtCoinIDs(assetID uint32, coinIDs []order.CoinID) string {
  1048  	out := make([]string, len(coinIDs))
  1049  	for i := range coinIDs {
  1050  		out[i] = fmtCoinID(assetID, coinIDs[i])
  1051  	}
  1052  	return fmt.Sprint(out)
  1053  }
  1054  
  1055  func safeMidGap(tunnel MarketTunnel) uint64 {
  1056  	midGap := tunnel.MidGap()
  1057  	if midGap == 0 {
  1058  		return tunnel.RateStep()
  1059  	}
  1060  	return midGap
  1061  }