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  }