decred.org/dcrdex@v1.0.3/client/webserver/http.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 webserver
     5  
     6  import (
     7  	"encoding/csv"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"os"
    12  	"sort"
    13  	"strconv"
    14  	"strings"
    15  	"time"
    16  
    17  	"decred.org/dcrdex/client/asset"
    18  	"decred.org/dcrdex/client/core"
    19  	"decred.org/dcrdex/dex"
    20  	"decred.org/dcrdex/dex/order"
    21  	qrcode "github.com/skip2/go-qrcode"
    22  )
    23  
    24  const (
    25  	homeRoute        = "/"
    26  	registerRoute    = "/register"
    27  	initRoute        = "/init"
    28  	loginRoute       = "/login"
    29  	marketsRoute     = "/markets"
    30  	walletsRoute     = "/wallets"
    31  	walletLogRoute   = "/wallets/logfile"
    32  	settingsRoute    = "/settings"
    33  	ordersRoute      = "/orders"
    34  	exportOrderRoute = "/orders/export"
    35  	marketMakerRoute = "/mm"
    36  	mmSettingsRoute  = "/mmsettings"
    37  	mmArchivesRoute  = "/mmarchives"
    38  	mmLogsRoute      = "/mmlogs"
    39  )
    40  
    41  // sendTemplate processes the template and sends the result.
    42  func (s *WebServer) sendTemplate(w http.ResponseWriter, tmplID string, data any) {
    43  	html := s.html.Load().(*templates)
    44  	page, err := html.exec(tmplID, data)
    45  	if err != nil {
    46  		log.Errorf("template exec error for %s: %v", tmplID, err)
    47  		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
    48  		return
    49  	}
    50  	w.Header().Set("Content-Type", "text/html;charset=UTF-8")
    51  	w.WriteHeader(http.StatusOK)
    52  	io.WriteString(w, page)
    53  }
    54  
    55  // CommonArguments are common page arguments that must be supplied to every
    56  // page to populate the <title> and <header> elements.
    57  type CommonArguments struct {
    58  	UserInfo       *userInfo
    59  	Title          string
    60  	UseDEXBranding bool
    61  }
    62  
    63  // Create the CommonArguments for the request.
    64  func (s *WebServer) commonArgs(r *http.Request, title string) *CommonArguments {
    65  	return &CommonArguments{
    66  		UserInfo:       extractUserInfo(r),
    67  		Title:          title,
    68  		UseDEXBranding: s.useDEXBranding,
    69  	}
    70  }
    71  
    72  // handleHome is the handler for the '/' page request. It redirects the
    73  // requester to the wallets page.
    74  func (s *WebServer) handleHome(w http.ResponseWriter, r *http.Request) {
    75  	http.Redirect(w, r, walletsRoute, http.StatusSeeOther)
    76  }
    77  
    78  // handleLogin is the handler for the '/login' page request.
    79  func (s *WebServer) handleLogin(w http.ResponseWriter, r *http.Request) {
    80  	cArgs := s.commonArgs(r, "Login | Decred DEX")
    81  	if cArgs.UserInfo.Authed {
    82  		http.Redirect(w, r, walletsRoute, http.StatusSeeOther)
    83  		return
    84  	}
    85  	s.sendTemplate(w, "login", cArgs)
    86  }
    87  
    88  // registerTmplData is template data for the /register page.
    89  type registerTmplData struct {
    90  	CommonArguments
    91  	KnownExchanges []string
    92  	// Host is optional. If provided, the register page will not display the add
    93  	// dex form, instead this host will be pre-selected for registration.
    94  	Host        string
    95  	Initialized bool
    96  }
    97  
    98  // handleRegister is the handler for the '/register' page request.
    99  func (s *WebServer) handleRegister(w http.ResponseWriter, r *http.Request) {
   100  	common := s.commonArgs(r, "Register | Decred DEX")
   101  	host, _ := getHostCtx(r)
   102  	s.sendTemplate(w, "register", &registerTmplData{
   103  		CommonArguments: *common,
   104  		Host:            host,
   105  		KnownExchanges:  s.knownUnregisteredExchanges(s.core.Exchanges()),
   106  		Initialized:     s.core.IsInitialized(),
   107  	})
   108  }
   109  
   110  // knownUnregisteredExchanges returns all the known exchanges that
   111  // the user has not registered for.
   112  func (s *WebServer) knownUnregisteredExchanges(registeredExchanges map[string]*core.Exchange) []string {
   113  	certs := core.CertStore[s.core.Network()]
   114  	exchanges := make([]string, 0, len(certs))
   115  	for host := range certs {
   116  		xc := registeredExchanges[host]
   117  		if xc == nil || xc.Auth.TargetTier == 0 {
   118  			exchanges = append(exchanges, host)
   119  		}
   120  	}
   121  	for host, xc := range registeredExchanges {
   122  		if certs[host] != nil || xc.Auth.TargetTier > 0 {
   123  			continue
   124  		}
   125  		exchanges = append(exchanges, host)
   126  	}
   127  	return exchanges
   128  }
   129  
   130  // handleMarkets is the handler for the '/markets' page request.
   131  func (s *WebServer) handleMarkets(w http.ResponseWriter, r *http.Request) {
   132  	s.sendTemplate(w, "markets", s.commonArgs(r, "Markets | Decred DEX"))
   133  }
   134  
   135  // handleMarketMaking is the handler for the '/mm' page request.
   136  func (s *WebServer) handleMarketMaking(w http.ResponseWriter, r *http.Request) {
   137  	s.sendTemplate(w, "mm", s.commonArgs(r, "Market Making | Decred DEX"))
   138  }
   139  
   140  // handleWallets is the handler for the '/wallets' page request.
   141  func (s *WebServer) handleWallets(w http.ResponseWriter, r *http.Request) {
   142  	assetMap := s.core.SupportedAssets()
   143  	// Sort assets by 1. wallet vs no wallet, and 2) alphabetically.
   144  	assets := make([]*core.SupportedAsset, 0, len(assetMap))
   145  	// over-allocating, but assuming user will not have set up most wallets.
   146  	nowallets := make([]*core.SupportedAsset, 0, len(assetMap))
   147  	for _, asset := range assetMap {
   148  		if asset.Wallet == nil {
   149  			nowallets = append(nowallets, asset)
   150  		} else {
   151  			assets = append(assets, asset)
   152  		}
   153  	}
   154  
   155  	sort.Slice(assets, func(i, j int) bool {
   156  		return assets[i].Name < assets[j].Name
   157  	})
   158  	sort.Slice(nowallets, func(i, j int) bool {
   159  		return nowallets[i].Name < nowallets[j].Name
   160  	})
   161  	s.sendTemplate(w, "wallets", s.commonArgs(r, "Wallets | Decred DEX"))
   162  }
   163  
   164  // handleWalletLogFile is the handler for the '/wallets/logfile' page request.
   165  func (s *WebServer) handleWalletLogFile(w http.ResponseWriter, r *http.Request) {
   166  	err := r.ParseForm()
   167  	if err != nil {
   168  		log.Errorf("error parsing form for wallet log file: %v", err)
   169  		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   170  		return
   171  	}
   172  
   173  	assetIDQueryString := r.Form["assetid"]
   174  	if len(assetIDQueryString) != 1 || len(assetIDQueryString[0]) == 0 {
   175  		log.Error("could not find asset id in query string")
   176  		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   177  		return
   178  	}
   179  
   180  	assetID, err := strconv.ParseUint(assetIDQueryString[0], 10, 32)
   181  	if err != nil {
   182  		log.Errorf("failed to parse asset id query string %v", err)
   183  		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   184  		return
   185  	}
   186  
   187  	logFilePath, err := s.core.WalletLogFilePath(uint32(assetID))
   188  	if err != nil {
   189  		log.Errorf("failed to get log file path %v", err)
   190  		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   191  		return
   192  	}
   193  
   194  	logFile, err := os.Open(logFilePath)
   195  	if err != nil {
   196  		log.Errorf("error opening log file: %v", err)
   197  		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   198  		return
   199  	}
   200  	defer logFile.Close()
   201  
   202  	assetName := dex.BipIDSymbol(uint32(assetID))
   203  	logFileName := fmt.Sprintf("dcrdex-%s-wallet.log", assetName)
   204  	w.Header().Set("Content-Disposition", "attachment; filename="+logFileName)
   205  	w.Header().Set("Content-Type", "text/plain")
   206  	w.WriteHeader(http.StatusOK)
   207  
   208  	_, err = io.Copy(w, logFile)
   209  	if err != nil {
   210  		log.Errorf("error copying log file: %v", err)
   211  	}
   212  }
   213  
   214  // handleGenerateQRCode is the handler for the '/generateqrcode' page request
   215  func (s *WebServer) handleGenerateQRCode(w http.ResponseWriter, r *http.Request) {
   216  	err := r.ParseForm()
   217  	if err != nil {
   218  		log.Errorf("error parsing form for generate qr code: %v", err)
   219  		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   220  		return
   221  	}
   222  
   223  	address := r.Form["address"]
   224  	if len(address) != 1 || len(address[0]) == 0 {
   225  		log.Error("form for generating qr code does not contain address")
   226  		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   227  		return
   228  	}
   229  
   230  	png, err := qrcode.Encode(address[0], qrcode.Medium, 200)
   231  	if err != nil {
   232  		log.Error("error generating qr code: %v", err)
   233  		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   234  		return
   235  	}
   236  
   237  	w.Header().Set("Content-Type", "image/png")
   238  	w.Header().Set("Content-Length", strconv.Itoa(len(png)))
   239  	w.WriteHeader(http.StatusOK)
   240  
   241  	_, err = w.Write(png)
   242  	if err != nil {
   243  		log.Errorf("error writing qr code image: %v", err)
   244  	}
   245  }
   246  
   247  // handleInit is the handler for the '/init' page request
   248  func (s *WebServer) handleInit(w http.ResponseWriter, r *http.Request) {
   249  	s.sendTemplate(w, "init", s.commonArgs(r, "Welcome | Decred DEX"))
   250  }
   251  
   252  // handleSettings is the handler for the '/settings' page request.
   253  func (s *WebServer) handleSettings(w http.ResponseWriter, r *http.Request) {
   254  	common := s.commonArgs(r, "Settings | Decred DEX")
   255  	xcs := s.core.Exchanges()
   256  	data := &struct {
   257  		CommonArguments
   258  		KnownExchanges  []string
   259  		FiatRateSources map[string]bool
   260  		FiatCurrency    string
   261  		Exchanges       map[string]*core.Exchange
   262  		IsInitialized   bool
   263  	}{
   264  		CommonArguments: *common,
   265  		KnownExchanges:  s.knownUnregisteredExchanges(xcs),
   266  		FiatCurrency:    core.DefaultFiatCurrency,
   267  		FiatRateSources: s.core.FiatRateSources(),
   268  		Exchanges:       xcs,
   269  		IsInitialized:   s.core.IsInitialized(),
   270  	}
   271  	s.sendTemplate(w, "settings", data)
   272  }
   273  
   274  // handleDexSettings is the handler for the '/dexsettings' page request.
   275  func (s *WebServer) handleDexSettings(w http.ResponseWriter, r *http.Request) {
   276  	host, err := getHostCtx(r)
   277  	if err != nil {
   278  		log.Errorf("error getting host ctx: %v", err)
   279  		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   280  		return
   281  	}
   282  
   283  	exchange, err := s.core.Exchange(host)
   284  	if err != nil {
   285  		log.Errorf("error getting exchange: %v", err)
   286  		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   287  		return
   288  	}
   289  
   290  	common := *s.commonArgs(r, fmt.Sprintf("%v Settings | Decred DEX", host))
   291  	data := &struct {
   292  		CommonArguments
   293  		Exchange       *core.Exchange
   294  		KnownExchanges []string
   295  	}{
   296  		CommonArguments: common,
   297  		Exchange:        exchange,
   298  		KnownExchanges:  s.knownUnregisteredExchanges(s.core.Exchanges()),
   299  	}
   300  
   301  	s.sendTemplate(w, "dexsettings", data)
   302  }
   303  
   304  // handleMMSettings is the handler for the '/mmsettings' page request.
   305  func (s *WebServer) handleMMSettings(w http.ResponseWriter, r *http.Request) {
   306  	common := *s.commonArgs(r, "Market Making Settings | Decred DEX")
   307  	s.sendTemplate(w, "mmsettings", common)
   308  }
   309  
   310  // handleMMArchives is the handler for the '/mmarchives' page request.
   311  func (s *WebServer) handleMMArchives(w http.ResponseWriter, r *http.Request) {
   312  	common := *s.commonArgs(r, "Market Making Archives | Decred DEX")
   313  	s.sendTemplate(w, "mmarchives", common)
   314  }
   315  
   316  // handleMMLogs is the handler for the '/mmlogs' page request.
   317  func (s *WebServer) handleMMLogs(w http.ResponseWriter, r *http.Request) {
   318  	common := *s.commonArgs(r, "Market Making Logs | Decred DEX")
   319  	s.sendTemplate(w, "mmlogs", common)
   320  }
   321  
   322  type ordersTmplData struct {
   323  	CommonArguments
   324  	Assets   map[uint32]*core.SupportedAsset
   325  	Hosts    []string
   326  	Statuses map[uint8]string
   327  }
   328  
   329  var allStatuses = map[uint8]string{
   330  	uint8(order.OrderStatusEpoch):    order.OrderStatusEpoch.String(),
   331  	uint8(order.OrderStatusBooked):   order.OrderStatusBooked.String(),
   332  	uint8(order.OrderStatusExecuted): order.OrderStatusExecuted.String(),
   333  	uint8(order.OrderStatusCanceled): order.OrderStatusCanceled.String(),
   334  	uint8(order.OrderStatusRevoked):  order.OrderStatusRevoked.String(),
   335  }
   336  
   337  // handleOrders is the handler for the /orders page request.
   338  func (s *WebServer) handleOrders(w http.ResponseWriter, r *http.Request) {
   339  	xcs := s.core.Exchanges()
   340  	hosts := make([]string, 0, len(xcs))
   341  	for _, xc := range xcs {
   342  		hosts = append(hosts, xc.Host)
   343  	}
   344  
   345  	s.sendTemplate(w, "orders", &ordersTmplData{
   346  		CommonArguments: *s.commonArgs(r, "Orders | Decred DEX"),
   347  		Assets:          s.core.SupportedAssets(),
   348  		Hosts:           hosts,
   349  		Statuses:        allStatuses,
   350  	})
   351  }
   352  
   353  // handleExportOrders is the handler for the /orders/export page request.
   354  func (s *WebServer) handleExportOrders(w http.ResponseWriter, r *http.Request) {
   355  	filter := new(core.OrderFilter)
   356  	err := r.ParseForm()
   357  	if err != nil {
   358  		log.Errorf("error parsing form for export order: %v", err)
   359  		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   360  		return
   361  	}
   362  
   363  	filter.Hosts = r.Form["hosts"]
   364  	assets := r.Form["assets"]
   365  	filter.Assets = make([]uint32, len(assets))
   366  	for k, assetStrID := range assets {
   367  		assetNumID, err := strconv.ParseUint(assetStrID, 10, 32)
   368  		if err != nil {
   369  			log.Errorf("error parsing asset id: %v", err)
   370  			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   371  			return
   372  		}
   373  		filter.Assets[k] = uint32(assetNumID)
   374  	}
   375  	statuses := r.Form["statuses"]
   376  	filter.Statuses = make([]order.OrderStatus, len(statuses))
   377  	for k, statusStrID := range statuses {
   378  		statusNumID, err := strconv.ParseUint(statusStrID, 10, 16)
   379  		if err != nil {
   380  			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   381  			log.Errorf("error parsing status id: %v", err)
   382  			return
   383  		}
   384  		filter.Statuses[k] = order.OrderStatus(statusNumID)
   385  	}
   386  
   387  	ords, err := s.core.Orders(filter)
   388  	if err != nil {
   389  		log.Errorf("error retrieving order: %v", err)
   390  		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   391  		return
   392  	}
   393  
   394  	w.Header().Set("Content-Disposition", "attachment; filename=orders.csv")
   395  	w.Header().Set("Content-Type", "text/csv")
   396  	w.WriteHeader(http.StatusOK)
   397  	csvWriter := csv.NewWriter(w)
   398  	csvWriter.UseCRLF = strings.Contains(r.UserAgent(), "Windows")
   399  
   400  	err = csvWriter.Write([]string{
   401  		"Host",
   402  		"Base",
   403  		"Quote",
   404  		"Base Quantity",
   405  		"Order Rate",
   406  		"Actual Rate",
   407  		"Base Fees",
   408  		"Base Fees Asset",
   409  		"Quote Fees",
   410  		"Quote Fees Asset",
   411  		"Type",
   412  		"Side",
   413  		"Time in Force",
   414  		"Status",
   415  		"Filled (%)",
   416  		"Settled (%)",
   417  		"Time",
   418  	})
   419  	if err != nil {
   420  		log.Errorf("error writing CSV: %v", err)
   421  		return
   422  	}
   423  	csvWriter.Flush()
   424  	err = csvWriter.Error()
   425  	if err != nil {
   426  		log.Errorf("error writing CSV: %v", err)
   427  		return
   428  	}
   429  
   430  	for _, ord := range ords {
   431  		ordReader := s.orderReader(ord)
   432  
   433  		timestamp := time.UnixMilli(int64(ord.Stamp)).Local().Format(time.RFC3339Nano)
   434  		err = csvWriter.Write([]string{
   435  			ord.Host,                      // Host
   436  			ord.BaseSymbol,                // Base
   437  			ord.QuoteSymbol,               // Quote
   438  			ordReader.BaseQtyString(),     // Base Quantity
   439  			ordReader.SimpleRateString(),  // Order Rate
   440  			ordReader.AverageRateString(), // Actual Rate
   441  			ordReader.BaseAssetFees(),     // Base Fees
   442  			ordReader.BaseFeeSymbol(),     // Base Fees Asset
   443  			ordReader.QuoteAssetFees(),    // Quote Fees
   444  			ordReader.QuoteFeeSymbol(),    // Quote Fees Asset
   445  			ordReader.Type.String(),       // Type
   446  			ordReader.SideString(),        // Side
   447  			ord.TimeInForce.String(),      // Time in Force
   448  			ordReader.StatusString(),      // Status
   449  			ordReader.FilledPercent(),     // Filled
   450  			ordReader.SettledPercent(),    // Settled
   451  			timestamp,                     // Time
   452  		})
   453  		if err != nil {
   454  			log.Errorf("error writing CSV: %v", err)
   455  			return
   456  		}
   457  		csvWriter.Flush()
   458  		err = csvWriter.Error()
   459  		if err != nil {
   460  			log.Errorf("error writing CSV: %v", err)
   461  			return
   462  		}
   463  	}
   464  }
   465  
   466  type orderTmplData struct {
   467  	CommonArguments
   468  	Order *core.OrderReader
   469  }
   470  
   471  // handleOrder is the handler for the /order/{oid} page request.
   472  func (s *WebServer) handleOrder(w http.ResponseWriter, r *http.Request) {
   473  	oid, err := getOrderIDCtx(r)
   474  	if err != nil {
   475  		log.Errorf("error retrieving order ID from request context: %v", err)
   476  		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   477  		return
   478  	}
   479  	ord, err := s.core.Order(oid)
   480  	if err != nil {
   481  		log.Errorf("error retrieving order: %v", err)
   482  		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
   483  		return
   484  	}
   485  	s.sendTemplate(w, "order", &orderTmplData{
   486  		CommonArguments: *s.commonArgs(r, "Order | Decred DEX"),
   487  		Order:           s.orderReader(ord),
   488  	})
   489  }
   490  
   491  func defaultUnitInfo(symbol string) dex.UnitInfo {
   492  	return dex.UnitInfo{
   493  		AtomicUnit: "atoms",
   494  		Conventional: dex.Denomination{
   495  			ConversionFactor: 1e8,
   496  			Unit:             symbol,
   497  		},
   498  	}
   499  }
   500  
   501  func (s *WebServer) orderReader(ord *core.Order) *core.OrderReader {
   502  	unitInfo := func(assetID uint32, symbol string) dex.UnitInfo {
   503  		unitInfo, err := asset.UnitInfo(assetID)
   504  		if err == nil {
   505  			return unitInfo
   506  		}
   507  		xc := s.core.Exchanges()[ord.Host]
   508  		a, found := xc.Assets[assetID]
   509  		if !found || a.UnitInfo.Conventional.ConversionFactor == 0 {
   510  			return defaultUnitInfo(symbol)
   511  		}
   512  		return a.UnitInfo
   513  	}
   514  
   515  	feeAssetInfo := func(assetID uint32, symbol string) (string, dex.UnitInfo) {
   516  		if token := asset.TokenInfo(assetID); token != nil {
   517  			parentAsset := asset.Asset(token.ParentID)
   518  			return unbip(parentAsset.ID), parentAsset.Info.UnitInfo
   519  		}
   520  		return unbip(assetID), unitInfo(assetID, symbol)
   521  	}
   522  
   523  	baseFeeAssetSymbol, baseFeeUintInfo := feeAssetInfo(ord.BaseID, ord.BaseSymbol)
   524  	quoteFeeAssetSymbol, quoteFeeUnitInfo := feeAssetInfo(ord.QuoteID, ord.QuoteSymbol)
   525  
   526  	return &core.OrderReader{
   527  		Order:               ord,
   528  		BaseUnitInfo:        unitInfo(ord.BaseID, ord.BaseSymbol),
   529  		BaseFeeUnitInfo:     baseFeeUintInfo,
   530  		BaseFeeAssetSymbol:  baseFeeAssetSymbol,
   531  		QuoteUnitInfo:       unitInfo(ord.QuoteID, ord.QuoteSymbol),
   532  		QuoteFeeUnitInfo:    quoteFeeUnitInfo,
   533  		QuoteFeeAssetSymbol: quoteFeeAssetSymbol,
   534  	}
   535  }