decred.org/dcrdex@v1.0.5/server/market/balancer.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  	"fmt"
     8  
     9  	"decred.org/dcrdex/dex"
    10  	"decred.org/dcrdex/dex/calc"
    11  	"decred.org/dcrdex/server/asset"
    12  )
    13  
    14  // PendingAccounter can view order-reserved funds for an account-based asset's
    15  // address. PendingAccounter is satisfied by *Market.
    16  type PendingAccounter interface {
    17  	// AccountPending retrieves the total pending order-reserved quantity for
    18  	// the asset, as well as the number of possible pending redemptions
    19  	// (a.k.a. ordered lots).
    20  	AccountPending(acctAddr string, assetID uint32) (qty, lots uint64, redeems int)
    21  	// Base is the base asset ID.
    22  	Base() uint32
    23  	// Quote is the quote asset ID.
    24  	Quote() uint32
    25  }
    26  
    27  // MatchNegotiator can view match-reserved funds for an account-based asset's
    28  // address. MatchNegotiator is satisfied by *Swapper.
    29  type MatchNegotiator interface {
    30  	// AccountStats collects stats about pending matches for account's address
    31  	// on an account-based asset. qty is the total pending outgoing quantity,
    32  	// swaps is the number matches with outstanding swaps funded by the account,
    33  	// and redeem is the number of matches with outstanding redemptions that pay
    34  	// to the account.
    35  	AccountStats(acctAddr string, assetID uint32) (qty, swaps uint64, redeems int)
    36  }
    37  
    38  // BackedBalancer is an asset manager that is capable of querying the entire DEX
    39  // for the balance required to fulfill new + existing orders and outstanding
    40  // redemptions.
    41  type DEXBalancer struct {
    42  	assets          map[uint32]*backedBalancer
    43  	matchNegotiator MatchNegotiator
    44  }
    45  
    46  // NewDEXBalancer is a constructor for a DEXBalancer. Provided assets will
    47  // be filtered for those that are account-based. The matchNegotiator is
    48  // satisfied by the *Swapper.
    49  func NewDEXBalancer(tunnels map[string]PendingAccounter, assets map[uint32]*asset.BackedAsset, matchNegotiator MatchNegotiator) (*DEXBalancer, error) {
    50  	balancers := make(map[uint32]*backedBalancer)
    51  
    52  	addAsset := func(ba *asset.BackedAsset) error {
    53  		assetID := ba.ID
    54  		balancer, is := ba.Backend.(asset.AccountBalancer)
    55  		if !is {
    56  			return nil
    57  		}
    58  
    59  		var markets []PendingAccounter
    60  		for _, mkt := range tunnels {
    61  			if mkt.Base() == assetID || mkt.Quote() == assetID {
    62  				markets = append(markets, mkt)
    63  			}
    64  		}
    65  
    66  		bb := &backedBalancer{
    67  			balancer:  balancer,
    68  			assetInfo: &ba.Asset,
    69  			feeFamily: make(map[uint32]*dex.Asset),
    70  			markets:   markets,
    71  		}
    72  		balancers[assetID] = bb
    73  
    74  		isToken, parentID := asset.IsToken(assetID)
    75  		if isToken {
    76  			parent, found := balancers[parentID]
    77  			if !found {
    78  				return fmt.Errorf("%s parent asset %d(%s) not found", ba.Symbol, parentID, dex.BipIDSymbol(parentID))
    79  			}
    80  			bb.feeFamily[parentID] = parent.assetInfo
    81  			for tokenID := range asset.Tokens(parentID) {
    82  				// Don't double count. Also, some tokens are specific
    83  				// to certain networks. A token not in the assets
    84  				// map indicates it does not exist for this network.
    85  				if tokenID == assetID || assets[tokenID] == nil {
    86  					continue
    87  				}
    88  				bb.feeFamily[tokenID] = &assets[tokenID].Asset
    89  			}
    90  			bb.feeBalancer = parent
    91  		} else {
    92  			for tokenID := range asset.Tokens(assetID) {
    93  				if familyAsset, found := assets[tokenID]; found {
    94  					if _, is := familyAsset.Backend.(asset.AccountBalancer); !is {
    95  						return fmt.Errorf("fee-family asset %s is not an AccountBalancer", familyAsset.Symbol)
    96  					}
    97  					bb.feeFamily[tokenID] = &familyAsset.Asset
    98  				}
    99  			}
   100  		}
   101  		return nil
   102  	}
   103  
   104  	// Add base chain assets first, then tokens.
   105  	tokens := make([]*asset.BackedAsset, 0)
   106  
   107  	for assetID, ba := range assets {
   108  		if isToken, _ := asset.IsToken(assetID); isToken {
   109  			tokens = append(tokens, ba)
   110  			continue
   111  		}
   112  		if err := addAsset(ba); err != nil {
   113  			return nil, err
   114  		}
   115  	}
   116  
   117  	for _, ba := range tokens {
   118  		if err := addAsset(ba); err != nil {
   119  			return nil, err
   120  		}
   121  	}
   122  
   123  	return &DEXBalancer{
   124  		assets:          balancers,
   125  		matchNegotiator: matchNegotiator,
   126  	}, nil
   127  }
   128  
   129  // CheckBalance checks if there is sufficient balance to support the specified
   130  // new funding and redemptions, given the existing orders throughout DEX that
   131  // fund from or redeem to the specified account address for the account-based
   132  // asset. It is an internally logged error to call CheckBalance for a
   133  // non-account-based asset or an asset that was not provided to the constructor.
   134  // Because these assets may have a base chain as well as degenerate tokens,
   135  // we need to consider outstanding orders and matches across all "fee family"
   136  // assets.
   137  // It is acceptable to call CheckBalance with qty = 0, lots = 0, redeems > 0
   138  // and assetID = redeemAssetID, as might be the case when checking that a user
   139  // has sufficient balance to redeem an order's matches.
   140  func (b *DEXBalancer) CheckBalance(acctAddr string, assetID, redeemAssetID uint32, qty, lots uint64, redeems int) bool {
   141  	backedAsset, found := b.assets[assetID]
   142  	if !found {
   143  		log.Errorf("(*DEXBalancer).CheckBalance: asset ID %d not a configured backedBalancer", assetID)
   144  		return false
   145  	}
   146  
   147  	log.Tracef("balance check for %s - %s: new qty = %d, new lots = %d, new redeems = %d",
   148  		backedAsset.assetInfo.Symbol, acctAddr, qty, lots, redeems)
   149  
   150  	// Make sure we can get the primary balance first.
   151  	bal, err := backedAsset.balancer.AccountBalance(acctAddr)
   152  	if err != nil {
   153  		log.Errorf("(*DEXBalancer).CheckBalance: error getting account balance for %q: %v", acctAddr, err)
   154  		return false
   155  	}
   156  	if qty > 0 && bal == 0 { // shortcut if they are requesting funds and have none.
   157  		log.Tracef("(*DEXBalancer).CheckBalance(%q, %d, %d, %d, %d) false for zero balance",
   158  			acctAddr, assetID, qty, lots, redeems)
   159  		return false
   160  	}
   161  
   162  	// Set up fee tracking for tokens.
   163  	var feeID uint32 // Asset ID of the base chain asset.
   164  	// feeQty will track the fee asset order-locked quantity, but is only used
   165  	// for tokens. When not a token (when assetID = base chain), the
   166  	// order-locked quantity is summed in reqFunds as for any other asset. We'll
   167  	// fetch feeBal to catch any zero bals or other errors early before
   168  	// iterating markets and querying the Swapper.
   169  	var feeQty, feeBal uint64 // Current balance.
   170  	feeBalancer := backedAsset.feeBalancer
   171  	isToken := feeBalancer != nil
   172  	if isToken {
   173  		feeID = feeBalancer.assetInfo.ID
   174  		feeBal, err = feeBalancer.balancer.AccountBalance(acctAddr)
   175  		if err != nil {
   176  			log.Error("(*DEXBalancer).CheckBalance: error getting fee asset balance for %q: %v", acctAddr, err)
   177  			return false
   178  		}
   179  		if feeBal == 0 {
   180  			log.Tracef("(*DEXBalancer).CheckBalance(%q, %d, %d, %d, %d) false for zero fee asset (%s) balance",
   181  				acctAddr, assetID, qty, lots, redeems, feeBalancer.assetInfo.Symbol)
   182  			return false
   183  		}
   184  	}
   185  
   186  	var swapFees, redeemFees uint64
   187  	redeemTxSize, initTxSize := backedAsset.balancer.RedeemSize(), backedAsset.balancer.InitTxSize()
   188  	addFees := func(assetInfo *dex.Asset, l uint64, r int) {
   189  		// The fee rate assigned to redemptions is at the discretion of the
   190  		// user. MaxFeeRate is used as a conservatively high estimate. This is
   191  		// then a server policy that clients must satisfy.
   192  		redeemFees += uint64(r) * redeemTxSize * assetInfo.MaxFeeRate
   193  		swapFees += calc.RequiredOrderFunds(0, 0, l, initTxSize, initTxSize, assetInfo.MaxFeeRate) // Alt: assetInfo.SwapSize * l
   194  	}
   195  
   196  	// Add the fees for the requested lots.
   197  	addFees(backedAsset.assetInfo, lots, redeems)
   198  
   199  	// Prepare a function to add fees pending orders for the asset and each
   200  	// asset in the fee-family.
   201  	addPending := func(assetID uint32) (q uint64) {
   202  		ba, found := b.assets[assetID]
   203  		if !found {
   204  			log.Errorf("(*DEXBalancer).CheckBalance: asset ID %d not a configured backedBalancer", assetID)
   205  			return 0
   206  		}
   207  
   208  		var l uint64
   209  		var r int
   210  		for _, mt := range ba.markets {
   211  			newQty, newLots, newRedeems := mt.AccountPending(acctAddr, assetID)
   212  			l += newLots
   213  			q += newQty
   214  			r += newRedeems
   215  		}
   216  
   217  		// Add in-process swaps.
   218  		newQty, newLots, newRedeems := b.matchNegotiator.AccountStats(acctAddr, assetID)
   219  		l += newLots
   220  		q += newQty
   221  		r += newRedeems
   222  
   223  		addFees(ba.assetInfo, l, r)
   224  		return
   225  	}
   226  
   227  	qty += addPending(assetID)
   228  
   229  	// Add fee-family asset pending fees.
   230  	for famID := range backedAsset.feeFamily {
   231  		q := addPending(famID)
   232  		if isToken && famID == feeID {
   233  			// Add the quantity of the base chain order reserves to our fee
   234  			// asset balance check.
   235  			feeQty = q
   236  		}
   237  	}
   238  
   239  	// Add redeems if we're redeeming this to a fee-family asset.
   240  	if assetID != redeemAssetID { // Don't double count
   241  		if redeemAsset, found := backedAsset.feeFamily[redeemAssetID]; found {
   242  			redeemFees += redeemTxSize * redeemAsset.MaxFeeRate * lots
   243  		}
   244  	}
   245  
   246  	reqFunds := qty
   247  	if isToken { // Check sufficient fee asset balance for tokens.
   248  		reqFeeAssetQty := swapFees + redeemFees + feeQty
   249  		if feeBal < reqFeeAssetQty {
   250  			log.Tracef("(*DEXBalancer).CheckBalance(%q, %d, %d, %d, %d) false for low fee asset balance. %d < %d",
   251  				acctAddr, assetID, qty, lots, redeems, feeBal, reqFeeAssetQty)
   252  			return false
   253  		}
   254  	} else { // Add fees to main qty for base chain assets.
   255  		reqFunds += redeemFees + swapFees
   256  	}
   257  
   258  	log.Tracef("(*DEXBalancer).CheckBalance: balance check for %s - %s: total qty = %d, "+
   259  		"total lots = %d, total redeems = %d, redeemCosts = %d, required = %d, bal = %d, fee bal = %d",
   260  		backedAsset.assetInfo.Symbol, acctAddr, qty, lots, redeems, redeemFees, reqFunds, bal, feeBal)
   261  
   262  	return bal >= reqFunds
   263  }
   264  
   265  // backedBalancer is similar to a BackedAsset, but with the Backends already
   266  // cast to AccountBalancer.
   267  type backedBalancer struct {
   268  	balancer    asset.AccountBalancer
   269  	assetInfo   *dex.Asset
   270  	feeBalancer *backedBalancer       // feeBalancer != nil implies that this is a token
   271  	feeFamily   map[uint32]*dex.Asset // Excluding self
   272  	markets     []PendingAccounter
   273  }