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", ®isterTmplData{ 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 }