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  }