decred.org/dcrdex@v1.0.3/client/core/core.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 core 5 6 import ( 7 "bytes" 8 "context" 9 "crypto/sha256" 10 "encoding/binary" 11 "encoding/csv" 12 "encoding/hex" 13 "encoding/json" 14 "errors" 15 "fmt" 16 "math" 17 "net" 18 "net/url" 19 "os" 20 "path/filepath" 21 "runtime" 22 "runtime/debug" 23 "sort" 24 "strconv" 25 "strings" 26 "sync" 27 "sync/atomic" 28 "time" 29 30 "decred.org/dcrdex/client/asset" 31 "decred.org/dcrdex/client/comms" 32 "decred.org/dcrdex/client/db" 33 "decred.org/dcrdex/client/db/bolt" 34 "decred.org/dcrdex/client/mnemonic" 35 "decred.org/dcrdex/client/orderbook" 36 "decred.org/dcrdex/dex" 37 "decred.org/dcrdex/dex/calc" 38 "decred.org/dcrdex/dex/config" 39 "decred.org/dcrdex/dex/encode" 40 "decred.org/dcrdex/dex/encrypt" 41 "decred.org/dcrdex/dex/msgjson" 42 "decred.org/dcrdex/dex/order" 43 "decred.org/dcrdex/dex/wait" 44 "decred.org/dcrdex/server/account" 45 serverdex "decred.org/dcrdex/server/dex" 46 "github.com/decred/dcrd/crypto/blake256" 47 "github.com/decred/dcrd/dcrec/secp256k1/v4" 48 "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" 49 "github.com/decred/dcrd/hdkeychain/v3" 50 "github.com/decred/go-socks/socks" 51 "golang.org/x/text/language" 52 "golang.org/x/text/message" 53 ) 54 55 const ( 56 // tickCheckDivisions is how many times to tick trades per broadcast timeout 57 // interval. e.g. 12 min btimeout / 8 divisions = 90 sec between checks. 58 tickCheckDivisions = 8 59 // defaultTickInterval is the tick interval used before the broadcast 60 // timeout is known (e.g. startup with down server). 61 defaultTickInterval = 30 * time.Second 62 63 marketBuyRedemptionSlippageBuffer = 2 64 65 // preimageReqTimeout the server's preimage request timeout period. When 66 // considered with a market's epoch duration, this is used to detect when an 67 // order should have gone through matching for a certain epoch. TODO: 68 // consider sharing const for the preimage timeout with the server packages, 69 // or a config response field if it should be considered variable. 70 preimageReqTimeout = 20 * time.Second 71 72 // wsMaxAnomalyCount is the maximum websocket connection anomaly after which 73 // a client receives a notification to check their connectivity. 74 wsMaxAnomalyCount = 3 75 // If a client's websocket connection to a server disconnects before 76 // wsAnomalyDuration since last connect time, the client's websocket 77 // connection anomaly count is increased. 78 wsAnomalyDuration = 60 * time.Minute 79 80 // This is a configurable server parameter, but we're assuming servers have 81 // changed it from the default , We're using this for the v1 ConnectResult, 82 // where we don't have the necessary information to calculate our bonded 83 // tier, so we calculate our bonus/revoked tier from the score in the 84 // ConnectResult. 85 defaultPenaltyThreshold = 20 86 87 // legacySeedLength is the length of the generated app seed used for app protection. 88 legacySeedLength = 64 89 90 // pokesCapacity is the maximum number of poke notifications that 91 // will be cached. 92 pokesCapacity = 100 93 94 // walletLockTimeout is the default timeout used when locking wallets. 95 walletLockTimeout = 5 * time.Second 96 ) 97 98 var ( 99 unbip = dex.BipIDSymbol 100 // The coin waiters will query for transaction data every recheckInterval. 101 recheckInterval = time.Second * 5 102 // When waiting for a wallet to sync, a SyncStatus check will be performed 103 // every syncTickerPeriod. var instead of const for testing purposes. 104 syncTickerPeriod = 3 * time.Second 105 // supportedAPIVers are the DEX server API versions this client is capable 106 // of communicating with. 107 // 108 // NOTE: API version may change at any time. Keep this in mind when 109 // updating the API. Long-running operations may start and end with 110 // differing versions. 111 supportedAPIVers = []int32{serverdex.V1APIVersion} 112 // ActiveOrdersLogoutErr is returned from logout when there are active 113 // orders. 114 ActiveOrdersLogoutErr = errors.New("cannot log out with active orders") 115 // walletDisabledErrStr is the error message returned when trying to use a 116 // disabled wallet. 117 walletDisabledErrStr = "%s wallet is disabled" 118 119 errTimeout = errors.New("timeout") 120 ) 121 122 type dexTicker struct { 123 dur int64 // atomic 124 *time.Ticker 125 } 126 127 func newDexTicker(dur time.Duration) *dexTicker { 128 return &dexTicker{ 129 dur: int64(dur), 130 Ticker: time.NewTicker(dur), 131 } 132 } 133 134 func (dt *dexTicker) Reset(dur time.Duration) { 135 atomic.StoreInt64(&dt.dur, int64(dur)) 136 dt.Ticker.Reset(dur) 137 } 138 139 func (dt *dexTicker) Dur() time.Duration { 140 return time.Duration(atomic.LoadInt64(&dt.dur)) 141 } 142 143 type pendingFeeState struct { 144 confs uint32 145 asset uint32 146 } 147 148 // dexConnection is the websocket connection and the DEX configuration. 149 type dexConnection struct { 150 comms.WsConn 151 connMaster *dex.ConnectionMaster 152 log dex.Logger 153 acct *dexAccount 154 notify func(Notification) 155 ticker *dexTicker 156 // apiVer is an atomic. An uninitiated connection should be set to -1. 157 apiVer int32 158 159 assetsMtx sync.RWMutex 160 assets map[uint32]*dex.Asset 161 162 cfgMtx sync.RWMutex 163 cfg *msgjson.ConfigResult 164 165 booksMtx sync.RWMutex 166 books map[string]*bookie 167 168 // tradeMtx is used to synchronize access to the trades map. 169 tradeMtx sync.RWMutex 170 // trades tracks outstanding orders issued by this client. 171 trades map[order.OrderID]*trackedTrade 172 // inFlightOrders tracks orders issued by this client that have not been 173 // processed by a dex server. 174 inFlightOrders map[uint64]*InFlightOrder 175 176 // A map linking cancel order IDs to trade order IDs. 177 cancelsMtx sync.RWMutex 178 cancels map[order.OrderID]order.OrderID 179 180 blindCancelsMtx sync.Mutex 181 blindCancels map[order.OrderID]order.Preimage 182 183 epochMtx sync.RWMutex 184 epoch map[string]uint64 185 // resolvedEpoch differs from epoch in that an epoch is not considered 186 // resolved until all of our orders are out of the epoch queue. i.e. 187 // we have received match or nomatch notification for all of our orders 188 // from the epoch. 189 resolvedEpoch map[string]uint64 190 191 // connectionStatus is a best guess on the ws connection status. 192 connectionStatus uint32 193 194 reportingConnects uint32 195 196 spotsMtx sync.RWMutex 197 spots map[string]*msgjson.Spot 198 199 // anomaliesCount tracks client's connection anomalies. 200 anomaliesCount uint32 // atomic 201 lastConnectMtx sync.RWMutex 202 lastConnect time.Time 203 } 204 205 // DefaultResponseTimeout is the default timeout for responses after a request is 206 // successfully sent. 207 const ( 208 DefaultResponseTimeout = comms.DefaultResponseTimeout 209 fundingTxWait = time.Minute // TODO: share var with server/market or put in config 210 ) 211 212 // running returns the status of the provided market. 213 func (dc *dexConnection) running(mkt string) bool { 214 dc.cfgMtx.RLock() 215 defer dc.cfgMtx.RUnlock() 216 mktCfg := dc.findMarketConfig(mkt) 217 if mktCfg == nil { 218 return false // not found means not running 219 } 220 return mktCfg.Running() 221 } 222 223 // status returns the status of the connection to the dex. 224 func (dc *dexConnection) status() comms.ConnectionStatus { 225 return comms.ConnectionStatus(atomic.LoadUint32(&dc.connectionStatus)) 226 } 227 228 func (dc *dexConnection) config() *msgjson.ConfigResult { 229 dc.cfgMtx.RLock() 230 defer dc.cfgMtx.RUnlock() 231 return dc.cfg 232 } 233 234 func (dc *dexConnection) bondAsset(assetID uint32) (*msgjson.BondAsset, uint64) { 235 assetSymb := dex.BipIDSymbol(assetID) 236 dc.cfgMtx.RLock() 237 defer dc.cfgMtx.RUnlock() 238 bondExpiry := dc.cfg.BondExpiry 239 bondAsset := dc.cfg.BondAssets[assetSymb] 240 return bondAsset, bondExpiry // bondAsset may be nil 241 } 242 243 func (dc *dexConnection) bondAssets() (map[uint32]*BondAsset, uint64) { 244 bondAssets := make(map[uint32]*BondAsset) 245 cfg := dc.config() 246 if cfg == nil { 247 return nil, 0 248 } 249 for symb, ba := range cfg.BondAssets { 250 assetID, ok := dex.BipSymbolID(symb) 251 if !ok { 252 continue 253 } 254 coreBondAsset := BondAsset(*ba) 255 bondAssets[assetID] = &coreBondAsset 256 } 257 return bondAssets, cfg.BondExpiry 258 } 259 260 func (dc *dexConnection) registerCancelLink(cid, oid order.OrderID) { 261 dc.cancelsMtx.Lock() 262 dc.cancels[cid] = oid 263 dc.cancelsMtx.Unlock() 264 } 265 266 func (dc *dexConnection) deleteCancelLink(cid order.OrderID) { 267 dc.cancelsMtx.Lock() 268 delete(dc.cancels, cid) 269 dc.cancelsMtx.Unlock() 270 } 271 272 func (dc *dexConnection) cancelTradeID(cid order.OrderID) (order.OrderID, bool) { 273 dc.cancelsMtx.RLock() 274 defer dc.cancelsMtx.RUnlock() 275 oid, found := dc.cancels[cid] 276 return oid, found 277 } 278 279 // marketConfig is the market's configuration, as returned by the server in the 280 // 'config' response. 281 func (dc *dexConnection) marketConfig(mktID string) *msgjson.Market { 282 dc.cfgMtx.RLock() 283 defer dc.cfgMtx.RUnlock() 284 return dc.findMarketConfig(mktID) 285 } 286 287 func (dc *dexConnection) assetConfig(assetID uint32) *dex.Asset { 288 dc.assetsMtx.RLock() 289 defer dc.assetsMtx.RUnlock() 290 return dc.assets[assetID] 291 } 292 293 // marketMap creates a map of this DEX's *Market keyed by name/ID, 294 // [base]_[quote]. 295 func (dc *dexConnection) marketMap() map[string]*Market { 296 dc.cfgMtx.RLock() 297 cfg := dc.cfg 298 dc.cfgMtx.RUnlock() 299 if cfg == nil { 300 return nil 301 } 302 mktConfigs := cfg.Markets 303 304 marketMap := make(map[string]*Market, len(mktConfigs)) 305 for _, msgMkt := range mktConfigs { 306 mkt := coreMarketFromMsgMarket(dc, msgMkt) 307 marketMap[mkt.marketName()] = mkt 308 } 309 310 // Populate spots. 311 dc.spotsMtx.RLock() 312 for mktID, mkt := range marketMap { 313 mkt.SpotPrice = dc.spots[mktID] 314 } 315 dc.spotsMtx.RUnlock() 316 317 return marketMap 318 } 319 320 // marketMap creates a map of this DEX's *Market keyed by name/ID, 321 // [base]_[quote]. 322 func (dc *dexConnection) coreMarket(mktName string) *Market { 323 dc.cfgMtx.RLock() 324 cfg := dc.cfg 325 dc.cfgMtx.RUnlock() 326 if cfg == nil { 327 return nil 328 } 329 var mkt *Market 330 for _, m := range cfg.Markets { 331 if m.Name == mktName { 332 mkt = coreMarketFromMsgMarket(dc, m) 333 break 334 } 335 } 336 if mkt == nil { 337 return nil 338 } 339 340 // Populate spots. 341 dc.spotsMtx.RLock() 342 mkt.SpotPrice = dc.spots[mktName] 343 dc.spotsMtx.RUnlock() 344 345 return mkt 346 } 347 348 func coreMarketFromMsgMarket(dc *dexConnection, msgMkt *msgjson.Market) *Market { 349 // The presence of the asset for every market was already verified when the 350 // dexConnection was created in connectDEX. 351 dc.assetsMtx.RLock() 352 base, quote := dc.assets[msgMkt.Base], dc.assets[msgMkt.Quote] 353 dc.assetsMtx.RUnlock() 354 355 bconv, qconv := base.UnitInfo.Conventional.ConversionFactor, quote.UnitInfo.Conventional.ConversionFactor 356 357 mkt := &Market{ 358 Name: msgMkt.Name, 359 BaseID: base.ID, 360 BaseSymbol: base.Symbol, 361 QuoteID: quote.ID, 362 QuoteSymbol: quote.Symbol, 363 LotSize: msgMkt.LotSize, 364 ParcelSize: msgMkt.ParcelSize, 365 RateStep: msgMkt.RateStep, 366 EpochLen: msgMkt.EpochLen, 367 StartEpoch: msgMkt.StartEpoch, 368 MarketBuyBuffer: msgMkt.MarketBuyBuffer, 369 AtomToConv: float64(bconv) / float64(qconv), 370 MinimumRate: dc.minimumMarketRate(quote, msgMkt.LotSize), 371 } 372 373 trades, inFlight := dc.marketTrades(mkt.marketName()) 374 mkt.InFlightOrders = inFlight 375 376 for _, trade := range trades { 377 mkt.Orders = append(mkt.Orders, trade.coreOrder()) 378 } 379 380 return mkt 381 } 382 383 func (dc *dexConnection) minimumMarketRate(q *dex.Asset, lotSize uint64) uint64 { 384 quoteDust, found := asset.MinimumLotSize(q.ID, q.MaxFeeRate) 385 if !found { 386 dc.log.Errorf("couldn't find minimum lot size for %s", q.Symbol) 387 return 0 388 } 389 return calc.MinimumMarketRate(lotSize, quoteDust) 390 } 391 392 // temporaryOrderIDCounter is used for inflight orders and must never be zero 393 // when used for an inflight order. 394 var temporaryOrderIDCounter uint64 395 396 // storeInFlightOrder stores an inflight order and returns a generated ID. 397 func (dc *dexConnection) storeInFlightOrder(ord *Order) uint64 { 398 tempID := atomic.AddUint64(&temporaryOrderIDCounter, 1) 399 dc.tradeMtx.Lock() 400 dc.inFlightOrders[tempID] = &InFlightOrder{ 401 Order: ord, 402 TemporaryID: tempID, 403 } 404 dc.tradeMtx.Unlock() 405 return tempID 406 } 407 408 func (dc *dexConnection) deleteInFlightOrder(tempID uint64) { 409 dc.tradeMtx.Lock() 410 delete(dc.inFlightOrders, tempID) 411 dc.tradeMtx.Unlock() 412 } 413 414 func (dc *dexConnection) trackedTrades() []*trackedTrade { 415 dc.tradeMtx.RLock() 416 defer dc.tradeMtx.RUnlock() 417 allTrades := make([]*trackedTrade, 0, len(dc.trades)) 418 for _, trade := range dc.trades { 419 allTrades = append(allTrades, trade) 420 } 421 return allTrades 422 } 423 424 // marketTrades returns a slice of active trades in the trades map and a slice 425 // of inflight orders in the inFlightOrders map. 426 func (dc *dexConnection) marketTrades(mktID string) ([]*trackedTrade, []*InFlightOrder) { 427 // Copy trades to avoid locking both tradeMtx and trackedTrade.mtx. 428 allTrades := dc.trackedTrades() 429 trades := make([]*trackedTrade, 0, len(allTrades)) // may over-allocate 430 for _, trade := range allTrades { 431 if trade.mktID == mktID && trade.isActive() { 432 trades = append(trades, trade) 433 } 434 // Retiring inactive orders is presently the responsibility of ticker. 435 } 436 437 dc.tradeMtx.RLock() 438 inFlight := make([]*InFlightOrder, 0, len(dc.inFlightOrders)) // may over-allocate 439 for _, ord := range dc.inFlightOrders { 440 if ord.MarketID == mktID { 441 inFlight = append(inFlight, ord) 442 } 443 } 444 dc.tradeMtx.RUnlock() 445 return trades, inFlight 446 } 447 448 // pendingBonds returns the PendingBondState for all pending bonds. pendingBonds 449 // should be called with the acct.authMtx locked. 450 func (dc *dexConnection) pendingBonds() []*PendingBondState { 451 pendingBonds := make([]*PendingBondState, len(dc.acct.pendingBonds)) 452 for i, pb := range dc.acct.pendingBonds { 453 bondIDStr := coinIDString(pb.AssetID, pb.CoinID) 454 confs := dc.acct.pendingBondsConfs[bondIDStr] 455 pendingBonds[i] = &PendingBondState{ 456 CoinID: bondIDStr, 457 AssetID: pb.AssetID, 458 Symbol: unbip(pb.AssetID), 459 Confs: confs, 460 } 461 } 462 return pendingBonds 463 } 464 465 func (c *Core) exchangeInfo(dc *dexConnection) *Exchange { 466 // Set AcctID to empty string if not registered. 467 acctID := dc.acct.ID().String() 468 var emptyAcctID account.AccountID 469 if dc.acct.ID() == emptyAcctID { 470 acctID = "" 471 } 472 473 dc.cfgMtx.RLock() 474 cfg := dc.cfg 475 dc.cfgMtx.RUnlock() 476 if cfg == nil { // no config, assets, or markets data 477 return &Exchange{ 478 Host: dc.acct.host, 479 AcctID: acctID, 480 ConnectionStatus: dc.status(), 481 Disabled: dc.acct.isDisabled(), 482 Markets: make(map[string]*Market), 483 Assets: make(map[uint32]*dex.Asset), 484 BondAssets: make(map[string]*BondAsset), 485 } 486 } 487 488 bondAssets := make(map[string]*BondAsset, len(cfg.BondAssets)) 489 for symb, bondAsset := range cfg.BondAssets { 490 assetID, ok := dex.BipSymbolID(symb) 491 if !ok || assetID != bondAsset.ID { 492 dc.log.Warnf("Invalid bondAssets config with mismatched asset symbol %q and ID %d", 493 symb, bondAsset.ID) 494 } 495 coreBondAsset := BondAsset(*bondAsset) // convert msgjson.BondAsset to core.BondAsset 496 497 bondAssets[symb] = &coreBondAsset 498 } 499 500 dc.assetsMtx.RLock() 501 assets := make(map[uint32]*dex.Asset, len(dc.assets)) 502 for assetID, dexAsset := range dc.assets { 503 assets[assetID] = dexAsset 504 } 505 dc.assetsMtx.RUnlock() 506 507 bondCfg := c.dexBondConfig(dc, time.Now().Unix()) 508 acctBondState := c.bondStateOfDEX(dc, bondCfg) 509 510 return &Exchange{ 511 Host: dc.acct.host, 512 AcctID: acctID, 513 Markets: dc.marketMap(), 514 Assets: assets, 515 BondExpiry: cfg.BondExpiry, 516 BondAssets: bondAssets, 517 ConnectionStatus: dc.status(), 518 CandleDurs: cfg.BinSizes, 519 ViewOnly: dc.acct.isViewOnly(), 520 Auth: acctBondState.ExchangeAuth, 521 MaxScore: cfg.MaxScore, 522 PenaltyThreshold: cfg.PenaltyThreshold, 523 Disabled: dc.acct.isDisabled(), 524 } 525 } 526 527 // assetFamily prepares a map of asset IDs for asset that share a parent asset 528 // with the specified assetID. The assetID and the parent asset's ID both have 529 // entries, as well as any tokens. 530 func assetFamily(assetID uint32) map[uint32]bool { 531 assetFamily := make(map[uint32]bool, 1) 532 var parentAsset *asset.RegisteredAsset 533 if parentAsset = asset.Asset(assetID); parentAsset == nil { 534 if tkn := asset.TokenInfo(assetID); tkn != nil { 535 parentAsset = asset.Asset(tkn.ParentID) 536 } 537 } 538 if parentAsset != nil { 539 assetFamily[parentAsset.ID] = true 540 for tokenID := range parentAsset.Tokens { 541 assetFamily[tokenID] = true 542 } 543 } 544 return assetFamily 545 } 546 547 // hasActiveAssetOrders checks whether there are any active orders or negotiating 548 // matches for the specified asset. 549 func (dc *dexConnection) hasActiveAssetOrders(assetID uint32) bool { 550 familial := assetFamily(assetID) 551 dc.tradeMtx.RLock() 552 defer dc.tradeMtx.RUnlock() 553 for _, inFlight := range dc.inFlightOrders { 554 if familial[inFlight.BaseID] || familial[inFlight.QuoteID] { 555 return true 556 } 557 } 558 559 for _, trade := range dc.trades { 560 if (familial[trade.Base()] || familial[trade.Quote()]) && 561 trade.isActive() { 562 return true 563 } 564 565 } 566 return false 567 } 568 569 // hasActiveOrders checks whether there are any active orders for the dexConnection. 570 func (dc *dexConnection) hasActiveOrders() bool { 571 dc.tradeMtx.RLock() 572 defer dc.tradeMtx.RUnlock() 573 574 if len(dc.inFlightOrders) > 0 { 575 return true 576 } 577 578 for _, trade := range dc.trades { 579 if trade.isActive() { 580 return true 581 } 582 } 583 return false 584 } 585 586 // activeOrders returns a slice of active orders and inflight orders. 587 func (dc *dexConnection) activeOrders() ([]*Order, []*InFlightOrder) { 588 dc.tradeMtx.RLock() 589 defer dc.tradeMtx.RUnlock() 590 591 var activeOrders []*Order 592 for _, trade := range dc.trades { 593 if trade.isActive() { 594 activeOrders = append(activeOrders, trade.coreOrder()) 595 } 596 } 597 598 var inflightOrders []*InFlightOrder 599 for _, ord := range dc.inFlightOrders { 600 inflightOrders = append(inflightOrders, ord) 601 } 602 603 return activeOrders, inflightOrders 604 } 605 606 // findOrder returns the tracker and preimage for an order ID, and a boolean 607 // indicating whether this is a cancel order. 608 func (dc *dexConnection) findOrder(oid order.OrderID) (tracker *trackedTrade, isCancel bool) { 609 dc.tradeMtx.RLock() 610 defer dc.tradeMtx.RUnlock() 611 // Try to find the order as a trade. 612 if tracker, found := dc.trades[oid]; found { 613 return tracker, false 614 } 615 616 if tid, found := dc.cancelTradeID(oid); found { 617 if tracker, found := dc.trades[tid]; found { 618 return tracker, true 619 } else { 620 dc.log.Errorf("Did not find trade for cancel order ID %s", oid) 621 } 622 } 623 return 624 } 625 626 func (c *Core) sendCancelOrder(dc *dexConnection, oid order.OrderID, base, quote uint32) (order.Preimage, *order.CancelOrder, []byte, chan struct{}, error) { 627 preImg := newPreimage() 628 co := &order.CancelOrder{ 629 P: order.Prefix{ 630 AccountID: dc.acct.ID(), 631 BaseAsset: base, 632 QuoteAsset: quote, 633 OrderType: order.CancelOrderType, 634 ClientTime: time.Now(), 635 Commit: preImg.Commit(), 636 }, 637 TargetOrderID: oid, 638 } 639 err := order.ValidateOrder(co, order.OrderStatusEpoch, 0) 640 if err != nil { 641 return preImg, nil, nil, nil, err 642 } 643 644 commitSig := make(chan struct{}) 645 c.sentCommitsMtx.Lock() 646 c.sentCommits[co.Commit] = commitSig 647 c.sentCommitsMtx.Unlock() 648 649 // Create and send the order message. Check the response before using it. 650 route, msgOrder, _ := messageOrder(co, nil) 651 var result = new(msgjson.OrderResult) 652 err = dc.signAndRequest(msgOrder, route, result, DefaultResponseTimeout) 653 if err != nil { 654 // At this point there is a possibility that the server got the request 655 // and created the cancel order, but we lost the connection before 656 // receiving the response with the cancel's order ID. Any preimage 657 // request will be unrecognized. This order is ABANDONED. 658 c.sentCommitsMtx.Lock() 659 delete(c.sentCommits, co.Commit) 660 c.sentCommitsMtx.Unlock() 661 return preImg, nil, nil, nil, fmt.Errorf("failed to submit cancel order targeting trade %v: %w", oid, err) 662 } 663 err = validateOrderResponse(dc, result, co, msgOrder) 664 if err != nil { 665 c.sentCommitsMtx.Lock() 666 delete(c.sentCommits, co.Commit) 667 c.sentCommitsMtx.Unlock() 668 return preImg, nil, nil, nil, fmt.Errorf("Abandoning order. preimage: %x, server time: %d: %w", 669 preImg[:], result.ServerTime, err) 670 } 671 672 return preImg, co, result.Sig, commitSig, nil 673 } 674 675 // tryCancel will look for an order with the specified order ID, and attempt to 676 // cancel the order. It is not an error if the order is not found. 677 func (c *Core) tryCancel(dc *dexConnection, oid order.OrderID) (found bool, err error) { 678 tracker, _ := dc.findOrder(oid) 679 if tracker == nil { 680 return // false, nil 681 } 682 return true, c.tryCancelTrade(dc, tracker) 683 } 684 685 // tryCancelTrade attempts to cancel the order. 686 func (c *Core) tryCancelTrade(dc *dexConnection, tracker *trackedTrade) error { 687 oid := tracker.ID() 688 if lo, ok := tracker.Order.(*order.LimitOrder); !ok || lo.Force != order.StandingTiF { 689 return fmt.Errorf("cannot cancel %s order %s that is not a standing limit order", tracker.Type(), oid) 690 } 691 692 mktConf := dc.marketConfig(tracker.mktID) 693 if mktConf == nil { 694 return newError(marketErr, "unknown market %q", tracker.mktID) 695 } 696 697 tracker.mtx.Lock() 698 defer tracker.mtx.Unlock() 699 700 if status := tracker.metaData.Status; status != order.OrderStatusEpoch && status != order.OrderStatusBooked { 701 return fmt.Errorf("order %v not cancellable in status %v", oid, status) 702 } 703 704 if tracker.cancel != nil { 705 // Existing cancel might be stale. Deleting it now allows this 706 // cancel attempt to proceed. 707 tracker.deleteStaleCancelOrder() 708 709 if tracker.cancel != nil { 710 return fmt.Errorf("order %s - only one cancel order can be submitted per order per epoch. "+ 711 "still waiting on cancel order %s to match", oid, tracker.cancel.ID()) 712 } 713 } 714 715 // Construct and send the order. 716 preImg, co, sig, commitSig, err := c.sendCancelOrder(dc, oid, tracker.Base(), tracker.Quote()) 717 if err != nil { 718 return err 719 } 720 defer close(commitSig) 721 722 // Store the cancel order with the tracker. 723 err = tracker.cancelTrade(co, preImg, mktConf.EpochLen) 724 if err != nil { 725 return fmt.Errorf("error storing cancel order info %s: %w", co.ID(), err) 726 } 727 728 // Store the cancel order. 729 err = c.db.UpdateOrder(&db.MetaOrder{ 730 MetaData: &db.OrderMetaData{ 731 Status: order.OrderStatusEpoch, 732 Host: dc.acct.host, 733 Proof: db.OrderProof{ 734 DEXSig: sig, 735 Preimage: preImg[:], 736 }, 737 EpochDur: mktConf.EpochLen, // epochIndex := result.ServerTime / mktConf.EpochLen 738 LinkedOrder: oid, 739 }, 740 Order: co, 741 }) 742 if err != nil { 743 return fmt.Errorf("failed to store order in database: %w", err) 744 } 745 746 c.log.Infof("Cancel order %s targeting order %s at %s has been placed", 747 co.ID(), oid, dc.acct.host) 748 749 subject, details := c.formatDetails(TopicCancellingOrder, makeOrderToken(tracker.token())) 750 c.notify(newOrderNote(TopicCancellingOrder, subject, details, db.Poke, tracker.coreOrderInternal())) 751 752 return nil 753 } 754 755 // signAndRequest signs and sends the request, unmarshaling the response into 756 // the provided interface. 757 func (dc *dexConnection) signAndRequest(signable msgjson.Signable, route string, result any, timeout time.Duration) error { 758 if dc.acct.locked() { 759 return fmt.Errorf("cannot sign: %s account locked", dc.acct.host) 760 } 761 sign(dc.acct.privKey, signable) 762 return sendRequest(dc.WsConn, route, signable, result, timeout) 763 } 764 765 // ack sends an Acknowledgement for a match-related request. 766 func (dc *dexConnection) ack(msgID uint64, matchID order.MatchID, signable msgjson.Signable) (err error) { 767 ack := &msgjson.Acknowledgement{ 768 MatchID: matchID[:], 769 } 770 sigMsg := signable.Serialize() 771 ack.Sig, err = dc.acct.sign(sigMsg) 772 if err != nil { 773 return fmt.Errorf("sign error - %w", err) 774 } 775 msg, err := msgjson.NewResponse(msgID, ack, nil) 776 if err != nil { 777 return fmt.Errorf("NewResponse error - %w", err) 778 } 779 err = dc.Send(msg) 780 if err != nil { 781 return fmt.Errorf("Send error - %w", err) 782 } 783 return nil 784 } 785 786 // serverMatches are an intermediate structure used by the dexConnection to 787 // sort incoming match notifications. 788 type serverMatches struct { 789 tracker *trackedTrade 790 msgMatches []*msgjson.Match 791 cancel *msgjson.Match 792 } 793 794 // parseMatches sorts the list of matches and associates them with a trade. This 795 // may be called from handleMatchRoute on receipt of a new 'match' request, or 796 // by authDEX with the list of active matches returned by the 'connect' request. 797 func (dc *dexConnection) parseMatches(msgMatches []*msgjson.Match, checkSigs bool) (map[order.OrderID]*serverMatches, []msgjson.Acknowledgement, error) { 798 var acks []msgjson.Acknowledgement 799 matches := make(map[order.OrderID]*serverMatches) 800 var errs []string 801 for _, msgMatch := range msgMatches { 802 var oid order.OrderID 803 copy(oid[:], msgMatch.OrderID) 804 tracker, isCancel := dc.findOrder(oid) 805 if tracker == nil { 806 dc.blindCancelsMtx.Lock() 807 _, found := dc.blindCancels[oid] 808 delete(dc.blindCancels, oid) 809 dc.blindCancelsMtx.Unlock() 810 if found { // We're done. The targeted order isn't tracked, and we don't need to ack. 811 dc.log.Infof("Blind cancel order %v matched.", oid) 812 continue 813 } 814 errs = append(errs, "order "+oid.String()+" not found") 815 continue 816 } 817 818 // Check the fee rate against the maxfeerate recorded at order time. 819 swapRate := msgMatch.FeeRateQuote 820 if tracker.Trade().Sell { 821 swapRate = msgMatch.FeeRateBase 822 } 823 if !isCancel && swapRate > tracker.metaData.MaxFeeRate { 824 errs = append(errs, fmt.Sprintf("rejecting match %s for order %s because assigned rate (%d) is > MaxFeeRate (%d)", 825 msgMatch.MatchID, msgMatch.OrderID, swapRate, tracker.metaData.MaxFeeRate)) 826 continue 827 } 828 829 sigMsg := msgMatch.Serialize() 830 if checkSigs { 831 err := dc.acct.checkSig(sigMsg, msgMatch.Sig) 832 if err != nil { 833 // If the caller (e.g. handleMatchRoute) requests signature 834 // verification, this is fatal. 835 return nil, nil, fmt.Errorf("parseMatches: match signature verification failed: %w", err) 836 } 837 } 838 sig, err := dc.acct.sign(sigMsg) 839 if err != nil { 840 errs = append(errs, err.Error()) 841 continue 842 } 843 844 // Success. Add the serverMatch and the Acknowledgement. 845 acks = append(acks, msgjson.Acknowledgement{ 846 MatchID: msgMatch.MatchID, 847 Sig: sig, 848 }) 849 850 trackerID := tracker.ID() 851 match := matches[trackerID] 852 if match == nil { 853 match = &serverMatches{ 854 tracker: tracker, 855 } 856 matches[trackerID] = match 857 } 858 if isCancel { 859 match.cancel = msgMatch // taker match 860 } else { 861 match.msgMatches = append(match.msgMatches, msgMatch) 862 } 863 864 status := order.MatchStatus(msgMatch.Status) 865 dc.log.Debugf("Registering match %v for order %v (%v) in status %v", 866 msgMatch.MatchID, oid, order.MatchSide(msgMatch.Side), status) 867 } 868 869 var err error 870 if len(errs) > 0 { 871 err = fmt.Errorf("parseMatches errors: %s", strings.Join(errs, ", ")) 872 } 873 // A non-nil error only means that at least one match failed to parse, so we 874 // must return the successful matches and acks for further processing. 875 return matches, acks, err 876 } 877 878 // matchDiscreps specifies a trackedTrades's missing and extra matches compared 879 // to the server's list of active matches as returned in the connect response. 880 type matchDiscreps struct { 881 trade *trackedTrade 882 missing []*matchTracker 883 extra []*msgjson.Match 884 } 885 886 // matchStatusConflict is a conflict between our status, and the status returned 887 // by the server in the connect response. 888 type matchStatusConflict struct { 889 trade *trackedTrade 890 matches []*matchTracker 891 } 892 893 // compareServerMatches resolves the matches reported by the server in the 894 // 'connect' response against those marked incomplete in the matchTracker map 895 // for each serverMatch. 896 // Reported matches with missing trackers are already checked by parseMatches, 897 // but we also must check for incomplete matches that the server is not 898 // reporting. 899 func (dc *dexConnection) compareServerMatches(srvMatches map[order.OrderID]*serverMatches) ( 900 exceptions map[order.OrderID]*matchDiscreps, statusConflicts map[order.OrderID]*matchStatusConflict) { 901 902 exceptions = make(map[order.OrderID]*matchDiscreps) 903 statusConflicts = make(map[order.OrderID]*matchStatusConflict) 904 905 // Identify extra matches named by the server response that we do not 906 // recognize. 907 for oid, match := range srvMatches { 908 var extra []*msgjson.Match 909 match.tracker.mtx.RLock() 910 for _, msgMatch := range match.msgMatches { 911 var matchID order.MatchID 912 copy(matchID[:], msgMatch.MatchID) 913 mt := match.tracker.matches[matchID] 914 if mt == nil { 915 extra = append(extra, msgMatch) 916 continue 917 } 918 mt.exceptionMtx.Lock() 919 mt.checkServerRevoke = false 920 mt.exceptionMtx.Unlock() 921 if mt.Status != order.MatchStatus(msgMatch.Status) { 922 conflict := statusConflicts[oid] 923 if conflict == nil { 924 conflict = &matchStatusConflict{trade: match.tracker} 925 statusConflicts[oid] = conflict 926 } 927 conflict.matches = append(conflict.matches, mt) 928 } 929 } 930 match.tracker.mtx.RUnlock() 931 if len(extra) > 0 { 932 exceptions[match.tracker.ID()] = &matchDiscreps{ 933 trade: match.tracker, 934 extra: extra, 935 } 936 } 937 } 938 939 in := func(matches []*msgjson.Match, mid []byte) bool { 940 for _, m := range matches { 941 if bytes.Equal(m.MatchID, mid) { 942 return true 943 } 944 } 945 return false 946 } 947 948 setMissing := func(trade *trackedTrade, missing []*matchTracker) { 949 if tt, found := exceptions[trade.ID()]; found { 950 tt.missing = missing 951 } else { 952 exceptions[trade.ID()] = &matchDiscreps{ 953 trade: trade, 954 missing: missing, 955 } 956 } 957 } 958 959 // Identify active matches that are missing from server's response. 960 dc.tradeMtx.RLock() 961 defer dc.tradeMtx.RUnlock() 962 for oid, trade := range dc.trades { 963 var activeMatches []*matchTracker 964 for _, m := range trade.activeMatches() { 965 // Server is not expected to report matches that have been fully 966 // redeemed or are revoked. Only client cares about redeem confs. 967 if m.Status >= order.MatchComplete || m.MetaData.Proof.IsRevoked() { 968 continue 969 } 970 activeMatches = append(activeMatches, m) 971 } 972 if len(activeMatches) == 0 { 973 continue 974 } 975 tradeMatches, found := srvMatches[oid] 976 if !found { 977 // ALL of this trade's active matches are missing. 978 setMissing(trade, activeMatches) 979 continue // check next local trade 980 } 981 // Check this local trade's active matches against server's reported 982 // matches for this trade. 983 var missing []*matchTracker 984 for _, match := range activeMatches { // each local match 985 if !in(tradeMatches.msgMatches, match.MatchID[:]) { // against reported matches 986 missing = append(missing, match) 987 } 988 } 989 if len(missing) > 0 { 990 setMissing(trade, missing) 991 } 992 } 993 994 return 995 } 996 997 // updateOrderStatus updates the order's status, cleaning up any associated 998 // cancel orders, unlocking funding coins and refund/redemption reserves, and 999 // updating the order in the DB. The trackedTrade's mutex must be write locked. 1000 func (dc *dexConnection) updateOrderStatus(trade *trackedTrade, newStatus order.OrderStatus) { 1001 oid := trade.ID() 1002 previousStatus := trade.metaData.Status 1003 if previousStatus == newStatus { // may be expected if no srvOrderStatuses provided 1004 return 1005 } 1006 trade.metaData.Status = newStatus 1007 // If there is an associated cancel order, and we are revising the 1008 // status of the targeted order to anything other than canceled, we can 1009 // infer the cancel order is done. Update the status of the cancel order 1010 // and unlink it from the trade. If the targeted order is reported as 1011 // canceled, that indicates we submitted a cancel order preimage but 1012 // missed the match notification, so the cancel order is executed. 1013 if newStatus != order.OrderStatusCanceled { 1014 trade.deleteCancelOrder() 1015 } else if trade.cancel != nil { 1016 cid := trade.cancel.ID() 1017 err := trade.db.UpdateOrderStatus(cid, order.OrderStatusExecuted) 1018 if err != nil { 1019 dc.log.Errorf("Failed to update status of executed cancel order %v: %v", cid, err) 1020 } 1021 } 1022 // If we're updating an order from an active state to executed, 1023 // canceled, or revoked, and there are no active matches, return the 1024 // locked funding coins and any refund/redeem reserves. 1025 trade.maybeReturnCoins() 1026 if newStatus >= order.OrderStatusExecuted && trade.Trade().Remaining() > 0 && 1027 (!trade.isMarketBuy() || len(trade.matches) == 0) { 1028 if trade.isMarketBuy() { 1029 trade.unlockRedemptionFraction(1, 1) 1030 trade.unlockRefundFraction(1, 1) 1031 } else { 1032 trade.unlockRedemptionFraction(trade.Trade().Remaining(), trade.Trade().Quantity) 1033 trade.unlockRefundFraction(trade.Trade().Remaining(), trade.Trade().Quantity) 1034 } 1035 } 1036 // Now update the trade. 1037 if err := trade.db.UpdateOrder(trade.metaOrder()); err != nil { 1038 dc.log.Errorf("Error updating status in db for order %v from %v to %v", oid, previousStatus, newStatus) 1039 } else { 1040 dc.log.Warnf("Order %v updated from recorded status %q to new status %q reported by DEX %s", 1041 oid, previousStatus, newStatus, dc.acct.host) 1042 } 1043 1044 subject, details := trade.formatDetails(TopicOrderStatusUpdate, makeOrderToken(trade.token()), previousStatus, newStatus) 1045 dc.notify(newOrderNote(TopicOrderStatusUpdate, subject, details, db.WarningLevel, trade.coreOrderInternal())) 1046 } 1047 1048 // syncOrderStatuses requests and updates the status for each of the trades. 1049 func (dc *dexConnection) syncOrderStatuses(orders []*trackedTrade) (reconciledOrdersCount int) { 1050 orderStatusRequests := make([]*msgjson.OrderStatusRequest, len(orders)) 1051 tradeMap := make(map[order.OrderID]*trackedTrade, len(orders)) 1052 for i, trade := range orders { 1053 oid := trade.ID() 1054 tradeMap[oid] = trade 1055 orderStatusRequests[i] = &msgjson.OrderStatusRequest{ 1056 Base: trade.Base(), 1057 Quote: trade.Quote(), 1058 OrderID: oid.Bytes(), 1059 } 1060 } 1061 1062 dc.log.Debugf("Requesting statuses for %d orders from DEX %s", len(orderStatusRequests), dc.acct.host) 1063 1064 // Send the 'order_status' request. 1065 var orderStatusResults []*msgjson.OrderStatus 1066 err := sendRequest(dc.WsConn, msgjson.OrderStatusRoute, orderStatusRequests, 1067 &orderStatusResults, DefaultResponseTimeout) 1068 if err != nil { 1069 dc.log.Errorf("Error retrieving order statuses from DEX %s: %v", dc.acct.host, err) 1070 return 1071 } 1072 1073 if len(orderStatusResults) != len(orderStatusRequests) { 1074 dc.log.Errorf("Retrieved statuses for %d out of %d orders from order_status route", 1075 len(orderStatusResults), len(orderStatusRequests)) 1076 } 1077 1078 // Update the orders with the statuses received. 1079 for _, srvOrderStatus := range orderStatusResults { 1080 var oid order.OrderID 1081 copy(oid[:], srvOrderStatus.ID) 1082 trade := tradeMap[oid] // no need to lock dc.tradeMtx 1083 if trade == nil { 1084 dc.log.Warnf("Server reported status for order %v that we did not request.", oid) 1085 continue 1086 } 1087 reconciledOrdersCount++ 1088 trade.mtx.Lock() 1089 dc.updateOrderStatus(trade, order.OrderStatus(srvOrderStatus.Status)) 1090 trade.mtx.Unlock() 1091 } 1092 1093 // Treat orders with no status reported as revoked. 1094 reqsLoop: 1095 for _, req := range orderStatusRequests { 1096 for _, res := range orderStatusResults { 1097 if req.OrderID.Equal(res.ID) { 1098 continue reqsLoop 1099 } 1100 } 1101 // No result for this order. 1102 dc.log.Warnf("Server did not report status for order %v", req.OrderID) 1103 var oid order.OrderID 1104 copy(oid[:], req.OrderID) 1105 trade := tradeMap[oid] 1106 reconciledOrdersCount++ 1107 trade.mtx.Lock() 1108 dc.updateOrderStatus(trade, order.OrderStatusRevoked) 1109 trade.mtx.Unlock() 1110 } 1111 1112 return 1113 } 1114 1115 // reconcileTrades compares the statuses of orders in the dc.trades map to the 1116 // statuses returned by the server on `connect`, updating the statuses of the 1117 // tracked trades where applicable e.g. 1118 // - Booked orders that were tracked as Epoch are updated to status Booked. 1119 // - Orders thought to be active in the dc.trades map but not returned by the 1120 // server are updated to Executed, Canceled or Revoked. 1121 // 1122 // Setting the order status appropriately now, especially for inactive orders, 1123 // ensures that... 1124 // - the affected trades can be retired once the trade ticker (in core.listen) 1125 // observes that there are no active matches for the trades. 1126 // - coins are unlocked either as the affected trades' matches are swapped or 1127 // revoked (for trades with active matches), or when the trades are retired. 1128 // 1129 // Also purges "stale" cancel orders if the targeted order is returned in the 1130 // server's `connect` response. See *trackedTrade.deleteStaleCancelOrder for 1131 // the definition of a stale cancel order. 1132 func (dc *dexConnection) reconcileTrades(srvOrderStatuses []*msgjson.OrderStatus) (unknownOrders []order.OrderID, reconciledOrdersCount int) { 1133 dc.tradeMtx.RLock() 1134 // Check for unknown orders reported as active by the server. If such 1135 // exists, could be that they were known to the client but were thought 1136 // to be inactive and thus were not loaded from db or were retired. 1137 srvActiveOrderStatuses := make(map[order.OrderID]*msgjson.OrderStatus, len(srvOrderStatuses)) 1138 for _, srvOrderStatus := range srvOrderStatuses { 1139 var oid order.OrderID 1140 copy(oid[:], srvOrderStatus.ID) 1141 if _, tracked := dc.trades[oid]; tracked { 1142 srvActiveOrderStatuses[oid] = srvOrderStatus 1143 } else { 1144 dc.log.Warnf("Unknown order %v reported by DEX %s as active", oid, dc.acct.host) 1145 unknownOrders = append(unknownOrders, oid) 1146 } 1147 } 1148 knownActiveTrades := make(map[order.OrderID]*trackedTrade) 1149 for oid, trade := range dc.trades { 1150 status := trade.status() 1151 if status == order.OrderStatusEpoch || status == order.OrderStatusBooked { 1152 knownActiveTrades[oid] = trade 1153 } else if srvOrderStatus := srvActiveOrderStatuses[oid]; srvOrderStatus != nil { 1154 // Lock redemption funds? 1155 dc.log.Warnf("Inactive order %v, status %q reported by DEX %s as active, status %q", 1156 oid, status, dc.acct.host, order.OrderStatus(srvOrderStatus.Status)) 1157 } 1158 } 1159 dc.tradeMtx.RUnlock() 1160 1161 // Compare the status reported by the server for each known active trade. 1162 // Orders for which the server did not return a status are no longer active 1163 // (now Executed, Canceled or Revoked). Use the order_status route to 1164 // determine the correct status for such orders and update accordingly. 1165 var mysteryOrders []*trackedTrade 1166 for oid, trade := range knownActiveTrades { 1167 srvOrderStatus := srvActiveOrderStatuses[oid] 1168 if srvOrderStatus == nil { 1169 // Order status not returned by server. Must be inactive now. 1170 // Request current status from the DEX. 1171 mysteryOrders = append(mysteryOrders, trade) 1172 continue 1173 } 1174 1175 trade.mtx.Lock() 1176 1177 // Server reports this order as active. Delete any associated cancel 1178 // order if the cancel order's epoch has passed. 1179 trade.deleteStaleCancelOrder() // could be too soon, so we'll have to check in tick too 1180 1181 ourStatus := trade.metaData.Status 1182 serverStatus := order.OrderStatus(srvOrderStatus.Status) 1183 if ourStatus == serverStatus { 1184 dc.log.Tracef("Status reconciliation not required for order %v, status %q, server-reported status %q", 1185 oid, ourStatus, serverStatus) 1186 } else if ourStatus == order.OrderStatusEpoch && serverStatus == order.OrderStatusBooked { 1187 // Only standing orders can move from Epoch to Booked. This must have 1188 // happened in the client's absence (maybe a missed nomatch message). 1189 if lo, ok := trade.Order.(*order.LimitOrder); ok && lo.Force == order.StandingTiF { 1190 reconciledOrdersCount++ 1191 dc.updateOrderStatus(trade, serverStatus) 1192 } else { 1193 dc.log.Warnf("Incorrect status %q reported for non-standing order %v by DEX %s, client status = %q", 1194 serverStatus, oid, dc.acct.host, ourStatus) 1195 } 1196 } else { 1197 dc.log.Warnf("Inconsistent status %q reported for order %v by DEX %s, client status = %q", 1198 serverStatus, oid, dc.acct.host, ourStatus) 1199 } 1200 1201 trade.mtx.Unlock() 1202 } 1203 1204 if len(mysteryOrders) > 0 { 1205 reconciledOrdersCount += dc.syncOrderStatuses(mysteryOrders) 1206 } 1207 1208 return 1209 } 1210 1211 // tickAsset checks open matches related to a specific asset for needed action. 1212 func (c *Core) tickAsset(dc *dexConnection, assetID uint32) assetMap { 1213 dc.tradeMtx.RLock() 1214 assetTrades := make([]*trackedTrade, 0, len(dc.trades)) 1215 for _, trade := range dc.trades { 1216 if trade.Base() == assetID || trade.Quote() == assetID { 1217 assetTrades = append(assetTrades, trade) 1218 } 1219 } 1220 dc.tradeMtx.RUnlock() 1221 1222 updated := make(assetMap) 1223 updateChan := make(chan assetMap) 1224 for _, trade := range assetTrades { 1225 if c.ctx.Err() != nil { // don't fail each one in sequence if shutting down 1226 return updated 1227 } 1228 trade := trade // bad go, bad 1229 go func() { 1230 newUpdates, err := c.tick(trade) 1231 if err != nil { 1232 c.log.Errorf("%s tick error: %v", dc.acct.host, err) 1233 } 1234 updateChan <- newUpdates 1235 }() 1236 } 1237 1238 for range assetTrades { 1239 updated.merge(<-updateChan) 1240 } 1241 return updated 1242 } 1243 1244 // Get the *dexConnection and connection status for the the host. 1245 func (c *Core) dex(addr string) (*dexConnection, bool, error) { 1246 host, err := addrHost(addr) 1247 if err != nil { 1248 return nil, false, newError(addressParseErr, "error parsing address: %w", err) 1249 } 1250 1251 // Get the dexConnection and the dex.Asset for each asset. 1252 c.connMtx.RLock() 1253 dc, found := c.conns[host] 1254 c.connMtx.RUnlock() 1255 if !found { 1256 return nil, false, fmt.Errorf("unknown DEX %s", addr) 1257 } 1258 return dc, dc.status() == comms.Connected, nil 1259 } 1260 1261 // addDexConnection is a helper used to add a dex connection. 1262 func (c *Core) addDexConnection(dc *dexConnection) { 1263 if dc == nil { 1264 return 1265 } 1266 c.connMtx.Lock() 1267 c.conns[dc.acct.host] = dc 1268 c.connMtx.Unlock() 1269 } 1270 1271 // Get the *dexConnection for the host. Return an error if the DEX is not 1272 // registered, connected, and unlocked. 1273 func (c *Core) registeredDEX(addr string) (*dexConnection, error) { 1274 dc, connected, err := c.dex(addr) 1275 if err != nil { 1276 return nil, err 1277 } 1278 1279 if dc.acct.isViewOnly() { 1280 return nil, fmt.Errorf("not yet registered at %s", dc.acct.host) 1281 } 1282 1283 if dc.acct.locked() { 1284 return nil, fmt.Errorf("account for %s is locked. Are you logged in?", dc.acct.host) 1285 } 1286 1287 if !connected { 1288 return nil, fmt.Errorf("currently disconnected from %s", dc.acct.host) 1289 } 1290 return dc, nil 1291 } 1292 1293 // setEpoch sets the epoch. If the passed epoch is greater than the highest 1294 // previously passed epoch, an epoch notification is sent to all subscribers and 1295 // true is returned. 1296 func (dc *dexConnection) setEpoch(mktID string, epochIdx uint64) bool { 1297 dc.epochMtx.Lock() 1298 defer dc.epochMtx.Unlock() 1299 if epochIdx > dc.epoch[mktID] { 1300 dc.epoch[mktID] = epochIdx 1301 dc.notify(newEpochNotification(dc.acct.host, mktID, epochIdx)) 1302 return true 1303 } 1304 return false 1305 } 1306 1307 // marketEpochDuration gets the market's epoch duration. If the market is not 1308 // known, an error is logged and 0 is returned. 1309 func (dc *dexConnection) marketEpochDuration(mktID string) uint64 { 1310 mkt := dc.marketConfig(mktID) 1311 if mkt == nil { 1312 return 0 1313 } 1314 return mkt.EpochLen 1315 } 1316 1317 // marketEpoch gets the epoch index for the specified market and time stamp. If 1318 // the market is not known, 0 is returned. 1319 func (dc *dexConnection) marketEpoch(mktID string, stamp time.Time) uint64 { 1320 epochLen := dc.marketEpochDuration(mktID) 1321 if epochLen == 0 { 1322 return 0 1323 } 1324 return uint64(stamp.UnixMilli()) / epochLen 1325 } 1326 1327 // fetchFeeRate gets an asset's fee rate estimate from the server. 1328 func (dc *dexConnection) fetchFeeRate(assetID uint32) (rate uint64) { 1329 msg, err := msgjson.NewRequest(dc.NextID(), msgjson.FeeRateRoute, assetID) 1330 if err != nil { 1331 dc.log.Errorf("Error fetching fee rate for %s: %v", unbip(assetID), err) 1332 return 1333 } 1334 errChan := make(chan error, 1) 1335 err = dc.RequestWithTimeout(msg, func(msg *msgjson.Message) { 1336 errChan <- msg.UnmarshalResult(&rate) 1337 }, DefaultResponseTimeout, func() { 1338 errChan <- fmt.Errorf("timed out waiting for fee_rate response") 1339 }) 1340 if err == nil { 1341 err = <-errChan 1342 } 1343 if err != nil { 1344 dc.log.Errorf("Error fetching fee rate for %s: %v", unbip(assetID), err) 1345 return 1346 } 1347 return 1348 } 1349 1350 // bestBookFeeSuggestion attempts to find a fee rate for the specified asset in 1351 // any synced book. 1352 func (dc *dexConnection) bestBookFeeSuggestion(assetID uint32) uint64 { 1353 dc.booksMtx.RLock() 1354 defer dc.booksMtx.RUnlock() 1355 for _, book := range dc.books { 1356 var feeRate uint64 1357 switch assetID { 1358 case book.base: 1359 feeRate = book.BaseFeeRate() 1360 case book.quote: 1361 feeRate = book.QuoteFeeRate() 1362 } 1363 if feeRate > 0 { 1364 return feeRate 1365 } 1366 } 1367 return 0 1368 } 1369 1370 type pokesCache struct { 1371 sync.RWMutex 1372 cache []*db.Notification 1373 cursor int 1374 pokesCapacity int 1375 } 1376 1377 func newPokesCache(pokesCapacity int) *pokesCache { 1378 return &pokesCache{ 1379 pokesCapacity: pokesCapacity, 1380 } 1381 } 1382 1383 func (c *pokesCache) add(poke *db.Notification) { 1384 c.Lock() 1385 defer c.Unlock() 1386 1387 if len(c.cache) >= c.pokesCapacity { 1388 c.cache[c.cursor] = poke 1389 } else { 1390 c.cache = append(c.cache, poke) 1391 } 1392 c.cursor = (c.cursor + 1) % c.pokesCapacity 1393 } 1394 1395 func (c *pokesCache) pokes() []*db.Notification { 1396 c.RLock() 1397 defer c.RUnlock() 1398 1399 pokes := make([]*db.Notification, len(c.cache)) 1400 copy(pokes, c.cache) 1401 sort.Slice(pokes, func(i, j int) bool { 1402 return pokes[i].TimeStamp < pokes[j].TimeStamp 1403 }) 1404 return pokes 1405 } 1406 1407 func (c *pokesCache) init(pokes []*db.Notification) { 1408 c.Lock() 1409 defer c.Unlock() 1410 1411 if len(pokes) > c.pokesCapacity { 1412 pokes = pokes[:len(pokes)-c.pokesCapacity] 1413 } 1414 c.cache = pokes 1415 c.cursor = len(pokes) % c.pokesCapacity 1416 } 1417 1418 // blockWaiter is a message waiting to be stamped, signed, and sent once a 1419 // specified coin has the requisite confirmations. The blockWaiter is similar to 1420 // dcrdex/server/blockWaiter.Waiter, but is different enough to warrant a 1421 // separate type. 1422 type blockWaiter struct { 1423 assetID uint32 1424 trigger func() (bool, error) 1425 action func(error) 1426 } 1427 1428 // Config is the configuration for the Core. 1429 type Config struct { 1430 // DBPath is a filepath to use for the client database. If the database does 1431 // not already exist, it will be created. 1432 DBPath string 1433 // Net is the current network. 1434 Net dex.Network 1435 // Logger is the Core's logger and is also used to create the sub-loggers 1436 // for the asset backends. 1437 Logger dex.Logger 1438 // Onion is the address (host:port) of a Tor proxy for use with DEX hosts 1439 // with a .onion address. To use Tor with regular DEX addresses as well, set 1440 // TorProxy. 1441 Onion string 1442 // TorProxy specifies the address of a Tor proxy server. 1443 TorProxy string 1444 // TorIsolation specifies whether to enable Tor circuit isolation. 1445 TorIsolation bool 1446 // Language. A BCP 47 language tag. Default is en-US. 1447 Language string 1448 1449 // NoAutoWalletLock instructs Core to skip locking the wallet on shutdown or 1450 // logout. This can be helpful if the user wants the wallet to remain 1451 // unlocked. e.g. They started with the wallet unlocked, or they intend to 1452 // start Core again and wish to avoid the time to unlock a locked wallet on 1453 // startup. 1454 NoAutoWalletLock bool // zero value is legacy behavior 1455 // NoAutoDBBackup instructs the DB to skip the creation of a backup DB file 1456 // on shutdown. This is useful if the consumer is using the BackupDB method, 1457 // or simply creating manual backups of the DB file after shutdown. 1458 NoAutoDBBackup bool // zero value is legacy behavior 1459 // UnlockCoinsOnLogin indicates that on wallet connect during login, or on 1460 // creation of a new wallet, all coins with the wallet should be unlocked. 1461 UnlockCoinsOnLogin bool 1462 // ExtensionModeFile is the path to a file that specifies configuration 1463 // for running core in extension mode, which gives the caller options for 1464 // e.g. limiting the ability to configure wallets. 1465 ExtensionModeFile string 1466 // TheOneHost will run core with only the specified server. 1467 TheOneHost string 1468 // PruneArchive will prune the order archive to the specified number of 1469 // orders. 1470 PruneArchive uint64 1471 } 1472 1473 // locale is data associated with the currently selected language. 1474 type locale struct { 1475 lang language.Tag 1476 m map[Topic]*translation 1477 printer *message.Printer 1478 } 1479 1480 // Core is the core client application. Core manages DEX connections, wallets, 1481 // database access, match negotiation and more. 1482 type Core struct { 1483 ctx context.Context 1484 wg sync.WaitGroup 1485 ready chan struct{} 1486 rotate chan struct{} 1487 cfg *Config 1488 log dex.Logger 1489 db db.DB 1490 net dex.Network 1491 lockTimeTaker time.Duration 1492 lockTimeMaker time.Duration 1493 intl atomic.Value // *locale 1494 1495 extensionModeConfig *ExtensionModeConfig 1496 1497 // construction or init sets credentials 1498 credMtx sync.RWMutex 1499 credentials *db.PrimaryCredentials 1500 1501 loginMtx sync.Mutex 1502 loggedIn bool 1503 bondXPriv *hdkeychain.ExtendedKey // derived from creds.EncSeed on login 1504 1505 seedGenerationTime uint64 1506 1507 wsConstructor func(*comms.WsCfg) (comms.WsConn, error) 1508 newCrypter func([]byte) encrypt.Crypter 1509 reCrypter func([]byte, []byte) (encrypt.Crypter, error) 1510 latencyQ *wait.TickerQueue 1511 1512 connMtx sync.RWMutex 1513 conns map[string]*dexConnection 1514 1515 walletMtx sync.RWMutex 1516 wallets map[uint32]*xcWallet 1517 1518 waiterMtx sync.RWMutex 1519 blockWaiters map[string]*blockWaiter 1520 1521 tickSchedMtx sync.Mutex 1522 tickSched map[order.OrderID]*time.Timer 1523 1524 noteMtx sync.RWMutex 1525 noteChans map[uint64]chan Notification 1526 1527 sentCommitsMtx sync.Mutex 1528 sentCommits map[order.Commitment]chan struct{} 1529 1530 ratesMtx sync.RWMutex 1531 fiatRateSources map[string]*commonRateSource 1532 1533 reFiat chan struct{} 1534 1535 pendingWalletsMtx sync.RWMutex 1536 pendingWallets map[uint32]bool 1537 1538 notes chan asset.WalletNotification 1539 1540 pokesCache *pokesCache 1541 1542 requestedActionMtx sync.RWMutex 1543 requestedActions map[string]*asset.ActionRequiredNote 1544 } 1545 1546 // New is the constructor for a new Core. 1547 func New(cfg *Config) (*Core, error) { 1548 if cfg.Logger == nil { 1549 return nil, fmt.Errorf("Core.Config must specify a Logger") 1550 } 1551 dbOpts := bolt.Opts{ 1552 BackupOnShutdown: !cfg.NoAutoDBBackup, 1553 PruneArchive: cfg.PruneArchive, 1554 } 1555 boltDB, err := bolt.NewDB(cfg.DBPath, cfg.Logger.SubLogger("DB"), dbOpts) 1556 if err != nil { 1557 return nil, fmt.Errorf("database initialization error: %w", err) 1558 } 1559 if cfg.TorProxy != "" { 1560 if _, _, err = net.SplitHostPort(cfg.TorProxy); err != nil { 1561 return nil, err 1562 } 1563 } 1564 if cfg.Onion != "" { 1565 if _, _, err = net.SplitHostPort(cfg.Onion); err != nil { 1566 return nil, err 1567 } 1568 } else { // default to torproxy if onion not set explicitly 1569 cfg.Onion = cfg.TorProxy 1570 } 1571 1572 parseLanguage := func(langStr string) (language.Tag, error) { 1573 acceptLang, err := language.Parse(langStr) 1574 if err != nil { 1575 return language.Und, fmt.Errorf("unable to parse requested language: %w", err) 1576 } 1577 var langs []language.Tag 1578 for locale := range locales { 1579 tag, err := language.Parse(locale) 1580 if err != nil { 1581 return language.Und, fmt.Errorf("bad %v: %w", locale, err) 1582 } 1583 langs = append(langs, tag) 1584 } 1585 matcher := language.NewMatcher(langs) 1586 _, idx, conf := matcher.Match(acceptLang) // use index because tag may end up as something hyper specific like zh-Hans-u-rg-cnzzzz 1587 tag := langs[idx] 1588 switch conf { 1589 case language.Exact: 1590 case language.High, language.Low: 1591 cfg.Logger.Infof("Using language %v", tag) 1592 case language.No: 1593 return language.Und, fmt.Errorf("no match for %q in recognized languages %v", cfg.Language, langs) 1594 } 1595 return tag, nil 1596 } 1597 1598 lang := language.Und 1599 1600 // Check if the user has set a language with SetLanguage. 1601 if langStr, err := boltDB.Language(); err != nil { 1602 cfg.Logger.Errorf("Error loading language from database: %v", err) 1603 } else if len(langStr) > 0 { 1604 if lang, err = parseLanguage(langStr); err != nil { 1605 cfg.Logger.Errorf("Error parsing language retrieved from database %q: %w", langStr, err) 1606 } 1607 } 1608 1609 // If they haven't changed the language through the UI, perhaps its set in 1610 // configuration. 1611 if lang.IsRoot() && cfg.Language != "" { 1612 if lang, err = parseLanguage(cfg.Language); err != nil { 1613 return nil, err 1614 } 1615 } 1616 1617 // Default language is English. 1618 if lang.IsRoot() { 1619 lang = language.AmericanEnglish 1620 } 1621 1622 cfg.Logger.Debugf("Using locale printer for %q", lang) 1623 1624 translations, found := locales[lang.String()] 1625 if !found { 1626 return nil, fmt.Errorf("no translations for language %s", lang) 1627 } 1628 1629 // Try to get the primary credentials, but ignore no-credentials error here 1630 // because the client may not be initialized. 1631 creds, err := boltDB.PrimaryCredentials() 1632 if err != nil && !errors.Is(err, db.ErrNoCredentials) { 1633 return nil, err 1634 } 1635 1636 seedGenerationTime, err := boltDB.SeedGenerationTime() 1637 if err != nil && !errors.Is(err, db.ErrNoSeedGenTime) { 1638 return nil, err 1639 } 1640 1641 var xCfg *ExtensionModeConfig 1642 if cfg.ExtensionModeFile != "" { 1643 b, err := os.ReadFile(cfg.ExtensionModeFile) 1644 if err != nil { 1645 return nil, fmt.Errorf("error reading extension mode file at %q: %w", cfg.ExtensionModeFile, err) 1646 } 1647 if err := json.Unmarshal(b, &xCfg); err != nil { 1648 return nil, fmt.Errorf("error unmarshalling extension mode file: %w", err) 1649 } 1650 } 1651 1652 c := &Core{ 1653 cfg: cfg, 1654 credentials: creds, 1655 ready: make(chan struct{}), 1656 rotate: make(chan struct{}, 1), 1657 log: cfg.Logger, 1658 db: boltDB, 1659 conns: make(map[string]*dexConnection), 1660 wallets: make(map[uint32]*xcWallet), 1661 net: cfg.Net, 1662 lockTimeTaker: dex.LockTimeTaker(cfg.Net), 1663 lockTimeMaker: dex.LockTimeMaker(cfg.Net), 1664 blockWaiters: make(map[string]*blockWaiter), 1665 sentCommits: make(map[order.Commitment]chan struct{}), 1666 tickSched: make(map[order.OrderID]*time.Timer), 1667 // Allowing to change the constructor makes testing a lot easier. 1668 wsConstructor: comms.NewWsConn, 1669 newCrypter: encrypt.NewCrypter, 1670 reCrypter: encrypt.Deserialize, 1671 latencyQ: wait.NewTickerQueue(recheckInterval), 1672 noteChans: make(map[uint64]chan Notification), 1673 1674 extensionModeConfig: xCfg, 1675 seedGenerationTime: seedGenerationTime, 1676 1677 fiatRateSources: make(map[string]*commonRateSource), 1678 reFiat: make(chan struct{}, 1), 1679 pendingWallets: make(map[uint32]bool), 1680 1681 notes: make(chan asset.WalletNotification, 128), 1682 requestedActions: make(map[string]*asset.ActionRequiredNote), 1683 } 1684 1685 c.intl.Store(&locale{ 1686 lang: lang, 1687 m: translations, 1688 printer: message.NewPrinter(lang), 1689 }) 1690 1691 // Populate the initial user data. User won't include any DEX info yet, as 1692 // those are retrieved when Run is called and the core connects to the DEXes. 1693 c.log.Debugf("new client core created") 1694 return c, nil 1695 } 1696 1697 // Run runs the core. Satisfies the runner.Runner interface. 1698 func (c *Core) Run(ctx context.Context) { 1699 c.log.Infof("Starting Bison Wallet core") 1700 // Store the context as a field, since we will need to spawn new DEX threads 1701 // when new accounts are registered. 1702 c.ctx = ctx 1703 if err := c.initialize(); err != nil { // connectDEX gets ctx for the wsConn 1704 c.log.Critical(err) 1705 close(c.ready) // unblock <-Ready() 1706 return 1707 } 1708 close(c.ready) 1709 1710 // The DB starts first and stops last. 1711 ctxDB, stopDB := context.WithCancel(context.Background()) 1712 var dbWG sync.WaitGroup 1713 dbWG.Add(1) 1714 go func() { 1715 defer dbWG.Done() 1716 c.db.Run(ctxDB) 1717 }() 1718 1719 c.wg.Add(1) 1720 go func() { 1721 defer c.wg.Done() 1722 c.latencyQ.Run(ctx) 1723 }() 1724 1725 // Retrieve disabled fiat rate sources from database. 1726 disabledSources, err := c.db.DisabledRateSources() 1727 if err != nil { 1728 c.log.Errorf("Unable to retrieve disabled fiat rate source: %v", err) 1729 } 1730 1731 // Construct enabled fiat rate sources. 1732 fetchers: 1733 for token, rateFetcher := range fiatRateFetchers { 1734 for _, v := range disabledSources { 1735 if token == v { 1736 continue fetchers 1737 } 1738 } 1739 c.fiatRateSources[token] = newCommonRateSource(rateFetcher) 1740 } 1741 c.fetchFiatExchangeRates(ctx) 1742 1743 // Start a goroutine to keep the FeeState updated. 1744 c.wg.Add(1) 1745 go func() { 1746 defer c.wg.Done() 1747 for { 1748 tick := time.NewTicker(time.Minute * 5) 1749 select { 1750 case <-tick.C: 1751 for _, w := range c.xcWallets() { 1752 if w.connected() { 1753 w.feeRate() // updates the fee state internally. 1754 } 1755 } 1756 case <-ctx.Done(): 1757 return 1758 } 1759 } 1760 }() 1761 1762 // Start bond supervisor. 1763 c.wg.Add(1) 1764 go func() { 1765 defer c.wg.Done() 1766 c.watchBonds(ctx) 1767 }() 1768 1769 // Handle wallet notifications. 1770 c.wg.Add(1) 1771 go func() { 1772 defer c.wg.Done() 1773 for { 1774 select { 1775 case n := <-c.notes: 1776 c.handleWalletNotification(n) 1777 case <-ctx.Done(): 1778 return 1779 } 1780 } 1781 }() 1782 1783 c.wg.Wait() // block here until all goroutines except DB complete 1784 1785 if err := c.db.SavePokes(c.pokes()); err != nil { 1786 c.log.Errorf("Error saving pokes: %v", err) 1787 } 1788 1789 // Stop the DB after dexConnections and other goroutines are done. 1790 stopDB() 1791 dbWG.Wait() 1792 1793 // At this point, it should be safe to access the data structures without 1794 // mutex protection. Goroutines have returned, and consumers should not call 1795 // Core methods after shutdown. We'll play it safe anyway. 1796 1797 // Clear account private keys and wait for the DEX ws connections that began 1798 // shutting down on context cancellation (the listen goroutines have already 1799 // returned however). Warn about specific active orders, and unlock any 1800 // locked coins for inactive orders that are not yet retired. 1801 for _, dc := range c.dexConnections() { 1802 // context is already canceled, allowing just a Wait(), but just in case 1803 // use Disconnect otherwise it could hang forever. 1804 dc.connMaster.Disconnect() 1805 dc.acct.lock() 1806 1807 // Note active orders, and unlock any coins locked by inactive orders. 1808 dc.tradeMtx.Lock() 1809 for _, trade := range dc.trades { 1810 oid := trade.ID() 1811 if trade.isActive() { 1812 c.log.Warnf("Shutting down with active order %v in status %v.", oid, trade.metaData.Status) 1813 continue 1814 } 1815 c.log.Debugf("Retiring inactive order %v. Unlocking coins = %v", 1816 oid, trade.coinsLocked || trade.changeLocked) 1817 delete(dc.trades, oid) // for inspection/debugging 1818 trade.returnCoins() 1819 // Do not bother with OrderNote/SubjectOrderRetired and BalanceNote 1820 // notes since any web/rpc servers should be down by now. Go 1821 // consumers can check orders on restart. 1822 } 1823 dc.tradeMtx.Unlock() 1824 } 1825 1826 // Lock and disconnect the wallets. 1827 c.walletMtx.Lock() 1828 defer c.walletMtx.Unlock() 1829 for assetID, wallet := range c.wallets { 1830 delete(c.wallets, assetID) 1831 if !wallet.connected() { 1832 continue 1833 } 1834 if !c.cfg.NoAutoWalletLock && wallet.unlocked() { // no-op if Logout did it 1835 symb := strings.ToUpper(unbip(assetID)) 1836 c.log.Infof("Locking %s wallet", symb) 1837 if err := wallet.Lock(walletLockTimeout); err != nil { 1838 c.log.Errorf("Failed to lock %v wallet: %v", symb, err) 1839 } 1840 } 1841 wallet.Disconnect() 1842 } 1843 1844 c.log.Infof("Bison Wallet core off") 1845 } 1846 1847 // Ready returns a channel that is closed when Run completes its initialization 1848 // tasks and Core becomes ready for use. 1849 func (c *Core) Ready() <-chan struct{} { 1850 return c.ready 1851 } 1852 1853 func (c *Core) locale() *locale { 1854 return c.intl.Load().(*locale) 1855 } 1856 1857 // SetLanguage sets the langauge used for notifications. The language set with 1858 // SetLanguage persists through restarts and will override any language set in 1859 // configuration. 1860 func (c *Core) SetLanguage(lang string) error { 1861 tag, err := language.Parse(lang) 1862 if err != nil { 1863 return fmt.Errorf("error parsing language %q: %w", lang, err) 1864 } 1865 1866 translations, found := locales[lang] 1867 if !found { 1868 return fmt.Errorf("no translations for language %s", lang) 1869 } 1870 if err := c.db.SetLanguage(lang); err != nil { 1871 return fmt.Errorf("error storing language: %w", err) 1872 } 1873 c.intl.Store(&locale{ 1874 m: translations, 1875 printer: message.NewPrinter(tag), 1876 }) 1877 return nil 1878 } 1879 1880 // Language is the currently configured language. 1881 func (c *Core) Language() string { 1882 return c.locale().lang.String() 1883 } 1884 1885 // BackupDB makes a backup of the database at the specified location, optionally 1886 // overwriting any existing file and compacting the database. 1887 func (c *Core) BackupDB(dst string, overwrite, compact bool) error { 1888 return c.db.BackupTo(dst, overwrite, compact) 1889 } 1890 1891 const defaultDEXPort = "7232" 1892 1893 // addrHost returns the host or url:port pair for an address. 1894 func addrHost(addr string) (string, error) { 1895 addr = strings.TrimSpace(addr) 1896 const defaultHost = "localhost" 1897 const missingPort = "missing port in address" 1898 // Empty addresses are localhost. 1899 if addr == "" { 1900 return defaultHost + ":" + defaultDEXPort, nil 1901 } 1902 host, port, splitErr := net.SplitHostPort(addr) 1903 _, portErr := strconv.ParseUint(port, 10, 16) 1904 1905 // net.SplitHostPort will error on anything not in the format 1906 // string:string or :string or if a colon is in an unexpected position, 1907 // such as in the scheme. 1908 // If the port isn't a port, it must also be parsed. 1909 if splitErr != nil || portErr != nil { 1910 // Any address with no colons is appended with the default port. 1911 var addrErr *net.AddrError 1912 if errors.As(splitErr, &addrErr) && addrErr.Err == missingPort { 1913 host = strings.Trim(addrErr.Addr, "[]") // JoinHostPort expects no brackets for ipv6 hosts 1914 return net.JoinHostPort(host, defaultDEXPort), nil 1915 } 1916 // These are addresses with at least one colon in an unexpected 1917 // position. 1918 a, err := url.Parse(addr) 1919 // This address is of an unknown format. 1920 if err != nil { 1921 return "", fmt.Errorf("addrHost: unable to parse address '%s'", addr) 1922 } 1923 host, port = a.Hostname(), a.Port() 1924 // If the address parses but there is no port, append the default port. 1925 if port == "" { 1926 return net.JoinHostPort(host, defaultDEXPort), nil 1927 } 1928 } 1929 // We have a port but no host. Replace with localhost. 1930 if host == "" { 1931 host = defaultHost 1932 } 1933 return net.JoinHostPort(host, port), nil 1934 } 1935 1936 // creds returns the *PrimaryCredentials. 1937 func (c *Core) creds() *db.PrimaryCredentials { 1938 c.credMtx.RLock() 1939 defer c.credMtx.RUnlock() 1940 if c.credentials == nil { 1941 return nil 1942 } 1943 if len(c.credentials.EncInnerKey) == 0 { 1944 // database upgraded, but Core hasn't updated the PrimaryCredentials. 1945 return nil 1946 } 1947 return c.credentials 1948 } 1949 1950 // setCredentials stores the *PrimaryCredentials. 1951 func (c *Core) setCredentials(creds *db.PrimaryCredentials) { 1952 c.credMtx.Lock() 1953 c.credentials = creds 1954 c.credMtx.Unlock() 1955 } 1956 1957 // Network returns the current DEX network. 1958 func (c *Core) Network() dex.Network { 1959 return c.net 1960 } 1961 1962 // Exchanges creates a map of *Exchange keyed by host, including markets and 1963 // orders. 1964 func (c *Core) Exchanges() map[string]*Exchange { 1965 dcs := c.dexConnections() 1966 infos := make(map[string]*Exchange, len(dcs)) 1967 for _, dc := range dcs { 1968 infos[dc.acct.host] = c.exchangeInfo(dc) 1969 } 1970 return infos 1971 } 1972 1973 // Exchange returns an exchange with a certain host. It returns an error if 1974 // no exchange exists at that host. 1975 func (c *Core) Exchange(host string) (*Exchange, error) { 1976 dc, _, err := c.dex(host) 1977 if err != nil { 1978 return nil, err 1979 } 1980 return c.exchangeInfo(dc), nil 1981 } 1982 1983 // ExchangeMarket returns the market with the given base and quote assets at the 1984 // given host. It returns an error if no market exists at that host. 1985 func (c *Core) ExchangeMarket(host string, baseID, quoteID uint32) (*Market, error) { 1986 dc, _, err := c.dex(host) 1987 if err != nil { 1988 return nil, err 1989 } 1990 1991 mkt := dc.coreMarket(marketName(baseID, quoteID)) 1992 if mkt == nil { 1993 return nil, fmt.Errorf("no market found for %s-%s at %s", unbip(baseID), unbip(quoteID), host) 1994 } 1995 1996 return mkt, nil 1997 } 1998 1999 // MarketConfig gets the configuration for the market. 2000 func (c *Core) MarketConfig(host string, baseID, quoteID uint32) (*msgjson.Market, error) { 2001 dc, _, err := c.dex(host) 2002 if err != nil { 2003 return nil, err 2004 } 2005 for _, mkt := range dc.config().Markets { 2006 if mkt.Base == baseID && mkt.Quote == quoteID { 2007 return mkt, nil 2008 } 2009 } 2010 return nil, fmt.Errorf("market (%d, %d) not found for host %s", baseID, quoteID, host) 2011 } 2012 2013 // dexConnections creates a slice of the *dexConnection in c.conns. 2014 func (c *Core) dexConnections() []*dexConnection { 2015 c.connMtx.RLock() 2016 defer c.connMtx.RUnlock() 2017 conns := make([]*dexConnection, 0, len(c.conns)) 2018 for _, conn := range c.conns { 2019 conns = append(conns, conn) 2020 } 2021 return conns 2022 } 2023 2024 // wallet gets the wallet for the specified asset ID in a thread-safe way. 2025 func (c *Core) wallet(assetID uint32) (*xcWallet, bool) { 2026 c.walletMtx.RLock() 2027 defer c.walletMtx.RUnlock() 2028 w, found := c.wallets[assetID] 2029 return w, found 2030 } 2031 2032 // encryptionKey retrieves the application encryption key. The password is used 2033 // to recreate the outer key/crypter, which is then used to decode and recreate 2034 // the inner key/crypter. 2035 func (c *Core) encryptionKey(pw []byte) (encrypt.Crypter, error) { 2036 creds := c.creds() 2037 if creds == nil { 2038 return nil, fmt.Errorf("primary credentials not retrieved. Is the client initialized?") 2039 } 2040 outerCrypter, err := c.reCrypter(pw, creds.OuterKeyParams) 2041 if err != nil { 2042 return nil, fmt.Errorf("outer key deserialization error: %w", err) 2043 } 2044 defer outerCrypter.Close() 2045 innerKey, err := outerCrypter.Decrypt(creds.EncInnerKey) 2046 if err != nil { 2047 return nil, fmt.Errorf("inner key decryption error: %w", err) 2048 } 2049 innerCrypter, err := c.reCrypter(innerKey, creds.InnerKeyParams) 2050 if err != nil { 2051 return nil, fmt.Errorf("inner key deserialization error: %w", err) 2052 } 2053 return innerCrypter, nil 2054 } 2055 2056 func (c *Core) storeDepositAddress(wdbID []byte, addr string) error { 2057 // Store the new address in the DB. 2058 dbWallet, err := c.db.Wallet(wdbID) 2059 if err != nil { 2060 return fmt.Errorf("error retrieving DB wallet: %w", err) 2061 } 2062 dbWallet.Address = addr 2063 return c.db.UpdateWallet(dbWallet) 2064 } 2065 2066 // connectAndUpdateWalletResumeTrades creates a connection to a wallet and 2067 // updates the balance. If resumeTrades is set to true, an attempt to resume 2068 // any trades that were unable to be resumed at startup will be made. 2069 func (c *Core) connectAndUpdateWalletResumeTrades(w *xcWallet, resumeTrades bool) error { 2070 assetID := w.AssetID 2071 2072 token := asset.TokenInfo(assetID) 2073 if token != nil { 2074 parentWallet, found := c.wallet(token.ParentID) 2075 if !found { 2076 return fmt.Errorf("token %s wallet has no %s parent?", unbip(assetID), unbip(token.ParentID)) 2077 } 2078 if !parentWallet.connected() { 2079 if err := c.connectAndUpdateWalletResumeTrades(parentWallet, resumeTrades); err != nil { 2080 return fmt.Errorf("failed to connect %s parent wallet for %s token: %v", 2081 unbip(token.ParentID), unbip(assetID), err) 2082 } 2083 } 2084 } 2085 2086 c.log.Debugf("Connecting wallet for %s", unbip(assetID)) 2087 addr := w.currentDepositAddress() 2088 newAddr, err := c.connectWalletResumeTrades(w, resumeTrades) 2089 if err != nil { 2090 return fmt.Errorf("connectWallet: %w", err) // core.Error with code connectWalletErr 2091 } 2092 if newAddr != addr { 2093 c.log.Infof("New deposit address for %v wallet: %v", unbip(assetID), newAddr) 2094 if err = c.storeDepositAddress(w.dbID, newAddr); err != nil { 2095 return fmt.Errorf("storeDepositAddress: %w", err) 2096 } 2097 } 2098 // First update balances since it is included in WalletState. Ignore errors 2099 // because some wallets may not reveal balance until unlocked. 2100 _, err = c.updateWalletBalance(w) 2101 if err != nil { 2102 // Warn because the balances will be stale. 2103 c.log.Warnf("Could not retrieve balances from %s wallet: %v", unbip(assetID), err) 2104 } 2105 2106 c.notify(newWalletStateNote(w.state())) 2107 return nil 2108 } 2109 2110 // connectAndUpdateWallet creates a connection to a wallet and updates the 2111 // balance. 2112 func (c *Core) connectAndUpdateWallet(w *xcWallet) error { 2113 return c.connectAndUpdateWalletResumeTrades(w, true) 2114 } 2115 2116 // connectedWallet fetches a wallet and will connect the wallet if it is not 2117 // already connected. If the wallet gets connected, this also emits WalletState 2118 // and WalletBalance notification. 2119 func (c *Core) connectedWallet(assetID uint32) (*xcWallet, error) { 2120 wallet, exists := c.wallet(assetID) 2121 if !exists { 2122 return nil, newError(missingWalletErr, "no configured wallet found for %s (%d)", 2123 strings.ToUpper(unbip(assetID)), assetID) 2124 } 2125 if !wallet.connected() { 2126 err := c.connectAndUpdateWallet(wallet) 2127 if err != nil { 2128 return nil, err 2129 } 2130 } 2131 return wallet, nil 2132 } 2133 2134 // connectWalletResumeTrades connects to the wallet and returns the deposit 2135 // address validated by the xcWallet after connecting. If the wallet backend 2136 // is still syncing, this also starts a goroutine to monitor sync status, 2137 // emitting WalletStateNotes on each progress update. If resumeTrades is set to 2138 // true, an attempt to resume any trades that were unable to be resumed at 2139 // startup will be made. 2140 func (c *Core) connectWalletResumeTrades(w *xcWallet, resumeTrades bool) (depositAddr string, err error) { 2141 if w.isDisabled() { 2142 return "", fmt.Errorf(walletDisabledErrStr, w.Symbol) 2143 } 2144 2145 err = w.Connect() // ensures valid deposit address 2146 if err != nil { 2147 return "", newError(connectWalletErr, "failed to connect %s wallet: %w", w.Symbol, err) 2148 } 2149 2150 // This may be a wallet that does not require a password, so we can attempt 2151 // to resume any active trades. 2152 if resumeTrades { 2153 go c.resumeTrades(nil) 2154 } 2155 2156 w.mtx.RLock() 2157 depositAddr = w.address 2158 synced := w.syncStatus.Synced 2159 w.mtx.RUnlock() 2160 2161 // If the wallet is synced, update the bond reserves, logging any balance 2162 // insufficiencies, otherwise start a loop to check the sync status until it 2163 // is. 2164 if synced { 2165 c.updateBondReserves(w.AssetID) 2166 } else { 2167 c.startWalletSyncMonitor(w) 2168 } 2169 2170 return 2171 } 2172 2173 // connectWallet connects to the wallet and returns the deposit address 2174 // validated by the xcWallet after connecting. If the wallet backend is still 2175 // syncing, this also starts a goroutine to monitor sync status, emitting 2176 // WalletStateNotes on each progress update. 2177 func (c *Core) connectWallet(w *xcWallet) (depositAddr string, err error) { 2178 return c.connectWalletResumeTrades(w, true) 2179 } 2180 2181 // unlockWalletResumeTrades will unlock a wallet if it is not yet unlocked. If 2182 // resumeTrades is set to true, an attempt to resume any trades that were 2183 // unable to be resumed at startup will be made. 2184 func (c *Core) unlockWalletResumeTrades(crypter encrypt.Crypter, wallet *xcWallet, resumeTrades bool) error { 2185 // Unlock if either the backend itself is locked or if we lack a cached 2186 // unencrypted password for encrypted wallets. 2187 if !wallet.unlocked() { 2188 if crypter == nil { 2189 return newError(noAuthError, "wallet locked and no password provided") 2190 } 2191 // Note that in cases where we already had the cached decrypted password 2192 // but it was just the backend reporting as locked, only unlocking the 2193 // backend is needed but this redecrypts the password using the provided 2194 // crypter. This case could instead be handled with a refreshUnlock. 2195 err := wallet.Unlock(crypter) 2196 if err != nil { 2197 return newError(walletAuthErr, "failed to unlock %s wallet: %w", 2198 unbip(wallet.AssetID), err) 2199 } 2200 // Notify new wallet state. 2201 c.notify(newWalletStateNote(wallet.state())) 2202 2203 if resumeTrades { 2204 go c.resumeTrades(crypter) 2205 } 2206 } 2207 2208 return nil 2209 } 2210 2211 // unlockWallet will unlock a wallet if it is not yet unlocked. 2212 func (c *Core) unlockWallet(crypter encrypt.Crypter, wallet *xcWallet) error { 2213 return c.unlockWalletResumeTrades(crypter, wallet, true) 2214 } 2215 2216 // connectAndUnlockResumeTrades will connect to the wallet if not already 2217 // connected, and unlock the wallet if not already unlocked. If the wallet 2218 // backend is still syncing, this also starts a goroutine to monitor sync 2219 // status, emitting WalletStateNotes on each progress update. If resumeTrades 2220 // is set to true, an attempt to resume any trades that were unable to be 2221 // resumed at startup will be made. 2222 func (c *Core) connectAndUnlockResumeTrades(crypter encrypt.Crypter, wallet *xcWallet, resumeTrades bool) error { 2223 if !wallet.connected() { 2224 err := c.connectAndUpdateWalletResumeTrades(wallet, resumeTrades) 2225 if err != nil { 2226 return err 2227 } 2228 } 2229 2230 return c.unlockWalletResumeTrades(crypter, wallet, resumeTrades) 2231 } 2232 2233 // connectAndUnlock will connect to the wallet if not already connected, 2234 // and unlock the wallet if not already unlocked. If the wallet backend 2235 // is still syncing, this also starts a goroutine to monitor sync status, 2236 // emitting WalletStateNotes on each progress update. 2237 func (c *Core) connectAndUnlock(crypter encrypt.Crypter, wallet *xcWallet) error { 2238 return c.connectAndUnlockResumeTrades(crypter, wallet, true) 2239 } 2240 2241 // walletBalance gets the xcWallet's current WalletBalance, which includes the 2242 // db.Balance plus order/contract locked amounts. The data is not stored. Use 2243 // updateWalletBalance instead to also update xcWallet.balance and the DB. 2244 func (c *Core) walletBalance(wallet *xcWallet) (*WalletBalance, error) { 2245 bal, err := wallet.Balance() 2246 if err != nil { 2247 return nil, err 2248 } 2249 contractLockedAmt, orderLockedAmt, bondLockedAmt := c.lockedAmounts(wallet.AssetID) 2250 return &WalletBalance{ 2251 Balance: &db.Balance{ 2252 Balance: *bal, 2253 Stamp: time.Now(), 2254 }, 2255 OrderLocked: orderLockedAmt, 2256 ContractLocked: contractLockedAmt, 2257 BondLocked: bondLockedAmt, 2258 }, nil 2259 } 2260 2261 // updateWalletBalance retrieves balances for the wallet, updates 2262 // xcWallet.balance and the balance in the DB, and emits a BalanceNote. 2263 func (c *Core) updateWalletBalance(wallet *xcWallet) (*WalletBalance, error) { 2264 walletBal, err := c.walletBalance(wallet) 2265 if err != nil { 2266 return nil, err 2267 } 2268 return walletBal, c.storeAndSendWalletBalance(wallet, walletBal) 2269 } 2270 2271 func (c *Core) storeAndSendWalletBalance(wallet *xcWallet, walletBal *WalletBalance) error { 2272 wallet.setBalance(walletBal) 2273 2274 // Store the db.Balance. 2275 err := c.db.UpdateBalance(wallet.dbID, walletBal.Balance) 2276 if err != nil { 2277 return fmt.Errorf("error updating %s balance in database: %w", unbip(wallet.AssetID), err) 2278 } 2279 c.notify(newBalanceNote(wallet.AssetID, walletBal)) 2280 return nil 2281 } 2282 2283 // lockedAmounts returns the total amount locked in unredeemed and unrefunded 2284 // swaps (contractLocked), the total amount locked by orders for future swaps 2285 // (orderLocked), and the total amount locked in fidelity bonds (bondLocked). 2286 // Only applies to trades where the specified assetID is the fromAssetID. 2287 func (c *Core) lockedAmounts(assetID uint32) (contractLocked, orderLocked, bondLocked uint64) { 2288 for _, dc := range c.dexConnections() { 2289 bondLocked, _ = dc.bondTotal(assetID) 2290 for _, tracker := range dc.trackedTrades() { 2291 if tracker.fromAssetID == assetID { 2292 tracker.mtx.RLock() 2293 contractLocked += tracker.unspentContractAmounts() 2294 orderLocked += tracker.lockedAmount() 2295 tracker.mtx.RUnlock() 2296 } 2297 } 2298 } 2299 return 2300 } 2301 2302 // updateBalances updates the balance for every key in the counter map. 2303 // Notifications are sent. 2304 func (c *Core) updateBalances(assets assetMap) { 2305 if len(assets) == 0 { 2306 return 2307 } 2308 for assetID := range assets { 2309 w, exists := c.wallet(assetID) 2310 if !exists { 2311 // This should never be the case, but log an error in case I'm 2312 // wrong or something changes. 2313 c.log.Errorf("non-existent %d wallet should exist", assetID) 2314 continue 2315 } 2316 _, err := c.updateWalletBalance(w) 2317 if err != nil { 2318 c.log.Errorf("error updating %q balance: %v", unbip(assetID), err) 2319 continue 2320 } 2321 2322 if token := asset.TokenInfo(assetID); token != nil { 2323 if _, alreadyUpdating := assets[token.ParentID]; alreadyUpdating { 2324 continue 2325 } 2326 parentWallet, exists := c.wallet(token.ParentID) 2327 if !exists { 2328 c.log.Errorf("non-existent %d wallet should exist", token.ParentID) 2329 continue 2330 } 2331 _, err := c.updateWalletBalance(parentWallet) 2332 if err != nil { 2333 c.log.Errorf("error updating %q balance: %v", unbip(token.ParentID), err) 2334 continue 2335 } 2336 } 2337 } 2338 } 2339 2340 // updateAssetBalance updates the balance for the specified asset. A 2341 // notification is sent. 2342 func (c *Core) updateAssetBalance(assetID uint32) { 2343 c.updateBalances(assetMap{assetID: struct{}{}}) 2344 } 2345 2346 // xcWallets creates a slice of the c.wallets xcWallets. 2347 func (c *Core) xcWallets() []*xcWallet { 2348 c.walletMtx.RLock() 2349 defer c.walletMtx.RUnlock() 2350 wallets := make([]*xcWallet, 0, len(c.wallets)) 2351 for _, wallet := range c.wallets { 2352 wallets = append(wallets, wallet) 2353 } 2354 return wallets 2355 } 2356 2357 // Wallets creates a slice of WalletState for all known wallets. 2358 func (c *Core) Wallets() []*WalletState { 2359 wallets := c.xcWallets() 2360 state := make([]*WalletState, 0, len(wallets)) 2361 for _, wallet := range wallets { 2362 state = append(state, wallet.state()) 2363 } 2364 return state 2365 } 2366 2367 // ToggleWalletStatus changes a wallet's status to either disabled or enabled. 2368 func (c *Core) ToggleWalletStatus(assetID uint32, disable bool) error { 2369 wallet, exists := c.wallet(assetID) 2370 if !exists { 2371 return newError(missingWalletErr, "no configured wallet found for %s (%d)", 2372 strings.ToUpper(unbip(assetID)), assetID) 2373 } 2374 2375 // Return early if this wallet is already disabled or already enabled. 2376 if disable == wallet.isDisabled() { 2377 return nil 2378 } 2379 2380 // If this wallet is a parent, disable/enable all token wallets. 2381 var affectedWallets []*xcWallet 2382 if disable { 2383 // Ensure wallet is not a parent of an enabled token wallet with active 2384 // orders. 2385 if assetInfo := asset.Asset(assetID); assetInfo != nil { 2386 for id := range assetInfo.Tokens { 2387 if wallet, exists := c.wallet(id); exists && !wallet.isDisabled() { 2388 if c.assetHasActiveOrders(wallet.AssetID) { 2389 return newError(activeOrdersErr, "active orders for %v", unbip(wallet.AssetID)) 2390 } 2391 affectedWallets = append(affectedWallets, wallet) 2392 } 2393 } 2394 } 2395 2396 // If wallet is a parent wallet, it will be the last to be disconnected 2397 // and disabled. 2398 affectedWallets = append(affectedWallets, wallet) 2399 2400 if c.assetHasActiveOrders(assetID) { 2401 return newError(activeOrdersErr, "active orders for %v", unbip(assetID)) 2402 } 2403 2404 // Ensure wallet is not an active bond asset wallet. This check will 2405 // cover for token wallets if this wallet is a parent. 2406 if c.isActiveBondAsset(assetID, true) { 2407 return newError(bondAssetErr, "%v is an active bond asset wallet", unbip(assetID)) 2408 } 2409 2410 // Disconnect and disable all affected wallets. 2411 for _, wallet := range affectedWallets { 2412 if wallet.connected() { 2413 wallet.Disconnect() // before disable or it refuses 2414 } 2415 wallet.setDisabled(true) 2416 } 2417 } else { 2418 if wallet.parent != nil && wallet.parent.isDisabled() { 2419 // Ensure parent wallet starts first. 2420 affectedWallets = append(affectedWallets, wallet.parent) 2421 } 2422 2423 affectedWallets = append(affectedWallets, wallet) 2424 2425 for _, wallet := range affectedWallets { 2426 // Update wallet status before attempting to connect wallet because disabled 2427 // wallets cannot be connected to. 2428 wallet.setDisabled(false) 2429 2430 // Attempt to connect wallet. 2431 err := c.connectAndUpdateWallet(wallet) 2432 if err != nil { 2433 c.log.Errorf("Error connecting to %s wallet: %v", unbip(assetID), err) 2434 } 2435 } 2436 } 2437 2438 for _, wallet := range affectedWallets { 2439 // Update db with wallet status. 2440 err := c.db.UpdateWalletStatus(wallet.dbID, disable) 2441 if err != nil { 2442 return fmt.Errorf("db.UpdateWalletStatus error: %w", err) 2443 } 2444 2445 c.notify(newWalletStateNote(wallet.state())) 2446 } 2447 2448 return nil 2449 } 2450 2451 // SupportedAssets returns a map of asset information for supported assets. 2452 func (c *Core) SupportedAssets() map[uint32]*SupportedAsset { 2453 return c.assetMap() 2454 } 2455 2456 func (c *Core) walletCreationPending(tokenID uint32) bool { 2457 c.pendingWalletsMtx.RLock() 2458 defer c.pendingWalletsMtx.RUnlock() 2459 return c.pendingWallets[tokenID] 2460 } 2461 2462 func (c *Core) setWalletCreationPending(tokenID uint32) error { 2463 c.pendingWalletsMtx.Lock() 2464 defer c.pendingWalletsMtx.Unlock() 2465 if c.pendingWallets[tokenID] { 2466 return fmt.Errorf("creation already pending for %s", unbip(tokenID)) 2467 } 2468 c.pendingWallets[tokenID] = true 2469 return nil 2470 } 2471 2472 func (c *Core) setWalletCreationComplete(tokenID uint32) { 2473 c.pendingWalletsMtx.Lock() 2474 delete(c.pendingWallets, tokenID) 2475 c.pendingWalletsMtx.Unlock() 2476 } 2477 2478 // assetMap returns a map of asset information for supported assets. 2479 func (c *Core) assetMap() map[uint32]*SupportedAsset { 2480 supported := asset.Assets() 2481 assets := make(map[uint32]*SupportedAsset, len(supported)) 2482 c.walletMtx.RLock() 2483 defer c.walletMtx.RUnlock() 2484 for assetID, asset := range supported { 2485 var wallet *WalletState 2486 w, found := c.wallets[assetID] 2487 if found { 2488 wallet = w.state() 2489 } 2490 assets[assetID] = &SupportedAsset{ 2491 ID: assetID, 2492 Symbol: asset.Symbol, 2493 Wallet: wallet, 2494 Info: asset.Info, 2495 Name: asset.Info.Name, 2496 UnitInfo: asset.Info.UnitInfo, 2497 } 2498 for tokenID, token := range asset.Tokens { 2499 wallet = nil 2500 w, found := c.wallets[tokenID] 2501 if found { 2502 wallet = w.state() 2503 } 2504 assets[tokenID] = &SupportedAsset{ 2505 ID: tokenID, 2506 Symbol: dex.BipIDSymbol(tokenID), 2507 Wallet: wallet, 2508 Token: token, 2509 Name: token.Name, 2510 UnitInfo: token.UnitInfo, 2511 WalletCreationPending: c.walletCreationPending(tokenID), 2512 } 2513 } 2514 } 2515 return assets 2516 } 2517 2518 func (c *Core) asset(assetID uint32) *SupportedAsset { 2519 var wallet *WalletState 2520 w, _ := c.wallet(assetID) 2521 if w != nil { 2522 wallet = w.state() 2523 } 2524 regAsset := asset.Asset(assetID) 2525 if regAsset != nil { 2526 return &SupportedAsset{ 2527 ID: assetID, 2528 Symbol: regAsset.Symbol, 2529 Wallet: wallet, 2530 Info: regAsset.Info, 2531 Name: regAsset.Info.Name, 2532 UnitInfo: regAsset.Info.UnitInfo, 2533 } 2534 } 2535 2536 token := asset.TokenInfo(assetID) 2537 if token == nil { 2538 return nil 2539 } 2540 2541 return &SupportedAsset{ 2542 ID: assetID, 2543 Symbol: dex.BipIDSymbol(assetID), 2544 Wallet: wallet, 2545 Token: token, 2546 Name: token.Name, 2547 UnitInfo: token.UnitInfo, 2548 WalletCreationPending: c.walletCreationPending(assetID), 2549 } 2550 } 2551 2552 // User is a thread-safe getter for the User. 2553 func (c *Core) User() *User { 2554 return &User{ 2555 Assets: c.assetMap(), 2556 Exchanges: c.Exchanges(), 2557 Initialized: c.IsInitialized(), 2558 SeedGenerationTime: c.seedGenerationTime, 2559 FiatRates: c.fiatConversions(), 2560 Net: c.net, 2561 ExtensionConfig: c.extensionModeConfig, 2562 Actions: c.requestedActionsList(), 2563 } 2564 } 2565 2566 func (c *Core) requestedActionsList() []*asset.ActionRequiredNote { 2567 c.requestedActionMtx.RLock() 2568 defer c.requestedActionMtx.RUnlock() 2569 actions := make([]*asset.ActionRequiredNote, 0, len(c.requestedActions)) 2570 for _, a := range c.requestedActions { 2571 actions = append(actions, a) 2572 } 2573 return actions 2574 } 2575 2576 // CreateWallet creates a new exchange wallet. 2577 func (c *Core) CreateWallet(appPW, walletPW []byte, form *WalletForm) error { 2578 assetID := form.AssetID 2579 symbol := unbip(assetID) 2580 _, exists := c.wallet(assetID) 2581 if exists { 2582 return fmt.Errorf("%s wallet already exists", symbol) 2583 } 2584 2585 crypter, err := c.encryptionKey(appPW) 2586 if err != nil { 2587 return err 2588 } 2589 2590 var creationQueued bool 2591 defer func() { 2592 if !creationQueued { 2593 crypter.Close() 2594 } 2595 }() 2596 2597 // If this isn't a token, easy route. 2598 token := asset.TokenInfo(assetID) 2599 if token == nil { 2600 _, err = c.createWalletOrToken(crypter, walletPW, form) 2601 return err 2602 } 2603 2604 // Prevent two different tokens from trying to create the parent simultaneously. 2605 if err = c.setWalletCreationPending(token.ParentID); err != nil { 2606 return err 2607 } 2608 defer c.setWalletCreationComplete(token.ParentID) 2609 2610 // If the parent already exists, easy route. 2611 _, found := c.wallet(token.ParentID) 2612 if found { 2613 _, err = c.createWalletOrToken(crypter, walletPW, form) 2614 return err 2615 } 2616 2617 // Double-registration mode. The parent wallet will be created 2618 // synchronously, then a goroutine is launched to wait for the parent to 2619 // sync before creating the token wallet. The caller can get information 2620 // about the asynchronous creation from WalletCreationNote notifications. 2621 2622 // First check that they configured the parent asset. 2623 if form.ParentForm == nil { 2624 return fmt.Errorf("no parent wallet %d for token %d (%s), and no parent asset configuration provided", 2625 token.ParentID, assetID, unbip(assetID)) 2626 } 2627 if form.ParentForm.AssetID != token.ParentID { 2628 return fmt.Errorf("parent form asset ID %d is not expected value %d", 2629 form.ParentForm.AssetID, token.ParentID) 2630 } 2631 2632 // Create the parent synchronously. 2633 parentWallet, err := c.createWalletOrToken(crypter, walletPW, form.ParentForm) 2634 if err != nil { 2635 return fmt.Errorf("error creating parent wallet: %v", err) 2636 } 2637 2638 if err = c.setWalletCreationPending(assetID); err != nil { 2639 return err 2640 } 2641 2642 // Start a goroutine to wait until the parent wallet is synced, and then 2643 // begin creation of the token wallet. 2644 c.wg.Add(1) 2645 2646 c.notify(newWalletCreationNote(TopicCreationQueued, "", "", db.Data, assetID)) 2647 2648 go func() { 2649 defer c.wg.Done() 2650 defer c.setWalletCreationComplete(assetID) 2651 defer crypter.Close() 2652 2653 for { 2654 parentWallet.mtx.RLock() 2655 synced := parentWallet.syncStatus.Synced 2656 parentWallet.mtx.RUnlock() 2657 if synced { 2658 break 2659 } 2660 select { 2661 case <-c.ctx.Done(): 2662 return 2663 case <-time.After(time.Second): 2664 } 2665 } 2666 // If there was a walletPW provided, it was for the parent wallet, so 2667 // use nil here. 2668 if _, err := c.createWalletOrToken(crypter, nil, form); err != nil { 2669 c.log.Errorf("failed to create token wallet: %v", err) 2670 subject, details := c.formatDetails(TopicQueuedCreationFailed, unbip(token.ParentID), symbol) 2671 c.notify(newWalletCreationNote(TopicQueuedCreationFailed, subject, details, db.ErrorLevel, assetID)) 2672 } else { 2673 c.notify(newWalletCreationNote(TopicQueuedCreationSuccess, "", "", db.Data, assetID)) 2674 } 2675 }() 2676 creationQueued = true 2677 return nil 2678 } 2679 2680 func (c *Core) createWalletOrToken(crypter encrypt.Crypter, walletPW []byte, form *WalletForm) (wallet *xcWallet, err error) { 2681 assetID := form.AssetID 2682 symbol := unbip(assetID) 2683 token := asset.TokenInfo(assetID) 2684 var dbWallet *db.Wallet 2685 if token != nil { 2686 dbWallet, err = c.createTokenWallet(assetID, token, form) 2687 } else { 2688 dbWallet, err = c.createWallet(crypter, walletPW, assetID, form) 2689 } 2690 if err != nil { 2691 return nil, err 2692 } 2693 2694 wallet, err = c.loadWallet(dbWallet) 2695 if err != nil { 2696 return nil, fmt.Errorf("error loading wallet for %d -> %s: %w", assetID, symbol, err) 2697 } 2698 // Block PeersChange until we know this wallet is ready. 2699 atomic.StoreUint32(wallet.broadcasting, 0) 2700 2701 dbWallet.Address, err = c.connectWallet(wallet) 2702 if err != nil { 2703 return nil, err 2704 } 2705 2706 if c.cfg.UnlockCoinsOnLogin { 2707 if err = wallet.ReturnCoins(nil); err != nil { 2708 c.log.Errorf("Failed to unlock all %s wallet coins: %v", unbip(wallet.AssetID), err) 2709 } 2710 } 2711 2712 initErr := func(s string, a ...any) (*xcWallet, error) { 2713 _ = wallet.Lock(2 * time.Second) // just try, but don't confuse the user with an error 2714 wallet.Disconnect() 2715 return nil, fmt.Errorf(s, a...) 2716 } 2717 2718 err = c.unlockWallet(crypter, wallet) // no-op if !wallet.Wallet.Locked() && len(encPW) == 0 2719 if err != nil { 2720 wallet.Disconnect() 2721 return nil, fmt.Errorf("%s wallet authentication error: %w", symbol, err) 2722 } 2723 2724 balances, err := c.walletBalance(wallet) 2725 if err != nil { 2726 return initErr("error getting wallet balance for %s: %w", symbol, err) 2727 } 2728 wallet.setBalance(balances) // update xcWallet's WalletBalance 2729 dbWallet.Balance = balances.Balance // store the db.Balance 2730 2731 // Store the wallet in the database. 2732 err = c.db.UpdateWallet(dbWallet) 2733 if err != nil { 2734 return initErr("error storing wallet credentials: %w", err) 2735 } 2736 2737 c.log.Infof("Created %s wallet. Balance available = %d / "+ 2738 "locked = %d / locked in contracts = %d, Deposit address = %s", 2739 symbol, balances.Available, balances.Locked, balances.ContractLocked, 2740 dbWallet.Address) 2741 2742 // The wallet has been successfully created. Store it. 2743 c.updateWallet(assetID, wallet) 2744 2745 atomic.StoreUint32(wallet.broadcasting, 1) 2746 c.notify(newWalletStateNote(wallet.state())) 2747 c.walletCheckAndNotify(wallet) 2748 2749 return wallet, nil 2750 } 2751 2752 func (c *Core) createWallet(crypter encrypt.Crypter, walletPW []byte, assetID uint32, form *WalletForm) (*db.Wallet, error) { 2753 walletDef, err := asset.WalletDef(assetID, form.Type) 2754 if err != nil { 2755 return nil, newError(assetSupportErr, "asset.WalletDef error: %w", err) 2756 } 2757 2758 // Sometimes core will insert data into the Settings map to communicate 2759 // information back to the wallet, so it cannot be nil. 2760 if form.Config == nil { 2761 form.Config = make(map[string]string) 2762 } 2763 2764 // Remove unused key-values from parsed settings before saving to db. 2765 // Especially necessary if settings was parsed from a config file, b/c 2766 // config files usually define more key-values than we need. 2767 // Expected keys should be lowercase because config.Parse returns lowercase 2768 // keys. 2769 expectedKeys := make(map[string]bool, len(walletDef.ConfigOpts)) 2770 for _, option := range walletDef.ConfigOpts { 2771 expectedKeys[strings.ToLower(option.Key)] = true 2772 } 2773 for key := range form.Config { 2774 if !expectedKeys[key] { 2775 delete(form.Config, key) 2776 } 2777 } 2778 2779 if walletDef.Seeded { 2780 if len(walletPW) > 0 { 2781 return nil, errors.New("external password incompatible with seeded wallet") 2782 } 2783 walletPW, err = c.createSeededWallet(assetID, crypter, form) 2784 if err != nil { 2785 return nil, err 2786 } 2787 } 2788 2789 var encPW []byte 2790 if len(walletPW) > 0 { 2791 encPW, err = crypter.Encrypt(walletPW) 2792 if err != nil { 2793 return nil, fmt.Errorf("wallet password encryption error: %w", err) 2794 } 2795 } 2796 2797 return &db.Wallet{ 2798 Type: walletDef.Type, 2799 AssetID: assetID, 2800 Settings: form.Config, 2801 EncryptedPW: encPW, 2802 // Balance and Address are set after connect. 2803 }, nil 2804 } 2805 2806 func (c *Core) createTokenWallet(tokenID uint32, token *asset.Token, form *WalletForm) (*db.Wallet, error) { 2807 wallet, found := c.wallet(token.ParentID) 2808 if !found { 2809 return nil, fmt.Errorf("no parent wallet %d for token %d (%s)", token.ParentID, tokenID, unbip(tokenID)) 2810 } 2811 2812 tokenMaster, is := wallet.Wallet.(asset.TokenMaster) 2813 if !is { 2814 return nil, fmt.Errorf("parent wallet %s is not a TokenMaster", unbip(token.ParentID)) 2815 } 2816 2817 // Sometimes core will insert data into the Settings map to communicate 2818 // information back to the wallet, so it cannot be nil. 2819 if form.Config == nil { 2820 form.Config = make(map[string]string) 2821 } 2822 2823 if err := tokenMaster.CreateTokenWallet(tokenID, form.Config); err != nil { 2824 return nil, fmt.Errorf("CreateTokenWallet error: %w", err) 2825 } 2826 2827 return &db.Wallet{ 2828 Type: form.Type, 2829 AssetID: tokenID, 2830 Settings: form.Config, 2831 // EncryptedPW ignored because we assume throughout that token wallet 2832 // authorization is handled by the parent. 2833 // Balance and Address are set after connect. 2834 }, nil 2835 } 2836 2837 // createSeededWallet initializes a seeded wallet with an asset-specific seed 2838 // and password derived deterministically from the app seed. The password is 2839 // returned for encrypting and storing. 2840 func (c *Core) createSeededWallet(assetID uint32, crypter encrypt.Crypter, form *WalletForm) ([]byte, error) { 2841 seed, pw, err := c.assetSeedAndPass(assetID, crypter) 2842 if err != nil { 2843 return nil, err 2844 } 2845 defer encode.ClearBytes(seed) 2846 2847 var bday uint64 2848 if creds := c.creds(); !creds.Birthday.IsZero() { 2849 bday = uint64(creds.Birthday.Unix()) 2850 } 2851 2852 c.log.Infof("Initializing a %s wallet", unbip(assetID)) 2853 if err = asset.CreateWallet(assetID, &asset.CreateWalletParams{ 2854 Type: form.Type, 2855 Seed: seed, 2856 Pass: pw, 2857 Birthday: bday, 2858 Settings: form.Config, 2859 DataDir: c.assetDataDirectory(assetID), 2860 Net: c.net, 2861 Logger: c.log.SubLogger(unbip(assetID)), 2862 }); err != nil { 2863 return nil, fmt.Errorf("Error creating wallet: %w", err) 2864 } 2865 2866 return pw, nil 2867 } 2868 2869 func (c *Core) assetSeedAndPass(assetID uint32, crypter encrypt.Crypter) (seed, pass []byte, err error) { 2870 creds := c.creds() 2871 if creds == nil { 2872 return nil, nil, errors.New("no v2 credentials stored") 2873 } 2874 2875 if tkn := asset.TokenInfo(assetID); tkn != nil { 2876 return nil, nil, fmt.Errorf("%s is a token. assets seeds are for base chains onlyu. did you want %s", 2877 tkn.Name, asset.Asset(tkn.ParentID).Info.Name) 2878 } 2879 2880 appSeed, err := crypter.Decrypt(creds.EncSeed) 2881 if err != nil { 2882 return nil, nil, fmt.Errorf("app seed decryption error: %w", err) 2883 } 2884 2885 seed, pass = AssetSeedAndPass(assetID, appSeed) 2886 return seed, pass, nil 2887 } 2888 2889 // AssetSeedAndPass derives the wallet seed and password that would be used to 2890 // create a native wallet for a particular asset and application seed. Depending 2891 // on external wallet software and their key derivation paths, this seed may be 2892 // usable for accessing funds outside of DEX applications, e.g. btcwallet. 2893 func AssetSeedAndPass(assetID uint32, appSeed []byte) ([]byte, []byte) { 2894 const accountBasedSeedAssetID = 60 // ETH 2895 seedAssetID := assetID 2896 if ai, _ := asset.Info(assetID); ai != nil && ai.IsAccountBased { 2897 seedAssetID = accountBasedSeedAssetID 2898 } 2899 // Tokens asset IDs shouldn't be passed in, but if they are, return the seed 2900 // for the parent ID. 2901 if tkn := asset.TokenInfo(assetID); tkn != nil { 2902 if ai, _ := asset.Info(tkn.ParentID); ai != nil { 2903 if ai.IsAccountBased { 2904 seedAssetID = accountBasedSeedAssetID 2905 } 2906 } 2907 } 2908 2909 b := make([]byte, len(appSeed)+4) 2910 copy(b, appSeed) 2911 binary.BigEndian.PutUint32(b[len(appSeed):], seedAssetID) 2912 s := blake256.Sum256(b) 2913 p := blake256.Sum256(s[:]) 2914 return s[:], p[:] 2915 } 2916 2917 // assetDataDirectory is a directory for a wallet to use for local storage. 2918 func (c *Core) assetDataDirectory(assetID uint32) string { 2919 return filepath.Join(filepath.Dir(c.cfg.DBPath), "assetdb", unbip(assetID)) 2920 } 2921 2922 // assetDataBackupDirectory is a directory for a wallet to use for backups of 2923 // data. Wallet data is copied here instead of being deleted when recovering a 2924 // wallet. 2925 func (c *Core) assetDataBackupDirectory(assetID uint32) string { 2926 return filepath.Join(filepath.Dir(c.cfg.DBPath), "assetdb-backup", unbip(assetID)) 2927 } 2928 2929 // loadWallet uses the data from the database to construct a new exchange 2930 // wallet. The returned wallet is running but not connected. 2931 func (c *Core) loadWallet(dbWallet *db.Wallet) (*xcWallet, error) { 2932 var parent *xcWallet 2933 assetID := dbWallet.AssetID 2934 2935 // Construct the unconnected xcWallet. 2936 contractLockedAmt, orderLockedAmt, bondLockedAmt := c.lockedAmounts(assetID) 2937 symbol := unbip(assetID) 2938 wallet := &xcWallet{ // captured by the PeersChange closure 2939 AssetID: assetID, 2940 Symbol: symbol, 2941 log: c.log.SubLogger(symbol), 2942 balance: &WalletBalance{ 2943 Balance: dbWallet.Balance, 2944 OrderLocked: orderLockedAmt, 2945 ContractLocked: contractLockedAmt, 2946 BondLocked: bondLockedAmt, 2947 }, 2948 encPass: dbWallet.EncryptedPW, 2949 address: dbWallet.Address, 2950 peerCount: -1, // no count yet 2951 dbID: dbWallet.ID(), 2952 walletType: dbWallet.Type, 2953 broadcasting: new(uint32), 2954 disabled: dbWallet.Disabled, 2955 syncStatus: &asset.SyncStatus{}, 2956 } 2957 2958 token := asset.TokenInfo(assetID) 2959 2960 peersChange := func(numPeers uint32, err error) { 2961 if c.ctx.Err() != nil { 2962 return 2963 } 2964 2965 c.wg.Add(1) 2966 go func() { 2967 defer c.wg.Done() 2968 c.peerChange(wallet, numPeers, err) 2969 }() 2970 } 2971 2972 log := c.log.SubLogger(unbip(assetID)) 2973 var w asset.Wallet 2974 var err error 2975 if token == nil { 2976 2977 walletCfg := &asset.WalletConfig{ 2978 Type: dbWallet.Type, 2979 Settings: dbWallet.Settings, 2980 Emit: asset.NewWalletEmitter(c.notes, assetID, log), 2981 PeersChange: peersChange, 2982 DataDir: c.assetDataDirectory(assetID), 2983 } 2984 2985 walletCfg.Settings[asset.SpecialSettingActivelyUsed] = 2986 strconv.FormatBool(c.assetHasActiveOrders(dbWallet.AssetID)) 2987 defer delete(walletCfg.Settings, asset.SpecialSettingActivelyUsed) 2988 2989 w, err = asset.OpenWallet(assetID, walletCfg, log, c.net) 2990 } else { 2991 var found bool 2992 parent, found = c.wallet(token.ParentID) 2993 if !found { 2994 return nil, fmt.Errorf("cannot load %s wallet before %s wallet", unbip(assetID), unbip(token.ParentID)) 2995 } 2996 2997 tokenMaster, is := parent.Wallet.(asset.TokenMaster) 2998 if !is { 2999 return nil, fmt.Errorf("%s token's %s parent wallet is not a TokenMaster", unbip(assetID), unbip(token.ParentID)) 3000 } 3001 3002 w, err = tokenMaster.OpenTokenWallet(&asset.TokenConfig{ 3003 AssetID: assetID, 3004 Settings: dbWallet.Settings, 3005 Emit: asset.NewWalletEmitter(c.notes, assetID, log), 3006 PeersChange: peersChange, 3007 }) 3008 } 3009 if err != nil { 3010 if errors.Is(err, asset.ErrWalletTypeDisabled) { 3011 subject, details := c.formatDetails(TopicWalletTypeDeprecated, unbip(assetID)) 3012 c.notify(newWalletConfigNote(TopicWalletTypeDeprecated, subject, details, db.WarningLevel, nil)) 3013 } 3014 return nil, fmt.Errorf("error opening wallet: %w", err) 3015 } 3016 3017 wallet.Wallet = w 3018 wallet.parent = parent 3019 wallet.supportedVersions = w.Info().SupportedVersions 3020 wallet.connector = dex.NewConnectionMaster(w) 3021 wallet.traits = asset.DetermineWalletTraits(w) 3022 atomic.StoreUint32(wallet.broadcasting, 1) 3023 return wallet, nil 3024 } 3025 3026 // WalletState returns the *WalletState for the asset ID. 3027 func (c *Core) WalletState(assetID uint32) *WalletState { 3028 c.walletMtx.Lock() 3029 defer c.walletMtx.Unlock() 3030 wallet, has := c.wallets[assetID] 3031 if !has { 3032 c.log.Tracef("wallet status requested for unknown asset %d -> %s", assetID, unbip(assetID)) 3033 return nil 3034 } 3035 return wallet.state() 3036 } 3037 3038 // WalletTraits gets the traits for the wallet. 3039 func (c *Core) WalletTraits(assetID uint32) (asset.WalletTrait, error) { 3040 w, found := c.wallet(assetID) 3041 if !found { 3042 return 0, fmt.Errorf("no %d wallet found", assetID) 3043 } 3044 return w.traits, nil 3045 } 3046 3047 // assetHasActiveOrders checks whether there are any active orders or 3048 // negotiating matches for the specified asset. 3049 func (c *Core) assetHasActiveOrders(assetID uint32) bool { 3050 for _, dc := range c.dexConnections() { 3051 if dc.hasActiveAssetOrders(assetID) { 3052 return true 3053 } 3054 } 3055 return false 3056 } 3057 3058 // walletIsActive combines assetHasActiveOrders with a check for pending 3059 // registration fee payments and pending bonds. 3060 func (c *Core) walletIsActive(assetID uint32) bool { 3061 if c.assetHasActiveOrders(assetID) { 3062 return true 3063 } 3064 for _, dc := range c.dexConnections() { 3065 dc.acct.authMtx.RLock() 3066 for _, pb := range dc.pendingBonds() { 3067 if pb.AssetID == assetID { 3068 dc.acct.authMtx.RUnlock() 3069 return true 3070 } 3071 } 3072 dc.acct.authMtx.RUnlock() 3073 } 3074 return false 3075 } 3076 3077 func (dc *dexConnection) bondOpts() (assetID uint32, targetTier, max uint64) { 3078 dc.acct.authMtx.RLock() 3079 defer dc.acct.authMtx.RUnlock() 3080 return dc.acct.bondAsset, dc.acct.targetTier, dc.acct.maxBondedAmt 3081 } 3082 3083 func (dc *dexConnection) bondTotalInternal(assetID uint32) (total, active uint64) { 3084 sum := func(bonds []*db.Bond) (amt uint64) { 3085 for _, b := range bonds { 3086 if assetID == b.AssetID { 3087 amt += b.Amount 3088 } 3089 } 3090 return 3091 } 3092 active = sum(dc.acct.bonds) 3093 return active + sum(dc.acct.pendingBonds) + sum(dc.acct.expiredBonds), active 3094 } 3095 3096 func (dc *dexConnection) bondTotal(assetID uint32) (total, active uint64) { 3097 dc.acct.authMtx.RLock() 3098 defer dc.acct.authMtx.RUnlock() 3099 return dc.bondTotalInternal(assetID) 3100 } 3101 3102 func (dc *dexConnection) hasUnspentAssetBond(assetID uint32) bool { 3103 total, _ := dc.bondTotal(assetID) 3104 return total > 0 3105 } 3106 3107 func (dc *dexConnection) hasUnspentBond() bool { 3108 dc.acct.authMtx.RLock() 3109 defer dc.acct.authMtx.RUnlock() 3110 return len(dc.acct.bonds) > 0 || len(dc.acct.pendingBonds) > 0 || len(dc.acct.expiredBonds) > 0 3111 } 3112 3113 // isActiveBondAsset indicates if a wallet (or it's parent if the asset is a 3114 // token, or it's children if it's a base asset) is needed for bonding on any 3115 // configured DEX. includeLive should be set to consider all existing unspent 3116 // bonds that need to be refunded in the future (only requires a broadcast, no 3117 // wallet signing ability). 3118 func (c *Core) isActiveBondAsset(assetID uint32, includeLive bool) bool { 3119 // Consider this asset and any child tokens if it is a base asset, or just 3120 // the parent asset if it's a token. 3121 assetIDs := map[uint32]bool{ 3122 assetID: true, 3123 } 3124 if ra := asset.Asset(assetID); ra != nil { // it's a base asset, all tokens need it 3125 for tknAssetID := range ra.Tokens { 3126 assetIDs[tknAssetID] = true 3127 } 3128 } else { // it's a token and we only care about the parent, not sibling tokens 3129 if tkn := asset.TokenInfo(assetID); tkn != nil { // it should be 3130 assetIDs[tkn.ParentID] = true 3131 } 3132 } 3133 3134 for _, dc := range c.dexConnections() { 3135 bondAsset, targetTier, _ := dc.bondOpts() 3136 if targetTier > 0 && assetIDs[bondAsset] { 3137 return true 3138 } 3139 if includeLive { 3140 for id := range assetIDs { 3141 if dc.hasUnspentAssetBond(id) { 3142 return true 3143 } 3144 } 3145 } 3146 } 3147 return false 3148 } 3149 3150 // walletCheckAndNotify sets the xcWallet's synced and syncProgress fields from 3151 // the wallet's SyncStatus result, emits a WalletStateNote, and returns the 3152 // synced value. When synced is true, this also updates the wallet's balance, 3153 // stores the balance in the DB, emits a BalanceNote, and updates the bond 3154 // reserves (with balance checking). 3155 func (c *Core) walletCheckAndNotify(w *xcWallet) bool { 3156 ss, err := w.SyncStatus() 3157 if err != nil { 3158 c.log.Errorf("Unable to get wallet/node sync status for %s: %v", 3159 unbip(w.AssetID), err) 3160 return false 3161 } 3162 3163 w.mtx.Lock() 3164 wasSynced := w.syncStatus.Synced 3165 w.syncStatus = ss 3166 w.mtx.Unlock() 3167 3168 if atomic.LoadUint32(w.broadcasting) == 1 { 3169 c.notify(newWalletSyncNote(w.AssetID, ss)) 3170 } 3171 if ss.Synced && !wasSynced { 3172 c.updateWalletBalance(w) 3173 c.log.Debugf("Wallet synced for asset %s", unbip(w.AssetID)) 3174 c.updateBondReserves(w.AssetID) 3175 } 3176 return ss.Synced 3177 } 3178 3179 // startWalletSyncMonitor repeatedly calls walletCheckAndNotify on a ticker 3180 // until it is synced. This launches the monitor goroutine, if not already 3181 // running, and immediately returns. 3182 func (c *Core) startWalletSyncMonitor(wallet *xcWallet) { 3183 // Prevent multiple sync monitors for this wallet. 3184 if !atomic.CompareAndSwapUint32(&wallet.monitored, 0, 1) { 3185 return // already monitoring 3186 } 3187 3188 c.wg.Add(1) 3189 go func() { 3190 defer c.wg.Done() 3191 defer atomic.StoreUint32(&wallet.monitored, 0) 3192 ticker := time.NewTicker(syncTickerPeriod) 3193 defer ticker.Stop() 3194 for { 3195 select { 3196 case <-ticker.C: 3197 if c.walletCheckAndNotify(wallet) { 3198 return 3199 } 3200 case <-wallet.connector.Done(): 3201 c.log.Warnf("%v wallet shut down before sync completed.", wallet.Info().Name) 3202 return 3203 case <-c.ctx.Done(): 3204 return 3205 } 3206 } 3207 }() 3208 } 3209 3210 // RescanWallet will issue a Rescan command to the wallet if supported by the 3211 // wallet implementation. It is up to the underlying wallet backend if and how 3212 // to implement this functionality. It may be asynchronous. Core will emit 3213 // wallet state notifications until the rescan is complete. If force is false, 3214 // this will check for active orders involving this asset before initiating a 3215 // rescan. WARNING: It is ill-advised to initiate a wallet rescan with active 3216 // orders unless as a last ditch effort to get the wallet to recognize a 3217 // transaction needed to complete a swap. 3218 func (c *Core) RescanWallet(assetID uint32, force bool) error { 3219 if !force && c.walletIsActive(assetID) { 3220 return newError(activeOrdersErr, "active orders or registration fee payments for %v", unbip(assetID)) 3221 } 3222 3223 wallet, err := c.connectedWallet(assetID) 3224 if err != nil { 3225 return fmt.Errorf("OpenWallet: wallet not found for %d -> %s: %w", 3226 assetID, unbip(assetID), err) 3227 } 3228 3229 walletDef, err := asset.WalletDef(assetID, wallet.walletType) 3230 if err != nil { 3231 return newError(assetSupportErr, "asset.WalletDef error: %w", err) 3232 } 3233 3234 var bday uint64 // unix time seconds 3235 if walletDef.Seeded { 3236 creds := c.creds() 3237 if !creds.Birthday.IsZero() { 3238 bday = uint64(creds.Birthday.Unix()) 3239 } 3240 } 3241 3242 // Begin potentially asynchronous wallet rescan operation. 3243 if err = wallet.rescan(c.ctx, bday); err != nil { 3244 return err 3245 } 3246 3247 if c.walletCheckAndNotify(wallet) { 3248 return nil // sync done, Rescan may have by synchronous or a no-op 3249 } 3250 3251 // Synchronization still running. Launch a status update goroutine. 3252 c.startWalletSyncMonitor(wallet) 3253 3254 return nil 3255 } 3256 3257 func (c *Core) removeWallet(assetID uint32) { 3258 c.walletMtx.Lock() 3259 defer c.walletMtx.Unlock() 3260 delete(c.wallets, assetID) 3261 } 3262 3263 // updateWallet stores or updates an asset's wallet. 3264 func (c *Core) updateWallet(assetID uint32, wallet *xcWallet) { 3265 c.walletMtx.Lock() 3266 defer c.walletMtx.Unlock() 3267 c.wallets[assetID] = wallet 3268 } 3269 3270 // RecoverWallet will retrieve some recovery information from the wallet, 3271 // which may not be possible if the wallet is too corrupted. Disconnect and 3272 // destroy the old wallet, create a new one, and if the recovery information 3273 // was retrieved from the old wallet, send this information to the new one. 3274 // If force is false, this will check for active orders involving this 3275 // asset before initiating a rescan. WARNING: It is ill-advised to initiate 3276 // a wallet recovery with active orders unless the wallet db is definitely 3277 // corrupted and even a rescan will not save it. 3278 // 3279 // DO NOT MAKE CONCURRENT CALLS TO THIS FUNCTION WITH THE SAME ASSET. 3280 func (c *Core) RecoverWallet(assetID uint32, appPW []byte, force bool) error { 3281 crypter, err := c.encryptionKey(appPW) 3282 if err != nil { 3283 return newError(authErr, "RecoverWallet password error: %w", err) 3284 } 3285 defer crypter.Close() 3286 3287 if !force { 3288 for _, dc := range c.dexConnections() { 3289 if dc.hasActiveAssetOrders(assetID) { 3290 return newError(activeOrdersErr, "active orders for %v", unbip(assetID)) 3291 } 3292 } 3293 } 3294 3295 oldWallet, found := c.wallet(assetID) 3296 if !found { 3297 return fmt.Errorf("RecoverWallet: wallet not found for %d -> %s: %w", 3298 assetID, unbip(assetID), err) 3299 } 3300 3301 recoverer, isRecoverer := oldWallet.Wallet.(asset.Recoverer) 3302 if !isRecoverer { 3303 return errors.New("wallet is not a recoverer") 3304 } 3305 walletDef, err := asset.WalletDef(assetID, oldWallet.walletType) 3306 if err != nil { 3307 return newError(assetSupportErr, "asset.WalletDef error: %w", err) 3308 } 3309 // Unseeded wallets shouldn't implement the Recoverer interface. This 3310 // is just an additional check for safety. 3311 if !walletDef.Seeded { 3312 return fmt.Errorf("can only recover a seeded wallet") 3313 } 3314 3315 dbWallet, err := c.db.Wallet(oldWallet.dbID) 3316 if err != nil { 3317 return fmt.Errorf("error retrieving DB wallet: %w", err) 3318 } 3319 3320 seed, pw, err := c.assetSeedAndPass(assetID, crypter) 3321 if err != nil { 3322 return err 3323 } 3324 defer encode.ClearBytes(seed) 3325 defer encode.ClearBytes(pw) 3326 3327 if oldWallet.connected() { 3328 if recoveryCfg, err := recoverer.GetRecoveryCfg(); err != nil { 3329 c.log.Errorf("RecoverWallet: unable to get recovery config: %v", err) 3330 } else { 3331 // merge recoveryCfg with dbWallet.Settings 3332 for key, val := range recoveryCfg { 3333 dbWallet.Settings[key] = val 3334 } 3335 } 3336 oldWallet.Disconnect() // wallet now shut down and w.hookedUp == false -> connected() returns false 3337 } 3338 // Before we pull the plug, remove the wallet from wallets map. Otherwise, 3339 // connectedWallet would try to connect it. 3340 c.removeWallet(assetID) 3341 3342 if err = recoverer.Move(c.assetDataBackupDirectory(assetID)); err != nil { 3343 return fmt.Errorf("failed to move wallet data to backup folder: %w", err) 3344 } 3345 3346 if err = asset.CreateWallet(assetID, &asset.CreateWalletParams{ 3347 Type: dbWallet.Type, 3348 Seed: seed, 3349 Pass: pw, 3350 Settings: dbWallet.Settings, 3351 DataDir: c.assetDataDirectory(assetID), 3352 Net: c.net, 3353 Logger: c.log.SubLogger(unbip(assetID)), 3354 }); err != nil { 3355 return fmt.Errorf("error creating wallet: %w", err) 3356 } 3357 3358 newWallet, err := c.loadWallet(dbWallet) 3359 if err != nil { 3360 return newError(walletErr, "error loading wallet for %d -> %s: %w", 3361 assetID, unbip(assetID), err) 3362 } 3363 3364 // Ensure we are not trying to connect to a disabled wallet. 3365 if newWallet.isDisabled() { 3366 c.updateWallet(assetID, newWallet) 3367 } else { 3368 _, err = c.connectWallet(newWallet) 3369 if err != nil { 3370 return err 3371 } 3372 c.updateWalletBalance(newWallet) 3373 3374 c.updateAssetWalletRefs(newWallet) 3375 3376 err = c.unlockWallet(crypter, newWallet) 3377 if err != nil { 3378 return err 3379 } 3380 } 3381 3382 c.notify(newWalletStateNote(newWallet.state())) 3383 3384 return nil 3385 } 3386 3387 // OpenWallet opens (unlocks) the wallet for use. 3388 func (c *Core) OpenWallet(assetID uint32, appPW []byte) error { 3389 crypter, err := c.encryptionKey(appPW) 3390 if err != nil { 3391 return err 3392 } 3393 defer crypter.Close() 3394 wallet, err := c.connectedWallet(assetID) 3395 if err != nil { 3396 return fmt.Errorf("OpenWallet: wallet not found for %d -> %s: %w", assetID, unbip(assetID), err) 3397 } 3398 err = c.unlockWallet(crypter, wallet) 3399 if err != nil { 3400 return newError(walletAuthErr, "failed to unlock %s wallet: %w", unbip(assetID), err) 3401 } 3402 3403 state := wallet.state() 3404 balances, err := c.updateWalletBalance(wallet) 3405 if err != nil { 3406 return err 3407 } 3408 c.log.Infof("Connected to and unlocked %s wallet. Balance available "+ 3409 "= %d / locked = %d / locked in contracts = %d, locked in bonds = %d, Deposit address = %s", 3410 state.Symbol, balances.Available, balances.Locked, balances.ContractLocked, 3411 balances.BondLocked, state.Address) 3412 3413 c.notify(newWalletStateNote(state)) 3414 return nil 3415 } 3416 3417 // CloseWallet closes the wallet for the specified asset. The wallet cannot be 3418 // closed if there are active negotiations for the asset. 3419 func (c *Core) CloseWallet(assetID uint32) error { 3420 if c.isActiveBondAsset(assetID, false) { // unlock not needed for refunds 3421 return fmt.Errorf("%s wallet must remain unlocked for bonding", unbip(assetID)) 3422 } 3423 if c.assetHasActiveOrders(assetID) { 3424 return fmt.Errorf("cannot lock %s wallet with active swap negotiations", unbip(assetID)) 3425 } 3426 wallet, err := c.connectedWallet(assetID) 3427 if err != nil { 3428 return fmt.Errorf("wallet not found for %d -> %s: %w", assetID, unbip(assetID), err) 3429 } 3430 err = wallet.Lock(walletLockTimeout) 3431 if err != nil { 3432 return err 3433 } 3434 3435 c.notify(newWalletStateNote(wallet.state())) 3436 3437 return nil 3438 } 3439 3440 // ConnectWallet connects to the wallet without unlocking. 3441 func (c *Core) ConnectWallet(assetID uint32) error { 3442 wallet, err := c.connectedWallet(assetID) 3443 if err != nil { 3444 return err 3445 } 3446 c.notify(newWalletStateNote(wallet.state())) 3447 return nil 3448 } 3449 3450 // WalletSettings fetches the current wallet configuration details from the 3451 // database. 3452 func (c *Core) WalletSettings(assetID uint32) (map[string]string, error) { 3453 wallet, found := c.wallet(assetID) 3454 if !found { 3455 return nil, newError(missingWalletErr, "%d -> %s wallet not found", assetID, unbip(assetID)) 3456 } 3457 // Get the settings from the database. 3458 dbWallet, err := c.db.Wallet(wallet.dbID) 3459 if err != nil { 3460 return nil, codedError(dbErr, err) 3461 } 3462 return dbWallet.Settings, nil 3463 } 3464 3465 // ChangeAppPass updates the application password to the provided new password 3466 // after validating the current password. 3467 func (c *Core) ChangeAppPass(appPW, newAppPW []byte) error { 3468 // Validate current password. 3469 if len(newAppPW) == 0 { 3470 return fmt.Errorf("application password cannot be empty") 3471 } 3472 creds := c.creds() 3473 if creds == nil { 3474 return fmt.Errorf("no primary credentials. Is the client initialized?") 3475 } 3476 3477 outerCrypter, err := c.reCrypter(appPW, creds.OuterKeyParams) 3478 if err != nil { 3479 return newError(authErr, "old password error: %w", err) 3480 } 3481 defer outerCrypter.Close() 3482 innerKey, err := outerCrypter.Decrypt(creds.EncInnerKey) 3483 if err != nil { 3484 return fmt.Errorf("inner key decryption error: %w", err) 3485 } 3486 3487 return c.changeAppPass(newAppPW, innerKey, creds) 3488 } 3489 3490 // changeAppPass is a shared method to reset or change user password. 3491 func (c *Core) changeAppPass(newAppPW, innerKey []byte, creds *db.PrimaryCredentials) error { 3492 newOuterCrypter := c.newCrypter(newAppPW) 3493 defer newOuterCrypter.Close() 3494 newEncInnerKey, err := newOuterCrypter.Encrypt(innerKey) 3495 if err != nil { 3496 return fmt.Errorf("encryption error: %v", err) 3497 } 3498 3499 newCreds := &db.PrimaryCredentials{ 3500 EncSeed: creds.EncSeed, 3501 EncInnerKey: newEncInnerKey, 3502 InnerKeyParams: creds.InnerKeyParams, 3503 Birthday: creds.Birthday, 3504 OuterKeyParams: newOuterCrypter.Serialize(), 3505 Version: creds.Version, 3506 } 3507 3508 err = c.db.SetPrimaryCredentials(newCreds) 3509 if err != nil { 3510 return fmt.Errorf("SetPrimaryCredentials error: %w", err) 3511 } 3512 3513 c.setCredentials(newCreds) 3514 3515 return nil 3516 } 3517 3518 // ResetAppPass resets the application password to the provided new password. 3519 func (c *Core) ResetAppPass(newPass []byte, seedStr string) (err error) { 3520 if !c.IsInitialized() { 3521 return fmt.Errorf("cannot reset password before client is initialized") 3522 } 3523 3524 if len(newPass) == 0 { 3525 return fmt.Errorf("application password cannot be empty") 3526 } 3527 3528 seed, _, err := decodeSeedString(seedStr) 3529 if err != nil { 3530 return fmt.Errorf("error decoding seed: %w", err) 3531 } 3532 3533 creds := c.creds() 3534 if creds == nil { 3535 return fmt.Errorf("no credentials stored") 3536 } 3537 3538 innerKey := seedInnerKey(seed) 3539 _, err = c.reCrypter(innerKey[:], creds.InnerKeyParams) 3540 if err != nil { 3541 c.log.Errorf("Error reseting password with seed: %v", err) 3542 return errors.New("incorrect seed") 3543 } 3544 3545 return c.changeAppPass(newPass, innerKey[:], creds) 3546 } 3547 3548 // ReconfigureWallet updates the wallet configuration settings, it also updates 3549 // the password if newWalletPW is non-nil. Do not make concurrent calls to 3550 // ReconfigureWallet for the same asset. 3551 func (c *Core) ReconfigureWallet(appPW, newWalletPW []byte, form *WalletForm) error { 3552 crypter, err := c.encryptionKey(appPW) 3553 if err != nil { 3554 return newError(authErr, "ReconfigureWallet password error: %w", err) 3555 } 3556 defer crypter.Close() 3557 3558 assetID := form.AssetID 3559 3560 walletDef, err := asset.WalletDef(assetID, form.Type) 3561 if err != nil { 3562 return newError(assetSupportErr, "asset.WalletDef error: %w", err) 3563 } 3564 if walletDef.Seeded && newWalletPW != nil { 3565 return newError(passwordErr, "cannot set a password on a built-in(seeded) wallet") 3566 } 3567 3568 oldWallet, found := c.wallet(assetID) 3569 if !found { 3570 return newError(missingWalletErr, "%d -> %s wallet not found", 3571 assetID, unbip(assetID)) 3572 } 3573 3574 if oldWallet.isDisabled() { // disabled wallet cannot perform operation. 3575 return fmt.Errorf(walletDisabledErrStr, strings.ToUpper(unbip(assetID))) 3576 } 3577 3578 oldDef, err := asset.WalletDef(assetID, oldWallet.walletType) 3579 if err != nil { 3580 return newError(assetSupportErr, "old wallet asset.WalletDef error: %w", err) 3581 } 3582 oldDepositAddr := oldWallet.currentDepositAddress() 3583 3584 dbWallet := &db.Wallet{ 3585 Type: form.Type, 3586 AssetID: oldWallet.AssetID, 3587 Settings: form.Config, 3588 Balance: &db.Balance{}, // in case retrieving new balance after connect fails 3589 EncryptedPW: oldWallet.encPW(), 3590 Address: oldDepositAddr, 3591 } 3592 3593 storeWithBalance := func(w *xcWallet, dbWallet *db.Wallet) error { 3594 balances, err := c.walletBalance(w) 3595 if err != nil { 3596 c.log.Warnf("Error getting balance for wallet %s: %v", unbip(assetID), err) 3597 // Do not fail in case this requires an unlocked wallet. 3598 } else { 3599 w.setBalance(balances) // update xcWallet's WalletBalance 3600 dbWallet.Balance = balances.Balance // store the db.Balance 3601 } 3602 3603 err = c.db.UpdateWallet(dbWallet) 3604 if err != nil { 3605 return newError(dbErr, "error saving wallet configuration: %w", err) 3606 } 3607 3608 c.notify(newBalanceNote(assetID, balances)) // redundant with wallet config note? 3609 subject, details := c.formatDetails(TopicWalletConfigurationUpdated, unbip(assetID), w.address) 3610 c.notify(newWalletConfigNote(TopicWalletConfigurationUpdated, subject, details, db.Success, w.state())) 3611 3612 return nil 3613 } 3614 3615 clearTickGovernors := func() { 3616 for _, dc := range c.dexConnections() { 3617 for _, t := range dc.trackedTrades() { 3618 if t.Base() != assetID && t.Quote() != assetID { 3619 continue 3620 } 3621 isFromAsset := t.wallets.fromWallet.AssetID == assetID 3622 t.mtx.RLock() 3623 for _, m := range t.matches { // maybe range t.activeMatches() 3624 m.exceptionMtx.Lock() 3625 if m.tickGovernor != nil && 3626 ((m.suspectSwap && isFromAsset) || (m.suspectRedeem && !isFromAsset)) { 3627 3628 m.tickGovernor.Stop() 3629 m.tickGovernor = nil 3630 } 3631 m.exceptionMtx.Unlock() 3632 } 3633 t.mtx.RUnlock() 3634 } 3635 } 3636 } 3637 3638 // See if the wallet offers a quick path. 3639 if configurer, is := oldWallet.Wallet.(asset.LiveReconfigurer); is && oldWallet.walletType == walletDef.Type && oldWallet.connected() { 3640 form.Config[asset.SpecialSettingActivelyUsed] = strconv.FormatBool(c.assetHasActiveOrders(dbWallet.AssetID)) 3641 defer delete(form.Config, asset.SpecialSettingActivelyUsed) 3642 3643 if restart, err := configurer.Reconfigure(c.ctx, &asset.WalletConfig{ 3644 Type: form.Type, 3645 Settings: form.Config, 3646 DataDir: c.assetDataDirectory(assetID), 3647 }, oldWallet.currentDepositAddress()); err != nil { 3648 return fmt.Errorf("Reconfigure: %v", err) 3649 } else if !restart { 3650 // Config was updated without a need to restart. 3651 if owns, err := oldWallet.OwnsDepositAddress(oldWallet.currentDepositAddress()); err != nil { 3652 return newError(walletErr, "error checking deposit address after live config update: %w", err) 3653 } else if !owns { 3654 if dbWallet.Address, err = oldWallet.refreshDepositAddress(); err != nil { 3655 return newError(newAddrErr, "error refreshing deposit address after live config update: %w", err) 3656 } 3657 } 3658 if !oldDef.Seeded && newWalletPW != nil { 3659 if err = c.setWalletPassword(oldWallet, newWalletPW, crypter); err != nil { 3660 return newError(walletAuthErr, "failed to update password: %v", err) 3661 } 3662 dbWallet.EncryptedPW = oldWallet.encPW() 3663 3664 } 3665 if err = storeWithBalance(oldWallet, dbWallet); err != nil { 3666 return err 3667 } 3668 clearTickGovernors() 3669 c.log.Infof("%s wallet configuration updated without a restart 👍", unbip(assetID)) 3670 return nil 3671 } 3672 } 3673 3674 c.log.Infof("%s wallet configuration update will require a restart", unbip(assetID)) 3675 3676 var restartOnFail bool 3677 3678 defer func() { 3679 if restartOnFail { 3680 if _, err := c.connectWallet(oldWallet); err != nil { 3681 c.log.Errorf("Failed to reconnect wallet after a failed reconfiguration attempt: %v", err) 3682 } 3683 } 3684 }() 3685 3686 if walletDef.Seeded { 3687 exists, err := asset.WalletExists(assetID, form.Type, c.assetDataDirectory(assetID), form.Config, c.net) 3688 if err != nil { 3689 return newError(existenceCheckErr, "error checking wallet pre-existence: %w", err) 3690 } 3691 3692 // The password on a seeded wallet is deterministic, based on the seed 3693 // itself, so if the seeded wallet of this Type for this asset already 3694 // exists, recompute the password from the app seed. 3695 var pw []byte 3696 if exists { 3697 _, pw, err = c.assetSeedAndPass(assetID, crypter) 3698 if err != nil { 3699 return newError(authErr, "error retrieving wallet password: %w", err) 3700 } 3701 } else { 3702 pw, err = c.createSeededWallet(assetID, crypter, form) 3703 if err != nil { 3704 return newError(createWalletErr, "error creating new %q-type %s wallet: %w", form.Type, unbip(assetID), err) 3705 } 3706 } 3707 dbWallet.EncryptedPW, err = crypter.Encrypt(pw) 3708 if err != nil { 3709 return fmt.Errorf("wallet password encryption error: %w", err) 3710 } 3711 3712 if oldDef.Seeded && oldWallet.connected() { 3713 oldWallet.Disconnect() 3714 restartOnFail = true 3715 } 3716 } else if newWalletPW == nil && oldDef.Seeded { 3717 // If we're switching from a seeded wallet to a non-seeded wallet and no 3718 // password was provided, use empty string = wallet not encrypted. 3719 newWalletPW = []byte{} 3720 } 3721 3722 // Reload the wallet with the new settings. 3723 wallet, err := c.loadWallet(dbWallet) 3724 if err != nil { 3725 return newError(walletErr, "error loading wallet for %d -> %s: %w", 3726 assetID, unbip(assetID), err) 3727 } 3728 3729 // Block PeersChange until we know this wallet is ready. 3730 atomic.StoreUint32(wallet.broadcasting, 0) 3731 var success bool 3732 defer func() { 3733 if success { 3734 atomic.StoreUint32(wallet.broadcasting, 1) 3735 c.notify(newWalletStateNote(wallet.state())) 3736 c.walletCheckAndNotify(wallet) 3737 } 3738 }() 3739 3740 // Helper funciton to make sure trades can be settled by the 3741 // keys held within the new wallet. 3742 sameWallet := func() error { 3743 if c.walletIsActive(assetID) { 3744 owns, err := wallet.OwnsDepositAddress(oldDepositAddr) 3745 if err != nil { 3746 return err 3747 } 3748 if !owns { 3749 return errors.New("new wallet in active use does not own the old deposit address. abandoning configuration update") 3750 } 3751 } 3752 return nil 3753 } 3754 3755 reloadWallet := func(w *xcWallet, dbWallet *db.Wallet, checkSameness bool) error { 3756 // Must connect to ensure settings are good. This comes before 3757 // setWalletPassword since it would use connectAndUpdateWallet, which 3758 // performs additional deposit address validation and balance updates that 3759 // are redundant with the rest of this function. 3760 dbWallet.Address, err = c.connectWalletResumeTrades(w, false) 3761 if err != nil { 3762 return fmt.Errorf("connectWallet: %w", err) 3763 } 3764 3765 if checkSameness { 3766 if err := sameWallet(); err != nil { 3767 wallet.Disconnect() 3768 return newError(walletErr, "new wallet cannot be used with current active trades: %w", err) 3769 } 3770 // If newWalletPW is non-nil, update the wallet's password. 3771 if newWalletPW != nil { // includes empty non-nil slice 3772 err = c.setWalletPassword(wallet, newWalletPW, crypter) 3773 if err != nil { 3774 wallet.Disconnect() 3775 return fmt.Errorf("setWalletPassword: %v", err) 3776 } 3777 // Update dbWallet so db.UpdateWallet below reflects the new password. 3778 dbWallet.EncryptedPW = wallet.encPW() 3779 } else if oldWallet.locallyUnlocked() { 3780 // If the password was not changed, carry over any cached password 3781 // regardless of backend lock state. loadWallet already copied encPW, so 3782 // this will decrypt pw rather than actually copying it, and it will 3783 // ensure the backend is also unlocked. 3784 err := wallet.Unlock(crypter) // decrypt encPW if set and unlock the backend 3785 if err != nil { 3786 wallet.Disconnect() 3787 return newError(walletAuthErr, "wallet successfully connected, but failed to unlock. "+ 3788 "reconfiguration not saved: %w", err) 3789 } 3790 } 3791 } 3792 3793 if err = storeWithBalance(w, dbWallet); err != nil { 3794 w.Disconnect() 3795 return err 3796 } 3797 3798 c.updateAssetWalletRefs(w) 3799 // reReserveFunding is likely a no-op because of the walletIsActive check 3800 // above, and because of the way current LiveReconfigurers are implemented. 3801 // For forward compatibility though, if a LiveReconfigurer with active 3802 // orders indicates restart and the new wallet still owns the keys, we can 3803 // end up here and we need to re-reserve. 3804 go c.reReserveFunding(w) 3805 return nil 3806 } 3807 3808 // Reload the wallet 3809 if err := reloadWallet(wallet, dbWallet, true); err != nil { 3810 return err 3811 } 3812 3813 restartOnFail = false 3814 success = true 3815 3816 // If there are tokens, reload those wallets. 3817 for tokenID := range asset.Asset(assetID).Tokens { 3818 tokenWallet, found := c.wallet(tokenID) 3819 if found { 3820 tokenDBWallet, err := c.db.Wallet((&db.Wallet{AssetID: tokenID}).ID()) 3821 if err != nil { 3822 c.log.Errorf("Error getting db wallet for token %s: %w", unbip(tokenID), err) 3823 continue 3824 } 3825 tokenWallet.Disconnect() 3826 tokenWallet, err = c.loadWallet(tokenDBWallet) 3827 if err != nil { 3828 c.log.Errorf("Error loading wallet for token %s: %w", unbip(tokenID), err) 3829 continue 3830 } 3831 if err := reloadWallet(tokenWallet, tokenDBWallet, false); err != nil { 3832 c.log.Errorf("Error reloading token wallet %s: %w", unbip(tokenID), err) 3833 } 3834 } 3835 } 3836 3837 if oldWallet.connected() { 3838 // NOTE: Cannot lock the wallet backend because it may be the same as 3839 // the one just connected. 3840 go oldWallet.Disconnect() 3841 } 3842 3843 clearTickGovernors() 3844 3845 c.resumeTrades(crypter) 3846 3847 return nil 3848 } 3849 3850 // updateAssetWalletRefs sets all references of an asset's wallet to newWallet. 3851 func (c *Core) updateAssetWalletRefs(newWallet *xcWallet) { 3852 assetID := newWallet.AssetID 3853 updateWalletSet := func(t *trackedTrade) { 3854 t.mtx.Lock() 3855 defer t.mtx.Unlock() 3856 3857 if t.wallets.fromWallet.AssetID == assetID { 3858 t.wallets.fromWallet = newWallet 3859 } else if t.wallets.toWallet.AssetID == assetID { 3860 t.wallets.toWallet = newWallet 3861 } else { 3862 return // no need to check base/quote wallet aliases 3863 } 3864 3865 // Also base/quote wallet aliases. The following is more fool-proof and 3866 // concise than nested t.Trade().Sell conditions above: 3867 if t.wallets.baseWallet.AssetID == assetID { 3868 t.wallets.baseWallet = newWallet 3869 } else /* t.wallets.quoteWallet.AssetID == assetID */ { 3870 t.wallets.quoteWallet = newWallet 3871 } 3872 } 3873 3874 for _, dc := range c.dexConnections() { 3875 for _, tracker := range dc.trackedTrades() { 3876 updateWalletSet(tracker) 3877 } 3878 } 3879 3880 c.updateWallet(assetID, newWallet) 3881 } 3882 3883 // SetWalletPassword updates the (encrypted) password for the wallet. Returns 3884 // passwordErr if provided newPW is nil. The wallet will be connected if it is 3885 // not already. 3886 func (c *Core) SetWalletPassword(appPW []byte, assetID uint32, newPW []byte) error { 3887 // Ensure newPW isn't nil. 3888 if newPW == nil { 3889 return newError(passwordErr, "SetWalletPassword password can't be nil") 3890 } 3891 3892 // Check the app password and get the crypter. 3893 crypter, err := c.encryptionKey(appPW) 3894 if err != nil { 3895 return newError(authErr, "SetWalletPassword password error: %w", err) 3896 } 3897 defer crypter.Close() 3898 3899 // Check that the specified wallet exists. 3900 c.walletMtx.Lock() 3901 defer c.walletMtx.Unlock() 3902 wallet, found := c.wallets[assetID] 3903 if !found { 3904 return newError(missingWalletErr, "wallet for %s (%d) is not known", unbip(assetID), assetID) 3905 } 3906 3907 // Set new password, connecting to it if necessary to verify. It is left 3908 // connected since it is in the wallets map. 3909 return c.setWalletPassword(wallet, newPW, crypter) 3910 } 3911 3912 // setWalletPassword updates the (encrypted) password for the wallet. 3913 func (c *Core) setWalletPassword(wallet *xcWallet, newPW []byte, crypter encrypt.Crypter) error { 3914 authenticator, is := wallet.Wallet.(asset.Authenticator) 3915 if !is { // password setting is not supported by wallet. 3916 return newError(passwordErr, "wallet does not support password setting") 3917 } 3918 3919 walletDef, err := asset.WalletDef(wallet.AssetID, wallet.walletType) 3920 if err != nil { 3921 return newError(assetSupportErr, "asset.WalletDef error: %w", err) 3922 } 3923 if walletDef.Seeded || asset.TokenInfo(wallet.AssetID) != nil { 3924 return newError(passwordErr, "cannot set a password on a seeded or token wallet") 3925 } 3926 3927 // Connect if necessary. 3928 wasConnected := wallet.connected() 3929 if !wasConnected { 3930 if err := c.connectAndUpdateWallet(wallet); err != nil { 3931 return newError(connectionErr, "SetWalletPassword connection error: %w", err) 3932 } 3933 } 3934 3935 wasUnlocked := wallet.unlocked() 3936 newPasswordSet := len(newPW) > 0 // excludes empty but non-nil 3937 3938 // Check that the new password works. 3939 if newPasswordSet { 3940 // Encrypt password if it's not an empty string. 3941 encNewPW, err := crypter.Encrypt(newPW) 3942 if err != nil { 3943 return newError(encryptionErr, "encryption error: %w", err) 3944 } 3945 err = authenticator.Unlock(newPW) 3946 if err != nil { 3947 return newError(authErr, 3948 "setWalletPassword unlocking wallet error, is the new password correct?: %w", err) 3949 } 3950 wallet.setEncPW(encNewPW) 3951 } else { 3952 // Test that the wallet is actually good with no password. At present, 3953 // this means the backend either cannot be locked or unlocks with an 3954 // empty password. The following Lock->Unlock cycle but may be required 3955 // to detect a newly-unprotected wallet without reconnecting. We will 3956 // ignore errors in this process as we are discovering the true state. 3957 // check the backend directly, not using the xcWallet 3958 _ = authenticator.Lock() 3959 _ = authenticator.Unlock([]byte{}) 3960 if authenticator.Locked() { 3961 if wasUnlocked { // try to re-unlock the wallet with previous encPW 3962 _ = c.unlockWallet(crypter, wallet) 3963 } 3964 return newError(authErr, "wallet appears to require a password") 3965 } 3966 wallet.setEncPW(nil) 3967 } 3968 3969 err = c.db.SetWalletPassword(wallet.dbID, wallet.encPW()) 3970 if err != nil { 3971 return codedError(dbErr, err) 3972 } 3973 3974 // Re-lock the wallet if it was previously locked. 3975 if !wasUnlocked && newPasswordSet { 3976 if err = wallet.Lock(2 * time.Second); err != nil { 3977 c.log.Warnf("Unable to relock %s wallet: %v", unbip(wallet.AssetID), err) 3978 } 3979 } 3980 3981 // Do not disconnect because the Wallet may not allow reconnection. 3982 3983 subject, details := c.formatDetails(TopicWalletPasswordUpdated, unbip(wallet.AssetID)) 3984 c.notify(newWalletConfigNote(TopicWalletPasswordUpdated, subject, details, db.Success, wallet.state())) 3985 3986 return nil 3987 } 3988 3989 // NewDepositAddress retrieves a new deposit address from the specified asset's 3990 // wallet, saves it to the database, and emits a notification. If the wallet 3991 // does not support generating new addresses, the current address will be 3992 // returned. 3993 func (c *Core) NewDepositAddress(assetID uint32) (string, error) { 3994 w, exists := c.wallet(assetID) 3995 if !exists { 3996 return "", newError(missingWalletErr, "no wallet found for %s", unbip(assetID)) 3997 } 3998 3999 var addr string 4000 if _, ok := w.Wallet.(asset.NewAddresser); ok { 4001 // Retrieve a fresh deposit address. 4002 var err error 4003 addr, err = w.refreshDepositAddress() 4004 if err != nil { 4005 return "", err 4006 } 4007 if err = c.storeDepositAddress(w.dbID, addr); err != nil { 4008 return "", err 4009 } 4010 // Update wallet state in the User data struct and emit a WalletStateNote. 4011 c.notify(newWalletStateNote(w.state())) 4012 } else { 4013 addr = w.address 4014 } 4015 4016 return addr, nil 4017 } 4018 4019 // AutoWalletConfig attempts to load setting from a wallet package's 4020 // asset.WalletInfo.DefaultConfigPath. If settings are not found, an empty map 4021 // is returned. 4022 func (c *Core) AutoWalletConfig(assetID uint32, walletType string) (map[string]string, error) { 4023 walletDef, err := asset.WalletDef(assetID, walletType) 4024 if err != nil { 4025 return nil, newError(assetSupportErr, "asset.WalletDef error: %w", err) 4026 } 4027 4028 if walletDef.DefaultConfigPath == "" { 4029 return nil, fmt.Errorf("no config path found for %s wallet, type %q", unbip(assetID), walletType) 4030 } 4031 4032 settings, err := config.Parse(walletDef.DefaultConfigPath) 4033 c.log.Infof("%d %s configuration settings loaded from file at default location %s", len(settings), unbip(assetID), walletDef.DefaultConfigPath) 4034 if err != nil { 4035 c.log.Debugf("config.Parse could not load settings from default path: %v", err) 4036 return make(map[string]string), nil 4037 } 4038 return settings, nil 4039 } 4040 4041 // tempDexConnection creates an unauthenticated dexConnection. The caller must 4042 // dc.connMaster.Disconnect when done with the connection. 4043 func (c *Core) tempDexConnection(dexAddr string, certI any) (*dexConnection, error) { 4044 host, err := addrHost(dexAddr) 4045 if err != nil { 4046 return nil, newError(addressParseErr, "error parsing address: %w", err) 4047 } 4048 cert, err := parseCert(host, certI, c.net) 4049 if err != nil { 4050 return nil, newError(fileReadErr, "failed to parse certificate: %w", err) 4051 } 4052 4053 c.connMtx.RLock() 4054 _, found := c.conns[host] 4055 c.connMtx.RUnlock() 4056 if found { 4057 return nil, newError(dupeDEXErr, "already registered at %s", dexAddr) 4058 } 4059 4060 // TODO: if a "keyless" (view-only) dex connection exists, this temp 4061 // connection may be used to replace the existing connection and likely 4062 // without (properly) closing the existing connection. Is this OK?? 4063 return c.connectDEXWithFlag(&db.AccountInfo{ 4064 Host: host, 4065 Cert: cert, 4066 BondAsset: defaultBondAsset, 4067 }, connectDEXFlagTemporary) 4068 } 4069 4070 // GetDEXConfig creates a temporary connection to the specified DEX Server and 4071 // fetches the full exchange config. The connection is closed after the config 4072 // is retrieved. An error is returned if user is already registered to the DEX 4073 // since a DEX connection is already established and the config is accessible 4074 // via the User or Exchanges methods. A TLS certificate, certI, can be provided 4075 // as either a string filename, or []byte file contents. 4076 func (c *Core) GetDEXConfig(dexAddr string, certI any) (*Exchange, error) { 4077 dc, err := c.tempDexConnection(dexAddr, certI) 4078 if err != nil { 4079 return nil, err 4080 } 4081 4082 // Since connectDEX succeeded, we have the server config. exchangeInfo is 4083 // guaranteed to return an *Exchange with full asset and market info. 4084 return c.exchangeInfo(dc), nil 4085 } 4086 4087 // AddDEX configures a view-only DEX connection. This allows watching trade 4088 // activity without setting up account keys or communicating account identity 4089 // with the DEX. DiscoverAccount, PostBond may be used to set up a trading 4090 // account for this DEX if required. 4091 func (c *Core) AddDEX(appPW []byte, dexAddr string, certI any) error { 4092 if !c.IsInitialized() { // TODO: Allow adding view-only DEX without init. 4093 return fmt.Errorf("cannot register DEX because app has not been initialized") 4094 } 4095 4096 host, err := addrHost(dexAddr) 4097 if err != nil { 4098 return newError(addressParseErr, "error parsing address: %w", err) 4099 } 4100 4101 cert, err := parseCert(host, certI, c.net) 4102 if err != nil { 4103 return newError(fileReadErr, "failed to parse certificate: %w", err) 4104 } 4105 4106 c.connMtx.RLock() 4107 _, found := c.conns[host] 4108 c.connMtx.RUnlock() 4109 if found { 4110 return newError(dupeDEXErr, "already connected to DEX at %s", dexAddr) 4111 } 4112 4113 dc, err := c.connectDEXWithFlag(&db.AccountInfo{ 4114 Host: host, 4115 Cert: cert, 4116 }, connectDEXFlagViewOnly) 4117 if err != nil { 4118 if dc != nil { 4119 // Stop (re)connect loop, which may be running even if err != nil. 4120 dc.connMaster.Disconnect() 4121 } 4122 return codedError(connectionErr, err) 4123 } 4124 4125 // Close the connection to the dex server if adding the dex fails. 4126 var success bool 4127 defer func() { 4128 if !success { 4129 dc.connMaster.Disconnect() 4130 } 4131 }() 4132 4133 // Don't allow adding another dex with the same pubKey. There can only be 4134 // one dex connection per pubKey. UpdateDEXHost must be called to connect to 4135 // the same dex using a different host name. 4136 exists, host := c.dexWithPubKeyExists(dc.acct.dexPubKey) 4137 if exists { 4138 return newError(dupeDEXErr, "already connected to DEX at %s but with different host name %s", dexAddr, host) 4139 } 4140 4141 err = c.db.CreateAccount(&db.AccountInfo{ 4142 Host: dc.acct.host, 4143 Cert: dc.acct.cert, 4144 DEXPubKey: dc.acct.dexPubKey, 4145 }) 4146 if err != nil { 4147 return fmt.Errorf("error saving account info for view-only DEX: %w", err) 4148 } 4149 4150 success = true 4151 c.connMtx.Lock() 4152 c.conns[dc.acct.host] = dc 4153 c.connMtx.Unlock() 4154 4155 // If a password was provided, try discoverAccount, but OK if we don't find 4156 // it. 4157 if len(appPW) > 0 { 4158 crypter, err := c.encryptionKey(appPW) 4159 if err != nil { 4160 return codedError(passwordErr, err) 4161 } 4162 defer crypter.Close() 4163 4164 paid, err := c.discoverAccount(dc, crypter) 4165 if err != nil { 4166 c.log.Errorf("discoverAccount error during AddDEX: %v", err) 4167 } else if paid { 4168 c.upgradeConnection(dc) 4169 } 4170 } 4171 4172 return nil 4173 } 4174 4175 // dbCreateOrUpdateAccount saves account info to db after an account is 4176 // discovered or registration/postbond completes. 4177 func (c *Core) dbCreateOrUpdateAccount(dc *dexConnection, ai *db.AccountInfo) error { 4178 dc.acct.keyMtx.Lock() 4179 defer dc.acct.keyMtx.Unlock() 4180 4181 if !dc.acct.viewOnly { 4182 return c.db.CreateAccount(ai) 4183 } 4184 4185 err := c.db.UpdateAccountInfo(ai) 4186 if err == nil { 4187 dc.acct.viewOnly = false 4188 } 4189 return err 4190 } 4191 4192 // discoverAccount attempts to identify existing accounts at the connected DEX. 4193 // The dexConnection.acct struct will have its encKey, privKey, and id fields 4194 // set. If the bool is true, the account will have been recorded in the DB, and 4195 // the isPaid and feeCoin fields of the account set. If the bool is false, the 4196 // account is not paid and the user should register. 4197 func (c *Core) discoverAccount(dc *dexConnection, crypter encrypt.Crypter) (bool, error) { 4198 if dc.acct.dexPubKey == nil { 4199 return false, fmt.Errorf("dex server does not support HD key accounts") 4200 } 4201 4202 // Setup our account keys and attempt to authorize with the DEX. 4203 creds := c.creds() 4204 4205 // Start at key index 0 and attempt to authorize accounts until either (1) 4206 // the server indicates the account is not found, and we return paid=false 4207 // to signal a new account should be registered, or (2) an account is found 4208 // that is not suspended, and we return with paid=true after storing the 4209 // discovered account and promoting it to a persistent connection. In this 4210 // process, we will increment the key index and try again whenever the 4211 // connect response indicates a suspended account is found. This instance of 4212 // Core lacks any order or match history for this dex to complete any active 4213 // swaps that might exist for a suspended account, so the user had better 4214 // have another instance with this data if they hope to recover those swaps. 4215 var keyIndex uint32 4216 for { 4217 err := dc.acct.setupCryptoV2(creds, crypter, keyIndex) 4218 if err != nil { 4219 return false, newError(acctKeyErr, "setupCryptoV2 error: %w", err) 4220 } 4221 4222 // Discover the account by attempting a 'connect' (authorize) request. 4223 err = c.authDEX(dc) 4224 if err != nil { 4225 var mErr *msgjson.Error 4226 if errors.As(err, &mErr) && (mErr.Code == msgjson.AccountNotFoundError || 4227 mErr.Code == msgjson.UnpaidAccountError) { 4228 if mErr.Code == msgjson.UnpaidAccountError { 4229 c.log.Warnf("Detected existing but unpaid account! Register " + 4230 "with the same credentials to complete registration with " + 4231 "the previously-assigned fee address and asset ID.") 4232 } 4233 return false, nil // all good, just go register/postbond now 4234 } 4235 return false, newError(authErr, "unexpected authDEX error: %w", err) 4236 } 4237 4238 // skip key if account cannot be used to trade, i.e. tier < 0 or tier == 4239 // 0 but server doesn't support bonds. If tier == 0 and server supports 4240 // bonds, a bond must be posted before the account can be used to trade, 4241 // but generating a new key isn't necessary. 4242 4243 // DRAFT NOTE: This was wrong? Isn't account suspended at tier 0? 4244 // cannotTrade := dc.acct.effectiveTier < 0 || (dc.acct.effectiveTier == 0 && dc.apiVersion() < serverdex.BondAPIVersion) 4245 rep := dc.acct.rep 4246 // Using <= though acknowledging that this ignores the possibility that 4247 // an existing revoked bond could be resurrected. 4248 if rep.BondedTier <= int64(rep.Penalties) { 4249 dc.acct.unAuth() // acct was marked as authenticated by authDEX above. 4250 c.log.Infof("HD account key for %s has tier %d, but %d penalties (not able to trade). Deriving another account key.", 4251 dc.acct.host, rep.BondedTier, rep.Penalties) 4252 time.Sleep(200 * time.Millisecond) // don't hammer 4253 keyIndex++ 4254 continue 4255 } 4256 4257 break // great, the account at this key index exists 4258 } 4259 4260 err := c.dbCreateOrUpdateAccount(dc, &db.AccountInfo{ 4261 Host: dc.acct.host, 4262 Cert: dc.acct.cert, 4263 DEXPubKey: dc.acct.dexPubKey, 4264 EncKeyV2: dc.acct.encKey, 4265 Bonds: dc.acct.bonds, // any reported by server 4266 BondAsset: dc.acct.bondAsset, 4267 }) 4268 if err != nil { 4269 return false, fmt.Errorf("error saving restored account: %w", err) 4270 } 4271 4272 return true, nil // great, just stay connected 4273 } 4274 4275 // dexWithPubKeyExists checks whether or not there is a non-disabled account 4276 // for a dex that has pubKey. 4277 func (c *Core) dexWithPubKeyExists(pubKey *secp256k1.PublicKey) (bool, string) { 4278 for _, dc := range c.dexConnections() { 4279 if dc.acct.dexPubKey == nil { 4280 continue 4281 } 4282 4283 if dc.acct.dexPubKey.IsEqual(pubKey) { 4284 return true, dc.acct.host 4285 } 4286 } 4287 4288 return false, "" 4289 } 4290 4291 // upgradeConnection promotes a temporary dex connection and starts listening 4292 // to the messages it receives. 4293 func (c *Core) upgradeConnection(dc *dexConnection) { 4294 if atomic.CompareAndSwapUint32(&dc.reportingConnects, 0, 1) { 4295 c.wg.Add(1) 4296 go c.listen(dc) 4297 go dc.subPriceFeed() 4298 } 4299 c.addDexConnection(dc) 4300 } 4301 4302 // DiscoverAccount fetches the DEX server's config, and if the server supports 4303 // the new deterministic account derivation scheme by providing its public key 4304 // in the config response, DiscoverAccount also checks if the account is already 4305 // paid. If the returned paid value is true, the account is ready for immediate 4306 // use. If paid is false, Register should be used to complete the registration. 4307 // For an older server that does not provide its pubkey in the config response, 4308 // paid will always be false and the user should proceed to use Register. 4309 // 4310 // The purpose of DiscoverAccount is existing account discovery when the client 4311 // has been restored from seed. As such, DiscoverAccount is not strictly necessary 4312 // to register on a DEX, and Register may be called directly, although it requires 4313 // the expected fee amount as an additional input and it will pay the fee if the 4314 // account is not discovered and paid. 4315 // 4316 // The Tier and BondsPending fields may be consulted to determine if it is still 4317 // necessary to PostBond (i.e. Tier == 0 && !BondsPending) before trading. The 4318 // Connected field should be consulted first. 4319 func (c *Core) DiscoverAccount(dexAddr string, appPW []byte, certI any) (*Exchange, bool, error) { 4320 if !c.IsInitialized() { 4321 return nil, false, fmt.Errorf("cannot register DEX because app has not been initialized") 4322 } 4323 4324 host, err := addrHost(dexAddr) 4325 if err != nil { 4326 return nil, false, newError(addressParseErr, "error parsing address: %w", err) 4327 } 4328 4329 crypter, err := c.encryptionKey(appPW) 4330 if err != nil { 4331 return nil, false, codedError(passwordErr, err) 4332 } 4333 defer crypter.Close() 4334 4335 c.connMtx.RLock() 4336 dc, existingConn := c.conns[host] 4337 c.connMtx.RUnlock() 4338 if existingConn && !dc.acct.isViewOnly() { 4339 // Already registered, but connection may be down and/or PostBond needed. 4340 return c.exchangeInfo(dc), true, nil // *Exchange has Tier and BondsPending 4341 } 4342 4343 var ready bool 4344 if !existingConn { 4345 dc, err = c.tempDexConnection(host, certI) 4346 if err != nil { 4347 return nil, false, err 4348 } 4349 4350 defer func() { 4351 // Either disconnect or promote this connection. 4352 if !ready { 4353 dc.connMaster.Disconnect() 4354 return 4355 } 4356 4357 c.upgradeConnection(dc) 4358 }() 4359 } 4360 4361 // Older DEX server. We won't allow registering without an HD account key, 4362 // but discovery can conclude we do not have an HD account with this DEX. 4363 if dc.acct.dexPubKey == nil { 4364 return c.exchangeInfo(dc), false, nil 4365 } 4366 4367 // Don't allow registering for another dex with the same pubKey. There can only 4368 // be one dex connection per pubKey. UpdateDEXHost must be called to connect to 4369 // the same dex using a different host name. 4370 if !existingConn { 4371 exists, host := c.dexWithPubKeyExists(dc.acct.dexPubKey) 4372 if exists { 4373 return nil, false, 4374 fmt.Errorf("the dex at %v is the same dex as %v. Use Update Host to switch host names", host, dexAddr) 4375 } 4376 } 4377 4378 // Setup our account keys and attempt to authorize with the DEX. 4379 paid, err := c.discoverAccount(dc, crypter) 4380 if err != nil { 4381 return nil, false, err 4382 } 4383 if !paid { 4384 return c.exchangeInfo(dc), false, nil // all good, just go register or postbond now 4385 } 4386 4387 ready = true // do not disconnect 4388 4389 return c.exchangeInfo(dc), true, nil 4390 } 4391 4392 // IsInitialized checks if the app is already initialized. 4393 func (c *Core) IsInitialized() bool { 4394 c.credMtx.RLock() 4395 defer c.credMtx.RUnlock() 4396 return c.credentials != nil 4397 } 4398 4399 // InitializeClient sets the initial app-wide password and app seed for the 4400 // client. The seed argument should be left nil unless restoring from seed. 4401 func (c *Core) InitializeClient(pw []byte, restorationSeed *string) (string, error) { 4402 if c.IsInitialized() { 4403 return "", fmt.Errorf("already initialized, login instead") 4404 } 4405 4406 _, creds, mnemonicSeed, err := c.generateCredentials(pw, restorationSeed) 4407 if err != nil { 4408 return "", err 4409 } 4410 4411 err = c.db.SetPrimaryCredentials(creds) 4412 if err != nil { 4413 return "", fmt.Errorf("SetPrimaryCredentials error: %w", err) 4414 } 4415 4416 freshSeed := restorationSeed == nil 4417 if freshSeed { 4418 now := uint64(time.Now().Unix()) 4419 err = c.db.SetSeedGenerationTime(now) 4420 if err != nil { 4421 return "", fmt.Errorf("SetSeedGenerationTime error: %w", err) 4422 } 4423 c.seedGenerationTime = now 4424 4425 subject, details := c.formatDetails(TopicSeedNeedsSaving) 4426 c.notify(newSecurityNote(TopicSeedNeedsSaving, subject, details, db.Success)) 4427 } 4428 4429 c.setCredentials(creds) 4430 return mnemonicSeed, nil 4431 } 4432 4433 // ExportSeed exports the application seed. 4434 func (c *Core) ExportSeed(pw []byte) (seedStr string, err error) { 4435 crypter, err := c.encryptionKey(pw) 4436 if err != nil { 4437 return "", fmt.Errorf("ExportSeed password error: %w", err) 4438 } 4439 defer crypter.Close() 4440 4441 creds := c.creds() 4442 if creds == nil { 4443 return "", fmt.Errorf("no v2 credentials stored") 4444 } 4445 4446 seed, err := crypter.Decrypt(creds.EncSeed) 4447 if err != nil { 4448 return "", fmt.Errorf("app seed decryption error: %w", err) 4449 } 4450 4451 if len(seed) == legacySeedLength { 4452 seedStr = hex.EncodeToString(seed) 4453 } else { 4454 seedStr, err = mnemonic.GenerateMnemonic(seed, creds.Birthday) 4455 if err != nil { 4456 return "", fmt.Errorf("error generating mnemonic: %w", err) 4457 } 4458 } 4459 4460 return seedStr, nil 4461 } 4462 4463 func decodeSeedString(seedStr string) (seed []byte, bday time.Time, err error) { 4464 // See if it decodes as a mnemonic seed first. 4465 seed, bday, err = mnemonic.DecodeMnemonic(seedStr) 4466 if err != nil { 4467 // Is it an old-school hex seed? 4468 bday = time.Time{} 4469 seed, err = hex.DecodeString(strings.Join(strings.Fields(seedStr), "")) 4470 if err != nil { 4471 return nil, time.Time{}, errors.New("unabled to decode provided seed") 4472 } 4473 if len(seed) != legacySeedLength { 4474 return nil, time.Time{}, errors.New("decoded seed is wrong length") 4475 } 4476 } 4477 return 4478 } 4479 4480 // generateCredentials generates a new set of *PrimaryCredentials. The 4481 // credentials are not stored to the database. A restoration seed can be 4482 // provided, otherwise should be nil. 4483 func (c *Core) generateCredentials(pw []byte, optionalSeed *string) (encrypt.Crypter, *db.PrimaryCredentials, string, error) { 4484 if len(pw) == 0 { 4485 return nil, nil, "", fmt.Errorf("empty password not allowed") 4486 } 4487 4488 var seed []byte 4489 defer encode.ClearBytes(seed) 4490 var bday time.Time 4491 var mnemonicSeed string 4492 if optionalSeed == nil { 4493 bday = time.Now() 4494 seed, mnemonicSeed = mnemonic.New() 4495 } else { 4496 var err error 4497 // Is it a mnemonic seed? 4498 seed, bday, err = decodeSeedString(*optionalSeed) 4499 if err != nil { 4500 return nil, nil, "", err 4501 } 4502 } 4503 4504 // Generate an inner key and it's Crypter. 4505 innerKey := seedInnerKey(seed) 4506 innerCrypter := c.newCrypter(innerKey[:]) 4507 encSeed, err := innerCrypter.Encrypt(seed) 4508 if err != nil { 4509 return nil, nil, "", fmt.Errorf("client seed encryption error: %w", err) 4510 } 4511 4512 // Generate the outer key. 4513 outerCrypter := c.newCrypter(pw) 4514 encInnerKey, err := outerCrypter.Encrypt(innerKey[:]) 4515 if err != nil { 4516 return nil, nil, "", fmt.Errorf("inner key encryption error: %w", err) 4517 } 4518 4519 creds := &db.PrimaryCredentials{ 4520 EncSeed: encSeed, 4521 EncInnerKey: encInnerKey, 4522 InnerKeyParams: innerCrypter.Serialize(), 4523 OuterKeyParams: outerCrypter.Serialize(), 4524 Birthday: bday, 4525 Version: 1, 4526 } 4527 4528 return innerCrypter, creds, mnemonicSeed, nil 4529 } 4530 4531 func seedInnerKey(seed []byte) []byte { 4532 // keyParam is a domain-specific value to ensure the resulting key is unique 4533 // for the specific use case of deriving an inner encryption key from the 4534 // seed. Any other uses of derivation from the seed should similarly create 4535 // their own domain-specific value to ensure uniqueness. 4536 // 4537 // It is equal to BLAKE-256([]byte("DCRDEX-InnerKey-v0")). 4538 keyParam := [32]byte{ 4539 0x75, 0x25, 0xb1, 0xb6, 0x53, 0x33, 0x9e, 0x33, 4540 0xbe, 0x11, 0x61, 0x45, 0x1a, 0x88, 0x6f, 0x37, 4541 0xe7, 0x74, 0xdf, 0xca, 0xb4, 0x8a, 0xee, 0x0e, 4542 0x7c, 0x84, 0x60, 0x01, 0xed, 0xe5, 0xf6, 0x97, 4543 } 4544 key := make([]byte, len(seed)+len(keyParam)) 4545 copy(key, seed) 4546 copy(key[len(seed):], keyParam[:]) 4547 innerKey := blake256.Sum256(key) 4548 return innerKey[:] 4549 } 4550 4551 func (c *Core) bondKeysReady() bool { 4552 c.loginMtx.Lock() 4553 defer c.loginMtx.Unlock() 4554 return c.bondXPriv != nil && c.bondXPriv.IsPrivate() // infer not Zeroed via IsPrivate 4555 } 4556 4557 // Login logs the user in. On the first login after startup or after a logout, 4558 // this function will connect wallets, resolve active trades, and decrypt 4559 // account keys for all known DEXes. Otherwise, it will only check whether or 4560 // not the app pass is correct. 4561 func (c *Core) Login(pw []byte) error { 4562 // Make sure the app has been initialized. This condition would error when 4563 // attempting to retrieve the encryption key below as well, but the 4564 // messaging may be confusing. 4565 c.credMtx.RLock() 4566 creds := c.credentials 4567 c.credMtx.RUnlock() 4568 4569 if creds == nil { 4570 return fmt.Errorf("cannot log in because app has not been initialized") 4571 } 4572 4573 c.notify(newLoginNote("Verifying credentials...")) 4574 if len(creds.EncInnerKey) == 0 { 4575 err := c.initializePrimaryCredentials(pw, creds.OuterKeyParams) 4576 if err != nil { 4577 // It's tempting to panic here, since Core and the db are probably 4578 // out of sync and the client shouldn't be doing anything else. 4579 c.log.Criticalf("v1 upgrade failed: %v", err) 4580 return err 4581 } 4582 } 4583 4584 crypter, err := c.encryptionKey(pw) 4585 if err != nil { 4586 return err 4587 } 4588 defer crypter.Close() 4589 4590 switch creds.Version { 4591 case 0: 4592 if crypter, creds, err = c.upgradeV0CredsToV1(pw, *creds); err != nil { 4593 return fmt.Errorf("error upgrading primary credentials from version 0 to 1: %w", err) 4594 } 4595 } 4596 4597 login := func() (needInit bool, err error) { 4598 c.loginMtx.Lock() 4599 defer c.loginMtx.Unlock() 4600 if !c.loggedIn { 4601 // Derive the bond extended key from the seed. 4602 seed, err := crypter.Decrypt(creds.EncSeed) 4603 if err != nil { 4604 return false, fmt.Errorf("seed decryption error: %w", err) 4605 } 4606 defer encode.ClearBytes(seed) 4607 c.bondXPriv, err = deriveBondXPriv(seed) 4608 if err != nil { 4609 return false, fmt.Errorf("GenDeepChild error: %w", err) 4610 } 4611 c.loggedIn = true 4612 return true, nil 4613 } 4614 return false, nil 4615 } 4616 4617 if needsInit, err := login(); err != nil { 4618 return err 4619 } else if needsInit { 4620 // It is not an error if we can't connect, unless we need the wallet 4621 // for active trades, but that condition is checked later in 4622 // resolveActiveTrades. We won't try to unlock here, but if the wallet 4623 // is needed for active trades, it will be unlocked in resolveActiveTrades 4624 // and the balance updated there. 4625 c.notify(newLoginNote("Connecting wallets...")) 4626 c.connectWallets(crypter) // initialize reserves 4627 c.notify(newLoginNote("Resuming active trades...")) 4628 c.resolveActiveTrades(crypter) 4629 c.notify(newLoginNote("Connecting to DEX servers...")) 4630 c.initializeDEXConnections(crypter) 4631 4632 } 4633 4634 return nil 4635 } 4636 4637 // upgradeV0CredsToV1 upgrades version 0 credentials to version 1. This update 4638 // changes the inner key to be derived from the seed. 4639 func (c *Core) upgradeV0CredsToV1(appPW []byte, creds db.PrimaryCredentials) (encrypt.Crypter, *db.PrimaryCredentials, error) { 4640 outerCrypter, err := c.reCrypter(appPW, creds.OuterKeyParams) 4641 if err != nil { 4642 return nil, nil, fmt.Errorf("app password error: %w", err) 4643 } 4644 innerKey, err := outerCrypter.Decrypt(creds.EncInnerKey) 4645 if err != nil { 4646 return nil, nil, fmt.Errorf("inner key decryption error: %w", err) 4647 } 4648 innerCrypter, err := c.reCrypter(innerKey, creds.InnerKeyParams) 4649 if err != nil { 4650 return nil, nil, fmt.Errorf("inner key deserialization error: %w", err) 4651 } 4652 seed, err := innerCrypter.Decrypt(creds.EncSeed) 4653 if err != nil { 4654 return nil, nil, fmt.Errorf("app seed decryption error: %w", err) 4655 } 4656 4657 // Update all the fields. 4658 newInnerKey := seedInnerKey(seed) 4659 newInnerCrypter := c.newCrypter(newInnerKey[:]) 4660 creds.Version = 1 4661 creds.InnerKeyParams = newInnerCrypter.Serialize() 4662 if creds.EncSeed, err = newInnerCrypter.Encrypt(seed); err != nil { 4663 return nil, nil, fmt.Errorf("error encrypting version 1 seed: %w", err) 4664 } 4665 if creds.EncInnerKey, err = outerCrypter.Encrypt(newInnerKey[:]); err != nil { 4666 return nil, nil, fmt.Errorf("error encrypting version 1 inner key: %w", err) 4667 } 4668 if err := c.recrypt(&creds, innerCrypter, newInnerCrypter); err != nil { 4669 return nil, nil, fmt.Errorf("recrypt error during v0 -> v1 credentials upgrade: %w", err) 4670 } 4671 4672 c.log.Infof("Upgraded to version 1 credentials") 4673 return newInnerCrypter, &creds, nil 4674 } 4675 4676 // connectWallets attempts to connect to and retrieve balance from all known 4677 // wallets. This should be done only ONCE on Login. 4678 func (c *Core) connectWallets(crypter encrypt.Crypter) { 4679 var wg sync.WaitGroup 4680 var connectCount uint32 4681 connectWallet := func(wallet *xcWallet) { 4682 defer wg.Done() 4683 // Return early if wallet is disabled. 4684 if wallet.isDisabled() { 4685 return 4686 } 4687 if !wallet.connected() { 4688 err := c.connectAndUpdateWallet(wallet) 4689 if err != nil { 4690 c.log.Errorf("Unable to connect to %s wallet (start and sync wallets BEFORE starting dex!): %v", 4691 unbip(wallet.AssetID), err) 4692 // NOTE: Details for this topic is in the context of fee 4693 // payment, but the subject pertains to a failure to connect 4694 // to the wallet. 4695 subject, _ := c.formatDetails(TopicWalletConnectionWarning) 4696 c.notify(newWalletConfigNote(TopicWalletConnectionWarning, subject, err.Error(), 4697 db.ErrorLevel, wallet.state())) 4698 return 4699 } 4700 if mw, is := wallet.Wallet.(asset.FundsMixer); is { 4701 startMixing := func() error { 4702 stats, err := mw.FundsMixingStats() 4703 if err != nil { 4704 return fmt.Errorf("error checking %s wallet mixing stats: %v", unbip(wallet.AssetID), err) 4705 } 4706 // If the wallet has no funds to transfer to the default account 4707 // and mixing is not enabled unlocking is not required. 4708 if !stats.Enabled && stats.MixedFunds == 0 && stats.TradingFunds == 0 { 4709 return nil 4710 } 4711 // Unlocking is required for mixing or to move funds if mixing 4712 // was recently turned off without funds being moved yet. 4713 if err := c.connectAndUnlock(crypter, wallet); err != nil { 4714 return fmt.Errorf("error unlocking %s wallet for mixing: %v", unbip(wallet.AssetID), err) 4715 } 4716 if err := mw.ConfigureFundsMixer(stats.Enabled); err != nil { 4717 return fmt.Errorf("error starting %s wallet mixing: %v", unbip(wallet.AssetID), err) 4718 } 4719 return nil 4720 } 4721 if err := startMixing(); err != nil { 4722 c.log.Errorf("Failed to start or stop mixing: %v", err) 4723 } 4724 } 4725 if c.cfg.UnlockCoinsOnLogin { 4726 if err = wallet.ReturnCoins(nil); err != nil { 4727 c.log.Errorf("Failed to unlock all %s wallet coins: %v", unbip(wallet.AssetID), err) 4728 } 4729 } 4730 } 4731 atomic.AddUint32(&connectCount, 1) 4732 } 4733 wallets := c.xcWallets() 4734 walletCount := len(wallets) 4735 var tokenWallets []*xcWallet 4736 4737 for _, wallet := range wallets { 4738 if asset.TokenInfo(wallet.AssetID) != nil { 4739 tokenWallets = append(tokenWallets, wallet) 4740 continue 4741 } 4742 wg.Add(1) 4743 go connectWallet(wallet) 4744 } 4745 wg.Wait() 4746 4747 for _, wallet := range tokenWallets { 4748 wg.Add(1) 4749 go connectWallet(wallet) 4750 } 4751 wg.Wait() 4752 4753 if walletCount > 0 { 4754 c.log.Infof("Connected to %d of %d wallets.", connectCount, walletCount) 4755 } 4756 } 4757 4758 // Notifications loads the latest notifications from the db. 4759 func (c *Core) Notifications(n int) (notes, pokes []*db.Notification, _ error) { 4760 notes, err := c.db.NotificationsN(n) 4761 if err != nil { 4762 return nil, nil, fmt.Errorf("error getting notifications: %w", err) 4763 } 4764 return notes, c.pokes(), nil 4765 } 4766 4767 // pokes returns a time-ordered copy of the pokes cache. 4768 func (c *Core) pokes() []*db.Notification { 4769 return c.pokesCache.pokes() 4770 } 4771 4772 func (c *Core) recrypt(creds *db.PrimaryCredentials, oldCrypter, newCrypter encrypt.Crypter) error { 4773 walletUpdates, acctUpdates, err := c.db.Recrypt(creds, oldCrypter, newCrypter) 4774 if err != nil { 4775 return err 4776 } 4777 4778 c.setCredentials(creds) 4779 4780 for assetID, newEncPW := range walletUpdates { 4781 w, found := c.wallet(assetID) 4782 if !found { 4783 c.log.Errorf("no wallet found for v1 upgrade asset ID %d", assetID) 4784 continue 4785 } 4786 w.setEncPW(newEncPW) 4787 } 4788 4789 for host, newEncKey := range acctUpdates { 4790 dc, _, err := c.dex(host) 4791 if err != nil { 4792 c.log.Warnf("no %s dexConnection to update", host) 4793 continue 4794 } 4795 acct := dc.acct 4796 acct.keyMtx.Lock() 4797 acct.encKey = newEncKey 4798 acct.keyMtx.Unlock() 4799 } 4800 4801 return nil 4802 } 4803 4804 // initializePrimaryCredentials sets the PrimaryCredential fields after the DB 4805 // upgrade. 4806 func (c *Core) initializePrimaryCredentials(pw []byte, oldKeyParams []byte) error { 4807 oldCrypter, err := c.reCrypter(pw, oldKeyParams) 4808 if err != nil { 4809 return fmt.Errorf("legacy encryption key deserialization error: %w", err) 4810 } 4811 4812 newCrypter, creds, _, err := c.generateCredentials(pw, nil) 4813 if err != nil { 4814 return err 4815 } 4816 4817 if err := c.recrypt(creds, oldCrypter, newCrypter); err != nil { 4818 return err 4819 } 4820 4821 subject, details := c.formatDetails(TopicUpgradedToSeed) 4822 c.notify(newSecurityNote(TopicUpgradedToSeed, subject, details, db.WarningLevel)) 4823 return nil 4824 } 4825 4826 // ActiveOrders returns a map of host to all of their active orders from db if 4827 // core is not yet logged in or from loaded trades map if core is logged in. 4828 // Inflight orders are also returned for all dex servers if any. 4829 func (c *Core) ActiveOrders() (map[string][]*Order, map[string][]*InFlightOrder, error) { 4830 c.loginMtx.Lock() 4831 loggedIn := c.loggedIn 4832 c.loginMtx.Unlock() 4833 4834 dexInflightOrders := make(map[string][]*InFlightOrder) 4835 dexActiveOrders := make(map[string][]*Order) 4836 for _, dc := range c.dexConnections() { 4837 if loggedIn { 4838 orders, inflight := dc.activeOrders() 4839 dexActiveOrders[dc.acct.host] = append(dexActiveOrders[dc.acct.host], orders...) 4840 dexInflightOrders[dc.acct.host] = append(dexInflightOrders[dc.acct.host], inflight...) 4841 continue 4842 } 4843 4844 // Not logged in, load from db orders. 4845 ords, err := c.dbOrders(dc.acct.host) 4846 if err != nil { 4847 return nil, nil, err 4848 } 4849 4850 for _, ord := range ords { 4851 dexActiveOrders[dc.acct.host] = append(dexActiveOrders[dc.acct.host], coreOrderFromTrade(ord.Order, ord.MetaData)) 4852 } 4853 } 4854 4855 return dexActiveOrders, dexInflightOrders, nil 4856 } 4857 4858 // Active indicates if there are any active orders across all configured 4859 // accounts. This includes booked orders and trades that are settling. 4860 func (c *Core) Active() bool { 4861 for _, dc := range c.dexConnections() { 4862 if dc.hasActiveOrders() { 4863 return true 4864 } 4865 } 4866 return false 4867 } 4868 4869 // Logout logs the user out 4870 func (c *Core) Logout() error { 4871 c.loginMtx.Lock() 4872 defer c.loginMtx.Unlock() 4873 4874 if !c.loggedIn { 4875 return nil 4876 } 4877 4878 // Check active orders 4879 if c.Active() { 4880 return codedError(activeOrdersErr, ActiveOrdersLogoutErr) 4881 } 4882 4883 // Lock wallets 4884 if !c.cfg.NoAutoWalletLock { 4885 // Ensure wallet lock in c.Run waits for c.Logout if this is called 4886 // before shutdown. 4887 c.wg.Add(1) 4888 for _, w := range c.xcWallets() { 4889 if w.connected() && w.unlocked() { 4890 symb := strings.ToUpper(unbip(w.AssetID)) 4891 c.log.Infof("Locking %s wallet", symb) 4892 if err := w.Lock(walletLockTimeout); err != nil { 4893 // A failure to lock the wallet need not block the ability to 4894 // lock the DEX accounts or shutdown Core gracefully. 4895 c.log.Warnf("Unable to lock %v wallet: %v", unbip(w.AssetID), err) 4896 } 4897 } 4898 } 4899 c.wg.Done() 4900 } 4901 4902 // With no open orders for any of the dex connections, and all wallets locked, 4903 // lock each dex account. 4904 for _, dc := range c.dexConnections() { 4905 dc.acct.lock() 4906 } 4907 4908 c.bondXPriv.Zero() 4909 c.bondXPriv = nil 4910 4911 c.loggedIn = false 4912 4913 return nil 4914 } 4915 4916 // Orders fetches a batch of user orders, filtered with the provided 4917 // OrderFilter. 4918 func (c *Core) Orders(filter *OrderFilter) ([]*Order, error) { 4919 var oid order.OrderID 4920 if len(filter.Offset) > 0 { 4921 if len(filter.Offset) != order.OrderIDSize { 4922 return nil, fmt.Errorf("invalid offset order ID length. wanted %d, got %d", order.OrderIDSize, len(filter.Offset)) 4923 } 4924 copy(oid[:], filter.Offset) 4925 } 4926 4927 var mkt *db.OrderFilterMarket 4928 if filter.Market != nil { 4929 mkt = &db.OrderFilterMarket{ 4930 Base: filter.Market.Base, 4931 Quote: filter.Market.Quote, 4932 } 4933 } 4934 4935 ords, err := c.db.Orders(&db.OrderFilter{ 4936 N: filter.N, 4937 Offset: oid, 4938 Hosts: filter.Hosts, 4939 Assets: filter.Assets, 4940 Market: mkt, 4941 Statuses: filter.Statuses, 4942 }) 4943 if err != nil { 4944 return nil, fmt.Errorf("UserOrders error: %w", err) 4945 } 4946 4947 cords := make([]*Order, 0, len(ords)) 4948 for _, mOrd := range ords { 4949 corder, err := c.coreOrderFromMetaOrder(mOrd) 4950 if err != nil { 4951 return nil, err 4952 } 4953 baseWallet, baseOK := c.wallet(corder.BaseID) 4954 quoteWallet, quoteOK := c.wallet(corder.QuoteID) 4955 corder.ReadyToTick = baseOK && baseWallet.connected() && baseWallet.unlocked() && 4956 quoteOK && quoteWallet.connected() && quoteWallet.unlocked() 4957 cords = append(cords, corder) 4958 } 4959 4960 return cords, nil 4961 } 4962 4963 // coreOrderFromMetaOrder creates an *Order from a *db.MetaOrder, including 4964 // loading matches from the database. The order is presumed to be inactive, so 4965 // swap coin confirmations will not be set. For active orders, get the 4966 // *trackedTrade and use the coreOrder method. 4967 func (c *Core) coreOrderFromMetaOrder(mOrd *db.MetaOrder) (*Order, error) { 4968 corder := coreOrderFromTrade(mOrd.Order, mOrd.MetaData) 4969 oid := mOrd.Order.ID() 4970 excludeCancels := false // maybe don't include cancel order matches? 4971 matches, err := c.db.MatchesForOrder(oid, excludeCancels) 4972 if err != nil { 4973 return nil, fmt.Errorf("MatchesForOrder error loading matches for %s: %w", oid, err) 4974 } 4975 corder.Matches = make([]*Match, 0, len(matches)) 4976 for _, match := range matches { 4977 corder.Matches = append(corder.Matches, matchFromMetaMatch(mOrd.Order, match)) 4978 } 4979 return corder, nil 4980 } 4981 4982 // Order fetches a single user order. 4983 func (c *Core) Order(oidB dex.Bytes) (*Order, error) { 4984 oid, err := order.IDFromBytes(oidB) 4985 if err != nil { 4986 return nil, err 4987 } 4988 // See if it's an active order first. 4989 for _, dc := range c.dexConnections() { 4990 tracker, _ := dc.findOrder(oid) 4991 if tracker != nil { 4992 return tracker.coreOrder(), nil 4993 } 4994 } 4995 // Must not be an active order. Get it from the database. 4996 mOrd, err := c.db.Order(oid) 4997 if err != nil { 4998 return nil, fmt.Errorf("error retrieving order %s: %w", oid, err) 4999 } 5000 5001 return c.coreOrderFromMetaOrder(mOrd) 5002 } 5003 5004 // marketWallets gets the 2 *dex.Assets and 2 *xcWallet associated with a 5005 // market. The wallets will be connected, but not necessarily unlocked. 5006 func (c *Core) marketWallets(host string, base, quote uint32) (ba, qa *dex.Asset, bw, qw *xcWallet, err error) { 5007 c.connMtx.RLock() 5008 dc, found := c.conns[host] 5009 c.connMtx.RUnlock() 5010 if !found { 5011 return nil, nil, nil, nil, fmt.Errorf("Unknown host: %s", host) 5012 } 5013 5014 ba, found = dc.assets[base] 5015 if !found { 5016 return nil, nil, nil, nil, fmt.Errorf("%s not supported by %s", unbip(base), host) 5017 } 5018 qa, found = dc.assets[quote] 5019 if !found { 5020 return nil, nil, nil, nil, fmt.Errorf("%s not supported by %s", unbip(quote), host) 5021 } 5022 5023 bw, err = c.connectedWallet(base) 5024 if err != nil { 5025 return nil, nil, nil, nil, fmt.Errorf("%s wallet error: %v", unbip(base), err) 5026 } 5027 qw, err = c.connectedWallet(quote) 5028 if err != nil { 5029 return nil, nil, nil, nil, fmt.Errorf("%s wallet error: %v", unbip(quote), err) 5030 } 5031 return 5032 } 5033 5034 // MaxBuy is the maximum-sized *OrderEstimate for a buy order on the specified 5035 // market. An order rate must be provided, since the number of lots available 5036 // for trading will vary based on the rate for a buy order (unlike a sell 5037 // order). 5038 func (c *Core) MaxBuy(host string, baseID, quoteID uint32, rate uint64) (*MaxOrderEstimate, error) { 5039 baseAsset, quoteAsset, baseWallet, quoteWallet, err := c.marketWallets(host, baseID, quoteID) 5040 if err != nil { 5041 return nil, err 5042 } 5043 5044 dc, _, err := c.dex(host) 5045 if err != nil { 5046 return nil, err 5047 } 5048 5049 mktID := marketName(baseID, quoteID) 5050 mktConf := dc.marketConfig(mktID) 5051 if mktConf == nil { 5052 return nil, newError(marketErr, "unknown market %q", mktID) 5053 } 5054 5055 lotSize := mktConf.LotSize 5056 quoteLotEst := calc.BaseToQuote(rate, lotSize) 5057 if quoteLotEst == 0 { 5058 return nil, fmt.Errorf("quote lot estimate of zero for market %s", mktID) 5059 } 5060 5061 swapFeeSuggestion := c.feeSuggestion(dc, quoteID) 5062 if swapFeeSuggestion == 0 { 5063 return nil, fmt.Errorf("failed to get swap fee suggestion for %s at %s", unbip(quoteID), host) 5064 } 5065 5066 redeemFeeSuggestion := c.feeSuggestionAny(baseID) 5067 if redeemFeeSuggestion == 0 { 5068 return nil, fmt.Errorf("failed to get redeem fee suggestion for %s at %s", unbip(baseID), host) 5069 } 5070 5071 maxBuy, err := quoteWallet.MaxOrder(&asset.MaxOrderForm{ 5072 LotSize: quoteLotEst, 5073 FeeSuggestion: swapFeeSuggestion, 5074 AssetVersion: quoteAsset.Version, // using the server's asset version, when our wallets support multiple vers 5075 MaxFeeRate: quoteAsset.MaxFeeRate, 5076 RedeemVersion: baseAsset.Version, 5077 RedeemAssetID: baseWallet.AssetID, 5078 }) 5079 if err != nil { 5080 return nil, fmt.Errorf("%s wallet MaxOrder error: %v", unbip(quoteID), err) 5081 } 5082 5083 preRedeem, err := baseWallet.PreRedeem(&asset.PreRedeemForm{ 5084 Version: baseAsset.Version, 5085 Lots: maxBuy.Lots, 5086 FeeSuggestion: redeemFeeSuggestion, 5087 }) 5088 if err != nil { 5089 return nil, fmt.Errorf("%s PreRedeem error: %v", unbip(baseID), err) 5090 } 5091 5092 return &MaxOrderEstimate{ 5093 Swap: maxBuy, 5094 Redeem: preRedeem.Estimate, 5095 }, nil 5096 } 5097 5098 // MaxSell is the maximum-sized *OrderEstimate for a sell order on the specified 5099 // market. 5100 func (c *Core) MaxSell(host string, base, quote uint32) (*MaxOrderEstimate, error) { 5101 baseAsset, quoteAsset, baseWallet, quoteWallet, err := c.marketWallets(host, base, quote) 5102 if err != nil { 5103 return nil, err 5104 } 5105 5106 dc, _, err := c.dex(host) 5107 if err != nil { 5108 return nil, err 5109 } 5110 mktID := marketName(base, quote) 5111 mktConf := dc.marketConfig(mktID) 5112 if mktConf == nil { 5113 return nil, newError(marketErr, "unknown market %q", mktID) 5114 } 5115 lotSize := mktConf.LotSize 5116 if lotSize == 0 { 5117 return nil, fmt.Errorf("cannot divide by lot size zero for max sell estimate on market %s", mktID) 5118 } 5119 5120 swapFeeSuggestion := c.feeSuggestion(dc, base) 5121 if swapFeeSuggestion == 0 { 5122 return nil, fmt.Errorf("failed to get swap fee suggestion for %s at %s", unbip(base), host) 5123 } 5124 5125 redeemFeeSuggestion := c.feeSuggestionAny(quote) 5126 if redeemFeeSuggestion == 0 { 5127 return nil, fmt.Errorf("failed to get redeem fee suggestion for %s at %s", unbip(quote), host) 5128 } 5129 5130 maxSell, err := baseWallet.MaxOrder(&asset.MaxOrderForm{ 5131 LotSize: lotSize, 5132 FeeSuggestion: swapFeeSuggestion, 5133 AssetVersion: baseAsset.Version, // using the server's asset version, when our wallets support multiple vers 5134 MaxFeeRate: baseAsset.MaxFeeRate, 5135 RedeemVersion: quoteAsset.Version, 5136 RedeemAssetID: quoteWallet.AssetID, 5137 }) 5138 if err != nil { 5139 return nil, fmt.Errorf("%s wallet MaxOrder error: %v", unbip(base), err) 5140 } 5141 5142 preRedeem, err := quoteWallet.PreRedeem(&asset.PreRedeemForm{ 5143 Version: quoteAsset.Version, 5144 Lots: maxSell.Lots, 5145 FeeSuggestion: redeemFeeSuggestion, 5146 }) 5147 if err != nil { 5148 return nil, fmt.Errorf("%s PreRedeem error: %v", unbip(quote), err) 5149 } 5150 5151 return &MaxOrderEstimate{ 5152 Swap: maxSell, 5153 Redeem: preRedeem.Estimate, 5154 }, nil 5155 } 5156 5157 // initializeDEXConnections connects to the DEX servers in the conns map and 5158 // authenticates the connection. 5159 func (c *Core) initializeDEXConnections(crypter encrypt.Crypter) { 5160 var wg sync.WaitGroup 5161 conns := c.dexConnections() 5162 for _, dc := range conns { 5163 wg.Add(1) 5164 go func(dc *dexConnection) { 5165 defer wg.Done() 5166 c.initializeDEXConnection(dc, crypter) 5167 }(dc) 5168 } 5169 5170 wg.Wait() 5171 } 5172 5173 // initializeDEXConnection connects to the DEX server in the conns map and 5174 // authenticates the connection. 5175 func (c *Core) initializeDEXConnection(dc *dexConnection, crypter encrypt.Crypter) { 5176 if dc.acct.isViewOnly() { 5177 return // don't attempt authDEX for view-only conn 5178 } 5179 5180 // Unlock before checking auth and continuing, because if the user 5181 // logged out and didn't shut down, the account is still authed, but 5182 // locked, and needs unlocked. 5183 err := dc.acct.unlock(crypter) 5184 if err != nil { 5185 subject, details := c.formatDetails(TopicAccountUnlockError, dc.acct.host, err) 5186 c.notify(newFeePaymentNote(TopicAccountUnlockError, subject, details, db.ErrorLevel, dc.acct.host)) // newDEXAuthNote? 5187 return 5188 } 5189 5190 if dc.acct.isDisabled() { 5191 return // For disabled account, we only want dc.acct.unlock above to initialize the account ID. 5192 } 5193 5194 // Unlock the bond wallet if a target tier is set. 5195 if bondAssetID, targetTier, maxBondedAmt := dc.bondOpts(); targetTier > 0 { 5196 c.log.Debugf("Preparing %s wallet to maintain target tier of %d for %v, bonding limit %v", 5197 unbip(bondAssetID), targetTier, dc.acct.host, maxBondedAmt) 5198 wallet, exists := c.wallet(bondAssetID) 5199 if !exists || !wallet.connected() { // connectWallets already run, just fail 5200 subject, details := c.formatDetails(TopicBondWalletNotConnected, unbip(bondAssetID)) 5201 var w *WalletState 5202 if exists { 5203 w = wallet.state() 5204 } 5205 c.notify(newWalletConfigNote(TopicBondWalletNotConnected, subject, details, db.ErrorLevel, w)) 5206 } else if !wallet.unlocked() { 5207 err = wallet.Unlock(crypter) 5208 if err != nil { 5209 subject, details := c.formatDetails(TopicWalletUnlockError, dc.acct.host, err) 5210 c.notify(newFeePaymentNote(TopicWalletUnlockError, subject, details, db.ErrorLevel, dc.acct.host)) 5211 } 5212 } 5213 } 5214 5215 if dc.acct.authed() { // should not be possible with newly idempotent login, but there's AccountImport... 5216 return // authDEX already done 5217 } 5218 5219 // Pending bonds will be handled by authDEX. Expired bonds will be 5220 // refunded by rotateBonds. 5221 5222 // If the connection is down, authDEX will fail on Send. 5223 if dc.IsDown() { 5224 c.log.Warnf("Connection to %v not available for authorization. "+ 5225 "It will automatically authorize when it connects.", dc.acct.host) 5226 subject, details := c.formatDetails(TopicDEXDisconnected, dc.acct.host) 5227 c.notify(newConnEventNote(TopicDEXDisconnected, subject, dc.acct.host, comms.Disconnected, details, db.ErrorLevel)) 5228 return 5229 } 5230 5231 // Authenticate dex connection 5232 err = c.authDEX(dc) 5233 if err != nil { 5234 subject, details := c.formatDetails(TopicDexAuthError, dc.acct.host, err) 5235 c.notify(newDEXAuthNote(TopicDexAuthError, subject, dc.acct.host, false, details, db.ErrorLevel)) 5236 } 5237 } 5238 5239 // resolveActiveTrades loads order and match data from the database. Only active 5240 // orders and orders with active matches are loaded. Also, only active matches 5241 // are loaded, even if there are inactive matches for the same order, but it may 5242 // be desirable to load all matches, so this behavior may change. 5243 func (c *Core) resolveActiveTrades(crypter encrypt.Crypter) { 5244 for _, dc := range c.dexConnections() { 5245 err := c.loadDBTrades(dc) 5246 if err != nil { 5247 c.log.Errorf("failed to load trades from db for dex at %s: %v", dc.acct.host, err) 5248 } 5249 } 5250 5251 // resumeTrades will be a no-op if there are no trades in any 5252 // dexConnection's trades map that is not ready to tick. 5253 c.resumeTrades(crypter) 5254 } 5255 5256 func (c *Core) wait(coinID []byte, assetID uint32, trigger func() (bool, error), action func(error)) { 5257 c.waiterMtx.Lock() 5258 defer c.waiterMtx.Unlock() 5259 c.blockWaiters[coinIDString(assetID, coinID)] = &blockWaiter{ 5260 assetID: assetID, 5261 trigger: trigger, 5262 action: action, 5263 } 5264 } 5265 5266 func (c *Core) waiting(coinID []byte, assetID uint32) bool { 5267 c.waiterMtx.RLock() 5268 defer c.waiterMtx.RUnlock() 5269 _, found := c.blockWaiters[coinIDString(assetID, coinID)] 5270 return found 5271 } 5272 5273 // removeWaiter removes a blockWaiter from the map. 5274 func (c *Core) removeWaiter(id string) { 5275 c.waiterMtx.Lock() 5276 delete(c.blockWaiters, id) 5277 c.waiterMtx.Unlock() 5278 } 5279 5280 // feeSuggestionAny gets a fee suggestion for the given asset from any source 5281 // with it available. It first checks for a capable wallet, then relevant books 5282 // for a cached fee rate obtained with an epoch_report message, and falls back 5283 // to directly requesting a rate from servers with a fee_rate request. 5284 func (c *Core) feeSuggestionAny(assetID uint32, preferredConns ...*dexConnection) uint64 { 5285 // See if the wallet supports fee rates. 5286 w, found := c.wallet(assetID) 5287 if found && w.connected() { 5288 if r := w.feeRate(); r != 0 { 5289 return r 5290 } 5291 } 5292 5293 // Look for cached rates from epoch_report messages. 5294 conns := append(preferredConns, c.dexConnections()...) 5295 for _, dc := range conns { 5296 feeSuggestion := dc.bestBookFeeSuggestion(assetID) 5297 if feeSuggestion > 0 { 5298 return feeSuggestion 5299 } 5300 } 5301 5302 // Helper function to determine if a server has an active market that pairs 5303 // the requested asset. 5304 hasActiveMarket := func(dc *dexConnection) bool { 5305 dc.cfgMtx.RLock() 5306 cfg := dc.cfg 5307 dc.cfgMtx.RUnlock() 5308 if cfg == nil { 5309 return false 5310 } 5311 for _, mkt := range cfg.Markets { 5312 if mkt.Base == assetID || mkt.Quote == assetID && mkt.Running() { 5313 return true 5314 } 5315 } 5316 return false 5317 } 5318 5319 // Request a rate with fee_rate. 5320 for _, dc := range conns { 5321 // The server should have at least one active market with the asset, 5322 // otherwise we might get an outdated rate for an asset whose backend 5323 // might be supported but not in active use, e.g. down for maintenance. 5324 // The fee_rate endpoint will happily return a very old rate without 5325 // indication. 5326 if !hasActiveMarket(dc) { 5327 continue 5328 } 5329 5330 feeSuggestion := dc.fetchFeeRate(assetID) 5331 if feeSuggestion > 0 { 5332 return feeSuggestion 5333 } 5334 } 5335 return 0 5336 } 5337 5338 // feeSuggestion gets the best fee suggestion, first from a synced order book, 5339 // and if not synced, directly from the server. 5340 func (c *Core) feeSuggestion(dc *dexConnection, assetID uint32) (feeSuggestion uint64) { 5341 // Prepare a fee suggestion based on the last reported fee rate in the 5342 // order book feed. 5343 feeSuggestion = dc.bestBookFeeSuggestion(assetID) 5344 if feeSuggestion > 0 { 5345 return 5346 } 5347 return dc.fetchFeeRate(assetID) 5348 } 5349 5350 // Send initiates either send or withdraw from an exchange wallet. if subtract 5351 // is true, fees are subtracted from the value else fees are taken from the 5352 // exchange wallet. 5353 func (c *Core) Send(pw []byte, assetID uint32, value uint64, address string, subtract bool) (asset.Coin, error) { 5354 var crypter encrypt.Crypter 5355 // Empty password can be provided if wallet is already unlocked. Webserver 5356 // and RPCServer should not allow empty password, but this is used for 5357 // bots. 5358 if len(pw) > 0 { 5359 var err error 5360 crypter, err = c.encryptionKey(pw) 5361 if err != nil { 5362 return nil, fmt.Errorf("Trade password error: %w", err) 5363 } 5364 defer crypter.Close() 5365 } 5366 5367 if value == 0 { 5368 return nil, fmt.Errorf("cannot send/withdraw zero %s", unbip(assetID)) 5369 } 5370 wallet, found := c.wallet(assetID) 5371 if !found { 5372 return nil, newError(missingWalletErr, "no wallet found for %s", unbip(assetID)) 5373 } 5374 err := c.connectAndUnlock(crypter, wallet) 5375 if err != nil { 5376 return nil, err 5377 } 5378 5379 if err = wallet.checkPeersAndSyncStatus(); err != nil { 5380 return nil, err 5381 } 5382 5383 var coin asset.Coin 5384 feeSuggestion := c.feeSuggestionAny(assetID) 5385 if !subtract { 5386 coin, err = wallet.Wallet.Send(address, value, feeSuggestion) 5387 } else { 5388 if withdrawer, isWithdrawer := wallet.Wallet.(asset.Withdrawer); isWithdrawer { 5389 coin, err = withdrawer.Withdraw(address, value, feeSuggestion) 5390 } else { 5391 return nil, fmt.Errorf("wallet does not support subtracting network fee from withdraw amount") 5392 } 5393 } 5394 if err != nil { 5395 subject, details := c.formatDetails(TopicSendError, unbip(assetID), err) 5396 c.notify(newSendNote(TopicSendError, subject, details, db.ErrorLevel)) 5397 return nil, err 5398 } 5399 5400 sentValue := wallet.Info().UnitInfo.ConventionalString(coin.Value()) 5401 subject, details := c.formatDetails(TopicSendSuccess, sentValue, unbip(assetID), address, coin) 5402 c.notify(newSendNote(TopicSendSuccess, subject, details, db.Success)) 5403 5404 c.updateAssetBalance(assetID) 5405 5406 return coin, nil 5407 } 5408 5409 // ValidateAddress checks that the provided address is valid. 5410 func (c *Core) ValidateAddress(address string, assetID uint32) (bool, error) { 5411 if address == "" { 5412 return false, nil 5413 } 5414 wallet, found := c.wallet(assetID) 5415 if !found { 5416 return false, newError(missingWalletErr, "no wallet found for %s", unbip(assetID)) 5417 } 5418 return wallet.Wallet.ValidateAddress(address), nil 5419 } 5420 5421 // ApproveToken calls a wallet's ApproveToken method. It approves the version 5422 // of the token used by the dex at the specified address. 5423 func (c *Core) ApproveToken(appPW []byte, assetID uint32, dexAddr string, onConfirm func()) (string, error) { 5424 crypter, err := c.encryptionKey(appPW) 5425 if err != nil { 5426 return "", err 5427 } 5428 5429 wallet, err := c.connectedWallet(assetID) 5430 if err != nil { 5431 return "", err 5432 } 5433 5434 err = wallet.Unlock(crypter) 5435 if err != nil { 5436 return "", err 5437 } 5438 5439 err = wallet.checkPeersAndSyncStatus() 5440 if err != nil { 5441 return "", err 5442 } 5443 5444 dex, connected, err := c.dex(dexAddr) 5445 if err != nil { 5446 return "", err 5447 } 5448 if !connected { 5449 return "", fmt.Errorf("not connected to %s", dexAddr) 5450 } 5451 5452 asset, found := dex.assets[assetID] 5453 if !found { 5454 return "", fmt.Errorf("asset %d not found for %s", assetID, dexAddr) 5455 } 5456 5457 walletOnConfirm := func() { 5458 go onConfirm() 5459 go c.notify(newTokenApprovalNote(wallet.state())) 5460 } 5461 5462 txID, err := wallet.ApproveToken(asset.Version, walletOnConfirm) 5463 if err != nil { 5464 return "", err 5465 } 5466 5467 c.notify(newTokenApprovalNote(wallet.state())) 5468 return txID, nil 5469 } 5470 5471 // UnapproveToken calls a wallet's UnapproveToken method for a specified 5472 // version of the token. 5473 func (c *Core) UnapproveToken(appPW []byte, assetID uint32, version uint32) (string, error) { 5474 crypter, err := c.encryptionKey(appPW) 5475 if err != nil { 5476 return "", err 5477 } 5478 5479 wallet, err := c.connectedWallet(assetID) 5480 if err != nil { 5481 return "", err 5482 } 5483 5484 err = wallet.Unlock(crypter) 5485 if err != nil { 5486 return "", err 5487 } 5488 5489 err = wallet.checkPeersAndSyncStatus() 5490 if err != nil { 5491 return "", err 5492 } 5493 5494 onConfirm := func() { 5495 go c.notify(newTokenApprovalNote(wallet.state())) 5496 } 5497 5498 txID, err := wallet.UnapproveToken(version, onConfirm) 5499 if err != nil { 5500 return "", err 5501 } 5502 5503 c.notify(newTokenApprovalNote(wallet.state())) 5504 return txID, nil 5505 } 5506 5507 // ApproveTokenFee returns the fee for a token approval/unapproval. 5508 func (c *Core) ApproveTokenFee(assetID uint32, version uint32, approval bool) (uint64, error) { 5509 wallet, err := c.connectedWallet(assetID) 5510 if err != nil { 5511 return 0, err 5512 } 5513 5514 return wallet.ApprovalFee(version, approval) 5515 } 5516 5517 // EstimateSendTxFee returns an estimate of the tx fee needed to send or 5518 // withdraw the specified amount. 5519 func (c *Core) EstimateSendTxFee(address string, assetID uint32, amount uint64, subtract, maxWithdraw bool) (fee uint64, isValidAddress bool, err error) { 5520 if amount == 0 { 5521 return 0, false, fmt.Errorf("cannot check fee for zero %s", unbip(assetID)) 5522 } 5523 5524 wallet, found := c.wallet(assetID) 5525 if !found { 5526 return 0, false, newError(missingWalletErr, "no wallet found for %s", unbip(assetID)) 5527 } 5528 5529 if !wallet.traits.IsTxFeeEstimator() { 5530 return 0, false, fmt.Errorf("wallet does not support fee estimation") 5531 } 5532 5533 if subtract && !wallet.traits.IsWithdrawer() { 5534 return 0, false, fmt.Errorf("wallet does not support checking network fee for withdrawal") 5535 } 5536 estimator, is := wallet.Wallet.(asset.TxFeeEstimator) 5537 if !is { 5538 return 0, false, fmt.Errorf("wallet does not support fee estimation") 5539 } 5540 5541 return estimator.EstimateSendTxFee(address, amount, c.feeSuggestionAny(assetID), subtract, maxWithdraw) 5542 } 5543 5544 // SingleLotFees returns the estimated swap, refund, and redeem fees for a single lot 5545 // trade. 5546 func (c *Core) SingleLotFees(form *SingleLotFeesForm) (swapFees, redeemFees, refundFees uint64, err error) { 5547 dc, _, err := c.dex(form.Host) 5548 if err != nil { 5549 return 0, 0, 0, err 5550 } 5551 5552 mktID := marketName(form.Base, form.Quote) 5553 mktConf := dc.marketConfig(mktID) 5554 if mktConf == nil { 5555 return 0, 0, 0, newError(marketErr, "unknown market %q", mktID) 5556 } 5557 5558 wallets, assetConfigs, versCompat, err := c.walletSet(dc, form.Base, form.Quote, form.Sell) 5559 if err != nil { 5560 return 0, 0, 0, err 5561 } 5562 if !versCompat { // covers missing asset config, but that's unlikely since there is a market config 5563 return 0, 0, 0, fmt.Errorf("client and server asset versions are incompatible for %v", form.Host) 5564 } 5565 5566 var swapFeeRate, redeemFeeRate uint64 5567 5568 if form.UseMaxFeeRate { 5569 dc.assetsMtx.Lock() 5570 swapAsset, redeemAsset := dc.assets[wallets.fromWallet.AssetID], dc.assets[wallets.toWallet.AssetID] 5571 dc.assetsMtx.Unlock() 5572 if swapAsset == nil { 5573 return 0, 0, 0, fmt.Errorf("no asset found for %d", wallets.fromWallet.AssetID) 5574 } 5575 if redeemAsset == nil { 5576 return 0, 0, 0, fmt.Errorf("no asset found for %d", wallets.toWallet.AssetID) 5577 } 5578 swapFeeRate, redeemFeeRate = swapAsset.MaxFeeRate, redeemAsset.MaxFeeRate 5579 } else { 5580 swapFeeRate = c.feeSuggestion(dc, wallets.fromWallet.AssetID) // server rates only for the swap init 5581 if swapFeeRate == 0 { 5582 return 0, 0, 0, fmt.Errorf("failed to get swap fee suggestion for %s at %s", wallets.fromWallet.Symbol, form.Host) 5583 } 5584 redeemFeeRate = c.feeSuggestionAny(wallets.toWallet.AssetID) // wallet rate or server rate 5585 if redeemFeeRate == 0 { 5586 return 0, 0, 0, fmt.Errorf("failed to get redeem fee suggestion for %s at %s", wallets.toWallet.Symbol, form.Host) 5587 } 5588 } 5589 5590 swapFees, refundFees, err = wallets.fromWallet.SingleLotSwapRefundFees(assetConfigs.fromAsset.Version, swapFeeRate, form.UseSafeTxSize) 5591 if err != nil { 5592 return 0, 0, 0, fmt.Errorf("error calculating swap/refund fees: %w", err) 5593 } 5594 5595 redeemFees, err = wallets.toWallet.SingleLotRedeemFees(assetConfigs.toAsset.Version, redeemFeeRate) 5596 if err != nil { 5597 return 0, 0, 0, fmt.Errorf("error calculating redeem fees: %w", err) 5598 } 5599 5600 return swapFees, redeemFees, refundFees, nil 5601 } 5602 5603 // MaxFundingFees gives the max fees required to fund a Trade or MultiTrade. 5604 // The host is needed to get the MaxFeeRate, which is used to calculate 5605 // the funding fees. 5606 func (c *Core) MaxFundingFees(fromAsset uint32, host string, numTrades uint32, options map[string]string) (uint64, error) { 5607 wallet, found := c.wallet(fromAsset) 5608 if !found { 5609 return 0, newError(missingWalletErr, "no wallet found for %s", unbip(fromAsset)) 5610 } 5611 5612 exchange, err := c.Exchange(host) 5613 if err != nil { 5614 return 0, err 5615 } 5616 5617 asset, found := exchange.Assets[fromAsset] 5618 if !found { 5619 return 0, fmt.Errorf("asset %d not found for %s", fromAsset, host) 5620 } 5621 5622 return wallet.MaxFundingFees(numTrades, asset.MaxFeeRate, options), nil 5623 } 5624 5625 // PreOrder calculates fee estimates for a trade. 5626 func (c *Core) PreOrder(form *TradeForm) (*OrderEstimate, error) { 5627 dc, err := c.registeredDEX(form.Host) 5628 if err != nil { 5629 return nil, err 5630 } 5631 5632 mktID := marketName(form.Base, form.Quote) 5633 mktConf := dc.marketConfig(mktID) 5634 if mktConf == nil { 5635 return nil, newError(marketErr, "unknown market %q", mktID) 5636 } 5637 5638 wallets, assetConfigs, versCompat, err := c.walletSet(dc, form.Base, form.Quote, form.Sell) 5639 if err != nil { 5640 return nil, err 5641 } 5642 if !versCompat { // covers missing asset config, but that's unlikely since there is a market config 5643 return nil, fmt.Errorf("client and server asset versions are incompatible for %v", form.Host) 5644 } 5645 5646 // So here's the thing. Our assets thus far don't require the wallet to be 5647 // unlocked to get order estimation (listunspent works on locked wallet), 5648 // but if we run into an asset that breaks that assumption, we may need 5649 // to require a password here before estimation. 5650 5651 // We need the wallets to be connected. 5652 if !wallets.fromWallet.connected() { 5653 err := c.connectAndUpdateWallet(wallets.fromWallet) 5654 if err != nil { 5655 c.log.Errorf("Error connecting to %s wallet: %v", wallets.fromWallet.Symbol, err) 5656 return nil, fmt.Errorf("Error connecting to %s wallet", wallets.fromWallet.Symbol) 5657 } 5658 } 5659 5660 if !wallets.toWallet.connected() { 5661 err := c.connectAndUpdateWallet(wallets.toWallet) 5662 if err != nil { 5663 c.log.Errorf("Error connecting to %s wallet: %v", wallets.toWallet.Symbol, err) 5664 return nil, fmt.Errorf("Error connecting to %s wallet", wallets.toWallet.Symbol) 5665 } 5666 } 5667 5668 // Fund the order and prepare the coins. 5669 lotSize := mktConf.LotSize 5670 lots := form.Qty / lotSize 5671 rate := form.Rate 5672 5673 if !form.IsLimit { 5674 // If this is a market order, we'll predict the fill price. 5675 book := dc.bookie(marketName(form.Base, form.Quote)) 5676 if book == nil { 5677 return nil, fmt.Errorf("Cannot estimate market order without a synced book") 5678 } 5679 5680 midGap, err := book.MidGap() 5681 if err != nil { 5682 return nil, fmt.Errorf("Cannot estimate market order with an empty order book") 5683 } 5684 5685 if !form.Sell && calc.BaseToQuote(lotSize, midGap) > form.Qty { 5686 return nil, fmt.Errorf("Market order quantity buys less than a single lot") 5687 } 5688 5689 var fills []*orderbook.Fill 5690 var filled bool 5691 if form.Sell { 5692 fills, filled = book.BestFill(form.Sell, form.Qty) 5693 } else { 5694 fills, filled = book.BestFillMarketBuy(form.Qty, lotSize) 5695 } 5696 5697 if !filled { 5698 return nil, fmt.Errorf("Market is too thin to estimate market order") 5699 } 5700 5701 // Get an average rate. 5702 var qtySum, product uint64 5703 for _, fill := range fills { 5704 product += fill.Quantity * fill.Rate 5705 qtySum += fill.Quantity 5706 } 5707 rate = product / qtySum 5708 if !form.Sell { 5709 lots = qtySum / lotSize 5710 } 5711 } 5712 5713 swapFeeSuggestion := c.feeSuggestion(dc, wallets.fromWallet.AssetID) // server rates only for the swap init 5714 if swapFeeSuggestion == 0 { 5715 return nil, fmt.Errorf("failed to get swap fee suggestion for %s at %s", wallets.fromWallet.Symbol, form.Host) 5716 } 5717 5718 redeemFeeSuggestion := c.feeSuggestionAny(wallets.toWallet.AssetID) // wallet rate or server rate 5719 if redeemFeeSuggestion == 0 { 5720 return nil, fmt.Errorf("failed to get redeem fee suggestion for %s at %s", wallets.toWallet.Symbol, form.Host) 5721 } 5722 5723 swapLotSize := lotSize 5724 if !form.Sell { 5725 swapLotSize = calc.BaseToQuote(rate, lotSize) 5726 } 5727 5728 swapEstimate, err := wallets.fromWallet.PreSwap(&asset.PreSwapForm{ 5729 Version: assetConfigs.fromAsset.Version, 5730 LotSize: swapLotSize, 5731 Lots: lots, 5732 MaxFeeRate: assetConfigs.fromAsset.MaxFeeRate, 5733 Immediate: (form.IsLimit && form.TifNow) || !form.IsLimit, 5734 FeeSuggestion: swapFeeSuggestion, 5735 SelectedOptions: form.Options, 5736 RedeemVersion: assetConfigs.toAsset.Version, 5737 RedeemAssetID: assetConfigs.toAsset.ID, 5738 }) 5739 if err != nil { 5740 return nil, fmt.Errorf("error getting swap estimate: %w", err) 5741 } 5742 5743 redeemEstimate, err := wallets.toWallet.PreRedeem(&asset.PreRedeemForm{ 5744 Version: assetConfigs.toAsset.Version, 5745 Lots: lots, 5746 FeeSuggestion: redeemFeeSuggestion, 5747 SelectedOptions: form.Options, 5748 }) 5749 if err != nil { 5750 return nil, fmt.Errorf("error getting redemption estimate: %v", err) 5751 } 5752 5753 return &OrderEstimate{ 5754 Swap: swapEstimate, 5755 Redeem: redeemEstimate, 5756 }, nil 5757 } 5758 5759 // MultiTradeResult is returned from MultiTrade. Some orders may be placed 5760 // successfully, while others may fail. 5761 type MultiTradeResult struct { 5762 Order *Order 5763 Error error 5764 } 5765 5766 // MultiTrade is used to place multiple standing limit orders on the same 5767 // side of the same market simultaneously. 5768 func (c *Core) MultiTrade(pw []byte, form *MultiTradeForm) []*MultiTradeResult { 5769 results := make([]*MultiTradeResult, 0, len(form.Placements)) 5770 5771 reqs, err := c.prepareMultiTradeRequests(pw, form) 5772 if err != nil { 5773 for range form.Placements { 5774 results = append(results, &MultiTradeResult{Error: err}) 5775 } 5776 return results 5777 } 5778 5779 for i := range form.Placements { 5780 if i >= len(reqs) { 5781 results = append(results, &MultiTradeResult{Error: errors.New("wallet unable to fund order")}) 5782 continue 5783 } 5784 5785 req := reqs[i] 5786 corder, err := c.sendTradeRequest(req) 5787 if err != nil { 5788 results = append(results, &MultiTradeResult{Error: err}) 5789 continue 5790 } 5791 results = append(results, &MultiTradeResult{Order: corder}) 5792 } 5793 5794 return results 5795 } 5796 5797 // TxHistory returns all the transactions a wallet has made. If refID 5798 // is nil, then transactions starting from the most recent are returned 5799 // (past is ignored). If past is true, the transactions prior to the 5800 // refID are returned, otherwise the transactions after the refID are 5801 // returned. n is the number of transactions to return. If n is <= 0, 5802 // all the transactions will be returned 5803 func (c *Core) TxHistory(assetID uint32, n int, refID *string, past bool) ([]*asset.WalletTransaction, error) { 5804 wallet, found := c.wallet(assetID) 5805 if !found { 5806 return nil, newError(missingWalletErr, "no wallet found for %s", unbip(assetID)) 5807 } 5808 5809 return wallet.TxHistory(n, refID, past) 5810 } 5811 5812 // WalletTransaction returns information about a transaction that the wallet 5813 // has made or one in which that wallet received funds. This function supports 5814 // both transaction ID and coin ID. 5815 func (c *Core) WalletTransaction(assetID uint32, txID string) (*asset.WalletTransaction, error) { 5816 wallet, found := c.wallet(assetID) 5817 if !found { 5818 return nil, newError(missingWalletErr, "no wallet found for %s", unbip(assetID)) 5819 } 5820 5821 return wallet.WalletTransaction(c.ctx, txID) 5822 } 5823 5824 // Trade is used to place a market or limit order. 5825 func (c *Core) Trade(pw []byte, form *TradeForm) (*Order, error) { 5826 req, err := c.prepareTradeRequest(pw, form) 5827 if err != nil { 5828 return nil, err 5829 } 5830 5831 corder, err := c.sendTradeRequest(req) 5832 if err != nil { 5833 return nil, err 5834 } 5835 5836 return corder, nil 5837 } 5838 5839 // TradeAsync is like Trade but a temporary order is returned before order 5840 // server validation. This helps handle some issues related to UI/UX where 5841 // server response might take a fairly long time (15 - 20s). 5842 func (c *Core) TradeAsync(pw []byte, form *TradeForm) (*InFlightOrder, error) { 5843 req, err := c.prepareTradeRequest(pw, form) 5844 if err != nil { 5845 return nil, err 5846 } 5847 5848 // Prepare and store the inflight order. 5849 corder := coreOrderFromTrade(req.dbOrder.Order, req.dbOrder.MetaData) 5850 corder.ReadyToTick = true 5851 tempID := req.dc.storeInFlightOrder(corder) 5852 req.tempID = tempID 5853 5854 // Send silent note for the async order. This improves the UI/UX, so 5855 // users don't have to wait for orders especially split tx orders. 5856 c.notify(newOrderNoteWithTempID(TopicAsyncOrderSubmitted, "", "", db.Data, corder, tempID)) 5857 5858 c.wg.Add(1) 5859 go func() { // so core does not shut down while processing this order. 5860 defer func() { 5861 // Cleanup when the inflight order has been processed. 5862 req.dc.deleteInFlightOrder(tempID) 5863 c.wg.Done() 5864 }() 5865 5866 _, err := c.sendTradeRequest(req) 5867 if err != nil { 5868 // If it's an OrderQuantityTooHigh error, send simplified notification 5869 var mErr *msgjson.Error 5870 if errors.As(err, &mErr) && mErr.Code == msgjson.OrderQuantityTooHigh { 5871 topic := TopicOrderQuantityTooHigh 5872 subject, details := c.formatDetails(topic, corder.Host) 5873 c.notify(newOrderNoteWithTempID(topic, subject, details, db.ErrorLevel, corder, tempID)) 5874 return 5875 } 5876 // Send async order error note. 5877 topic := TopicAsyncOrderFailure 5878 subject, details := c.formatDetails(topic, tempID, err) 5879 c.notify(newOrderNoteWithTempID(topic, subject, details, db.ErrorLevel, corder, tempID)) 5880 } 5881 }() 5882 5883 return &InFlightOrder{ 5884 corder, 5885 tempID, 5886 }, nil 5887 } 5888 5889 // tradeRequest hold all the information required to send a trade request to a 5890 // server. 5891 type tradeRequest struct { 5892 mktID, route string 5893 dc *dexConnection 5894 preImg order.Preimage 5895 form *TradeForm 5896 dbOrder *db.MetaOrder 5897 msgOrder msgjson.Stampable 5898 coins asset.Coins 5899 recoveryCoin asset.Coin 5900 wallets *walletSet 5901 errCloser *dex.ErrorCloser 5902 tempID uint64 5903 commitSig chan struct{} 5904 } 5905 5906 func (c *Core) prepareForTradeRequestPrep(pw []byte, base, quote uint32, host string, sell bool) (wallets *walletSet, assetConfig *assetSet, dc *dexConnection, mktConf *msgjson.Market, err error) { 5907 fail := func(err error) (*walletSet, *assetSet, *dexConnection, *msgjson.Market, error) { 5908 return nil, nil, nil, nil, err 5909 } 5910 5911 // Check the user password. A Trade can be attempted with an empty password, 5912 // which should work if both wallets are unlocked. We use this feature for 5913 // bots. 5914 var crypter encrypt.Crypter 5915 if len(pw) > 0 { 5916 var err error 5917 crypter, err = c.encryptionKey(pw) 5918 if err != nil { 5919 return fail(fmt.Errorf("Trade password error: %w", err)) 5920 } 5921 defer crypter.Close() 5922 } 5923 5924 dc, err = c.registeredDEX(host) 5925 if err != nil { 5926 return fail(err) 5927 } 5928 if dc.acct.suspended() { 5929 return fail(newError(suspendedAcctErr, "%w", ErrAccountSuspended)) 5930 } 5931 5932 mktID := marketName(base, quote) 5933 mktConf = dc.marketConfig(mktID) 5934 if mktConf == nil { 5935 return fail(newError(marketErr, "order placed for unknown market %q", mktID)) 5936 } 5937 5938 // Proceed with the order if there is no trade suspension 5939 // scheduled for the market. 5940 if !dc.running(mktID) { 5941 return fail(newError(marketErr, "%s market trading is suspended", mktID)) 5942 } 5943 5944 wallets, assetConfigs, versCompat, err := c.walletSet(dc, base, quote, sell) 5945 if err != nil { 5946 return fail(err) 5947 } 5948 if !versCompat { // also covers missing asset config, but that's unlikely since there is a market config 5949 return fail(fmt.Errorf("client and server asset versions are incompatible for %v", dc.acct.host)) 5950 } 5951 5952 fromWallet, toWallet := wallets.fromWallet, wallets.toWallet 5953 5954 prepareWallet := func(w *xcWallet) error { 5955 // NOTE: If the wallet is already internally unlocked (the decrypted 5956 // password cached in xcWallet.pw), this could be done without the 5957 // crypter via refreshUnlock. 5958 err := c.connectAndUnlock(crypter, w) 5959 if err != nil { 5960 return fmt.Errorf("%s connectAndUnlock error: %w", 5961 assetConfigs.fromAsset.Symbol, err) 5962 } 5963 w.mtx.RLock() 5964 defer w.mtx.RUnlock() 5965 if w.peerCount < 1 { 5966 return &WalletNoPeersError{w.AssetID} 5967 } 5968 if !w.syncStatus.Synced { 5969 return &WalletSyncError{w.AssetID, w.syncStatus.BlockProgress()} 5970 } 5971 return nil 5972 } 5973 5974 err = prepareWallet(fromWallet) 5975 if err != nil { 5976 return fail(err) 5977 } 5978 5979 err = prepareWallet(toWallet) 5980 if err != nil { 5981 return fail(err) 5982 } 5983 5984 return wallets, assetConfigs, dc, mktConf, nil 5985 } 5986 5987 func (c *Core) createTradeRequest(wallets *walletSet, coins asset.Coins, redeemScripts []dex.Bytes, dc *dexConnection, redeemAddr string, 5988 form *TradeForm, redemptionRefundLots uint64, fundingFees uint64, assetConfigs *assetSet, mktConf *msgjson.Market, errCloser *dex.ErrorCloser) (*tradeRequest, error) { 5989 coinIDs := make([]order.CoinID, 0, len(coins)) 5990 for i := range coins { 5991 coinIDs = append(coinIDs, []byte(coins[i].ID())) 5992 } 5993 5994 fromWallet, toWallet := wallets.fromWallet, wallets.toWallet 5995 accountRedeemer, isAccountRedemption := toWallet.Wallet.(asset.AccountLocker) 5996 accountRefunder, isAccountRefund := fromWallet.Wallet.(asset.AccountLocker) 5997 5998 // In the special case that there is a single coin that implements 5999 // RecoveryCoin, set that as the change coin. 6000 var recoveryCoin asset.Coin 6001 var changeID []byte 6002 if len(coins) == 1 { 6003 c := coins[0] 6004 if rc, is := c.(asset.RecoveryCoin); is { 6005 recoveryCoin = c 6006 changeID = rc.RecoveryID() 6007 } 6008 } 6009 6010 preImg := newPreimage() 6011 prefix := &order.Prefix{ 6012 AccountID: dc.acct.ID(), 6013 BaseAsset: form.Base, 6014 QuoteAsset: form.Quote, 6015 OrderType: order.MarketOrderType, 6016 ClientTime: time.Now(), 6017 Commit: preImg.Commit(), 6018 } 6019 var ord order.Order 6020 if form.IsLimit { 6021 prefix.OrderType = order.LimitOrderType 6022 tif := order.StandingTiF 6023 if form.TifNow { 6024 tif = order.ImmediateTiF 6025 } 6026 ord = &order.LimitOrder{ 6027 P: *prefix, 6028 T: order.Trade{ 6029 Coins: coinIDs, 6030 Sell: form.Sell, 6031 Quantity: form.Qty, 6032 Address: redeemAddr, 6033 }, 6034 Rate: form.Rate, 6035 Force: tif, 6036 } 6037 } else { 6038 ord = &order.MarketOrder{ 6039 P: *prefix, 6040 T: order.Trade{ 6041 Coins: coinIDs, 6042 Sell: form.Sell, 6043 Quantity: form.Qty, 6044 Address: redeemAddr, 6045 }, 6046 } 6047 } 6048 6049 err := order.ValidateOrder(ord, order.OrderStatusEpoch, mktConf.LotSize) 6050 if err != nil { 6051 return nil, fmt.Errorf("ValidateOrder error: %w", err) 6052 } 6053 6054 msgCoins, err := messageCoins(fromWallet, coins, redeemScripts) 6055 if err != nil { 6056 return nil, fmt.Errorf("%v wallet failed to sign coins: %w", assetConfigs.fromAsset.Symbol, err) 6057 } 6058 6059 // Everything is ready. Send the order. 6060 route, msgOrder, msgTrade := messageOrder(ord, msgCoins) 6061 6062 // If the to asset is an AccountLocker, we need to lock up redemption 6063 // funds. 6064 var redemptionReserves uint64 6065 if isAccountRedemption { 6066 pubKeys, sigs, err := toWallet.SignMessage(nil, msgOrder.Serialize()) 6067 if err != nil { 6068 return nil, codedError(signatureErr, fmt.Errorf("SignMessage error: %w", err)) 6069 } 6070 if len(pubKeys) == 0 || len(sigs) == 0 { 6071 return nil, newError(signatureErr, "wrong number of pubkeys or signatures, %d & %d", len(pubKeys), len(sigs)) 6072 } 6073 redemptionReserves, err = accountRedeemer.ReserveNRedemptions(redemptionRefundLots, 6074 assetConfigs.toAsset.Version, assetConfigs.toAsset.MaxFeeRate) 6075 if err != nil { 6076 return nil, codedError(walletErr, fmt.Errorf("ReserveNRedemptions error: %w", err)) 6077 } 6078 defer func() { 6079 if _, err := c.updateWalletBalance(toWallet); err != nil { 6080 c.log.Errorf("updateWalletBalance error: %v", err) 6081 } 6082 if toToken := asset.TokenInfo(assetConfigs.toAsset.ID); toToken != nil { 6083 c.updateAssetBalance(toToken.ParentID) 6084 } 6085 }() 6086 6087 msgTrade.RedeemSig = &msgjson.RedeemSig{ 6088 PubKey: pubKeys[0], 6089 Sig: sigs[0], 6090 } 6091 errCloser.Add(func() error { 6092 accountRedeemer.UnlockRedemptionReserves(redemptionReserves) 6093 return nil 6094 }) 6095 } 6096 6097 // If the from asset is an AccountLocker, we need to lock up refund funds. 6098 var refundReserves uint64 6099 if isAccountRefund { 6100 refundReserves, err = accountRefunder.ReserveNRefunds(redemptionRefundLots, 6101 assetConfigs.fromAsset.Version, assetConfigs.fromAsset.MaxFeeRate) 6102 if err != nil { 6103 return nil, codedError(walletErr, fmt.Errorf("ReserveNRefunds error: %w", err)) 6104 } 6105 errCloser.Add(func() error { 6106 accountRefunder.UnlockRefundReserves(refundReserves) 6107 return nil 6108 }) 6109 } 6110 6111 // A non-nil changeID indicates that this is an account based coin. The 6112 // first coin is an address and the entire serialized message needs to 6113 // be signed with that address's private key. 6114 if changeID != nil { 6115 if _, msgTrade.Coins[0].Sigs, err = fromWallet.SignMessage(nil, msgOrder.Serialize()); err != nil { 6116 return nil, fmt.Errorf("%v wallet failed to sign for redeem: %w", 6117 assetConfigs.fromAsset.Symbol, err) 6118 } 6119 } 6120 6121 commitSig := make(chan struct{}) 6122 c.sentCommitsMtx.Lock() 6123 c.sentCommits[prefix.Commit] = commitSig 6124 c.sentCommitsMtx.Unlock() 6125 6126 // Prepare order meta data. 6127 dbOrder := &db.MetaOrder{ 6128 MetaData: &db.OrderMetaData{ 6129 Host: dc.acct.host, 6130 EpochDur: mktConf.EpochLen, // epochIndex := result.ServerTime / mktConf.EpochLen 6131 FromSwapConf: assetConfigs.fromAsset.SwapConf, 6132 ToSwapConf: assetConfigs.toAsset.SwapConf, 6133 MaxFeeRate: assetConfigs.fromAsset.MaxFeeRate, 6134 RedeemMaxFeeRate: assetConfigs.toAsset.MaxFeeRate, 6135 FromVersion: assetConfigs.fromAsset.Version, 6136 ToVersion: assetConfigs.toAsset.Version, // and we're done with the server's asset configs. 6137 Options: form.Options, 6138 RedemptionReserves: redemptionReserves, 6139 RefundReserves: refundReserves, 6140 ChangeCoin: changeID, 6141 FundingFeesPaid: fundingFees, 6142 }, 6143 Order: ord, 6144 } 6145 6146 return &tradeRequest{ 6147 mktID: marketName(form.Base, form.Quote), 6148 route: route, 6149 dc: dc, 6150 form: form, 6151 dbOrder: dbOrder, 6152 msgOrder: msgOrder, 6153 recoveryCoin: recoveryCoin, 6154 coins: coins, 6155 wallets: wallets, 6156 errCloser: errCloser.Copy(), 6157 preImg: preImg, 6158 commitSig: commitSig, 6159 }, nil 6160 } 6161 6162 // prepareTradeRequest prepares a trade request. 6163 func (c *Core) prepareTradeRequest(pw []byte, form *TradeForm) (*tradeRequest, error) { 6164 wallets, assetConfigs, dc, mktConf, err := c.prepareForTradeRequestPrep(pw, form.Base, form.Quote, form.Host, form.Sell) 6165 if err != nil { 6166 return nil, err 6167 } 6168 6169 fromWallet, toWallet := wallets.fromWallet, wallets.toWallet 6170 mktID := marketName(form.Base, form.Quote) 6171 6172 rate, qty := form.Rate, form.Qty 6173 if form.IsLimit { 6174 if rate == 0 { 6175 return nil, newError(orderParamsErr, "zero-rate order not allowed") 6176 } 6177 if minRate := dc.minimumMarketRate(assetConfigs.quoteAsset, mktConf.LotSize); rate < minRate { 6178 return nil, newError(orderParamsErr, "order's rate is lower than market's minimum rate. %d < %d", rate, minRate) 6179 } 6180 } 6181 6182 // Get an address for the swap contract. 6183 redeemAddr, err := toWallet.RedemptionAddress() 6184 if err != nil { 6185 return nil, codedError(walletErr, fmt.Errorf("%s RedemptionAddress error: %w", 6186 assetConfigs.toAsset.Symbol, err)) 6187 } 6188 6189 // Fund the order and prepare the coins. 6190 lotSize := mktConf.LotSize 6191 fundQty := qty 6192 lots := qty / lotSize 6193 if form.IsLimit && !form.Sell { 6194 fundQty = calc.BaseToQuote(rate, fundQty) 6195 } 6196 redemptionRefundLots := lots 6197 6198 isImmediate := (!form.IsLimit || form.TifNow) 6199 6200 // Market buy order 6201 if !form.IsLimit && !form.Sell { 6202 _, isAccountRedemption := toWallet.Wallet.(asset.AccountLocker) 6203 6204 // There is some ambiguity here about whether the specified quantity for 6205 // a market buy order should include projected fees, or whether fees 6206 // should be in addition to the quantity. If the fees should be 6207 // additional to the order quantity (the approach taken here), we should 6208 // try to estimate the number of lots based on the current market. If 6209 // the market is not synced, fall back to a single-lot estimate, with 6210 // the knowledge that such an estimate means that the specified amount 6211 // might not all be available for matching once fees are considered. 6212 lots = 1 6213 book := dc.bookie(mktID) 6214 if book != nil { 6215 midGap, err := book.MidGap() 6216 // An error is only returned when there are no orders on the book. 6217 // In that case, fall back to the 1 lot estimate for now. 6218 if err == nil { 6219 baseQty := calc.QuoteToBase(midGap, fundQty) 6220 lots = baseQty / lotSize 6221 redemptionRefundLots = lots * marketBuyRedemptionSlippageBuffer 6222 if lots == 0 { 6223 err = newError(orderParamsErr, 6224 "order quantity is too low for current market rates. "+ 6225 "qty = %d %s, mid-gap = %d, base-qty = %d %s, lot size = %d", 6226 qty, assetConfigs.quoteAsset.Symbol, midGap, baseQty, 6227 assetConfigs.baseAsset.Symbol, lotSize) 6228 return nil, err 6229 } 6230 } else if isAccountRedemption { 6231 return nil, newError(orderParamsErr, "cannot estimate redemption count") 6232 } 6233 } 6234 } 6235 6236 if lots == 0 { 6237 return nil, newError(orderParamsErr, "order quantity < 1 lot. qty = %d %s, rate = %d, lot size = %d", 6238 qty, assetConfigs.baseAsset.Symbol, rate, mktConf.LotSize) 6239 } 6240 6241 coins, redeemScripts, fundingFees, err := fromWallet.FundOrder(&asset.Order{ 6242 Version: assetConfigs.fromAsset.Version, 6243 Value: fundQty, 6244 MaxSwapCount: lots, 6245 MaxFeeRate: assetConfigs.fromAsset.MaxFeeRate, 6246 Immediate: isImmediate, 6247 FeeSuggestion: c.feeSuggestion(dc, assetConfigs.fromAsset.ID), 6248 Options: form.Options, 6249 RedeemVersion: assetConfigs.toAsset.Version, 6250 RedeemAssetID: assetConfigs.toAsset.ID, 6251 }) 6252 if err != nil { 6253 return nil, codedError(walletErr, fmt.Errorf("FundOrder error for %s, funding quantity %d (%d lots): %w", 6254 assetConfigs.fromAsset.Symbol, fundQty, lots, err)) 6255 } 6256 defer func() { 6257 if _, err := c.updateWalletBalance(fromWallet); err != nil { 6258 c.log.Errorf("updateWalletBalance error: %v", err) 6259 } 6260 if fromToken := asset.TokenInfo(assetConfigs.fromAsset.ID); fromToken != nil { 6261 c.updateAssetBalance(fromToken.ParentID) 6262 } 6263 }() 6264 6265 // The coins selected for this order will need to be unlocked 6266 // if the order does not get to the server successfully. 6267 errCloser := dex.NewErrorCloser() 6268 defer errCloser.Done(c.log) 6269 errCloser.Add(func() error { 6270 err := fromWallet.ReturnCoins(coins) 6271 if err != nil { 6272 return fmt.Errorf("Unable to return %s funding coins: %v", unbip(fromWallet.AssetID), err) 6273 } 6274 return nil 6275 }) 6276 6277 tradeRequest, err := c.createTradeRequest(wallets, coins, redeemScripts, dc, redeemAddr, form, 6278 redemptionRefundLots, fundingFees, assetConfigs, mktConf, errCloser) 6279 if err != nil { 6280 return nil, err 6281 } 6282 6283 errCloser.Success() 6284 6285 return tradeRequest, nil 6286 } 6287 6288 func (c *Core) prepareMultiTradeRequests(pw []byte, form *MultiTradeForm) ([]*tradeRequest, error) { 6289 wallets, assetConfigs, dc, mktConf, err := c.prepareForTradeRequestPrep(pw, form.Base, form.Quote, form.Host, form.Sell) 6290 if err != nil { 6291 return nil, err 6292 } 6293 fromWallet, toWallet := wallets.fromWallet, wallets.toWallet 6294 6295 for _, trade := range form.Placements { 6296 if trade.Rate == 0 { 6297 return nil, newError(orderParamsErr, "zero rate is invalid") 6298 } 6299 if trade.Qty == 0 { 6300 return nil, newError(orderParamsErr, "zero quantity is invalid") 6301 } 6302 } 6303 6304 redeemAddresses := make([]string, 0, len(form.Placements)) 6305 for range form.Placements { 6306 redeemAddr, err := toWallet.RedemptionAddress() 6307 if err != nil { 6308 return nil, codedError(walletErr, fmt.Errorf("%s RedemptionAddress error: %w", 6309 assetConfigs.toAsset.Symbol, err)) 6310 } 6311 redeemAddresses = append(redeemAddresses, redeemAddr) 6312 } 6313 6314 orderValues := make([]*asset.MultiOrderValue, 0, len(form.Placements)) 6315 for _, trade := range form.Placements { 6316 fundQty := trade.Qty 6317 lots := fundQty / mktConf.LotSize 6318 if lots == 0 { 6319 return nil, newError(orderParamsErr, "order quantity < 1 lot") 6320 } 6321 6322 if !form.Sell { 6323 fundQty = calc.BaseToQuote(trade.Rate, fundQty) 6324 } 6325 orderValues = append(orderValues, &asset.MultiOrderValue{ 6326 MaxSwapCount: lots, 6327 Value: fundQty, 6328 }) 6329 } 6330 6331 allCoins, allRedeemScripts, fundingFees, err := fromWallet.FundMultiOrder(&asset.MultiOrder{ 6332 Version: assetConfigs.fromAsset.Version, 6333 Values: orderValues, 6334 MaxFeeRate: assetConfigs.fromAsset.MaxFeeRate, 6335 FeeSuggestion: c.feeSuggestion(dc, assetConfigs.fromAsset.ID), 6336 Options: form.Options, 6337 RedeemVersion: assetConfigs.toAsset.Version, 6338 RedeemAssetID: assetConfigs.toAsset.ID, 6339 }, form.MaxLock) 6340 if err != nil { 6341 return nil, codedError(walletErr, fmt.Errorf("FundMultiOrder error for %s: %v", assetConfigs.fromAsset.Symbol, err)) 6342 } 6343 6344 if len(allCoins) != len(form.Placements) { 6345 c.log.Infof("FundMultiOrder only funded %d orders out of %d (options = %+v)", len(allCoins), len(form.Placements), form.Options) 6346 } 6347 defer func() { 6348 if _, err := c.updateWalletBalance(fromWallet); err != nil { 6349 c.log.Errorf("updateWalletBalance error: %v", err) 6350 } 6351 if fromToken := asset.TokenInfo(assetConfigs.fromAsset.ID); fromToken != nil { 6352 c.updateAssetBalance(fromToken.ParentID) 6353 } 6354 }() 6355 6356 errClosers := make([]*dex.ErrorCloser, 0, len(allCoins)) 6357 for _, coins := range allCoins { 6358 theseCoins := coins 6359 errCloser := dex.NewErrorCloser() 6360 defer errCloser.Done(c.log) 6361 errCloser.Add(func() error { 6362 err := fromWallet.ReturnCoins(theseCoins) 6363 if err != nil { 6364 return fmt.Errorf("unable to return %s funding coins: %v", unbip(fromWallet.AssetID), err) 6365 } 6366 return nil 6367 }) 6368 errClosers = append(errClosers, errCloser) 6369 } 6370 6371 tradeRequests := make([]*tradeRequest, 0, len(allCoins)) 6372 for i, coins := range allCoins { 6373 tradeForm := &TradeForm{ 6374 Host: form.Host, 6375 IsLimit: true, 6376 Sell: form.Sell, 6377 Base: form.Base, 6378 Quote: form.Quote, 6379 Qty: form.Placements[i].Qty, 6380 Rate: form.Placements[i].Rate, 6381 Options: form.Options, 6382 } 6383 // Only count the funding fees once. 6384 var fees uint64 6385 if i == 0 { 6386 fees = fundingFees 6387 } 6388 req, err := c.createTradeRequest(wallets, coins, allRedeemScripts[i], dc, redeemAddresses[i], tradeForm, 6389 orderValues[i].MaxSwapCount, fees, assetConfigs, mktConf, errClosers[i]) 6390 if err != nil { 6391 return nil, err 6392 } 6393 tradeRequests = append(tradeRequests, req) 6394 } 6395 6396 for _, errCloser := range errClosers { 6397 errCloser.Success() 6398 } 6399 6400 return tradeRequests, nil 6401 } 6402 6403 // sendTradeRequest sends an order, processes the result, then prepares and 6404 // stores the trackedTrade. 6405 func (c *Core) sendTradeRequest(tr *tradeRequest) (*Order, error) { 6406 dc, dbOrder, wallets, form, route := tr.dc, tr.dbOrder, tr.wallets, tr.form, tr.route 6407 mktID, msgOrder, preImg, recoveryCoin, coins := tr.mktID, tr.msgOrder, tr.preImg, tr.recoveryCoin, tr.coins 6408 defer tr.errCloser.Done(c.log) 6409 defer close(tr.commitSig) // signals on both success and failure 6410 6411 // Send and get the result. 6412 result := new(msgjson.OrderResult) 6413 err := dc.signAndRequest(msgOrder, route, result, fundingTxWait+DefaultResponseTimeout) 6414 if err != nil { 6415 // At this point there is a possibility that the server got the request 6416 // and created the trade order, but we lost the connection before 6417 // receiving the response with the trade's order ID. Any preimage 6418 // request will be unrecognized. This order is ABANDONED. 6419 return nil, fmt.Errorf("new order request with DEX server %v market %v failed: %w", dc.acct.host, mktID, err) 6420 } 6421 6422 ord := dbOrder.Order 6423 err = validateOrderResponse(dc, result, ord, msgOrder) // stamps the order, giving it a valid ID 6424 if err != nil { 6425 c.log.Errorf("Abandoning order. preimage: %x, server time: %d: %v", 6426 preImg[:], result.ServerTime, fmt.Sprintf("order response validation failure: %v", err)) 6427 return nil, fmt.Errorf("validateOrderResponse error: %w", err) 6428 } 6429 6430 // TODO: Need xcWallet fields for acceptable SwapConf values: a min 6431 // acceptable for security, and even a max confs override to act sooner. 6432 6433 // Store the order. 6434 tr.dbOrder.MetaData.Status = order.OrderStatusEpoch 6435 tr.dbOrder.MetaData.Proof = db.OrderProof{ 6436 DEXSig: result.Sig, 6437 Preimage: tr.preImg[:], 6438 } 6439 6440 err = c.db.UpdateOrder(dbOrder) 6441 if err != nil { 6442 c.log.Errorf("Abandoning order. preimage: %x, server time: %d: %v", 6443 preImg[:], result.ServerTime, fmt.Sprintf("failed to store order in database: %v", err)) 6444 return nil, fmt.Errorf("db.UpdateOrder error: %w", err) 6445 } 6446 6447 // Prepare and store the tracker and get the core.Order to return. 6448 tracker := newTrackedTrade(dbOrder, preImg, dc, c.lockTimeTaker, c.lockTimeMaker, 6449 c.db, c.latencyQ, wallets, coins, c.notify, c.formatDetails) 6450 6451 tracker.redemptionLocked = tracker.redemptionReserves 6452 tracker.refundLocked = tracker.refundReserves 6453 6454 if recoveryCoin != nil { 6455 tracker.change = recoveryCoin 6456 tracker.coinsLocked = false 6457 tracker.changeLocked = true 6458 } 6459 6460 dc.tradeMtx.Lock() 6461 dc.trades[tracker.ID()] = tracker 6462 dc.tradeMtx.Unlock() 6463 6464 // Send a low-priority notification. 6465 corder := tracker.coreOrder() 6466 if !form.IsLimit && !form.Sell { 6467 ui := wallets.quoteWallet.Info().UnitInfo 6468 subject, details := c.formatDetails(TopicYoloPlaced, 6469 ui.ConventionalString(corder.Qty), ui.Conventional.Unit, makeOrderToken(tracker.token())) 6470 c.notify(newOrderNoteWithTempID(TopicYoloPlaced, subject, details, db.Poke, corder, tr.tempID)) 6471 } else { 6472 rateString := "market" 6473 if form.IsLimit { 6474 rateString = wallets.trimmedConventionalRateString(corder.Rate) 6475 } 6476 ui := wallets.baseWallet.Info().UnitInfo 6477 topic := TopicBuyOrderPlaced 6478 if corder.Sell { 6479 topic = TopicSellOrderPlaced 6480 } 6481 subject, details := c.formatDetails(topic, ui.ConventionalString(corder.Qty), ui.Conventional.Unit, rateString, makeOrderToken(tracker.token())) 6482 c.notify(newOrderNoteWithTempID(topic, subject, details, db.Poke, corder, tr.tempID)) 6483 } 6484 6485 tr.errCloser.Success() 6486 6487 return corder, nil 6488 } 6489 6490 // walletSet is a pair of wallets with asset configurations identified in useful 6491 // ways. 6492 type walletSet struct { 6493 fromWallet *xcWallet 6494 toWallet *xcWallet 6495 baseWallet *xcWallet 6496 quoteWallet *xcWallet 6497 } 6498 6499 // assetSet bundles a server's asset "config" for a pair of assets. 6500 type assetSet struct { 6501 baseAsset *dex.Asset 6502 quoteAsset *dex.Asset 6503 fromAsset *dex.Asset 6504 toAsset *dex.Asset 6505 } 6506 6507 // conventionalRate converts the message-rate encoded rate to a rate in 6508 // conventional units. 6509 func (w *walletSet) conventionalRate(msgRate uint64) float64 { 6510 return calc.ConventionalRate(msgRate, w.baseWallet.Info().UnitInfo, w.quoteWallet.Info().UnitInfo) 6511 } 6512 6513 func (w *walletSet) trimmedConventionalRateString(r uint64) string { 6514 s := strconv.FormatFloat(w.conventionalRate(r), 'f', 8, 64) 6515 return strings.TrimRight(strings.TrimRight(s, "0"), ".") 6516 } 6517 6518 // walletSet constructs a walletSet and an assetSet for a certain DEX server and 6519 // asset pair, with the trade direction (sell) used to assign to/from aliases in 6520 // the returned structs. It is not an error if one or both asset configurations 6521 // are missing on the DEX, so the caller must nil check the fields. This also 6522 // returns if our wallet versions and the server's asset versions are 6523 // compatible. 6524 func (c *Core) walletSet(dc *dexConnection, baseID, quoteID uint32, sell bool) (*walletSet, *assetSet, bool, error) { 6525 // Connect and open the wallets if needed. 6526 baseWallet, found := c.wallet(baseID) 6527 if !found { 6528 return nil, nil, false, newError(missingWalletErr, "no wallet found for %s", unbip(baseID)) 6529 } 6530 quoteWallet, found := c.wallet(quoteID) 6531 if !found { 6532 return nil, nil, false, newError(missingWalletErr, "no wallet found for %s", unbip(quoteID)) 6533 } 6534 6535 dc.assetsMtx.RLock() 6536 baseAsset := dc.assets[baseID] 6537 quoteAsset := dc.assets[quoteID] 6538 dc.assetsMtx.RUnlock() 6539 6540 var versCompat bool 6541 if baseAsset == nil { 6542 c.log.Warnf("Base asset server configuration not available for %s (asset %s).", 6543 dc.acct.host, unbip(baseID)) 6544 } else { 6545 versCompat = baseWallet.supportsVer(baseAsset.Version) 6546 } 6547 if quoteAsset == nil { 6548 c.log.Warnf("Quote asset server configuration not available for %s (asset %s).", 6549 dc.acct.host, unbip(quoteID)) 6550 } else { 6551 versCompat = versCompat && quoteWallet.supportsVer(quoteAsset.Version) 6552 } 6553 6554 // We actually care less about base/quote, and more about from/to, which 6555 // depends on whether this is a buy or sell order. 6556 fromAsset, toAsset := baseAsset, quoteAsset 6557 fromWallet, toWallet := baseWallet, quoteWallet 6558 if !sell { 6559 fromAsset, toAsset = quoteAsset, baseAsset 6560 fromWallet, toWallet = quoteWallet, baseWallet 6561 } 6562 6563 return &walletSet{ 6564 fromWallet: fromWallet, 6565 toWallet: toWallet, 6566 baseWallet: baseWallet, 6567 quoteWallet: quoteWallet, 6568 }, &assetSet{ 6569 baseAsset: baseAsset, 6570 quoteAsset: quoteAsset, 6571 fromAsset: fromAsset, 6572 toAsset: toAsset, 6573 }, versCompat, nil 6574 } 6575 6576 func (c *Core) Cancel(oidB dex.Bytes) error { 6577 oid, err := order.IDFromBytes(oidB) 6578 if err != nil { 6579 return err 6580 } 6581 return c.cancelOrder(oid) 6582 } 6583 6584 func (c *Core) cancelOrder(oid order.OrderID) error { 6585 for _, dc := range c.dexConnections() { 6586 found, err := c.tryCancel(dc, oid) 6587 if err != nil { 6588 return err 6589 } 6590 if found { 6591 return nil 6592 } 6593 } 6594 6595 return fmt.Errorf("Cancel: failed to find order %s", oid) 6596 } 6597 6598 func assetBond(bond *db.Bond) *asset.Bond { 6599 return &asset.Bond{ 6600 Version: bond.Version, 6601 AssetID: bond.AssetID, 6602 Amount: bond.Amount, 6603 CoinID: bond.CoinID, 6604 Data: bond.Data, 6605 SignedTx: bond.SignedTx, 6606 UnsignedTx: bond.UnsignedTx, 6607 RedeemTx: bond.RefundTx, 6608 } 6609 } 6610 6611 // bondKey creates a unique map key for a bond by its asset ID and coin ID. 6612 func bondKey(assetID uint32, coinID []byte) string { 6613 return string(append(encode.Uint32Bytes(assetID), coinID...)) 6614 } 6615 6616 // updateReputation sets the account's reputation-related fields. 6617 // updateReputation must be called with the authMtx locked. 6618 func (dc *dexConnection) updateReputation( 6619 newReputation *account.Reputation, 6620 ) { 6621 dc.acct.rep = *newReputation 6622 } 6623 6624 // findBondKeyIdx will attempt to find the address index whose public key hashes 6625 // to a specific hash. 6626 func (c *Core) findBondKeyIdx(pkhEqualFn func(bondKey *secp256k1.PrivateKey) bool, assetID uint32) (idx uint32, err error) { 6627 if !c.bondKeysReady() { 6628 return 0, errors.New("bond key is not initialized") 6629 } 6630 nbki, err := c.db.NextBondKeyIndex(assetID) 6631 if err != nil { 6632 return 0, fmt.Errorf("unable to get next bond key index: %v", err) 6633 } 6634 maxIdx := nbki + 10_000 6635 for i := uint32(0); i < maxIdx; i++ { 6636 bondKey, err := c.bondKeyIdx(assetID, i) 6637 if err != nil { 6638 return 0, fmt.Errorf("error getting bond key at idx %d: %v", i, err) 6639 } 6640 equal := pkhEqualFn(bondKey) 6641 bondKey.Zero() 6642 if equal { 6643 return i, nil 6644 } 6645 } 6646 return 0, fmt.Errorf("searched until idx %d but did not find a pubkey match", maxIdx) 6647 } 6648 6649 // findBond will attempt to find an unknown bond and add it to the live bonds 6650 // slice and db for refunding later. Returns the bond strength if no error. 6651 func (c *Core) findBond(dc *dexConnection, bond *msgjson.Bond) (strength, bondAssetID uint32) { 6652 if bond.AssetID == account.PrepaidBondID { 6653 c.insertPrepaidBond(dc, bond) 6654 return bond.Strength, bond.AssetID 6655 } 6656 symb := dex.BipIDSymbol(bond.AssetID) 6657 bondIDStr := coinIDString(bond.AssetID, bond.CoinID) 6658 c.log.Warnf("Unknown bond reported by server: %v (%s)", bondIDStr, symb) 6659 6660 wallet, err := c.connectedWallet(bond.AssetID) 6661 if err != nil { 6662 c.log.Errorf("%d -> %s wallet error: %w", bond.AssetID, unbip(bond.AssetID), err) 6663 return 0, 0 6664 } 6665 6666 // The server will only tell us about active bonds, so we only need 6667 // search in the possible active timeframe before that. Server will tell 6668 // us when the expiry is, so can subtract from that. Add a day out of 6669 // caution. 6670 bondExpiry := int64(dc.config().BondExpiry) 6671 activeBondTimeframe := minBondLifetime(c.net, bondExpiry) - time.Second*time.Duration(bondExpiry) + time.Second*(60*60*24) // seconds * minutes * hours 6672 6673 bondDetails, err := wallet.FindBond(c.ctx, bond.CoinID, time.Unix(int64(bond.Expiry), 0).Add(-activeBondTimeframe)) 6674 if err != nil { 6675 c.log.Errorf("Unable to find active bond reported by the server: %v", err) 6676 return 0, 0 6677 } 6678 6679 bondAsset, _ := dc.bondAsset(bond.AssetID) 6680 if bondAsset == nil { 6681 // Probably not possible since the dex told us about it. Keep 6682 // going to refund it later. 6683 c.log.Warnf("Dex does not support fidelity bonds in asset %s", symb) 6684 strength = bond.Strength 6685 } else { 6686 strength = uint32(bondDetails.Amount / bondAsset.Amt) 6687 } 6688 6689 idx, err := c.findBondKeyIdx(bondDetails.CheckPrivKey, bond.AssetID) 6690 if err != nil { 6691 c.log.Warnf("Unable to find bond key index for unknown bond %s, will not be able to refund: %v", bondIDStr, err) 6692 idx = math.MaxUint32 6693 } 6694 6695 dbBond := &db.Bond{ 6696 Version: bondDetails.Version, 6697 AssetID: bondDetails.AssetID, 6698 CoinID: bondDetails.CoinID, 6699 Data: bondDetails.Data, 6700 Amount: bondDetails.Amount, 6701 LockTime: uint64(bondDetails.LockTime.Unix()), 6702 KeyIndex: idx, 6703 Strength: strength, 6704 Confirmed: true, 6705 } 6706 6707 err = c.db.AddBond(dc.acct.host, dbBond) 6708 if err != nil { 6709 c.log.Errorf("Failed to store bond %s (%s) for dex %v: %w", 6710 bondIDStr, unbip(bond.AssetID), dc.acct.host, err) 6711 return 0, 0 6712 } 6713 6714 dc.acct.authMtx.Lock() 6715 dc.acct.bonds = append(dc.acct.bonds, dbBond) 6716 dc.acct.authMtx.Unlock() 6717 c.log.Infof("Restored unknown bond %s", bondIDStr) 6718 return strength, bondDetails.AssetID 6719 } 6720 6721 func (c *Core) insertPrepaidBond(dc *dexConnection, bond *msgjson.Bond) { 6722 lockTime := bond.Expiry + dc.config().BondExpiry 6723 dbBond := &db.Bond{ 6724 Version: bond.Version, 6725 AssetID: bond.AssetID, 6726 CoinID: bond.CoinID, 6727 LockTime: lockTime, 6728 Strength: bond.Strength, 6729 Confirmed: true, 6730 } 6731 6732 err := c.db.AddBond(dc.acct.host, dbBond) 6733 if err != nil { 6734 c.log.Errorf("Failed to store pre-paid bond dex %v: %w", dc.acct.host, err) 6735 } 6736 6737 dc.acct.authMtx.Lock() 6738 dc.acct.bonds = append(dc.acct.bonds, dbBond) 6739 dc.acct.authMtx.Unlock() 6740 } 6741 6742 func (dc *dexConnection) maxScore() uint32 { 6743 if maxScore := dc.config().MaxScore; maxScore > 0 { 6744 return maxScore 6745 } 6746 return 60 // Assume the default for < v2 servers. 6747 } 6748 6749 // authDEX authenticates the connection for a DEX. 6750 func (c *Core) authDEX(dc *dexConnection) error { 6751 bondAssets, bondExpiry := dc.bondAssets() 6752 if bondAssets == nil { // reconnect loop may be running 6753 return fmt.Errorf("dex connection not usable prior to config request") 6754 } 6755 6756 // Copy the local bond slices since bondConfirmed will modify them. 6757 dc.acct.authMtx.RLock() 6758 localActiveBonds := make([]*db.Bond, len(dc.acct.bonds)) 6759 6760 copy(localActiveBonds, dc.acct.bonds) 6761 localPendingBonds := make([]*db.Bond, len(dc.acct.pendingBonds)) 6762 copy(localPendingBonds, dc.acct.pendingBonds) 6763 dc.acct.authMtx.RUnlock() 6764 6765 // Prepare and sign the message for the 'connect' route. 6766 acctID := dc.acct.ID() 6767 payload := &msgjson.Connect{ 6768 AccountID: acctID[:], 6769 APIVersion: 0, 6770 Time: uint64(time.Now().UnixMilli()), 6771 } 6772 sigMsg := payload.Serialize() 6773 sig, err := dc.acct.sign(sigMsg) 6774 if err != nil { 6775 return fmt.Errorf("signing error: %w", err) 6776 } 6777 payload.SetSig(sig) 6778 6779 // Send the 'connect' request. 6780 req, err := msgjson.NewRequest(dc.NextID(), msgjson.ConnectRoute, payload) 6781 if err != nil { 6782 return fmt.Errorf("error encoding 'connect' request: %w", err) 6783 } 6784 errChan := make(chan error, 1) 6785 result := new(msgjson.ConnectResult) 6786 err = dc.RequestWithTimeout(req, func(msg *msgjson.Message) { 6787 errChan <- msg.UnmarshalResult(result) 6788 }, DefaultResponseTimeout, func() { 6789 errChan <- fmt.Errorf("timed out waiting for '%s' response", msgjson.ConnectRoute) 6790 }) 6791 // Check the request error. 6792 if err != nil { 6793 return err 6794 } 6795 6796 // Check the response error. 6797 err = <-errChan 6798 // AccountNotFoundError may signal we have an initial bond to post. 6799 var mErr *msgjson.Error 6800 if errors.As(err, &mErr) && mErr.Code == msgjson.AccountNotFoundError { 6801 for _, dbBond := range localPendingBonds { 6802 bondAsset := bondAssets[dbBond.AssetID] 6803 if bondAsset == nil { 6804 c.log.Warnf("authDEX: No info on bond asset %s. Cannot start postbond waiter.", 6805 dex.BipIDSymbol(dbBond.AssetID)) 6806 continue 6807 } 6808 c.monitorBondConfs(dc, assetBond(dbBond), bondAsset.Confs) 6809 } 6810 } 6811 if err != nil { 6812 return fmt.Errorf("'connect' error: %w", err) 6813 } 6814 6815 // Check the servers response signature. 6816 err = dc.acct.checkSig(sigMsg, result.Sig) 6817 if err != nil { 6818 return newError(signatureErr, "DEX signature validation error: %w", err) 6819 } 6820 6821 // Check active and pending bonds, comparing against result.ActiveBonds. For 6822 // pendingBonds, rebroadcast and start waiter to postBond. For 6823 // (locally-confirmed) bonds that are not in connectResp.Bonds, postBond. 6824 6825 // Start by mapping the server-reported bonds: 6826 remoteLiveBonds := make(map[string]*msgjson.Bond) 6827 for _, bond := range result.ActiveBonds { 6828 remoteLiveBonds[bondKey(bond.AssetID, bond.CoinID)] = bond 6829 } 6830 6831 type queuedBond struct { 6832 bond *asset.Bond 6833 confs uint32 6834 } 6835 var toPost, toConfirmLocally []queuedBond 6836 6837 // Identify bonds we consider live that are either pending or missing from 6838 // server. In either case, do c.monitorBondConfs (will be immediate postBond 6839 // and bondConfirmed if at required confirmations). 6840 for _, bond := range localActiveBonds { 6841 if bond.AssetID == account.PrepaidBondID { 6842 continue 6843 } 6844 6845 symb := dex.BipIDSymbol(bond.AssetID) 6846 bondIDStr := coinIDString(bond.AssetID, bond.CoinID) 6847 bondAsset := bondAssets[bond.AssetID] 6848 if bondAsset == nil { 6849 c.log.Warnf("Server no longer supports %d as a bond asset!", bond.AssetID) 6850 continue 6851 } 6852 6853 key := bondKey(bond.AssetID, bond.CoinID) 6854 _, found := remoteLiveBonds[key] 6855 if found { 6856 continue // good, it's live server-side too 6857 } // else needs post retry or it's expired 6858 6859 // Double check bond expiry. It will be moved to the expiredBonds slice 6860 // by the rotateBonds goroutine shortly after. 6861 if bond.LockTime <= uint64(time.Now().Unix())+bondExpiry+2 { 6862 c.log.Debugf("Recently expired bond not reported by server (OK): %s (%s)", bondIDStr, symb) 6863 continue 6864 } 6865 6866 c.log.Warnf("Locally-active bond %v (%s) not reported by server", 6867 bondIDStr, symb) // unexpected, but postbond again 6868 6869 // Unknown on server. postBond at required confs. 6870 c.log.Infof("Preparing to post locally-confirmed bond %v (%s).", bondIDStr, symb) 6871 toPost = append(toPost, queuedBond{assetBond(bond), bondAsset.Confs}) 6872 continue 6873 } 6874 6875 // Identify bonds we consider pending that are either live or missing from 6876 // server. If live on server, do c.bondConfirmed. If missing, do 6877 // c.monitorBondConfs. 6878 for _, bond := range localPendingBonds { 6879 key := bondKey(bond.AssetID, bond.CoinID) 6880 symb := dex.BipIDSymbol(bond.AssetID) 6881 bondIDStr := coinIDString(bond.AssetID, bond.CoinID) 6882 6883 bondAsset := bondAssets[bond.AssetID] 6884 if bondAsset == nil { 6885 c.log.Warnf("Server no longer supports %v as a bond asset!", symb) 6886 continue // will retry, eventually refund 6887 } 6888 6889 _, found := remoteLiveBonds[key] 6890 if found { 6891 // It's live server-side. Confirm it locally (db and slices). 6892 toConfirmLocally = append(toConfirmLocally, queuedBond{assetBond(bond), 0}) 6893 continue 6894 } 6895 6896 c.log.Debugf("Starting coin waiter for pending bond %v (%s)", bondIDStr, symb) 6897 6898 // Still pending on server. Start waiting for confs. 6899 c.log.Debugf("Preparing to post pending bond %v (%s).", bondIDStr, symb) 6900 toPost = append(toPost, queuedBond{assetBond(bond), bondAsset.Confs}) 6901 } 6902 6903 updatedAssets := make(assetMap) 6904 // Flag as authenticated before bondConfirmed and monitorBondConfs, which 6905 // may call authDEX if not flagged as such. 6906 dc.acct.authMtx.Lock() 6907 // Reasons we are here: (1) first auth after login, (2) re-auth on 6908 // reconnect, (3) bondConfirmed for the initial bond for the account. 6909 // totalReserved is non-zero in #3, but zero in #1. There are no reserves 6910 // actions to take in #3 since PostBond reserves prior to post. 6911 dc.updateReputation(result.Reputation) 6912 rep := dc.acct.rep 6913 effectiveTier := rep.EffectiveTier() 6914 c.log.Infof("Authenticated connection to %s, acct %v, %d active bonds, %d active orders, %d active matches, score %d, tier %d", 6915 dc.acct.host, acctID, len(result.ActiveBonds), len(result.ActiveOrderStatuses), len(result.ActiveMatches), result.Score, effectiveTier) 6916 dc.acct.isAuthed = true 6917 6918 c.log.Debugf("Tier/bonding with %v: effectiveTier = %d, targetTier = %d, bondedTiers = %d, revokedTiers = %d", 6919 dc.acct.host, effectiveTier, dc.acct.targetTier, rep.BondedTier, rep.Penalties) 6920 dc.acct.authMtx.Unlock() 6921 6922 c.notify(newReputationNote(dc.acct.host, rep)) 6923 6924 for _, pending := range toPost { 6925 c.monitorBondConfs(dc, pending.bond, pending.confs, true) 6926 } 6927 for _, confirmed := range toConfirmLocally { 6928 bond := confirmed.bond 6929 bondIDStr := coinIDString(bond.AssetID, bond.CoinID) 6930 c.log.Debugf("Confirming pending bond %v that is confirmed server side", bondIDStr) 6931 if err = c.bondConfirmed(dc, bond.AssetID, bond.CoinID, &msgjson.PostBondResult{Reputation: &rep} /* no change */); err != nil { 6932 c.log.Errorf("Unable to confirm bond %s: %v", bondIDStr, err) 6933 } 6934 } 6935 6936 localBondMap := make(map[string]struct{}, len(localActiveBonds)+len(localPendingBonds)) 6937 for _, dbBond := range localActiveBonds { 6938 localBondMap[bondKey(dbBond.AssetID, dbBond.CoinID)] = struct{}{} 6939 } 6940 for _, dbBond := range localPendingBonds { 6941 localBondMap[bondKey(dbBond.AssetID, dbBond.CoinID)] = struct{}{} 6942 } 6943 6944 var unknownBondStrength uint32 6945 unknownBondAssetID := -1 6946 for _, bond := range result.ActiveBonds { 6947 key := bondKey(bond.AssetID, bond.CoinID) 6948 if _, found := localBondMap[key]; found { 6949 continue 6950 } 6951 // Server reported a bond we do not know about. 6952 ubs, ubaID := c.findBond(dc, bond) 6953 unknownBondStrength += ubs 6954 if unknownBondAssetID != -1 && ubs != 0 && uint32(unknownBondAssetID) != ubaID { 6955 c.log.Warnf("Found unknown bonds for different assets. %s and %s.", 6956 unbip(uint32(unknownBondAssetID)), unbip(ubaID)) 6957 } 6958 if ubs != 0 { 6959 unknownBondAssetID = int(ubaID) 6960 } 6961 } 6962 6963 // If there were unknown bonds and tier is zero, this may be a restored 6964 // client and so requires action by the user to set their target bond 6965 // tier. Warn the user of this. 6966 if unknownBondStrength > 0 && dc.acct.targetTier == 0 { 6967 subject, details := c.formatDetails(TopicUnknownBondTierZero, unbip(uint32(unknownBondAssetID)), dc.acct.host) 6968 c.notify(newUnknownBondTierZeroNote(subject, details)) 6969 c.log.Warnf("Unknown bonds for asset %s found for dex %s while target tier is zero.", 6970 unbip(uint32(unknownBondAssetID)), dc.acct.host) 6971 } 6972 6973 // Associate the matches with known trades. 6974 matches, _, err := dc.parseMatches(result.ActiveMatches, false) 6975 if err != nil { 6976 c.log.Error(err) 6977 } 6978 6979 exceptions, matchConflicts := dc.compareServerMatches(matches) 6980 for oid, matchAnomalies := range exceptions { 6981 trade := matchAnomalies.trade 6982 missing, extras := matchAnomalies.missing, matchAnomalies.extra 6983 6984 trade.mtx.Lock() 6985 6986 // Flag each of the missing matches as revoked. 6987 for _, match := range missing { 6988 c.log.Warnf("DEX %s did not report active match %s on order %s - assuming revoked, status %v.", 6989 dc.acct.host, match, oid, match.Status) 6990 // Must have been revoked while we were gone. Flag to allow recovery 6991 // and subsequent retirement of the match and parent trade. 6992 match.MetaData.Proof.SelfRevoked = true 6993 if err := c.db.UpdateMatch(&match.MetaMatch); err != nil { 6994 c.log.Errorf("Failed to update missing/revoked match: %v", err) 6995 } 6996 } 6997 6998 // Send a "Missing matches" order note if there are missing match message. 6999 // Also, check if the now-Revoked matches were the last set of matches that 7000 // required sending swaps, and unlock coins if so. 7001 if len(missing) > 0 { 7002 if trade.maybeReturnCoins() { 7003 updatedAssets.count(trade.wallets.fromWallet.AssetID) 7004 } 7005 7006 subject, details := c.formatDetails(TopicMissingMatches, 7007 len(missing), makeOrderToken(trade.token()), dc.acct.host) 7008 c.notify(newOrderNote(TopicMissingMatches, subject, details, db.ErrorLevel, trade.coreOrderInternal())) 7009 } 7010 7011 // Start negotiation for extra matches for this trade. 7012 if len(extras) > 0 { 7013 err := trade.negotiate(extras) 7014 if err != nil { 7015 c.log.Errorf("Error negotiating one or more previously unknown matches for order %s reported by %s on connect: %v", 7016 oid, dc.acct.host, err) 7017 subject, details := c.formatDetails(TopicMatchResolutionError, len(extras), dc.acct.host, makeOrderToken(trade.token())) 7018 c.notify(newOrderNote(TopicMatchResolutionError, subject, details, db.ErrorLevel, trade.coreOrderInternal())) 7019 } else { 7020 // For taker matches in MakerSwapCast, queue up match status 7021 // resolution to retrieve the maker's contract and coin. 7022 for _, extra := range extras { 7023 if order.MatchSide(extra.Side) == order.Taker && order.MatchStatus(extra.Status) == order.MakerSwapCast { 7024 var matchID order.MatchID 7025 copy(matchID[:], extra.MatchID) 7026 match, found := trade.matches[matchID] 7027 if !found { 7028 c.log.Errorf("Extra match %v was not registered by negotiate (db error?)", matchID) 7029 continue 7030 } 7031 c.log.Infof("Queueing match status resolution for newly discovered match %v (%s) "+ 7032 "as taker to MakerSwapCast status.", matchID, match.Status) // had better be NewlyMatched! 7033 7034 oid := trade.ID() 7035 conflicts := matchConflicts[oid] 7036 if conflicts == nil { 7037 conflicts = &matchStatusConflict{trade: trade} 7038 matchConflicts[oid] = conflicts 7039 } 7040 conflicts.matches = append(conflicts.matches, trade.matches[matchID]) 7041 } 7042 } 7043 } 7044 } 7045 7046 trade.mtx.Unlock() 7047 } 7048 7049 // Compare the server-returned active orders with tracked trades, updating 7050 // the trade statuses where necessary. This is done after processing the 7051 // connect resp matches so that where possible, available match data can be 7052 // used to properly set order statuses and filled amount. 7053 unknownOrders, reconciledOrdersCount := dc.reconcileTrades(result.ActiveOrderStatuses) 7054 if len(unknownOrders) > 0 { 7055 subject, details := c.formatDetails(TopicUnknownOrders, len(unknownOrders), dc.acct.host) 7056 c.notify(newDEXAuthNote(TopicUnknownOrders, subject, dc.acct.host, false, details, db.Poke)) 7057 } 7058 if reconciledOrdersCount > 0 { 7059 subject, details := c.formatDetails(TopicOrdersReconciled, reconciledOrdersCount) 7060 c.notify(newDEXAuthNote(TopicOrdersReconciled, subject, dc.acct.host, false, details, db.Poke)) 7061 } 7062 7063 if len(matchConflicts) > 0 { 7064 var n int 7065 for _, c := range matchConflicts { 7066 n += len(c.matches) 7067 } 7068 c.log.Warnf("Beginning match status resolution for %d matches...", n) 7069 c.resolveMatchConflicts(dc, matchConflicts) 7070 } 7071 7072 // List and cancel standing limit orders that are in epoch or booked status, 7073 // but without funding coins for new matches. This should be done after the 7074 // order status resolution done above. 7075 var brokenTrades []*trackedTrade 7076 dc.tradeMtx.RLock() 7077 for _, trade := range dc.trades { 7078 if lo, ok := trade.Order.(*order.LimitOrder); !ok || lo.Force != order.StandingTiF { 7079 continue // only standing limit orders need to be canceled 7080 } 7081 trade.mtx.RLock() 7082 status := trade.metaData.Status 7083 if (status == order.OrderStatusEpoch || status == order.OrderStatusBooked) && 7084 !trade.hasFundingCoins() { 7085 brokenTrades = append(brokenTrades, trade) 7086 } 7087 trade.mtx.RUnlock() 7088 } 7089 dc.tradeMtx.RUnlock() 7090 for _, trade := range brokenTrades { 7091 c.log.Warnf("Canceling unfunded standing limit order %v", trade.ID()) 7092 if err = c.tryCancelTrade(dc, trade); err != nil { 7093 c.log.Warnf("Unable to cancel unfunded trade %v: %v", trade.ID(), err) 7094 } 7095 } 7096 7097 if len(updatedAssets) > 0 { 7098 c.updateBalances(updatedAssets) 7099 } 7100 7101 // Try to cancel unknown orders. 7102 for _, oid := range unknownOrders { 7103 // Even if we have a record of this order, it is inactive from our 7104 // perspective, so we don't try to track it as a trackedTrade. 7105 var base, quote uint32 7106 if metaUnknown, _ := c.db.Order(oid); metaUnknown != nil { 7107 if metaUnknown.Order.Type() != order.LimitOrderType { 7108 continue // can't cancel a cancel or market order, it should just go away from server 7109 } 7110 base, quote = metaUnknown.Order.Base(), metaUnknown.Order.Quote() 7111 } else { 7112 c.log.Warnf("Order %v not found in DB, so cancelling may fail.", oid) 7113 // Otherwise try with (42,0) and hope server will dig for it based 7114 // on just the targeted order ID if that market is incorrect. 7115 base, quote = 42, 0 7116 } 7117 preImg, co, _, commitSig, err := c.sendCancelOrder(dc, oid, base, quote) 7118 if err != nil { 7119 c.log.Errorf("Failed to send cancel for unknown order %v: %v", oid, err) 7120 continue 7121 } 7122 c.log.Warnf("Sent request to cancel unknown order %v, cancel order ID %v", oid, co.ID()) 7123 dc.blindCancelsMtx.Lock() 7124 dc.blindCancels[co.ID()] = preImg 7125 dc.blindCancelsMtx.Unlock() 7126 close(commitSig) // ready to handle the preimage request 7127 } 7128 7129 return nil 7130 } 7131 7132 // AssetBalance retrieves and updates the current wallet balance. 7133 func (c *Core) AssetBalance(assetID uint32) (*WalletBalance, error) { 7134 wallet, err := c.connectedWallet(assetID) 7135 if err != nil { 7136 return nil, fmt.Errorf("%d -> %s wallet error: %w", assetID, unbip(assetID), err) 7137 } 7138 return c.updateWalletBalance(wallet) 7139 } 7140 7141 func pluralize(n int) string { 7142 if n == 1 { 7143 return "" 7144 } 7145 return "s" 7146 } 7147 7148 // initialize pulls the known DEXes from the database and attempts to connect 7149 // and retrieve the DEX configuration. 7150 func (c *Core) initialize() error { 7151 accts, err := c.db.Accounts() 7152 if err != nil { 7153 return fmt.Errorf("failed to retrieve accounts from database: %w", err) 7154 } 7155 7156 pokes, err := c.db.LoadPokes() 7157 c.pokesCache = newPokesCache(pokesCapacity) 7158 if err != nil { 7159 c.log.Errorf("Error loading pokes from db: %v", err) 7160 } else { 7161 c.pokesCache.init(pokes) 7162 } 7163 7164 // Start connecting to DEX servers. 7165 var liveConns uint32 7166 var wg sync.WaitGroup 7167 for _, acct := range accts { 7168 wg.Add(1) 7169 go func(acct *db.AccountInfo) { 7170 defer wg.Done() 7171 if _, connected := c.connectAccount(acct); connected { 7172 atomic.AddUint32(&liveConns, 1) 7173 } 7174 }(acct) 7175 } 7176 7177 // Load wallet configurations. Actual connections are established on Login. 7178 dbWallets, err := c.db.Wallets() 7179 if err != nil { 7180 c.log.Errorf("error loading wallets from database: %v", err) 7181 } 7182 7183 // Wait for dexConnections to be loaded to ensure they are ready for 7184 // authentication when Login is triggered. NOTE/TODO: Login could just as 7185 // easily make the connection, but arguably configured DEXs should be 7186 // available for unauthenticated operations such as watching market feeds. 7187 // 7188 // loadWallet requires dexConnections loaded to set proper locked balances 7189 // (contracts and bonds), so we don't wait after the dbWallets loop. 7190 wg.Wait() 7191 c.log.Infof("Connected to %d of %d DEX servers", liveConns, len(accts)) 7192 7193 for _, dbWallet := range dbWallets { 7194 if asset.Asset(dbWallet.AssetID) == nil && asset.TokenInfo(dbWallet.AssetID) == nil { 7195 c.log.Infof("Wallet for asset %s no longer supported", dex.BipIDSymbol(dbWallet.AssetID)) 7196 continue 7197 } 7198 assetID := dbWallet.AssetID 7199 wallet, err := c.loadWallet(dbWallet) 7200 if err != nil { 7201 c.log.Errorf("error loading %d -> %s wallet: %v", assetID, unbip(assetID), err) 7202 continue 7203 } 7204 // Wallet is loaded from the DB, but not yet connected. 7205 c.log.Tracef("Loaded %s wallet configuration.", unbip(assetID)) 7206 c.updateWallet(assetID, wallet) 7207 } 7208 7209 // Check DB for active orders on any DEX. 7210 for _, acct := range accts { 7211 host, _ := addrHost(acct.Host) 7212 activeOrders, _ := c.dbOrders(host) // non-nil error will load 0 orders, and any subsequent db error will cause a shutdown on dex auth or sooner 7213 if n := len(activeOrders); n > 0 { 7214 c.log.Warnf("\n\n\t **** IMPORTANT: You have %d active order%s on %s. LOGIN immediately! **** \n", 7215 n, pluralize(n), host) 7216 } 7217 } 7218 7219 return nil 7220 } 7221 7222 // connectAccount makes a connection to the DEX for the given account. If a 7223 // non-nil dexConnection is returned from newDEXConnection, it was inserted into 7224 // the conns map even if the connection attempt failed (connected == false), and 7225 // the connect retry / keepalive loop is active. The intial connection attempt 7226 // or keepalive loop will not run if acct is disabled. 7227 func (c *Core) connectAccount(acct *db.AccountInfo) (dc *dexConnection, connected bool) { 7228 host, err := addrHost(acct.Host) 7229 if err != nil { 7230 c.log.Errorf("skipping loading of %s due to address parse error: %v", host, err) 7231 return 7232 } 7233 7234 if c.cfg.TheOneHost != "" && c.cfg.TheOneHost != host { 7235 c.log.Infof("Running with --onehost = %q.", c.cfg.TheOneHost) 7236 return 7237 } 7238 7239 var connectFlag connectDEXFlag 7240 if acct.ViewOnly() { 7241 connectFlag |= connectDEXFlagViewOnly 7242 } 7243 7244 dc, err = c.newDEXConnection(acct, connectFlag) 7245 if err != nil { 7246 c.log.Errorf("Unable to prepare DEX %s: %v", host, err) 7247 return 7248 } 7249 7250 err = c.startDexConnection(acct, dc) 7251 if err != nil { 7252 c.log.Errorf("Trouble establishing connection to %s (will retry). Error: %v", host, err) 7253 } 7254 7255 // Connected or not, the dexConnection goes in the conns map now. 7256 c.addDexConnection(dc) 7257 return dc, err == nil 7258 } 7259 7260 func (c *Core) dbOrders(host string) ([]*db.MetaOrder, error) { 7261 // Prepare active orders, according to the DB. 7262 dbOrders, err := c.db.ActiveDEXOrders(host) 7263 if err != nil { 7264 return nil, fmt.Errorf("database error when fetching orders for %s: %w", host, err) 7265 } 7266 c.log.Infof("Loaded %d active orders.", len(dbOrders)) 7267 7268 // It's possible for an order to not be active, but still have active matches. 7269 // Grab the orders for those too. 7270 haveOrder := func(oid order.OrderID) bool { 7271 for _, dbo := range dbOrders { 7272 if dbo.Order.ID() == oid { 7273 return true 7274 } 7275 } 7276 return false 7277 } 7278 7279 activeMatchOrders, err := c.db.DEXOrdersWithActiveMatches(host) 7280 if err != nil { 7281 return nil, fmt.Errorf("database error fetching active match orders for %s: %w", host, err) 7282 } 7283 c.log.Infof("Loaded %d active match orders", len(activeMatchOrders)) 7284 for _, oid := range activeMatchOrders { 7285 if haveOrder(oid) { 7286 continue 7287 } 7288 dbOrder, err := c.db.Order(oid) 7289 if err != nil { 7290 return nil, fmt.Errorf("database error fetching order %s for %s: %w", oid, host, err) 7291 } 7292 dbOrders = append(dbOrders, dbOrder) 7293 } 7294 7295 return dbOrders, nil 7296 } 7297 7298 // dbTrackers prepares trackedTrades based on active orders and matches in the 7299 // database. Since dbTrackers is during the login process when wallets are not yet 7300 // connected or unlocked, wallets and coins are not added to the returned trackers. 7301 // Use resumeTrades with the app Crypter to prepare wallets and coins. 7302 func (c *Core) dbTrackers(dc *dexConnection) (map[order.OrderID]*trackedTrade, error) { 7303 // Prepare active orders, according to the DB. 7304 dbOrders, err := c.dbOrders(dc.acct.host) 7305 if err != nil { 7306 return nil, err 7307 } 7308 7309 // Index all of the cancel orders so we can account for them when loading 7310 // the trade orders. Whatever remains is orphaned. 7311 unknownCancels := make(map[order.OrderID]struct{}) 7312 for _, dbOrder := range dbOrders { 7313 if dbOrder.Order.Type() == order.CancelOrderType { 7314 unknownCancels[dbOrder.Order.ID()] = struct{}{} 7315 } 7316 } 7317 7318 // For older orders, we'll attempt to get the SwapConf from the server's 7319 // asset config. Newer orders will have it stored in the DB. 7320 assetSwapConf := func(assetID uint32) uint32 { 7321 if asset := dc.assetConfig(assetID); asset != nil { 7322 return asset.SwapConf 7323 } 7324 return 0 // server may be gone 7325 } 7326 7327 trackers := make(map[order.OrderID]*trackedTrade, len(dbOrders)) 7328 excludeCancelMatches := true 7329 for _, dbOrder := range dbOrders { 7330 ord := dbOrder.Order 7331 oid := ord.ID() 7332 // Ignore cancel orders here. They'll be retrieved from LinkedOrder for 7333 // trade orders below. 7334 if ord.Type() == order.CancelOrderType { 7335 continue 7336 } 7337 7338 mktID := marketName(ord.Base(), ord.Quote()) 7339 if mktConf := dc.marketConfig(mktID); mktConf == nil { 7340 c.log.Warnf("Active %s order retrieved for unknown market %s at %v (server status: %v). Loading it anyway.", 7341 oid, mktID, dc.acct.host, dc.status()) 7342 } else { 7343 if dbOrder.MetaData.EpochDur == 0 { // do our best for old orders + down dex 7344 dbOrder.MetaData.EpochDur = mktConf.EpochLen 7345 } 7346 } 7347 if dbOrder.MetaData.ToSwapConf == 0 { // upgraded with active order :/ 7348 if dbOrder.Order.Trade().Sell { 7349 dbOrder.MetaData.ToSwapConf = assetSwapConf(ord.Quote()) 7350 } else { 7351 dbOrder.MetaData.ToSwapConf = assetSwapConf(ord.Base()) 7352 } 7353 } 7354 if dbOrder.MetaData.FromSwapConf == 0 { 7355 if dbOrder.Order.Trade().Sell { 7356 dbOrder.MetaData.FromSwapConf = assetSwapConf(ord.Base()) 7357 } else { 7358 dbOrder.MetaData.FromSwapConf = assetSwapConf(ord.Quote()) 7359 } 7360 } 7361 7362 var preImg order.Preimage 7363 copy(preImg[:], dbOrder.MetaData.Proof.Preimage) 7364 tracker := newTrackedTrade(dbOrder, preImg, dc, c.lockTimeTaker, c.lockTimeMaker, 7365 c.db, c.latencyQ, nil, nil, c.notify, c.formatDetails) 7366 tracker.readyToTick = false 7367 trackers[dbOrder.Order.ID()] = tracker 7368 7369 // Get matches. 7370 dbMatches, err := c.db.MatchesForOrder(oid, excludeCancelMatches) 7371 if err != nil { 7372 return nil, fmt.Errorf("error loading matches for order %s: %w", oid, err) 7373 } 7374 var makerCancel *msgjson.Match 7375 for _, dbMatch := range dbMatches { 7376 // Only trade matches are added to the matches map. Detect and skip 7377 // cancel order matches, which have an empty Address field. 7378 if dbMatch.Address == "" { // only correct for maker's cancel match 7379 // tracker.cancel is set from LinkedOrder with cancelTrade. 7380 makerCancel = &msgjson.Match{ 7381 OrderID: oid[:], 7382 MatchID: dbMatch.MatchID[:], 7383 Quantity: dbMatch.Quantity, 7384 } 7385 continue 7386 } 7387 // Make sure that a taker will not prematurely send an 7388 // initialization until it is confirmed with the server 7389 // that the match is not revoked. 7390 checkServerRevoke := dbMatch.Side == order.Taker && dbMatch.Status == order.MakerSwapCast 7391 tracker.matches[dbMatch.MatchID] = &matchTracker{ 7392 prefix: tracker.Prefix(), 7393 trade: tracker.Trade(), 7394 MetaMatch: *dbMatch, 7395 // Ensure logging on the first check of counterparty contract 7396 // confirms and own contract expiry. 7397 counterConfirms: -1, 7398 lastExpireDur: 365 * 24 * time.Hour, 7399 checkServerRevoke: checkServerRevoke, 7400 } 7401 } 7402 7403 // Load any linked cancel order. 7404 cancelID := tracker.metaData.LinkedOrder 7405 if cancelID.IsZero() { 7406 continue 7407 } 7408 metaCancel, err := c.db.Order(cancelID) 7409 if err != nil { 7410 c.log.Errorf("cancel order %s not found for trade %s", cancelID, oid) 7411 continue 7412 } 7413 co, ok := metaCancel.Order.(*order.CancelOrder) 7414 if !ok { 7415 c.log.Errorf("linked order %s is not a cancel order", cancelID) 7416 continue 7417 } 7418 epochDur := metaCancel.MetaData.EpochDur 7419 if epochDur == 0 { 7420 epochDur = dbOrder.MetaData.EpochDur // could still be zero this is an old order and server down 7421 } 7422 var pimg order.Preimage 7423 copy(pimg[:], metaCancel.MetaData.Proof.Preimage) 7424 err = tracker.cancelTrade(co, pimg, epochDur) // set tracker.cancel and link 7425 if err != nil { 7426 c.log.Errorf("Error setting cancel order info %s: %v", co.ID(), err) 7427 } else { 7428 tracker.cancel.matches.maker = makerCancel 7429 } 7430 delete(unknownCancels, cancelID) // this one is known 7431 c.log.Debugf("Loaded cancel order %v for trade %v", cancelID, oid) 7432 // TODO: The trackedTrade.cancel.matches is not being repopulated on 7433 // startup. The consequences are that the Filled value will not include 7434 // the canceled portion, and the *CoreOrder generated by 7435 // coreOrderInternal will be Cancelling, but not Canceled. Instead of 7436 // using the matchTracker.matches msgjson.Match fields, we should be 7437 // storing the match data in the OrderMetaData so that it can be 7438 // tracked across sessions. 7439 } 7440 7441 // Retire any remaining cancel orders that don't have active target orders. 7442 // This means we somehow already retired the trade, but not the cancel. 7443 for cid := range unknownCancels { 7444 c.log.Warnf("Retiring orphaned cancel order %v", cid) 7445 err = c.db.UpdateOrderStatus(cid, order.OrderStatusRevoked) 7446 if err != nil { 7447 c.log.Errorf("Failed to update status of orphaned cancel order %v: %v", cid, err) 7448 } 7449 } 7450 7451 return trackers, nil 7452 } 7453 7454 // loadDBTrades load's the active trades from the db, populates the trade's 7455 // wallets field and some other metadata, and adds the trade to the 7456 // dexConnection's trades map. Every trade added to the trades map will 7457 // have wallets set. readyToTick will still be set to false, so resumeTrades 7458 // must be run before the trades will be processed. 7459 func (c *Core) loadDBTrades(dc *dexConnection) error { 7460 trackers, err := c.dbTrackers(dc) 7461 if err != nil { 7462 return fmt.Errorf("error retrieving active matches: %w", err) 7463 } 7464 7465 var tradesLoaded uint32 7466 for _, tracker := range trackers { 7467 if !tracker.isActive() { 7468 // In this event, there is a discrepancy between the active criteria 7469 // between dbTrackers and isActive that should be resolved. 7470 c.log.Warnf("Loaded inactive trade %v from the DB.", tracker.ID()) 7471 continue 7472 } 7473 7474 trade := tracker.Trade() 7475 7476 walletSet, assetConfigs, versCompat, err := c.walletSet(dc, tracker.Base(), tracker.Quote(), trade.Sell) 7477 if err != nil { 7478 err = fmt.Errorf("failed to load wallets for trade ID %s: %w", tracker.ID(), err) 7479 subject, details := c.formatDetails(TopicOrderLoadFailure, err) 7480 c.notify(newOrderNote(TopicOrderLoadFailure, subject, details, db.ErrorLevel, nil)) 7481 continue 7482 } 7483 7484 // Every trade in the trades map must have wallets set. 7485 tracker.wallets = walletSet 7486 dc.tradeMtx.Lock() 7487 if _, found := dc.trades[tracker.ID()]; found { 7488 dc.tradeMtx.Unlock() 7489 continue 7490 } 7491 dc.trades[tracker.ID()] = tracker 7492 dc.tradeMtx.Unlock() 7493 7494 mktConf := dc.marketConfig(tracker.mktID) 7495 if tracker.metaData.EpochDur == 0 { // upgraded with live orders... smart :/ 7496 if mktConf != nil { // may remain zero if market also vanished 7497 tracker.metaData.EpochDur = mktConf.EpochLen 7498 } 7499 } 7500 if tracker.metaData.FromSwapConf == 0 && assetConfigs.fromAsset != nil { 7501 tracker.metaData.FromSwapConf = assetConfigs.fromAsset.SwapConf 7502 } 7503 if tracker.metaData.ToSwapConf == 0 && assetConfigs.toAsset != nil { 7504 tracker.metaData.ToSwapConf = assetConfigs.toAsset.SwapConf 7505 } 7506 7507 c.notify(newOrderNote(TopicOrderLoaded, "", "", db.Data, tracker.coreOrder())) 7508 7509 if mktConf == nil || !versCompat { 7510 if tracker.status() < order.OrderStatusExecuted { 7511 // Either we couldn't connect at startup and we don't have the 7512 // server config, or we have a server config and the market no 7513 // longer exists. Either way, revoke the order. 7514 tracker.revoke() 7515 } 7516 tracker.setSelfGoverned(true) // redeem and refund only 7517 c.log.Warnf("No server market or incompatible/missing asset configurations for trade %v, market %v, host %v!", 7518 tracker.Order.ID(), tracker.mktID, dc.acct.host) 7519 } else { 7520 tracker.setSelfGoverned(false) 7521 } 7522 7523 tradesLoaded++ 7524 } 7525 7526 c.log.Infof("Loaded %d incomplete orders with DEX %v", tradesLoaded, dc.acct.host) 7527 return nil 7528 } 7529 7530 // resumeTrade recovers the state of active matches including loading audit info 7531 // needed to finish swaps and funding coins needed to create new matches on an order. 7532 // If both of the wallets needed for this trade are able to be connected and unlocked, 7533 // readyToTick will be set to true, even if the funding coins for the order could 7534 // not be found or the audit info could not be loaded. 7535 func (c *Core) resumeTrade(tracker *trackedTrade, crypter encrypt.Crypter, failed map[uint32]bool, relocks assetMap) bool { 7536 notifyErr := func(tracker *trackedTrade, topic Topic, args ...any) { 7537 subject, detail := c.formatDetails(topic, args...) 7538 c.notify(newOrderNote(topic, subject, detail, db.ErrorLevel, tracker.coreOrderInternal())) 7539 } 7540 7541 // markUnfunded is used to allow an unfunded order to enter the trades map 7542 // so that status resolution and match negotiation for unaffected matches 7543 // may continue. By not self-revoking, the user may have the opportunity to 7544 // resolve any wallet issues that may have lead to a failure to find the 7545 // funding coins. Otherwise the server will (or already did) revoke some or 7546 // all of the matches and the order itself. 7547 markUnfunded := func(trade *trackedTrade, matches []*matchTracker) { 7548 // Block negotiating new matches. 7549 trade.changeLocked = false 7550 trade.coinsLocked = false 7551 // Block swap txn attempts on matches needing funds. 7552 for _, match := range matches { 7553 match.swapErr = errors.New("no funding coins for swap") 7554 } 7555 // Will not be retired until revoke or cancel of the order and all 7556 // matches, which may happen on status resolution after authenticating 7557 // with the DEX server, or from a revoke_match/revoke_order notification 7558 // after timeout. However, the order should be unconditionally canceled. 7559 } 7560 7561 lockStuff := func() { 7562 trade := tracker.Trade() 7563 wallets := tracker.wallets 7564 7565 // Find the least common multiplier to use as the denom for adding 7566 // reserve fractions. 7567 denom, marketMult, limitMult := lcm(uint64(len(tracker.matches)), tracker.Trade().Quantity) 7568 var refundNum, redeemNum uint64 7569 7570 addMatchRedemption := func(match *matchTracker) { 7571 if tracker.isMarketBuy() { 7572 redeemNum += marketMult // * 1 7573 } else { 7574 redeemNum += match.Quantity * limitMult 7575 } 7576 } 7577 7578 addMatchRefund := func(match *matchTracker) { 7579 if tracker.isMarketBuy() { 7580 refundNum += marketMult // * 1 7581 } else { 7582 refundNum += match.Quantity * limitMult 7583 } 7584 } 7585 7586 // If matches haven't redeemed, but the counter-swap has been received, 7587 // reload the audit info. 7588 var matchesNeedingCoins []*matchTracker 7589 for _, match := range tracker.matches { 7590 var needsAuditInfo bool 7591 var counterSwap []byte 7592 if match.Side == order.Maker { 7593 if match.Status < order.MakerSwapCast { 7594 matchesNeedingCoins = append(matchesNeedingCoins, match) 7595 } 7596 if match.Status >= order.TakerSwapCast && match.Status < order.MatchConfirmed { 7597 needsAuditInfo = true // maker needs AuditInfo for takers contract 7598 counterSwap = match.MetaData.Proof.TakerSwap 7599 } 7600 if match.Status < order.MakerRedeemed { 7601 addMatchRedemption(match) 7602 addMatchRefund(match) 7603 } 7604 } else { // Taker 7605 if match.Status < order.TakerSwapCast { 7606 matchesNeedingCoins = append(matchesNeedingCoins, match) 7607 } 7608 if match.Status < order.MatchConfirmed && match.Status >= order.MakerSwapCast { 7609 needsAuditInfo = true // taker needs AuditInfo for maker's contract 7610 counterSwap = match.MetaData.Proof.MakerSwap 7611 } 7612 if match.Status < order.MakerRedeemed { 7613 addMatchRefund(match) 7614 } 7615 if match.Status < order.MatchComplete { 7616 addMatchRedemption(match) 7617 } 7618 } 7619 c.log.Tracef("Trade %v match %v needs coins = %v, needs audit info = %v", 7620 tracker.ID(), match.MatchID, len(matchesNeedingCoins) > 0, needsAuditInfo) 7621 if needsAuditInfo { 7622 // Check for unresolvable states. 7623 if len(counterSwap) == 0 { 7624 match.swapErr = fmt.Errorf("missing counter-swap, order %s, match %s", tracker.ID(), match) 7625 notifyErr(tracker, TopicMatchErrorCoin, match.Side, tracker.token(), match.Status) 7626 continue 7627 } 7628 counterContract := match.MetaData.Proof.CounterContract 7629 if len(counterContract) == 0 { 7630 match.swapErr = fmt.Errorf("missing counter-contract, order %s, match %s", tracker.ID(), match) 7631 notifyErr(tracker, TopicMatchErrorContract, match.Side, tracker.token(), match.Status) 7632 continue 7633 } 7634 counterTxData := match.MetaData.Proof.CounterTxData 7635 7636 // Note that this does not actually audit the contract's value, 7637 // recipient, expiration, or secret hash (if maker), as that was 7638 // already done when it was initially stored as CounterScript. 7639 auditInfo, err := wallets.toWallet.AuditContract(counterSwap, counterContract, counterTxData, true) 7640 if err != nil { 7641 // This case is unlikely to happen since the original audit 7642 // message handling would have passed the audit based on the 7643 // tx data, but it depends on the asset backend. 7644 toAssetID := wallets.toWallet.AssetID 7645 contractStr := coinIDString(toAssetID, counterSwap) 7646 c.log.Warnf("Starting search for counterparty contract %v (%s)", contractStr, unbip(toAssetID)) 7647 // Start the audit retry waiter. Set swapErr to block tick 7648 // actions like counterSwap.Confirmations checks while it is 7649 // searching since matchTracker.counterSwap is not yet set. 7650 // We may consider removing this if AuditContract is an 7651 // offline action for all wallet implementations. 7652 match.swapErr = fmt.Errorf("audit in progress, please wait") // don't frighten the users 7653 go func(tracker *trackedTrade, match *matchTracker) { 7654 auditInfo, err := tracker.searchAuditInfo(match, counterSwap, counterContract, counterTxData) 7655 tracker.mtx.Lock() 7656 defer tracker.mtx.Unlock() 7657 if err != nil { // contract data could be bad, or just already spent (refunded) 7658 match.swapErr = fmt.Errorf("audit error: %w", err) 7659 // NOTE: This behaviour differs from the audit request handler behaviour for failed audits. 7660 // handleAuditRoute does NOT set a swapErr in case a revised audit request is received from 7661 // the server. Audit requests are currently NOT resent, so this difference is trivial. IF 7662 // a revised audit request did come through though, no further actions will be taken for this 7663 // match even if the revised audit passes validation. 7664 c.log.Debugf("AuditContract error for match %v status %v, refunded = %v, revoked = %v: %v", 7665 match, match.Status, len(match.MetaData.Proof.RefundCoin) > 0, 7666 match.MetaData.Proof.IsRevoked(), err) 7667 subject, detail := c.formatDetails(TopicMatchRecoveryError, 7668 unbip(toAssetID), contractStr, tracker.token(), err) 7669 c.notify(newOrderNote(TopicMatchRecoveryError, subject, detail, 7670 db.ErrorLevel, tracker.coreOrderInternal())) // tracker.mtx already locked 7671 // The match may be revoked by server. Only refund possible now. 7672 return 7673 } 7674 match.counterSwap = auditInfo 7675 match.swapErr = nil // unblock tick actions 7676 c.log.Infof("Successfully re-validated counterparty contract %v (%s)", 7677 contractStr, unbip(toAssetID)) 7678 }(tracker, match) 7679 7680 continue // leave auditInfo nil 7681 } 7682 match.counterSwap = auditInfo 7683 continue 7684 } 7685 } 7686 7687 if refundNum != 0 { 7688 tracker.lockRefundFraction(refundNum, denom) 7689 } 7690 if redeemNum != 0 { 7691 tracker.lockRedemptionFraction(redeemNum, denom) 7692 } 7693 7694 // Active orders and orders with matches with unsent swaps need funding 7695 // coin(s). If they are not found, block new matches and swap attempts. 7696 needsCoins := len(matchesNeedingCoins) > 0 7697 isActive := tracker.metaData.Status == order.OrderStatusBooked || tracker.metaData.Status == order.OrderStatusEpoch 7698 if isActive || needsCoins { 7699 coinIDs := trade.Coins 7700 if len(tracker.metaData.ChangeCoin) != 0 { 7701 coinIDs = []order.CoinID{tracker.metaData.ChangeCoin} 7702 } 7703 tracker.coins = map[string]asset.Coin{} // should already be 7704 if len(coinIDs) == 0 { 7705 notifyErr(tracker, TopicOrderCoinError, tracker.token()) 7706 markUnfunded(tracker, matchesNeedingCoins) // bug - no user resolution 7707 } else { 7708 byteIDs := make([]dex.Bytes, 0, len(coinIDs)) 7709 for _, cid := range coinIDs { 7710 byteIDs = append(byteIDs, []byte(cid)) 7711 } 7712 coins, err := wallets.fromWallet.FundingCoins(byteIDs) 7713 if err != nil || len(coins) == 0 { 7714 notifyErr(tracker, TopicOrderCoinFetchError, tracker.token(), unbip(wallets.fromWallet.AssetID), err) 7715 // Block matches needing funding coins. 7716 markUnfunded(tracker, matchesNeedingCoins) 7717 // Note: tracker is still added to trades map for (1) status 7718 // resolution, (2) continued settlement of matches that no 7719 // longer require funding coins, and (3) cancellation in 7720 // authDEX if the order is booked. 7721 c.log.Warnf("Check the status of your %s wallet and the coins logged above! "+ 7722 "Resolve the wallet issue if possible and restart Bison Wallet.", 7723 strings.ToUpper(unbip(wallets.fromWallet.AssetID))) 7724 c.log.Warnf("Unfunded order %v will be canceled on connect, but %d active matches need funding coins!", 7725 tracker.ID(), len(matchesNeedingCoins)) 7726 // If the funding coins are spent or inaccessible, the user 7727 // can only wait for match revocation. 7728 } else { 7729 // NOTE: change and changeLocked are not set even if the 7730 // funding coins were loaded from the DB's ChangeCoin. 7731 tracker.coinsLocked = true 7732 tracker.coins = mapifyCoins(coins) 7733 } 7734 } 7735 } 7736 7737 tracker.recalcFilled() 7738 7739 if isActive { 7740 tracker.lockRedemptionFraction(trade.Remaining(), trade.Quantity) 7741 tracker.lockRefundFraction(trade.Remaining(), trade.Quantity) 7742 } 7743 7744 // Balances should be updated for any orders with locked wallet coins, 7745 // or orders with funds locked in contracts. 7746 if isActive || needsCoins || tracker.unspentContractAmounts() > 0 { 7747 relocks.count(tracker.wallets.fromWallet.AssetID) 7748 if _, is := tracker.accountRedeemer(); is { 7749 relocks.count(tracker.wallets.toWallet.AssetID) 7750 } 7751 } 7752 } 7753 7754 tracker.mtx.Lock() 7755 defer tracker.mtx.Unlock() 7756 7757 if tracker.readyToTick { 7758 return true 7759 } 7760 7761 if failed[tracker.Base()] || failed[tracker.Quote()] { 7762 return false 7763 } 7764 7765 // This should never happen as every wallet added to the trades map has a 7766 // walletSet, but this is a good sanity check and also allows tests which 7767 // don't have the wallets set to not panic. 7768 if tracker.wallets == nil || tracker.wallets.baseWallet == nil || tracker.wallets.quoteWallet == nil { 7769 return false 7770 } 7771 7772 err := c.connectAndUnlockResumeTrades(crypter, tracker.wallets.baseWallet, false) 7773 if err != nil { 7774 failed[tracker.Base()] = true 7775 return false 7776 } 7777 7778 err = c.connectAndUnlockResumeTrades(crypter, tracker.wallets.quoteWallet, false) 7779 if err != nil { 7780 failed[tracker.Quote()] = true 7781 return false 7782 } 7783 7784 lockStuff() 7785 tracker.readyToTick = true 7786 return true 7787 } 7788 7789 // resumeTrades recovers the states of active trades and matches for all 7790 // trades in all dexConnection's that are not yet readyToTick. If there are no 7791 // trades that are not readyToTick, this will be a no-op. 7792 func (c *Core) resumeTrades(crypter encrypt.Crypter) { 7793 7794 failed := make(map[uint32]bool) 7795 relocks := make(assetMap) 7796 7797 for _, dc := range c.dexConnections() { 7798 for _, tracker := range dc.trackedTrades() { 7799 tracker.mtx.RLock() 7800 if tracker.readyToTick { 7801 tracker.mtx.RUnlock() 7802 continue 7803 } 7804 tracker.mtx.RUnlock() 7805 7806 if c.resumeTrade(tracker, crypter, failed, relocks) { 7807 c.notify(newOrderNote(TopicOrderLoaded, "", "", db.Data, tracker.coreOrder())) 7808 } else { 7809 tracker.mtx.RLock() 7810 err := fmt.Errorf("failed to connect and unlock wallets for trade ID %s", tracker.ID()) 7811 tracker.mtx.RUnlock() 7812 subject, details := c.formatDetails(TopicOrderResumeFailure, err) 7813 c.notify(newOrderNote(TopicOrderResumeFailure, subject, details, db.ErrorLevel, nil)) 7814 } 7815 } 7816 } 7817 7818 c.updateBalances(relocks) 7819 } 7820 7821 // reReserveFunding reserves funding coins for a newly instantiated wallet. 7822 // reReserveFunding is closely modeled on resumeTrades, so see resumeTrades for 7823 // docs. 7824 func (c *Core) reReserveFunding(w *xcWallet) { 7825 7826 markUnfunded := func(trade *trackedTrade, matches []*matchTracker) { 7827 trade.changeLocked = false 7828 trade.coinsLocked = false 7829 for _, match := range matches { 7830 match.swapErr = errors.New("no funding coins for swap") 7831 } 7832 } 7833 7834 c.updateBondReserves(w.AssetID) 7835 7836 for _, dc := range c.dexConnections() { 7837 for _, tracker := range dc.trackedTrades() { 7838 // TODO: Consider tokens 7839 if tracker.Base() != w.AssetID && tracker.Quote() != w.AssetID { 7840 continue 7841 } 7842 7843 notifyErr := func(topic Topic, args ...any) { 7844 subject, detail := c.formatDetails(topic, args...) 7845 c.notify(newOrderNote(topic, subject, detail, db.ErrorLevel, tracker.coreOrderInternal())) 7846 } 7847 7848 trade := tracker.Trade() 7849 7850 fromID := tracker.Quote() 7851 if trade.Sell { 7852 fromID = tracker.Base() 7853 } 7854 7855 denom, marketMult, limitMult := lcm(uint64(len(tracker.matches)), trade.Quantity) 7856 var refundNum, redeemNum uint64 7857 7858 addMatchRedemption := func(match *matchTracker) { 7859 if tracker.isMarketBuy() { 7860 redeemNum += marketMult // * 1 7861 } else { 7862 redeemNum += match.Quantity * limitMult 7863 } 7864 } 7865 7866 addMatchRefund := func(match *matchTracker) { 7867 if tracker.isMarketBuy() { 7868 refundNum += marketMult // * 1 7869 } else { 7870 refundNum += match.Quantity * limitMult 7871 } 7872 } 7873 7874 isActive := tracker.metaData.Status == order.OrderStatusBooked || tracker.metaData.Status == order.OrderStatusEpoch 7875 var matchesNeedingCoins []*matchTracker 7876 for _, match := range tracker.matches { 7877 if match.Side == order.Maker { 7878 if match.Status < order.MakerSwapCast { 7879 matchesNeedingCoins = append(matchesNeedingCoins, match) 7880 } 7881 if match.Status < order.MakerRedeemed { 7882 addMatchRedemption(match) 7883 addMatchRefund(match) 7884 } 7885 } else { // Taker 7886 if match.Status < order.TakerSwapCast { 7887 matchesNeedingCoins = append(matchesNeedingCoins, match) 7888 } 7889 if match.Status < order.MakerRedeemed { 7890 addMatchRefund(match) 7891 } 7892 if match.Status < order.MatchComplete { 7893 addMatchRedemption(match) 7894 } 7895 } 7896 } 7897 7898 if c.ctx.Err() != nil { 7899 return 7900 } 7901 7902 // Prepare funding coins, but don't update tracker until the mutex 7903 // is locked. 7904 needsCoins := len(matchesNeedingCoins) > 0 7905 // nil coins = no locking required, empty coins = something went 7906 // wrong, non-empty means locking required. 7907 var coins asset.Coins 7908 if fromID == w.AssetID && (isActive || needsCoins) { 7909 coins = []asset.Coin{} // should already be 7910 coinIDs := trade.Coins 7911 if len(tracker.metaData.ChangeCoin) != 0 { 7912 coinIDs = []order.CoinID{tracker.metaData.ChangeCoin} 7913 } 7914 if len(coinIDs) == 0 { 7915 notifyErr(TopicOrderCoinError, tracker.token()) 7916 markUnfunded(tracker, matchesNeedingCoins) // bug - no user resolution 7917 } else { 7918 byteIDs := make([]dex.Bytes, 0, len(coinIDs)) 7919 for _, cid := range coinIDs { 7920 byteIDs = append(byteIDs, []byte(cid)) 7921 } 7922 var err error 7923 coins, err = w.FundingCoins(byteIDs) 7924 if err != nil || len(coins) == 0 { 7925 notifyErr(TopicOrderCoinFetchError, tracker.token(), unbip(fromID), err) 7926 c.log.Warnf("(re-reserve) Check the status of your %s wallet and the coins logged above! "+ 7927 "Resolve the wallet issue if possible and restart Bison Wallet.", 7928 strings.ToUpper(unbip(fromID))) 7929 c.log.Warnf("(re-reserve) Unfunded order %v will be revoked if %d active matches don't get funding coins!", 7930 tracker.ID(), len(matchesNeedingCoins)) 7931 } 7932 } 7933 } 7934 7935 tracker.mtx.Lock() 7936 7937 // Refund and redemption reserves for active matches. Doing this 7938 // under mutex lock, but noting that the underlying calls to 7939 // ReReserveRedemption and ReReserveRefund could potentially involve 7940 // long-running RPC calls. 7941 if fromID == w.AssetID { 7942 tracker.refundLocked = 0 7943 if refundNum != 0 { 7944 tracker.lockRefundFraction(refundNum, denom) 7945 } 7946 } else { 7947 tracker.redemptionLocked = 0 7948 if redeemNum != 0 { 7949 tracker.lockRedemptionFraction(redeemNum, denom) 7950 } 7951 } 7952 7953 // Funding coins 7954 if coins != nil { 7955 tracker.coinsLocked = len(coins) > 0 7956 tracker.coins = mapifyCoins(coins) 7957 } 7958 7959 // Refund and redemption reserves for booked orders. 7960 7961 tracker.recalcFilled() // Make sure Remaining is accurate. 7962 7963 if isActive { 7964 if fromID == w.AssetID { 7965 tracker.lockRefundFraction(trade.Remaining(), trade.Quantity) 7966 } else { 7967 tracker.lockRedemptionFraction(trade.Remaining(), trade.Quantity) 7968 } 7969 } 7970 7971 tracker.mtx.Unlock() 7972 } 7973 } 7974 } 7975 7976 // generateDEXMaps creates the associated assets, market and epoch maps of the 7977 // DEXs from the provided configuration. 7978 func generateDEXMaps(host string, cfg *msgjson.ConfigResult) (map[uint32]*dex.Asset, map[string]uint64, error) { 7979 assets := make(map[uint32]*dex.Asset, len(cfg.Assets)) 7980 for _, asset := range cfg.Assets { 7981 assets[asset.ID] = convertAssetInfo(asset) 7982 } 7983 // Validate the markets so we don't have to check every time later. 7984 for _, mkt := range cfg.Markets { 7985 _, ok := assets[mkt.Base] 7986 if !ok { 7987 return nil, nil, fmt.Errorf("%s reported a market with base "+ 7988 "asset %d, but did not provide the asset info.", host, mkt.Base) 7989 } 7990 _, ok = assets[mkt.Quote] 7991 if !ok { 7992 return nil, nil, fmt.Errorf("%s reported a market with quote "+ 7993 "asset %d, but did not provide the asset info.", host, mkt.Quote) 7994 } 7995 } 7996 7997 epochMap := make(map[string]uint64) 7998 for _, mkt := range cfg.Markets { 7999 epochMap[mkt.Name] = 0 8000 } 8001 8002 return assets, epochMap, nil 8003 } 8004 8005 // runMatches runs the sorted matches returned from parseMatches. 8006 func (c *Core) runMatches(tradeMatches map[order.OrderID]*serverMatches) (assetMap, error) { 8007 runMatch := func(sm *serverMatches) (assetMap, error) { 8008 updatedAssets := make(assetMap) 8009 tracker := sm.tracker 8010 oid := tracker.ID() 8011 8012 // Verify and record any cancel Match targeting this trade. 8013 if sm.cancel != nil { 8014 err := tracker.processCancelMatch(sm.cancel) 8015 if err != nil { 8016 return updatedAssets, fmt.Errorf("processCancelMatch for cancel order %v targeting order %v failed: %w", 8017 sm.cancel.OrderID, oid, err) 8018 } 8019 } 8020 8021 // Begin negotiation for any trade Matches. 8022 if len(sm.msgMatches) > 0 { 8023 tracker.mtx.Lock() 8024 err := tracker.negotiate(sm.msgMatches) 8025 tracker.mtx.Unlock() 8026 if err != nil { 8027 return updatedAssets, fmt.Errorf("negotiate order %v matches failed: %w", oid, err) 8028 } 8029 8030 // Coins may be returned for canceled orders. 8031 tracker.mtx.RLock() 8032 if tracker.metaData.Status == order.OrderStatusCanceled { 8033 updatedAssets.count(tracker.fromAssetID) 8034 if _, is := tracker.wallets.toWallet.Wallet.(asset.AccountLocker); is { 8035 updatedAssets.count(tracker.wallets.toWallet.AssetID) 8036 } 8037 } 8038 tracker.mtx.RUnlock() 8039 8040 // Try to tick the trade now, but do not interrupt on error. The 8041 // trade will tick again automatically. 8042 tickUpdatedAssets, err := c.tick(tracker) 8043 updatedAssets.merge(tickUpdatedAssets) 8044 if err != nil { 8045 return updatedAssets, fmt.Errorf("tick of order %v failed: %w", oid, err) 8046 } 8047 } 8048 8049 return updatedAssets, nil 8050 } 8051 8052 // Process the trades concurrently. 8053 type runMatchResult struct { 8054 updatedAssets assetMap 8055 err error 8056 } 8057 resultChan := make(chan *runMatchResult) 8058 for _, trade := range tradeMatches { 8059 go func(trade *serverMatches) { 8060 assetsUpdated, err := runMatch(trade) 8061 resultChan <- &runMatchResult{assetsUpdated, err} 8062 }(trade) 8063 } 8064 8065 errs := newErrorSet("runMatches - ") 8066 assetsUpdated := make(assetMap) 8067 for range tradeMatches { 8068 result := <-resultChan 8069 assetsUpdated.merge(result.updatedAssets) // assets might be updated even if an error occurs 8070 if result.err != nil { 8071 errs.addErr(result.err) 8072 } 8073 } 8074 8075 return assetsUpdated, errs.ifAny() 8076 } 8077 8078 // sendOutdatedClientNotification will send a notification to the UI that 8079 // indicates the client should be updated to be used with this DEX server. 8080 func sendOutdatedClientNotification(c *Core, dc *dexConnection) { 8081 subject, details := c.formatDetails(TopicUpgradeNeeded, dc.acct.host) 8082 c.notify(newUpgradeNote(TopicUpgradeNeeded, subject, details, db.WarningLevel)) 8083 } 8084 8085 func isOnionHost(addr string) bool { 8086 host, _, err := net.SplitHostPort(addr) 8087 if err != nil { 8088 return false 8089 } 8090 return strings.HasSuffix(host, ".onion") 8091 } 8092 8093 type connectDEXFlag uint8 8094 8095 const ( 8096 connectDEXFlagTemporary connectDEXFlag = 1 << iota 8097 connectDEXFlagViewOnly 8098 ) 8099 8100 // connectDEX is like connectDEXWithFlag but always creates a full connection 8101 // for use with a trading account. For a temporary or view-only dexConnection, 8102 // use connectDEXWithFlag. 8103 func (c *Core) connectDEX(acctInfo *db.AccountInfo) (*dexConnection, error) { 8104 return c.connectDEXWithFlag(acctInfo, 0) 8105 } 8106 8107 // connectDEXWithFlag establishes a ws connection to a DEX server using the 8108 // provided account info, but does not authenticate the connection through the 8109 // 'connect' route. If the connectDEXFlagTemporary bit is set in flag, the 8110 // c.listen(dc) goroutine is not started so that associated trades are not 8111 // processed and no incoming requests and notifications are handled. A temporary 8112 // dexConnection may be used to inspect the config response or check if a (paid) 8113 // HD account exists with a DEX. If connecting fails, there are no retries. To 8114 // allow an initial connection error to begin a reconnect loop, either use the 8115 // connectAccount method, or manually use newDEXConnection and 8116 // startDexConnection to tolerate initial connection failure. 8117 func (c *Core) connectDEXWithFlag(acctInfo *db.AccountInfo, flag connectDEXFlag) (*dexConnection, error) { 8118 dc, err := c.newDEXConnection(acctInfo, flag) 8119 if err != nil { 8120 return nil, err 8121 } 8122 8123 err = c.startDexConnection(acctInfo, dc) 8124 if err != nil { 8125 dc.connMaster.Disconnect() // stop any retry loop for this new connection. 8126 return nil, err 8127 } 8128 8129 return dc, nil 8130 } 8131 8132 // newDEXConnection creates a new valid instance of *dexConnection. 8133 func (c *Core) newDEXConnection(acctInfo *db.AccountInfo, flag connectDEXFlag) (*dexConnection, error) { 8134 // Get the host from the DEX URL. 8135 host, err := addrHost(acctInfo.Host) 8136 if err != nil { 8137 return nil, newError(addressParseErr, "error parsing address: %v", err) 8138 } 8139 wsURL, err := url.Parse("wss://" + host + "/ws") 8140 if err != nil { 8141 return nil, newError(addressParseErr, "error parsing ws address from host %s: %w", host, err) 8142 } 8143 8144 listen := flag&connectDEXFlagTemporary == 0 8145 viewOnly := flag&connectDEXFlagViewOnly != 0 8146 var reporting uint32 8147 if listen { 8148 reporting = 1 8149 } 8150 8151 dc := &dexConnection{ 8152 log: c.log, 8153 acct: newDEXAccount(acctInfo, viewOnly), 8154 notify: c.notify, 8155 ticker: newDexTicker(defaultTickInterval), // updated when server config obtained 8156 books: make(map[string]*bookie), 8157 trades: make(map[order.OrderID]*trackedTrade), 8158 cancels: make(map[order.OrderID]order.OrderID), 8159 inFlightOrders: make(map[uint64]*InFlightOrder), 8160 blindCancels: make(map[order.OrderID]order.Preimage), 8161 apiVer: -1, 8162 reportingConnects: reporting, 8163 spots: make(map[string]*msgjson.Spot), 8164 connectionStatus: uint32(comms.Disconnected), 8165 // On connect, must set: cfg, epoch, and assets. 8166 } 8167 8168 wsCfg := comms.WsCfg{ 8169 URL: wsURL.String(), 8170 PingWait: 20 * time.Second, // larger than server's pingPeriod (server/comms/server.go) 8171 Cert: acctInfo.Cert, 8172 Logger: c.log.SubLogger(wsURL.String()), 8173 } 8174 8175 isOnionHost := isOnionHost(wsURL.Host) 8176 if isOnionHost || c.cfg.TorProxy != "" { 8177 proxyAddr := c.cfg.TorProxy 8178 if isOnionHost { 8179 if c.cfg.Onion == "" { 8180 return nil, errors.New("tor must be configured for .onion addresses") 8181 } 8182 proxyAddr = c.cfg.Onion 8183 8184 wsURL.Scheme = "ws" 8185 wsCfg.URL = wsURL.String() 8186 } 8187 proxy := &socks.Proxy{ 8188 Addr: proxyAddr, 8189 TorIsolation: c.cfg.TorIsolation, // need socks.NewPool with isolation??? 8190 } 8191 wsCfg.NetDialContext = proxy.DialContext 8192 } 8193 8194 wsCfg.ConnectEventFunc = func(status comms.ConnectionStatus) { 8195 c.handleConnectEvent(dc, status) 8196 } 8197 wsCfg.ReconnectSync = func() { 8198 go c.handleReconnect(host) 8199 } 8200 8201 // Create a websocket "connection" to the server. (Don't actually connect.) 8202 conn, err := c.wsConstructor(&wsCfg) 8203 if err != nil { 8204 return nil, err 8205 } 8206 8207 dc.WsConn = conn 8208 dc.connMaster = dex.NewConnectionMaster(conn) 8209 8210 return dc, nil 8211 } 8212 8213 // startDexConnection attempts to connect the provided dexConnection. dc must be 8214 // a new dexConnection returned from newDEXConnection above. Callers can choose 8215 // to stop reconnect retries and any current goroutine for the provided 8216 // dexConnection using dc.connMaster.Disconnect(). 8217 func (c *Core) startDexConnection(acctInfo *db.AccountInfo, dc *dexConnection) error { 8218 // Start listening for messages. The listener stops when core shuts down or 8219 // the dexConnection's ConnectionMaster is shut down. This goroutine should 8220 // be started as long as the reconnect loop is running. It only returns when 8221 // the wsConn is stopped. 8222 listen := dc.broadcastingConnect() && !dc.acct.isDisabled() 8223 if listen { 8224 c.wg.Add(1) 8225 go c.listen(dc) 8226 } 8227 8228 // Categorize bonds now for sake of expired bonds that need to be refunded. 8229 categorizeBonds := func(lockTimeThresh int64) { 8230 dc.acct.authMtx.Lock() 8231 defer dc.acct.authMtx.Unlock() 8232 8233 for _, dbBond := range acctInfo.Bonds { 8234 if dbBond.Refunded { // maybe don't even load these, but it may be of use for record keeping 8235 continue 8236 } 8237 8238 // IDEA: unspent bonds to register with wallet on first connect, if 8239 // we need wallet Disconnect to *not* clear reserves. 8240 // dc.acct.unreserved = append(dc.acct.unreserved, dbBond) 8241 8242 bondIDStr := coinIDString(dbBond.AssetID, dbBond.CoinID) 8243 8244 if int64(dbBond.LockTime) <= lockTimeThresh { 8245 c.log.Infof("Loaded expired bond %v. Refund tx: %v", bondIDStr, dbBond.RefundTx) 8246 dc.acct.expiredBonds = append(dc.acct.expiredBonds, dbBond) 8247 continue 8248 } 8249 8250 if dbBond.Confirmed { 8251 // This bond has already been confirmed by the server. 8252 c.log.Infof("Loaded active bond %v. BACKUP refund tx: %v", bondIDStr, dbBond.RefundTx) 8253 dc.acct.bonds = append(dc.acct.bonds, dbBond) 8254 continue 8255 } 8256 8257 // Server has not yet confirmed this bond. 8258 c.log.Infof("Loaded pending bond %v. Refund tx: %v", bondIDStr, dbBond.RefundTx) 8259 dc.acct.pendingBonds = append(dc.acct.pendingBonds, dbBond) 8260 8261 // We need to start monitorBondConfs on login since postbond 8262 // requires the account keys. 8263 } 8264 8265 // Now in authDEX, we must reconcile the above categorized bonds 8266 // according to ConnectResult.Bonds slice. 8267 } 8268 8269 if dc.acct.isDisabled() { 8270 // Sort out the bonds with current time to indicate refundable bonds. 8271 categorizeBonds(time.Now().Unix()) 8272 return nil // nothing else to do 8273 } 8274 8275 err := dc.connMaster.Connect(c.ctx) 8276 if err != nil { 8277 // Sort out the bonds with current time to indicate refundable bonds. 8278 categorizeBonds(time.Now().Unix()) 8279 // Not connected, but reconnect cycle is running. Caller should track 8280 // this dexConnection, and a listen goroutine must be running to handle 8281 // messages received when the connection is eventually established. 8282 return err 8283 } 8284 8285 // Request the market configuration. 8286 cfg, err := dc.refreshServerConfig() // handleReconnect must too 8287 if err != nil { 8288 // Sort out the bonds with current time to indicate refundable bonds. 8289 categorizeBonds(time.Now().Unix()) 8290 if errors.Is(err, outdatedClientErr) { 8291 sendOutdatedClientNotification(c, dc) 8292 } 8293 return err // no dc.acct.dexPubKey 8294 } 8295 // handleConnectEvent sets dc.connected, even on first connect 8296 8297 // Given bond config, sort through our db.Bond slice. 8298 categorizeBonds(time.Now().Unix() + int64(cfg.BondExpiry)) 8299 8300 if listen { 8301 c.log.Infof("Connected to DEX server at %s and listening for messages.", dc.acct.host) 8302 go dc.subPriceFeed() 8303 } else { 8304 c.log.Infof("Connected to DEX server at %s but NOT listening for messages.", dc.acct.host) 8305 } 8306 8307 return nil 8308 } 8309 8310 // handleReconnect is called when a WsConn indicates that a lost connection has 8311 // been re-established. 8312 func (c *Core) handleReconnect(host string) { 8313 c.connMtx.RLock() 8314 dc, found := c.conns[host] 8315 c.connMtx.RUnlock() 8316 if !found { 8317 c.log.Errorf("handleReconnect: Unable to find previous connection to DEX at %s", host) 8318 return 8319 } 8320 8321 // The server's configuration may have changed, so retrieve the current 8322 // server configuration. 8323 cfg, err := dc.refreshServerConfig() 8324 if err != nil { 8325 if errors.Is(err, outdatedClientErr) { 8326 sendOutdatedClientNotification(c, dc) 8327 } 8328 c.log.Errorf("handleReconnect: Unable to apply new configuration for DEX at %s: %v", host, err) 8329 return 8330 } 8331 8332 type market struct { // for book re-subscribe 8333 name string 8334 base uint32 8335 quote uint32 8336 } 8337 mkts := make(map[string]*market, len(dc.cfg.Markets)) 8338 for _, m := range cfg.Markets { 8339 mkts[m.Name] = &market{ 8340 name: m.Name, 8341 base: m.Base, 8342 quote: m.Quote, 8343 } 8344 } 8345 8346 // Update the orders' selfGoverned flag according to the configured markets. 8347 for _, trade := range dc.trackedTrades() { 8348 // If the server's market is gone, we're on our own, otherwise we are 8349 // now free to swap for this order. 8350 auto := mkts[trade.mktID] == nil 8351 if !auto { // market exists, now check asset config and version 8352 baseCfg := dc.assetConfig(trade.Base()) 8353 auto = baseCfg == nil || !trade.wallets.baseWallet.supportsVer(baseCfg.Version) 8354 } 8355 if !auto { 8356 quoteCfg := dc.assetConfig(trade.Quote()) 8357 auto = quoteCfg == nil || !trade.wallets.quoteWallet.supportsVer(quoteCfg.Version) 8358 } 8359 8360 if trade.setSelfGoverned(auto) { 8361 if auto { 8362 c.log.Warnf("DEX %v is MISSING/INCOMPATIBLE market %v for trade %v!", host, trade.mktID, trade.ID()) 8363 } else { 8364 c.log.Infof("DEX %v with market %v restored for trade %v", host, trade.mktID, trade.ID()) 8365 } 8366 } 8367 // We could refresh the asset configs in the walletSet, but we'll stick 8368 // to what we have recorded in OrderMetaData at time of order placement. 8369 } 8370 8371 go dc.subPriceFeed() 8372 8373 // If this isn't a view-only connection, authenticate. 8374 if !dc.acct.isViewOnly() { 8375 if !dc.acct.locked() /* && dc.acct.feePaid() */ { 8376 err = c.authDEX(dc) 8377 if err != nil { 8378 c.log.Errorf("handleReconnect: Unable to authorize DEX at %s: %v", host, err) 8379 return 8380 } 8381 } else { 8382 c.log.Infof("Connection to %v established, but you still need to login.", host) 8383 // Continue to resubscribe to market fees. 8384 } 8385 } 8386 8387 // Now that reconcileTrades has been run in authDEX, make a list of epoch 8388 // status orders that should be re-checked in the next epoch because we may 8389 // have missed the preimage request while disconnected. 8390 epochOrders := make(map[string][]*trackedTrade) 8391 for _, trade := range dc.trackedTrades() { 8392 if trade.status() == order.OrderStatusEpoch { 8393 epochOrders[trade.mktID] = append(epochOrders[trade.mktID], trade) 8394 } 8395 } 8396 for mkt := range epochOrders { 8397 trades := epochOrders[mkt] // don't capture loop var below 8398 time.AfterFunc( 8399 preimageReqTimeout+time.Duration(dc.marketEpochDuration(mkt))*time.Millisecond, 8400 func() { 8401 if c.ctx.Err() != nil { 8402 return // core shut down 8403 } 8404 var stillEpochOrders []*trackedTrade 8405 for _, trade := range trades { 8406 if trade.status() == order.OrderStatusEpoch { 8407 stillEpochOrders = append(stillEpochOrders, trade) 8408 } 8409 } 8410 if len(stillEpochOrders) > 0 { 8411 dc.syncOrderStatuses(stillEpochOrders) 8412 } 8413 }, 8414 ) 8415 } 8416 8417 resubMkt := func(mkt *market) { 8418 // Locate any bookie for this market. 8419 booky := dc.bookie(mkt.name) 8420 if booky == nil { 8421 // Was not previously subscribed with the server for this market. 8422 return 8423 } 8424 8425 // Resubscribe since our old subscription was probably lost by the 8426 // server when the connection dropped. 8427 snap, err := dc.subscribe(mkt.base, mkt.quote) 8428 if err != nil { 8429 c.log.Errorf("handleReconnect: Failed to Subscribe to market %q 'orderbook': %v", mkt.name, err) 8430 return 8431 } 8432 8433 // Create a fresh OrderBook for the bookie. 8434 err = booky.Reset(snap) 8435 if err != nil { 8436 c.log.Errorf("handleReconnect: Failed to Sync market %q order book snapshot: %v", mkt.name, err) 8437 } 8438 8439 // Send a FreshBookAction to the subscribers. 8440 booky.send(&BookUpdate{ 8441 Action: FreshBookAction, 8442 Host: dc.acct.host, 8443 MarketID: mkt.name, 8444 Payload: &MarketOrderBook{ 8445 Base: mkt.base, 8446 Quote: mkt.quote, 8447 Book: booky.book(), 8448 }, 8449 }) 8450 } 8451 8452 // For each market, resubscribe to any market books. 8453 for _, mkt := range mkts { 8454 resubMkt(mkt) 8455 } 8456 } 8457 8458 func (dc *dexConnection) broadcastingConnect() bool { 8459 return atomic.LoadUint32(&dc.reportingConnects) == 1 8460 } 8461 8462 // handleConnectEvent is called when a WsConn indicates that a connection was 8463 // lost or established. 8464 // 8465 // NOTE: Disconnect event notifications may lag behind actual disconnections. 8466 func (c *Core) handleConnectEvent(dc *dexConnection, status comms.ConnectionStatus) { 8467 atomic.StoreUint32(&dc.connectionStatus, uint32(status)) 8468 8469 topic := TopicDEXDisconnected 8470 if status == comms.Connected { 8471 topic = TopicDEXConnected 8472 dc.lastConnectMtx.Lock() 8473 dc.lastConnect = time.Now() 8474 dc.lastConnectMtx.Unlock() 8475 } else { 8476 dc.lastConnectMtx.RLock() 8477 lastConnect := dc.lastConnect 8478 dc.lastConnectMtx.RUnlock() 8479 if time.Since(lastConnect) < wsAnomalyDuration { 8480 // Increase anomalies count for this connection. 8481 count := atomic.AddUint32(&dc.anomaliesCount, 1) 8482 if count%wsMaxAnomalyCount == 0 { 8483 // Send notification to check connectivity. 8484 subject, details := c.formatDetails(TopicDexConnectivity, dc.acct.host) 8485 c.notify(newConnEventNote(TopicDexConnectivity, subject, dc.acct.host, dc.status(), details, db.Poke)) 8486 } 8487 } else { 8488 atomic.StoreUint32(&dc.anomaliesCount, 0) 8489 } 8490 8491 for _, tracker := range dc.trackedTrades() { 8492 tracker.setSelfGoverned(true) // reconnect handles unflagging based on fresh market config 8493 8494 tracker.mtx.RLock() 8495 for _, match := range tracker.matches { 8496 // Make sure that a taker will not prematurely send an 8497 // initialization until it is confirmed with the server 8498 // that the match is not revoked. 8499 if match.Side == order.Taker && match.Status == order.MakerSwapCast { 8500 match.exceptionMtx.Lock() 8501 match.checkServerRevoke = true 8502 match.exceptionMtx.Unlock() 8503 } 8504 } 8505 tracker.mtx.RUnlock() 8506 } 8507 } 8508 8509 if dc.broadcastingConnect() { 8510 subject, details := c.formatDetails(topic, dc.acct.host) 8511 dc.notify(newConnEventNote(topic, subject, dc.acct.host, status, details, db.Poke)) 8512 } 8513 } 8514 8515 // handleMatchProofMsg is called when a match_proof notification is received. 8516 func handleMatchProofMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error { 8517 var note msgjson.MatchProofNote 8518 err := msg.Unmarshal(¬e) 8519 if err != nil { 8520 return fmt.Errorf("match proof note unmarshal error: %w", err) 8521 } 8522 8523 // Expire the epoch 8524 dc.setEpoch(note.MarketID, note.Epoch+1) 8525 8526 book := dc.bookie(note.MarketID) 8527 if book == nil { 8528 return fmt.Errorf("no order book found with market id %q", 8529 note.MarketID) 8530 } 8531 8532 err = book.ValidateMatchProof(note) 8533 if err != nil { 8534 return fmt.Errorf("match proof validation failed: %w", err) 8535 } 8536 8537 // Validate match_proof commitment checksum for client orders in this epoch. 8538 for _, trade := range dc.trackedTrades() { 8539 if note.MarketID != trade.mktID { 8540 continue 8541 } 8542 8543 // Validation can fail either due to server trying to cheat (by 8544 // requesting a preimage before closing the epoch to more orders), or 8545 // client losing trades' epoch csums (e.g. due to restarting, since we 8546 // don't persistently store these at the moment). 8547 // 8548 // Just warning the user for now, later on we might wanna revoke the 8549 // order if this happens. 8550 if err = trade.verifyCSum(note.CSum, note.Epoch); err != nil { 8551 c.log.Warnf("Failed to validate commitment checksum for %s epoch %d at %s: %v", 8552 note.MarketID, note.Epoch, dc.acct.host, err) 8553 } 8554 } 8555 8556 return nil 8557 } 8558 8559 // handleRevokeOrderMsg is called when a revoke_order message is received. 8560 func handleRevokeOrderMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error { 8561 var revocation msgjson.RevokeOrder 8562 err := msg.Unmarshal(&revocation) 8563 if err != nil { 8564 return fmt.Errorf("revoke order unmarshal error: %w", err) 8565 } 8566 8567 var oid order.OrderID 8568 copy(oid[:], revocation.OrderID) 8569 8570 tracker, isCancel := dc.findOrder(oid) 8571 if tracker == nil { 8572 return fmt.Errorf("no order found with id %s", oid.String()) 8573 } 8574 8575 if isCancel { 8576 // Cancel order revoked (e.g. we missed the preimage request). Don't 8577 // revoke the targeted order, just unlink the cancel order. 8578 c.log.Warnf("Deleting failed cancel order %v that targeted trade order %v", oid, tracker.ID()) 8579 tracker.deleteCancelOrder() 8580 subject, details := c.formatDetails(TopicFailedCancel, tracker.token()) 8581 c.notify(newOrderNote(TopicFailedCancel, subject, details, db.WarningLevel, tracker.coreOrder())) 8582 return nil 8583 } 8584 8585 if tracker.status() == order.OrderStatusRevoked { 8586 // Already revoked is expected if entire book was purged in a suspend 8587 // ntfn, which emits a gentler and more informative notification. 8588 // However, we may not be subscribed to orderbook notifications. 8589 return nil 8590 } 8591 tracker.revoke() 8592 8593 subject, details := c.formatDetails(TopicOrderRevoked, tracker.token(), tracker.mktID, dc.acct.host) 8594 c.notify(newOrderNote(TopicOrderRevoked, subject, details, db.ErrorLevel, tracker.coreOrder())) 8595 8596 // Update market orders, and the balance to account for unlocked coins. 8597 c.updateAssetBalance(tracker.fromAssetID) 8598 return nil 8599 } 8600 8601 // handleRevokeMatchMsg is called when a revoke_match message is received. 8602 func handleRevokeMatchMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error { 8603 var revocation msgjson.RevokeMatch 8604 err := msg.Unmarshal(&revocation) 8605 if err != nil { 8606 return fmt.Errorf("revoke match unmarshal error: %w", err) 8607 } 8608 8609 var oid order.OrderID 8610 copy(oid[:], revocation.OrderID) 8611 8612 tracker, _ := dc.findOrder(oid) 8613 if tracker == nil { 8614 return fmt.Errorf("no order found with id %s (not an error if you've completed your side of the swap)", oid.String()) 8615 } 8616 8617 if len(revocation.MatchID) != order.MatchIDSize { 8618 return fmt.Errorf("invalid match ID %v", revocation.MatchID) 8619 } 8620 8621 var matchID order.MatchID 8622 copy(matchID[:], revocation.MatchID) 8623 8624 tracker.mtx.Lock() 8625 err = tracker.revokeMatch(matchID, true) 8626 tracker.mtx.Unlock() 8627 if err != nil { 8628 return fmt.Errorf("unable to revoke match %s for order %s: %w", matchID, tracker.ID(), err) 8629 } 8630 8631 // Update market orders, and the balance to account for unlocked coins. 8632 c.updateAssetBalance(tracker.fromAssetID) 8633 return nil 8634 } 8635 8636 // handleNotifyMsg is called when a notify notification is received. 8637 func handleNotifyMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error { 8638 var txt string 8639 err := msg.Unmarshal(&txt) 8640 if err != nil { 8641 return fmt.Errorf("notify unmarshal error: %w", err) 8642 } 8643 subject, details := c.formatDetails(TopicDEXNotification, dc.acct.host, txt) 8644 c.notify(newServerNotifyNote(TopicDEXNotification, subject, details, db.WarningLevel)) 8645 return nil 8646 } 8647 8648 // handlePenaltyMsg is called when a Penalty notification is received. 8649 // 8650 // TODO: Consider other steps needed to take immediately after being banned. 8651 func handlePenaltyMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error { 8652 var note msgjson.PenaltyNote 8653 err := msg.Unmarshal(¬e) 8654 if err != nil { 8655 return fmt.Errorf("penalty note unmarshal error: %w", err) 8656 } 8657 // Check the signature. 8658 err = dc.acct.checkSig(note.Serialize(), note.Sig) 8659 if err != nil { 8660 return newError(signatureErr, "handlePenaltyMsg: DEX signature validation error: %w", err) 8661 } 8662 t := time.UnixMilli(int64(note.Penalty.Time)) 8663 8664 subject, details := c.formatDetails(TopicPenalized, dc.acct.host, note.Penalty.Rule, t, note.Penalty.Details) 8665 c.notify(newServerNotifyNote(TopicPenalized, subject, details, db.WarningLevel)) 8666 return nil 8667 } 8668 8669 func handleTierChangeMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error { 8670 var tierChanged *msgjson.TierChangedNotification 8671 err := msg.Unmarshal(&tierChanged) 8672 if err != nil { 8673 return fmt.Errorf("tier changed note unmarshal error: %w", err) 8674 } 8675 if tierChanged == nil { 8676 return errors.New("empty message") 8677 } 8678 // Check the signature. 8679 err = dc.acct.checkSig(tierChanged.Serialize(), tierChanged.Sig) 8680 if err != nil { 8681 return newError(signatureErr, "handleTierChangeMsg: DEX signature validation error: %v", err) // warn? 8682 } 8683 dc.acct.authMtx.Lock() 8684 dc.updateReputation(tierChanged.Reputation) 8685 targetTier := dc.acct.targetTier 8686 rep := dc.acct.rep 8687 dc.acct.authMtx.Unlock() 8688 c.log.Infof("Received tierchanged notification from %v for account %v. New tier = %v (target = %d)", 8689 dc.acct.host, dc.acct.ID(), tierChanged.Tier, targetTier) 8690 c.notify(newReputationNote(dc.acct.host, rep)) 8691 return nil 8692 } 8693 8694 func handleScoreChangeMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error { 8695 var scoreChange *msgjson.ScoreChangedNotification 8696 err := msg.Unmarshal(&scoreChange) 8697 if err != nil { 8698 return fmt.Errorf("tier changed note unmarshal error: %w", err) 8699 } 8700 if scoreChange == nil { 8701 return errors.New("empty message") 8702 } 8703 8704 // Check the signature. 8705 err = dc.acct.checkSig(scoreChange.Serialize(), scoreChange.Sig) 8706 if err != nil { 8707 return newError(signatureErr, "handleScoreChangeMsg: DEX signature validation error: %v", err) // warn? 8708 } 8709 8710 r := scoreChange.Reputation 8711 tier := r.EffectiveTier() 8712 8713 dc.acct.authMtx.Lock() 8714 dc.updateReputation(&r) 8715 dc.acct.authMtx.Unlock() 8716 8717 dc.log.Debugf("Score changed at %s. New score is %d / %d, tier = %d, penalties = %d", 8718 dc.acct.host, r.Score, dc.maxScore(), tier, r.Penalties) 8719 8720 c.notify(newReputationNote(dc.acct.host, r)) 8721 return nil 8722 } 8723 8724 func handleBondExpiredMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error { 8725 var bondExpired *msgjson.BondExpiredNotification 8726 err := msg.Unmarshal(&bondExpired) 8727 if err != nil { 8728 return fmt.Errorf("bond expired note unmarshal error: %w", err) 8729 } 8730 if bondExpired == nil { 8731 return errors.New("empty message") 8732 } 8733 // Check the signature. 8734 err = dc.acct.checkSig(bondExpired.Serialize(), bondExpired.Sig) 8735 if err != nil { 8736 return newError(signatureErr, "handleBondExpiredMsg: DEX signature validation error: %v", err) // warn? 8737 } 8738 8739 acctID := dc.acct.ID() 8740 if !bytes.Equal(bondExpired.AccountID, acctID[:]) { 8741 return fmt.Errorf("invalid account ID %v, expected %v", bondExpired.AccountID, acctID) 8742 } 8743 8744 c.log.Infof("Received bondexpired notification from %v for account %v...", dc.acct.host, acctID) 8745 8746 return c.bondExpired(dc, bondExpired.AssetID, bondExpired.BondCoinID, bondExpired) 8747 } 8748 8749 // routeHandler is a handler for a message from the DEX. 8750 type routeHandler func(*Core, *dexConnection, *msgjson.Message) error 8751 8752 var reqHandlers = map[string]routeHandler{ 8753 msgjson.PreimageRoute: handlePreimageRequest, 8754 msgjson.MatchRoute: handleMatchRoute, 8755 msgjson.AuditRoute: handleAuditRoute, 8756 msgjson.RedemptionRoute: handleRedemptionRoute, // TODO: to ntfn 8757 } 8758 8759 var noteHandlers = map[string]routeHandler{ 8760 msgjson.MatchProofRoute: handleMatchProofMsg, 8761 msgjson.BookOrderRoute: handleBookOrderMsg, 8762 msgjson.EpochOrderRoute: handleEpochOrderMsg, 8763 msgjson.UnbookOrderRoute: handleUnbookOrderMsg, 8764 msgjson.PriceUpdateRoute: handlePriceUpdateNote, 8765 msgjson.UpdateRemainingRoute: handleUpdateRemainingMsg, 8766 msgjson.EpochReportRoute: handleEpochReportMsg, 8767 msgjson.SuspensionRoute: handleTradeSuspensionMsg, 8768 msgjson.ResumptionRoute: handleTradeResumptionMsg, 8769 msgjson.NotifyRoute: handleNotifyMsg, 8770 msgjson.PenaltyRoute: handlePenaltyMsg, 8771 msgjson.NoMatchRoute: handleNoMatchRoute, 8772 msgjson.RevokeOrderRoute: handleRevokeOrderMsg, 8773 msgjson.RevokeMatchRoute: handleRevokeMatchMsg, 8774 msgjson.TierChangeRoute: handleTierChangeMsg, 8775 msgjson.ScoreChangeRoute: handleScoreChangeMsg, 8776 msgjson.BondExpiredRoute: handleBondExpiredMsg, 8777 } 8778 8779 // listen monitors the DEX websocket connection for server requests and 8780 // notifications. This should be run as a goroutine. listen will return when 8781 // either c.ctx is canceled or the Message channel from the dexConnection's 8782 // MessageSource method is closed. The latter would be the case when the 8783 // dexConnection's WsConn is shut down / ConnectionMaster stopped. 8784 func (c *Core) listen(dc *dexConnection) { 8785 defer c.wg.Done() 8786 msgs := dc.MessageSource() // dc.connMaster.Disconnect closes it e.g. cancel of client/comms.(*wsConn).Connect 8787 8788 defer dc.ticker.Stop() 8789 lastTick := time.Now() 8790 8791 // Messages must be run in the order in which they are received, but they 8792 // should not be blocking or run concurrently. TODO: figure out which if any 8793 // can run asynchronously, maybe all. 8794 type msgJob struct { 8795 hander routeHandler 8796 msg *msgjson.Message 8797 } 8798 runJob := func(job *msgJob) { 8799 tStart := time.Now() 8800 defer func() { 8801 if pv := recover(); pv != nil { 8802 c.log.Criticalf("Uh-oh! Panic while handling message from %v.\n\n"+ 8803 "Message:\n\n%#v\n\nPanic:\n\n%v\n\nStack:\n\n%v\n\n", 8804 dc.acct.host, job.msg, pv, string(debug.Stack())) 8805 } 8806 if eTime := time.Since(tStart); eTime > 250*time.Millisecond { 8807 c.log.Infof("runJob(%v) completed in %v", job.msg.Route, eTime) 8808 } 8809 }() 8810 if err := job.hander(c, dc, job.msg); err != nil { 8811 c.log.Errorf("Route '%v' %v handler error (DEX %s): %v", job.msg.Route, 8812 job.msg.Type, dc.acct.host, err) 8813 } 8814 } 8815 // Start a single runner goroutine to run jobs one at a time in the order 8816 // that they were received. Include the handler goroutine in the WaitGroup 8817 // to allow it to complete if the connection master desires. 8818 nextJob := make(chan *msgJob, 1024) // start blocking at this cap 8819 defer close(nextJob) 8820 c.wg.Add(1) 8821 go func() { 8822 defer c.wg.Done() 8823 for job := range nextJob { 8824 runJob(job) 8825 } 8826 }() 8827 8828 checkTrades := func() { 8829 var doneTrades, activeTrades []*trackedTrade 8830 // NOTE: Don't lock tradeMtx while also locking a trackedTrade's mtx 8831 // since we risk blocking access to the trades map if there is lock 8832 // contention for even one trade. 8833 for _, trade := range dc.trackedTrades() { 8834 if trade.isActive() { 8835 activeTrades = append(activeTrades, trade) 8836 continue 8837 } 8838 doneTrades = append(doneTrades, trade) 8839 } 8840 8841 if len(doneTrades) > 0 { 8842 dc.tradeMtx.Lock() 8843 8844 for _, trade := range doneTrades { 8845 // Log an error if redemption funds are still reserved. 8846 trade.mtx.RLock() 8847 redeemLocked := trade.redemptionLocked 8848 refundLocked := trade.refundLocked 8849 trade.mtx.RUnlock() 8850 if redeemLocked > 0 { 8851 dc.log.Errorf("retiring order %s with %d > 0 redemption funds locked", trade.ID(), redeemLocked) 8852 } 8853 if refundLocked > 0 { 8854 dc.log.Errorf("retiring order %s with %d > 0 refund funds locked", trade.ID(), refundLocked) 8855 } 8856 8857 c.notify(newOrderNote(TopicOrderRetired, "", "", db.Data, trade.coreOrder())) 8858 delete(dc.trades, trade.ID()) 8859 } 8860 dc.tradeMtx.Unlock() 8861 } 8862 8863 // Unlock funding coins for retired orders for good measure, in case 8864 // there were not unlocked at an earlier time. 8865 updatedAssets := make(assetMap) 8866 for _, trade := range doneTrades { 8867 trade.mtx.Lock() 8868 c.log.Debugf("Retiring inactive order %v in status %v", trade.ID(), trade.metaData.Status) 8869 trade.returnCoins() 8870 trade.mtx.Unlock() 8871 updatedAssets.count(trade.wallets.fromWallet.AssetID) 8872 } 8873 8874 for _, trade := range activeTrades { 8875 if c.ctx.Err() != nil { // don't fail each one in sequence if shutting down 8876 return 8877 } 8878 newUpdates, err := c.tick(trade) 8879 if err != nil { 8880 c.log.Error(err) 8881 } 8882 updatedAssets.merge(newUpdates) 8883 } 8884 8885 if len(updatedAssets) > 0 { 8886 c.updateBalances(updatedAssets) 8887 } 8888 } 8889 8890 stopTicks := make(chan struct{}) 8891 defer close(stopTicks) 8892 c.wg.Add(1) 8893 go func() { 8894 defer c.wg.Done() 8895 for { 8896 select { 8897 case <-dc.ticker.C: 8898 sinceLast := time.Since(lastTick) 8899 lastTick = time.Now() 8900 if sinceLast >= 2*dc.ticker.Dur() { 8901 // The app likely just woke up from being suspended. Skip this 8902 // tick to let DEX connections reconnect and resync matches. 8903 c.log.Warnf("Long delay since previous trade check (just resumed?): %v. "+ 8904 "Skipping this check to allow reconnect.", sinceLast) 8905 continue 8906 } 8907 8908 checkTrades() 8909 case <-stopTicks: 8910 return 8911 case <-c.ctx.Done(): 8912 return 8913 } 8914 } 8915 }() 8916 8917 out: 8918 for { 8919 select { 8920 case msg, ok := <-msgs: 8921 if !ok { 8922 c.log.Debugf("listen(dc): Connection terminated for %s.", dc.acct.host) 8923 // TODO: This just means that wsConn, which created the 8924 // MessageSource channel, was shut down before this loop 8925 // returned via ctx.Done. It may be necessary to investigate the 8926 // most appropriate normal shutdown sequence (i.e. close all 8927 // connections before stopping Core). 8928 return 8929 } 8930 8931 var handler routeHandler 8932 var found bool 8933 switch msg.Type { 8934 case msgjson.Request: 8935 handler, found = reqHandlers[msg.Route] 8936 case msgjson.Notification: 8937 handler, found = noteHandlers[msg.Route] 8938 case msgjson.Response: 8939 // client/comms.wsConn handles responses to requests we sent. 8940 c.log.Errorf("A response was received in the message queue: %s", msg) 8941 continue 8942 default: 8943 c.log.Errorf("Invalid message type %d from MessageSource", msg.Type) 8944 continue 8945 } 8946 // Until all the routes have handlers, check for nil too. 8947 if !found || handler == nil { 8948 c.log.Errorf("No handler found for route '%s'", msg.Route) 8949 continue 8950 } 8951 8952 // Queue the handling of this message. 8953 nextJob <- &msgJob{handler, msg} 8954 8955 case <-c.ctx.Done(): 8956 break out 8957 } 8958 } 8959 } 8960 8961 // handlePreimageRequest handles a DEX-originating request for an order 8962 // preimage. If the order id in the request is not known, it may launch a 8963 // goroutine to wait for a market/limit/cancel request to finish processing. 8964 func handlePreimageRequest(c *Core, dc *dexConnection, msg *msgjson.Message) error { 8965 req := new(msgjson.PreimageRequest) 8966 err := msg.Unmarshal(req) 8967 if err != nil { 8968 return fmt.Errorf("preimage request parsing error: %w", err) 8969 } 8970 8971 oid, err := order.IDFromBytes(req.OrderID) 8972 if err != nil { 8973 return err 8974 } 8975 8976 if len(req.Commitment) != order.CommitmentSize { 8977 return fmt.Errorf("received preimage request for %s with no corresponding order submission response", oid) 8978 } 8979 8980 // See if we recognize that commitment, and if we do, just wait for the 8981 // order ID, and process the request. 8982 var commit order.Commitment 8983 copy(commit[:], req.Commitment) 8984 8985 c.sentCommitsMtx.Lock() 8986 defer c.sentCommitsMtx.Unlock() 8987 commitSig, found := c.sentCommits[commit] 8988 if !found { // this is the main benefit of a commitment index 8989 return fmt.Errorf("received preimage request for unknown commitment %v, order %v", 8990 req.Commitment, oid) 8991 } 8992 delete(c.sentCommits, commit) 8993 8994 dc.log.Debugf("Received preimage request for order %v with known commitment %v", oid, commit) 8995 8996 // Go async while waiting. 8997 go func() { 8998 // Order request success OR fail closes the channel. 8999 <-commitSig 9000 if err := processPreimageRequest(c, dc, msg.ID, oid, req.CommitChecksum); err != nil { 9001 c.log.Errorf("async processPreimageRequest for %v failed: %v", oid, err) 9002 } else { 9003 c.log.Debugf("async processPreimageRequest for %v succeeded", oid) 9004 } 9005 }() 9006 9007 return nil 9008 } 9009 9010 func processPreimageRequest(c *Core, dc *dexConnection, reqID uint64, oid order.OrderID, commitChecksum dex.Bytes) error { 9011 tracker, isCancel := dc.findOrder(oid) 9012 var preImg order.Preimage 9013 if tracker == nil { 9014 var found bool 9015 dc.blindCancelsMtx.Lock() 9016 preImg, found = dc.blindCancels[oid] 9017 dc.blindCancelsMtx.Unlock() 9018 if !found { 9019 return fmt.Errorf("no active order found for preimage request for %s", oid) 9020 } // delete the entry in match/nomatch 9021 } else { 9022 // Record the csum if this preimage request is novel, and deny it if 9023 // this is a duplicate request with an altered csum. 9024 var accept bool 9025 if accept, preImg = acceptCsum(tracker, isCancel, commitChecksum); !accept { 9026 csumErr := errors.New("invalid csum in duplicate preimage request") 9027 resp, err := msgjson.NewResponse(reqID, nil, 9028 msgjson.NewError(msgjson.InvalidRequestError, "%v", csumErr)) 9029 if err != nil { 9030 c.log.Errorf("Failed to encode response to denied preimage request: %v", err) 9031 return csumErr 9032 } 9033 err = dc.Send(resp) 9034 if err != nil { 9035 c.log.Errorf("Failed to send response to denied preimage request: %v", err) 9036 } 9037 return csumErr 9038 } 9039 } 9040 9041 resp, err := msgjson.NewResponse(reqID, &msgjson.PreimageResponse{ 9042 Preimage: preImg[:], 9043 }, nil) 9044 if err != nil { 9045 return fmt.Errorf("preimage response encoding error: %w", err) 9046 } 9047 err = dc.Send(resp) 9048 if err != nil { 9049 return fmt.Errorf("preimage send error: %w", err) 9050 } 9051 9052 if tracker != nil { 9053 topic := TopicPreimageSent 9054 if isCancel { 9055 topic = TopicCancelPreimageSent 9056 } 9057 c.notify(newOrderNote(topic, "", "", db.Data, tracker.coreOrder())) 9058 } 9059 9060 return nil 9061 } 9062 9063 // acceptCsum will record the commitment checksum so we can verify that the 9064 // subsequent match_proof with this order has the same checksum. If it does not, 9065 // the server may have used the knowledge of this preimage we are sending them 9066 // now to alter the epoch shuffle. The return value is false if a previous 9067 // checksum has been recorded that differs from the provided one. 9068 func acceptCsum(tracker *trackedTrade, isCancel bool, commitChecksum dex.Bytes) (bool, order.Preimage) { 9069 // Do not allow csum to be changed once it has been committed to 9070 // (initialized to something other than `nil`) because it is probably a 9071 // malicious behavior by the server. 9072 tracker.csumMtx.Lock() 9073 defer tracker.csumMtx.Unlock() 9074 if isCancel { 9075 if tracker.cancelCsum == nil { 9076 tracker.cancelCsum = commitChecksum 9077 return true, tracker.cancelPreimg 9078 } 9079 return bytes.Equal(commitChecksum, tracker.cancelCsum), tracker.cancelPreimg 9080 } 9081 if tracker.csum == nil { 9082 tracker.csum = commitChecksum 9083 return true, tracker.preImg 9084 } 9085 9086 return bytes.Equal(commitChecksum, tracker.csum), tracker.preImg 9087 } 9088 9089 // handleMatchRoute processes the DEX-originating match route request, 9090 // indicating that a match has been made and needs to be negotiated. 9091 func handleMatchRoute(c *Core, dc *dexConnection, msg *msgjson.Message) error { 9092 msgMatches := make([]*msgjson.Match, 0) 9093 err := msg.Unmarshal(&msgMatches) 9094 if err != nil { 9095 return fmt.Errorf("match request parsing error: %w", err) 9096 } 9097 9098 // TODO: If the dexConnection.acct is locked, prompt the user to login. 9099 // Maybe even spin here before failing with no hope of retrying the match 9100 // request handling. 9101 9102 // Acknowledgements MUST be in the same orders as the msgjson.Matches. 9103 matches, acks, err := dc.parseMatches(msgMatches, true) 9104 if err != nil { 9105 // Even one failed match fails them all since the server requires acks 9106 // for them all, and in the same order. TODO: consider lifting this 9107 // requirement, which requires changes to the server's handling. 9108 return err 9109 } 9110 9111 mktIDs := make(map[string]struct{}) 9112 9113 // Warn about new matches for unfunded orders. We still must ack all the 9114 // matches in the 'match' request for the server to accept it, although the 9115 // server doesn't require match acks. See (*Swapper).processMatchAcks. 9116 for oid, srvMatch := range matches { 9117 mktIDs[srvMatch.tracker.mktID] = struct{}{} 9118 if !srvMatch.tracker.hasFundingCoins() { 9119 c.log.Warnf("Received new match for unfunded order %v!", oid) 9120 // In runMatches>tracker.negotiate we generate the matchTracker and 9121 // set swapErr after updating order status and filled amount, and 9122 // storing the match to the DB. It may still be possible for the 9123 // user to recover if the issue is just that the wrong wallet is 9124 // connected by fixing wallet config and restarting. p.s. Hopefully 9125 // we are maker. 9126 } 9127 } 9128 9129 resp, err := msgjson.NewResponse(msg.ID, acks, nil) 9130 if err != nil { 9131 return err 9132 } 9133 9134 // Send the match acknowledgments. 9135 err = dc.Send(resp) 9136 if err != nil { 9137 // Do not bail on the matches on error, just log it. 9138 c.log.Errorf("Send match response: %v", err) 9139 } 9140 9141 // Begin match negotiation. 9142 updatedAssets, err := c.runMatches(matches) 9143 if len(updatedAssets) > 0 { 9144 c.updateBalances(updatedAssets) 9145 } 9146 9147 for mktID := range mktIDs { 9148 c.checkEpochResolution(dc.acct.host, mktID) 9149 } 9150 9151 return err 9152 } 9153 9154 // handleNoMatchRoute handles the DEX-originating nomatch request, which is sent 9155 // when an order does not match during the epoch match cycle. 9156 func handleNoMatchRoute(c *Core, dc *dexConnection, msg *msgjson.Message) error { 9157 nomatchMsg := new(msgjson.NoMatch) 9158 err := msg.Unmarshal(nomatchMsg) 9159 if err != nil { 9160 return fmt.Errorf("nomatch request parsing error: %w", err) 9161 } 9162 var oid order.OrderID 9163 copy(oid[:], nomatchMsg.OrderID) 9164 9165 tracker, _ := dc.findOrder(oid) 9166 if tracker == nil { 9167 dc.blindCancelsMtx.Lock() 9168 _, found := dc.blindCancels[oid] 9169 delete(dc.blindCancels, oid) 9170 dc.blindCancelsMtx.Unlock() 9171 if found { // if it didn't match, the targeted order isn't booked and we're done 9172 c.log.Infof("Blind cancel order %v did not match. Its targeted order is assumed to be unbooked.", oid) 9173 return nil 9174 } 9175 return newError(unknownOrderErr, "nomatch request received for unknown order %v from %s", oid, dc.acct.host) 9176 } 9177 9178 updatedAssets, err := tracker.nomatch(oid) 9179 if len(updatedAssets) > 0 { 9180 c.updateBalances(updatedAssets) 9181 } 9182 c.checkEpochResolution(dc.acct.host, tracker.mktID) 9183 return err 9184 } 9185 9186 func (c *Core) schedTradeTick(tracker *trackedTrade) { 9187 oid := tracker.ID() 9188 c.tickSchedMtx.Lock() 9189 defer c.tickSchedMtx.Unlock() 9190 if _, found := c.tickSched[oid]; found { 9191 return // already going to tick this trade 9192 } 9193 9194 tick := func() { 9195 assets, err := c.tick(tracker) 9196 if len(assets) > 0 { 9197 c.updateBalances(assets) 9198 } 9199 if err != nil { 9200 c.log.Errorf("tick error for order %v: %v", oid, err) 9201 } 9202 } 9203 9204 numMatches := len(tracker.activeMatches()) 9205 switch numMatches { 9206 case 0: 9207 return 9208 case 1: 9209 go tick() 9210 return 9211 default: 9212 } 9213 9214 // Schedule a tick for this trade. 9215 delay := 2*time.Second + time.Duration(numMatches)*time.Second/10 // 1 sec extra delay for every 10 active matches 9216 if delay > 5*time.Second { 9217 delay = 5 * time.Second 9218 } 9219 c.log.Debugf("Waiting %v to tick trade %v with %d active matches", delay, oid, numMatches) 9220 c.tickSched[oid] = time.AfterFunc(delay, func() { 9221 c.tickSchedMtx.Lock() 9222 defer c.tickSchedMtx.Unlock() 9223 defer delete(c.tickSched, oid) 9224 tick() 9225 }) 9226 } 9227 9228 // handleAuditRoute handles the DEX-originating audit request, which is sent 9229 // when a match counter-party reports their initiation transaction. 9230 func handleAuditRoute(c *Core, dc *dexConnection, msg *msgjson.Message) error { 9231 audit := new(msgjson.Audit) 9232 err := msg.Unmarshal(audit) 9233 if err != nil { 9234 return fmt.Errorf("audit request parsing error: %w", err) 9235 } 9236 var oid order.OrderID 9237 copy(oid[:], audit.OrderID) 9238 9239 tracker, _ := dc.findOrder(oid) 9240 if tracker == nil { 9241 return fmt.Errorf("audit request received for unknown order: %s", string(msg.Payload)) 9242 } 9243 return tracker.processAuditMsg(msg.ID, audit) 9244 } 9245 9246 // handleRedemptionRoute handles the DEX-originating redemption request, which 9247 // is sent when a match counter-party reports their redemption transaction. 9248 func handleRedemptionRoute(c *Core, dc *dexConnection, msg *msgjson.Message) error { 9249 redemption := new(msgjson.Redemption) 9250 err := msg.Unmarshal(redemption) 9251 if err != nil { 9252 return fmt.Errorf("redemption request parsing error: %w", err) 9253 } 9254 9255 sigMsg := redemption.Serialize() 9256 err = dc.acct.checkSig(sigMsg, redemption.Sig) 9257 if err != nil { 9258 c.log.Warnf("Server redemption signature error: %v", err) // just warn 9259 } 9260 9261 var oid order.OrderID 9262 copy(oid[:], redemption.OrderID) 9263 9264 tracker, isCancel := dc.findOrder(oid) 9265 if tracker != nil { 9266 if isCancel { 9267 return fmt.Errorf("redemption request received for cancel order %v, match %v (you ok server?)", 9268 oid, redemption.MatchID) 9269 } 9270 err = tracker.processRedemption(msg.ID, redemption) 9271 if err != nil { 9272 return err 9273 } 9274 c.schedTradeTick(tracker) 9275 return nil 9276 } 9277 9278 // This might be an order we completed on our own as taker without waiting 9279 // for redeem information to be provided to us, or as maker we retired the 9280 // order after redeeming but before receiving the taker's redeem info that 9281 // we don't need except for establishing a complete record of all 9282 // transactions in the atomic swap. Check the DB for the order, and if we 9283 // were taker with our redeem recorded, send it to the server. 9284 matches, err := c.db.MatchesForOrder(oid, true) 9285 if err != nil { 9286 return err 9287 } 9288 9289 for _, match := range matches { 9290 if !bytes.Equal(match.MatchID[:], redemption.MatchID) { 9291 continue 9292 } 9293 9294 // Respond to the DEX's redemption request with an ack. 9295 err = dc.ack(msg.ID, match.MatchID, redemption) 9296 if err != nil { 9297 c.log.Warnf("Failed to send redeem ack: %v", err) // just warn 9298 } 9299 9300 // Store the counterparty's redeem coin if we don't already have it 9301 // recorded, and if we are the taker, also send our redeem request. 9302 9303 proof := &match.MetaData.Proof 9304 9305 ourRedeem := proof.TakerRedeem 9306 if match.Side == order.Maker { 9307 ourRedeem = proof.MakerRedeem 9308 } 9309 9310 c.log.Debugf("Handling redemption request for inactive order %v, match %v in status %v, side %v "+ 9311 "(revoked = %v, refunded = %v, redeemed = %v)", 9312 oid, match, match.Status, match.Side, proof.IsRevoked(), 9313 len(proof.RefundCoin) > 0, len(ourRedeem) > 0) 9314 9315 // If we are maker, we are being informed of the taker's redeem, so we 9316 // just record it TakerRedeem and be done. Presently server does not do 9317 // this anymore, but if it does again, we would record this. 9318 if match.Side == order.Maker { 9319 proof.TakerRedeem = order.CoinID(redemption.CoinID) 9320 return c.db.UpdateMatch(match) 9321 } 9322 // If we are taker, we are being informed of the maker's redeem, but 9323 // since we did not have this order actively tracked, that should mean 9324 // we found it on our own first and already redeemed. Load up the 9325 // details of our redeem and send our redeem request as required even 9326 // though it's mostly pointless as the last step. 9327 9328 // Do some sanity checks considering that this order is NOT active. We 9329 // won't actually try to resolve any discrepancy since we have retired 9330 // this order by own usual match negotiation process, and server could 9331 // just be spamming nonsense, but make some noise in the logs. 9332 if len(proof.RefundCoin) > 0 { 9333 c.log.Warnf("We have supposedly refunded inactive match %v as taker, "+ 9334 "but server is telling us the counterparty just redeemed it!", match) 9335 // That should imply we have no redeem coin to send, but check. 9336 } 9337 if len(ourRedeem) == 0 { 9338 c.log.Warnf("We have not redeemed inactive match %v as taker (refunded = %v), "+ 9339 "but server is telling us the counterparty just redeemed ours!", 9340 match, len(proof.RefundCoin) > 0) // nothing to send, return 9341 return fmt.Errorf("we have no record of our own redeem as taker on match %v", match.MatchID) 9342 } 9343 9344 makerRedeem := order.CoinID(redemption.CoinID) 9345 if len(proof.MakerRedeem) == 0 { // findMakersRedemption or processMakersRedemption would have recorded this! 9346 c.log.Warnf("We (taker) have no previous record of the maker's redeem for inactive match %v.", match) 9347 // proof.MakerRedeem = makerRedeem; _ = c.db.UpdateMatch(match) // maybe, but this is unexpected 9348 } else if !bytes.Equal(proof.MakerRedeem, makerRedeem) { 9349 c.log.Warnf("We (taker) have a different maker redeem coin already recorded: "+ 9350 "recorded (%v) != notified (%v)", proof.MakerRedeem, makerRedeem) 9351 } 9352 9353 msgRedeem := &msgjson.Redeem{ 9354 OrderID: redemption.OrderID, 9355 MatchID: redemption.MatchID, 9356 CoinID: dex.Bytes(ourRedeem), 9357 Secret: proof.Secret, // silly for taker, but send it back as required 9358 } 9359 9360 c.wg.Add(1) 9361 go func() { 9362 defer c.wg.Done() 9363 ack := new(msgjson.Acknowledgement) 9364 err := dc.signAndRequest(msgRedeem, msgjson.RedeemRoute, ack, 30*time.Second) 9365 if err != nil { 9366 c.log.Errorf("error sending 'redeem' message: %v", err) 9367 return 9368 } 9369 9370 err = dc.acct.checkSig(msgRedeem.Serialize(), ack.Sig) 9371 if err != nil { 9372 c.log.Errorf("'redeem' ack signature error: %v", err) 9373 return 9374 } 9375 9376 c.log.Debugf("Received valid ack for 'redeem' request for match %s", match) 9377 auth := &proof.Auth 9378 auth.RedeemSig = ack.Sig 9379 auth.RedeemStamp = uint64(time.Now().UnixMilli()) 9380 err = c.db.UpdateMatch(match) 9381 if err != nil { 9382 c.log.Errorf("error storing redeem ack sig in database: %v", err) 9383 } 9384 }() 9385 9386 return nil 9387 } 9388 9389 return fmt.Errorf("redemption request received for unknown order: %s", string(msg.Payload)) 9390 } 9391 9392 // peerChange is called by a wallet backend when the peer count changes or 9393 // cannot be determined. A wallet state note is always emitted. In addition to 9394 // recording the number of peers, if the number of peers is 0, the wallet is 9395 // flagged as not synced. If the number of peers has just dropped to zero, a 9396 // notification that includes wallet state is emitted with the topic 9397 // TopicWalletPeersWarning. If the number of peers is >0 and was previously 9398 // zero, a resync monitor goroutine is launched to poll SyncStatus until the 9399 // wallet has caught up with its network. The monitor goroutine will regularly 9400 // emit wallet state notes, and once sync has been restored, a wallet balance 9401 // note will be emitted. If peerChangeErr is non-nil, numPeers should be zero. 9402 func (c *Core) peerChange(w *xcWallet, numPeers uint32, peerChangeErr error) { 9403 if peerChangeErr != nil { 9404 c.log.Warnf("%s wallet communication issue: %q", unbip(w.AssetID), peerChangeErr.Error()) 9405 } else if numPeers == 0 { 9406 c.log.Warnf("Wallet for asset %s has zero network peers!", unbip(w.AssetID)) 9407 } else { 9408 c.log.Tracef("New peer count for asset %s: %v", unbip(w.AssetID), numPeers) 9409 } 9410 9411 ss, err := w.SyncStatus() 9412 if err != nil { 9413 c.log.Errorf("error getting sync status after peer change: %v", err) 9414 return 9415 } 9416 9417 w.mtx.Lock() 9418 wasDisconnected := w.peerCount == 0 // excludes no count (-1) 9419 w.peerCount = int32(numPeers) 9420 w.syncStatus = ss 9421 w.mtx.Unlock() 9422 9423 c.notify(newWalletConfigNote(TopicWalletPeersUpdate, "", "", db.Data, w.state())) 9424 9425 // When we get peers after having none, start waiting for re-sync, otherwise 9426 // leave synced alone. This excludes the unknown state (-1) prior to the 9427 // initial peer count report. 9428 if wasDisconnected && numPeers > 0 { 9429 subject, details := c.formatDetails(TopicWalletPeersRestored, w.Info().Name) 9430 c.notify(newWalletConfigNote(TopicWalletPeersRestored, subject, details, 9431 db.Success, w.state())) 9432 c.startWalletSyncMonitor(w) 9433 } else if !ss.Synced { 9434 c.startWalletSyncMonitor(w) 9435 } 9436 9437 // Send a WalletStateNote in case Synced or anything else has changed. 9438 if atomic.LoadUint32(w.broadcasting) == 1 { 9439 if (numPeers == 0 || peerChangeErr != nil) && !wasDisconnected { // was connected or initial report 9440 if peerChangeErr != nil { 9441 subject, details := c.formatDetails(TopicWalletCommsWarning, 9442 w.Info().Name, peerChangeErr.Error()) 9443 c.notify(newWalletConfigNote(TopicWalletCommsWarning, subject, details, 9444 db.ErrorLevel, w.state())) 9445 } else { 9446 subject, details := c.formatDetails(TopicWalletPeersWarning, w.Info().Name) 9447 c.notify(newWalletConfigNote(TopicWalletPeersWarning, subject, details, 9448 db.WarningLevel, w.state())) 9449 } 9450 } 9451 c.notify(newWalletStateNote(w.state())) 9452 } 9453 } 9454 9455 // handleWalletNotification processes an asynchronous wallet notification. 9456 func (c *Core) handleWalletNotification(ni asset.WalletNotification) { 9457 switch n := ni.(type) { 9458 case *asset.TipChangeNote: 9459 c.tipChange(n.AssetID) 9460 case *asset.BalanceChangeNote: 9461 w, ok := c.wallet(n.AssetID) 9462 if !ok { 9463 return 9464 } 9465 contractLockedAmt, orderLockedAmt, bondLockedAmt := c.lockedAmounts(n.AssetID) 9466 bal := &WalletBalance{ 9467 Balance: &db.Balance{ 9468 Balance: *n.Balance, 9469 Stamp: time.Now(), 9470 }, 9471 OrderLocked: orderLockedAmt, 9472 ContractLocked: contractLockedAmt, 9473 BondLocked: bondLockedAmt, 9474 } 9475 if err := c.storeAndSendWalletBalance(w, bal); err != nil { 9476 c.log.Errorf("Error storing and sending emitted balance: %v", err) 9477 } 9478 return // Notification sent already. 9479 case *asset.ActionRequiredNote: 9480 c.requestedActionMtx.Lock() 9481 c.requestedActions[n.UniqueID] = n 9482 c.requestedActionMtx.Unlock() 9483 case *asset.ActionResolvedNote: 9484 c.deleteRequestedAction(n.UniqueID) 9485 } 9486 c.notify(newWalletNote(ni)) 9487 } 9488 9489 // tipChange is called by a wallet backend when the tip block changes, or when 9490 // a connection error is encountered such that tip change reporting may be 9491 // adversely affected. 9492 func (c *Core) tipChange(assetID uint32) { 9493 c.log.Tracef("Processing tip change for %s", unbip(assetID)) 9494 c.waiterMtx.RLock() 9495 for id, waiter := range c.blockWaiters { 9496 if waiter.assetID != assetID { 9497 continue 9498 } 9499 go func(id string, waiter *blockWaiter) { 9500 ok, err := waiter.trigger() 9501 if err != nil { 9502 waiter.action(err) 9503 c.removeWaiter(id) 9504 return 9505 } 9506 if ok { 9507 waiter.action(nil) 9508 c.removeWaiter(id) 9509 } 9510 }(id, waiter) 9511 } 9512 c.waiterMtx.RUnlock() 9513 9514 assets := make(assetMap) 9515 for _, dc := range c.dexConnections() { 9516 newUpdates := c.tickAsset(dc, assetID) 9517 if len(newUpdates) > 0 { 9518 assets.merge(newUpdates) 9519 } 9520 } 9521 9522 if _, exists := c.wallet(assetID); exists { 9523 // Ensure we always at least update this asset's balance regardless of 9524 // trade status changes. 9525 assets.count(assetID) 9526 } 9527 c.updateBalances(assets) 9528 } 9529 9530 // convertAssetInfo converts from a *msgjson.Asset to the nearly identical 9531 // *dex.Asset. 9532 func convertAssetInfo(ai *msgjson.Asset) *dex.Asset { 9533 return &dex.Asset{ 9534 ID: ai.ID, 9535 Symbol: ai.Symbol, 9536 Version: ai.Version, 9537 MaxFeeRate: ai.MaxFeeRate, 9538 SwapConf: uint32(ai.SwapConf), 9539 UnitInfo: ai.UnitInfo, 9540 } 9541 } 9542 9543 // checkSigS256 checks that the message's signature was created with the private 9544 // key for the provided secp256k1 public key on the sha256 hash of the message. 9545 func checkSigS256(msg, pkBytes, sigBytes []byte) error { 9546 pubKey, err := secp256k1.ParsePubKey(pkBytes) 9547 if err != nil { 9548 return fmt.Errorf("error decoding secp256k1 PublicKey from bytes: %w", err) 9549 } 9550 signature, err := ecdsa.ParseDERSignature(sigBytes) 9551 if err != nil { 9552 return fmt.Errorf("error decoding secp256k1 Signature from bytes: %w", err) 9553 } 9554 hash := sha256.Sum256(msg) 9555 if !signature.Verify(hash[:], pubKey) { 9556 return fmt.Errorf("secp256k1 signature verification failed") 9557 } 9558 return nil 9559 } 9560 9561 // signMsg hashes and signs the message with the sha256 hash function and the 9562 // provided private key. 9563 func signMsg(privKey *secp256k1.PrivateKey, msg []byte) []byte { 9564 // NOTE: legacy servers will not accept this signature. 9565 hash := sha256.Sum256(msg) 9566 return ecdsa.Sign(privKey, hash[:]).Serialize() 9567 } 9568 9569 // sign signs the msgjson.Signable with the provided private key. 9570 func sign(privKey *secp256k1.PrivateKey, payload msgjson.Signable) { 9571 sigMsg := payload.Serialize() 9572 payload.SetSig(signMsg(privKey, sigMsg)) 9573 } 9574 9575 // stampAndSign time stamps the msgjson.Stampable, and signs it with the given 9576 // private key. 9577 func stampAndSign(privKey *secp256k1.PrivateKey, payload msgjson.Stampable) { 9578 payload.Stamp(uint64(time.Now().UnixMilli())) 9579 sign(privKey, payload) 9580 } 9581 9582 // sendRequest sends a request via the specified ws connection and unmarshals 9583 // the response into the provided interface. 9584 // TODO: Modify to accept a context.Context argument so callers can pass core's 9585 // context to break out of the reply wait when Core starts shutting down. 9586 func sendRequest(conn comms.WsConn, route string, request, response any, timeout time.Duration) error { 9587 reqMsg, err := msgjson.NewRequest(conn.NextID(), route, request) 9588 if err != nil { 9589 return fmt.Errorf("error encoding %q request: %w", route, err) 9590 } 9591 9592 errChan := make(chan error, 1) 9593 err = conn.RequestWithTimeout(reqMsg, func(msg *msgjson.Message) { 9594 errChan <- msg.UnmarshalResult(response) 9595 }, timeout, func() { 9596 errChan <- fmt.Errorf("timed out waiting for %q response (%w)", route, errTimeout) // code this as a timeout! like today!!! 9597 }) 9598 // Check the request error. 9599 if err != nil { 9600 return err // code this as a send error! 9601 } 9602 9603 // Check the response error. 9604 return <-errChan 9605 } 9606 9607 // newPreimage creates a random order commitment. If you require a matching 9608 // commitment, generate a Preimage, then Preimage.Commit(). 9609 func newPreimage() (p order.Preimage) { 9610 copy(p[:], encode.RandomBytes(order.PreimageSize)) 9611 return 9612 } 9613 9614 // messagePrefix converts the order.Prefix to a msgjson.Prefix. 9615 func messagePrefix(prefix *order.Prefix) *msgjson.Prefix { 9616 oType := uint8(msgjson.LimitOrderNum) 9617 switch prefix.OrderType { 9618 case order.MarketOrderType: 9619 oType = msgjson.MarketOrderNum 9620 case order.CancelOrderType: 9621 oType = msgjson.CancelOrderNum 9622 } 9623 return &msgjson.Prefix{ 9624 AccountID: prefix.AccountID[:], 9625 Base: prefix.BaseAsset, 9626 Quote: prefix.QuoteAsset, 9627 OrderType: oType, 9628 ClientTime: uint64(prefix.ClientTime.UnixMilli()), 9629 Commit: prefix.Commit[:], 9630 } 9631 } 9632 9633 // messageTrade converts the order.Trade to a msgjson.Trade, adding the coins. 9634 func messageTrade(trade *order.Trade, coins []*msgjson.Coin) *msgjson.Trade { 9635 side := uint8(msgjson.BuyOrderNum) 9636 if trade.Sell { 9637 side = msgjson.SellOrderNum 9638 } 9639 return &msgjson.Trade{ 9640 Side: side, 9641 Quantity: trade.Quantity, 9642 Coins: coins, 9643 Address: trade.Address, 9644 } 9645 } 9646 9647 // messageCoin converts the []asset.Coin to a []*msgjson.Coin, signing the coin 9648 // IDs and retrieving the pubkeys too. 9649 func messageCoins(wallet *xcWallet, coins asset.Coins, redeemScripts []dex.Bytes) ([]*msgjson.Coin, error) { 9650 msgCoins := make([]*msgjson.Coin, 0, len(coins)) 9651 for i, coin := range coins { 9652 coinID := coin.ID() 9653 pubKeys, sigs, err := wallet.SignMessage(coin, coinID) 9654 if err != nil { 9655 return nil, fmt.Errorf("%s SignMessage error: %w", unbip(wallet.AssetID), err) 9656 } 9657 msgCoins = append(msgCoins, &msgjson.Coin{ 9658 ID: coinID, 9659 PubKeys: pubKeys, 9660 Sigs: sigs, 9661 Redeem: redeemScripts[i], 9662 }) 9663 } 9664 return msgCoins, nil 9665 } 9666 9667 // messageOrder converts an order.Order of any underlying type to an appropriate 9668 // msgjson type used for submitting the order. 9669 func messageOrder(ord order.Order, coins []*msgjson.Coin) (string, msgjson.Stampable, *msgjson.Trade) { 9670 prefix, trade := ord.Prefix(), ord.Trade() 9671 switch o := ord.(type) { 9672 case *order.LimitOrder: 9673 tifFlag := uint8(msgjson.StandingOrderNum) 9674 if o.Force == order.ImmediateTiF { 9675 tifFlag = msgjson.ImmediateOrderNum 9676 } 9677 msgOrd := &msgjson.LimitOrder{ 9678 Prefix: *messagePrefix(prefix), 9679 Trade: *messageTrade(trade, coins), 9680 Rate: o.Rate, 9681 TiF: tifFlag, 9682 } 9683 return msgjson.LimitRoute, msgOrd, &msgOrd.Trade 9684 case *order.MarketOrder: 9685 msgOrd := &msgjson.MarketOrder{ 9686 Prefix: *messagePrefix(prefix), 9687 Trade: *messageTrade(trade, coins), 9688 } 9689 return msgjson.MarketRoute, msgOrd, &msgOrd.Trade 9690 case *order.CancelOrder: 9691 return msgjson.CancelRoute, &msgjson.CancelOrder{ 9692 Prefix: *messagePrefix(prefix), 9693 TargetID: o.TargetOrderID[:], 9694 }, nil 9695 default: 9696 panic("unknown order type") 9697 } 9698 } 9699 9700 // validateOrderResponse validates the response against the order and the order 9701 // message, and stamps the order with the ServerTime, giving it a valid OrderID. 9702 func validateOrderResponse(dc *dexConnection, result *msgjson.OrderResult, ord order.Order, msgOrder msgjson.Stampable) error { 9703 if result.ServerTime == 0 { 9704 return fmt.Errorf("OrderResult cannot have servertime = 0") 9705 } 9706 msgOrder.Stamp(result.ServerTime) 9707 msg := msgOrder.Serialize() 9708 err := dc.acct.checkSig(msg, result.Sig) 9709 if err != nil { 9710 return fmt.Errorf("signature error. order abandoned") 9711 } 9712 ord.SetTime(time.UnixMilli(int64(result.ServerTime))) 9713 checkID, err := order.IDFromBytes(result.OrderID) 9714 if err != nil { 9715 return err 9716 } 9717 oid := ord.ID() 9718 if oid != checkID { 9719 return fmt.Errorf("failed ID match. order abandoned") 9720 } 9721 return nil 9722 } 9723 9724 // parseCert returns the (presumed to be) TLS certificate. If the certI is a 9725 // string, it will be treated as a filepath and the raw file contents returned. 9726 // if certI is already a []byte, it is presumed to be the raw file contents, and 9727 // is returned unmodified. 9728 func parseCert(host string, certI any, net dex.Network) ([]byte, error) { 9729 switch c := certI.(type) { 9730 case string: 9731 if len(c) == 0 { 9732 return CertStore[net][host], nil // not found is ok (try without TLS) 9733 } 9734 cert, err := os.ReadFile(c) 9735 if err != nil { 9736 return nil, newError(fileReadErr, "failed to read certificate file from %s: %w", c, err) 9737 } 9738 return cert, nil 9739 case []byte: 9740 if len(c) == 0 { 9741 return CertStore[net][host], nil // not found is ok (try without TLS) 9742 } 9743 return c, nil 9744 case nil: 9745 return CertStore[net][host], nil // not found is ok (try without TLS) 9746 } 9747 return nil, fmt.Errorf("not a valid certificate type %T", certI) 9748 } 9749 9750 // WalletLogFilePath returns the path to the wallet's log file. 9751 func (c *Core) WalletLogFilePath(assetID uint32) (string, error) { 9752 wallet, exists := c.wallet(assetID) 9753 if !exists { 9754 return "", newError(missingWalletErr, "no configured wallet found for %s (%d)", 9755 strings.ToUpper(unbip(assetID)), assetID) 9756 } 9757 9758 return wallet.logFilePath() 9759 } 9760 9761 // WalletRestorationInfo returns information about how to restore the currently 9762 // loaded wallet for assetID in various external wallet software. This function 9763 // will return an error if the currently loaded wallet for assetID does not 9764 // implement the WalletRestorer interface. 9765 func (c *Core) WalletRestorationInfo(pw []byte, assetID uint32) ([]*asset.WalletRestoration, error) { 9766 crypter, err := c.encryptionKey(pw) 9767 if err != nil { 9768 return nil, fmt.Errorf("WalletRestorationInfo password error: %w", err) 9769 } 9770 defer crypter.Close() 9771 9772 seed, _, err := c.assetSeedAndPass(assetID, crypter) 9773 if err != nil { 9774 return nil, fmt.Errorf("assetSeedAndPass error: %w", err) 9775 } 9776 defer encode.ClearBytes(seed) 9777 9778 wallet, found := c.wallet(assetID) 9779 if !found { 9780 return nil, fmt.Errorf("no wallet configured for asset %d", assetID) 9781 } 9782 9783 restorer, ok := wallet.Wallet.(asset.WalletRestorer) 9784 if !ok { 9785 return nil, fmt.Errorf("wallet for asset %d doesn't support exporting functionality", assetID) 9786 } 9787 9788 restorationInfo, err := restorer.RestorationInfo(seed) 9789 if err != nil { 9790 return nil, fmt.Errorf("failed to get restoration info for wallet %w", err) 9791 } 9792 9793 return restorationInfo, nil 9794 } 9795 9796 // createFile creates a new file and will create the file directory if it does 9797 // not exist. 9798 func createFile(fileName string) (*os.File, error) { 9799 if fileName == "" { 9800 return nil, errors.New("no file path specified for creating") 9801 } 9802 fileDir := filepath.Dir(fileName) 9803 if !dex.FileExists(fileDir) { 9804 err := os.MkdirAll(fileDir, 0755) 9805 if err != nil { 9806 return nil, fmt.Errorf("os.MkdirAll error: %w", err) 9807 } 9808 } 9809 fileName = dex.CleanAndExpandPath(fileName) 9810 // Errors if file exists. 9811 f, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) 9812 if err != nil { 9813 return nil, err 9814 } 9815 return f, nil 9816 } 9817 9818 func (c *Core) deleteOrderFn(ordersFileStr string) (perOrderFn func(*db.MetaOrder) error, cleanUpFn func() error, err error) { 9819 ordersFile, err := createFile(ordersFileStr) 9820 if err != nil { 9821 return nil, nil, fmt.Errorf("problem opening orders file: %v", err) 9822 } 9823 csvWriter := csv.NewWriter(ordersFile) 9824 csvWriter.UseCRLF = runtime.GOOS == "windows" 9825 err = csvWriter.Write([]string{ 9826 "Host", 9827 "Order ID", 9828 "Base", 9829 "Quote", 9830 "Base Quantity", 9831 "Order Rate", 9832 "Actual Rate", 9833 "Base Fees", 9834 "Base Fees Asset", 9835 "Quote Fees", 9836 "Quote Fees Asset", 9837 "Type", 9838 "Side", 9839 "Time in Force", 9840 "Status", 9841 "TargetOrderID", 9842 "Filled (%)", 9843 "Settled (%)", 9844 "Time", 9845 }) 9846 if err != nil { 9847 ordersFile.Close() 9848 return nil, nil, fmt.Errorf("error writing CSV: %v", err) 9849 } 9850 csvWriter.Flush() 9851 err = csvWriter.Error() 9852 if err != nil { 9853 ordersFile.Close() 9854 return nil, nil, fmt.Errorf("error writing CSV: %v", err) 9855 } 9856 return func(ord *db.MetaOrder) error { 9857 cord := coreOrderFromTrade(ord.Order, ord.MetaData) 9858 9859 baseUnitInfo, err := asset.UnitInfo(cord.BaseID) 9860 if err != nil { 9861 return fmt.Errorf("unable to get base unit info for %v: %v", cord.BaseSymbol, err) 9862 } 9863 9864 baseFeeAssetSymbol := unbip(cord.BaseID) 9865 baseFeeUnitInfo := baseUnitInfo 9866 if baseToken := asset.TokenInfo(cord.BaseID); baseToken != nil { 9867 baseFeeAssetSymbol = unbip(baseToken.ParentID) 9868 baseFeeUnitInfo, err = asset.UnitInfo(baseToken.ParentID) 9869 if err != nil { 9870 return fmt.Errorf("unable to get base fee unit info for %v: %v", baseToken.ParentID, err) 9871 } 9872 } 9873 9874 quoteUnitInfo, err := asset.UnitInfo(cord.QuoteID) 9875 if err != nil { 9876 return fmt.Errorf("unable to get quote unit info for %v: %v", cord.QuoteSymbol, err) 9877 } 9878 9879 quoteFeeAssetSymbol := unbip(cord.QuoteID) 9880 quoteFeeUnitInfo := quoteUnitInfo 9881 if quoteToken := asset.TokenInfo(cord.QuoteID); quoteToken != nil { 9882 quoteFeeAssetSymbol = unbip(quoteToken.ParentID) 9883 quoteFeeUnitInfo, err = asset.UnitInfo(quoteToken.ParentID) 9884 if err != nil { 9885 return fmt.Errorf("unable to get quote fee unit info for %v: %v", quoteToken.ParentID, err) 9886 } 9887 } 9888 9889 ordReader := &OrderReader{ 9890 Order: cord, 9891 BaseUnitInfo: baseUnitInfo, 9892 BaseFeeUnitInfo: baseFeeUnitInfo, 9893 BaseFeeAssetSymbol: baseFeeAssetSymbol, 9894 QuoteUnitInfo: quoteUnitInfo, 9895 QuoteFeeUnitInfo: quoteFeeUnitInfo, 9896 QuoteFeeAssetSymbol: quoteFeeAssetSymbol, 9897 } 9898 9899 timestamp := time.UnixMilli(int64(cord.Stamp)).Local().Format(time.RFC3339Nano) 9900 err = csvWriter.Write([]string{ 9901 cord.Host, // Host 9902 ord.Order.ID().String(), // Order ID 9903 cord.BaseSymbol, // Base 9904 cord.QuoteSymbol, // Quote 9905 ordReader.BaseQtyString(), // Base Quantity 9906 ordReader.SimpleRateString(), // Order Rate 9907 ordReader.AverageRateString(), // Actual Rate 9908 ordReader.BaseAssetFees(), // Base Fees 9909 ordReader.BaseFeeSymbol(), // Base Fees Asset 9910 ordReader.QuoteAssetFees(), // Quote Fees 9911 ordReader.QuoteFeeSymbol(), // Quote Fees Asset 9912 ordReader.Type.String(), // Type 9913 ordReader.SideString(), // Side 9914 cord.TimeInForce.String(), // Time in Force 9915 ordReader.StatusString(), // Status 9916 cord.TargetOrderID.String(), // Target Order ID 9917 ordReader.FilledPercent(), // Filled 9918 ordReader.SettledPercent(), // Settled 9919 timestamp, // Time 9920 }) 9921 if err != nil { 9922 return fmt.Errorf("error writing orders CSV: %v", err) 9923 } 9924 csvWriter.Flush() 9925 err = csvWriter.Error() 9926 if err != nil { 9927 return fmt.Errorf("error writing orders CSV: %v", err) 9928 } 9929 return nil 9930 }, ordersFile.Close, nil 9931 } 9932 9933 func deleteMatchFn(matchesFileStr string) (perMatchFn func(*db.MetaMatch, bool) error, cleanUpFn func() error, err error) { 9934 matchesFile, err := createFile(matchesFileStr) 9935 if err != nil { 9936 return nil, nil, fmt.Errorf("problem opening orders file: %v", err) 9937 } 9938 csvWriter := csv.NewWriter(matchesFile) 9939 csvWriter.UseCRLF = runtime.GOOS == "windows" 9940 9941 err = csvWriter.Write([]string{ 9942 "Host", 9943 "Base", 9944 "Quote", 9945 "Match ID", 9946 "Order ID", 9947 "Quantity", 9948 "Rate", 9949 "Swap Fee Rate", 9950 "Swap Address", 9951 "Status", 9952 "Side", 9953 "Secret Hash", 9954 "Secret", 9955 "Maker Swap Coin ID", 9956 "Maker Redeem Coin ID", 9957 "Taker Swap Coin ID", 9958 "Taker Redeem Coin ID", 9959 "Refund Coin ID", 9960 "Time", 9961 }) 9962 if err != nil { 9963 matchesFile.Close() 9964 return nil, nil, fmt.Errorf("error writing matches CSV: %v", err) 9965 } 9966 csvWriter.Flush() 9967 err = csvWriter.Error() 9968 if err != nil { 9969 matchesFile.Close() 9970 return nil, nil, fmt.Errorf("error writing matches CSV: %v", err) 9971 } 9972 return func(mtch *db.MetaMatch, isSell bool) error { 9973 numToStr := func(n any) string { 9974 return fmt.Sprintf("%d", n) 9975 } 9976 base, quote := mtch.MetaData.Base, mtch.MetaData.Quote 9977 9978 makerAsset, takerAsset := base, quote 9979 // If we are either not maker or not buying, invert it. Double 9980 // inverse would be no change. 9981 if (mtch.Side == order.Taker) != isSell { 9982 makerAsset, takerAsset = quote, base 9983 } 9984 9985 var ( 9986 makerSwapID, makerRedeemID, takerSwapID, redeemSwapID, refundCoinID string 9987 err error 9988 ) 9989 9990 decode := func(assetID uint32, coin []byte) (string, error) { 9991 if coin == nil { 9992 return "", nil 9993 } 9994 return asset.DecodeCoinID(assetID, coin) 9995 } 9996 9997 makerSwapID, err = decode(takerAsset, mtch.MetaData.Proof.MakerSwap) 9998 if err != nil { 9999 return fmt.Errorf("unable to format maker's swap: %v", err) 10000 } 10001 makerRedeemID, err = decode(makerAsset, mtch.MetaData.Proof.MakerRedeem) 10002 if err != nil { 10003 return fmt.Errorf("unable to format maker's redeem: %v", err) 10004 } 10005 takerSwapID, err = decode(makerAsset, mtch.MetaData.Proof.TakerSwap) 10006 if err != nil { 10007 return fmt.Errorf("unable to format taker's swap: %v", err) 10008 } 10009 redeemSwapID, err = decode(takerAsset, mtch.MetaData.Proof.TakerRedeem) 10010 if err != nil { 10011 return fmt.Errorf("unable to format taker's redeem: %v", err) 10012 } 10013 refundCoinID, err = decode(makerAsset, mtch.MetaData.Proof.RefundCoin) 10014 if err != nil { 10015 return fmt.Errorf("unable to format maker's refund: %v", err) 10016 } 10017 10018 timestamp := time.UnixMilli(int64(mtch.MetaData.Stamp)).Local().Format(time.RFC3339Nano) 10019 err = csvWriter.Write([]string{ 10020 mtch.MetaData.DEX, // Host 10021 dex.BipIDSymbol(base), // Base 10022 dex.BipIDSymbol(quote), // Quote 10023 mtch.MatchID.String(), // Match ID 10024 mtch.OrderID.String(), // Order ID 10025 numToStr(mtch.Quantity), // Quantity 10026 numToStr(mtch.Rate), // Rate 10027 numToStr(mtch.FeeRateSwap), // Swap Fee Rate 10028 mtch.Address, // Swap Address 10029 mtch.Status.String(), // Status 10030 mtch.Side.String(), // Side 10031 fmt.Sprintf("%x", mtch.MetaData.Proof.SecretHash), // Secret Hash 10032 fmt.Sprintf("%x", mtch.MetaData.Proof.Secret), // Secret 10033 makerSwapID, // Maker Swap Coin ID 10034 makerRedeemID, // Maker Redeem Coin ID 10035 takerSwapID, // Taker Swap Coin ID 10036 redeemSwapID, // Taker Redeem Coin ID 10037 refundCoinID, // Refund Coin ID 10038 timestamp, // Time 10039 }) 10040 if err != nil { 10041 return fmt.Errorf("error writing matches CSV: %v", err) 10042 } 10043 csvWriter.Flush() 10044 err = csvWriter.Error() 10045 if err != nil { 10046 return fmt.Errorf("error writing matches CSV: %v", err) 10047 } 10048 return nil 10049 }, matchesFile.Close, nil 10050 } 10051 10052 // archivedRecordsDataDirectory returns a data directory to save deleted archive 10053 // records. 10054 func (c *Core) archivedRecordsDataDirectory() string { 10055 return filepath.Join(filepath.Dir(c.cfg.DBPath), "archived-records") 10056 } 10057 10058 // DeleteArchivedRecordsWithBackup is like DeleteArchivedRecords but the 10059 // required filepaths are provided by Core and the path where archived records 10060 // are stored is returned. 10061 func (c *Core) DeleteArchivedRecordsWithBackup(olderThan *time.Time, saveMatchesToFile, saveOrdersToFile bool) (string, int, error) { 10062 var matchesFile, ordersFile string 10063 if saveMatchesToFile { 10064 matchesFile = filepath.Join(c.archivedRecordsDataDirectory(), fmt.Sprintf("archived-matches-%d", time.Now().Unix())) 10065 } 10066 if saveOrdersToFile { 10067 ordersFile = filepath.Join(c.archivedRecordsDataDirectory(), fmt.Sprintf("archived-orders-%d", time.Now().Unix())) 10068 } 10069 nRecordsDeleted, err := c.DeleteArchivedRecords(olderThan, matchesFile, ordersFile) 10070 if nRecordsDeleted > 0 && (saveMatchesToFile || saveOrdersToFile) { 10071 return c.archivedRecordsDataDirectory(), nRecordsDeleted, err 10072 } 10073 return "", nRecordsDeleted, err 10074 } 10075 10076 // DeleteArchivedRecords deletes archived matches from the database and returns 10077 // the total number of records deleted. Optionally set a time to delete older 10078 // records and file paths to save deleted records as comma separated values. If 10079 // a nil *time.Time is provided, current time is used. 10080 func (c *Core) DeleteArchivedRecords(olderThan *time.Time, matchesFile, ordersFile string) (int, error) { 10081 var ( 10082 err error 10083 perMtchFn func(*db.MetaMatch, bool) error 10084 nMatchesDeleted int 10085 ) 10086 // If provided a file to write the orders csv to, write the header and 10087 // defer closing the file. 10088 if matchesFile != "" { 10089 var cleanup func() error 10090 perMtchFn, cleanup, err = deleteMatchFn(matchesFile) 10091 if err != nil { 10092 return 0, fmt.Errorf("unable to set up orders csv: %v", err) 10093 } 10094 defer func() { 10095 cleanup() 10096 // If no match was deleted, remove the matches file. 10097 if nMatchesDeleted == 0 { 10098 os.Remove(matchesFile) 10099 } 10100 }() 10101 } 10102 10103 // Delete matches while saving to csv if available until the database 10104 // says that's all or context is canceled. 10105 nMatchesDeleted, err = c.db.DeleteInactiveMatches(c.ctx, olderThan, perMtchFn) 10106 if err != nil { 10107 return 0, fmt.Errorf("unable to delete matches: %v", err) 10108 } 10109 10110 var ( 10111 perOrdFn func(*db.MetaOrder) error 10112 nOrdersDeleted int 10113 ) 10114 10115 // If provided a file to write the orders csv to, write the header and 10116 // defer closing the file. 10117 if ordersFile != "" { 10118 var cleanup func() error 10119 perOrdFn, cleanup, err = c.deleteOrderFn(ordersFile) 10120 if err != nil { 10121 return 0, fmt.Errorf("unable to set up orders csv: %v", err) 10122 } 10123 defer func() { 10124 cleanup() 10125 // If no order was deleted, remove the orders file. 10126 if nOrdersDeleted == 0 { 10127 os.Remove(ordersFile) 10128 } 10129 }() 10130 } 10131 10132 // Delete orders while saving to csv if available until the database 10133 // says that's all or context is canceled. 10134 nOrdersDeleted, err = c.db.DeleteInactiveOrders(c.ctx, olderThan, perOrdFn) 10135 if err != nil { 10136 return 0, fmt.Errorf("unable to delete orders: %v", err) 10137 } 10138 return nOrdersDeleted + nMatchesDeleted, nil 10139 } 10140 10141 // AccelerateOrder will use the Child-Pays-For-Parent technique to accelerate 10142 // the swap transactions in an order. 10143 func (c *Core) AccelerateOrder(pw []byte, oidB dex.Bytes, newFeeRate uint64) (string, error) { 10144 _, err := c.encryptionKey(pw) 10145 if err != nil { 10146 return "", fmt.Errorf("AccelerateOrder password error: %w", err) 10147 } 10148 10149 oid, err := order.IDFromBytes(oidB) 10150 if err != nil { 10151 return "", err 10152 } 10153 tracker, err := c.findActiveOrder(oid) 10154 if err != nil { 10155 return "", err 10156 } 10157 10158 if !tracker.wallets.fromWallet.traits.IsAccelerator() { 10159 return "", fmt.Errorf("the %s wallet is not an accelerator", tracker.wallets.fromWallet.Symbol) 10160 } 10161 10162 tracker.mtx.Lock() 10163 defer tracker.mtx.Unlock() 10164 10165 swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, err := tracker.orderAccelerationParameters() 10166 if err != nil { 10167 return "", err 10168 } 10169 10170 newChangeCoin, txID, err := 10171 tracker.wallets.fromWallet.accelerateOrder(swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, newFeeRate) 10172 if err != nil { 10173 return "", err 10174 } 10175 if newChangeCoin != nil { 10176 tracker.metaData.ChangeCoin = order.CoinID(newChangeCoin.ID()) 10177 tracker.coins[newChangeCoin.ID().String()] = newChangeCoin 10178 } else { 10179 tracker.metaData.ChangeCoin = nil 10180 } 10181 tracker.metaData.AccelerationCoins = append(tracker.metaData.AccelerationCoins, tracker.metaData.ChangeCoin) 10182 return txID, tracker.db.UpdateOrderMetaData(oid, tracker.metaData) 10183 } 10184 10185 // AccelerationEstimate returns the amount of funds that would be needed to 10186 // accelerate the swap transactions in an order to a desired fee rate. 10187 func (c *Core) AccelerationEstimate(oidB dex.Bytes, newFeeRate uint64) (uint64, error) { 10188 oid, err := order.IDFromBytes(oidB) 10189 if err != nil { 10190 return 0, err 10191 } 10192 10193 tracker, err := c.findActiveOrder(oid) 10194 if err != nil { 10195 return 0, err 10196 } 10197 10198 if !tracker.wallets.fromWallet.traits.IsAccelerator() { 10199 return 0, fmt.Errorf("the %s wallet is not an accelerator", tracker.wallets.fromWallet.Symbol) 10200 } 10201 10202 tracker.mtx.RLock() 10203 defer tracker.mtx.RUnlock() 10204 10205 swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, err := tracker.orderAccelerationParameters() 10206 if err != nil { 10207 return 0, err 10208 } 10209 10210 accelerationFee, err := tracker.wallets.fromWallet.accelerationEstimate(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, newFeeRate) 10211 if err != nil { 10212 return 0, err 10213 } 10214 10215 return accelerationFee, nil 10216 } 10217 10218 // PreAccelerateOrder returns information the user can use to decide how much 10219 // to accelerate stuck swap transactions in an order. 10220 func (c *Core) PreAccelerateOrder(oidB dex.Bytes) (*PreAccelerate, error) { 10221 oid, err := order.IDFromBytes(oidB) 10222 if err != nil { 10223 return nil, err 10224 } 10225 10226 tracker, err := c.findActiveOrder(oid) 10227 if err != nil { 10228 return nil, err 10229 } 10230 10231 if !tracker.wallets.fromWallet.traits.IsAccelerator() { 10232 return nil, fmt.Errorf("the %s wallet is not an accelerator", tracker.wallets.fromWallet.Symbol) 10233 } 10234 10235 feeSuggestion := c.feeSuggestionAny(tracker.fromAssetID) 10236 10237 tracker.mtx.RLock() 10238 defer tracker.mtx.RUnlock() 10239 swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, err := tracker.orderAccelerationParameters() 10240 if err != nil { 10241 return nil, err 10242 } 10243 10244 currentRate, suggestedRange, earlyAcceleration, err := 10245 tracker.wallets.fromWallet.preAccelerate(swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, feeSuggestion) 10246 if err != nil { 10247 return nil, err 10248 } 10249 10250 if suggestedRange == nil { 10251 // this should never happen 10252 return nil, fmt.Errorf("suggested range is nil") 10253 } 10254 10255 return &PreAccelerate{ 10256 SwapRate: currentRate, 10257 SuggestedRate: feeSuggestion, 10258 SuggestedRange: *suggestedRange, 10259 EarlyAcceleration: earlyAcceleration, 10260 }, nil 10261 } 10262 10263 // WalletPeers returns a list of peers that a wallet is connected to. It also 10264 // returns the user added peers that the wallet is not connected to. 10265 func (c *Core) WalletPeers(assetID uint32) ([]*asset.WalletPeer, error) { 10266 w, err := c.connectedWallet(assetID) 10267 if err != nil { 10268 return nil, err 10269 } 10270 10271 peerManager, is := w.Wallet.(asset.PeerManager) 10272 if !is { 10273 return nil, fmt.Errorf("%s wallet is not a peer manager", unbip(assetID)) 10274 } 10275 10276 return peerManager.Peers() 10277 } 10278 10279 // AddWalletPeer connects the wallet to a new peer, and also persists this peer 10280 // to be connected to on future startups. 10281 func (c *Core) AddWalletPeer(assetID uint32, address string) error { 10282 w, err := c.connectedWallet(assetID) 10283 if err != nil { 10284 return err 10285 } 10286 10287 peerManager, is := w.Wallet.(asset.PeerManager) 10288 if !is { 10289 return fmt.Errorf("%s wallet is not a peer manager", unbip(assetID)) 10290 } 10291 10292 return peerManager.AddPeer(address) 10293 } 10294 10295 // RemoveWalletPeer disconnects from a peer that the user previously added. It 10296 // will no longer be guaranteed to connect to this peer in the future. 10297 func (c *Core) RemoveWalletPeer(assetID uint32, address string) error { 10298 w, err := c.connectedWallet(assetID) 10299 if err != nil { 10300 return err 10301 } 10302 10303 peerManager, is := w.Wallet.(asset.PeerManager) 10304 if !is { 10305 return fmt.Errorf("%s wallet is not a peer manager", unbip(assetID)) 10306 } 10307 10308 return peerManager.RemovePeer(address) 10309 } 10310 10311 // findActiveOrder will search the dex connections for an active order by order 10312 // id. An error is returned if it cannot be found. 10313 func (c *Core) findActiveOrder(oid order.OrderID) (*trackedTrade, error) { 10314 for _, dc := range c.dexConnections() { 10315 tracker, _ := dc.findOrder(oid) 10316 if tracker != nil { 10317 return tracker, nil 10318 } 10319 } 10320 return nil, fmt.Errorf("could not find active order with order id: %s", oid) 10321 } 10322 10323 // fetchFiatExchangeRates starts the fiat rate fetcher goroutine and schedules 10324 // refresh cycles. Use under ratesMtx lock. 10325 func (c *Core) fetchFiatExchangeRates(ctx context.Context) { 10326 c.log.Debug("starting fiat rate fetching") 10327 10328 c.wg.Add(1) 10329 go func() { 10330 defer c.wg.Done() 10331 tick := time.NewTicker(fiatRateRequestInterval) 10332 defer tick.Stop() 10333 for { 10334 c.refreshFiatRates(ctx) 10335 10336 select { 10337 case <-tick.C: 10338 case <-c.reFiat: 10339 case <-ctx.Done(): 10340 return 10341 10342 } 10343 } 10344 }() 10345 } 10346 10347 func (c *Core) fiatSources() []*commonRateSource { 10348 c.ratesMtx.RLock() 10349 defer c.ratesMtx.RUnlock() 10350 sources := make([]*commonRateSource, 0, len(c.fiatRateSources)) 10351 for _, s := range c.fiatRateSources { 10352 sources = append(sources, s) 10353 } 10354 return sources 10355 } 10356 10357 // refreshFiatRates refreshes the fiat rates for rate sources whose values have 10358 // not been updated since fiatRateRequestInterval. It also checks if fiat rates 10359 // are expired and does some clean-up. 10360 func (c *Core) refreshFiatRates(ctx context.Context) { 10361 var wg sync.WaitGroup 10362 supportedAssets := c.SupportedAssets() 10363 for _, source := range c.fiatSources() { 10364 wg.Add(1) 10365 go func(source *commonRateSource) { 10366 defer wg.Done() 10367 source.refreshRates(ctx, c.log, supportedAssets) 10368 }(source) 10369 } 10370 wg.Wait() 10371 10372 // Remove expired rate source if any. 10373 c.removeExpiredRateSources() 10374 10375 fiatRatesMap := c.fiatConversions() 10376 if len(fiatRatesMap) != 0 { 10377 c.notify(newFiatRatesUpdate(fiatRatesMap)) 10378 } 10379 } 10380 10381 // FiatRateSources returns a list of fiat rate sources and their individual 10382 // status. 10383 func (c *Core) FiatRateSources() map[string]bool { 10384 c.ratesMtx.RLock() 10385 defer c.ratesMtx.RUnlock() 10386 rateSources := make(map[string]bool, len(fiatRateFetchers)) 10387 for token := range fiatRateFetchers { 10388 rateSources[token] = c.fiatRateSources[token] != nil 10389 } 10390 return rateSources 10391 } 10392 10393 // FiatConversionRates are the currently cached fiat conversion rates. Must have 10394 // 1 or more fiat rate sources enabled. 10395 func (c *Core) FiatConversionRates() map[uint32]float64 { 10396 return c.fiatConversions() 10397 } 10398 10399 // fiatConversions returns fiat rate for all supported assets that have a 10400 // wallet. 10401 func (c *Core) fiatConversions() map[uint32]float64 { 10402 assetIDs := make(map[uint32]struct{}) 10403 supportedAssets := asset.Assets() 10404 for assetID, asset := range supportedAssets { 10405 assetIDs[assetID] = struct{}{} 10406 for tokenID := range asset.Tokens { 10407 assetIDs[tokenID] = struct{}{} 10408 } 10409 } 10410 10411 fiatRatesMap := make(map[uint32]float64, len(supportedAssets)) 10412 for assetID := range assetIDs { 10413 var rateSum float64 10414 var sources int 10415 for _, source := range c.fiatSources() { 10416 rateInfo := source.assetRate(assetID) 10417 if rateInfo != nil && time.Since(rateInfo.lastUpdate) < fiatRateDataExpiry && rateInfo.rate > 0 { 10418 sources++ 10419 rateSum += rateInfo.rate 10420 } 10421 } 10422 if rateSum != 0 { 10423 fiatRatesMap[assetID] = rateSum / float64(sources) // get average rate. 10424 } 10425 } 10426 return fiatRatesMap 10427 } 10428 10429 // ToggleRateSourceStatus toggles a fiat rate source status. If disable is true, 10430 // the fiat rate source is disabled, otherwise the rate source is enabled. 10431 func (c *Core) ToggleRateSourceStatus(source string, disable bool) error { 10432 if disable { 10433 return c.disableRateSource(source) 10434 } 10435 return c.enableRateSource(source) 10436 } 10437 10438 // enableRateSource enables a fiat rate source. 10439 func (c *Core) enableRateSource(source string) error { 10440 // Check if it's an invalid rate source or it is already enabled. 10441 rateFetcher, found := fiatRateFetchers[source] 10442 if !found { 10443 return errors.New("cannot enable unknown fiat rate source") 10444 } 10445 10446 c.ratesMtx.Lock() 10447 defer c.ratesMtx.Unlock() 10448 if c.fiatRateSources[source] != nil { 10449 return nil // already enabled. 10450 } 10451 10452 // Build fiat rate source. 10453 rateSource := newCommonRateSource(rateFetcher) 10454 c.fiatRateSources[source] = rateSource 10455 10456 select { 10457 case c.reFiat <- struct{}{}: 10458 default: 10459 } 10460 10461 // Update disabled fiat rate source. 10462 c.saveDisabledRateSources() 10463 10464 c.log.Infof("Enabled %s to fetch fiat rates.", source) 10465 return nil 10466 } 10467 10468 // disableRateSource disables a fiat rate source. 10469 func (c *Core) disableRateSource(source string) error { 10470 // Check if it's an invalid fiat rate source or it is already 10471 // disabled. 10472 _, found := fiatRateFetchers[source] 10473 if !found { 10474 return errors.New("cannot disable unknown fiat rate source") 10475 } 10476 10477 c.ratesMtx.Lock() 10478 defer c.ratesMtx.Unlock() 10479 10480 if c.fiatRateSources[source] == nil { 10481 return nil // already disabled. 10482 } 10483 10484 // Remove fiat rate source. 10485 delete(c.fiatRateSources, source) 10486 10487 // Save disabled fiat rate sources to database. 10488 c.saveDisabledRateSources() 10489 10490 c.log.Infof("Disabled %s from fetching fiat rates.", source) 10491 return nil 10492 } 10493 10494 // removeExpiredRateSources disables expired fiat rate source. 10495 func (c *Core) removeExpiredRateSources() { 10496 c.ratesMtx.Lock() 10497 defer c.ratesMtx.Unlock() 10498 10499 // Remove fiat rate source with expired exchange rate data. 10500 var disabledSources []string 10501 for token, source := range c.fiatRateSources { 10502 if source.isExpired(fiatRateDataExpiry) { 10503 delete(c.fiatRateSources, token) 10504 disabledSources = append(disabledSources, token) 10505 } 10506 } 10507 10508 // Ensure disabled fiat rate fetchers are saved to database. 10509 if len(disabledSources) > 0 { 10510 c.saveDisabledRateSources() 10511 c.log.Warnf("Expired rate source(s) has been disabled: %v", strings.Join(disabledSources, ", ")) 10512 } 10513 } 10514 10515 // saveDisabledRateSources saves disabled fiat rate sources to database and 10516 // shuts down rate fetching if there are no exchange rate source. Use under 10517 // ratesMtx lock. 10518 func (c *Core) saveDisabledRateSources() { 10519 var disabled []string 10520 for token := range fiatRateFetchers { 10521 if c.fiatRateSources[token] == nil { 10522 disabled = append(disabled, token) 10523 } 10524 } 10525 10526 err := c.db.SaveDisabledRateSources(disabled) 10527 if err != nil { 10528 c.log.Errorf("Unable to save disabled fiat rate source to database: %v", err) 10529 } 10530 } 10531 10532 // stakingWallet fetches the staking wallet and returns its asset.TicketBuyer 10533 // interface. Errors if no wallet is currently loaded. Used for ticket 10534 // purchasing. 10535 func (c *Core) stakingWallet(assetID uint32) (*xcWallet, asset.TicketBuyer, error) { 10536 wallet, exists := c.wallet(assetID) 10537 if !exists { 10538 return nil, nil, newError(missingWalletErr, "no configured wallet found for %s", unbip(assetID)) 10539 } 10540 ticketBuyer, is := wallet.Wallet.(asset.TicketBuyer) 10541 if !is { 10542 return nil, nil, fmt.Errorf("%s wallet is not a TicketBuyer", unbip(assetID)) 10543 } 10544 return wallet, ticketBuyer, nil 10545 } 10546 10547 // StakeStatus returns current staking statuses such as currently owned 10548 // tickets, ticket price, and current voting preferences. Used for 10549 // ticket purchasing. 10550 func (c *Core) StakeStatus(assetID uint32) (*asset.TicketStakingStatus, error) { 10551 _, tb, err := c.stakingWallet(assetID) 10552 if err != nil { 10553 return nil, err 10554 } 10555 return tb.StakeStatus() 10556 } 10557 10558 // SetVSP sets the VSP provider. Used for ticket purchasing. 10559 func (c *Core) SetVSP(assetID uint32, addr string) error { 10560 _, tb, err := c.stakingWallet(assetID) 10561 if err != nil { 10562 return err 10563 } 10564 return tb.SetVSP(addr) 10565 } 10566 10567 // PurchaseTickets purchases n tickets. Returns the purchased ticket hashes if 10568 // successful. Used for ticket purchasing. 10569 func (c *Core) PurchaseTickets(assetID uint32, pw []byte, n int) error { 10570 wallet, tb, err := c.stakingWallet(assetID) 10571 if err != nil { 10572 return err 10573 } 10574 crypter, err := c.encryptionKey(pw) 10575 if err != nil { 10576 return fmt.Errorf("password error: %w", err) 10577 } 10578 defer crypter.Close() 10579 10580 if err = c.connectAndUnlock(crypter, wallet); err != nil { 10581 return err 10582 } 10583 10584 if err = tb.PurchaseTickets(n, c.feeSuggestionAny(assetID)); err != nil { 10585 return err 10586 } 10587 c.updateAssetBalance(assetID) 10588 // TODO: Send tickets bought notification. 10589 //subject, details := c.formatDetails(TopicSendSuccess, sentValue, unbip(assetID), address, coin) 10590 //c.notify(newSendNote(TopicSendSuccess, subject, details, db.Success)) 10591 return nil 10592 } 10593 10594 // SetVotingPreferences sets default voting settings for all active tickets and 10595 // future tickets. Nil maps can be provided for no change. Used for ticket 10596 // purchasing. 10597 func (c *Core) SetVotingPreferences(assetID uint32, choices, tSpendPolicy, 10598 treasuryPolicy map[string]string) error { 10599 _, tb, err := c.stakingWallet(assetID) 10600 if err != nil { 10601 return err 10602 } 10603 return tb.SetVotingPreferences(choices, tSpendPolicy, treasuryPolicy) 10604 } 10605 10606 // ListVSPs lists known available voting service providers. 10607 func (c *Core) ListVSPs(assetID uint32) ([]*asset.VotingServiceProvider, error) { 10608 _, tb, err := c.stakingWallet(assetID) 10609 if err != nil { 10610 return nil, err 10611 } 10612 return tb.ListVSPs() 10613 } 10614 10615 // TicketPage fetches a page of TicketBuyer tickets within a range of block 10616 // numbers with a target page size and optional offset. scanStart it the block 10617 // in which to start the scan. The scan progresses in reverse block number 10618 // order, starting at scanStart and going to progressively lower blocks. 10619 // scanStart can be set to -1 to indicate the current chain tip. 10620 func (c *Core) TicketPage(assetID uint32, scanStart int32, n, skipN int) ([]*asset.Ticket, error) { 10621 _, tb, err := c.stakingWallet(assetID) 10622 if err != nil { 10623 return nil, err 10624 } 10625 return tb.TicketPage(scanStart, n, skipN) 10626 } 10627 10628 func (c *Core) mixingWallet(assetID uint32) (*xcWallet, asset.FundsMixer, error) { 10629 w, known := c.wallet(assetID) 10630 if !known { 10631 return nil, nil, fmt.Errorf("unknown wallet %d", assetID) 10632 } 10633 mw, is := w.Wallet.(asset.FundsMixer) 10634 if !is { 10635 return nil, nil, fmt.Errorf("%s wallet is not a FundsMixer", w.Info().Name) 10636 } 10637 return w, mw, nil 10638 } 10639 10640 // FundsMixingStats returns the current state of the wallet's funds mixer. 10641 func (c *Core) FundsMixingStats(assetID uint32) (*asset.FundsMixingStats, error) { 10642 _, mw, err := c.mixingWallet(assetID) 10643 if err != nil { 10644 return nil, err 10645 } 10646 return mw.FundsMixingStats() 10647 } 10648 10649 // ConfigureFundsMixer configures the wallet for funds mixing. 10650 func (c *Core) ConfigureFundsMixer(pw []byte, assetID uint32, isMixerEnabled bool) error { 10651 wallet, mw, err := c.mixingWallet(assetID) 10652 if err != nil { 10653 return err 10654 } 10655 crypter, err := c.encryptionKey(pw) 10656 if err != nil { 10657 return fmt.Errorf("mixing password error: %w", err) 10658 } 10659 defer crypter.Close() 10660 if err := c.connectAndUnlock(crypter, wallet); err != nil { 10661 return err 10662 } 10663 return mw.ConfigureFundsMixer(isMixerEnabled) 10664 } 10665 10666 // NetworkFeeRate generates a network tx fee rate for the specified asset. 10667 // If the wallet implements FeeRater, the wallet will be queried for the 10668 // fee rate. If the wallet is not a FeeRater, local book feed caches are 10669 // checked. If no relevant books are synced, connected DCRDEX servers will be 10670 // queried. 10671 func (c *Core) NetworkFeeRate(assetID uint32) uint64 { 10672 return c.feeSuggestionAny(assetID) 10673 } 10674 10675 func (c *Core) deleteRequestedAction(uniqueID string) { 10676 c.requestedActionMtx.Lock() 10677 delete(c.requestedActions, uniqueID) 10678 c.requestedActionMtx.Unlock() 10679 } 10680 10681 // handleRetryRedemptionAction handles a response to a user response to an 10682 // ActionRequiredNote for a rejected redemption transaction. 10683 func (c *Core) handleRetryRedemptionAction(actionB []byte) error { 10684 var req struct { 10685 OrderID dex.Bytes `json:"orderID"` 10686 CoinID dex.Bytes `json:"coinID"` 10687 Retry bool `json:"retry"` 10688 } 10689 if err := json.Unmarshal(actionB, &req); err != nil { 10690 return fmt.Errorf("error decoding request: %w", err) 10691 } 10692 c.deleteRequestedAction(req.CoinID.String()) 10693 10694 if !req.Retry { 10695 // Do nothing 10696 return nil 10697 } 10698 var oid order.OrderID 10699 copy(oid[:], req.OrderID) 10700 var tracker *trackedTrade 10701 for _, dc := range c.dexConnections() { 10702 tracker, _ = dc.findOrder(oid) 10703 if tracker != nil { 10704 break 10705 } 10706 } 10707 if tracker == nil { 10708 return fmt.Errorf("order %s not known", oid) 10709 } 10710 tracker.mtx.Lock() 10711 defer tracker.mtx.Unlock() 10712 10713 for _, match := range tracker.matches { 10714 coinID := match.MetaData.Proof.TakerRedeem 10715 if match.Side == order.Maker { 10716 coinID = match.MetaData.Proof.MakerRedeem 10717 } 10718 if bytes.Equal(coinID, req.CoinID) { 10719 if match.Side == order.Taker && match.Status == order.MatchComplete { 10720 // Try to redeem again. 10721 match.redemptionRejected = false 10722 match.MetaData.Proof.TakerRedeem = nil 10723 match.Status = order.MakerRedeemed 10724 if err := c.db.UpdateMatch(&match.MetaMatch); err != nil { 10725 c.log.Errorf("Failed to update match in DB: %v", err) 10726 } 10727 } else if match.Side == order.Maker && match.Status == order.MakerRedeemed { 10728 match.redemptionRejected = false 10729 match.MetaData.Proof.MakerRedeem = nil 10730 match.Status = order.TakerSwapCast 10731 if err := c.db.UpdateMatch(&match.MetaMatch); err != nil { 10732 c.log.Errorf("Failed to update match in DB: %v", err) 10733 } 10734 } else { 10735 c.log.Errorf("Redemption retry attempted for order side %s status %s", match.Side, match.Status) 10736 } 10737 } 10738 } 10739 return nil 10740 } 10741 10742 // handleCoreAction checks if the actionID is a known core action, and if so 10743 // attempts to take the action requested. 10744 func (c *Core) handleCoreAction(actionID string, actionB json.RawMessage) ( /* handled */ bool, error) { 10745 switch actionID { 10746 case ActionIDRedeemRejected: 10747 return true, c.handleRetryRedemptionAction(actionB) 10748 } 10749 return false, nil 10750 } 10751 10752 // TakeAction is called in response to a ActionRequiredNote. The note may have 10753 // come from core or from a wallet. 10754 func (c *Core) TakeAction(assetID uint32, actionID string, actionB json.RawMessage) (err error) { 10755 defer func() { 10756 if err != nil { 10757 c.log.Errorf("Error while attempting user action %q with parameters %q, asset ID %d: %v", 10758 actionID, string(actionB), assetID, err) 10759 } else { 10760 c.log.Infof("User completed action %q with parameters %q, asset ID %d", 10761 actionID, string(actionB), assetID) 10762 } 10763 }() 10764 if handled, err := c.handleCoreAction(actionID, actionB); handled { 10765 return err 10766 } 10767 w, err := c.connectedWallet(assetID) 10768 if err != nil { 10769 return err 10770 } 10771 goGetter, is := w.Wallet.(asset.ActionTaker) 10772 if !is { 10773 return fmt.Errorf("wallet for %s cannot handle user actions", w.Symbol) 10774 } 10775 return goGetter.TakeAction(actionID, actionB) 10776 } 10777 10778 // GenerateBCHRecoveryTransaction generates a tx that spends all inputs from the 10779 // deprecated BCH wallet to the given recipient. 10780 func (c *Core) GenerateBCHRecoveryTransaction(appPW []byte, recipient string) ([]byte, error) { 10781 const bipID = 145 10782 crypter, err := c.encryptionKey(appPW) 10783 if err != nil { 10784 return nil, err 10785 } 10786 _, walletPW, err := c.assetSeedAndPass(bipID, crypter) 10787 if err != nil { 10788 return nil, err 10789 } 10790 return asset.SPVWithdrawTx(c.ctx, bipID, walletPW, recipient, c.assetDataDirectory(bipID), c.net, c.log.SubLogger("BCH")) 10791 } 10792 10793 func (c *Core) checkEpochResolution(host string, mktID string) { 10794 dc, _, _ := c.dex(host) 10795 if dc == nil { 10796 return 10797 } 10798 currentEpoch := dc.marketEpoch(mktID, time.Now()) 10799 lastEpoch := currentEpoch - 1 10800 10801 // Short path if we're already resolved. 10802 dc.epochMtx.RLock() 10803 resolvedEpoch := dc.resolvedEpoch[mktID] 10804 dc.epochMtx.RUnlock() 10805 if lastEpoch == resolvedEpoch { 10806 return 10807 } 10808 10809 ts, inFlights := dc.marketTrades(mktID) 10810 for _, ord := range inFlights { 10811 if ord.Epoch == lastEpoch { 10812 return 10813 } 10814 } 10815 for _, t := range ts { 10816 // Is this order from the last epoch and still not booked or executed? 10817 if t.epochIdx() == lastEpoch && t.status() == order.OrderStatusEpoch { 10818 return 10819 } 10820 // Does this order have an in-flight cancel order that is not yet 10821 // resolved? 10822 t.mtx.RLock() 10823 unresolvedCancel := t.cancel != nil && t.cancelEpochIdx() == lastEpoch && t.cancel.matches.taker == nil 10824 t.mtx.RUnlock() 10825 if unresolvedCancel { 10826 return 10827 } 10828 } 10829 10830 // We don't have any unresolved orders or cancel orders from the last epoch. 10831 // Just make sure that not other thread has resolved the epoch and then send 10832 // the notification. 10833 dc.epochMtx.Lock() 10834 sendUpdate := lastEpoch > dc.resolvedEpoch[mktID] 10835 dc.resolvedEpoch[mktID] = lastEpoch 10836 dc.epochMtx.Unlock() 10837 if sendUpdate { 10838 if bookie := dc.bookie(mktID); bookie != nil { 10839 bookie.send(&BookUpdate{ 10840 Action: EpochResolved, 10841 Host: dc.acct.host, 10842 MarketID: mktID, 10843 Payload: &ResolvedEpoch{ 10844 Current: currentEpoch, 10845 Resolved: lastEpoch, 10846 }, 10847 }) 10848 } 10849 10850 } 10851 } 10852 10853 // RedeemGeocode redeems the provided game code with the wallet and redeems the 10854 // prepaid bond (code is a prepaid bond). If the user is not registered with 10855 // dex.decred.org yet, the dex will be added first. 10856 func (c *Core) RedeemGeocode(appPW, code []byte, msg string) (dex.Bytes, uint64, error) { 10857 const dcrBipID = 42 10858 dcrWallet, found := c.wallet(dcrBipID) 10859 if !found { 10860 return nil, 0, errors.New("no decred wallet") 10861 } 10862 if !dcrWallet.connected() { 10863 return nil, 0, errors.New("decred wallet is not connected") 10864 } 10865 10866 host := "dex.decred.org:7232" 10867 switch c.net { 10868 case dex.Testnet: 10869 host = "bison.exchange:17232" 10870 case dex.Simnet: 10871 host = "127.0.0.1:17273" 10872 } 10873 cert := CertStore[c.net][host] 10874 10875 c.connMtx.RLock() 10876 dc, found := c.conns[host] 10877 c.connMtx.RUnlock() 10878 if !found { 10879 if err := c.AddDEX(appPW, host, cert); err != nil { 10880 return nil, 0, fmt.Errorf("error adding %s: %w", host, err) 10881 } 10882 c.connMtx.RLock() 10883 _, found = c.conns[host] 10884 c.connMtx.RUnlock() 10885 if !found { 10886 return nil, 0, fmt.Errorf("dex not found after adding") 10887 } 10888 } else if dc.status() != comms.Connected { 10889 return nil, 0, fmt.Errorf("not currently connected to %s", host) 10890 } 10891 10892 w, is := dcrWallet.Wallet.(asset.GeocodeRedeemer) 10893 if !is { 10894 return nil, 0, errors.New("decred wallet is not a GeocodeRedeemer?") 10895 } 10896 10897 coinID, win, err := w.RedeemGeocode(code, msg) 10898 if err != nil { 10899 return nil, 0, fmt.Errorf("error redeeming geocode: %w", err) 10900 } 10901 10902 if _, err := c.RedeemPrepaidBond(appPW, code, host, cert); err != nil { 10903 return nil, 0, fmt.Errorf("geocode redeemed, but failed to redeem prepaid bond: %w", err) 10904 } 10905 10906 return coinID, win, nil 10907 } 10908 10909 // ExtensionModeConfig is the configuration parsed from the extension-mode file. 10910 func (c *Core) ExtensionModeConfig() *ExtensionModeConfig { 10911 return c.extensionModeConfig 10912 } 10913 10914 // calcParcelLimit computes the users score-scaled user parcel limit. 10915 func calcParcelLimit(tier int64, score, maxScore int32) uint32 { 10916 // Users limit starts at 2 parcels per tier. 10917 lowerLimit := tier * dex.PerTierBaseParcelLimit 10918 // Limit can scale up to 3x with score. 10919 upperLimit := lowerLimit * dex.ParcelLimitScoreMultiplier 10920 limitRange := upperLimit - lowerLimit 10921 var scaleFactor float64 10922 if score > 0 { 10923 scaleFactor = float64(score) / float64(maxScore) 10924 } 10925 return uint32(lowerLimit) + uint32(math.Round(scaleFactor*float64(limitRange))) 10926 } 10927 10928 // TradingLimits returns the number of parcels the user can trade on an 10929 // exchange and the amount that are currently being traded. 10930 func (c *Core) TradingLimits(host string) (userParcels, parcelLimit uint32, err error) { 10931 dc, _, err := c.dex(host) 10932 if err != nil { 10933 return 0, 0, err 10934 } 10935 10936 cfg := dc.config() 10937 dc.acct.authMtx.RLock() 10938 rep := dc.acct.rep 10939 dc.acct.authMtx.RUnlock() 10940 10941 mkts := make(map[string]*msgjson.Market, len(cfg.Markets)) 10942 for _, mkt := range cfg.Markets { 10943 mkts[mkt.Name] = mkt 10944 } 10945 mktTrades := make(map[string][]*trackedTrade) 10946 for _, t := range dc.trackedTrades() { 10947 mktTrades[t.mktID] = append(mktTrades[t.mktID], t) 10948 } 10949 10950 parcelLimit = calcParcelLimit(rep.EffectiveTier(), rep.Score, int32(cfg.MaxScore)) 10951 for mktID, trades := range mktTrades { 10952 mkt := mkts[mktID] 10953 if mkt == nil { 10954 c.log.Warnf("trade for unknown market %q", mktID) 10955 continue 10956 } 10957 10958 var midGap, mktWeight uint64 10959 for _, t := range trades { 10960 if t.isEpochOrder() && midGap == 0 { 10961 midGap, err = dc.midGap(mkt.Base, mkt.Quote) 10962 if err != nil && !errors.Is(err, orderbook.ErrEmptyOrderbook) { 10963 return 0, 0, err 10964 } 10965 } 10966 mktWeight += t.marketWeight(midGap, mkt.LotSize) 10967 } 10968 userParcels += uint32(mktWeight / (uint64(mkt.ParcelSize) * mkt.LotSize)) 10969 } 10970 10971 return userParcels, parcelLimit, nil 10972 }