decred.org/dcrdex@v1.0.3/client/webserver/api.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/hex" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "net/http" 12 "time" 13 14 "decred.org/dcrdex/client/asset" 15 "decred.org/dcrdex/client/core" 16 "decred.org/dcrdex/client/db" 17 "decred.org/dcrdex/client/mm" 18 "decred.org/dcrdex/client/mm/libxc" 19 "decred.org/dcrdex/dex" 20 "decred.org/dcrdex/dex/config" 21 "decred.org/dcrdex/dex/encode" 22 ) 23 24 var zero = encode.ClearBytes 25 26 // apiAddDEX is the handler for the '/adddex' API request. 27 func (s *WebServer) apiAddDEX(w http.ResponseWriter, r *http.Request) { 28 form := new(addDexForm) 29 if !readPost(w, r, form) { 30 return 31 } 32 defer form.AppPW.Clear() 33 appPW, err := s.resolvePass(form.AppPW, r) 34 if err != nil { 35 s.writeAPIError(w, fmt.Errorf("password error: %w", err)) 36 return 37 } 38 cert := []byte(form.Cert) 39 40 if err = s.core.AddDEX(appPW, form.Addr, cert); err != nil { 41 s.writeAPIError(w, err) 42 return 43 } 44 writeJSON(w, simpleAck()) 45 } 46 47 // apiDiscoverAccount is the handler for the '/discoveracct' API request. 48 func (s *WebServer) apiDiscoverAccount(w http.ResponseWriter, r *http.Request) { 49 form := new(registrationForm) 50 defer form.Password.Clear() 51 if !readPost(w, r, form) { 52 return 53 } 54 cert := []byte(form.Cert) 55 pass, err := s.resolvePass(form.Password, r) 56 if err != nil { 57 s.writeAPIError(w, fmt.Errorf("password error: %w", err)) 58 return 59 } 60 defer zero(pass) 61 exchangeInfo, paid, err := s.core.DiscoverAccount(form.Addr, pass, cert) // TODO: update when paid return removed 62 if err != nil { 63 s.writeAPIError(w, err) 64 return 65 } 66 resp := struct { 67 OK bool `json:"ok"` 68 Exchange *core.Exchange `json:"xc,omitempty"` 69 Paid bool `json:"paid"` 70 }{ 71 OK: true, 72 Exchange: exchangeInfo, 73 Paid: paid, 74 } 75 writeJSON(w, resp) 76 } 77 78 // apiValidateAddress is the handlers for the '/validateaddress' API request. 79 func (s *WebServer) apiValidateAddress(w http.ResponseWriter, r *http.Request) { 80 form := &struct { 81 Addr string `json:"addr"` 82 AssetID *uint32 `json:"assetID"` 83 }{} 84 if !readPost(w, r, form) { 85 return 86 } 87 if form.AssetID == nil { 88 s.writeAPIError(w, errors.New("missing asset ID")) 89 return 90 } 91 valid, err := s.core.ValidateAddress(form.Addr, *form.AssetID) 92 if err != nil { 93 s.writeAPIError(w, err) 94 return 95 } 96 resp := struct { 97 OK bool `json:"ok"` 98 }{ 99 OK: valid, 100 } 101 writeJSON(w, resp) 102 } 103 104 // apiEstimateSendTxFee is the handler for the '/txfee' API request. 105 func (s *WebServer) apiEstimateSendTxFee(w http.ResponseWriter, r *http.Request) { 106 form := new(sendTxFeeForm) 107 if !readPost(w, r, form) { 108 return 109 } 110 if form.AssetID == nil { 111 s.writeAPIError(w, errors.New("missing asset ID")) 112 return 113 } 114 txFee, validAddress, err := s.core.EstimateSendTxFee(form.Addr, *form.AssetID, form.Value, form.Subtract, form.MaxWithdraw) 115 if err != nil { 116 s.writeAPIError(w, err) 117 return 118 } 119 resp := struct { 120 OK bool `json:"ok"` 121 TxFee uint64 `json:"txfee"` 122 ValidAddress bool `json:"validaddress"` 123 }{ 124 OK: true, 125 TxFee: txFee, 126 ValidAddress: validAddress, 127 } 128 writeJSON(w, resp) 129 } 130 131 // apiGetWalletPeers is the handler for the '/getwalletpeers' API request. 132 func (s *WebServer) apiGetWalletPeers(w http.ResponseWriter, r *http.Request) { 133 var form struct { 134 AssetID uint32 `json:"assetID"` 135 } 136 if !readPost(w, r, &form) { 137 return 138 } 139 peers, err := s.core.WalletPeers(form.AssetID) 140 if err != nil { 141 s.writeAPIError(w, err) 142 return 143 } 144 resp := struct { 145 OK bool `json:"ok"` 146 Peers []*asset.WalletPeer `json:"peers"` 147 }{ 148 OK: true, 149 Peers: peers, 150 } 151 writeJSON(w, resp) 152 } 153 154 // apiAddWalletPeer is the handler for the '/addwalletpeer' API request. 155 func (s *WebServer) apiAddWalletPeer(w http.ResponseWriter, r *http.Request) { 156 var form struct { 157 AssetID uint32 `json:"assetID"` 158 Address string `json:"addr"` 159 } 160 if !readPost(w, r, &form) { 161 return 162 } 163 err := s.core.AddWalletPeer(form.AssetID, form.Address) 164 if err != nil { 165 s.writeAPIError(w, err) 166 return 167 } 168 writeJSON(w, simpleAck()) 169 } 170 171 // apiRemoveWalletPeer is the handler for the '/removewalletpeer' API request. 172 func (s *WebServer) apiRemoveWalletPeer(w http.ResponseWriter, r *http.Request) { 173 var form struct { 174 AssetID uint32 `json:"assetID"` 175 Address string `json:"addr"` 176 } 177 if !readPost(w, r, &form) { 178 return 179 } 180 err := s.core.RemoveWalletPeer(form.AssetID, form.Address) 181 if err != nil { 182 s.writeAPIError(w, err) 183 return 184 } 185 writeJSON(w, simpleAck()) 186 } 187 188 func (s *WebServer) apiApproveTokenFee(w http.ResponseWriter, r *http.Request) { 189 var form struct { 190 AssetID uint32 `json:"assetID"` 191 Version uint32 `json:"version"` 192 Approval bool `json:"approval"` 193 } 194 if !readPost(w, r, &form) { 195 return 196 } 197 198 txFee, err := s.core.ApproveTokenFee(form.AssetID, form.Version, form.Approval) 199 if err != nil { 200 s.writeAPIError(w, err) 201 return 202 } 203 204 resp := struct { 205 OK bool `json:"ok"` 206 TxFee uint64 `json:"txFee"` 207 }{ 208 OK: true, 209 TxFee: txFee, 210 } 211 writeJSON(w, resp) 212 } 213 214 func (s *WebServer) apiApproveToken(w http.ResponseWriter, r *http.Request) { 215 var form struct { 216 AssetID uint32 `json:"assetID"` 217 DexAddr string `json:"dexAddr"` 218 Password encode.PassBytes `json:"pass"` 219 } 220 if !readPost(w, r, &form) { 221 return 222 } 223 pass, err := s.resolvePass(form.Password, r) 224 if err != nil { 225 s.writeAPIError(w, fmt.Errorf("password error: %w", err)) 226 return 227 } 228 defer zero(pass) 229 230 txID, err := s.core.ApproveToken(pass, form.AssetID, form.DexAddr, func() {}) 231 if err != nil { 232 s.writeAPIError(w, err) 233 return 234 } 235 resp := struct { 236 OK bool `json:"ok"` 237 TxID string `json:"txID"` 238 }{ 239 OK: true, 240 TxID: txID, 241 } 242 writeJSON(w, resp) 243 } 244 245 func (s *WebServer) apiUnapproveToken(w http.ResponseWriter, r *http.Request) { 246 var form struct { 247 AssetID uint32 `json:"assetID"` 248 Version uint32 `json:"version"` 249 Password encode.PassBytes `json:"pass"` 250 } 251 if !readPost(w, r, &form) { 252 return 253 } 254 pass, err := s.resolvePass(form.Password, r) 255 if err != nil { 256 s.writeAPIError(w, fmt.Errorf("password error: %w", err)) 257 return 258 } 259 defer zero(pass) 260 261 txID, err := s.core.UnapproveToken(pass, form.AssetID, form.Version) 262 if err != nil { 263 s.writeAPIError(w, err) 264 return 265 } 266 resp := struct { 267 OK bool `json:"ok"` 268 TxID string `json:"txID"` 269 }{ 270 OK: true, 271 TxID: txID, 272 } 273 writeJSON(w, resp) 274 } 275 276 // apiGetDEXInfo is the handler for the '/getdexinfo' API request. 277 func (s *WebServer) apiGetDEXInfo(w http.ResponseWriter, r *http.Request) { 278 form := new(registrationForm) 279 if !readPost(w, r, form) { 280 return 281 } 282 cert := []byte(form.Cert) 283 exchangeInfo, err := s.core.GetDEXConfig(form.Addr, cert) 284 if err != nil { 285 s.writeAPIError(w, err) 286 return 287 } 288 resp := struct { 289 OK bool `json:"ok"` 290 Exchange *core.Exchange `json:"xc,omitempty"` 291 }{ 292 OK: true, 293 Exchange: exchangeInfo, 294 } 295 writeJSON(w, resp) 296 } 297 298 // bondsFeeBuffer is a caching helper for the bonds fee buffer. Values for a 299 // given asset are cached for 45 minutes. These values are meant to provide a 300 // sensible but well-padded fee buffer for bond transactions now and well into 301 // the future, so a long expiry is appropriate. 302 func (s *WebServer) bondsFeeBuffer(assetID uint32) (feeBuffer uint64, err error) { 303 // (*Core).BondsFeeBuffer returns a fresh fee buffer based on a current (but 304 // padded) fee rate estimate. We assist the frontend by stabilizing this 305 // value for up to 45 minutes from the last request for a given asset. A web 306 // app could conceivably do the same, but we'll do this here between the 307 // backend (Core) and UI so that a webapp does not need to employ local 308 // storage/cookies and associated caching logic. 309 const expiry = 45 * time.Minute 310 s.bondBufMtx.Lock() 311 defer s.bondBufMtx.Unlock() 312 if buf, ok := s.bondBuf[assetID]; ok && time.Since(buf.stamp) < expiry { 313 feeBuffer = buf.val 314 log.Tracef("Using cached bond fee buffer (%v old): %d", 315 time.Since(buf.stamp), feeBuffer) 316 } else { 317 feeBuffer, err = s.core.BondsFeeBuffer(assetID) 318 if err != nil { 319 return 320 } 321 log.Tracef("Obtained fresh bond fee buffer: %d", feeBuffer) 322 s.bondBuf[assetID] = valStamp{feeBuffer, time.Now()} 323 } 324 return 325 } 326 327 // apiBondsFeeBuffer is the handler for the '/bondsfeebuffer' API request. 328 func (s *WebServer) apiBondsFeeBuffer(w http.ResponseWriter, r *http.Request) { 329 form := new(bondsFeeBufferForm) 330 if !readPost(w, r, form) { 331 return 332 } 333 feeBuffer, err := s.bondsFeeBuffer(form.AssetID) 334 if err != nil { 335 s.writeAPIError(w, err) 336 return 337 } 338 resp := struct { 339 OK bool `json:"ok"` 340 FeeBuffer uint64 `json:"feeBuffer"` 341 }{ 342 OK: true, 343 FeeBuffer: feeBuffer, 344 } 345 writeJSON(w, resp) 346 } 347 348 // apiPostBond is the handler for the '/postbond' API request. 349 func (s *WebServer) apiPostBond(w http.ResponseWriter, r *http.Request) { 350 post := new(postBondForm) 351 defer post.Password.Clear() 352 if !readPost(w, r, post) { 353 return 354 } 355 assetID := uint32(42) 356 if post.AssetID != nil { 357 assetID = *post.AssetID 358 } 359 wallet := s.core.WalletState(assetID) 360 if wallet == nil { 361 s.writeAPIError(w, errors.New("no wallet")) 362 return 363 } 364 pass, err := s.resolvePass(post.Password, r) 365 if err != nil { 366 s.writeAPIError(w, fmt.Errorf("password error: %w", err)) 367 return 368 } 369 defer zero(pass) 370 371 bondForm := &core.PostBondForm{ 372 Addr: post.Addr, 373 Cert: []byte(post.Cert), 374 AppPass: pass, 375 Bond: post.Bond, 376 Asset: &assetID, 377 LockTime: post.LockTime, 378 // Options valid only when creating an account with bond: 379 MaintainTier: post.Maintain, 380 MaxBondedAmt: post.MaxBondedAmt, 381 } 382 383 if post.FeeBuffer != nil { 384 bondForm.FeeBuffer = *post.FeeBuffer 385 } 386 387 _, err = s.core.PostBond(bondForm) 388 if err != nil { 389 s.writeAPIError(w, fmt.Errorf("add bond error: %w", err)) 390 return 391 } 392 // There was no error paying the fee, but we must wait on confirmations 393 // before informing the DEX of the fee payment. Those results will come 394 // through as a notification. 395 writeJSON(w, simpleAck()) 396 } 397 398 // apiUpdateBondOptions is the handler for the '/updatebondoptions' API request. 399 func (s *WebServer) apiUpdateBondOptions(w http.ResponseWriter, r *http.Request) { 400 form := new(core.BondOptionsForm) 401 if !readPost(w, r, form) { 402 return 403 } 404 405 err := s.core.UpdateBondOptions(form) 406 if err != nil { 407 s.writeAPIError(w, fmt.Errorf("update bond options error: %w", err)) 408 return 409 } 410 411 writeJSON(w, simpleAck()) 412 } 413 414 func (s *WebServer) apiRedeemPrepaidBond(w http.ResponseWriter, r *http.Request) { 415 var req struct { 416 Host string `json:"host"` 417 Code dex.Bytes `json:"code"` 418 AppPW encode.PassBytes `json:"appPW"` 419 Cert string `json:"cert"` 420 } 421 defer req.AppPW.Clear() 422 if !readPost(w, r, &req) { 423 return 424 } 425 appPW, err := s.resolvePass(req.AppPW, r) 426 if err != nil { 427 s.writeAPIError(w, fmt.Errorf("password error: %w", err)) 428 return 429 } 430 tier, err := s.core.RedeemPrepaidBond(appPW, req.Code, req.Host, req.Cert) 431 if err != nil { 432 s.writeAPIError(w, err) 433 return 434 } 435 resp := &struct { 436 OK bool `json:"ok"` 437 Tier uint64 `json:"tier"` 438 }{ 439 OK: true, 440 Tier: tier, 441 } 442 writeJSON(w, resp) 443 } 444 445 // apiNewWallet is the handler for the '/newwallet' API request. 446 func (s *WebServer) apiNewWallet(w http.ResponseWriter, r *http.Request) { 447 form := new(newWalletForm) 448 defer form.AppPW.Clear() 449 defer form.Pass.Clear() 450 if !readPost(w, r, form) { 451 return 452 } 453 has := s.core.WalletState(form.AssetID) != nil 454 if has { 455 s.writeAPIError(w, fmt.Errorf("already have a wallet for %s", unbip(form.AssetID))) 456 return 457 } 458 pass, err := s.resolvePass(form.AppPW, r) 459 if err != nil { 460 s.writeAPIError(w, fmt.Errorf("password error: %w", err)) 461 return 462 } 463 defer zero(pass) 464 var parentForm *core.WalletForm 465 if f := form.ParentForm; f != nil { 466 parentForm = &core.WalletForm{ 467 AssetID: f.AssetID, 468 Config: f.Config, 469 Type: f.WalletType, 470 } 471 } 472 // Wallet does not exist yet. Try to create it. 473 err = s.core.CreateWallet(pass, form.Pass, &core.WalletForm{ 474 AssetID: form.AssetID, 475 Type: form.WalletType, 476 Config: form.Config, 477 ParentForm: parentForm, 478 }) 479 if err != nil { 480 s.writeAPIError(w, fmt.Errorf("error creating %s wallet: %w", unbip(form.AssetID), err)) 481 return 482 } 483 484 writeJSON(w, simpleAck()) 485 } 486 487 // apiRecoverWallet is the handler for the '/recoverwallet' API request. Commands 488 // a recovery of the specified wallet. 489 func (s *WebServer) apiRecoverWallet(w http.ResponseWriter, r *http.Request) { 490 var form struct { 491 AppPW encode.PassBytes `json:"appPW"` 492 AssetID uint32 `json:"assetID"` 493 Force bool `json:"force"` 494 } 495 if !readPost(w, r, &form) { 496 return 497 } 498 appPW, err := s.resolvePass(form.AppPW, r) 499 if err != nil { 500 s.writeAPIError(w, fmt.Errorf("password error: %w", err)) 501 return 502 } 503 status := s.core.WalletState(form.AssetID) 504 if status == nil { 505 s.writeAPIError(w, fmt.Errorf("no wallet for %d -> %s", form.AssetID, unbip(form.AssetID))) 506 return 507 } 508 err = s.core.RecoverWallet(form.AssetID, appPW, form.Force) 509 if err != nil { 510 // NOTE: client may check for code activeOrdersErr to prompt for 511 // override the active orders safety check. 512 s.writeAPIError(w, fmt.Errorf("error recovering %s wallet: %w", unbip(form.AssetID), err)) 513 return 514 } 515 516 writeJSON(w, simpleAck()) 517 } 518 519 // apiRescanWallet is the handler for the '/rescanwallet' API request. Commands 520 // a rescan of the specified wallet. 521 func (s *WebServer) apiRescanWallet(w http.ResponseWriter, r *http.Request) { 522 var form struct { 523 AssetID uint32 `json:"assetID"` 524 Force bool `json:"force"` 525 } 526 if !readPost(w, r, &form) { 527 return 528 } 529 status := s.core.WalletState(form.AssetID) 530 if status == nil { 531 s.writeAPIError(w, fmt.Errorf("No wallet for %d -> %s", form.AssetID, unbip(form.AssetID))) 532 return 533 } 534 err := s.core.RescanWallet(form.AssetID, form.Force) 535 if err != nil { 536 s.writeAPIError(w, fmt.Errorf("error rescanning %s wallet: %w", unbip(form.AssetID), err)) 537 return 538 } 539 540 writeJSON(w, simpleAck()) 541 } 542 543 // apiOpenWallet is the handler for the '/openwallet' API request. Unlocks the 544 // specified wallet. 545 func (s *WebServer) apiOpenWallet(w http.ResponseWriter, r *http.Request) { 546 form := new(openWalletForm) 547 defer form.Pass.Clear() 548 if !readPost(w, r, form) { 549 return 550 } 551 status := s.core.WalletState(form.AssetID) 552 if status == nil { 553 s.writeAPIError(w, fmt.Errorf("No wallet for %d -> %s", form.AssetID, unbip(form.AssetID))) 554 return 555 } 556 pass, err := s.resolvePass(form.Pass, r) 557 if err != nil { 558 s.writeAPIError(w, fmt.Errorf("password error: %w", err)) 559 return 560 } 561 defer zero(pass) 562 err = s.core.OpenWallet(form.AssetID, pass) 563 if err != nil { 564 s.writeAPIError(w, fmt.Errorf("error unlocking %s wallet: %w", unbip(form.AssetID), err)) 565 return 566 } 567 568 writeJSON(w, simpleAck()) 569 } 570 571 // apiNewDepositAddress gets a new deposit address from a wallet. 572 func (s *WebServer) apiNewDepositAddress(w http.ResponseWriter, r *http.Request) { 573 form := &struct { 574 AssetID *uint32 `json:"assetID"` 575 }{} 576 if !readPost(w, r, form) { 577 return 578 } 579 if form.AssetID == nil { 580 s.writeAPIError(w, errors.New("missing asset ID")) 581 return 582 } 583 assetID := *form.AssetID 584 585 addr, err := s.core.NewDepositAddress(assetID) 586 if err != nil { 587 s.writeAPIError(w, fmt.Errorf("error connecting to %s wallet: %w", unbip(assetID), err)) 588 return 589 } 590 591 writeJSON(w, &struct { 592 OK bool `json:"ok"` 593 Address string `json:"address"` 594 }{ 595 OK: true, 596 Address: addr, 597 }) 598 } 599 600 // apiConnectWallet is the handler for the '/connectwallet' API request. 601 // Connects to a specified wallet, but does not unlock it. 602 func (s *WebServer) apiConnectWallet(w http.ResponseWriter, r *http.Request) { 603 form := &struct { 604 AssetID uint32 `json:"assetID"` 605 }{} 606 if !readPost(w, r, form) { 607 return 608 } 609 err := s.core.ConnectWallet(form.AssetID) 610 if err != nil { 611 s.writeAPIError(w, fmt.Errorf("error connecting to %s wallet: %w", unbip(form.AssetID), err)) 612 return 613 } 614 615 writeJSON(w, simpleAck()) 616 } 617 618 // apiTrade is the handler for the '/trade' API request. 619 func (s *WebServer) apiTrade(w http.ResponseWriter, r *http.Request) { 620 form := new(tradeForm) 621 defer form.Pass.Clear() 622 if !readPost(w, r, form) { 623 return 624 } 625 r.Close = true 626 pass, err := s.resolvePass(form.Pass, r) 627 if err != nil { 628 s.writeAPIError(w, fmt.Errorf("password error: %w", err)) 629 return 630 } 631 defer zero(pass) 632 if form.Order == nil { 633 s.writeAPIError(w, errors.New("order missing")) 634 return 635 } 636 ord, err := s.core.Trade(pass, form.Order) 637 if err != nil { 638 s.writeAPIError(w, fmt.Errorf("error placing order: %w", err)) 639 return 640 } 641 resp := &struct { 642 OK bool `json:"ok"` 643 Order *core.Order `json:"order"` 644 }{ 645 OK: true, 646 Order: ord, 647 } 648 w.Header().Set("Connection", "close") 649 writeJSON(w, resp) 650 } 651 652 // apiTradeAsync is the handler for the '/tradeasync' API request. 653 func (s *WebServer) apiTradeAsync(w http.ResponseWriter, r *http.Request) { 654 form := new(tradeForm) 655 defer form.Pass.Clear() 656 if !readPost(w, r, form) { 657 return 658 } 659 r.Close = true 660 pass, err := s.resolvePass(form.Pass, r) 661 if err != nil { 662 s.writeAPIError(w, fmt.Errorf("password error: %w", err)) 663 return 664 } 665 defer zero(pass) 666 ord, err := s.core.TradeAsync(pass, form.Order) 667 if err != nil { 668 s.writeAPIError(w, fmt.Errorf("error placing order: %w", err)) 669 return 670 } 671 resp := &struct { 672 OK bool `json:"ok"` 673 Order *core.InFlightOrder `json:"order"` 674 }{ 675 OK: true, 676 Order: ord, 677 } 678 w.Header().Set("Connection", "close") 679 writeJSON(w, resp) 680 } 681 682 // apiAccountExport is the handler for the '/exportaccount' API request. 683 func (s *WebServer) apiAccountExport(w http.ResponseWriter, r *http.Request) { 684 form := new(accountExportForm) 685 defer form.Pass.Clear() 686 if !readPost(w, r, form) { 687 return 688 } 689 r.Close = true 690 pass, err := s.resolvePass(form.Pass, r) 691 if err != nil { 692 s.writeAPIError(w, fmt.Errorf("password error: %w", err)) 693 return 694 } 695 defer zero(pass) 696 account, bonds, err := s.core.AccountExport(pass, form.Host) 697 if err != nil { 698 s.writeAPIError(w, fmt.Errorf("error exporting account: %w", err)) 699 return 700 } 701 if bonds == nil { 702 bonds = make([]*db.Bond, 0) // marshal to [], not null 703 } 704 w.Header().Set("Connection", "close") 705 res := &struct { 706 OK bool `json:"ok"` 707 Account *core.Account `json:"account"` 708 Bonds []*db.Bond `json:"bonds"` 709 }{ 710 OK: true, 711 Account: account, 712 Bonds: bonds, 713 } 714 writeJSON(w, res) 715 } 716 717 // apiExportSeed is the handler for the '/exportseed' API request. 718 func (s *WebServer) apiExportSeed(w http.ResponseWriter, r *http.Request) { 719 form := &struct { 720 Pass encode.PassBytes `json:"pass"` 721 }{} 722 defer form.Pass.Clear() 723 if !readPost(w, r, form) { 724 return 725 } 726 r.Close = true 727 seed, err := s.core.ExportSeed(form.Pass) 728 if err != nil { 729 s.writeAPIError(w, fmt.Errorf("error exporting seed: %w", err)) 730 return 731 } 732 writeJSON(w, &struct { 733 OK bool `json:"ok"` 734 Seed string `json:"seed"` 735 }{ 736 OK: true, 737 Seed: seed, 738 }) 739 } 740 741 // apiAccountImport is the handler for the '/importaccount' API request. 742 func (s *WebServer) apiAccountImport(w http.ResponseWriter, r *http.Request) { 743 form := new(accountImportForm) 744 defer form.Pass.Clear() 745 if !readPost(w, r, form) { 746 return 747 } 748 r.Close = true 749 pass, err := s.resolvePass(form.Pass, r) 750 if err != nil { 751 s.writeAPIError(w, fmt.Errorf("password error: %w", err)) 752 return 753 } 754 defer zero(pass) 755 if form.Account == nil { 756 s.writeAPIError(w, errors.New("account missing")) 757 return 758 } 759 err = s.core.AccountImport(pass, form.Account, form.Bonds) 760 if err != nil { 761 s.writeAPIError(w, fmt.Errorf("error importing account: %w", err)) 762 return 763 } 764 w.Header().Set("Connection", "close") 765 writeJSON(w, simpleAck()) 766 } 767 768 func (s *WebServer) apiUpdateCert(w http.ResponseWriter, r *http.Request) { 769 form := &struct { 770 Host string `json:"host"` 771 Cert string `json:"cert"` 772 }{} 773 if !readPost(w, r, form) { 774 return 775 } 776 777 err := s.core.UpdateCert(form.Host, []byte(form.Cert)) 778 if err != nil { 779 s.writeAPIError(w, fmt.Errorf("error updating cert: %w", err)) 780 return 781 } 782 783 writeJSON(w, simpleAck()) 784 } 785 786 func (s *WebServer) apiUpdateDEXHost(w http.ResponseWriter, r *http.Request) { 787 form := &struct { 788 Pass encode.PassBytes `json:"pw"` 789 OldHost string `json:"oldHost"` 790 NewHost string `json:"newHost"` 791 Cert string `json:"cert"` 792 }{} 793 defer form.Pass.Clear() 794 if !readPost(w, r, form) { 795 return 796 } 797 pass, err := s.resolvePass(form.Pass, r) 798 if err != nil { 799 s.writeAPIError(w, fmt.Errorf("password error: %w", err)) 800 return 801 } 802 defer zero(pass) 803 cert := []byte(form.Cert) 804 exchange, err := s.core.UpdateDEXHost(form.OldHost, form.NewHost, pass, cert) 805 if err != nil { 806 s.writeAPIError(w, fmt.Errorf("error updating host: %w", err)) 807 return 808 } 809 810 resp := struct { 811 OK bool `json:"ok"` 812 Exchange *core.Exchange `json:"xc,omitempty"` 813 }{ 814 OK: true, 815 Exchange: exchange, 816 } 817 818 writeJSON(w, resp) 819 } 820 821 // apiRestoreWalletInfo is the handler for the '/restorewalletinfo' API 822 // request. 823 func (s *WebServer) apiRestoreWalletInfo(w http.ResponseWriter, r *http.Request) { 824 form := &struct { 825 AssetID uint32 826 Pass encode.PassBytes 827 }{} 828 defer form.Pass.Clear() 829 if !readPost(w, r, form) { 830 return 831 } 832 833 info, err := s.core.WalletRestorationInfo(form.Pass, form.AssetID) 834 if err != nil { 835 s.writeAPIError(w, fmt.Errorf("error updating cert: %w", err)) 836 return 837 } 838 839 resp := struct { 840 OK bool `json:"ok"` 841 RestorationInfo []*asset.WalletRestoration `json:"restorationinfo,omitempty"` 842 }{ 843 OK: true, 844 RestorationInfo: info, 845 } 846 writeJSON(w, resp) 847 } 848 849 // apiToggleAccountStatus is the handler for the '/toggleaccountstatus' API request. 850 func (s *WebServer) apiToggleAccountStatus(w http.ResponseWriter, r *http.Request) { 851 form := new(updateAccountStatusForm) 852 defer form.Pass.Clear() 853 if !readPost(w, r, form) { 854 return 855 } 856 defer form.Pass.Clear() 857 appPW, err := s.resolvePass(form.Pass, r) 858 if err != nil { 859 s.writeAPIError(w, fmt.Errorf("password error: %w", err)) 860 return 861 } 862 // Disable account. 863 err = s.core.ToggleAccountStatus(appPW, form.Host, form.Disable) 864 if err != nil { 865 s.writeAPIError(w, fmt.Errorf("error updating account status: %w", err)) 866 return 867 } 868 if form.Disable { 869 w.Header().Set("Connection", "close") 870 } 871 writeJSON(w, simpleAck()) 872 } 873 874 // apiCancel is the handler for the '/cancel' API request. 875 func (s *WebServer) apiCancel(w http.ResponseWriter, r *http.Request) { 876 form := new(cancelForm) 877 if !readPost(w, r, form) { 878 return 879 } 880 err := s.core.Cancel(form.OrderID) 881 if err != nil { 882 s.writeAPIError(w, fmt.Errorf("error cancelling order %s: %w", form.OrderID, err)) 883 return 884 } 885 writeJSON(w, simpleAck()) 886 } 887 888 // apiCloseWallet is the handler for the '/closewallet' API request. 889 func (s *WebServer) apiCloseWallet(w http.ResponseWriter, r *http.Request) { 890 form := &struct { 891 AssetID uint32 `json:"assetID"` 892 }{} 893 if !readPost(w, r, form) { 894 return 895 } 896 err := s.core.CloseWallet(form.AssetID) 897 if err != nil { 898 s.writeAPIError(w, fmt.Errorf("error locking %s wallet: %w", unbip(form.AssetID), err)) 899 return 900 } 901 902 writeJSON(w, simpleAck()) 903 } 904 905 // apiInit is the handler for the '/init' API request. 906 func (s *WebServer) apiInit(w http.ResponseWriter, r *http.Request) { 907 var init struct { 908 Pass encode.PassBytes `json:"pass"` 909 Seed string `json:"seed,omitempty"` 910 } 911 defer init.Pass.Clear() 912 if !readPost(w, r, &init) { 913 return 914 } 915 var seed *string 916 if len(init.Seed) > 0 { 917 seed = &init.Seed 918 } 919 mnemonicSeed, err := s.core.InitializeClient(init.Pass, seed) 920 if err != nil { 921 s.writeAPIError(w, fmt.Errorf("initialization error: %w", err)) 922 return 923 } 924 err = s.actuallyLogin(w, r, &loginForm{Pass: init.Pass}) 925 if err != nil { 926 s.writeAPIError(w, err) 927 return 928 } 929 930 writeJSON(w, struct { 931 OK bool `json:"ok"` 932 Hosts []string `json:"hosts"` 933 MnemonicSeed string `json:"mnemonic"` 934 }{ 935 OK: true, 936 Hosts: s.knownUnregisteredExchanges(map[string]*core.Exchange{}), 937 MnemonicSeed: mnemonicSeed, 938 }) 939 } 940 941 // apiIsInitialized is the handler for the '/isinitialized' request. 942 func (s *WebServer) apiIsInitialized(w http.ResponseWriter, r *http.Request) { 943 writeJSON(w, &struct { 944 OK bool `json:"ok"` 945 Initialized bool `json:"initialized"` 946 }{ 947 OK: true, 948 Initialized: s.core.IsInitialized(), 949 }) 950 } 951 952 func (s *WebServer) apiLocale(w http.ResponseWriter, r *http.Request) { 953 var lang string 954 if !readPost(w, r, &lang) { 955 return 956 } 957 m, found := localesMap[lang] 958 if !found { 959 s.writeAPIError(w, fmt.Errorf("no locale for language %q", lang)) 960 return 961 } 962 resp := make(map[string]string) 963 for translationID, defaultTranslation := range enUS { 964 t, found := m[translationID] 965 if !found { 966 t = defaultTranslation 967 } 968 resp[translationID] = t.T 969 } 970 971 writeJSON(w, resp) 972 } 973 974 func (s *WebServer) apiSetLocale(w http.ResponseWriter, r *http.Request) { 975 var lang string 976 if !readPost(w, r, &lang) { 977 return 978 } 979 if err := s.core.SetLanguage(lang); err != nil { 980 s.writeAPIError(w, err) 981 return 982 } 983 984 s.lang.Store(lang) 985 if err := s.buildTemplates(lang); err != nil { 986 s.writeAPIError(w, err) 987 return 988 } 989 990 writeJSON(w, simpleAck()) 991 } 992 993 // apiLogin handles the 'login' API request. 994 func (s *WebServer) apiLogin(w http.ResponseWriter, r *http.Request) { 995 login := new(loginForm) 996 defer login.Pass.Clear() 997 if !readPost(w, r, login) { 998 return 999 } 1000 1001 err := s.actuallyLogin(w, r, login) 1002 if err != nil { 1003 s.writeAPIError(w, err) 1004 return 1005 } 1006 1007 notes, pokes, err := s.core.Notifications(100) 1008 if err != nil { 1009 log.Errorf("failed to get notifications: %v", err) 1010 } 1011 1012 writeJSON(w, &struct { 1013 OK bool `json:"ok"` 1014 Notes []*db.Notification `json:"notes"` 1015 Pokes []*db.Notification `json:"pokes"` 1016 }{ 1017 OK: true, 1018 Notes: notes, 1019 Pokes: pokes, 1020 }) 1021 } 1022 1023 func (s *WebServer) apiNotes(w http.ResponseWriter, r *http.Request) { 1024 notes, pokes, err := s.core.Notifications(100) 1025 if err != nil { 1026 s.writeAPIError(w, fmt.Errorf("failed to get notifications: %w", err)) 1027 return 1028 } 1029 1030 writeJSON(w, &struct { 1031 OK bool `json:"ok"` 1032 Notes []*db.Notification `json:"notes"` 1033 Pokes []*db.Notification `json:"pokes"` 1034 }{ 1035 OK: true, 1036 Notes: notes, 1037 Pokes: pokes, 1038 }) 1039 } 1040 1041 // apiLogout handles the 'logout' API request. 1042 func (s *WebServer) apiLogout(w http.ResponseWriter, r *http.Request) { 1043 err := s.core.Logout() 1044 if err != nil { 1045 s.writeAPIError(w, fmt.Errorf("logout error: %w", err)) 1046 return 1047 } 1048 1049 // With Core locked up, invalidate all known auth tokens and cached passwords 1050 // to force any other sessions to login again. 1051 s.deauth() 1052 1053 clearCookie(authCK, w) 1054 clearCookie(pwKeyCK, w) 1055 1056 response := struct { 1057 OK bool `json:"ok"` 1058 }{ 1059 OK: true, 1060 } 1061 writeJSON(w, response) 1062 } 1063 1064 // apiGetBalance handles the 'balance' API request. 1065 func (s *WebServer) apiGetBalance(w http.ResponseWriter, r *http.Request) { 1066 form := &struct { 1067 AssetID uint32 `json:"assetID"` 1068 }{} 1069 if !readPost(w, r, form) { 1070 return 1071 } 1072 bal, err := s.core.AssetBalance(form.AssetID) 1073 if err != nil { 1074 s.writeAPIError(w, fmt.Errorf("balance error: %w", err)) 1075 return 1076 } 1077 resp := &struct { 1078 OK bool `json:"ok"` 1079 Balance *core.WalletBalance `json:"balance"` 1080 }{ 1081 OK: true, 1082 Balance: bal, 1083 } 1084 writeJSON(w, resp) 1085 1086 } 1087 1088 // apiParseConfig parses an INI config file into a map[string]string. 1089 func (s *WebServer) apiParseConfig(w http.ResponseWriter, r *http.Request) { 1090 form := &struct { 1091 ConfigText string `json:"configtext"` 1092 }{} 1093 if !readPost(w, r, form) { 1094 return 1095 } 1096 configMap, err := config.Parse([]byte(form.ConfigText)) 1097 if err != nil { 1098 s.writeAPIError(w, fmt.Errorf("parse error: %w", err)) 1099 return 1100 } 1101 resp := &struct { 1102 OK bool `json:"ok"` 1103 Map map[string]string `json:"map"` 1104 }{ 1105 OK: true, 1106 Map: configMap, 1107 } 1108 writeJSON(w, resp) 1109 } 1110 1111 // apiWalletSettings fetches the currently stored wallet configuration settings. 1112 func (s *WebServer) apiWalletSettings(w http.ResponseWriter, r *http.Request) { 1113 form := &struct { 1114 AssetID uint32 `json:"assetID"` 1115 }{} 1116 if !readPost(w, r, form) { 1117 return 1118 } 1119 settings, err := s.core.WalletSettings(form.AssetID) 1120 if err != nil { 1121 s.writeAPIError(w, fmt.Errorf("error setting wallet settings: %w", err)) 1122 return 1123 } 1124 writeJSON(w, &struct { 1125 OK bool `json:"ok"` 1126 Map map[string]string `json:"map"` 1127 }{ 1128 OK: true, 1129 Map: settings, 1130 }) 1131 } 1132 1133 // apiToggleWalletStatus updates the wallet's status. 1134 func (s *WebServer) apiToggleWalletStatus(w http.ResponseWriter, r *http.Request) { 1135 form := new(walletStatusForm) 1136 if !readPost(w, r, form) { 1137 return 1138 } 1139 err := s.core.ToggleWalletStatus(form.AssetID, form.Disable) 1140 if err != nil { 1141 s.writeAPIError(w, fmt.Errorf("error setting wallet settings: %w", err)) 1142 return 1143 } 1144 response := struct { 1145 OK bool `json:"ok"` 1146 }{ 1147 OK: true, 1148 } 1149 writeJSON(w, response) 1150 } 1151 1152 // apiDefaultWalletCfg attempts to load configuration settings from the 1153 // asset's default path on the server. 1154 func (s *WebServer) apiDefaultWalletCfg(w http.ResponseWriter, r *http.Request) { 1155 form := &struct { 1156 AssetID uint32 `json:"assetID"` 1157 Type string `json:"type"` 1158 }{} 1159 if !readPost(w, r, form) { 1160 return 1161 } 1162 cfg, err := s.core.AutoWalletConfig(form.AssetID, form.Type) 1163 if err != nil { 1164 s.writeAPIError(w, fmt.Errorf("error getting wallet config: %w", err)) 1165 return 1166 } 1167 writeJSON(w, struct { 1168 OK bool `json:"ok"` 1169 Config map[string]string `json:"config"` 1170 }{ 1171 OK: true, 1172 Config: cfg, 1173 }) 1174 } 1175 1176 // apiOrders responds with a filtered list of user orders. 1177 func (s *WebServer) apiOrders(w http.ResponseWriter, r *http.Request) { 1178 filter := new(core.OrderFilter) 1179 if !readPost(w, r, filter) { 1180 return 1181 } 1182 1183 ords, err := s.core.Orders(filter) 1184 if err != nil { 1185 s.writeAPIError(w, fmt.Errorf("Orders error: %w", err)) 1186 return 1187 } 1188 writeJSON(w, &struct { 1189 OK bool `json:"ok"` 1190 Orders []*core.Order `json:"orders"` 1191 }{ 1192 OK: true, 1193 Orders: ords, 1194 }) 1195 } 1196 1197 // apiAccelerateOrder speeds up the mining of transactions in an order. 1198 func (s *WebServer) apiAccelerateOrder(w http.ResponseWriter, r *http.Request) { 1199 form := struct { 1200 Pass encode.PassBytes `json:"pw"` 1201 OrderID dex.Bytes `json:"orderID"` 1202 NewRate uint64 `json:"newRate"` 1203 }{} 1204 defer form.Pass.Clear() 1205 if !readPost(w, r, &form) { 1206 return 1207 } 1208 pass, err := s.resolvePass(form.Pass, r) 1209 if err != nil { 1210 s.writeAPIError(w, fmt.Errorf("password error: %w", err)) 1211 return 1212 } 1213 1214 txID, err := s.core.AccelerateOrder(pass, form.OrderID, form.NewRate) 1215 if err != nil { 1216 s.writeAPIError(w, fmt.Errorf("Accelerate Order error: %w", err)) 1217 return 1218 } 1219 1220 writeJSON(w, &struct { 1221 OK bool `json:"ok"` 1222 TxID string `json:"txID"` 1223 }{ 1224 OK: true, 1225 TxID: txID, 1226 }) 1227 } 1228 1229 // apiPreAccelerate responds with information about accelerating the mining of 1230 // swaps in an order 1231 func (s *WebServer) apiPreAccelerate(w http.ResponseWriter, r *http.Request) { 1232 var oid dex.Bytes 1233 if !readPost(w, r, &oid) { 1234 return 1235 } 1236 1237 preAccelerate, err := s.core.PreAccelerateOrder(oid) 1238 if err != nil { 1239 s.writeAPIError(w, fmt.Errorf("Pre accelerate error: %w", err)) 1240 return 1241 } 1242 1243 writeJSON(w, &struct { 1244 OK bool `json:"ok"` 1245 PreAccelerate *core.PreAccelerate `json:"preAccelerate"` 1246 }{ 1247 OK: true, 1248 PreAccelerate: preAccelerate, 1249 }) 1250 } 1251 1252 // apiAccelerationEstimate responds with how much it would cost to accelerate 1253 // an order to the requested fee rate. 1254 func (s *WebServer) apiAccelerationEstimate(w http.ResponseWriter, r *http.Request) { 1255 form := struct { 1256 OrderID dex.Bytes `json:"orderID"` 1257 NewRate uint64 `json:"newRate"` 1258 }{} 1259 1260 if !readPost(w, r, &form) { 1261 return 1262 } 1263 1264 fee, err := s.core.AccelerationEstimate(form.OrderID, form.NewRate) 1265 if err != nil { 1266 s.writeAPIError(w, fmt.Errorf("Accelerate Order error: %w", err)) 1267 return 1268 } 1269 1270 writeJSON(w, &struct { 1271 OK bool `json:"ok"` 1272 Fee uint64 `json:"fee"` 1273 }{ 1274 OK: true, 1275 Fee: fee, 1276 }) 1277 } 1278 1279 // apiOrder responds with data for an order. 1280 func (s *WebServer) apiOrder(w http.ResponseWriter, r *http.Request) { 1281 var oid dex.Bytes 1282 if !readPost(w, r, &oid) { 1283 return 1284 } 1285 1286 ord, err := s.core.Order(oid) 1287 if err != nil { 1288 s.writeAPIError(w, fmt.Errorf("Order error: %w", err)) 1289 return 1290 } 1291 writeJSON(w, &struct { 1292 OK bool `json:"ok"` 1293 Order *core.Order `json:"order"` 1294 }{ 1295 OK: true, 1296 Order: ord, 1297 }) 1298 } 1299 1300 // apiChangeAppPass updates the application password. 1301 func (s *WebServer) apiChangeAppPass(w http.ResponseWriter, r *http.Request) { 1302 form := &struct { 1303 AppPW encode.PassBytes `json:"appPW"` 1304 NewAppPW encode.PassBytes `json:"newAppPW"` 1305 }{} 1306 defer form.AppPW.Clear() 1307 defer form.NewAppPW.Clear() 1308 if !readPost(w, r, form) { 1309 return 1310 } 1311 1312 // Update application password. 1313 err := s.core.ChangeAppPass(form.AppPW, form.NewAppPW) 1314 if err != nil { 1315 s.writeAPIError(w, fmt.Errorf("change app pass error: %w", err)) 1316 return 1317 } 1318 1319 passwordIsCached := s.isPasswordCached(r) 1320 // Since the user changed the password, we clear all of the auth tokens 1321 // and cached passwords. However, we assign a new auth token and cache 1322 // the new password (if it was previously cached) for this session. 1323 s.deauth() 1324 authToken := s.authorize() 1325 setCookie(authCK, authToken, w) 1326 if passwordIsCached { 1327 key, err := s.cacheAppPassword(form.NewAppPW, authToken) 1328 if err != nil { 1329 log.Errorf("unable to cache password: %w", err) 1330 clearCookie(pwKeyCK, w) 1331 } else { 1332 setCookie(pwKeyCK, hex.EncodeToString(key), w) 1333 zero(key) 1334 } 1335 } 1336 1337 writeJSON(w, simpleAck()) 1338 } 1339 1340 // apiResetAppPassword resets the application password. 1341 func (s *WebServer) apiResetAppPassword(w http.ResponseWriter, r *http.Request) { 1342 form := new(struct { 1343 NewPass encode.PassBytes `json:"newPass"` 1344 Seed string `json:"seed"` 1345 }) 1346 defer form.NewPass.Clear() 1347 if !readPost(w, r, form) { 1348 return 1349 } 1350 1351 err := s.core.ResetAppPass(form.NewPass, form.Seed) 1352 if err != nil { 1353 s.writeAPIError(w, err) 1354 return 1355 } 1356 1357 writeJSON(w, simpleAck()) 1358 } 1359 1360 // apiReconfig sets new configuration details for the wallet. 1361 func (s *WebServer) apiReconfig(w http.ResponseWriter, r *http.Request) { 1362 form := &struct { 1363 AssetID uint32 `json:"assetID"` 1364 WalletType string `json:"walletType"` 1365 Config map[string]string `json:"config"` 1366 // newWalletPW json field should be omitted in case caller isn't interested 1367 // in setting new password, passing null JSON value will cause an unmarshal 1368 // error. 1369 NewWalletPW encode.PassBytes `json:"newWalletPW"` 1370 AppPW encode.PassBytes `json:"appPW"` 1371 }{} 1372 defer form.NewWalletPW.Clear() 1373 defer form.AppPW.Clear() 1374 if !readPost(w, r, form) { 1375 return 1376 } 1377 pass, err := s.resolvePass(form.AppPW, r) 1378 if err != nil { 1379 s.writeAPIError(w, fmt.Errorf("password error: %w", err)) 1380 return 1381 } 1382 defer zero(pass) 1383 // Update wallet settings. 1384 err = s.core.ReconfigureWallet(pass, form.NewWalletPW, &core.WalletForm{ 1385 AssetID: form.AssetID, 1386 Config: form.Config, 1387 Type: form.WalletType, 1388 }) 1389 if err != nil { 1390 s.writeAPIError(w, fmt.Errorf("reconfig error: %w", err)) 1391 return 1392 } 1393 1394 writeJSON(w, simpleAck()) 1395 } 1396 1397 // apiSend handles the 'send' API request. 1398 func (s *WebServer) apiSend(w http.ResponseWriter, r *http.Request) { 1399 form := new(sendForm) 1400 defer form.Pass.Clear() 1401 if !readPost(w, r, form) { 1402 return 1403 } 1404 state := s.core.WalletState(form.AssetID) 1405 if state == nil { 1406 s.writeAPIError(w, fmt.Errorf("no wallet found for %s", unbip(form.AssetID))) 1407 return 1408 } 1409 if len(form.Pass) == 0 { 1410 s.writeAPIError(w, fmt.Errorf("empty password")) 1411 return 1412 } 1413 coin, err := s.core.Send(form.Pass, form.AssetID, form.Value, form.Address, form.Subtract) 1414 if err != nil { 1415 s.writeAPIError(w, fmt.Errorf("send/withdraw error: %w", err)) 1416 return 1417 } 1418 resp := struct { 1419 OK bool `json:"ok"` 1420 Coin string `json:"coin"` 1421 }{ 1422 OK: true, 1423 Coin: coin.String(), 1424 } 1425 writeJSON(w, resp) 1426 } 1427 1428 // apiMaxBuy handles the 'maxbuy' API request. 1429 func (s *WebServer) apiMaxBuy(w http.ResponseWriter, r *http.Request) { 1430 form := &struct { 1431 Host string `json:"host"` 1432 Base uint32 `json:"base"` 1433 Quote uint32 `json:"quote"` 1434 Rate uint64 `json:"rate"` 1435 }{} 1436 if !readPost(w, r, form) { 1437 return 1438 } 1439 maxBuy, err := s.core.MaxBuy(form.Host, form.Base, form.Quote, form.Rate) 1440 if err != nil { 1441 s.writeAPIError(w, fmt.Errorf("max order estimation error: %w", err)) 1442 return 1443 } 1444 resp := struct { 1445 OK bool `json:"ok"` 1446 MaxBuy *core.MaxOrderEstimate `json:"maxBuy"` 1447 }{ 1448 OK: true, 1449 MaxBuy: maxBuy, 1450 } 1451 writeJSON(w, resp) 1452 } 1453 1454 // apiMaxSell handles the 'maxsell' API request. 1455 func (s *WebServer) apiMaxSell(w http.ResponseWriter, r *http.Request) { 1456 form := &struct { 1457 Host string `json:"host"` 1458 Base uint32 `json:"base"` 1459 Quote uint32 `json:"quote"` 1460 }{} 1461 if !readPost(w, r, form) { 1462 return 1463 } 1464 maxSell, err := s.core.MaxSell(form.Host, form.Base, form.Quote) 1465 if err != nil { 1466 s.writeAPIError(w, fmt.Errorf("max order estimation error: %w", err)) 1467 return 1468 } 1469 resp := struct { 1470 OK bool `json:"ok"` 1471 MaxSell *core.MaxOrderEstimate `json:"maxSell"` 1472 }{ 1473 OK: true, 1474 MaxSell: maxSell, 1475 } 1476 writeJSON(w, resp) 1477 } 1478 1479 // apiPreOrder handles the 'preorder' API request. 1480 func (s *WebServer) apiPreOrder(w http.ResponseWriter, r *http.Request) { 1481 form := new(core.TradeForm) 1482 if !readPost(w, r, form) { 1483 return 1484 } 1485 1486 est, err := s.core.PreOrder(form) 1487 if err != nil { 1488 s.writeAPIError(w, err) 1489 return 1490 } 1491 1492 resp := struct { 1493 OK bool `json:"ok"` 1494 Estimate *core.OrderEstimate `json:"estimate"` 1495 }{ 1496 OK: true, 1497 Estimate: est, 1498 } 1499 1500 writeJSON(w, resp) 1501 } 1502 1503 // apiActuallyLogin logs the user in. login form private data is expected to be 1504 // cleared by the caller. 1505 func (s *WebServer) actuallyLogin(w http.ResponseWriter, r *http.Request, login *loginForm) error { 1506 pass, err := s.resolvePass(login.Pass, r) 1507 defer zero(pass) 1508 if err != nil { 1509 return fmt.Errorf("password error: %w", err) 1510 } 1511 err = s.core.Login(pass) 1512 if err != nil { 1513 return fmt.Errorf("login error: %w", err) 1514 } 1515 1516 if !s.isAuthed(r) { 1517 authToken := s.authorize() 1518 setCookie(authCK, authToken, w) 1519 key, err := s.cacheAppPassword(pass, authToken) 1520 if err != nil { 1521 return fmt.Errorf("login error: %w", err) 1522 1523 } 1524 setCookie(pwKeyCK, hex.EncodeToString(key), w) 1525 zero(key) 1526 } 1527 1528 return nil 1529 } 1530 1531 // apiUser handles the 'user' API request. 1532 func (s *WebServer) apiUser(w http.ResponseWriter, r *http.Request) { 1533 var u *core.User 1534 if s.isAuthed(r) { 1535 u = s.core.User() 1536 } 1537 1538 var mmStatus *mm.Status 1539 if s.mm != nil { 1540 mmStatus = s.mm.Status() 1541 } 1542 1543 response := struct { 1544 User *core.User `json:"user"` 1545 Lang string `json:"lang"` 1546 Langs []string `json:"langs"` 1547 Inited bool `json:"inited"` 1548 OK bool `json:"ok"` 1549 MMStatus *mm.Status `json:"mmStatus"` 1550 }{ 1551 User: u, 1552 Lang: s.lang.Load().(string), 1553 Langs: s.langs, 1554 Inited: s.core.IsInitialized(), 1555 OK: true, 1556 MMStatus: mmStatus, 1557 } 1558 writeJSON(w, response) 1559 } 1560 1561 // apiToggleRateSource handles the /toggleratesource API request. 1562 func (s *WebServer) apiToggleRateSource(w http.ResponseWriter, r *http.Request) { 1563 form := &struct { 1564 Disable bool `json:"disable"` 1565 Source string `json:"source"` 1566 }{} 1567 if !readPost(w, r, form) { 1568 return 1569 } 1570 err := s.core.ToggleRateSourceStatus(form.Source, form.Disable) 1571 if err != nil { 1572 s.writeAPIError(w, fmt.Errorf("error disabling/enabling rate source: %w", err)) 1573 return 1574 } 1575 writeJSON(w, simpleAck()) 1576 } 1577 1578 // apiDeleteArchiveRecords handles the '/deletearchivedrecords' API request. 1579 func (s *WebServer) apiDeleteArchivedRecords(w http.ResponseWriter, r *http.Request) { 1580 form := new(deleteRecordsForm) 1581 if !readPost(w, r, form) { 1582 return 1583 } 1584 1585 var olderThan *time.Time 1586 if form.OlderThanMs > 0 { 1587 ot := time.UnixMilli(form.OlderThanMs) 1588 olderThan = &ot 1589 } 1590 1591 archivedRecordsPath, nRecordsDeleted, err := s.core.DeleteArchivedRecordsWithBackup(olderThan, form.SaveMatchesToFile, form.SaveOrdersToFile) 1592 if err != nil { 1593 s.writeAPIError(w, fmt.Errorf("error deleting archived records: %w", err)) 1594 return 1595 } 1596 resp := &struct { 1597 Ok bool `json:"ok"` 1598 ArchivedRecordsDeleted int `json:"archivedRecordsDeleted"` 1599 ArchivedRecordsPath string `json:"archivedRecordsPath"` 1600 }{ 1601 Ok: true, 1602 ArchivedRecordsDeleted: nRecordsDeleted, 1603 ArchivedRecordsPath: archivedRecordsPath, 1604 } 1605 writeJSON(w, resp) 1606 } 1607 1608 func (s *WebServer) apiMarketReport(w http.ResponseWriter, r *http.Request) { 1609 form := &struct { 1610 BaseID uint32 `json:"baseID"` 1611 QuoteID uint32 `json:"quoteID"` 1612 Host string `json:"host"` 1613 }{} 1614 if !readPost(w, r, form) { 1615 return 1616 } 1617 report, err := s.mm.MarketReport(form.Host, form.BaseID, form.QuoteID) 1618 if err != nil { 1619 s.writeAPIError(w, fmt.Errorf("error getting market report: %w", err)) 1620 return 1621 } 1622 writeJSON(w, &struct { 1623 OK bool `json:"ok"` 1624 Report *mm.MarketReport `json:"report"` 1625 }{ 1626 OK: true, 1627 Report: report, 1628 }) 1629 } 1630 1631 func (s *WebServer) apiCEXBalance(w http.ResponseWriter, r *http.Request) { 1632 var req struct { 1633 CEXName string `json:"cexName"` 1634 AssetID uint32 `json:"assetID"` 1635 } 1636 if !readPost(w, r, &req) { 1637 return 1638 } 1639 bal, err := s.mm.CEXBalance(req.CEXName, req.AssetID) 1640 if err != nil { 1641 s.writeAPIError(w, fmt.Errorf("error getting cex balance: %w", err)) 1642 return 1643 } 1644 writeJSON(w, &struct { 1645 OK bool `json:"ok"` 1646 CEXBalance *libxc.ExchangeBalance `json:"cexBalance"` 1647 }{ 1648 OK: true, 1649 CEXBalance: bal, 1650 }) 1651 } 1652 1653 func (s *WebServer) apiArchivedRuns(w http.ResponseWriter, r *http.Request) { 1654 runs, err := s.mm.ArchivedRuns() 1655 if err != nil { 1656 s.writeAPIError(w, fmt.Errorf("error getting archived runs: %w", err)) 1657 return 1658 } 1659 1660 writeJSON(w, &struct { 1661 OK bool `json:"ok"` 1662 Runs []*mm.MarketMakingRun `json:"runs"` 1663 }{ 1664 OK: true, 1665 Runs: runs, 1666 }) 1667 } 1668 1669 func (s *WebServer) apiRunLogs(w http.ResponseWriter, r *http.Request) { 1670 var req struct { 1671 StartTime int64 `json:"startTime"` 1672 Market *mm.MarketWithHost `json:"market"` 1673 N uint64 `json:"n"` 1674 RefID *uint64 `json:"refID,omitempty"` 1675 Filters *mm.RunLogFilters `json:"filters,omitempty"` 1676 } 1677 if !readPost(w, r, &req) { 1678 return 1679 } 1680 1681 if req.Market == nil { 1682 s.writeAPIError(w, errors.New("market missing")) 1683 return 1684 } 1685 1686 logs, updatedLogs, overview, err := s.mm.RunLogs(req.StartTime, req.Market, req.N, req.RefID, req.Filters) 1687 if err != nil { 1688 s.writeAPIError(w, fmt.Errorf("error getting run logs: %w", err)) 1689 return 1690 } 1691 1692 writeJSON(w, &struct { 1693 OK bool `json:"ok"` 1694 Overview *mm.MarketMakingRunOverview `json:"overview"` 1695 Logs []*mm.MarketMakingEvent `json:"logs"` 1696 UpdatedLogs []*mm.MarketMakingEvent `json:"updatedLogs"` 1697 }{ 1698 OK: true, 1699 Overview: overview, 1700 Logs: logs, 1701 UpdatedLogs: updatedLogs, 1702 }) 1703 } 1704 1705 func (s *WebServer) apiCEXBook(w http.ResponseWriter, r *http.Request) { 1706 var req struct { 1707 Host string `json:"host"` 1708 BaseID uint32 `json:"baseID"` 1709 QuoteID uint32 `json:"quoteID"` 1710 } 1711 if !readPost(w, r, &req) { 1712 return 1713 } 1714 buys, sells, err := s.mm.CEXBook(req.Host, req.BaseID, req.QuoteID) 1715 if err != nil { 1716 s.writeAPIError(w, fmt.Errorf("error CEX Book: %w", err)) 1717 return 1718 } 1719 1720 writeJSON(w, &struct { 1721 OK bool `json:"ok"` 1722 Book *core.OrderBook `json:"book"` 1723 }{ 1724 OK: true, 1725 Book: &core.OrderBook{ 1726 Buys: buys, 1727 Sells: sells, 1728 }, 1729 }) 1730 1731 } 1732 1733 func (s *WebServer) apiStakeStatus(w http.ResponseWriter, r *http.Request) { 1734 var assetID uint32 1735 if !readPost(w, r, &assetID) { 1736 return 1737 } 1738 status, err := s.core.StakeStatus(assetID) 1739 if err != nil { 1740 s.writeAPIError(w, fmt.Errorf("error fetching stake status for asset ID %d: %w", assetID, err)) 1741 return 1742 } 1743 writeJSON(w, &struct { 1744 OK bool `json:"ok"` 1745 Status *asset.TicketStakingStatus `json:"status"` 1746 }{ 1747 OK: true, 1748 Status: status, 1749 }) 1750 } 1751 1752 func (s *WebServer) apiSetVSP(w http.ResponseWriter, r *http.Request) { 1753 var req struct { 1754 AssetID uint32 `json:"assetID"` 1755 URL string `json:"url"` 1756 } 1757 if !readPost(w, r, &req) { 1758 return 1759 } 1760 if err := s.core.SetVSP(req.AssetID, req.URL); err != nil { 1761 s.writeAPIError(w, fmt.Errorf("error settings vsp to %q for asset ID %d: %w", req.URL, req.AssetID, err)) 1762 return 1763 } 1764 writeJSON(w, simpleAck()) 1765 } 1766 1767 func (s *WebServer) apiPurchaseTickets(w http.ResponseWriter, r *http.Request) { 1768 var req struct { 1769 AssetID uint32 `json:"assetID"` 1770 N int `json:"n"` 1771 AppPW encode.PassBytes `json:"appPW"` 1772 } 1773 if !readPost(w, r, &req) { 1774 return 1775 } 1776 appPW, err := s.resolvePass(req.AppPW, r) 1777 defer zero(appPW) 1778 if err != nil { 1779 s.writeAPIError(w, fmt.Errorf("password error: %w", err)) 1780 return 1781 } 1782 if err = s.core.PurchaseTickets(req.AssetID, appPW, req.N); err != nil { 1783 s.writeAPIError(w, fmt.Errorf("error purchasing tickets for asset ID %d: %w", req.AssetID, err)) 1784 return 1785 } 1786 writeJSON(w, simpleAck()) 1787 } 1788 1789 func (s *WebServer) apiSetVotingPreferences(w http.ResponseWriter, r *http.Request) { 1790 var req struct { 1791 AssetID uint32 `json:"assetID"` 1792 Choices map[string]string `json:"choices"` 1793 TSpendPolicy map[string]string `json:"tSpendPolicy"` 1794 TreasuryPolicy map[string]string `json:"treasuryPolicy"` 1795 } 1796 if !readPost(w, r, &req) { 1797 return 1798 } 1799 if err := s.core.SetVotingPreferences(req.AssetID, req.Choices, req.TSpendPolicy, req.TreasuryPolicy); err != nil { 1800 s.writeAPIError(w, fmt.Errorf("error setting voting preferences for asset ID %d: %w", req.AssetID, err)) 1801 return 1802 } 1803 writeJSON(w, simpleAck()) 1804 } 1805 1806 func (s *WebServer) apiListVSPs(w http.ResponseWriter, r *http.Request) { 1807 var assetID uint32 1808 if !readPost(w, r, &assetID) { 1809 return 1810 } 1811 vsps, err := s.core.ListVSPs(assetID) 1812 if err != nil { 1813 s.writeAPIError(w, fmt.Errorf("error listing VSPs for asset ID %d: %w", assetID, err)) 1814 return 1815 } 1816 writeJSON(w, &struct { 1817 OK bool `json:"ok"` 1818 VSPs []*asset.VotingServiceProvider `json:"vsps"` 1819 }{ 1820 OK: true, 1821 VSPs: vsps, 1822 }) 1823 } 1824 1825 func (s *WebServer) apiTicketPage(w http.ResponseWriter, r *http.Request) { 1826 var req struct { 1827 AssetID uint32 `json:"assetID"` 1828 ScanStart int32 `json:"scanStart"` 1829 N int `json:"n"` 1830 SkipN int `json:"skipN"` 1831 } 1832 if !readPost(w, r, &req) { 1833 return 1834 } 1835 tickets, err := s.core.TicketPage(req.AssetID, req.ScanStart, req.N, req.SkipN) 1836 if err != nil { 1837 s.writeAPIError(w, fmt.Errorf("error retrieving ticket page for %d: %w", req.AssetID, err)) 1838 return 1839 } 1840 writeJSON(w, &struct { 1841 OK bool `json:"ok"` 1842 Tickets []*asset.Ticket `json:"tickets"` 1843 }{ 1844 OK: true, 1845 Tickets: tickets, 1846 }) 1847 } 1848 1849 func (s *WebServer) apiMixingStats(w http.ResponseWriter, r *http.Request) { 1850 var req struct { 1851 AssetID uint32 `json:"assetID"` 1852 } 1853 if !readPost(w, r, &req) { 1854 return 1855 } 1856 stats, err := s.core.FundsMixingStats(req.AssetID) 1857 if err != nil { 1858 s.writeAPIError(w, fmt.Errorf("error reteiving mixing stats for %d: %w", req.AssetID, err)) 1859 return 1860 } 1861 writeJSON(w, &struct { 1862 OK bool `json:"ok"` 1863 Stats *asset.FundsMixingStats `json:"stats"` 1864 }{ 1865 OK: true, 1866 Stats: stats, 1867 }) 1868 } 1869 1870 func (s *WebServer) apiConfigureMixer(w http.ResponseWriter, r *http.Request) { 1871 var req struct { 1872 AssetID uint32 `json:"assetID"` 1873 Enabled bool `json:"enabled"` 1874 } 1875 if !readPost(w, r, &req) { 1876 return 1877 } 1878 pass, err := s.resolvePass(nil, r) 1879 if err != nil { 1880 s.writeAPIError(w, fmt.Errorf("password error: %w", err)) 1881 return 1882 } 1883 defer zero(pass) 1884 if err := s.core.ConfigureFundsMixer(pass, req.AssetID, req.Enabled); err != nil { 1885 s.writeAPIError(w, fmt.Errorf("error configuring mixing for %d: %w", req.AssetID, err)) 1886 return 1887 } 1888 writeJSON(w, simpleAck()) 1889 } 1890 1891 func (s *WebServer) apiStartMarketMakingBot(w http.ResponseWriter, r *http.Request) { 1892 var form struct { 1893 Config *mm.StartConfig `json:"config"` 1894 AppPW encode.PassBytes `json:"appPW"` 1895 } 1896 defer form.AppPW.Clear() 1897 if !readPost(w, r, &form) { 1898 s.writeAPIError(w, fmt.Errorf("failed to read form")) 1899 return 1900 } 1901 appPW, err := s.resolvePass(form.AppPW, r) 1902 if err != nil { 1903 s.writeAPIError(w, fmt.Errorf("password error: %w", err)) 1904 return 1905 } 1906 defer zero(appPW) 1907 if form.Config == nil { 1908 s.writeAPIError(w, errors.New("config missing")) 1909 return 1910 } 1911 if err = s.mm.StartBot(form.Config, nil, appPW); err != nil { 1912 s.writeAPIError(w, fmt.Errorf("error starting market making: %v", err)) 1913 return 1914 } 1915 1916 writeJSON(w, simpleAck()) 1917 } 1918 1919 func (s *WebServer) apiStopMarketMakingBot(w http.ResponseWriter, r *http.Request) { 1920 var form struct { 1921 Market *mm.MarketWithHost `json:"market"` 1922 } 1923 if !readPost(w, r, &form) { 1924 s.writeAPIError(w, fmt.Errorf("failed to read form")) 1925 return 1926 } 1927 if form.Market == nil { 1928 s.writeAPIError(w, errors.New("market missing")) 1929 return 1930 } 1931 if err := s.mm.StopBot(form.Market); err != nil { 1932 s.writeAPIError(w, fmt.Errorf("error stopping mm bot %q: %v", form.Market, err)) 1933 return 1934 } 1935 writeJSON(w, simpleAck()) 1936 } 1937 1938 func (s *WebServer) apiUpdateCEXConfig(w http.ResponseWriter, r *http.Request) { 1939 var updatedCfg *mm.CEXConfig 1940 if !readPost(w, r, &updatedCfg) { 1941 s.writeAPIError(w, fmt.Errorf("failed to read config")) 1942 return 1943 } 1944 1945 if err := s.mm.UpdateCEXConfig(updatedCfg); err != nil { 1946 s.writeAPIError(w, err) 1947 return 1948 } 1949 1950 writeJSON(w, simpleAck()) 1951 } 1952 1953 func (s *WebServer) apiUpdateBotConfig(w http.ResponseWriter, r *http.Request) { 1954 var updatedCfg *mm.BotConfig 1955 if !readPost(w, r, &updatedCfg) { 1956 s.writeAPIError(w, fmt.Errorf("failed to read config")) 1957 return 1958 } 1959 1960 if err := s.mm.UpdateBotConfig(updatedCfg); err != nil { 1961 s.writeAPIError(w, err) 1962 return 1963 } 1964 1965 writeJSON(w, simpleAck()) 1966 } 1967 1968 func (s *WebServer) apiRemoveBotConfig(w http.ResponseWriter, r *http.Request) { 1969 var form struct { 1970 Host string `json:"host"` 1971 BaseID uint32 `json:"baseID"` 1972 QuoteID uint32 `json:"quoteID"` 1973 } 1974 if !readPost(w, r, &form) { 1975 s.writeAPIError(w, fmt.Errorf("failed to read form")) 1976 return 1977 } 1978 1979 if err := s.mm.RemoveBotConfig(form.Host, form.BaseID, form.QuoteID); err != nil { 1980 s.writeAPIError(w, err) 1981 return 1982 } 1983 1984 writeJSON(w, simpleAck()) 1985 } 1986 1987 func (s *WebServer) apiMarketMakingStatus(w http.ResponseWriter, r *http.Request) { 1988 writeJSON(w, &struct { 1989 OK bool `json:"ok"` 1990 Status *mm.Status `json:"status"` 1991 }{ 1992 OK: true, 1993 Status: s.mm.Status(), 1994 }) 1995 } 1996 1997 func (s *WebServer) apiTxHistory(w http.ResponseWriter, r *http.Request) { 1998 var form struct { 1999 AssetID uint32 `json:"assetID"` 2000 N int `json:"n"` 2001 RefID string `json:"refID"` 2002 Past bool `json:"past"` 2003 } 2004 if !readPost(w, r, &form) { 2005 return 2006 } 2007 2008 var refID *string 2009 if len(form.RefID) > 0 { 2010 refID = &form.RefID 2011 } 2012 2013 txs, err := s.core.TxHistory(form.AssetID, form.N, refID, form.Past) 2014 if err != nil { 2015 s.writeAPIError(w, fmt.Errorf("error getting transaction history: %w", err)) 2016 return 2017 } 2018 writeJSON(w, &struct { 2019 OK bool `json:"ok"` 2020 Txs []*asset.WalletTransaction `json:"txs"` 2021 }{ 2022 OK: true, 2023 Txs: txs, 2024 }) 2025 } 2026 2027 func (s *WebServer) apiTakeAction(w http.ResponseWriter, r *http.Request) { 2028 var req struct { 2029 AssetID uint32 `json:"assetID"` 2030 ActionID string `json:"actionID"` 2031 Action json.RawMessage `json:"action"` 2032 } 2033 if !readPost(w, r, &req) { 2034 return 2035 } 2036 if err := s.core.TakeAction(req.AssetID, req.ActionID, req.Action); err != nil { 2037 s.writeAPIError(w, fmt.Errorf("error taking action: %w", err)) 2038 return 2039 } 2040 writeJSON(w, simpleAck()) 2041 } 2042 2043 func (s *WebServer) redeemGameCode(w http.ResponseWriter, r *http.Request) { 2044 var form struct { 2045 Code dex.Bytes `json:"code"` 2046 Msg string `json:"msg"` 2047 AppPW encode.PassBytes `json:"appPW"` 2048 } 2049 if !readPost(w, r, &form) { 2050 return 2051 } 2052 defer form.AppPW.Clear() 2053 appPW, err := s.resolvePass(form.AppPW, r) 2054 if err != nil { 2055 s.writeAPIError(w, fmt.Errorf("password error: %w", err)) 2056 return 2057 } 2058 coinID, win, err := s.core.RedeemGeocode(appPW, form.Code, form.Msg) 2059 if err != nil { 2060 s.writeAPIError(w, fmt.Errorf("redemption error: %w", err)) 2061 return 2062 } 2063 const dcrBipID = 42 2064 coinIDString, _ := asset.DecodeCoinID(dcrBipID, coinID) 2065 writeJSON(w, &struct { 2066 OK bool `json:"ok"` 2067 CoinID dex.Bytes `json:"coinID"` 2068 CoinString string `json:"coinString"` 2069 Win uint64 `json:"win"` 2070 }{ 2071 OK: true, 2072 CoinID: coinID, 2073 CoinString: coinIDString, 2074 Win: win, 2075 }) 2076 } 2077 2078 // writeAPIError logs the formatted error and sends a standardResponse with the 2079 // error message. 2080 func (s *WebServer) writeAPIError(w http.ResponseWriter, err error) { 2081 var cErr *core.Error 2082 var code *int 2083 if errors.As(err, &cErr) { 2084 code = cErr.Code() 2085 } 2086 2087 innerErr := core.UnwrapErr(err) 2088 resp := &standardResponse{ 2089 OK: false, 2090 Msg: innerErr.Error(), 2091 Code: code, 2092 } 2093 log.Error(err.Error()) 2094 writeJSON(w, resp) 2095 } 2096 2097 // setCookie sets the value of a cookie in the http response. 2098 func setCookie(name, value string, w http.ResponseWriter) { 2099 http.SetCookie(w, &http.Cookie{ 2100 Name: name, 2101 Path: "/", 2102 Value: value, 2103 SameSite: http.SameSiteStrictMode, 2104 }) 2105 } 2106 2107 // clearCookie removes a cookie in the http response. 2108 func clearCookie(name string, w http.ResponseWriter) { 2109 http.SetCookie(w, &http.Cookie{ 2110 Name: name, 2111 Path: "/", 2112 Value: "", 2113 Expires: time.Unix(0, 0), 2114 SameSite: http.SameSiteStrictMode, 2115 }) 2116 } 2117 2118 // resolvePass returns the appPW if it has a value, but if not, it attempts 2119 // to retrieve the cached password using the information in cookies. 2120 func (s *WebServer) resolvePass(appPW []byte, r *http.Request) ([]byte, error) { 2121 if len(appPW) > 0 { 2122 return appPW, nil 2123 } 2124 cachedPass, err := s.getCachedPasswordUsingRequest(r) 2125 if err != nil { 2126 if errors.Is(err, errNoCachedPW) { 2127 return nil, fmt.Errorf("app pass cannot be empty") 2128 } 2129 return nil, fmt.Errorf("error retrieving cached pw: %w", err) 2130 } 2131 return cachedPass, nil 2132 }