decred.org/dcrdex@v1.0.5/server/dex/dex.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 dex 5 6 import ( 7 "context" 8 "encoding/json" 9 "fmt" 10 "io" 11 "math" 12 "net/http" 13 "os" 14 "path/filepath" 15 "sort" 16 "strconv" 17 "strings" 18 "sync" 19 "time" 20 21 "decred.org/dcrdex/dex" 22 "decred.org/dcrdex/dex/calc" 23 "decred.org/dcrdex/dex/candles" 24 "decred.org/dcrdex/dex/fiatrates" 25 "decred.org/dcrdex/dex/msgjson" 26 "decred.org/dcrdex/dex/order" 27 "decred.org/dcrdex/server/account" 28 "decred.org/dcrdex/server/apidata" 29 "decred.org/dcrdex/server/asset" 30 "decred.org/dcrdex/server/auth" 31 "decred.org/dcrdex/server/coinlock" 32 "decred.org/dcrdex/server/comms" 33 "decred.org/dcrdex/server/db" 34 "decred.org/dcrdex/server/db/driver/pg" 35 "decred.org/dcrdex/server/market" 36 "decred.org/dcrdex/server/noderelay" 37 "decred.org/dcrdex/server/swap" 38 "github.com/decred/dcrd/dcrec/secp256k1/v4" 39 "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" 40 "github.com/go-chi/chi/v5" 41 "github.com/go-chi/chi/v5/middleware" 42 ) 43 44 const ( 45 // PreAPIVersion covers all API iterations before versioning started. 46 PreAPIVersion = iota 47 BondAPIVersion // when we drop the legacy reg fee proto 48 V1APIVersion 49 50 // APIVersion is the current API version. 51 APIVersion = V1APIVersion 52 ) 53 54 // Asset represents an asset in the Config file. 55 type Asset struct { 56 Symbol string `json:"bip44symbol"` 57 Network string `json:"network"` 58 LotSizeOLD uint64 `json:"lotSize,omitempty"` 59 RateStepOLD uint64 `json:"rateStep,omitempty"` 60 MaxFeeRate uint64 `json:"maxFeeRate"` 61 SwapConf uint32 `json:"swapConf"` 62 ConfigPath string `json:"configPath"` 63 RegFee uint64 `json:"regFee,omitempty"` 64 RegConfs uint32 `json:"regConfs,omitempty"` 65 RegXPub string `json:"regXPub,omitempty"` 66 BondAmt uint64 `json:"bondAmt,omitempty"` 67 BondConfs uint32 `json:"bondConfs,omitempty"` 68 Disabled bool `json:"disabled"` 69 NodeRelayID string `json:"nodeRelayID,omitempty"` 70 } 71 72 // Market represents the markets specified in the Config file. 73 type Market struct { 74 Base string `json:"base"` 75 Quote string `json:"quote"` 76 LotSize uint64 `json:"lotSize"` 77 ParcelSize uint32 `json:"parcelSize"` 78 RateStep uint64 `json:"rateStep"` 79 Duration uint64 `json:"epochDuration"` 80 MBBuffer float64 `json:"marketBuyBuffer"` 81 Disabled bool `json:"disabled"` 82 } 83 84 // Config is a market and asset configuration file. 85 type Config struct { 86 Markets []*Market `json:"markets"` 87 Assets map[string]*Asset `json:"assets"` 88 } 89 90 // LoadConfig loads the Config from the specified file. 91 func LoadConfig(net dex.Network, filePath string) ([]*dex.MarketInfo, []*Asset, error) { 92 src, err := os.Open(filePath) 93 if err != nil { 94 return nil, nil, err 95 } 96 defer src.Close() 97 return loadMarketConf(net, src) 98 } 99 100 func loadMarketConf(net dex.Network, src io.Reader) ([]*dex.MarketInfo, []*Asset, error) { 101 settings, err := io.ReadAll(src) 102 if err != nil { 103 return nil, nil, err 104 } 105 106 var conf Config 107 err = json.Unmarshal(settings, &conf) 108 if err != nil { 109 return nil, nil, err 110 } 111 112 log.Debug("|-------------------- BEGIN parsed markets.json --------------------") 113 log.Debug("MARKETS") 114 log.Debug(" Base Quote LotSize EpochDur") 115 for i, mktConf := range conf.Markets { 116 if mktConf.LotSize == 0 { 117 return nil, nil, fmt.Errorf("market (%s, %s) has NO lot size specified (was an asset setting)", 118 mktConf.Base, mktConf.Quote) 119 } 120 if mktConf.RateStep == 0 { 121 return nil, nil, fmt.Errorf("market (%s, %s) has NO rate step specified (was an asset setting)", 122 mktConf.Base, mktConf.Quote) 123 } 124 log.Debugf("Market %d: % 12s % 12s %6de8 % 8d ms", 125 i, mktConf.Base, mktConf.Quote, mktConf.LotSize/1e8, mktConf.Duration) 126 } 127 log.Debug("") 128 129 log.Debug("ASSETS") 130 log.Debug(" MaxFeeRate SwapConf Network") 131 for asset, assetConf := range conf.Assets { 132 if assetConf.LotSizeOLD > 0 { 133 return nil, nil, fmt.Errorf("asset %s has a lot size (%d) specified, "+ 134 "but this is now a market setting", asset, assetConf.LotSizeOLD) 135 } 136 if assetConf.RateStepOLD > 0 { 137 return nil, nil, fmt.Errorf("asset %s has a rate step (%d) specified, "+ 138 "but this is now a market setting", asset, assetConf.RateStepOLD) 139 } 140 log.Debugf("%-12s % 10d % 9d % 9s", asset, assetConf.MaxFeeRate, assetConf.SwapConf, assetConf.Network) 141 } 142 log.Debug("|--------------------- END parsed markets.json ---------------------|") 143 144 // Normalize the asset names to lower case. 145 var assets []*Asset 146 assetMap := make(map[uint32]struct{}) 147 unused := make(map[uint32]string) 148 for assetName, assetConf := range conf.Assets { 149 if assetConf.Disabled { 150 continue 151 } 152 network, err := dex.NetFromString(assetConf.Network) 153 if err != nil { 154 return nil, nil, fmt.Errorf("unrecognized network %s for asset %s", 155 assetConf.Network, assetName) 156 } 157 if net != network { 158 continue 159 } 160 161 symbol := strings.ToLower(assetConf.Symbol) 162 assetID, found := dex.BipSymbolID(symbol) 163 if !found { 164 return nil, nil, fmt.Errorf("asset %q symbol %q unrecognized", assetName, assetConf.Symbol) 165 } 166 167 if assetConf.MaxFeeRate == 0 { 168 return nil, nil, fmt.Errorf("max fee rate of 0 is invalid for asset %q", assetConf.Symbol) 169 } 170 171 unused[assetID] = assetConf.Symbol 172 assetMap[assetID] = struct{}{} 173 assets = append(assets, assetConf) 174 } 175 176 sort.Slice(assets, func(i, j int) bool { 177 return assets[i].Symbol < assets[j].Symbol 178 }) 179 180 var markets []*dex.MarketInfo 181 for _, mktConf := range conf.Markets { 182 if mktConf.Disabled { 183 continue 184 } 185 baseConf, ok := conf.Assets[mktConf.Base] 186 if !ok { 187 return nil, nil, fmt.Errorf("missing configuration for asset %s", mktConf.Base) 188 } 189 if baseConf.Disabled { 190 return nil, nil, fmt.Errorf("required base asset %s is disabled", mktConf.Base) 191 } 192 quoteConf, ok := conf.Assets[mktConf.Quote] 193 if !ok { 194 return nil, nil, fmt.Errorf("missing configuration for asset %s", mktConf.Quote) 195 } 196 if quoteConf.Disabled { 197 return nil, nil, fmt.Errorf("required quote asset %s is disabled", mktConf.Base) 198 } 199 200 baseID, _ := dex.BipSymbolID(baseConf.Symbol) 201 quoteID, _ := dex.BipSymbolID(quoteConf.Symbol) 202 203 delete(unused, baseID) 204 delete(unused, quoteID) 205 206 if is, parentID := asset.IsToken(baseID); is { 207 if _, found := assetMap[parentID]; !found { 208 return nil, nil, fmt.Errorf("parent asset %s not enabled for token %s", dex.BipIDSymbol(parentID), baseConf.Symbol) 209 } 210 delete(unused, parentID) 211 } 212 213 if is, parentID := asset.IsToken(quoteID); is { 214 if _, found := assetMap[parentID]; !found { 215 return nil, nil, fmt.Errorf("parent asset %s not enabled for token %s", dex.BipIDSymbol(parentID), quoteConf.Symbol) 216 } 217 delete(unused, parentID) 218 } 219 220 baseNet, err := dex.NetFromString(baseConf.Network) 221 if err != nil { 222 return nil, nil, fmt.Errorf("unrecognized network %s", baseConf.Network) 223 } 224 quoteNet, err := dex.NetFromString(quoteConf.Network) 225 if err != nil { 226 return nil, nil, fmt.Errorf("unrecognized network %s", quoteConf.Network) 227 } 228 229 if baseNet != quoteNet { 230 return nil, nil, fmt.Errorf("assets are for different networks (%s and %s)", 231 baseConf.Network, quoteConf.Network) 232 } 233 234 if baseNet != net { 235 continue 236 } 237 238 if mktConf.ParcelSize == 0 { 239 return nil, nil, fmt.Errorf("parcel size cannot be zero") 240 } 241 242 mkt, err := dex.NewMarketInfoFromSymbols(baseConf.Symbol, quoteConf.Symbol, 243 mktConf.LotSize, mktConf.RateStep, mktConf.Duration, mktConf.ParcelSize, mktConf.MBBuffer) 244 if err != nil { 245 return nil, nil, err 246 } 247 markets = append(markets, mkt) 248 } 249 250 if len(unused) > 0 { 251 symbols := make([]string, 0, len(unused)) 252 for _, symbol := range unused { 253 symbols = append(symbols, symbol) 254 } 255 return nil, nil, fmt.Errorf("unused assets %+v", symbols) 256 } 257 258 return markets, assets, nil 259 } 260 261 // DBConf groups the database configuration parameters. 262 type DBConf struct { 263 DBName string 264 User string 265 Pass string 266 Host string 267 Port uint16 268 ShowPGConfig bool 269 } 270 271 // ValidateConfigFile validates the market+assets configuration file. 272 // ValidateConfigFile prints information to stdout. An error is returned for any 273 // configuration errors. 274 func ValidateConfigFile(cfgPath string, net dex.Network, log dex.Logger) error { 275 ctx, cancel := context.WithCancel(context.Background()) 276 defer cancel() 277 278 registeredAssets := asset.Assets() 279 coinpapAssets := make([]*fiatrates.CoinpaprikaAsset, 0, len(registeredAssets)) 280 for _, a := range registeredAssets { 281 coinpapAssets = append(coinpapAssets, &fiatrates.CoinpaprikaAsset{ 282 AssetID: a.AssetID, 283 Name: a.Name, 284 Symbol: a.Symbol, 285 }) 286 } 287 fiatRates := fiatrates.FetchCoinpaprikaRates(ctx, coinpapAssets, dex.StdOutLogger("CPAP", dex.LevelInfo)) 288 289 log.Debugf("Loaded %d fiat rates from coinpaprika", len(fiatRates)) 290 291 markets, assets, err := LoadConfig(net, cfgPath) 292 if err != nil { 293 return fmt.Errorf("error loading config file at %q: %w", cfgPath, err) 294 } 295 296 log.Debugf("Loaded %d markets and %d assets from configuration", len(markets), len(assets)) 297 298 var failures []string 299 300 type parsedAsset struct { 301 *Asset 302 unit string 303 minLotSize uint64 304 minBondSize uint64 305 fiatRate float64 306 cFactor float64 307 ui *dex.UnitInfo 308 } 309 parsedAssets := make(map[uint32]*parsedAsset, len(assets)) 310 311 printSuccess := func(s string, a ...interface{}) { 312 fmt.Printf(s+"\n", a...) 313 } 314 315 for _, a := range assets { 316 if dex.TokenSymbol(a.Symbol) == "dextt" { 317 continue 318 } 319 320 assetID, ok := dex.BipSymbolID(a.Symbol) 321 if !ok { 322 return fmt.Errorf("no asset ID found for symbol %q", a.Symbol) 323 } 324 325 minLotSize, minBondSize, found := asset.Minimums(assetID, a.MaxFeeRate) 326 if !found { 327 return fmt.Errorf("no asset registered for %s (%d)", a.Symbol, assetID) 328 } 329 330 ui, err := asset.UnitInfo(assetID) 331 if err != nil { 332 return fmt.Errorf("error getting unit info for %s: %w", a.Symbol, err) 333 } 334 335 fiatRate, found := fiatRates[assetID] 336 if !found { 337 return fmt.Errorf("no fiat exchange rate found for asset %s (%d)", dex.BipIDSymbol(assetID), assetID) 338 } 339 340 unit := ui.Conventional.Unit 341 minLotSizeUSD := float64(minLotSize) / float64(ui.Conventional.ConversionFactor) * fiatRate 342 minBondSizeUSD := float64(minLotSize) / float64(ui.Conventional.ConversionFactor) * fiatRate 343 parsedAssets[assetID] = &parsedAsset{ 344 Asset: a, 345 unit: unit, 346 minLotSize: minLotSize, 347 minBondSize: minBondSize, 348 fiatRate: fiatRate, 349 cFactor: float64(ui.Conventional.ConversionFactor), 350 ui: &ui, 351 } 352 printSuccess( 353 "Calculated for %s: min lot size = %s %s (%d %s) (~ %.4f USD), min bond size = %s %s (%d %s) (%.4f USD)", 354 a.Symbol, ui.ConventionalString(minLotSize), unit, minLotSize, ui.AtomicUnit, minLotSizeUSD, 355 ui.ConventionalString(minBondSize), unit, minBondSize, ui.AtomicUnit, minBondSizeUSD, 356 ) 357 } 358 359 for _, a := range parsedAssets { 360 if a.BondAmt == 0 { 361 continue 362 } 363 if a.BondAmt < a.minBondSize { 364 failures = append(failures, fmt.Sprintf("Bond amount for %s is too small. %d < %d", a.Symbol, a.BondAmt, a.minBondSize)) 365 } else { 366 printSuccess("Bond size for %s passes: %d >= %d", a.Symbol, a.BondAmt, a.minBondSize) 367 } 368 } 369 370 for _, m := range markets { 371 // Check lot size. 372 b, q := parsedAssets[m.Base], parsedAssets[m.Quote] 373 if b == nil || q == nil { 374 continue // should be dextt pair 375 } 376 const quoteConversionBuffer = 1.5 // Buffer for accomodating rate changes. 377 minFromQuote := uint64(math.Round(float64(q.minLotSize) / q.cFactor * q.fiatRate / b.fiatRate * b.cFactor * quoteConversionBuffer)) 378 // Slightly different messaging if we're limited by the conversion from 379 // the quote asset minimums. 380 if minFromQuote > b.minLotSize { 381 if m.LotSize < minFromQuote { 382 failures = append(failures, fmt.Sprintf("Lot size for %s (converted from quote asset %s) is too low. %d < %d", m.Name, q.unit, m.LotSize, minFromQuote)) 383 } else { 384 printSuccess("Market %s lot size (converted from quote asset %s) passes: %d >= %d", m.Name, q.unit, m.LotSize, minFromQuote) 385 } 386 } else { 387 if m.LotSize < b.minLotSize { 388 failures = append(failures, fmt.Sprintf("Lot size for %s is too low. %d < %d", m.Name, m.LotSize, b.minLotSize)) 389 } else { 390 printSuccess("Market %s lot size passes: %d >= %d", m.Name, m.LotSize, b.minLotSize) 391 } 392 } 393 } 394 395 for _, s := range failures { 396 fmt.Println("FAIL:", s) 397 } 398 399 if len(failures) > 0 { 400 return fmt.Errorf("%d market or asset configuration problems need fixing", len(failures)) 401 } 402 403 return nil 404 } 405 406 // RPCConfig is an alias for the comms Server's RPC config struct. 407 type RPCConfig = comms.RPCConfig 408 409 // DexConf is the configuration data required to create a new DEX. 410 type DexConf struct { 411 DataDir string 412 LogBackend *dex.LoggerMaker 413 Markets []*dex.MarketInfo 414 Assets []*Asset 415 Network dex.Network 416 DBConf *DBConf 417 BroadcastTimeout time.Duration 418 TxWaitExpiration time.Duration 419 CancelThreshold float64 420 FreeCancels bool 421 PenaltyThreshold uint32 422 DEXPrivKey *secp256k1.PrivateKey 423 CommsCfg *RPCConfig 424 NoResumeSwaps bool 425 NodeRelayAddr string 426 } 427 428 type signer struct { 429 *secp256k1.PrivateKey 430 } 431 432 func (s signer) Sign(hash []byte) *ecdsa.Signature { 433 return ecdsa.Sign(s.PrivateKey, hash) 434 } 435 436 type subsystem struct { 437 name string 438 // either a ssw or cm 439 ssw *dex.StartStopWaiter 440 cm *dex.ConnectionMaster 441 } 442 443 func (ss *subsystem) stop() { 444 if ss.ssw != nil { 445 ss.ssw.Stop() 446 ss.ssw.WaitForShutdown() 447 } else { 448 ss.cm.Disconnect() 449 ss.cm.Wait() 450 } 451 } 452 453 // DEX is the DEX manager, which creates and controls the lifetime of all 454 // components of the DEX. 455 type DEX struct { 456 network dex.Network 457 markets map[string]*market.Market 458 assets map[uint32]*swap.SwapperAsset 459 storage db.DEXArchivist 460 authMgr *auth.AuthManager 461 swapper *swap.Swapper 462 orderRouter *market.OrderRouter 463 bookRouter *market.BookRouter 464 subsystems []subsystem 465 server *comms.Server 466 467 configRespMtx sync.RWMutex 468 configResp *configResponse 469 } 470 471 // configResponse is defined here to leave open the possibility for hot 472 // adjustable parameters while storing a pre-encoded config response message. An 473 // update method will need to be defined in the future for this purpose. 474 type configResponse struct { 475 configMsg *msgjson.ConfigResult // constant for now 476 configEnc json.RawMessage 477 } 478 479 func newConfigResponse(cfg *DexConf, bondAssets map[string]*msgjson.BondAsset, 480 cfgAssets []*msgjson.Asset, cfgMarkets []*msgjson.Market) (*configResponse, error) { 481 482 configMsg := &msgjson.ConfigResult{ 483 APIVersion: uint16(APIVersion), 484 DEXPubKey: cfg.DEXPrivKey.PubKey().SerializeCompressed(), 485 BroadcastTimeout: uint64(cfg.BroadcastTimeout.Milliseconds()), 486 CancelMax: cfg.CancelThreshold, 487 Assets: cfgAssets, 488 Markets: cfgMarkets, 489 BondAssets: bondAssets, 490 BondExpiry: uint64(dex.BondExpiry(cfg.Network)), // temporary while we figure it out 491 BinSizes: candles.BinSizes, 492 PenaltyThreshold: cfg.PenaltyThreshold, 493 MaxScore: auth.ScoringMatchLimit, 494 } 495 496 // NOTE/TODO: To include active epoch in the market status objects, we need 497 // a channel from Market to push status changes back to DEX manager. 498 // Presently just include start epoch that we set when launching the 499 // Markets, and suspend info that DEX obtained when calling the Market's 500 // Suspend method. 501 502 encResult, err := json.Marshal(configMsg) 503 if err != nil { 504 return nil, err 505 } 506 507 return &configResponse{ 508 configMsg: configMsg, 509 configEnc: encResult, 510 }, nil 511 } 512 513 func (cr *configResponse) setMktSuspend(name string, finalEpoch uint64, persist bool) { 514 for _, mkt := range cr.configMsg.Markets { 515 if mkt.Name == name { 516 mkt.MarketStatus.FinalEpoch = finalEpoch 517 mkt.MarketStatus.Persist = &persist 518 cr.remarshal() 519 return 520 } 521 } 522 log.Errorf("Failed to update MarketStatus for market %q", name) 523 } 524 525 func (cr *configResponse) setMktResume(name string, startEpoch uint64) (epochLen uint64) { 526 for _, mkt := range cr.configMsg.Markets { 527 if mkt.Name == name { 528 mkt.MarketStatus.StartEpoch = startEpoch 529 mkt.MarketStatus.FinalEpoch = 0 530 cr.remarshal() 531 return mkt.EpochLen 532 } 533 } 534 log.Errorf("Failed to update MarketStatus for market %q", name) 535 return 0 536 } 537 538 func (cr *configResponse) remarshal() { 539 encResult, err := json.Marshal(cr.configMsg) 540 if err != nil { 541 log.Errorf("failed to marshal config message: %v", err) 542 return 543 } 544 cr.configEnc = encResult 545 } 546 547 // Stop shuts down the DEX. Stop returns only after all components have 548 // completed their shutdown. 549 func (dm *DEX) Stop() { 550 log.Infof("Stopping all DEX subsystems.") 551 for _, ss := range dm.subsystems { 552 log.Infof("Stopping %s...", ss.name) 553 ss.stop() 554 log.Infof("%s is now shut down.", ss.name) 555 } 556 log.Infof("Stopping storage...") 557 if err := dm.storage.Close(); err != nil { 558 log.Errorf("DEXArchivist.Close: %v", err) 559 } 560 } 561 562 func marketSubSysName(name string) string { 563 return fmt.Sprintf("Market[%s]", name) 564 } 565 566 func (dm *DEX) handleDEXConfig(any) (any, error) { 567 dm.configRespMtx.RLock() 568 defer dm.configRespMtx.RUnlock() 569 return dm.configResp.configEnc, nil 570 } 571 572 func (dm *DEX) handleHealthFlag(any) (any, error) { 573 return dm.Healthy(), nil 574 } 575 576 // FeeCoiner describes a type that can check a transaction output, namely a fee 577 // payment, for a particular asset. 578 type FeeCoiner interface { 579 FeeCoin(coinID []byte) (addr string, val uint64, confs int64, err error) 580 } 581 582 // Bonder describes a type that supports parsing raw bond transactions and 583 // locating them on-chain via coin ID. 584 type Bonder interface { 585 BondVer() uint16 586 BondCoin(ctx context.Context, ver uint16, coinID []byte) (amt, lockTime, confs int64, 587 acct account.AccountID, err error) 588 ParseBondTx(ver uint16, rawTx []byte) (bondCoinID []byte, amt int64, bondAddr string, 589 bondPubKeyHash []byte, lockTime int64, acct account.AccountID, err error) 590 } 591 592 // NewDEX creates the dex manager and starts all subsystems. Use Stop to 593 // shutdown cleanly. The Context is used to abort setup. 594 // 1. Validate each specified asset. 595 // 2. Create CoinLockers for each asset. 596 // 3. Create and start asset backends. 597 // 4. Create the archivist and connect to the storage backend. 598 // 5. Create the authentication manager. 599 // 6. Create and start the Swapper. 600 // 7. Create and start the markets. 601 // 8. Create and start the book router, and create the order router. 602 // 9. Create and start the comms server. 603 func NewDEX(ctx context.Context, cfg *DexConf) (*DEX, error) { 604 var subsystems []subsystem 605 startSubSys := func(name string, rc any) (err error) { 606 subsys := subsystem{name: name} 607 switch st := rc.(type) { 608 case dex.Runner: 609 subsys.ssw = dex.NewStartStopWaiter(st) 610 subsys.ssw.Start(context.Background()) // stopped with Stop 611 case dex.Connector: 612 subsys.cm = dex.NewConnectionMaster(st) 613 err = subsys.cm.Connect(context.Background()) // stopped with Disconnect 614 if err != nil { 615 return 616 } 617 default: 618 panic(fmt.Sprintf("Invalid subsystem type %T", rc)) 619 } 620 621 subsystems = append([]subsystem{subsys}, subsystems...) // top of stack 622 return 623 } 624 625 // Do not wrap the caller's context for the DB since we must coordinate it's 626 // shutdown in sequence with the other subsystems. 627 ctxDB, cancelDB := context.WithCancel(context.Background()) 628 var ready bool 629 defer func() { 630 if ready { 631 return 632 } 633 for _, ss := range subsystems { 634 ss.stop() 635 } 636 // If the DB is running, kill it too. 637 cancelDB() 638 }() 639 640 // Check each configured asset. 641 assetIDs := make([]uint32, len(cfg.Assets)) 642 var nodeRelayIDs []string 643 for i, assetConf := range cfg.Assets { 644 symbol := strings.ToLower(assetConf.Symbol) 645 646 // Ensure the symbol is a recognized BIP44 symbol, and retrieve its ID. 647 assetID, found := dex.BipSymbolID(symbol) 648 if !found { 649 return nil, fmt.Errorf("asset symbol %q unrecognized", assetConf.Symbol) 650 } 651 652 // Double check the asset's network. 653 net, err := dex.NetFromString(assetConf.Network) 654 if err != nil { 655 return nil, fmt.Errorf("unrecognized network %s for asset %s", 656 assetConf.Network, symbol) 657 } 658 if cfg.Network != net { 659 return nil, fmt.Errorf("asset %q is configured for network %q, expected %q", 660 symbol, assetConf.Network, cfg.Network.String()) 661 } 662 663 if assetConf.MaxFeeRate == 0 { 664 return nil, fmt.Errorf("max fee rate of 0 is invalid for asset %q", symbol) 665 } 666 667 if assetConf.NodeRelayID != "" { 668 nodeRelayIDs = append(nodeRelayIDs, assetConf.NodeRelayID) 669 } 670 671 assetIDs[i] = assetID 672 } 673 674 // Create DEXArchivist with the pg DB driver. The fee Addressers require the 675 // archivist for key index storage and retrieval. 676 pgCfg := &pg.Config{ 677 Host: cfg.DBConf.Host, 678 Port: strconv.Itoa(int(cfg.DBConf.Port)), 679 User: cfg.DBConf.User, 680 Pass: cfg.DBConf.Pass, 681 DBName: cfg.DBConf.DBName, 682 ShowPGConfig: cfg.DBConf.ShowPGConfig, 683 QueryTimeout: 20 * time.Minute, 684 MarketCfg: cfg.Markets, 685 } 686 // After DEX construction, the storage subsystem should be stopped 687 // gracefully with its Close method, and in coordination with other 688 // subsystems via Stop. To abort its setup, rig a temporary link to the 689 // caller's Context. 690 running := make(chan struct{}) 691 defer close(running) // break the link 692 go func() { 693 select { 694 case <-ctx.Done(): // cancelled construction 695 cancelDB() 696 case <-running: // DB shutdown now only via dex.Stop=>db.Close 697 } 698 }() 699 storage, err := db.Open(ctxDB, "pg", pgCfg) 700 if err != nil { 701 return nil, fmt.Errorf("db.Open: %w", err) 702 } 703 704 relayAddrs := make(map[string]string, len(nodeRelayIDs)) 705 if len(nodeRelayIDs) > 0 { 706 nexusPort := "17537" 707 switch cfg.Network { 708 case dex.Testnet: 709 nexusPort = "17538" 710 case dex.Simnet: 711 nexusPort = "17539" 712 } 713 relayDir := filepath.Join(cfg.DataDir, "noderelay") 714 relay, err := noderelay.NewNexus(&noderelay.NexusConfig{ 715 ExternalAddr: cfg.NodeRelayAddr, 716 Dir: relayDir, 717 Port: nexusPort, 718 Logger: cfg.LogBackend.NewLogger("NR", log.Level()), 719 RelayIDs: nodeRelayIDs, 720 }) 721 if err != nil { 722 return nil, fmt.Errorf("error creating node relay: %w", err) 723 } 724 if err := startSubSys("Node relay", relay); err != nil { 725 return nil, fmt.Errorf("error starting node relay: %w", err) 726 } 727 select { 728 case <-relay.WaitForSourceNodes(): 729 case <-ctx.Done(): 730 return nil, ctx.Err() 731 } 732 for _, relayID := range nodeRelayIDs { 733 if relayAddrs[relayID], err = relay.RelayAddr(relayID); err != nil { 734 return nil, fmt.Errorf("error getting relay address for ID %s: %w", relayID, err) 735 } 736 } 737 } 738 739 // Create a MasterCoinLocker for each asset. 740 dexCoinLocker := coinlock.NewDEXCoinLocker(assetIDs) 741 742 // Prepare bonders. 743 bondAssets := make(map[string]*msgjson.BondAsset) 744 bonders := make(map[uint32]Bonder) 745 746 // Start asset backends. 747 lockableAssets := make(map[uint32]*swap.SwapperAsset, len(cfg.Assets)) 748 backedAssets := make(map[uint32]*asset.BackedAsset, len(cfg.Assets)) 749 cfgAssets := make([]*msgjson.Asset, 0, len(cfg.Assets)) 750 assetLogger := cfg.LogBackend.Logger("ASSET") 751 txDataSources := make(map[uint32]auth.TxDataSource) 752 feeMgr := NewFeeManager() 753 addAsset := func(assetID uint32, assetConf *Asset) error { 754 symbol := strings.ToLower(assetConf.Symbol) 755 756 assetVer, err := asset.Version(assetID) 757 if err != nil { 758 return fmt.Errorf("failed to retrieve asset %q version: %w", symbol, err) 759 } 760 761 // Create a new asset backend. An asset driver with a name matching the 762 // asset symbol must be available. 763 log.Infof("Starting asset backend %q...", symbol) 764 logger := assetLogger.SubLogger(symbol) 765 766 isToken, parentID := asset.IsToken(assetID) 767 var be asset.Backend 768 if isToken { 769 parent, found := backedAssets[parentID] 770 if !found { 771 return fmt.Errorf("attempting to load token asset %d before parent %d", assetID, parentID) 772 } 773 backer, is := parent.Backend.(asset.TokenBacker) 774 if !is { 775 return fmt.Errorf("token %d parent %d is not a TokenBacker", assetID, parentID) 776 } 777 be, err = backer.TokenBackend(assetID, assetConf.ConfigPath) 778 if err != nil { 779 return fmt.Errorf("failed to setup token %q: %w", symbol, err) 780 } 781 } else { 782 cfg := &asset.BackendConfig{ 783 AssetID: assetID, 784 ConfigPath: assetConf.ConfigPath, 785 Logger: logger, 786 Net: cfg.Network, 787 RelayAddr: relayAddrs[assetConf.NodeRelayID], 788 } 789 be, err = asset.Setup(cfg) 790 if err != nil { 791 return fmt.Errorf("failed to setup asset %q: %w", symbol, err) 792 } 793 } 794 795 err = startSubSys(fmt.Sprintf("Asset[%s]", symbol), be) 796 if err != nil { 797 return fmt.Errorf("failed to start asset %q: %w", symbol, err) 798 } 799 800 if assetConf.BondAmt > 0 && assetConf.BondConfs > 0 { 801 // Make sure we can check on fee transactions. 802 bc, ok := be.(Bonder) 803 if !ok { 804 return fmt.Errorf("asset %v is not a Bonder", symbol) 805 } 806 bondAssets[symbol] = &msgjson.BondAsset{ 807 Version: bc.BondVer(), 808 ID: assetID, 809 Amt: assetConf.BondAmt, 810 Confs: assetConf.BondConfs, 811 } 812 bonders[assetID] = bc 813 log.Infof("Bonds accepted using %s: amount %d, confs %d", 814 symbol, assetConf.BondAmt, assetConf.BondConfs) 815 } 816 817 unitInfo, err := asset.UnitInfo(assetID) 818 if err != nil { 819 return err 820 } 821 822 var coinLocker coinlock.CoinLocker 823 if _, isAccountRedeemer := be.(asset.AccountBalancer); isAccountRedeemer { 824 coinLocker = dexCoinLocker.AssetLocker(assetID).Swap() 825 } 826 827 ba := &asset.BackedAsset{ 828 Asset: dex.Asset{ 829 ID: assetID, 830 Symbol: symbol, 831 Version: assetVer, 832 MaxFeeRate: assetConf.MaxFeeRate, 833 SwapConf: assetConf.SwapConf, 834 UnitInfo: unitInfo, 835 }, 836 Backend: be, 837 } 838 839 backedAssets[assetID] = ba 840 lockableAssets[assetID] = &swap.SwapperAsset{ 841 BackedAsset: ba, 842 Locker: coinLocker, 843 } 844 feeMgr.AddFetcher(ba) 845 846 // Prepare assets portion of config response. 847 cfgAssets = append(cfgAssets, &msgjson.Asset{ 848 Symbol: assetConf.Symbol, 849 ID: assetID, 850 Version: assetVer, 851 MaxFeeRate: assetConf.MaxFeeRate, 852 SwapConf: uint16(assetConf.SwapConf), 853 UnitInfo: unitInfo, 854 }) 855 856 txDataSources[assetID] = be.TxData 857 return nil 858 } 859 860 // Add base chain assets before tokens. 861 tokens := make(map[uint32]*Asset) 862 863 for i, assetConf := range cfg.Assets { 864 assetID := assetIDs[i] 865 if isToken, _ := asset.IsToken(assetID); isToken { 866 tokens[assetID] = assetConf 867 continue 868 } 869 if err := addAsset(assetID, assetConf); err != nil { 870 return nil, err 871 } 872 } 873 874 for assetID, assetConf := range tokens { 875 if err := addAsset(assetID, assetConf); err != nil { 876 return nil, err 877 } 878 } 879 880 for _, mkt := range cfg.Markets { 881 mkt.Name = strings.ToLower(mkt.Name) 882 } 883 884 if err := ctx.Err(); err != nil { 885 return nil, err 886 } 887 888 // Create the user order unbook dispatcher for the AuthManager. 889 markets := make(map[string]*market.Market, len(cfg.Markets)) 890 userUnbookFun := func(user account.AccountID) { 891 for _, mkt := range markets { 892 mkt.UnbookUserOrders(user) 893 } 894 } 895 896 bondChecker := func(ctx context.Context, assetID uint32, version uint16, coinID []byte) (amt, lockTime, confs int64, 897 acct account.AccountID, err error) { 898 bc := bonders[assetID] 899 if bc == nil { 900 err = fmt.Errorf("unsupported bond asset") 901 return 902 } 903 return bc.BondCoin(ctx, version, coinID) 904 } 905 906 bondTxParser := func(assetID uint32, version uint16, rawTx []byte) (bondCoinID []byte, 907 amt, lockTime int64, acct account.AccountID, err error) { 908 bc := bonders[assetID] 909 if bc == nil { 910 err = fmt.Errorf("unsupported bond asset") 911 return 912 } 913 bondCoinID, amt, _, _, lockTime, acct, err = bc.ParseBondTx(version, rawTx) 914 return 915 } 916 917 if cfg.PenaltyThreshold == 0 { 918 cfg.PenaltyThreshold = auth.DefaultPenaltyThreshold 919 } 920 921 // Client comms RPC server. 922 server, err := comms.NewServer(cfg.CommsCfg) 923 if err != nil { 924 return nil, fmt.Errorf("NewServer failed: %w", err) 925 } 926 927 dataAPI := apidata.NewDataAPI(storage, server.RegisterHTTP) 928 929 authCfg := auth.Config{ 930 Storage: storage, 931 Signer: signer{cfg.DEXPrivKey}, 932 BondAssets: bondAssets, 933 BondTxParser: bondTxParser, 934 BondChecker: bondChecker, 935 BondExpiry: uint64(dex.BondExpiry(cfg.Network)), 936 UserUnbooker: userUnbookFun, 937 MiaUserTimeout: cfg.BroadcastTimeout, 938 CancelThreshold: cfg.CancelThreshold, 939 FreeCancels: cfg.FreeCancels, 940 PenaltyThreshold: cfg.PenaltyThreshold, 941 TxDataSources: txDataSources, 942 Route: server.Route, 943 } 944 945 authMgr := auth.NewAuthManager(&authCfg) 946 log.Infof("Cancellation rate threshold %f, new user grace period %d cancels", 947 cfg.CancelThreshold, authMgr.GraceLimit()) 948 log.Infof("MIA user order unbook timeout %v", cfg.BroadcastTimeout) 949 if authCfg.FreeCancels { 950 log.Infof("Cancellations are NOT COUNTED (the cancellation rate threshold is ignored).") 951 } 952 log.Infof("Penalty threshold is %v", cfg.PenaltyThreshold) 953 954 // Create a swapDone dispatcher for the Swapper. 955 swapDone := func(ord order.Order, match *order.Match, fail bool) { 956 name, err := dex.MarketName(ord.Base(), ord.Quote()) 957 if err != nil { 958 log.Errorf("bad market for order %v: %v", ord.ID(), err) 959 return 960 } 961 markets[name].SwapDone(ord, match, fail) 962 } 963 964 // Create the swapper. 965 swapperCfg := &swap.Config{ 966 Assets: lockableAssets, 967 Storage: storage, 968 AuthManager: authMgr, 969 BroadcastTimeout: cfg.BroadcastTimeout, 970 TxWaitExpiration: cfg.TxWaitExpiration, 971 LockTimeTaker: dex.LockTimeTaker(cfg.Network), 972 LockTimeMaker: dex.LockTimeMaker(cfg.Network), 973 SwapDone: swapDone, 974 NoResume: cfg.NoResumeSwaps, 975 // TODO: set the AllowPartialRestore bool to allow startup with a 976 // missing asset backend if necessary in an emergency. 977 } 978 979 swapper, err := swap.NewSwapper(swapperCfg) 980 if err != nil { 981 return nil, fmt.Errorf("NewSwapper: %w", err) 982 } 983 984 if err := ctx.Err(); err != nil { 985 return nil, err 986 } 987 988 // Because the dexBalancer relies on the marketTunnels map, and NewMarket 989 // checks necessary balances for account-based assets using the dexBalancer, 990 // that means that each market can only query orders for the markets that 991 // were initialized before it was, which is fine, but notable. The 992 // resulting behavior is that a user could have orders involving an 993 // account-based asset approved for re-booking on one market, but have 994 // orders rejected on a market involving the same asset created afterwards, 995 // since the later balance query is accounting for the earlier market. 996 // 997 // The current behavior is to reject all orders for the market if the 998 // account balance is too low to support them all, though an algorithm could 999 // be developed to do reject only some orders, based on available funding. 1000 // 1001 // This pattern is only safe because the markets are not Run until after 1002 // they are all instantiated, so we are synchronous in our use of the 1003 // marketTunnels map. 1004 marketTunnels := make(map[string]market.MarketTunnel, len(cfg.Markets)) 1005 pendingAccounters := make(map[string]market.PendingAccounter, len(cfg.Markets)) 1006 1007 dexBalancer, err := market.NewDEXBalancer(pendingAccounters, backedAssets, swapper) 1008 if err != nil { 1009 return nil, fmt.Errorf("NewDEXBalancer error: %w", err) 1010 } 1011 1012 // Markets 1013 var orderRouter *market.OrderRouter 1014 usersWithOrders := make(map[account.AccountID]struct{}) 1015 for _, mktInf := range cfg.Markets { 1016 // nilness of the coin locker signals account-based asset. 1017 var baseCoinLocker, quoteCoinLocker coinlock.CoinLocker 1018 b, q := backedAssets[mktInf.Base], backedAssets[mktInf.Quote] 1019 if _, ok := b.Backend.(asset.OutputTracker); ok { 1020 baseCoinLocker = dexCoinLocker.AssetLocker(mktInf.Base).Book() 1021 } 1022 if _, ok := q.Backend.(asset.OutputTracker); ok { 1023 quoteCoinLocker = dexCoinLocker.AssetLocker(mktInf.Quote).Book() 1024 } 1025 1026 // Calculate a minimum market rate that avoids dust. 1027 // quote_dust = base_lot * min_rate / rate_encoding_factor 1028 // => min_rate = quote_dust * rate_encoding_factor * base_lot 1029 quoteMinLotSize, _, _ := asset.Minimums(mktInf.Quote, q.Asset.MaxFeeRate) 1030 minRate := calc.MinimumMarketRate(mktInf.LotSize, quoteMinLotSize) 1031 1032 mkt, err := market.NewMarket(&market.Config{ 1033 MarketInfo: mktInf, 1034 Storage: storage, 1035 Swapper: swapper, 1036 AuthManager: authMgr, 1037 FeeFetcherBase: feeMgr.FeeFetcher(mktInf.Base), 1038 CoinLockerBase: baseCoinLocker, 1039 FeeFetcherQuote: feeMgr.FeeFetcher(mktInf.Quote), 1040 CoinLockerQuote: quoteCoinLocker, 1041 DataCollector: dataAPI, 1042 Balancer: dexBalancer, 1043 CheckParcelLimit: func(user account.AccountID, calcParcels market.MarketParcelCalculator) bool { 1044 return orderRouter.CheckParcelLimit(user, mktInf.Name, calcParcels) 1045 }, 1046 MinimumRate: minRate, 1047 }) 1048 if err != nil { 1049 return nil, fmt.Errorf("NewMarket failed: %w", err) 1050 } 1051 markets[mktInf.Name] = mkt 1052 marketTunnels[mktInf.Name] = mkt 1053 pendingAccounters[mktInf.Name] = mkt 1054 log.Infof("Preparing historical market data API for market %v...", mktInf.Name) 1055 err = dataAPI.AddMarketSource(mkt) 1056 if err != nil { 1057 return nil, fmt.Errorf("DataSource.AddMarketSource: %w", err) 1058 } 1059 1060 // Having loaded the book, get the accounts owning the orders. 1061 _, buys, sells := mkt.Book() 1062 for _, lo := range buys { 1063 usersWithOrders[lo.AccountID] = struct{}{} 1064 } 1065 for _, lo := range sells { 1066 usersWithOrders[lo.AccountID] = struct{}{} 1067 } 1068 } 1069 1070 // Having enumerated all users with booked orders, configure the AuthManager 1071 // to expect them to connect in a certain time period. 1072 authMgr.ExpectUsers(usersWithOrders, cfg.BroadcastTimeout) 1073 1074 // Start the AuthManager and Swapper subsystems after populating the markets 1075 // map used by the unbook callbacks, and setting the AuthManager's unbook 1076 // timers for the users with currently booked orders. 1077 startSubSys("Auth manager", authMgr) 1078 startSubSys("Swapper", swapper) 1079 1080 // Set start epoch index for each market. Also create BookSources for the 1081 // BookRouter, and MarketTunnels for the OrderRouter. 1082 now := time.Now().UnixMilli() 1083 bookSources := make(map[string]market.BookSource, len(cfg.Markets)) 1084 cfgMarkets := make([]*msgjson.Market, 0, len(cfg.Markets)) 1085 for name, mkt := range markets { 1086 startEpochIdx := 1 + now/int64(mkt.EpochDuration()) 1087 mkt.SetStartEpochIdx(startEpochIdx) 1088 bookSources[name] = mkt 1089 cfgMarkets = append(cfgMarkets, &msgjson.Market{ 1090 Name: name, 1091 Base: mkt.Base(), 1092 Quote: mkt.Quote(), 1093 LotSize: mkt.LotSize(), 1094 RateStep: mkt.RateStep(), 1095 EpochLen: mkt.EpochDuration(), 1096 MarketBuyBuffer: mkt.MarketBuyBuffer(), 1097 ParcelSize: mkt.ParcelSize(), 1098 MarketStatus: msgjson.MarketStatus{ 1099 StartEpoch: uint64(startEpochIdx), 1100 }, 1101 }) 1102 } 1103 1104 // Book router 1105 bookRouter := market.NewBookRouter(bookSources, feeMgr, server.Route) 1106 startSubSys("BookRouter", bookRouter) 1107 1108 // The data API gets the order book from the book router. 1109 dataAPI.SetBookSource(bookRouter) 1110 1111 // Market, now that book router is running. 1112 for name, mkt := range markets { 1113 startSubSys(marketSubSysName(name), mkt) 1114 } 1115 1116 // Order router 1117 orderRouter = market.NewOrderRouter(&market.OrderRouterConfig{ 1118 Assets: backedAssets, 1119 AuthManager: authMgr, 1120 Markets: marketTunnels, 1121 FeeSource: feeMgr, 1122 DEXBalancer: dexBalancer, 1123 MatchSwapper: swapper, 1124 }) 1125 startSubSys("OrderRouter", orderRouter) 1126 1127 if err := ctx.Err(); err != nil { 1128 return nil, err 1129 } 1130 1131 cfgResp, err := newConfigResponse(cfg, bondAssets, cfgAssets, cfgMarkets) 1132 if err != nil { 1133 return nil, err 1134 } 1135 1136 dexMgr := &DEX{ 1137 network: cfg.Network, 1138 markets: markets, 1139 assets: lockableAssets, 1140 swapper: swapper, 1141 authMgr: authMgr, 1142 storage: storage, 1143 orderRouter: orderRouter, 1144 bookRouter: bookRouter, 1145 subsystems: subsystems, 1146 server: server, 1147 configResp: cfgResp, 1148 } 1149 1150 server.RegisterHTTP(msgjson.ConfigRoute, dexMgr.handleDEXConfig) 1151 server.RegisterHTTP(msgjson.HealthRoute, dexMgr.handleHealthFlag) 1152 1153 mux := server.Mux() 1154 1155 // Data API endpoints. 1156 mux.Route("/api", func(rr chi.Router) { 1157 if log.Level() == dex.LevelTrace { 1158 rr.Use(middleware.Logger) 1159 } 1160 rr.Use(server.LimitRate) 1161 rr.Get("/config", server.NewRouteHandler(msgjson.ConfigRoute)) 1162 rr.Get("/healthy", server.NewRouteHandler(msgjson.HealthRoute)) 1163 rr.Get("/spots", server.NewRouteHandler(msgjson.SpotsRoute)) 1164 rr.With(candleParamsParser).Get("/candles/{baseSymbol}/{quoteSymbol}/{binSize}", server.NewRouteHandler(msgjson.CandlesRoute)) 1165 rr.With(candleParamsParser).Get("/candles/{baseSymbol}/{quoteSymbol}/{binSize}/{count}", server.NewRouteHandler(msgjson.CandlesRoute)) 1166 rr.With(orderBookParamsParser).Get("/orderbook/{baseSymbol}/{quoteSymbol}", server.NewRouteHandler(msgjson.OrderBookRoute)) 1167 }) 1168 1169 startSubSys("Comms Server", server) 1170 1171 ready = true // don't shut down on return 1172 1173 return dexMgr, nil 1174 } 1175 1176 // Asset retrieves an asset backend by its ID. 1177 func (dm *DEX) Asset(id uint32) (*asset.BackedAsset, error) { 1178 asset, found := dm.assets[id] 1179 if !found { 1180 return nil, fmt.Errorf("no backend for asset %d", id) 1181 } 1182 return asset.BackedAsset, nil 1183 } 1184 1185 // SetFeeRateScale specifies a scale factor that the Swapper should use to scale 1186 // the optimal fee rates for new swaps for for the specified asset. That is, 1187 // values above 1 increase the fee rate, while values below 1 decrease it. 1188 func (dm *DEX) SetFeeRateScale(assetID uint32, scale float64) { 1189 for _, mkt := range dm.markets { 1190 if mkt.Base() == assetID || mkt.Quote() == assetID { 1191 mkt.SetFeeRateScale(assetID, scale) 1192 } 1193 } 1194 } 1195 1196 // ScaleFeeRate scales the provided fee rate with the given asset's swap fee 1197 // rate scale factor, which is 1.0 by default. 1198 func (dm *DEX) ScaleFeeRate(assetID uint32, rate uint64) uint64 { 1199 // Any market will have the rate. Just find the first one. 1200 for _, mkt := range dm.markets { 1201 if mkt.Base() == assetID || mkt.Quote() == assetID { 1202 return mkt.ScaleFeeRate(assetID, rate) 1203 } 1204 } 1205 return rate 1206 } 1207 1208 // ConfigMsg returns the current dex configuration, marshalled to JSON. 1209 func (dm *DEX) ConfigMsg() json.RawMessage { 1210 dm.configRespMtx.RLock() 1211 defer dm.configRespMtx.RUnlock() 1212 return dm.configResp.configEnc 1213 } 1214 1215 // TODO: for just market running status, the DEX manager should use its 1216 // knowledge of Market subsystem state. 1217 func (dm *DEX) MarketRunning(mktName string) (found, running bool) { 1218 mkt := dm.markets[mktName] 1219 if mkt == nil { 1220 return 1221 } 1222 return true, mkt.Running() 1223 } 1224 1225 // MarketStatus returns the market.Status for the named market. If the market is 1226 // unknown to the DEX, nil is returned. 1227 func (dm *DEX) MarketStatus(mktName string) *market.Status { 1228 mkt := dm.markets[mktName] 1229 if mkt == nil { 1230 return nil 1231 } 1232 return mkt.Status() 1233 } 1234 1235 // MarketStatuses returns a map of market names to market.Status for all known 1236 // markets. 1237 func (dm *DEX) MarketStatuses() map[string]*market.Status { 1238 statuses := make(map[string]*market.Status, len(dm.markets)) 1239 for name, mkt := range dm.markets { 1240 statuses[name] = mkt.Status() 1241 } 1242 return statuses 1243 } 1244 1245 // SuspendMarket schedules a suspension of a given market, with the option to 1246 // persist the orders on the book (or purge the book automatically on market 1247 // shutdown). The scheduled final epoch and suspend time are returned. This is a 1248 // passthrough to the OrderRouter. A TradeSuspension notification is broadcasted 1249 // to all connected clients. 1250 func (dm *DEX) SuspendMarket(name string, tSusp time.Time, persistBooks bool) (suspEpoch *market.SuspendEpoch, err error) { 1251 name = strings.ToLower(name) 1252 1253 // Locate the (running) subsystem for this market. 1254 i := dm.findSubsys(marketSubSysName(name)) 1255 if i == -1 { 1256 err = fmt.Errorf("market subsystem %s not found", name) 1257 return 1258 } 1259 if !dm.subsystems[i].ssw.On() { 1260 err = fmt.Errorf("market subsystem %s is not running", name) 1261 return 1262 } 1263 1264 // Go through the order router since OrderRouter is likely to have market 1265 // status tracking built into it to facilitate resume. 1266 suspEpoch = dm.orderRouter.SuspendMarket(name, tSusp, persistBooks) 1267 if suspEpoch == nil { 1268 err = fmt.Errorf("unable to locate market %s", name) 1269 return 1270 } 1271 1272 // Update config message with suspend schedule. 1273 dm.configRespMtx.Lock() 1274 dm.configResp.setMktSuspend(name, uint64(suspEpoch.Idx), persistBooks) 1275 dm.configRespMtx.Unlock() 1276 1277 // Broadcast a TradeSuspension notification to all connected clients. 1278 note, errMsg := msgjson.NewNotification(msgjson.SuspensionRoute, msgjson.TradeSuspension{ 1279 MarketID: name, 1280 FinalEpoch: uint64(suspEpoch.Idx), 1281 SuspendTime: uint64(suspEpoch.End.UnixMilli()), 1282 Persist: persistBooks, 1283 }) 1284 if errMsg != nil { 1285 log.Errorf("Failed to create suspend notification: %v", errMsg) 1286 // Notification or not, the market is resuming, so do not return error. 1287 } else { 1288 dm.server.Broadcast(note) 1289 } 1290 return 1291 } 1292 1293 func (dm *DEX) findSubsys(name string) int { 1294 for i := range dm.subsystems { 1295 if dm.subsystems[i].name == name { 1296 return i 1297 } 1298 } 1299 return -1 1300 } 1301 1302 // ResumeMarket launches a stopped market subsystem as early as the given time. 1303 // The actual time the market will resume depends on the configure epoch 1304 // duration, as the market only starts at the beginning of an epoch. 1305 func (dm *DEX) ResumeMarket(name string, asSoonAs time.Time) (startEpoch int64, startTime time.Time, err error) { 1306 name = strings.ToLower(name) 1307 mkt := dm.markets[name] 1308 if mkt == nil { 1309 err = fmt.Errorf("unknown market %s", name) 1310 return 1311 } 1312 1313 // Get the next available start epoch given the earliest allowed time. 1314 // Requires the market to be stopped already. 1315 startEpoch = mkt.ResumeEpoch(asSoonAs) 1316 if startEpoch == 0 { 1317 err = fmt.Errorf("unable to resume market %s at time %v", name, asSoonAs) 1318 return 1319 } 1320 1321 // Locate the (stopped) subsystem for this market. 1322 i := dm.findSubsys(marketSubSysName(name)) 1323 if i == -1 { 1324 err = fmt.Errorf("market subsystem %s not found", name) 1325 return 1326 } 1327 if dm.subsystems[i].ssw.On() { 1328 err = fmt.Errorf("market subsystem %s not stopped", name) 1329 return 1330 } 1331 1332 // Update config message with resume schedule. 1333 dm.configRespMtx.Lock() 1334 epochLen := dm.configResp.setMktResume(name, uint64(startEpoch)) 1335 dm.configRespMtx.Unlock() 1336 if epochLen == 0 { 1337 return // couldn't set the new start epoch 1338 } 1339 1340 // Configure the start epoch with the Market. 1341 startTimeMS := int64(epochLen) * startEpoch 1342 startTime = time.UnixMilli(startTimeMS) 1343 mkt.SetStartEpochIdx(startEpoch) 1344 1345 // Relaunch the market. 1346 ssw := dex.NewStartStopWaiter(mkt) 1347 dm.subsystems[i].ssw = ssw 1348 ssw.Start(context.Background()) 1349 1350 // Broadcast a TradeResumption notification to all connected clients. 1351 note, errMsg := msgjson.NewNotification(msgjson.ResumptionRoute, msgjson.TradeResumption{ 1352 MarketID: name, 1353 ResumeTime: uint64(startTimeMS), 1354 StartEpoch: uint64(startEpoch), 1355 }) 1356 if errMsg != nil { 1357 log.Errorf("Failed to create resume notification: %v", errMsg) 1358 // Notification or not, the market is resuming, so do not return error. 1359 } else { 1360 dm.server.Broadcast(note) 1361 } 1362 1363 return 1364 } 1365 1366 // AccountInfo returns data for an account. 1367 func (dm *DEX) AccountInfo(aid account.AccountID) (*db.Account, error) { 1368 // TODO: consider asking the auth manager for account info, including tier. 1369 // connected, tier := dm.authMgr.AcctStatus(aid) 1370 return dm.storage.AccountInfo(aid) 1371 } 1372 1373 // ForgiveMatchFail forgives a user for a specific match failure, potentially 1374 // allowing them to resume trading if their score becomes passing. 1375 func (dm *DEX) ForgiveMatchFail(aid account.AccountID, mid order.MatchID) (forgiven, unbanned bool, err error) { 1376 return dm.authMgr.ForgiveMatchFail(aid, mid) 1377 } 1378 1379 func (dm *DEX) CreatePrepaidBonds(n int, strength uint32, durSecs int64) ([][]byte, error) { 1380 return dm.authMgr.CreatePrepaidBonds(n, strength, durSecs) 1381 } 1382 1383 func (dm *DEX) AccountMatchOutcomesN(aid account.AccountID, n int) ([]*auth.MatchOutcome, error) { 1384 return dm.authMgr.AccountMatchOutcomesN(aid, n) 1385 } 1386 1387 func (dm *DEX) UserMatchFails(aid account.AccountID, n int) ([]*auth.MatchFail, error) { 1388 return dm.authMgr.UserMatchFails(aid, n) 1389 } 1390 1391 // Notify sends a text notification to a connected client. 1392 func (dm *DEX) Notify(acctID account.AccountID, msg *msgjson.Message) { 1393 dm.authMgr.Notify(acctID, msg) 1394 } 1395 1396 // NotifyAll sends a text notification to all connected clients. 1397 func (dm *DEX) NotifyAll(msg *msgjson.Message) { 1398 dm.server.Broadcast(msg) 1399 } 1400 1401 // BookOrders returns booked orders for market with base and quote. 1402 func (dm *DEX) BookOrders(base, quote uint32) ([]*order.LimitOrder, error) { 1403 return dm.storage.BookOrders(base, quote) 1404 } 1405 1406 // EpochOrders returns epoch orders for market with base and quote. 1407 func (dm *DEX) EpochOrders(base, quote uint32) ([]order.Order, error) { 1408 return dm.storage.EpochOrders(base, quote) 1409 } 1410 1411 // Healthy returns the health status of the DEX. This is true if 1412 // the storage does not report an error and the BTC backend is synced. 1413 func (dm *DEX) Healthy() bool { 1414 if dm.storage.LastErr() != nil { 1415 return false 1416 } 1417 if assetID, found := dex.BipSymbolID("btc"); found { 1418 if synced, _ := dm.assets[assetID].Backend.Synced(); !synced { 1419 return false 1420 } 1421 } 1422 return true 1423 } 1424 1425 // MatchData embeds db.MatchData with decoded swap transaction coin IDs. 1426 type MatchData struct { 1427 db.MatchData 1428 MakerSwap string 1429 TakerSwap string 1430 MakerRedeem string 1431 TakerRedeem string 1432 } 1433 1434 func convertMatchData(baseAsset, quoteAsset asset.Backend, md *db.MatchDataWithCoins) *MatchData { 1435 matchData := MatchData{ 1436 MatchData: md.MatchData, 1437 } 1438 // asset0 is the maker swap / taker redeem asset. 1439 // asset1 is the taker swap / maker redeem asset. 1440 // Maker selling means asset 0 is base; asset 1 is quote. 1441 asset0, asset1 := baseAsset, quoteAsset 1442 if md.TakerSell { 1443 asset0, asset1 = quoteAsset, baseAsset 1444 } 1445 if len(md.MakerSwapCoin) > 0 { 1446 coinStr, err := asset0.ValidateCoinID(md.MakerSwapCoin) 1447 if err != nil { 1448 log.Errorf("Unable to decode coin %x: %v", md.MakerSwapCoin, err) 1449 } 1450 matchData.MakerSwap = coinStr 1451 } 1452 if len(md.TakerSwapCoin) > 0 { 1453 coinStr, err := asset1.ValidateCoinID(md.TakerSwapCoin) 1454 if err != nil { 1455 log.Errorf("Unable to decode coin %x: %v", md.TakerSwapCoin, err) 1456 } 1457 matchData.TakerSwap = coinStr 1458 } 1459 if len(md.MakerRedeemCoin) > 0 { 1460 coinStr, err := asset0.ValidateCoinID(md.MakerRedeemCoin) 1461 if err != nil { 1462 log.Errorf("Unable to decode coin %x: %v", md.MakerRedeemCoin, err) 1463 } 1464 matchData.MakerRedeem = coinStr 1465 } 1466 if len(md.TakerRedeemCoin) > 0 { 1467 coinStr, err := asset1.ValidateCoinID(md.TakerRedeemCoin) 1468 if err != nil { 1469 log.Errorf("Unable to decode coin %x: %v", md.TakerRedeemCoin, err) 1470 } 1471 matchData.TakerRedeem = coinStr 1472 } 1473 1474 return &matchData 1475 } 1476 1477 // MarketMatchesStreaming streams all matches for market with base and quote. 1478 func (dm *DEX) MarketMatchesStreaming(base, quote uint32, includeInactive bool, N int64, f func(*MatchData) error) (int, error) { 1479 baseAsset := dm.assets[base] 1480 if baseAsset == nil { 1481 return 0, fmt.Errorf("asset %d not found", base) 1482 } 1483 quoteAsset := dm.assets[quote] 1484 if quoteAsset == nil { 1485 return 0, fmt.Errorf("asset %d not found", quote) 1486 } 1487 fDB := func(md *db.MatchDataWithCoins) error { 1488 matchData := convertMatchData(baseAsset.Backend, quoteAsset.Backend, md) 1489 return f(matchData) 1490 } 1491 return dm.storage.MarketMatchesStreaming(base, quote, includeInactive, N, fDB) 1492 } 1493 1494 // MarketMatches returns matches for market with base and quote. 1495 func (dm *DEX) MarketMatches(base, quote uint32) ([]*MatchData, error) { 1496 baseAsset := dm.assets[base] 1497 if baseAsset == nil { 1498 return nil, fmt.Errorf("asset %d not found", base) 1499 } 1500 quoteAsset := dm.assets[quote] 1501 if quoteAsset == nil { 1502 return nil, fmt.Errorf("asset %d not found", quote) 1503 } 1504 mds, err := dm.storage.MarketMatches(base, quote) 1505 if err != nil { 1506 return nil, err 1507 } 1508 1509 matchDatas := make([]*MatchData, 0, len(mds)) 1510 for _, md := range mds { 1511 matchData := convertMatchData(baseAsset.Backend, quoteAsset.Backend, md) 1512 matchDatas = append(matchDatas, matchData) 1513 } 1514 1515 return matchDatas, nil 1516 } 1517 1518 // EnableDataAPI can be called via admin API to enable or disable the HTTP data 1519 // API endpoints. 1520 func (dm *DEX) EnableDataAPI(yes bool) { 1521 dm.server.EnableDataAPI(yes) 1522 } 1523 1524 // candlesParamsParser is middleware for the /candles routes. Parses the 1525 // *msgjson.CandlesRequest from the URL parameters. 1526 func candleParamsParser(next http.Handler) http.Handler { 1527 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1528 baseID, quoteID, errMsg := parseBaseQuoteIDs(r) 1529 if errMsg != "" { 1530 http.Error(w, errMsg, http.StatusBadRequest) 1531 return 1532 } 1533 1534 // Ensure the bin size is a valid duration string. 1535 binSize := chi.URLParam(r, "binSize") 1536 _, err := time.ParseDuration(binSize) 1537 if err != nil { 1538 http.Error(w, "bin size unparseable", http.StatusBadRequest) 1539 return 1540 } 1541 1542 countStr := chi.URLParam(r, "count") 1543 count := 0 1544 if countStr != "" { 1545 count, err = strconv.Atoi(countStr) 1546 if err != nil { 1547 http.Error(w, "count unparseable", http.StatusBadRequest) 1548 return 1549 } 1550 } 1551 ctx := context.WithValue(r.Context(), comms.CtxThing, &msgjson.CandlesRequest{ 1552 BaseID: baseID, 1553 QuoteID: quoteID, 1554 BinSize: binSize, 1555 NumCandles: count, 1556 }) 1557 next.ServeHTTP(w, r.WithContext(ctx)) 1558 }) 1559 } 1560 1561 // orderBookParamsParser is middleware for the /orderbook route. Parses the 1562 // *msgjson.OrderBookSubscription from the URL parameters. 1563 func orderBookParamsParser(next http.Handler) http.Handler { 1564 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1565 baseID, quoteID, errMsg := parseBaseQuoteIDs(r) 1566 if errMsg != "" { 1567 http.Error(w, errMsg, http.StatusBadRequest) 1568 return 1569 } 1570 ctx := context.WithValue(r.Context(), comms.CtxThing, &msgjson.OrderBookSubscription{ 1571 Base: baseID, 1572 Quote: quoteID, 1573 }) 1574 next.ServeHTTP(w, r.WithContext(ctx)) 1575 }) 1576 } 1577 1578 // parseBaseQuoteIDs parses the "baseSymbol" and "quoteSymbol" URL parameters 1579 // from the request. 1580 func parseBaseQuoteIDs(r *http.Request) (baseID, quoteID uint32, errMsg string) { 1581 baseID, found := dex.BipSymbolID(chi.URLParam(r, "baseSymbol")) 1582 if !found { 1583 return 0, 0, "unknown base" 1584 } 1585 quoteID, found = dex.BipSymbolID(chi.URLParam(r, "quoteSymbol")) 1586 if !found { 1587 return 0, 0, "unknown quote" 1588 } 1589 return 1590 }