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