decred.org/dcrdex@v1.0.5/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, []byte(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  // apiAddressUsed checks whether an address has been used.
   601  func (s *WebServer) apiAddressUsed(w http.ResponseWriter, r *http.Request) {
   602  	form := &struct {
   603  		AssetID *uint32 `json:"assetID"`
   604  		Addr    string  `json:"addr"`
   605  	}{}
   606  	if !readPost(w, r, form) {
   607  		return
   608  	}
   609  	if form.AssetID == nil {
   610  		s.writeAPIError(w, errors.New("missing asset ID"))
   611  		return
   612  	}
   613  	assetID := *form.AssetID
   614  
   615  	used, err := s.core.AddressUsed(assetID, form.Addr)
   616  	if err != nil {
   617  		s.writeAPIError(w, err)
   618  		return
   619  	}
   620  
   621  	writeJSON(w, &struct {
   622  		OK   bool `json:"ok"`
   623  		Used bool `json:"used"`
   624  	}{
   625  		OK:   true,
   626  		Used: used,
   627  	})
   628  }
   629  
   630  // apiConnectWallet is the handler for the '/connectwallet' API request.
   631  // Connects to a specified wallet, but does not unlock it.
   632  func (s *WebServer) apiConnectWallet(w http.ResponseWriter, r *http.Request) {
   633  	form := &struct {
   634  		AssetID uint32 `json:"assetID"`
   635  	}{}
   636  	if !readPost(w, r, form) {
   637  		return
   638  	}
   639  	err := s.core.ConnectWallet(form.AssetID)
   640  	if err != nil {
   641  		s.writeAPIError(w, fmt.Errorf("error connecting to %s wallet: %w", unbip(form.AssetID), err))
   642  		return
   643  	}
   644  
   645  	writeJSON(w, simpleAck())
   646  }
   647  
   648  // apiTrade is the handler for the '/trade' API request.
   649  func (s *WebServer) apiTrade(w http.ResponseWriter, r *http.Request) {
   650  	form := new(tradeForm)
   651  	defer form.Pass.Clear()
   652  	if !readPost(w, r, form) {
   653  		return
   654  	}
   655  	r.Close = true
   656  	pass, err := s.resolvePass(form.Pass, r)
   657  	if err != nil {
   658  		s.writeAPIError(w, fmt.Errorf("password error: %w", err))
   659  		return
   660  	}
   661  	defer zero(pass)
   662  	if form.Order == nil {
   663  		s.writeAPIError(w, errors.New("order missing"))
   664  		return
   665  	}
   666  	ord, err := s.core.Trade(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.Order `json:"order"`
   674  	}{
   675  		OK:    true,
   676  		Order: ord,
   677  	}
   678  	w.Header().Set("Connection", "close")
   679  	writeJSON(w, resp)
   680  }
   681  
   682  // apiTradeAsync is the handler for the '/tradeasync' API request.
   683  func (s *WebServer) apiTradeAsync(w http.ResponseWriter, r *http.Request) {
   684  	form := new(tradeForm)
   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  	ord, err := s.core.TradeAsync(pass, form.Order)
   697  	if err != nil {
   698  		s.writeAPIError(w, fmt.Errorf("error placing order: %w", err))
   699  		return
   700  	}
   701  	resp := &struct {
   702  		OK    bool                `json:"ok"`
   703  		Order *core.InFlightOrder `json:"order"`
   704  	}{
   705  		OK:    true,
   706  		Order: ord,
   707  	}
   708  	w.Header().Set("Connection", "close")
   709  	writeJSON(w, resp)
   710  }
   711  
   712  // apiAccountExport is the handler for the '/exportaccount' API request.
   713  func (s *WebServer) apiAccountExport(w http.ResponseWriter, r *http.Request) {
   714  	form := new(accountExportForm)
   715  	defer form.Pass.Clear()
   716  	if !readPost(w, r, form) {
   717  		return
   718  	}
   719  	r.Close = true
   720  	pass, err := s.resolvePass(form.Pass, r)
   721  	if err != nil {
   722  		s.writeAPIError(w, fmt.Errorf("password error: %w", err))
   723  		return
   724  	}
   725  	defer zero(pass)
   726  	account, bonds, err := s.core.AccountExport(pass, form.Host)
   727  	if err != nil {
   728  		s.writeAPIError(w, fmt.Errorf("error exporting account: %w", err))
   729  		return
   730  	}
   731  	if bonds == nil {
   732  		bonds = make([]*db.Bond, 0) // marshal to [], not null
   733  	}
   734  	w.Header().Set("Connection", "close")
   735  	res := &struct {
   736  		OK      bool          `json:"ok"`
   737  		Account *core.Account `json:"account"`
   738  		Bonds   []*db.Bond    `json:"bonds"`
   739  	}{
   740  		OK:      true,
   741  		Account: account,
   742  		Bonds:   bonds,
   743  	}
   744  	writeJSON(w, res)
   745  }
   746  
   747  // apiExportSeed is the handler for the '/exportseed' API request.
   748  func (s *WebServer) apiExportSeed(w http.ResponseWriter, r *http.Request) {
   749  	form := &struct {
   750  		Pass encode.PassBytes `json:"pass"`
   751  	}{}
   752  	defer form.Pass.Clear()
   753  	if !readPost(w, r, form) {
   754  		return
   755  	}
   756  	r.Close = true
   757  	seed, err := s.core.ExportSeed(form.Pass)
   758  	if err != nil {
   759  		s.writeAPIError(w, fmt.Errorf("error exporting seed: %w", err))
   760  		return
   761  	}
   762  	writeJSON(w, &struct {
   763  		OK   bool   `json:"ok"`
   764  		Seed string `json:"seed"`
   765  	}{
   766  		OK:   true,
   767  		Seed: seed,
   768  	})
   769  }
   770  
   771  // apiAccountImport is the handler for the '/importaccount' API request.
   772  func (s *WebServer) apiAccountImport(w http.ResponseWriter, r *http.Request) {
   773  	form := new(accountImportForm)
   774  	defer form.Pass.Clear()
   775  	if !readPost(w, r, form) {
   776  		return
   777  	}
   778  	r.Close = true
   779  	pass, err := s.resolvePass(form.Pass, r)
   780  	if err != nil {
   781  		s.writeAPIError(w, fmt.Errorf("password error: %w", err))
   782  		return
   783  	}
   784  	defer zero(pass)
   785  	if form.Account == nil {
   786  		s.writeAPIError(w, errors.New("account missing"))
   787  		return
   788  	}
   789  	err = s.core.AccountImport(pass, form.Account, form.Bonds)
   790  	if err != nil {
   791  		s.writeAPIError(w, fmt.Errorf("error importing account: %w", err))
   792  		return
   793  	}
   794  	w.Header().Set("Connection", "close")
   795  	writeJSON(w, simpleAck())
   796  }
   797  
   798  func (s *WebServer) apiUpdateCert(w http.ResponseWriter, r *http.Request) {
   799  	form := &struct {
   800  		Host string `json:"host"`
   801  		Cert string `json:"cert"`
   802  	}{}
   803  	if !readPost(w, r, form) {
   804  		return
   805  	}
   806  
   807  	err := s.core.UpdateCert(form.Host, []byte(form.Cert))
   808  	if err != nil {
   809  		s.writeAPIError(w, fmt.Errorf("error updating cert: %w", err))
   810  		return
   811  	}
   812  
   813  	writeJSON(w, simpleAck())
   814  }
   815  
   816  func (s *WebServer) apiUpdateDEXHost(w http.ResponseWriter, r *http.Request) {
   817  	form := &struct {
   818  		Pass    encode.PassBytes `json:"pw"`
   819  		OldHost string           `json:"oldHost"`
   820  		NewHost string           `json:"newHost"`
   821  		Cert    string           `json:"cert"`
   822  	}{}
   823  	defer form.Pass.Clear()
   824  	if !readPost(w, r, form) {
   825  		return
   826  	}
   827  	pass, err := s.resolvePass(form.Pass, r)
   828  	if err != nil {
   829  		s.writeAPIError(w, fmt.Errorf("password error: %w", err))
   830  		return
   831  	}
   832  	defer zero(pass)
   833  	cert := []byte(form.Cert)
   834  	exchange, err := s.core.UpdateDEXHost(form.OldHost, form.NewHost, pass, cert)
   835  	if err != nil {
   836  		s.writeAPIError(w, fmt.Errorf("error updating host: %w", err))
   837  		return
   838  	}
   839  
   840  	resp := struct {
   841  		OK       bool           `json:"ok"`
   842  		Exchange *core.Exchange `json:"xc,omitempty"`
   843  	}{
   844  		OK:       true,
   845  		Exchange: exchange,
   846  	}
   847  
   848  	writeJSON(w, resp)
   849  }
   850  
   851  // apiRestoreWalletInfo is the handler for the '/restorewalletinfo' API
   852  // request.
   853  func (s *WebServer) apiRestoreWalletInfo(w http.ResponseWriter, r *http.Request) {
   854  	form := &struct {
   855  		AssetID uint32
   856  		Pass    encode.PassBytes
   857  	}{}
   858  	defer form.Pass.Clear()
   859  	if !readPost(w, r, form) {
   860  		return
   861  	}
   862  
   863  	info, err := s.core.WalletRestorationInfo(form.Pass, form.AssetID)
   864  	if err != nil {
   865  		s.writeAPIError(w, fmt.Errorf("error updating cert: %w", err))
   866  		return
   867  	}
   868  
   869  	resp := struct {
   870  		OK              bool                       `json:"ok"`
   871  		RestorationInfo []*asset.WalletRestoration `json:"restorationinfo,omitempty"`
   872  	}{
   873  		OK:              true,
   874  		RestorationInfo: info,
   875  	}
   876  	writeJSON(w, resp)
   877  }
   878  
   879  // apiToggleAccountStatus is the handler for the '/toggleaccountstatus' API request.
   880  func (s *WebServer) apiToggleAccountStatus(w http.ResponseWriter, r *http.Request) {
   881  	form := new(updateAccountStatusForm)
   882  	defer form.Pass.Clear()
   883  	if !readPost(w, r, form) {
   884  		return
   885  	}
   886  	defer form.Pass.Clear()
   887  	appPW, err := s.resolvePass(form.Pass, r)
   888  	if err != nil {
   889  		s.writeAPIError(w, fmt.Errorf("password error: %w", err))
   890  		return
   891  	}
   892  	// Disable account.
   893  	err = s.core.ToggleAccountStatus(appPW, form.Host, form.Disable)
   894  	if err != nil {
   895  		s.writeAPIError(w, fmt.Errorf("error updating account status: %w", err))
   896  		return
   897  	}
   898  	if form.Disable {
   899  		w.Header().Set("Connection", "close")
   900  	}
   901  	writeJSON(w, simpleAck())
   902  }
   903  
   904  // apiCancel is the handler for the '/cancel' API request.
   905  func (s *WebServer) apiCancel(w http.ResponseWriter, r *http.Request) {
   906  	form := new(cancelForm)
   907  	if !readPost(w, r, form) {
   908  		return
   909  	}
   910  	err := s.core.Cancel(form.OrderID)
   911  	if err != nil {
   912  		s.writeAPIError(w, fmt.Errorf("error cancelling order %s: %w", form.OrderID, err))
   913  		return
   914  	}
   915  	writeJSON(w, simpleAck())
   916  }
   917  
   918  // apiCloseWallet is the handler for the '/closewallet' API request.
   919  func (s *WebServer) apiCloseWallet(w http.ResponseWriter, r *http.Request) {
   920  	form := &struct {
   921  		AssetID uint32 `json:"assetID"`
   922  	}{}
   923  	if !readPost(w, r, form) {
   924  		return
   925  	}
   926  	err := s.core.CloseWallet(form.AssetID)
   927  	if err != nil {
   928  		s.writeAPIError(w, fmt.Errorf("error locking %s wallet: %w", unbip(form.AssetID), err))
   929  		return
   930  	}
   931  
   932  	writeJSON(w, simpleAck())
   933  }
   934  
   935  // apiInit is the handler for the '/init' API request.
   936  func (s *WebServer) apiInit(w http.ResponseWriter, r *http.Request) {
   937  	var init struct {
   938  		Pass encode.PassBytes `json:"pass"`
   939  		Seed string           `json:"seed,omitempty"`
   940  	}
   941  	defer init.Pass.Clear()
   942  	if !readPost(w, r, &init) {
   943  		return
   944  	}
   945  	var seed *string
   946  	if len(init.Seed) > 0 {
   947  		seed = &init.Seed
   948  	}
   949  	mnemonicSeed, err := s.core.InitializeClient(init.Pass, seed)
   950  	if err != nil {
   951  		s.writeAPIError(w, fmt.Errorf("initialization error: %w", err))
   952  		return
   953  	}
   954  	err = s.actuallyLogin(w, r, &loginForm{Pass: init.Pass})
   955  	if err != nil {
   956  		s.writeAPIError(w, err)
   957  		return
   958  	}
   959  
   960  	writeJSON(w, struct {
   961  		OK           bool     `json:"ok"`
   962  		Hosts        []string `json:"hosts"`
   963  		MnemonicSeed string   `json:"mnemonic"`
   964  	}{
   965  		OK:           true,
   966  		Hosts:        s.knownUnregisteredExchanges(map[string]*core.Exchange{}),
   967  		MnemonicSeed: mnemonicSeed,
   968  	})
   969  }
   970  
   971  // apiIsInitialized is the handler for the '/isinitialized' request.
   972  func (s *WebServer) apiIsInitialized(w http.ResponseWriter, r *http.Request) {
   973  	writeJSON(w, &struct {
   974  		OK          bool `json:"ok"`
   975  		Initialized bool `json:"initialized"`
   976  	}{
   977  		OK:          true,
   978  		Initialized: s.core.IsInitialized(),
   979  	})
   980  }
   981  
   982  func (s *WebServer) apiLocale(w http.ResponseWriter, r *http.Request) {
   983  	var lang string
   984  	if !readPost(w, r, &lang) {
   985  		return
   986  	}
   987  	m, found := localesMap[lang]
   988  	if !found {
   989  		s.writeAPIError(w, fmt.Errorf("no locale for language %q", lang))
   990  		return
   991  	}
   992  	resp := make(map[string]string)
   993  	for translationID, defaultTranslation := range enUS {
   994  		t, found := m[translationID]
   995  		if !found {
   996  			t = defaultTranslation
   997  		}
   998  		resp[translationID] = t.T
   999  	}
  1000  
  1001  	writeJSON(w, resp)
  1002  }
  1003  
  1004  func (s *WebServer) apiSetLocale(w http.ResponseWriter, r *http.Request) {
  1005  	var lang string
  1006  	if !readPost(w, r, &lang) {
  1007  		return
  1008  	}
  1009  	if err := s.core.SetLanguage(lang); err != nil {
  1010  		s.writeAPIError(w, err)
  1011  		return
  1012  	}
  1013  
  1014  	// Get actual language after SetLanguage (in case of fallback)
  1015  	actualLang := s.core.Language()
  1016  	s.lang.Store(actualLang)
  1017  	if err := s.buildTemplates(actualLang); err != nil {
  1018  		s.writeAPIError(w, err)
  1019  		return
  1020  	}
  1021  
  1022  	writeJSON(w, simpleAck())
  1023  }
  1024  
  1025  // apiBuildInfo is the handler for the '/buildinfo' API request.
  1026  func (s *WebServer) apiBuildInfo(w http.ResponseWriter, r *http.Request) {
  1027  	resp := buildInfoResponse{
  1028  		OK:       true,
  1029  		Version:  s.appVersion,
  1030  		Revision: commitHash,
  1031  	}
  1032  	writeJSON(w, resp)
  1033  }
  1034  
  1035  // apiLogin handles the 'login' API request.
  1036  func (s *WebServer) apiLogin(w http.ResponseWriter, r *http.Request) {
  1037  	login := new(loginForm)
  1038  	defer login.Pass.Clear()
  1039  	if !readPost(w, r, login) {
  1040  		return
  1041  	}
  1042  
  1043  	err := s.actuallyLogin(w, r, login)
  1044  	if err != nil {
  1045  		s.writeAPIError(w, err)
  1046  		return
  1047  	}
  1048  
  1049  	notes, pokes, err := s.core.Notifications(100)
  1050  	if err != nil {
  1051  		log.Errorf("failed to get notifications: %v", err)
  1052  	}
  1053  
  1054  	writeJSON(w, &struct {
  1055  		OK    bool               `json:"ok"`
  1056  		Notes []*db.Notification `json:"notes"`
  1057  		Pokes []*db.Notification `json:"pokes"`
  1058  	}{
  1059  		OK:    true,
  1060  		Notes: notes,
  1061  		Pokes: pokes,
  1062  	})
  1063  }
  1064  
  1065  func (s *WebServer) apiNotes(w http.ResponseWriter, r *http.Request) {
  1066  	notes, pokes, err := s.core.Notifications(100)
  1067  	if err != nil {
  1068  		s.writeAPIError(w, fmt.Errorf("failed to get notifications: %w", err))
  1069  		return
  1070  	}
  1071  
  1072  	writeJSON(w, &struct {
  1073  		OK    bool               `json:"ok"`
  1074  		Notes []*db.Notification `json:"notes"`
  1075  		Pokes []*db.Notification `json:"pokes"`
  1076  	}{
  1077  		OK:    true,
  1078  		Notes: notes,
  1079  		Pokes: pokes,
  1080  	})
  1081  }
  1082  
  1083  // apiLogout handles the 'logout' API request.
  1084  func (s *WebServer) apiLogout(w http.ResponseWriter, r *http.Request) {
  1085  	err := s.core.Logout()
  1086  	if err != nil {
  1087  		s.writeAPIError(w, fmt.Errorf("logout error: %w", err))
  1088  		return
  1089  	}
  1090  
  1091  	// With Core locked up, invalidate all known auth tokens and cached passwords
  1092  	// to force any other sessions to login again.
  1093  	s.deauth()
  1094  
  1095  	clearCookie(authCK, w)
  1096  	clearCookie(pwKeyCK, w)
  1097  
  1098  	response := struct {
  1099  		OK bool `json:"ok"`
  1100  	}{
  1101  		OK: true,
  1102  	}
  1103  	writeJSON(w, response)
  1104  }
  1105  
  1106  // apiGetBalance handles the 'balance' API request.
  1107  func (s *WebServer) apiGetBalance(w http.ResponseWriter, r *http.Request) {
  1108  	form := &struct {
  1109  		AssetID uint32 `json:"assetID"`
  1110  	}{}
  1111  	if !readPost(w, r, form) {
  1112  		return
  1113  	}
  1114  	bal, err := s.core.AssetBalance(form.AssetID)
  1115  	if err != nil {
  1116  		s.writeAPIError(w, fmt.Errorf("balance error: %w", err))
  1117  		return
  1118  	}
  1119  	resp := &struct {
  1120  		OK      bool                `json:"ok"`
  1121  		Balance *core.WalletBalance `json:"balance"`
  1122  	}{
  1123  		OK:      true,
  1124  		Balance: bal,
  1125  	}
  1126  	writeJSON(w, resp)
  1127  
  1128  }
  1129  
  1130  // apiParseConfig parses an INI config file into a map[string]string.
  1131  func (s *WebServer) apiParseConfig(w http.ResponseWriter, r *http.Request) {
  1132  	form := &struct {
  1133  		ConfigText string `json:"configtext"`
  1134  	}{}
  1135  	if !readPost(w, r, form) {
  1136  		return
  1137  	}
  1138  	configMap, err := config.Parse([]byte(form.ConfigText))
  1139  	if err != nil {
  1140  		s.writeAPIError(w, fmt.Errorf("parse error: %w", err))
  1141  		return
  1142  	}
  1143  	resp := &struct {
  1144  		OK  bool              `json:"ok"`
  1145  		Map map[string]string `json:"map"`
  1146  	}{
  1147  		OK:  true,
  1148  		Map: configMap,
  1149  	}
  1150  	writeJSON(w, resp)
  1151  }
  1152  
  1153  // apiWalletSettings fetches the currently stored wallet configuration settings.
  1154  func (s *WebServer) apiWalletSettings(w http.ResponseWriter, r *http.Request) {
  1155  	form := &struct {
  1156  		AssetID uint32 `json:"assetID"`
  1157  	}{}
  1158  	if !readPost(w, r, form) {
  1159  		return
  1160  	}
  1161  	settings, err := s.core.WalletSettings(form.AssetID)
  1162  	if err != nil {
  1163  		s.writeAPIError(w, fmt.Errorf("error setting wallet settings: %w", err))
  1164  		return
  1165  	}
  1166  	writeJSON(w, &struct {
  1167  		OK  bool              `json:"ok"`
  1168  		Map map[string]string `json:"map"`
  1169  	}{
  1170  		OK:  true,
  1171  		Map: settings,
  1172  	})
  1173  }
  1174  
  1175  // apiToggleWalletStatus updates the wallet's status.
  1176  func (s *WebServer) apiToggleWalletStatus(w http.ResponseWriter, r *http.Request) {
  1177  	form := new(walletStatusForm)
  1178  	if !readPost(w, r, form) {
  1179  		return
  1180  	}
  1181  	err := s.core.ToggleWalletStatus(form.AssetID, form.Disable)
  1182  	if err != nil {
  1183  		s.writeAPIError(w, fmt.Errorf("error setting wallet settings: %w", err))
  1184  		return
  1185  	}
  1186  	response := struct {
  1187  		OK bool `json:"ok"`
  1188  	}{
  1189  		OK: true,
  1190  	}
  1191  	writeJSON(w, response)
  1192  }
  1193  
  1194  // apiDefaultWalletCfg attempts to load configuration settings from the
  1195  // asset's default path on the server.
  1196  func (s *WebServer) apiDefaultWalletCfg(w http.ResponseWriter, r *http.Request) {
  1197  	form := &struct {
  1198  		AssetID uint32 `json:"assetID"`
  1199  		Type    string `json:"type"`
  1200  	}{}
  1201  	if !readPost(w, r, form) {
  1202  		return
  1203  	}
  1204  	cfg, err := s.core.AutoWalletConfig(form.AssetID, form.Type)
  1205  	if err != nil {
  1206  		s.writeAPIError(w, fmt.Errorf("error getting wallet config: %w", err))
  1207  		return
  1208  	}
  1209  	writeJSON(w, struct {
  1210  		OK     bool              `json:"ok"`
  1211  		Config map[string]string `json:"config"`
  1212  	}{
  1213  		OK:     true,
  1214  		Config: cfg,
  1215  	})
  1216  }
  1217  
  1218  // apiOrders responds with a filtered list of user orders.
  1219  func (s *WebServer) apiOrders(w http.ResponseWriter, r *http.Request) {
  1220  	filter := new(core.OrderFilter)
  1221  	if !readPost(w, r, filter) {
  1222  		return
  1223  	}
  1224  
  1225  	ords, err := s.core.Orders(filter)
  1226  	if err != nil {
  1227  		s.writeAPIError(w, fmt.Errorf("Orders error: %w", err))
  1228  		return
  1229  	}
  1230  	writeJSON(w, &struct {
  1231  		OK     bool          `json:"ok"`
  1232  		Orders []*core.Order `json:"orders"`
  1233  	}{
  1234  		OK:     true,
  1235  		Orders: ords,
  1236  	})
  1237  }
  1238  
  1239  // apiAccelerateOrder speeds up the mining of transactions in an order.
  1240  func (s *WebServer) apiAccelerateOrder(w http.ResponseWriter, r *http.Request) {
  1241  	form := struct {
  1242  		Pass    encode.PassBytes `json:"pw"`
  1243  		OrderID dex.Bytes        `json:"orderID"`
  1244  		NewRate uint64           `json:"newRate"`
  1245  	}{}
  1246  	defer form.Pass.Clear()
  1247  	if !readPost(w, r, &form) {
  1248  		return
  1249  	}
  1250  	pass, err := s.resolvePass(form.Pass, r)
  1251  	if err != nil {
  1252  		s.writeAPIError(w, fmt.Errorf("password error: %w", err))
  1253  		return
  1254  	}
  1255  
  1256  	txID, err := s.core.AccelerateOrder(pass, form.OrderID, form.NewRate)
  1257  	if err != nil {
  1258  		s.writeAPIError(w, fmt.Errorf("Accelerate Order error: %w", err))
  1259  		return
  1260  	}
  1261  
  1262  	writeJSON(w, &struct {
  1263  		OK   bool   `json:"ok"`
  1264  		TxID string `json:"txID"`
  1265  	}{
  1266  		OK:   true,
  1267  		TxID: txID,
  1268  	})
  1269  }
  1270  
  1271  // apiPreAccelerate responds with information about accelerating the mining of
  1272  // swaps in an order
  1273  func (s *WebServer) apiPreAccelerate(w http.ResponseWriter, r *http.Request) {
  1274  	var oid dex.Bytes
  1275  	if !readPost(w, r, &oid) {
  1276  		return
  1277  	}
  1278  
  1279  	preAccelerate, err := s.core.PreAccelerateOrder(oid)
  1280  	if err != nil {
  1281  		s.writeAPIError(w, fmt.Errorf("Pre accelerate error: %w", err))
  1282  		return
  1283  	}
  1284  
  1285  	writeJSON(w, &struct {
  1286  		OK            bool                `json:"ok"`
  1287  		PreAccelerate *core.PreAccelerate `json:"preAccelerate"`
  1288  	}{
  1289  		OK:            true,
  1290  		PreAccelerate: preAccelerate,
  1291  	})
  1292  }
  1293  
  1294  // apiAccelerationEstimate responds with how much it would cost to accelerate
  1295  // an order to the requested fee rate.
  1296  func (s *WebServer) apiAccelerationEstimate(w http.ResponseWriter, r *http.Request) {
  1297  	form := struct {
  1298  		OrderID dex.Bytes `json:"orderID"`
  1299  		NewRate uint64    `json:"newRate"`
  1300  	}{}
  1301  
  1302  	if !readPost(w, r, &form) {
  1303  		return
  1304  	}
  1305  
  1306  	fee, err := s.core.AccelerationEstimate(form.OrderID, form.NewRate)
  1307  	if err != nil {
  1308  		s.writeAPIError(w, fmt.Errorf("Accelerate Order error: %w", err))
  1309  		return
  1310  	}
  1311  
  1312  	writeJSON(w, &struct {
  1313  		OK  bool   `json:"ok"`
  1314  		Fee uint64 `json:"fee"`
  1315  	}{
  1316  		OK:  true,
  1317  		Fee: fee,
  1318  	})
  1319  }
  1320  
  1321  // apiOrder responds with data for an order.
  1322  func (s *WebServer) apiOrder(w http.ResponseWriter, r *http.Request) {
  1323  	var oid dex.Bytes
  1324  	if !readPost(w, r, &oid) {
  1325  		return
  1326  	}
  1327  
  1328  	ord, err := s.core.Order(oid)
  1329  	if err != nil {
  1330  		s.writeAPIError(w, fmt.Errorf("Order error: %w", err))
  1331  		return
  1332  	}
  1333  	writeJSON(w, &struct {
  1334  		OK    bool        `json:"ok"`
  1335  		Order *core.Order `json:"order"`
  1336  	}{
  1337  		OK:    true,
  1338  		Order: ord,
  1339  	})
  1340  }
  1341  
  1342  // apiChangeAppPass updates the application password.
  1343  func (s *WebServer) apiChangeAppPass(w http.ResponseWriter, r *http.Request) {
  1344  	form := &struct {
  1345  		AppPW    encode.PassBytes `json:"appPW"`
  1346  		NewAppPW encode.PassBytes `json:"newAppPW"`
  1347  	}{}
  1348  	defer form.AppPW.Clear()
  1349  	defer form.NewAppPW.Clear()
  1350  	if !readPost(w, r, form) {
  1351  		return
  1352  	}
  1353  
  1354  	// Update application password.
  1355  	err := s.core.ChangeAppPass(form.AppPW, form.NewAppPW)
  1356  	if err != nil {
  1357  		s.writeAPIError(w, fmt.Errorf("change app pass error: %w", err))
  1358  		return
  1359  	}
  1360  
  1361  	passwordIsCached := s.isPasswordCached(r)
  1362  	// Since the user changed the password, we clear all of the auth tokens
  1363  	// and cached passwords. However, we assign a new auth token and cache
  1364  	// the new password (if it was previously cached) for this session.
  1365  	s.deauth()
  1366  	authToken := s.authorize()
  1367  	setCookie(authCK, authToken, w)
  1368  	if passwordIsCached {
  1369  		key, err := s.cacheAppPassword(form.NewAppPW, authToken)
  1370  		if err != nil {
  1371  			log.Errorf("unable to cache password: %w", err)
  1372  			clearCookie(pwKeyCK, w)
  1373  		} else {
  1374  			setCookie(pwKeyCK, hex.EncodeToString(key), w)
  1375  			zero(key)
  1376  		}
  1377  	}
  1378  
  1379  	writeJSON(w, simpleAck())
  1380  }
  1381  
  1382  // apiResetAppPassword resets the application password.
  1383  func (s *WebServer) apiResetAppPassword(w http.ResponseWriter, r *http.Request) {
  1384  	form := new(struct {
  1385  		NewPass encode.PassBytes `json:"newPass"`
  1386  		Seed    string           `json:"seed"`
  1387  	})
  1388  	defer form.NewPass.Clear()
  1389  	if !readPost(w, r, form) {
  1390  		return
  1391  	}
  1392  
  1393  	err := s.core.ResetAppPass(form.NewPass, form.Seed)
  1394  	if err != nil {
  1395  		s.writeAPIError(w, err)
  1396  		return
  1397  	}
  1398  
  1399  	writeJSON(w, simpleAck())
  1400  }
  1401  
  1402  // apiReconfig sets new configuration details for the wallet.
  1403  func (s *WebServer) apiReconfig(w http.ResponseWriter, r *http.Request) {
  1404  	form := &struct {
  1405  		AssetID    uint32            `json:"assetID"`
  1406  		WalletType string            `json:"walletType"`
  1407  		Config     map[string]string `json:"config"`
  1408  		// newWalletPW json field should be omitted in case caller isn't interested
  1409  		// in setting new password, passing null JSON value will cause an unmarshal
  1410  		// error.
  1411  		NewWalletPW encode.PassBytes `json:"newWalletPW"`
  1412  		AppPW       encode.PassBytes `json:"appPW"`
  1413  	}{}
  1414  	defer form.NewWalletPW.Clear()
  1415  	defer form.AppPW.Clear()
  1416  	if !readPost(w, r, form) {
  1417  		return
  1418  	}
  1419  	pass, err := s.resolvePass(form.AppPW, r)
  1420  	if err != nil {
  1421  		s.writeAPIError(w, fmt.Errorf("password error: %w", err))
  1422  		return
  1423  	}
  1424  	defer zero(pass)
  1425  	// Update wallet settings.
  1426  	err = s.core.ReconfigureWallet(pass, form.NewWalletPW, &core.WalletForm{
  1427  		AssetID: form.AssetID,
  1428  		Config:  form.Config,
  1429  		Type:    form.WalletType,
  1430  	})
  1431  	if err != nil {
  1432  		s.writeAPIError(w, fmt.Errorf("reconfig error: %w", err))
  1433  		return
  1434  	}
  1435  
  1436  	writeJSON(w, simpleAck())
  1437  }
  1438  
  1439  // apiSend handles the 'send' API request.
  1440  func (s *WebServer) apiSend(w http.ResponseWriter, r *http.Request) {
  1441  	form := new(sendForm)
  1442  	defer form.Pass.Clear()
  1443  	if !readPost(w, r, form) {
  1444  		return
  1445  	}
  1446  	state := s.core.WalletState(form.AssetID)
  1447  	if state == nil {
  1448  		s.writeAPIError(w, fmt.Errorf("no wallet found for %s", unbip(form.AssetID)))
  1449  		return
  1450  	}
  1451  	if len(form.Pass) == 0 {
  1452  		s.writeAPIError(w, fmt.Errorf("empty password"))
  1453  		return
  1454  	}
  1455  	coin, err := s.core.Send(form.Pass, form.AssetID, form.Value, form.Address, form.Subtract)
  1456  	if err != nil {
  1457  		s.writeAPIError(w, fmt.Errorf("send/withdraw error: %w", err))
  1458  		return
  1459  	}
  1460  	resp := struct {
  1461  		OK   bool   `json:"ok"`
  1462  		Coin string `json:"coin"`
  1463  	}{
  1464  		OK:   true,
  1465  		Coin: coin.String(),
  1466  	}
  1467  	writeJSON(w, resp)
  1468  }
  1469  
  1470  // apiMaxBuy handles the 'maxbuy' API request.
  1471  func (s *WebServer) apiMaxBuy(w http.ResponseWriter, r *http.Request) {
  1472  	form := &struct {
  1473  		Host  string `json:"host"`
  1474  		Base  uint32 `json:"base"`
  1475  		Quote uint32 `json:"quote"`
  1476  		Rate  uint64 `json:"rate"`
  1477  	}{}
  1478  	if !readPost(w, r, form) {
  1479  		return
  1480  	}
  1481  	maxBuy, err := s.core.MaxBuy(form.Host, form.Base, form.Quote, form.Rate)
  1482  	if err != nil {
  1483  		s.writeAPIError(w, fmt.Errorf("max order estimation error: %w", err))
  1484  		return
  1485  	}
  1486  	resp := struct {
  1487  		OK     bool                   `json:"ok"`
  1488  		MaxBuy *core.MaxOrderEstimate `json:"maxBuy"`
  1489  	}{
  1490  		OK:     true,
  1491  		MaxBuy: maxBuy,
  1492  	}
  1493  	writeJSON(w, resp)
  1494  }
  1495  
  1496  // apiMaxSell handles the 'maxsell' API request.
  1497  func (s *WebServer) apiMaxSell(w http.ResponseWriter, r *http.Request) {
  1498  	form := &struct {
  1499  		Host  string `json:"host"`
  1500  		Base  uint32 `json:"base"`
  1501  		Quote uint32 `json:"quote"`
  1502  	}{}
  1503  	if !readPost(w, r, form) {
  1504  		return
  1505  	}
  1506  	maxSell, err := s.core.MaxSell(form.Host, form.Base, form.Quote)
  1507  	if err != nil {
  1508  		s.writeAPIError(w, fmt.Errorf("max order estimation error: %w", err))
  1509  		return
  1510  	}
  1511  	resp := struct {
  1512  		OK      bool                   `json:"ok"`
  1513  		MaxSell *core.MaxOrderEstimate `json:"maxSell"`
  1514  	}{
  1515  		OK:      true,
  1516  		MaxSell: maxSell,
  1517  	}
  1518  	writeJSON(w, resp)
  1519  }
  1520  
  1521  // apiPreOrder handles the 'preorder' API request.
  1522  func (s *WebServer) apiPreOrder(w http.ResponseWriter, r *http.Request) {
  1523  	form := new(core.TradeForm)
  1524  	if !readPost(w, r, form) {
  1525  		return
  1526  	}
  1527  
  1528  	est, err := s.core.PreOrder(form)
  1529  	if err != nil {
  1530  		s.writeAPIError(w, err)
  1531  		return
  1532  	}
  1533  
  1534  	resp := struct {
  1535  		OK       bool                `json:"ok"`
  1536  		Estimate *core.OrderEstimate `json:"estimate"`
  1537  	}{
  1538  		OK:       true,
  1539  		Estimate: est,
  1540  	}
  1541  
  1542  	writeJSON(w, resp)
  1543  }
  1544  
  1545  // apiActuallyLogin logs the user in. login form private data is expected to be
  1546  // cleared by the caller.
  1547  func (s *WebServer) actuallyLogin(w http.ResponseWriter, r *http.Request, login *loginForm) error {
  1548  	pass, err := s.resolvePass(login.Pass, r)
  1549  	defer zero(pass)
  1550  	if err != nil {
  1551  		return fmt.Errorf("password error: %w", err)
  1552  	}
  1553  	err = s.core.Login(pass)
  1554  	if err != nil {
  1555  		return fmt.Errorf("login error: %w", err)
  1556  	}
  1557  
  1558  	if !s.isAuthed(r) {
  1559  		authToken := s.authorize()
  1560  		setCookie(authCK, authToken, w)
  1561  		key, err := s.cacheAppPassword(pass, authToken)
  1562  		if err != nil {
  1563  			return fmt.Errorf("login error: %w", err)
  1564  
  1565  		}
  1566  		setCookie(pwKeyCK, hex.EncodeToString(key), w)
  1567  		zero(key)
  1568  	}
  1569  
  1570  	return nil
  1571  }
  1572  
  1573  // apiUser handles the 'user' API request.
  1574  func (s *WebServer) apiUser(w http.ResponseWriter, r *http.Request) {
  1575  	var u *core.User
  1576  	if s.isAuthed(r) {
  1577  		u = s.core.User()
  1578  	}
  1579  
  1580  	var mmStatus *mm.Status
  1581  	if s.mm != nil {
  1582  		mmStatus = s.mm.Status()
  1583  	}
  1584  
  1585  	response := struct {
  1586  		User     *core.User `json:"user"`
  1587  		Lang     string     `json:"lang"`
  1588  		Langs    []string   `json:"langs"`
  1589  		Inited   bool       `json:"inited"`
  1590  		OK       bool       `json:"ok"`
  1591  		MMStatus *mm.Status `json:"mmStatus"`
  1592  	}{
  1593  		User:     u,
  1594  		Lang:     s.lang.Load().(string),
  1595  		Langs:    s.langs,
  1596  		Inited:   s.core.IsInitialized(),
  1597  		OK:       true,
  1598  		MMStatus: mmStatus,
  1599  	}
  1600  	writeJSON(w, response)
  1601  }
  1602  
  1603  // apiToggleRateSource handles the /toggleratesource API request.
  1604  func (s *WebServer) apiToggleRateSource(w http.ResponseWriter, r *http.Request) {
  1605  	form := &struct {
  1606  		Disable bool   `json:"disable"`
  1607  		Source  string `json:"source"`
  1608  	}{}
  1609  	if !readPost(w, r, form) {
  1610  		return
  1611  	}
  1612  	err := s.core.ToggleRateSourceStatus(form.Source, form.Disable)
  1613  	if err != nil {
  1614  		s.writeAPIError(w, fmt.Errorf("error disabling/enabling rate source: %w", err))
  1615  		return
  1616  	}
  1617  	writeJSON(w, simpleAck())
  1618  }
  1619  
  1620  // apiDeleteArchiveRecords handles the '/deletearchivedrecords' API request.
  1621  func (s *WebServer) apiDeleteArchivedRecords(w http.ResponseWriter, r *http.Request) {
  1622  	form := new(deleteRecordsForm)
  1623  	if !readPost(w, r, form) {
  1624  		return
  1625  	}
  1626  
  1627  	var olderThan *time.Time
  1628  	if form.OlderThanMs > 0 {
  1629  		ot := time.UnixMilli(form.OlderThanMs)
  1630  		olderThan = &ot
  1631  	}
  1632  
  1633  	archivedRecordsPath, nRecordsDeleted, err := s.core.DeleteArchivedRecordsWithBackup(olderThan, form.SaveMatchesToFile, form.SaveOrdersToFile)
  1634  	if err != nil {
  1635  		s.writeAPIError(w, fmt.Errorf("error deleting archived records: %w", err))
  1636  		return
  1637  	}
  1638  	resp := &struct {
  1639  		Ok                     bool   `json:"ok"`
  1640  		ArchivedRecordsDeleted int    `json:"archivedRecordsDeleted"`
  1641  		ArchivedRecordsPath    string `json:"archivedRecordsPath"`
  1642  	}{
  1643  		Ok:                     true,
  1644  		ArchivedRecordsDeleted: nRecordsDeleted,
  1645  		ArchivedRecordsPath:    archivedRecordsPath,
  1646  	}
  1647  	writeJSON(w, resp)
  1648  }
  1649  
  1650  func (s *WebServer) apiMarketReport(w http.ResponseWriter, r *http.Request) {
  1651  	form := &struct {
  1652  		BaseID  uint32 `json:"baseID"`
  1653  		QuoteID uint32 `json:"quoteID"`
  1654  		Host    string `json:"host"`
  1655  	}{}
  1656  	if !readPost(w, r, form) {
  1657  		return
  1658  	}
  1659  	report, err := s.mm.MarketReport(form.Host, form.BaseID, form.QuoteID)
  1660  	if err != nil {
  1661  		s.writeAPIError(w, fmt.Errorf("error getting market report: %w", err))
  1662  		return
  1663  	}
  1664  	writeJSON(w, &struct {
  1665  		OK     bool             `json:"ok"`
  1666  		Report *mm.MarketReport `json:"report"`
  1667  	}{
  1668  		OK:     true,
  1669  		Report: report,
  1670  	})
  1671  }
  1672  
  1673  func (s *WebServer) apiCEXBalance(w http.ResponseWriter, r *http.Request) {
  1674  	var req struct {
  1675  		CEXName string `json:"cexName"`
  1676  		AssetID uint32 `json:"assetID"`
  1677  	}
  1678  	if !readPost(w, r, &req) {
  1679  		return
  1680  	}
  1681  	bal, err := s.mm.CEXBalance(req.CEXName, req.AssetID)
  1682  	if err != nil {
  1683  		s.writeAPIError(w, fmt.Errorf("error getting cex balance: %w", err))
  1684  		return
  1685  	}
  1686  	writeJSON(w, &struct {
  1687  		OK         bool                   `json:"ok"`
  1688  		CEXBalance *libxc.ExchangeBalance `json:"cexBalance"`
  1689  	}{
  1690  		OK:         true,
  1691  		CEXBalance: bal,
  1692  	})
  1693  }
  1694  
  1695  func (s *WebServer) apiArchivedRuns(w http.ResponseWriter, r *http.Request) {
  1696  	runs, err := s.mm.ArchivedRuns()
  1697  	if err != nil {
  1698  		s.writeAPIError(w, fmt.Errorf("error getting archived runs: %w", err))
  1699  		return
  1700  	}
  1701  
  1702  	writeJSON(w, &struct {
  1703  		OK   bool                  `json:"ok"`
  1704  		Runs []*mm.MarketMakingRun `json:"runs"`
  1705  	}{
  1706  		OK:   true,
  1707  		Runs: runs,
  1708  	})
  1709  }
  1710  
  1711  func (s *WebServer) apiRunLogs(w http.ResponseWriter, r *http.Request) {
  1712  	var req struct {
  1713  		StartTime int64              `json:"startTime"`
  1714  		Market    *mm.MarketWithHost `json:"market"`
  1715  		N         uint64             `json:"n"`
  1716  		RefID     *uint64            `json:"refID,omitempty"`
  1717  		Filters   *mm.RunLogFilters  `json:"filters,omitempty"`
  1718  	}
  1719  	if !readPost(w, r, &req) {
  1720  		return
  1721  	}
  1722  
  1723  	if req.Market == nil {
  1724  		s.writeAPIError(w, errors.New("market missing"))
  1725  		return
  1726  	}
  1727  
  1728  	logs, updatedLogs, overview, err := s.mm.RunLogs(req.StartTime, req.Market, req.N, req.RefID, req.Filters)
  1729  	if err != nil {
  1730  		s.writeAPIError(w, fmt.Errorf("error getting run logs: %w", err))
  1731  		return
  1732  	}
  1733  
  1734  	writeJSON(w, &struct {
  1735  		OK          bool                        `json:"ok"`
  1736  		Overview    *mm.MarketMakingRunOverview `json:"overview"`
  1737  		Logs        []*mm.MarketMakingEvent     `json:"logs"`
  1738  		UpdatedLogs []*mm.MarketMakingEvent     `json:"updatedLogs"`
  1739  	}{
  1740  		OK:          true,
  1741  		Overview:    overview,
  1742  		Logs:        logs,
  1743  		UpdatedLogs: updatedLogs,
  1744  	})
  1745  }
  1746  
  1747  func (s *WebServer) apiCEXBook(w http.ResponseWriter, r *http.Request) {
  1748  	var req struct {
  1749  		Host    string `json:"host"`
  1750  		BaseID  uint32 `json:"baseID"`
  1751  		QuoteID uint32 `json:"quoteID"`
  1752  	}
  1753  	if !readPost(w, r, &req) {
  1754  		return
  1755  	}
  1756  	buys, sells, err := s.mm.CEXBook(req.Host, req.BaseID, req.QuoteID)
  1757  	if err != nil {
  1758  		s.writeAPIError(w, fmt.Errorf("error CEX Book: %w", err))
  1759  		return
  1760  	}
  1761  
  1762  	writeJSON(w, &struct {
  1763  		OK   bool            `json:"ok"`
  1764  		Book *core.OrderBook `json:"book"`
  1765  	}{
  1766  		OK: true,
  1767  		Book: &core.OrderBook{
  1768  			Buys:  buys,
  1769  			Sells: sells,
  1770  		},
  1771  	})
  1772  
  1773  }
  1774  
  1775  func (s *WebServer) apiStakeStatus(w http.ResponseWriter, r *http.Request) {
  1776  	var assetID uint32
  1777  	if !readPost(w, r, &assetID) {
  1778  		return
  1779  	}
  1780  	status, err := s.core.StakeStatus(assetID)
  1781  	if err != nil {
  1782  		s.writeAPIError(w, fmt.Errorf("error fetching stake status for asset ID %d: %w", assetID, err))
  1783  		return
  1784  	}
  1785  	writeJSON(w, &struct {
  1786  		OK     bool                       `json:"ok"`
  1787  		Status *asset.TicketStakingStatus `json:"status"`
  1788  	}{
  1789  		OK:     true,
  1790  		Status: status,
  1791  	})
  1792  }
  1793  
  1794  func (s *WebServer) apiSetVSP(w http.ResponseWriter, r *http.Request) {
  1795  	var req struct {
  1796  		AssetID uint32 `json:"assetID"`
  1797  		URL     string `json:"url"`
  1798  	}
  1799  	if !readPost(w, r, &req) {
  1800  		return
  1801  	}
  1802  	if err := s.core.SetVSP(req.AssetID, req.URL); err != nil {
  1803  		s.writeAPIError(w, fmt.Errorf("error settings vsp to %q for asset ID %d: %w", req.URL, req.AssetID, err))
  1804  		return
  1805  	}
  1806  	writeJSON(w, simpleAck())
  1807  }
  1808  
  1809  func (s *WebServer) apiPurchaseTickets(w http.ResponseWriter, r *http.Request) {
  1810  	var req struct {
  1811  		AssetID uint32           `json:"assetID"`
  1812  		N       int              `json:"n"`
  1813  		AppPW   encode.PassBytes `json:"appPW"`
  1814  	}
  1815  	if !readPost(w, r, &req) {
  1816  		return
  1817  	}
  1818  	appPW, err := s.resolvePass(req.AppPW, r)
  1819  	defer zero(appPW)
  1820  	if err != nil {
  1821  		s.writeAPIError(w, fmt.Errorf("password error: %w", err))
  1822  		return
  1823  	}
  1824  	if err = s.core.PurchaseTickets(req.AssetID, appPW, req.N); err != nil {
  1825  		s.writeAPIError(w, fmt.Errorf("error purchasing tickets for asset ID %d: %w", req.AssetID, err))
  1826  		return
  1827  	}
  1828  	writeJSON(w, simpleAck())
  1829  }
  1830  
  1831  func (s *WebServer) apiSetVotingPreferences(w http.ResponseWriter, r *http.Request) {
  1832  	var req struct {
  1833  		AssetID        uint32            `json:"assetID"`
  1834  		Choices        map[string]string `json:"choices"`
  1835  		TSpendPolicy   map[string]string `json:"tSpendPolicy"`
  1836  		TreasuryPolicy map[string]string `json:"treasuryPolicy"`
  1837  	}
  1838  	if !readPost(w, r, &req) {
  1839  		return
  1840  	}
  1841  	if err := s.core.SetVotingPreferences(req.AssetID, req.Choices, req.TSpendPolicy, req.TreasuryPolicy); err != nil {
  1842  		s.writeAPIError(w, fmt.Errorf("error setting voting preferences for asset ID %d: %w", req.AssetID, err))
  1843  		return
  1844  	}
  1845  	writeJSON(w, simpleAck())
  1846  }
  1847  
  1848  func (s *WebServer) apiListVSPs(w http.ResponseWriter, r *http.Request) {
  1849  	var assetID uint32
  1850  	if !readPost(w, r, &assetID) {
  1851  		return
  1852  	}
  1853  	vsps, err := s.core.ListVSPs(assetID)
  1854  	if err != nil {
  1855  		s.writeAPIError(w, fmt.Errorf("error listing VSPs for asset ID %d: %w", assetID, err))
  1856  		return
  1857  	}
  1858  	writeJSON(w, &struct {
  1859  		OK   bool                           `json:"ok"`
  1860  		VSPs []*asset.VotingServiceProvider `json:"vsps"`
  1861  	}{
  1862  		OK:   true,
  1863  		VSPs: vsps,
  1864  	})
  1865  }
  1866  
  1867  func (s *WebServer) apiTicketPage(w http.ResponseWriter, r *http.Request) {
  1868  	var req struct {
  1869  		AssetID   uint32 `json:"assetID"`
  1870  		ScanStart int32  `json:"scanStart"`
  1871  		N         int    `json:"n"`
  1872  		SkipN     int    `json:"skipN"`
  1873  	}
  1874  	if !readPost(w, r, &req) {
  1875  		return
  1876  	}
  1877  	tickets, err := s.core.TicketPage(req.AssetID, req.ScanStart, req.N, req.SkipN)
  1878  	if err != nil {
  1879  		s.writeAPIError(w, fmt.Errorf("error retrieving ticket page for %d: %w", req.AssetID, err))
  1880  		return
  1881  	}
  1882  	writeJSON(w, &struct {
  1883  		OK      bool            `json:"ok"`
  1884  		Tickets []*asset.Ticket `json:"tickets"`
  1885  	}{
  1886  		OK:      true,
  1887  		Tickets: tickets,
  1888  	})
  1889  }
  1890  
  1891  func (s *WebServer) apiMixingStats(w http.ResponseWriter, r *http.Request) {
  1892  	var req struct {
  1893  		AssetID uint32 `json:"assetID"`
  1894  	}
  1895  	if !readPost(w, r, &req) {
  1896  		return
  1897  	}
  1898  	stats, err := s.core.FundsMixingStats(req.AssetID)
  1899  	if err != nil {
  1900  		s.writeAPIError(w, fmt.Errorf("error reteiving mixing stats for %d: %w", req.AssetID, err))
  1901  		return
  1902  	}
  1903  	writeJSON(w, &struct {
  1904  		OK    bool                    `json:"ok"`
  1905  		Stats *asset.FundsMixingStats `json:"stats"`
  1906  	}{
  1907  		OK:    true,
  1908  		Stats: stats,
  1909  	})
  1910  }
  1911  
  1912  func (s *WebServer) apiConfigureMixer(w http.ResponseWriter, r *http.Request) {
  1913  	var req struct {
  1914  		AssetID uint32 `json:"assetID"`
  1915  		Enabled bool   `json:"enabled"`
  1916  	}
  1917  	if !readPost(w, r, &req) {
  1918  		return
  1919  	}
  1920  	pass, err := s.resolvePass(nil, r)
  1921  	if err != nil {
  1922  		s.writeAPIError(w, fmt.Errorf("password error: %w", err))
  1923  		return
  1924  	}
  1925  	defer zero(pass)
  1926  	if err := s.core.ConfigureFundsMixer(pass, req.AssetID, req.Enabled); err != nil {
  1927  		s.writeAPIError(w, fmt.Errorf("error configuring mixing for %d: %w", req.AssetID, err))
  1928  		return
  1929  	}
  1930  	writeJSON(w, simpleAck())
  1931  }
  1932  
  1933  func (s *WebServer) apiStartMarketMakingBot(w http.ResponseWriter, r *http.Request) {
  1934  	var form struct {
  1935  		Config *mm.StartConfig  `json:"config"`
  1936  		AppPW  encode.PassBytes `json:"appPW"`
  1937  	}
  1938  	defer form.AppPW.Clear()
  1939  	if !readPost(w, r, &form) {
  1940  		s.writeAPIError(w, fmt.Errorf("failed to read form"))
  1941  		return
  1942  	}
  1943  	appPW, err := s.resolvePass(form.AppPW, r)
  1944  	if err != nil {
  1945  		s.writeAPIError(w, fmt.Errorf("password error: %w", err))
  1946  		return
  1947  	}
  1948  	defer zero(appPW)
  1949  	if form.Config == nil {
  1950  		s.writeAPIError(w, errors.New("config missing"))
  1951  		return
  1952  	}
  1953  	if err = s.mm.StartBot(form.Config, nil, appPW, true); err != nil {
  1954  		s.writeAPIError(w, fmt.Errorf("error starting market making: %v", err))
  1955  		return
  1956  	}
  1957  
  1958  	writeJSON(w, simpleAck())
  1959  }
  1960  
  1961  func (s *WebServer) apiStopMarketMakingBot(w http.ResponseWriter, r *http.Request) {
  1962  	var form struct {
  1963  		Market *mm.MarketWithHost `json:"market"`
  1964  	}
  1965  	if !readPost(w, r, &form) {
  1966  		s.writeAPIError(w, fmt.Errorf("failed to read form"))
  1967  		return
  1968  	}
  1969  	if form.Market == nil {
  1970  		s.writeAPIError(w, errors.New("market missing"))
  1971  		return
  1972  	}
  1973  	if err := s.mm.StopBot(form.Market); err != nil {
  1974  		s.writeAPIError(w, fmt.Errorf("error stopping mm bot %q: %v", form.Market, err))
  1975  		return
  1976  	}
  1977  	writeJSON(w, simpleAck())
  1978  }
  1979  
  1980  func (s *WebServer) apiUpdateCEXConfig(w http.ResponseWriter, r *http.Request) {
  1981  	var updatedCfg *mm.CEXConfig
  1982  	if !readPost(w, r, &updatedCfg) {
  1983  		s.writeAPIError(w, fmt.Errorf("failed to read config"))
  1984  		return
  1985  	}
  1986  
  1987  	if err := s.mm.UpdateCEXConfig(updatedCfg); err != nil {
  1988  		s.writeAPIError(w, err)
  1989  		return
  1990  	}
  1991  
  1992  	writeJSON(w, simpleAck())
  1993  }
  1994  
  1995  func (s *WebServer) apiUpdateBotConfig(w http.ResponseWriter, r *http.Request) {
  1996  	var updatedCfg *mm.BotConfig
  1997  	if !readPost(w, r, &updatedCfg) {
  1998  		s.writeAPIError(w, fmt.Errorf("failed to read config"))
  1999  		return
  2000  	}
  2001  
  2002  	if err := s.mm.UpdateBotConfig(updatedCfg); err != nil {
  2003  		s.writeAPIError(w, err)
  2004  		return
  2005  	}
  2006  
  2007  	writeJSON(w, simpleAck())
  2008  }
  2009  
  2010  func (s *WebServer) apiRemoveBotConfig(w http.ResponseWriter, r *http.Request) {
  2011  	var form struct {
  2012  		Host    string `json:"host"`
  2013  		BaseID  uint32 `json:"baseID"`
  2014  		QuoteID uint32 `json:"quoteID"`
  2015  	}
  2016  	if !readPost(w, r, &form) {
  2017  		s.writeAPIError(w, fmt.Errorf("failed to read form"))
  2018  		return
  2019  	}
  2020  
  2021  	if err := s.mm.RemoveBotConfig(form.Host, form.BaseID, form.QuoteID); err != nil {
  2022  		s.writeAPIError(w, err)
  2023  		return
  2024  	}
  2025  
  2026  	writeJSON(w, simpleAck())
  2027  }
  2028  
  2029  func (s *WebServer) apiMarketMakingStatus(w http.ResponseWriter, r *http.Request) {
  2030  	writeJSON(w, &struct {
  2031  		OK     bool       `json:"ok"`
  2032  		Status *mm.Status `json:"status"`
  2033  	}{
  2034  		OK:     true,
  2035  		Status: s.mm.Status(),
  2036  	})
  2037  }
  2038  
  2039  func (s *WebServer) apiTxHistory(w http.ResponseWriter, r *http.Request) {
  2040  	var form struct {
  2041  		AssetID uint32 `json:"assetID"`
  2042  		N       int    `json:"n"`
  2043  		RefID   string `json:"refID"`
  2044  		Past    bool   `json:"past"`
  2045  	}
  2046  	if !readPost(w, r, &form) {
  2047  		return
  2048  	}
  2049  
  2050  	var refID *string
  2051  	if len(form.RefID) > 0 {
  2052  		refID = &form.RefID
  2053  	}
  2054  
  2055  	txs, err := s.core.TxHistory(form.AssetID, form.N, refID, form.Past)
  2056  	if err != nil {
  2057  		s.writeAPIError(w, fmt.Errorf("error getting transaction history: %w", err))
  2058  		return
  2059  	}
  2060  	writeJSON(w, &struct {
  2061  		OK  bool                       `json:"ok"`
  2062  		Txs []*asset.WalletTransaction `json:"txs"`
  2063  	}{
  2064  		OK:  true,
  2065  		Txs: txs,
  2066  	})
  2067  }
  2068  
  2069  func (s *WebServer) apiTakeAction(w http.ResponseWriter, r *http.Request) {
  2070  	var req struct {
  2071  		AssetID  uint32          `json:"assetID"`
  2072  		ActionID string          `json:"actionID"`
  2073  		Action   json.RawMessage `json:"action"`
  2074  	}
  2075  	if !readPost(w, r, &req) {
  2076  		return
  2077  	}
  2078  	if err := s.core.TakeAction(req.AssetID, req.ActionID, req.Action); err != nil {
  2079  		s.writeAPIError(w, fmt.Errorf("error taking action: %w", err))
  2080  		return
  2081  	}
  2082  	writeJSON(w, simpleAck())
  2083  }
  2084  
  2085  func (s *WebServer) redeemGameCode(w http.ResponseWriter, r *http.Request) {
  2086  	var form struct {
  2087  		Code  dex.Bytes        `json:"code"`
  2088  		Msg   string           `json:"msg"`
  2089  		AppPW encode.PassBytes `json:"appPW"`
  2090  	}
  2091  	if !readPost(w, r, &form) {
  2092  		return
  2093  	}
  2094  	defer form.AppPW.Clear()
  2095  	appPW, err := s.resolvePass(form.AppPW, r)
  2096  	if err != nil {
  2097  		s.writeAPIError(w, fmt.Errorf("password error: %w", err))
  2098  		return
  2099  	}
  2100  	coinID, win, err := s.core.RedeemGeocode(appPW, form.Code, form.Msg)
  2101  	if err != nil {
  2102  		s.writeAPIError(w, fmt.Errorf("redemption error: %w", err))
  2103  		return
  2104  	}
  2105  	const dcrBipID = 42
  2106  	coinIDString, _ := asset.DecodeCoinID(dcrBipID, coinID)
  2107  	writeJSON(w, &struct {
  2108  		OK         bool      `json:"ok"`
  2109  		CoinID     dex.Bytes `json:"coinID"`
  2110  		CoinString string    `json:"coinString"`
  2111  		Win        uint64    `json:"win"`
  2112  	}{
  2113  		OK:         true,
  2114  		CoinID:     coinID,
  2115  		CoinString: coinIDString,
  2116  		Win:        win,
  2117  	})
  2118  }
  2119  
  2120  // writeAPIError logs the formatted error and sends a standardResponse with the
  2121  // error message.
  2122  func (s *WebServer) writeAPIError(w http.ResponseWriter, err error) {
  2123  	var cErr *core.Error
  2124  	var code *int
  2125  	if errors.As(err, &cErr) {
  2126  		code = cErr.Code()
  2127  	}
  2128  
  2129  	innerErr := core.UnwrapErr(err)
  2130  	resp := &standardResponse{
  2131  		OK:   false,
  2132  		Msg:  innerErr.Error(),
  2133  		Code: code,
  2134  	}
  2135  	log.Error(err.Error())
  2136  	writeJSON(w, resp)
  2137  }
  2138  
  2139  // setCookie sets the value of a cookie in the http response.
  2140  func setCookie(name, value string, w http.ResponseWriter) {
  2141  	http.SetCookie(w, &http.Cookie{
  2142  		Name:     name,
  2143  		Path:     "/",
  2144  		Value:    value,
  2145  		SameSite: http.SameSiteStrictMode,
  2146  	})
  2147  }
  2148  
  2149  // clearCookie removes a cookie in the http response.
  2150  func clearCookie(name string, w http.ResponseWriter) {
  2151  	http.SetCookie(w, &http.Cookie{
  2152  		Name:     name,
  2153  		Path:     "/",
  2154  		Value:    "",
  2155  		Expires:  time.Unix(0, 0),
  2156  		SameSite: http.SameSiteStrictMode,
  2157  	})
  2158  }
  2159  
  2160  // resolvePass returns the appPW if it has a value, but if not, it attempts
  2161  // to retrieve the cached password using the information in cookies.
  2162  func (s *WebServer) resolvePass(appPW []byte, r *http.Request) ([]byte, error) {
  2163  	if len(appPW) > 0 {
  2164  		return appPW, nil
  2165  	}
  2166  	cachedPass, err := s.getCachedPasswordUsingRequest(r)
  2167  	if err != nil {
  2168  		if errors.Is(err, errNoCachedPW) {
  2169  			return nil, fmt.Errorf("app pass cannot be empty")
  2170  		}
  2171  		return nil, fmt.Errorf("error retrieving cached pw: %w", err)
  2172  	}
  2173  	return cachedPass, nil
  2174  }