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 }