decred.org/dcrdex@v1.0.5/client/rpcserver/handlers.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 rpcserver
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"os"
    10  	"sort"
    11  	"strings"
    12  	"time"
    13  
    14  	"decred.org/dcrdex/client/asset"
    15  	"decred.org/dcrdex/client/core"
    16  	"decred.org/dcrdex/client/mm"
    17  	"decred.org/dcrdex/dex"
    18  	"decred.org/dcrdex/dex/msgjson"
    19  	"decred.org/dcrdex/dex/order"
    20  )
    21  
    22  // routes
    23  const (
    24  	cancelRoute                = "cancel"
    25  	closeWalletRoute           = "closewallet"
    26  	discoverAcctRoute          = "discoveracct"
    27  	exchangesRoute             = "exchanges"
    28  	helpRoute                  = "help"
    29  	initRoute                  = "init"
    30  	loginRoute                 = "login"
    31  	logoutRoute                = "logout"
    32  	myOrdersRoute              = "myorders"
    33  	newWalletRoute             = "newwallet"
    34  	openWalletRoute            = "openwallet"
    35  	toggleWalletStatusRoute    = "togglewalletstatus"
    36  	orderBookRoute             = "orderbook"
    37  	getDEXConfRoute            = "getdexconfig"
    38  	bondAssetsRoute            = "bondassets"
    39  	postBondRoute              = "postbond"
    40  	bondOptionsRoute           = "bondopts"
    41  	tradeRoute                 = "trade"
    42  	versionRoute               = "version"
    43  	walletsRoute               = "wallets"
    44  	rescanWalletRoute          = "rescanwallet"
    45  	withdrawRoute              = "withdraw"
    46  	sendRoute                  = "send"
    47  	appSeedRoute               = "appseed"
    48  	deleteArchivedRecordsRoute = "deletearchivedrecords"
    49  	walletPeersRoute           = "walletpeers"
    50  	addWalletPeerRoute         = "addwalletpeer"
    51  	removeWalletPeerRoute      = "removewalletpeer"
    52  	notificationsRoute         = "notifications"
    53  	startBotRoute              = "startmmbot"
    54  	stopBotRoute               = "stopmmbot"
    55  	updateRunningBotCfgRoute   = "updaterunningbotcfg"
    56  	updateRunningBotInvRoute   = "updaterunningbotinv"
    57  	mmAvailableBalancesRoute   = "mmavailablebalances"
    58  	mmStatusRoute              = "mmstatus"
    59  	multiTradeRoute            = "multitrade"
    60  	stakeStatusRoute           = "stakestatus"
    61  	setVSPRoute                = "setvsp"
    62  	purchaseTicketsRoute       = "purchasetickets"
    63  	setVotingPreferencesRoute  = "setvotingprefs"
    64  	txHistoryRoute             = "txhistory"
    65  	walletTxRoute              = "wallettx"
    66  	withdrawBchSpvRoute        = "withdrawbchspv"
    67  )
    68  
    69  const (
    70  	initializedStr    = "app initialized"
    71  	walletCreatedStr  = "%s wallet created and unlocked"
    72  	walletLockedStr   = "%s wallet locked"
    73  	walletUnlockedStr = "%s wallet unlocked"
    74  	canceledOrderStr  = "canceled order %s"
    75  	logoutStr         = "goodbye"
    76  	walletStatusStr   = "%s wallet has been %s"
    77  	setVotePrefsStr   = "vote preferences set"
    78  	setVSPStr         = "vsp set to %s"
    79  )
    80  
    81  // createResponse creates a msgjson response payload.
    82  func createResponse(op string, res any, resErr *msgjson.Error) *msgjson.ResponsePayload {
    83  	encodedRes, err := json.Marshal(res)
    84  	if err != nil {
    85  		err := fmt.Errorf("unable to marshal data for %s: %w", op, err)
    86  		panic(err)
    87  	}
    88  	return &msgjson.ResponsePayload{Result: encodedRes, Error: resErr}
    89  }
    90  
    91  // usage creates and returns usage for route combined with a passed error as a
    92  // *msgjson.ResponsePayload.
    93  func usage(route string, err error) *msgjson.ResponsePayload {
    94  	usage, _ := commandUsage(route, false)
    95  	resErr := msgjson.NewError(msgjson.RPCArgumentsError, "%v\n\n%s", err, usage)
    96  	return createResponse(route, nil, resErr)
    97  }
    98  
    99  // routes maps routes to a handler function.
   100  var routes = map[string]func(s *RPCServer, params *RawParams) *msgjson.ResponsePayload{
   101  	cancelRoute:                handleCancel,
   102  	closeWalletRoute:           handleCloseWallet,
   103  	discoverAcctRoute:          handleDiscoverAcct,
   104  	exchangesRoute:             handleExchanges,
   105  	helpRoute:                  handleHelp,
   106  	initRoute:                  handleInit,
   107  	loginRoute:                 handleLogin,
   108  	logoutRoute:                handleLogout,
   109  	myOrdersRoute:              handleMyOrders,
   110  	newWalletRoute:             handleNewWallet,
   111  	openWalletRoute:            handleOpenWallet,
   112  	toggleWalletStatusRoute:    handleToggleWalletStatus,
   113  	orderBookRoute:             handleOrderBook,
   114  	getDEXConfRoute:            handleGetDEXConfig,
   115  	postBondRoute:              handlePostBond,
   116  	bondOptionsRoute:           handleBondOptions,
   117  	bondAssetsRoute:            handleBondAssets,
   118  	tradeRoute:                 handleTrade,
   119  	versionRoute:               handleVersion,
   120  	walletsRoute:               handleWallets,
   121  	rescanWalletRoute:          handleRescanWallet,
   122  	withdrawRoute:              handleWithdraw,
   123  	sendRoute:                  handleSend,
   124  	appSeedRoute:               handleAppSeed,
   125  	deleteArchivedRecordsRoute: handleDeleteArchivedRecords,
   126  	walletPeersRoute:           handleWalletPeers,
   127  	addWalletPeerRoute:         handleAddWalletPeer,
   128  	removeWalletPeerRoute:      handleRemoveWalletPeer,
   129  	notificationsRoute:         handleNotifications,
   130  	startBotRoute:              handleStartBot,
   131  	stopBotRoute:               handleStopBot,
   132  	mmAvailableBalancesRoute:   handleMMAvailableBalances,
   133  	mmStatusRoute:              handleMMStatus,
   134  	updateRunningBotCfgRoute:   handleUpdateRunningBotCfg,
   135  	updateRunningBotInvRoute:   handleUpdateRunningBotInventory,
   136  	multiTradeRoute:            handleMultiTrade,
   137  	stakeStatusRoute:           handleStakeStatus,
   138  	setVSPRoute:                handleSetVSP,
   139  	purchaseTicketsRoute:       handlePurchaseTickets,
   140  	setVotingPreferencesRoute:  handleSetVotingPreferences,
   141  	txHistoryRoute:             handleTxHistory,
   142  	walletTxRoute:              handleWalletTx,
   143  	withdrawBchSpvRoute:        handleWithdrawBchSpv,
   144  }
   145  
   146  // handleHelp handles requests for help. Returns general help for all commands
   147  // if no arguments are passed or verbose help if the passed argument is a known
   148  // command.
   149  func handleHelp(_ *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   150  	form, err := parseHelpArgs(params)
   151  	if err != nil {
   152  		return usage(helpRoute, err)
   153  	}
   154  	res := ""
   155  	if form.helpWith == "" {
   156  		// List all commands if no arguments.
   157  		res = ListCommands(form.includePasswords)
   158  	} else {
   159  		var err error
   160  		res, err = commandUsage(form.helpWith, form.includePasswords)
   161  		if err != nil {
   162  			resErr := msgjson.NewError(msgjson.RPCUnknownRoute, "error getting usage: %v", err)
   163  			return createResponse(helpRoute, nil, resErr)
   164  		}
   165  	}
   166  	return createResponse(helpRoute, &res, nil)
   167  }
   168  
   169  // handleInit handles requests for init. *msgjson.ResponsePayload.Error is empty
   170  // if successful.
   171  func handleInit(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   172  	appPass, seed, err := parseInitArgs(params)
   173  	if err != nil {
   174  		return usage(initRoute, err)
   175  	}
   176  	defer appPass.Clear()
   177  	if _, err := s.core.InitializeClient(appPass, seed); err != nil {
   178  		resErr := msgjson.NewError(msgjson.RPCInitError, "unable to initialize client: %v", err)
   179  		return createResponse(initRoute, nil, resErr)
   180  	}
   181  	res := initializedStr
   182  	return createResponse(initRoute, &res, nil)
   183  }
   184  
   185  // handleVersion handles requests for version. It returns the rpc server version
   186  // and dexc version.
   187  func handleVersion(s *RPCServer, _ *RawParams) *msgjson.ResponsePayload {
   188  	result := &VersionResponse{
   189  		RPCServerVer: &dex.Semver{
   190  			Major: rpcSemverMajor,
   191  			Minor: rpcSemverMinor,
   192  			Patch: rpcSemverPatch,
   193  		},
   194  		BWVersion: s.bwVersion,
   195  	}
   196  
   197  	return createResponse(versionRoute, result, nil)
   198  }
   199  
   200  // handleNewWallet handles requests for newwallet.
   201  // *msgjson.ResponsePayload.Error is empty if successful. Returns a
   202  // msgjson.RPCWalletExistsError if a wallet for the assetID already exists.
   203  // Wallet will be unlocked if successful.
   204  func handleNewWallet(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   205  	form, err := parseNewWalletArgs(params)
   206  	if err != nil {
   207  		return usage(newWalletRoute, err)
   208  	}
   209  
   210  	// zero password params in request payload when done handling this request
   211  	defer func() {
   212  		form.appPass.Clear()
   213  		form.walletPass.Clear()
   214  	}()
   215  
   216  	if s.core.WalletState(form.assetID) != nil {
   217  		resErr := msgjson.NewError(msgjson.RPCWalletExistsError, "error creating %s wallet: wallet already exists", dex.BipIDSymbol(form.assetID))
   218  		return createResponse(newWalletRoute, nil, resErr)
   219  	}
   220  
   221  	walletDef, err := asset.WalletDef(form.assetID, form.walletType)
   222  	if err != nil {
   223  		resErr := msgjson.NewError(msgjson.RPCWalletDefinitionError, "error creating %s wallet: unable to get wallet definition: %v", dex.BipIDSymbol(form.assetID), err)
   224  		return createResponse(newWalletRoute, nil, resErr)
   225  	}
   226  
   227  	// Apply default config options if they exist.
   228  	for _, opt := range walletDef.ConfigOpts {
   229  		if _, has := form.config[opt.Key]; !has {
   230  			form.config[opt.Key] = opt.DefaultValue
   231  		}
   232  	}
   233  
   234  	// Wallet does not exist yet. Try to create it.
   235  	err = s.core.CreateWallet(form.appPass, form.walletPass, &core.WalletForm{
   236  		Type:    form.walletType,
   237  		AssetID: form.assetID,
   238  		Config:  form.config,
   239  	})
   240  	if err != nil {
   241  		resErr := msgjson.NewError(msgjson.RPCCreateWalletError, "error creating %s wallet: %v", dex.BipIDSymbol(form.assetID), err)
   242  		return createResponse(newWalletRoute, nil, resErr)
   243  	}
   244  
   245  	err = s.core.OpenWallet(form.assetID, form.appPass)
   246  	if err != nil {
   247  		resErr := msgjson.NewError(msgjson.RPCOpenWalletError, "wallet connected, but failed to open with provided password: %v", err)
   248  		return createResponse(newWalletRoute, nil, resErr)
   249  	}
   250  
   251  	res := fmt.Sprintf(walletCreatedStr, dex.BipIDSymbol(form.assetID))
   252  	return createResponse(newWalletRoute, &res, nil)
   253  }
   254  
   255  // handleOpenWallet handles requests for openWallet.
   256  // *msgjson.ResponsePayload.Error is empty if successful. Requires the app
   257  // password. Opens the wallet.
   258  func handleOpenWallet(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   259  	form, err := parseOpenWalletArgs(params)
   260  	if err != nil {
   261  		return usage(openWalletRoute, err)
   262  	}
   263  	defer form.appPass.Clear()
   264  
   265  	err = s.core.OpenWallet(form.assetID, form.appPass)
   266  	if err != nil {
   267  		resErr := msgjson.NewError(msgjson.RPCOpenWalletError, "error unlocking %s wallet: %v", dex.BipIDSymbol(form.assetID), err)
   268  		return createResponse(openWalletRoute, nil, resErr)
   269  	}
   270  
   271  	res := fmt.Sprintf(walletUnlockedStr, dex.BipIDSymbol(form.assetID))
   272  	return createResponse(openWalletRoute, &res, nil)
   273  }
   274  
   275  // handleCloseWallet handles requests for closeWallet.
   276  // *msgjson.ResponsePayload.Error is empty if successful. Closes the wallet.
   277  func handleCloseWallet(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   278  	assetID, err := parseCloseWalletArgs(params)
   279  	if err != nil {
   280  		return usage(closeWalletRoute, err)
   281  	}
   282  	if err := s.core.CloseWallet(assetID); err != nil {
   283  		resErr := msgjson.NewError(msgjson.RPCCloseWalletError, "unable to close wallet %s: %v", dex.BipIDSymbol(assetID), err)
   284  		return createResponse(closeWalletRoute, nil, resErr)
   285  	}
   286  
   287  	res := fmt.Sprintf(walletLockedStr, dex.BipIDSymbol(assetID))
   288  	return createResponse(closeWalletRoute, &res, nil)
   289  }
   290  
   291  // handleToggleWalletStatus handles requests for toggleWalletStatus.
   292  // *msgjson.ResponsePayload.Error is empty if successful. Disables or enables a
   293  // wallet.
   294  func handleToggleWalletStatus(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   295  	form, err := parseToggleWalletStatusArgs(params)
   296  	if err != nil {
   297  		return usage(toggleWalletStatusRoute, err)
   298  	}
   299  	if err := s.core.ToggleWalletStatus(form.assetID, form.disable); err != nil {
   300  		resErr := msgjson.NewError(msgjson.RPCToggleWalletStatusError, "unable to change %s wallet status: %v", dex.BipIDSymbol(form.assetID), err)
   301  		return createResponse(toggleWalletStatusRoute, nil, resErr)
   302  	}
   303  
   304  	status := "enabled"
   305  	if form.disable {
   306  		status = "disabled"
   307  	}
   308  
   309  	res := fmt.Sprintf(walletStatusStr, dex.BipIDSymbol(form.assetID), status)
   310  	return createResponse(toggleWalletStatusRoute, &res, nil)
   311  }
   312  
   313  // handleWallets handles requests for wallets. Returns a list of wallet details.
   314  func handleWallets(s *RPCServer, _ *RawParams) *msgjson.ResponsePayload {
   315  	walletsStates := s.core.Wallets()
   316  	return createResponse(walletsRoute, walletsStates, nil)
   317  }
   318  
   319  // handleBondAssets handles requests for bondassets.
   320  // *msgjson.ResponsePayload.Error is empty if successful. Requires the address
   321  // of a dex and returns the bond expiry and supported asset bond details.
   322  func handleBondAssets(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   323  	host, cert, err := parseBondAssetsArgs(params)
   324  	if err != nil {
   325  		return usage(bondAssetsRoute, err)
   326  	}
   327  	exchInf := s.core.Exchanges()
   328  	exchCfg := exchInf[host]
   329  	if exchCfg == nil {
   330  		exchCfg, err = s.core.GetDEXConfig(host, cert) // cert is file contents, not name
   331  		if err != nil {
   332  			resErr := msgjson.NewError(msgjson.RPCGetDEXConfigError, "%v", err)
   333  			return createResponse(bondAssetsRoute, nil, resErr)
   334  		}
   335  	}
   336  	res := &getBondAssetsResponse{
   337  		Expiry: exchCfg.BondExpiry,
   338  		Assets: exchCfg.BondAssets,
   339  	}
   340  	return createResponse(bondAssetsRoute, res, nil)
   341  }
   342  
   343  // handleGetDEXConfig handles requests for getdexconfig.
   344  // *msgjson.ResponsePayload.Error is empty if successful. Requires the address
   345  // of a dex and returns its config..
   346  func handleGetDEXConfig(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   347  	host, cert, err := parseGetDEXConfigArgs(params)
   348  	if err != nil {
   349  		return usage(getDEXConfRoute, err)
   350  	}
   351  	exchange, err := s.core.GetDEXConfig(host, cert) // cert is file contents, not name
   352  	if err != nil {
   353  		resErr := msgjson.NewError(msgjson.RPCGetDEXConfigError, "%v", err)
   354  		return createResponse(getDEXConfRoute, nil, resErr)
   355  	}
   356  	return createResponse(getDEXConfRoute, exchange, nil)
   357  }
   358  
   359  // handleDiscoverAcct is the handler for discoveracct. *msgjson.ResponsePayload.Error
   360  // is empty if successful.
   361  func handleDiscoverAcct(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   362  	form, err := parseDiscoverAcctArgs(params)
   363  	if err != nil {
   364  		return usage(discoverAcctRoute, err)
   365  	}
   366  	defer form.appPass.Clear()
   367  	_, paid, err := s.core.DiscoverAccount(form.addr, form.appPass, form.cert)
   368  	if err != nil {
   369  		resErr := &msgjson.Error{Code: msgjson.RPCDiscoverAcctError, Message: err.Error()}
   370  		return createResponse(discoverAcctRoute, nil, resErr)
   371  	}
   372  	return createResponse(discoverAcctRoute, &paid, nil)
   373  }
   374  
   375  func handleBondOptions(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   376  	form, err := parseBondOptsArgs(params)
   377  	if err != nil {
   378  		return usage(bondOptionsRoute, err)
   379  	}
   380  	err = s.core.UpdateBondOptions(form)
   381  	if err != nil {
   382  		resErr := &msgjson.Error{Code: msgjson.RPCPostBondError, Message: err.Error()}
   383  		return createResponse(bondOptionsRoute, nil, resErr)
   384  	}
   385  	return createResponse(bondOptionsRoute, "ok", nil)
   386  }
   387  
   388  // handlePostBond handles requests for postbond. *msgjson.ResponsePayload.Error
   389  // is empty if successful.
   390  func handlePostBond(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   391  	form, err := parsePostBondArgs(params)
   392  	if err != nil {
   393  		return usage(postBondRoute, err)
   394  	}
   395  	defer form.AppPass.Clear()
   396  	// Get the exchange config with Exchanges(), not GetDEXConfig, since we may
   397  	// already be connected and even with an existing account.
   398  	exchInf := s.core.Exchanges()
   399  	exchCfg := exchInf[form.Addr]
   400  	if exchCfg == nil {
   401  		// Not already registered.
   402  		exchCfg, err = s.core.GetDEXConfig(form.Addr, form.Cert)
   403  		if err != nil {
   404  			resErr := &msgjson.Error{Code: msgjson.RPCGetDEXConfigError, Message: err.Error()}
   405  			return createResponse(postBondRoute, nil, resErr)
   406  		}
   407  	}
   408  	// Registration with different assets will be supported in the future, but
   409  	// for now, this requires DCR.
   410  	assetID := uint32(42)
   411  	if form.Asset != nil {
   412  		assetID = *form.Asset
   413  	}
   414  	symb := dex.BipIDSymbol(assetID)
   415  
   416  	bondAsset, supported := exchCfg.BondAssets[symb]
   417  	if !supported {
   418  		resErr := msgjson.NewError(msgjson.RPCPostBondError, "DEX %s does not support registration with %s", form.Addr, symb)
   419  		return createResponse(postBondRoute, nil, resErr)
   420  	}
   421  	if bondAsset.Amt > form.Bond || form.Bond%bondAsset.Amt != 0 {
   422  		resErr := msgjson.NewError(msgjson.RPCPostBondError, "DEX at %s expects a bond amount in multiples of %d %s but %d was offered",
   423  			form.Addr, bondAsset.Amt, dex.BipIDSymbol(assetID), form.Bond)
   424  		return createResponse(postBondRoute, nil, resErr)
   425  	}
   426  	res, err := s.core.PostBond(form)
   427  	if err != nil {
   428  		resErr := &msgjson.Error{Code: msgjson.RPCPostBondError, Message: err.Error()}
   429  		return createResponse(postBondRoute, nil, resErr)
   430  	}
   431  	if res.BondID == "" {
   432  		return createResponse(postBondRoute, "existing account configured - no bond posted", nil)
   433  	}
   434  	return createResponse(postBondRoute, res, nil)
   435  }
   436  
   437  // handleExchanges handles requests for exchanges. It takes no arguments and
   438  // returns a map of exchanges.
   439  func handleExchanges(s *RPCServer, _ *RawParams) *msgjson.ResponsePayload {
   440  	// Convert something to a map[string]any.
   441  	convM := func(in any) map[string]any {
   442  		var m map[string]any
   443  		b, err := json.Marshal(in)
   444  		if err != nil {
   445  			panic(err)
   446  		}
   447  		if err = json.Unmarshal(b, &m); err != nil {
   448  			panic(err)
   449  		}
   450  		return m
   451  	}
   452  	res := s.core.Exchanges()
   453  	exchanges := convM(res)
   454  	// Iterate through exchanges converting structs into maps in order to
   455  	// remove some fields. Keys are DEX addresses.
   456  	for k, exchange := range exchanges {
   457  		exchangeDetails := convM(exchange)
   458  		// Remove a redundant address field.
   459  		delete(exchangeDetails, "host")
   460  		markets := convM(exchangeDetails["markets"])
   461  		// Market keys are market name.
   462  		for k, market := range markets {
   463  			marketDetails := convM(market)
   464  			// Remove redundant name field.
   465  			delete(marketDetails, "name")
   466  			delete(marketDetails, "orders")
   467  			markets[k] = marketDetails
   468  		}
   469  		assets := convM(exchangeDetails["assets"])
   470  		// Asset keys are assetIDs.
   471  		for k, asset := range assets {
   472  			assetDetails := convM(asset)
   473  			// Remove redundant id field.
   474  			delete(assetDetails, "id")
   475  			assets[k] = assetDetails
   476  		}
   477  		exchangeDetails["markets"] = markets
   478  		exchangeDetails["assets"] = assets
   479  		exchanges[k] = exchangeDetails
   480  	}
   481  	return createResponse(exchangesRoute, &exchanges, nil)
   482  }
   483  
   484  // handleLogin sets up the dex connections.
   485  func handleLogin(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   486  	appPass, err := parseLoginArgs(params)
   487  	if err != nil {
   488  		return usage(loginRoute, err)
   489  	}
   490  	defer appPass.Clear()
   491  	err = s.core.Login(appPass)
   492  	if err != nil {
   493  		resErr := msgjson.NewError(msgjson.RPCLoginError, "unable to login: %v", err)
   494  		return createResponse(loginRoute, nil, resErr)
   495  	}
   496  	res := "successfully logged in"
   497  	return createResponse(loginRoute, &res, nil)
   498  }
   499  
   500  // handleTrade handles requests for trade. *msgjson.ResponsePayload.Error is
   501  // empty if successful.
   502  func handleTrade(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   503  	form, err := parseTradeArgs(params)
   504  	if err != nil {
   505  		return usage(tradeRoute, err)
   506  	}
   507  	defer form.appPass.Clear()
   508  	res, err := s.core.Trade(form.appPass, form.srvForm)
   509  	if err != nil {
   510  		resErr := msgjson.NewError(msgjson.RPCTradeError, "unable to trade: %v", err)
   511  		return createResponse(tradeRoute, nil, resErr)
   512  	}
   513  	tradeRes := &tradeResponse{
   514  		OrderID: res.ID.String(),
   515  		Sig:     res.Sig.String(),
   516  		Stamp:   res.Stamp,
   517  	}
   518  	return createResponse(tradeRoute, &tradeRes, nil)
   519  }
   520  
   521  func handleMultiTrade(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   522  	form, err := parseMultiTradeArgs(params)
   523  	if err != nil {
   524  		return usage(multiTradeRoute, err)
   525  	}
   526  	defer form.appPass.Clear()
   527  	results := s.core.MultiTrade(form.appPass, form.srvForm)
   528  	trades := make([]*tradeResponse, 0, len(results))
   529  	for _, res := range results {
   530  		if res.Error != nil {
   531  			trades = append(trades, &tradeResponse{
   532  				Error: res.Error,
   533  			})
   534  			continue
   535  		}
   536  		trade := res.Order
   537  		trades = append(trades, &tradeResponse{
   538  			OrderID: trade.ID.String(),
   539  			Sig:     trade.Sig.String(),
   540  			Stamp:   trade.Stamp,
   541  		})
   542  	}
   543  	return createResponse(multiTradeRoute, &trades, nil)
   544  }
   545  
   546  // handleCancel handles requests for cancel. *msgjson.ResponsePayload.Error is
   547  // empty if successful.
   548  func handleCancel(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   549  	form, err := parseCancelArgs(params)
   550  	if err != nil {
   551  		return usage(cancelRoute, err)
   552  	}
   553  	if err := s.core.Cancel(form.orderID); err != nil {
   554  		resErr := msgjson.NewError(msgjson.RPCCancelError, "unable to cancel order %q: %v", form.orderID, err)
   555  		return createResponse(cancelRoute, nil, resErr)
   556  	}
   557  	res := fmt.Sprintf(canceledOrderStr, form.orderID)
   558  	return createResponse(cancelRoute, &res, nil)
   559  }
   560  
   561  // handleWithdraw handles requests for withdraw. *msgjson.ResponsePayload.Error
   562  // is empty if successful.
   563  func handleWithdraw(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   564  	return send(s, params, withdrawRoute)
   565  }
   566  
   567  // handleSend handles the request for send. *msgjson.ResponsePayload.Error
   568  // is empty if successful.
   569  func handleSend(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   570  	return send(s, params, sendRoute)
   571  }
   572  
   573  func send(s *RPCServer, params *RawParams, route string) *msgjson.ResponsePayload {
   574  	form, err := parseSendOrWithdrawArgs(params)
   575  	if err != nil {
   576  		return usage(route, err)
   577  	}
   578  	defer form.appPass.Clear()
   579  	subtract := false
   580  	if route == withdrawRoute {
   581  		subtract = true
   582  	}
   583  	if len(form.appPass) == 0 {
   584  		resErr := msgjson.NewError(msgjson.RPCFundTransferError, "empty pass")
   585  		return createResponse(route, nil, resErr)
   586  	}
   587  	coin, err := s.core.Send(form.appPass, form.assetID, form.value, form.address, subtract)
   588  	if err != nil {
   589  		resErr := msgjson.NewError(msgjson.RPCFundTransferError, "unable to %s: %v", route, err)
   590  		return createResponse(route, nil, resErr)
   591  	}
   592  	res := coin.String()
   593  	return createResponse(route, &res, nil)
   594  }
   595  
   596  // handleRescanWallet handles requests to rescan a wallet. This may trigger an
   597  // asynchronous resynchronization of wallet address activity, and the wallet
   598  // state should be consulted for status. *msgjson.ResponsePayload.Error is empty
   599  // if successful.
   600  func handleRescanWallet(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   601  	assetID, force, err := parseRescanWalletArgs(params)
   602  	if err != nil {
   603  		return usage(rescanWalletRoute, err)
   604  	}
   605  	err = s.core.RescanWallet(assetID, force)
   606  	if err != nil {
   607  		resErr := msgjson.NewError(msgjson.RPCWalletRescanError, "unable to rescan wallet: %v", err)
   608  		return createResponse(rescanWalletRoute, nil, resErr)
   609  	}
   610  	return createResponse(rescanWalletRoute, "started", nil)
   611  }
   612  
   613  // handleLogout logs out Bison Wallet. *msgjson.ResponsePayload.Error is empty
   614  // if successful.
   615  func handleLogout(s *RPCServer, _ *RawParams) *msgjson.ResponsePayload {
   616  	if err := s.core.Logout(); err != nil {
   617  		resErr := msgjson.NewError(msgjson.RPCLogoutError, "unable to logout: %v", err)
   618  		return createResponse(logoutRoute, nil, resErr)
   619  	}
   620  	res := logoutStr
   621  	return createResponse(logoutRoute, &res, nil)
   622  }
   623  
   624  // truncateOrderBook truncates book to the top nOrders of buys and sells.
   625  func truncateOrderBook(book *core.OrderBook, nOrders uint64) {
   626  	truncFn := func(orders []*core.MiniOrder) []*core.MiniOrder {
   627  		if uint64(len(orders)) > nOrders {
   628  			// Nullify pointers stored in the unused part of the
   629  			// underlying array to allow for GC.
   630  			for i := nOrders; i < uint64(len(orders)); i++ {
   631  				orders[i] = nil
   632  			}
   633  			orders = orders[:nOrders]
   634  
   635  		}
   636  		return orders
   637  	}
   638  	book.Buys = truncFn(book.Buys)
   639  	book.Sells = truncFn(book.Sells)
   640  }
   641  
   642  // handleOrderBook handles requests for orderbook.
   643  // *msgjson.ResponsePayload.Error is empty if successful.
   644  func handleOrderBook(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   645  	form, err := parseOrderBookArgs(params)
   646  	if err != nil {
   647  		return usage(orderBookRoute, err)
   648  	}
   649  	book, err := s.core.Book(form.host, form.base, form.quote)
   650  	if err != nil {
   651  		resErr := msgjson.NewError(msgjson.RPCOrderBookError, "unable to retrieve order book: %v", err)
   652  		return createResponse(orderBookRoute, nil, resErr)
   653  	}
   654  	if form.nOrders > 0 {
   655  		truncateOrderBook(book, form.nOrders)
   656  	}
   657  	return createResponse(orderBookRoute, book, nil)
   658  }
   659  
   660  // parseCoreOrder converts a *core.Order into a *myOrder.
   661  func parseCoreOrder(co *core.Order, b, q uint32) *myOrder {
   662  	// matchesParser parses core.Match slice & calculates how much of the order
   663  	// has been settled/finalized.
   664  	parseMatches := func(matches []*core.Match) (ms []*match, settled uint64) {
   665  		ms = make([]*match, 0, len(matches))
   666  		// coinSafeString gets the Coin's StringID safely.
   667  		coinSafeString := func(c *core.Coin) string {
   668  			if c == nil {
   669  				return ""
   670  			}
   671  			return c.StringID
   672  		}
   673  		for _, m := range matches {
   674  			// Sum up settled value.
   675  			if (m.Side == order.Maker && m.Status >= order.MakerRedeemed) ||
   676  				(m.Side == order.Taker && m.Status >= order.MatchComplete) {
   677  				settled += m.Qty
   678  			}
   679  			match := &match{
   680  				MatchID:  m.MatchID.String(),
   681  				Status:   m.Status.String(),
   682  				Revoked:  m.Revoked,
   683  				Rate:     m.Rate,
   684  				Qty:      m.Qty,
   685  				Side:     m.Side.String(),
   686  				FeeRate:  m.FeeRate,
   687  				Stamp:    m.Stamp,
   688  				IsCancel: m.IsCancel,
   689  			}
   690  
   691  			match.Swap = coinSafeString(m.Swap)
   692  			match.CounterSwap = coinSafeString(m.CounterSwap)
   693  			match.Redeem = coinSafeString(m.Redeem)
   694  			match.CounterRedeem = coinSafeString(m.CounterRedeem)
   695  			match.Refund = coinSafeString(m.Refund)
   696  			ms = append(ms, match)
   697  		}
   698  		return ms, settled
   699  	}
   700  	srvTime := time.UnixMilli(int64(co.Stamp))
   701  	age := time.Since(srvTime).Round(time.Millisecond)
   702  	cancelling := co.Cancelling
   703  	// If the order is executed, canceled, or revoked, it is no longer cancelling.
   704  	if co.Status >= order.OrderStatusExecuted {
   705  		cancelling = false
   706  	}
   707  	o := &myOrder{
   708  		Host:        co.Host,
   709  		MarketName:  co.MarketID,
   710  		BaseID:      b,
   711  		QuoteID:     q,
   712  		ID:          co.ID.String(),
   713  		Type:        co.Type.String(),
   714  		Sell:        co.Sell,
   715  		Stamp:       co.Stamp,
   716  		SubmitTime:  co.SubmitTime,
   717  		Age:         age.String(),
   718  		Rate:        co.Rate,
   719  		Quantity:    co.Qty,
   720  		Filled:      co.Filled,
   721  		Status:      co.Status.String(),
   722  		Cancelling:  cancelling,
   723  		Canceled:    co.Canceled,
   724  		TimeInForce: co.TimeInForce.String(),
   725  	}
   726  
   727  	// Parse matches & calculate settled value
   728  	o.Matches, o.Settled = parseMatches(co.Matches)
   729  
   730  	return o
   731  }
   732  
   733  // handleMyOrders handles requests for myorders. *msgjson.ResponsePayload.Error
   734  // is empty if successful.
   735  func handleMyOrders(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   736  	form, err := parseMyOrdersArgs(params)
   737  	if err != nil {
   738  		return usage(myOrdersRoute, err)
   739  	}
   740  	var myOrders myOrdersResponse
   741  	filterMkts := form.base != nil && form.quote != nil
   742  	exchanges := s.core.Exchanges()
   743  	for host, exchange := range exchanges {
   744  		if form.host != "" && form.host != host {
   745  			continue
   746  		}
   747  		for _, market := range exchange.Markets {
   748  			if filterMkts && (market.BaseID != *form.base || market.QuoteID != *form.quote) {
   749  				continue
   750  			}
   751  			for _, order := range market.Orders {
   752  				myOrders = append(myOrders, parseCoreOrder(order, market.BaseID, market.QuoteID))
   753  			}
   754  			for _, inFlight := range market.InFlightOrders {
   755  				myOrders = append(myOrders, parseCoreOrder(inFlight.Order, market.BaseID, market.QuoteID))
   756  			}
   757  		}
   758  	}
   759  	return createResponse(myOrdersRoute, myOrders, nil)
   760  }
   761  
   762  // handleAppSeed handles requests for the app seed. *msgjson.ResponsePayload.Error
   763  // is empty if successful.
   764  func handleAppSeed(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   765  	appPass, err := parseAppSeedArgs(params)
   766  	if err != nil {
   767  		return usage(appSeedRoute, err)
   768  	}
   769  	defer appPass.Clear()
   770  	seed, err := s.core.ExportSeed(appPass)
   771  	if err != nil {
   772  		resErr := msgjson.NewError(msgjson.RPCExportSeedError, "unable to retrieve app seed: %v", err)
   773  		return createResponse(appSeedRoute, nil, resErr)
   774  	}
   775  
   776  	return createResponse(appSeedRoute, seed, nil)
   777  }
   778  
   779  // handleDeleteArchivedRecords handles requests for deleting archived records.
   780  // *msgjson.ResponsePayload.Error is empty if successful.
   781  func handleDeleteArchivedRecords(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   782  	form, err := parseDeleteArchivedRecordsArgs(params)
   783  	if err != nil {
   784  		return usage(deleteArchivedRecordsRoute, err)
   785  	}
   786  	nRecordsDeleted, err := s.core.DeleteArchivedRecords(form.olderThan, form.matchesFileStr, form.ordersFileStr)
   787  	if err != nil {
   788  		resErr := msgjson.NewError(msgjson.RPCDeleteArchivedRecordsError, "unable to delete records: %v", err)
   789  		return createResponse(deleteArchivedRecordsRoute, nil, resErr)
   790  	}
   791  
   792  	msg := fmt.Sprintf("%d archived records has been deleted successfully", nRecordsDeleted)
   793  	if nRecordsDeleted <= 0 {
   794  		msg = "No archived records found"
   795  	}
   796  	return createResponse(deleteArchivedRecordsRoute, msg, nil)
   797  }
   798  
   799  func handleWalletPeers(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   800  	assetID, err := parseWalletPeersArgs(params)
   801  	if err != nil {
   802  		return usage(walletPeersRoute, err)
   803  	}
   804  
   805  	peers, err := s.core.WalletPeers(assetID)
   806  	if err != nil {
   807  		resErr := msgjson.NewError(msgjson.RPCWalletPeersError, "unable to get wallet peers: %v", err)
   808  		return createResponse(walletPeersRoute, nil, resErr)
   809  	}
   810  	return createResponse(walletPeersRoute, peers, nil)
   811  }
   812  
   813  func handleAddWalletPeer(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   814  	form, err := parseAddRemoveWalletPeerArgs(params)
   815  	if err != nil {
   816  		return usage(addWalletPeerRoute, err)
   817  	}
   818  
   819  	err = s.core.AddWalletPeer(form.assetID, form.address)
   820  	if err != nil {
   821  		resErr := msgjson.NewError(msgjson.RPCWalletPeersError, "unable to add wallet peer: %v", err)
   822  		return createResponse(addWalletPeerRoute, nil, resErr)
   823  	}
   824  
   825  	return createResponse(addWalletPeerRoute, "successfully added peer", nil)
   826  }
   827  
   828  func handleRemoveWalletPeer(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   829  	form, err := parseAddRemoveWalletPeerArgs(params)
   830  	if err != nil {
   831  		return usage(removeWalletPeerRoute, err)
   832  	}
   833  
   834  	err = s.core.RemoveWalletPeer(form.assetID, form.address)
   835  	if err != nil {
   836  		resErr := msgjson.NewError(msgjson.RPCWalletPeersError, "unable to remove wallet peer: %v", err)
   837  		return createResponse(removeWalletPeerRoute, nil, resErr)
   838  	}
   839  
   840  	return createResponse(removeWalletPeerRoute, "successfully removed peer", nil)
   841  }
   842  
   843  func handleNotifications(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   844  	numNotes, err := parseNotificationsArgs(params)
   845  	if err != nil {
   846  		return usage(notificationsRoute, err)
   847  	}
   848  
   849  	notes, _, err := s.core.Notifications(numNotes)
   850  	if err != nil {
   851  		resErr := msgjson.NewError(msgjson.RPCNotificationsError, "unable to handle notification: %v", err)
   852  		return createResponse(notificationsRoute, nil, resErr)
   853  	}
   854  
   855  	return createResponse(notificationsRoute, notes, nil)
   856  }
   857  
   858  func handleMMAvailableBalances(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   859  	form, err := parseMMAvailableBalancesArgs(params)
   860  	if err != nil {
   861  		return usage(mmAvailableBalancesRoute, err)
   862  	}
   863  
   864  	dexBalances, cexBalances, err := s.mm.AvailableBalances(form.mkt, &form.cfgFilePath)
   865  	if err != nil {
   866  		resErr := msgjson.NewError(msgjson.RPCMMAvailableBalancesError, "unable to get available balances: %v", err)
   867  		return createResponse(mmAvailableBalancesRoute, nil, resErr)
   868  	}
   869  
   870  	res := struct {
   871  		DEXBalances map[uint32]uint64 `json:"dexBalances"`
   872  		CEXBalances map[uint32]uint64 `json:"cexBalances"`
   873  	}{
   874  		DEXBalances: dexBalances,
   875  		CEXBalances: cexBalances,
   876  	}
   877  
   878  	return createResponse(mmAvailableBalancesRoute, res, nil)
   879  }
   880  
   881  func handleStartBot(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   882  	form, err := parseStartBotArgs(params)
   883  	if err != nil {
   884  		return usage(startBotRoute, err)
   885  	}
   886  
   887  	err = s.mm.StartBot(&mm.StartConfig{MarketWithHost: *form.mkt}, &form.cfgFilePath, form.appPass, true)
   888  	if err != nil {
   889  		resErr := msgjson.NewError(msgjson.RPCStartMarketMakingError, "unable to start market making: %v", err)
   890  		return createResponse(startBotRoute, nil, resErr)
   891  	}
   892  
   893  	return createResponse(startBotRoute, "started bot", nil)
   894  }
   895  
   896  func handleStopBot(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   897  	mkt, err := parseStopBotArgs(params)
   898  	if err != nil {
   899  		return usage(startBotRoute, err)
   900  	}
   901  
   902  	err = s.mm.StopBot(mkt)
   903  	if err != nil {
   904  		resErr := msgjson.NewError(msgjson.RPCStopMarketMakingError, "unable to stop market making: %v", err)
   905  		return createResponse(stopBotRoute, nil, resErr)
   906  	}
   907  
   908  	return createResponse(stopBotRoute, "stopped bot", nil)
   909  }
   910  
   911  func handleUpdateRunningBotCfg(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   912  	form, err := parseUpdateRunningBotArgs(params)
   913  	if err != nil {
   914  		return usage(updateRunningBotCfgRoute, err)
   915  	}
   916  
   917  	data, err := os.ReadFile(form.cfgFilePath)
   918  	if err != nil {
   919  		resErr := msgjson.NewError(msgjson.RPCUpdateRunningBotCfgError, "unable to read config file: %v", err)
   920  		return createResponse(updateRunningBotCfgRoute, nil, resErr)
   921  	}
   922  
   923  	cfg := &mm.MarketMakingConfig{}
   924  	err = json.Unmarshal(data, cfg)
   925  	if err != nil {
   926  		resErr := msgjson.NewError(msgjson.RPCUpdateRunningBotCfgError, "unable to unmarshal config: %v", err)
   927  		return createResponse(updateRunningBotCfgRoute, nil, resErr)
   928  	}
   929  
   930  	var botCfg *mm.BotConfig
   931  	for _, bot := range cfg.BotConfigs {
   932  		if bot.Host == form.mkt.Host && bot.BaseID == form.mkt.BaseID && bot.QuoteID == form.mkt.QuoteID {
   933  			botCfg = bot
   934  			break
   935  		}
   936  	}
   937  
   938  	if botCfg == nil {
   939  		resErr := msgjson.NewError(msgjson.RPCUpdateRunningBotCfgError, "bot config not found for market %s", form.mkt.String())
   940  		return createResponse(updateRunningBotCfgRoute, nil, resErr)
   941  	}
   942  
   943  	err = s.mm.UpdateRunningBotCfg(botCfg, form.balances, false)
   944  	if err != nil {
   945  		resErr := msgjson.NewError(msgjson.RPCUpdateRunningBotCfgError, "unable to update running bot: %v", err)
   946  		return createResponse(updateRunningBotCfgRoute, nil, resErr)
   947  	}
   948  
   949  	return createResponse(updateRunningBotCfgRoute, "updated running bot", nil)
   950  }
   951  
   952  func handleUpdateRunningBotInventory(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   953  	form, err := parseUpdateRunningBotInventoryArgs(params)
   954  	if err != nil {
   955  		return usage(updateRunningBotCfgRoute, err)
   956  	}
   957  
   958  	err = s.mm.UpdateRunningBotInventory(form.mkt, form.balances)
   959  	if err != nil {
   960  		resErr := msgjson.NewError(msgjson.RPCUpdateRunningBotInvError, "unable to update running bot: %v", err)
   961  		return createResponse(updateRunningBotCfgRoute, nil, resErr)
   962  	}
   963  
   964  	return createResponse(updateRunningBotCfgRoute, "updated running bot", nil)
   965  }
   966  
   967  func handleMMStatus(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   968  	status := s.mm.RunningBotsStatus()
   969  	return createResponse(mmStatusRoute, status, nil)
   970  }
   971  
   972  func handleSetVSP(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   973  	form, err := parseSetVSPArgs(params)
   974  	if err != nil {
   975  		return usage(setVSPRoute, err)
   976  	}
   977  
   978  	err = s.core.SetVSP(form.assetID, form.addr)
   979  	if err != nil {
   980  		resErr := msgjson.NewError(msgjson.RPCSetVSPError, "unable to set vsp: %v", err)
   981  		return createResponse(setVSPRoute, nil, resErr)
   982  	}
   983  
   984  	return createResponse(setVSPRoute, fmt.Sprintf(setVSPStr, form.addr), nil)
   985  }
   986  
   987  func handlePurchaseTickets(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
   988  	form, err := parsePurchaseTicketsArgs(params)
   989  	if err != nil {
   990  		return usage(purchaseTicketsRoute, err)
   991  	}
   992  	defer form.appPass.Clear()
   993  
   994  	if err = s.core.PurchaseTickets(form.assetID, form.appPass, form.num); err != nil {
   995  		resErr := msgjson.NewError(msgjson.RPCPurchaseTicketsError, "unable to purchase tickets: %v", err)
   996  		return createResponse(purchaseTicketsRoute, nil, resErr)
   997  	}
   998  
   999  	return createResponse(purchaseTicketsRoute, true, nil)
  1000  }
  1001  
  1002  func handleStakeStatus(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
  1003  	assetID, err := parseStakeStatusArgs(params)
  1004  	if err != nil {
  1005  		return usage(stakeStatusRoute, err)
  1006  	}
  1007  	stakeStatus, err := s.core.StakeStatus(assetID)
  1008  	if err != nil {
  1009  		resErr := msgjson.NewError(msgjson.RPCStakeStatusError, "unable to get staking status: %v", err)
  1010  		return createResponse(stakeStatusRoute, nil, resErr)
  1011  	}
  1012  
  1013  	return createResponse(stakeStatusRoute, &stakeStatus, nil)
  1014  }
  1015  
  1016  func handleSetVotingPreferences(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
  1017  	form, err := parseSetVotingPreferencesArgs(params)
  1018  	if err != nil {
  1019  		return usage(setVotingPreferencesRoute, err)
  1020  	}
  1021  
  1022  	err = s.core.SetVotingPreferences(form.assetID, form.voteChoices, form.tSpendPolicy, form.treasuryPolicy)
  1023  	if err != nil {
  1024  		resErr := msgjson.NewError(msgjson.RPCSetVotingPreferencesError, "unable to set voting preferences: %v", err)
  1025  		return createResponse(setVotingPreferencesRoute, nil, resErr)
  1026  	}
  1027  
  1028  	return createResponse(setVotingPreferencesRoute, "vote preferences set", nil)
  1029  }
  1030  
  1031  func handleTxHistory(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
  1032  	form, err := parseTxHistoryArgs(params)
  1033  	if err != nil {
  1034  		return usage(txHistoryRoute, err)
  1035  	}
  1036  
  1037  	txs, err := s.core.TxHistory(form.assetID, form.num, form.refID, form.past)
  1038  	if err != nil {
  1039  		resErr := msgjson.NewError(msgjson.RPCTxHistoryError, "unable to get tx history: %v", err)
  1040  		return createResponse(txHistoryRoute, nil, resErr)
  1041  	}
  1042  
  1043  	return createResponse(txHistoryRoute, txs, nil)
  1044  }
  1045  
  1046  func handleWalletTx(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
  1047  	form, err := parseWalletTxArgs(params)
  1048  	if err != nil {
  1049  		return usage(walletTxRoute, err)
  1050  	}
  1051  
  1052  	tx, err := s.core.WalletTransaction(form.assetID, form.txID)
  1053  	if err != nil {
  1054  		resErr := msgjson.NewError(msgjson.RPCTxHistoryError, "unable to get wallet tx: %v", err)
  1055  		return createResponse(walletTxRoute, nil, resErr)
  1056  	}
  1057  
  1058  	return createResponse(walletTxRoute, tx, nil)
  1059  }
  1060  
  1061  func handleWithdrawBchSpv(s *RPCServer, params *RawParams) *msgjson.ResponsePayload {
  1062  	appPW, recipient, err := parseBchWithdrawArgs(params)
  1063  	if err != nil {
  1064  		return usage(withdrawBchSpvRoute, err)
  1065  	}
  1066  	defer appPW.Clear()
  1067  
  1068  	txB, err := s.core.GenerateBCHRecoveryTransaction(appPW, recipient)
  1069  	if err != nil {
  1070  		resErr := msgjson.NewError(msgjson.RPCCreateWalletError, "error generating tx: %v", err)
  1071  		return createResponse(withdrawBchSpvRoute, nil, resErr)
  1072  	}
  1073  
  1074  	return createResponse(withdrawBchSpvRoute, dex.Bytes(txB).String(), nil)
  1075  }
  1076  
  1077  // format concatenates thing and tail. If thing is empty, returns an empty
  1078  // string.
  1079  func format(thing, tail string) string {
  1080  	if thing == "" {
  1081  		return ""
  1082  	}
  1083  	return fmt.Sprintf("%s%s", thing, tail)
  1084  }
  1085  
  1086  // ListCommands prints a short usage string for every route available to the
  1087  // rpcserver.
  1088  func ListCommands(includePasswords bool) string {
  1089  	var sb strings.Builder
  1090  	var err error
  1091  	for _, r := range sortHelpKeys() {
  1092  		msg := helpMsgs[r]
  1093  		// If help should include password arguments and this command
  1094  		// has password arguments, add them to the help message.
  1095  		if includePasswords && msg.pwArgsShort != "" {
  1096  			_, err = sb.WriteString(fmt.Sprintf("%s %s%s\n", r,
  1097  				format(msg.pwArgsShort, " "), msg.argsShort))
  1098  		} else {
  1099  			_, err = sb.WriteString(fmt.Sprintf("%s %s\n", r, msg.argsShort))
  1100  		}
  1101  		if err != nil {
  1102  			log.Errorf("unable to parse help message for %s", r)
  1103  			return ""
  1104  		}
  1105  	}
  1106  	s := sb.String()
  1107  	// Remove trailing newline.
  1108  	return s[:len(s)-1]
  1109  }
  1110  
  1111  // commandUsage returns a help message for cmd or an error if cmd is unknown.
  1112  func commandUsage(cmd string, includePasswords bool) (string, error) {
  1113  	msg, exists := helpMsgs[cmd]
  1114  	if !exists {
  1115  		return "", fmt.Errorf("%w: %s", errUnknownCmd, cmd)
  1116  	}
  1117  	// If help should include password arguments and this command has
  1118  	// password arguments, return them as part of the help message.
  1119  	if includePasswords && msg.pwArgsShort != "" {
  1120  		return fmt.Sprintf("%s %s%s\n\n%s\n\n%s%s%s",
  1121  			cmd, format(msg.pwArgsShort, " "), msg.argsShort,
  1122  			msg.cmdSummary, format(msg.pwArgsLong, "\n\n"), format(msg.argsLong, "\n\n"),
  1123  			msg.returns), nil
  1124  	}
  1125  	return fmt.Sprintf("%s %s\n\n%s\n\n%s%s", cmd, msg.argsShort,
  1126  		msg.cmdSummary, format(msg.argsLong, "\n\n"), msg.returns), nil
  1127  }
  1128  
  1129  // sortHelpKeys returns a sorted list of helpMsgs keys.
  1130  func sortHelpKeys() []string {
  1131  	keys := make([]string, 0, len(helpMsgs))
  1132  	for k := range helpMsgs {
  1133  		keys = append(keys, k)
  1134  	}
  1135  	sort.Slice(keys, func(i, j int) bool {
  1136  		return keys[i] < keys[j]
  1137  	})
  1138  	return keys
  1139  }
  1140  
  1141  type helpMsg struct {
  1142  	pwArgsShort, argsShort, cmdSummary, pwArgsLong, argsLong, returns string
  1143  }
  1144  
  1145  // helpMsgs are a map of routes to help messages. They are broken down into six
  1146  // sections.
  1147  // In descending order:
  1148  //  1. Password argument example inputs. These are arguments the caller may not
  1149  //     want to echo listed in order of input.
  1150  //  2. Argument example inputs. These are non-sensitive arguments listed in
  1151  //     order of input.
  1152  //  3. A description of the command.
  1153  //  4. An extensive breakdown of the password arguments.
  1154  //  5. An extensive breakdown of the arguments.
  1155  //  6. An extensive breakdown of the returned values.
  1156  var helpMsgs = map[string]helpMsg{
  1157  	helpRoute: {
  1158  		pwArgsShort: ``,                           // password args example input
  1159  		argsShort:   `("cmd") (includePasswords)`, // args example input
  1160  		cmdSummary:  `Print a help message.`,      // command explanation
  1161  		pwArgsLong:  ``,                           // password args breakdown
  1162  		argsLong: `Args:
  1163      cmd (string): Optional. The command to print help for.
  1164      includePasswords (bool): Optional. Default is false. Whether to include
  1165        password arguments in the returned help.`, // args breakdown
  1166  		returns: `Returns:
  1167      string: The help message for command.`, // returns breakdown
  1168  	},
  1169  	versionRoute: {
  1170  		cmdSummary: `Print the Bison Wallet rpcserver version.`,
  1171  		returns: `Returns:
  1172      string: The Bison Wallet rpcserver version.`,
  1173  	},
  1174  	discoverAcctRoute: {
  1175  		pwArgsShort: `"appPass"`,
  1176  		argsShort:   `"addr" ("cert")`,
  1177  		cmdSummary: `Discover an account that is used for a dex. Useful when restoring
  1178      an account and can be used in place of register. Will error if
  1179      the account has already been discovered/restored.`,
  1180  		pwArgsLong: `Password Args:
  1181      appPass (string): The Bison Wallet password.`,
  1182  		argsLong: `Args:
  1183      addr (string): The DEX address to discover an account for.
  1184      cert (string): Optional. The TLS certificate path.`,
  1185  		returns: `Returns:
  1186      bool: True if the account has has been registered and paid for.`,
  1187  	},
  1188  	initRoute: {
  1189  		pwArgsShort: `"appPass"`,
  1190  		argsShort:   `("seed")`,
  1191  		cmdSummary:  `Initialize the client.`,
  1192  		pwArgsLong: `Password Args:
  1193      appPass (string): The Bison Wallet password.`,
  1194  		argsLong: `Args:
  1195      seed (string): Optional. hex-encoded 512-bit restoration seed.`,
  1196  		returns: `Returns:
  1197      string: The message "` + initializedStr + `"`,
  1198  	},
  1199  	deleteArchivedRecordsRoute: {
  1200  		argsShort: `("unix time milli") ("matches csv path") ("orders csv path")`,
  1201  		cmdSummary: `Delete archived records from the database and returns total deleted. Optionally
  1202  	set a time to delete records before and file paths to save deleted records as comma separated
  1203      values. Note that file locations are from the perspective of dexc and not the caller.`,
  1204  		argsLong: `Args:
  1205      unix time milli (int): Optional. If set deletes records before the date in unix time
  1206        in milliseconds (not seconds). Unset or 0 will default to the current time.
  1207      matches csv path (string): Optional. A path to save a csv with deleted matches.
  1208        Will not save by default.
  1209      orders csv path (string): Optional. A path to save a csv with deleted orders.
  1210        Will not save by default.`,
  1211  		returns: `Returns:
  1212      Nothing.`,
  1213  	},
  1214  	bondAssetsRoute: {
  1215  		argsShort:  `"dex" ("cert")`,
  1216  		cmdSummary: `Get dex bond asset config.`,
  1217  		argsLong: `Args:
  1218      dex (string): The dex address to get bond info for.
  1219      cert (string): Optional. The TLS certificate path.`,
  1220  		returns: `Returns:
  1221      obj: The getBondAssets result.
  1222      {
  1223        "expiry" (int): Bond expiry in seconds remaining until locktime.
  1224  	  "assets" (object): {
  1225  		"id" (int): The BIP-44 coin type for the asset.
  1226  		"confs" (int): The required confirmations for the bond transaction.
  1227  		"amount" (int): The minimum bond amount.
  1228  	  }
  1229      }`,
  1230  	},
  1231  	getDEXConfRoute: {
  1232  		argsShort:  `"dex" ("cert")`,
  1233  		cmdSummary: `Get a DEX configuration.`,
  1234  		argsLong: `Args:
  1235      dex (string): The dex address to get config for.
  1236      cert (string): Optional. The TLS certificate path.`,
  1237  		returns: `Returns:
  1238      obj: The getdexconfig result. See the 'exchanges' result.`,
  1239  	},
  1240  	newWalletRoute: {
  1241  		pwArgsShort: `"appPass" "walletPass"`,
  1242  		argsShort:   `assetID walletType ("path" "settings")`,
  1243  		cmdSummary:  `Connect to a new wallet.`,
  1244  		pwArgsLong: `Password Args:
  1245      appPass (string): The Bison Wallet password.
  1246      walletPass (string): The wallet's password. Leave the password empty for wallets without a password set.`,
  1247  		argsLong: `Args:
  1248      assetID (int): The asset's BIP-44 registered coin index. e.g. 42 for DCR.
  1249        See https://github.com/satoshilabs/slips/blob/master/slip-0044.md
  1250      walletType (string): The wallet type.
  1251      path (string): Optional. The path to a configuration file.
  1252      settings (string): A JSON-encoded string->string mapping of additional
  1253         configuration settings. These settings take precedence over any settings
  1254         parsed from file. e.g. '{"account":"default"}' for Decred accounts, and
  1255         '{"walletname":""}' for the default Bitcoin wallet where bitcoind's listwallets RPC gives possible walletnames.`,
  1256  		returns: `Returns:
  1257      string: The message "` + fmt.Sprintf(walletCreatedStr, "[coin symbol]") + `"`,
  1258  	},
  1259  	openWalletRoute: {
  1260  		pwArgsShort: `"appPass"`,
  1261  		argsShort:   `assetID`,
  1262  		cmdSummary:  `Open an existing wallet.`,
  1263  		pwArgsLong: `Password Args:
  1264      appPass (string): The Bison Wallet password.`,
  1265  		argsLong: `Args:
  1266      assetID (int): The asset's BIP-44 registered coin index. e.g. 42 for DCR.
  1267        See https://github.com/satoshilabs/slips/blob/master/slip-0044.md`,
  1268  		returns: `Returns:
  1269      string: The message "` + fmt.Sprintf(walletUnlockedStr, "[coin symbol]") + `"`,
  1270  	},
  1271  	closeWalletRoute: {
  1272  		argsShort:  `assetID`,
  1273  		cmdSummary: `Close an open wallet.`,
  1274  		argsLong: `Args:
  1275      assetID (int): The asset's BIP-44 registered coin index. e.g. 42 for DCR.
  1276        See https://github.com/satoshilabs/slips/blob/master/slip-0044.md`,
  1277  		returns: `Returns:
  1278      string: The message "` + fmt.Sprintf(walletLockedStr, "[coin symbol]") + `"`,
  1279  	},
  1280  	toggleWalletStatusRoute: {
  1281  		pwArgsShort: "appPass",
  1282  		argsShort:   `assetID disable`,
  1283  		cmdSummary: `Disable or enable an existing wallet. When disabling a chain's primary asset wallet,
  1284  	all token wallets for that chain will be disabled too.`,
  1285  		pwArgsLong: `Password Args:
  1286      appPass (string): The Bison Wallet password.`,
  1287  		argsLong: `Args:
  1288     assetID (int): The asset's BIP-44 registered coin index. e.g. 42 for DCR.
  1289                    See https://github.com/satoshilabs/slips/blob/master/slip-0044.md
  1290    disable (bool): The wallet's status. e.g To disable a wallet set to "true", to enable set to "false".`,
  1291  		returns: `Returns:
  1292      string: The message "` + fmt.Sprintf(walletStatusStr, "[coin symbol]", "[wallet status]") + `".`,
  1293  	},
  1294  	walletsRoute: {
  1295  		cmdSummary: `List all wallets.`,
  1296  		returns: `Returns:
  1297      array: An array of wallet results.
  1298      [
  1299        {
  1300          "symbol" (string): The coin symbol.
  1301          "assetID" (int): The asset's BIP-44 registered coin index. e.g. 42 for DCR.
  1302            See https://github.com/satoshilabs/slips/blob/master/slip-0044.md
  1303          "open" (bool): Whether the wallet is unlocked.
  1304          "running" (bool): Whether the wallet is running.
  1305  		"disabled" (bool): Whether the wallet is disabled.
  1306          "updated" (int): Unix time of last balance update. Seconds since 00:00:00 Jan 1 1970.
  1307          "balance" (obj): {
  1308            "available" (int): The balance available for funding orders case.
  1309            "immature" (int): Balance that requires confirmations before use.
  1310            "locked" (int): The total locked balance.
  1311            "stamp" (string): Time stamp.
  1312          }
  1313          "address" (string): A wallet address.
  1314          "feerate" (int): The fee rate.
  1315          "units" (string): Unit of measure for amounts.
  1316        },...
  1317      ]`,
  1318  	},
  1319  	postBondRoute: {
  1320  		pwArgsShort: `"appPass"`,
  1321  		argsShort:   `"addr" bond assetID (maintain "cert")`,
  1322  		cmdSummary: `Post new bond for DEX. An ok response does not mean that the bond is active.
  1323  		Bond is active after the bond transaction has been confirmed and the server notified.`,
  1324  		pwArgsLong: `Password Args:
  1325      appPass (string): The Bison Wallet password.`,
  1326  		argsLong: `Args:
  1327      addr (string): The DEX address to post bond for for.
  1328      bond (int): The bond amount (in DCR presently).
  1329      assetID (int): The asset ID with which to pay the fee.
  1330      maintain (bool): Optional. Whether to maintain the trading tier established by this bond. Only applicable when registering. (default is true)
  1331      cert (string): Optional. The TLS certificate path. Only applicable when registering.`,
  1332  		returns: `Returns:
  1333      {
  1334        "bondID" (string): The bond transactions's txid and output index.
  1335        "reqConfirms" (int): The number of confirmations required to start trading.
  1336      }`,
  1337  	},
  1338  	bondOptionsRoute: {
  1339  		argsShort:  `"addr" targetTier (maxBondedAmt bondAssetID penaltyComps)`,
  1340  		cmdSummary: `Change bond options for a DEX.`,
  1341  		argsLong: `Args:
  1342      addr (string): The DEX address to post bond for for.
  1343      targetTier (int): The target trading tier.
  1344      maxBondedAmt (int): The maximum amount that may be locked in bonds.
  1345      bondAssetID (int): The asset ID with which to auto-post bonds.
  1346      penaltyComp (int): The maximum number of penalties to compensate`,
  1347  		returns: `Returns: "ok"`,
  1348  	},
  1349  	exchangesRoute: {
  1350  		cmdSummary: `Detailed information about known exchanges and markets.`,
  1351  		returns: `Returns:
  1352      obj: The exchanges result.
  1353      {
  1354        "[DEX host]": {
  1355          "acctID" (string):  The client's account ID associated with this DEX.,
  1356          "markets": {
  1357            "[assetID-assetID]": {
  1358              "baseid" (int): The base asset ID
  1359              "basesymbol" (string): The base ticker symbol.
  1360              "quoteid" (int): The quote asset ID.
  1361              "quotesymbol" (string): The quote asset ID symbol,
  1362              "epochlen" (int): Duration of a epoch in milliseconds.
  1363              "startepoch" (int): Time of start of the last epoch in milliseconds
  1364  	      since 00:00:00 Jan 1 1970.
  1365              "buybuffer" (float): The minimum order size for a market buy order.
  1366            },...
  1367          },
  1368          "assets": {
  1369            "[assetID]": {
  1370              "symbol" (string): The asset's coin symbol.
  1371              "lotSize" (int): The amount of units of a coin in one lot.
  1372              "rateStep" (int): the price rate increment in atoms.
  1373              "feeRate" (int): The transaction fee in atoms per byte.
  1374              "swapSize" (int): The size of a swap transaction in bytes.
  1375              "swapSizeBase" (int): The size of a swap transaction minus inputs in bytes.
  1376              "swapConf" (int): The number of confirmations needed to confirm
  1377  	      trade transactions.
  1378            },...
  1379          },
  1380          "regFees": {
  1381            "[assetSymbol]": {
  1382              "id" (int): The asset's BIP-44 coin ID.
  1383              "confs" (int): The number of confirmations required.
  1384              "amt" (int): The fee amount.
  1385            },...
  1386          }
  1387        },...
  1388      }`,
  1389  	},
  1390  	loginRoute: {
  1391  		pwArgsShort: `"appPass"`,
  1392  		cmdSummary:  `Attempt to login to all registered DEX servers.`,
  1393  		pwArgsLong: `Password Args:
  1394      appPass (string): The Bison Wallet password.`,
  1395  		returns: `Returns:
  1396      obj: A map of notifications and dexes.
  1397      {
  1398        "notifications" (array): An array of most recent notifications.
  1399        [
  1400          {
  1401            "type" (string): The notification type.
  1402            "subject" (string): A clarification of type.
  1403            "details"(string): The notification details.
  1404            "severity" (int): The importance of the notification on a scale of 0
  1405              through 5.
  1406            "stamp" (int): Unix time of the notification. Seconds since 00:00:00 Jan 1 1970.
  1407            "acked" (bool): Whether the notification was acknowledged.
  1408            "id" (string): A unique hex ID.
  1409          },...
  1410        ],
  1411        "dexes" (array): An array of login attempted dexes.
  1412        [
  1413          {
  1414            "host" (string): The DEX address.
  1415            "acctID" (string): A unique hex ID.
  1416            "authed" (bool): Whether the dex has been successfully authed.
  1417            "autherr" (string): Omitted if authed. If not authed, the reason.
  1418            "tradeIDs" (array): An array of active trade IDs.
  1419          },...
  1420        ]
  1421      }`,
  1422  	},
  1423  	tradeRoute: {
  1424  		pwArgsShort: `"appPass"`,
  1425  		argsShort:   `"host" isLimit sell base quote qty rate immediate`,
  1426  		cmdSummary:  `Make an order to buy or sell an asset.`,
  1427  		pwArgsLong: `Password Args:
  1428      appPass (string): The Bison Wallet password.`,
  1429  		argsLong: `Args:
  1430      host (string): The DEX to trade on.
  1431      isLimit (bool): Whether the order is a limit order.
  1432      sell (bool): Whether the order is selling.
  1433      base (int): The BIP-44 coin index for the market's base asset.
  1434      quote (int): The BIP-44 coin index for the market's quote asset.
  1435      qty (int): The number of units to buy/sell. Must be a multiple of the lot size.
  1436      rate (int): The atoms quote asset to pay/accept per unit base asset. e.g.
  1437        156000 satoshi/DCR for the DCR(base)_BTC(quote).
  1438      immediate (bool): Require immediate match. Do not book the order.
  1439      options (string): A JSON-encoded string->string mapping of additional
  1440         trade options.`,
  1441  		returns: `Returns:
  1442      obj: The order details.
  1443      {
  1444        "orderid" (string): The order's unique hex identifier.
  1445        "sig" (string): The DEX's signature of the order information.
  1446        "stamp" (int): The time the order was signed in milliseconds since 00:00:00
  1447          Jan 1 1970.
  1448      }`,
  1449  	},
  1450  	multiTradeRoute: {
  1451  		pwArgsShort: `"appPass"`,
  1452  		argsShort:   `"host" sell base quote maxLock [[qty,rate]] options`,
  1453  		cmdSummary:  `Place multiple orders in one go.`,
  1454  		pwArgsLong: `Password Args:
  1455      appPass (string): The Bison Wallet password.`,
  1456  		argsLong: `Args:
  1457      host (string): The DEX to trade on.
  1458      sell (bool): Whether the order is selling.
  1459      base (int): The BIP-44 coin index for the market's base asset.
  1460      quote (int): The BIP-44 coin index for the market's quote asset.
  1461      maxLock (int): The maximum amount the wallet can lock for this order. 0 means no limit.
  1462      placements ([[int,int]]):  An array of [qty,rate] placements. Quantity must be
  1463  	 a multiple of the lot size. Rate must be in atomic units of the quote asset.
  1464      options (string): A JSON-encoded string->string mapping of additional
  1465         trade options.`,
  1466  		returns: `Returns:
  1467      obj: The details of each order.
  1468      [{
  1469        "orderid" (string): The order's unique hex identifier.
  1470        "sig" (string): The DEX's signature of the order information.
  1471        "stamp" (int): The time the order was signed in milliseconds since 00:00:00
  1472          Jan 1 1970.
  1473      }]`,
  1474  	},
  1475  	cancelRoute: {
  1476  		pwArgsShort: `"appPass"`,
  1477  		argsShort:   `"orderID"`,
  1478  		cmdSummary:  `Cancel an order.`,
  1479  		pwArgsLong: `Password Args:
  1480      appPass (string): The Bison Wallet password.`,
  1481  		argsLong: `Args:
  1482      orderID (string): The hex ID of the order to cancel`,
  1483  		returns: `Returns:
  1484      string: The message "` + fmt.Sprintf(canceledOrderStr, "[order ID]") + `"`,
  1485  	},
  1486  	rescanWalletRoute: {
  1487  		argsShort: `assetID (force)`,
  1488  		cmdSummary: `Initiate a rescan of an asset's wallet. This is only supported for certain
  1489  wallet types. Wallet resynchronization may be asynchronous, and the wallet
  1490  state should be consulted for progress.
  1491  
  1492  WARNING: It is ill-advised to initiate a wallet rescan with active orders
  1493  unless as a last ditch effort to get the wallet to recognize a transaction
  1494  needed to complete a swap.`,
  1495  		returns: `Returns:
  1496      string: "started"`,
  1497  		argsLong: `Args:
  1498      assetID (int): The asset's BIP-44 registered coin index. Used to identify
  1499        which wallet to withdraw from. e.g. 42 for DCR. See
  1500        https://github.com/satoshilabs/slips/blob/master/slip-0044.md
  1501      force (bool): Force a wallet rescan even if their are active orders. The
  1502        default is false.`,
  1503  	},
  1504  	withdrawRoute: {
  1505  		pwArgsShort: `"appPass"`,
  1506  		argsShort:   `assetID value "address"`,
  1507  		cmdSummary:  `Withdraw value from an exchange wallet to address. Fees are subtracted from the value.`,
  1508  		pwArgsLong: `Password Args:
  1509      appPass (string): The Bison Wallet password.`,
  1510  		argsLong: `Args:
  1511      assetID (int): The asset's BIP-44 registered coin index. Used to identify
  1512        which wallet to withdraw from. e.g. 42 for DCR. See
  1513        https://github.com/satoshilabs/slips/blob/master/slip-0044.md
  1514      value (int): The amount to withdraw in units of the asset's smallest
  1515        denomination (e.g. satoshis, atoms, etc.)"
  1516      address (string): The address to which withdrawn funds are sent.`,
  1517  		returns: `Returns:
  1518      string: "[coin ID]"`,
  1519  	},
  1520  	sendRoute: {
  1521  		pwArgsShort: `"appPass"`,
  1522  		argsShort:   `assetID value "address"`,
  1523  		cmdSummary:  `Sends exact value from an exchange wallet to address.`,
  1524  		pwArgsLong: `Password Args:
  1525      appPass (string): The Bison Wallet password.`,
  1526  		argsLong: `Args:
  1527      assetID (int): The asset's BIP-44 registered coin index. Used to identify
  1528        which wallet to withdraw from. e.g. 42 for DCR. See
  1529        https://github.com/satoshilabs/slips/blob/master/slip-0044.md
  1530      value (int): The amount to send in units of the asset's smallest
  1531        denomination (e.g. satoshis, atoms, etc.)"
  1532      address (string): The address to which funds are sent.`,
  1533  		returns: `Returns:
  1534      string: "[coin ID]"`,
  1535  	},
  1536  	logoutRoute: {
  1537  		cmdSummary: `Logout of Bison Wallet.`,
  1538  		returns: `Returns:
  1539      string: The message "` + logoutStr + `"`,
  1540  	},
  1541  	orderBookRoute: {
  1542  		argsShort:  `"host" base quote (nOrders)`,
  1543  		cmdSummary: `Retrieve all orders for a market.`,
  1544  		argsLong: `Args:
  1545      host (string): The DEX to retrieve the order book from.
  1546      base (int): The BIP-44 coin index for the market's base asset.
  1547      quote (int): The BIP-44 coin index for the market's quote asset.
  1548      nOrders (int): Optional. Default is 0, which returns all orders. The number
  1549        of orders from the top of buys and sells to return. Epoch orders are not
  1550        truncated.`,
  1551  		returns: `Returns:
  1552      obj: A map of orders.
  1553      {
  1554        "sells" (array): An array of booked sell orders.
  1555        [
  1556          {
  1557            "qty" (float): The number of coins base asset being sold.
  1558            "rate" (float): The coins quote asset to pay per coin base asset.
  1559            "sell" (bool): Always true because this is a sell order.
  1560            "token" (string): The first 8 bytes of the order id, coded in hex.
  1561          },...
  1562        ],
  1563        "buys" (array): An array of booked buy orders.
  1564        [
  1565          {
  1566            "qty" (float): The number of coins base asset being bought.
  1567            "rate" (float): The coins quote asset to accept per coin base asset.
  1568            "sell" (bool): Always false because this is a buy order.
  1569            "token" (string): The first 8 bytes of the order id, coded in hex.
  1570          },...
  1571        ],
  1572        "epoch" (array): An array of epoch orders. Epoch orders include all kinds
  1573          of orders, even those that cannot or may not be booked. They are not
  1574          truncated.
  1575        [
  1576          {
  1577            "qty" (float): The number of coins base asset being bought or sold.
  1578            "rate" (float): The coins quote asset to accept per coin base asset.
  1579            "sell" (bool): Whether this order is a sell order.
  1580            "token" (string): The first 8 bytes of the order id, coded in hex.
  1581            "epoch" (int): The order's epoch.
  1582          },...
  1583        ],
  1584      }`,
  1585  	},
  1586  	myOrdersRoute: {
  1587  		argsShort: `("host") (base) (quote)`,
  1588  		cmdSummary: `Fetch all active and recently executed orders
  1589      belonging to the user.`,
  1590  		argsLong: `Args:
  1591      host (string): Optional. The DEX to show orders from.
  1592      base (int): Optional. The BIP-44 coin index for the market's base asset.
  1593      quote (int): Optional. The BIP-44 coin index for the market's quote asset.`,
  1594  		returns: `Returns:
  1595    array: An array of orders.
  1596    [
  1597      {
  1598        "host" (string): The DEX address.
  1599        "marketName" (string): The market's name. e.g. "DCR_BTC".
  1600        "baseID" (int): The market's base asset BIP-44 coin index. e.g. 42 for DCR.
  1601        "quoteID" (int): The market's quote asset BIP-44 coin index. e.g. 0 for BTC.
  1602        "id" (string): The order's unique hex ID.
  1603        "type" (string): The type of order. "limit", "market", or "cancel".
  1604        "sell" (string): Whether this order is selling.
  1605        "stamp" (int): Server's time stamp of the order in milliseconds since 00:00:00 Jan 1 1970.
  1606        "submitTime" (int): Time of order submission, also in milliseconds.
  1607        "age" (string): The time that this order has been active in human readable form.
  1608        "rate" (int): The exchange rate limit. Limit orders only. Units: quote
  1609          asset per unit base asset.
  1610        "quantity" (int): The amount being traded.
  1611        "filled" (int): The order quantity that has matched.
  1612        "settled" (int): The sum quantity of all completed matches.
  1613        "status" (string): The status of the order. "epoch", "booked", "executed",
  1614          "canceled", or "revoked".
  1615        "cancelling" (bool): Whether this order is in the process of cancelling.
  1616        "canceled" (bool): Whether this order has been canceled.
  1617        "tif" (string): "immediate" if this limit order will only match for one epoch.
  1618          "standing" if the order can continue matching until filled or cancelled.
  1619        "matches": (array): An array of matches associated with the order.
  1620        [
  1621          {
  1622            "matchID (string): The match's ID."
  1623            "status" (string): The match's status."
  1624            "revoked" (bool): Indicates if match was revoked.
  1625            "rate"    (int): The match's rate.
  1626            "qty"     (int): The match's amount.
  1627            "side"    (string): The match's side, "maker" or "taker".
  1628            "feerate" (int): The match's fee rate.
  1629            "swap"    (string): The match's swap transaction.
  1630            "counterSwap" (string): The match's counter swap transaction.
  1631            "redeem" (string): The match's redeem transaction.
  1632            "counterRedeem" (string): The match's counter redeem transaction.
  1633            "refund" (string): The match's refund transaction.
  1634            "stamp" (int): The match's stamp.
  1635            "isCancel" (bool): Indicates if match is canceled.
  1636          },...
  1637        ]
  1638      },...
  1639    ]`,
  1640  	},
  1641  	appSeedRoute: {
  1642  		pwArgsShort: `"appPass"`,
  1643  		cmdSummary: `Show the application's seed. It is recommended to not store the seed
  1644    digitally. Make a copy on paper with pencil and keep it safe.`,
  1645  		pwArgsLong: `Password Args:
  1646      appPass (string): The Bison Wallet password.`,
  1647  		returns: `Returns:
  1648      string: The application's seed as hex.`,
  1649  	},
  1650  	walletPeersRoute: {
  1651  		cmdSummary: `Show the peers a wallet is connected to.`,
  1652  		argsShort:  `(assetID)`,
  1653  		argsLong: `Args:
  1654  		assetID (int): The asset's BIP-44 registered coin index. Used to identify
  1655  		which wallet's peers to return.`,
  1656  		returns: `Returns:
  1657  		[]string: Addresses of wallet peers.`,
  1658  	},
  1659  	addWalletPeerRoute: {
  1660  		cmdSummary: `Add a new wallet peer connection.`,
  1661  		argsShort:  `(assetID) (addr)`,
  1662  		argsLong: `Args:
  1663  		assetID (int): The asset's BIP-44 registered coin index. Used to identify
  1664  		which wallet to add a peer.
  1665  		addr (string): The peer's address (host:port).`,
  1666  	},
  1667  	removeWalletPeerRoute: {
  1668  		cmdSummary: `Remove an added wallet peer.`,
  1669  		argsShort:  `(assetID) (addr)`,
  1670  		argsLong: `Args:
  1671  		assetID (int): The asset's BIP-44 registered coin index. Used to identify
  1672  		which wallet to add a peer.
  1673  		addr (string): The peer's address (host:port).`,
  1674  	},
  1675  	notificationsRoute: {
  1676  		cmdSummary: `See recent notifications.`,
  1677  		argsShort:  `(num)`,
  1678  		argsLong: `Args:
  1679  		num (int): The number of notifications to load.`,
  1680  	},
  1681  	startBotRoute: {
  1682  		cmdSummary: `Start market making.`,
  1683  		argsShort:  `(cfgPath) (host) (baseID) (quoteID) (dexBals) (dexBals)`,
  1684  		argsLong: `Args:
  1685  		cfgPath (string): The path to the market maker config file.
  1686  		host (string): The DEX address.
  1687  		baseID (int): The base asset's BIP-44 registered coin index.
  1688  		quoteID (int): The quote asset's BIP-44 registered coin index.
  1689  		dexBalances (array): The DEX balances i.e. [[60,1000000],[42,10000000]].
  1690  		cexBalances (array): The CEX balances i.e. [[60,1000000],[42,10000000]].`,
  1691  	},
  1692  	stopBotRoute: {
  1693  		cmdSummary: `Stop market making.`,
  1694  		argsShort:  `(host) (baseID) (quoteID)`,
  1695  		argsLong: `Args:
  1696  		host (string): The DEX address.
  1697  		baseID (int): The base asset's BIP-44 registered coin index.
  1698  		quoteID (int): The quote asset's BIP-44 registered coin index.`,
  1699  	},
  1700  	mmAvailableBalancesRoute: {
  1701  		cmdSummary: `Get available balances for starting a bot or adding additional balance to a running bot.`,
  1702  		argsShort:  `(cfgPath) (host) (baseID) (quoteID)`,
  1703  		argsLong: `Args:
  1704  		cfgPath (string): The path to the market maker config file.
  1705  		host (string): The DEX address.
  1706  		baseID (int): The base asset's BIP-44 registered coin index.
  1707  		quoteID (int): The quote asset's BIP-44 registered coin index.`,
  1708  	},
  1709  	mmStatusRoute: {
  1710  		cmdSummary: `Get market making status.`,
  1711  	},
  1712  	updateRunningBotCfgRoute: {
  1713  		cmdSummary: `Update the config and optionally the inventory of a running bot`,
  1714  		argsShort:  `(cfgPath) (host) (baseID) (quoteID) (dexInventory) (cexInventory)`,
  1715  		argsLong: `Args:
  1716  		cfgPath (string): The path to the market maker config file.
  1717  		host (string): The DEX address.
  1718  		baseID (int): The base asset's BIP-44 registered coin index.
  1719  		quoteID (int): The quote asset's BIP-44 registered coin index.
  1720  		dexInventory (obj): (optional) The DEX inventory adjustments i.e. [[60,-1000000],[42,10000000]].
  1721  		cexInventory (obj): (optional) The CEX inventory adjustments i.e. [[60,-1000000],[42,10000000]].`,
  1722  	},
  1723  	updateRunningBotInvRoute: {
  1724  		cmdSummary: `Update the inventory of a running bot`,
  1725  		argsShort:  `(host) (baseID) (quoteID) (dexInventory) (cexInventory)`,
  1726  		argsLong: `Args:
  1727  		host (string): The DEX address.
  1728  		baseID (int): The base asset's BIP-44 registered coin index.
  1729  		quoteID (int): The quote asset's BIP-44 registered coin index.
  1730  		dexInventory (obj): The DEX inventory adjustments i.e. [[60,-1000000],[42,10000000]].
  1731  		cexInventory (obj): The CEX inventory adjustments i.e. [[60,-1000000],[42,10000000]].`,
  1732  	},
  1733  	stakeStatusRoute: {
  1734  		cmdSummary: `Get stake status. `,
  1735  		argsShort:  `assetID`,
  1736  		argsLong: `Args:
  1737    assetID (int): The asset's BIP-44 registered coin index.`,
  1738  		returns: `Returns:
  1739    obj: The staking status.
  1740      {
  1741        ticketPrice (uint64): The current ticket price in atoms.
  1742        vsp (string): The url of the currently set vsp (voting service provider).
  1743        isRPC (bool): Whether the wallet is an RPC wallet. False indicates
  1744  an spv wallet and enables options to view and set the vsp.
  1745        tickets (array): An array of ticket objects.
  1746        [
  1747          {
  1748            tx (obj): Ticket transaction data.
  1749            {
  1750              hash (string): The ticket hash as hex.
  1751              ticketPrice (int): The amount paid for the ticket in atoms.
  1752              fees (int): The ticket transaction's tx fee.
  1753              stamp (int): The UNIX time the ticket was purchased.
  1754              blockHeight (int): The block number the ticket was mined.
  1755            },
  1756            status: (int) The ticket status. 0: unknown, 1: unmined, 2: immature, 3: live,
  1757                          4: voted, 5: missed, 6:expired, 7: unspent, 8: revoked.
  1758            spender (string): The transaction that votes on or revokes the ticket if available.
  1759         },
  1760       ],...
  1761       stances (obj): Voting policies.
  1762       {
  1763         agendas (array): An array of consensus vote choices.
  1764         [
  1765           {
  1766             id (string): The agenda ID,
  1767             description (string): A description of the agenda being voted on.
  1768             currentChoice (string): Your current choice.
  1769             choices ([{id: "string", description: "string"}, ...]): A description of the available choices.
  1770           },
  1771         ],...
  1772         tspends (array): An array of TSpend policies.
  1773         [
  1774           {
  1775             hash (string): The TSpend txid.,
  1776             value (int): The total value send in the tspend.,
  1777             currentValue (string): The policy.
  1778           },
  1779         ],...
  1780         treasuryKeys (array): An array of treasury policies.
  1781         [
  1782           {
  1783             key (string): The pubkey of the tspend creator.
  1784             policy (string): The policy.
  1785           },
  1786         ],...
  1787       }
  1788    }`,
  1789  	},
  1790  	setVSPRoute: {
  1791  		argsShort:  `assetID "addr"`,
  1792  		cmdSummary: `Set a vsp by url.`,
  1793  		argsLong: `Args:
  1794    assetID (int): The asset's BIP-44 registered coin index.
  1795    addr (string): The vsp's url.`,
  1796  		returns: `Returns:
  1797    string: The message "` + fmt.Sprintf(setVSPStr, "[vsp url]") + `"`,
  1798  	},
  1799  	purchaseTicketsRoute: {
  1800  		pwArgsShort: `"appPass"`,
  1801  		argsShort:   `assetID num`,
  1802  		cmdSummary:  `Starts a asyncrhonous ticket purchasing process. Check stakestatus for number of tickets remaining to be purchased.`,
  1803  		pwArgsLong: `Password Args:
  1804    appPass (string): The Bison Wallet password.`,
  1805  		argsLong: `Args:
  1806    assetID (int): The asset's BIP-44 registered coin index.
  1807    num (int): The number of tickets to purchase`,
  1808  		returns: `Returns:
  1809    	bool: true is the only non-error return value`,
  1810  	},
  1811  	setVotingPreferencesRoute: {
  1812  		argsShort:  `assetID (choicesMap) (tSpendPolicyMap) (treasuryPolicyMap)`,
  1813  		cmdSummary: `Cancel an order.`,
  1814  		argsLong: `Args:
  1815    assetID (int): The asset's BIP-44 registered coin index.
  1816    choicesMap ({"agendaid": "choiceid", ...}): A map of choices IDs to choice policies.
  1817    tSpendPolicyMap ({"hash": "policy", ...}): A map of tSpend txids to tSpend policies.
  1818    treasuryPolicyMap ({"key": "policy", ...}): A map of treasury spender public keys to tSpend policies.`,
  1819  		returns: `Returns:
  1820    string: The message "` + setVotePrefsStr + `"`,
  1821  	},
  1822  	txHistoryRoute: {
  1823  		argsShort:  `assetID (n) (refTxID) (past)`,
  1824  		cmdSummary: `Get transaction history for a wallet`,
  1825  		argsLong: `Args:
  1826  		  assetID (int): The asset's BIP-44 registered coin index.
  1827  		  n (int): Optional. The number of transactions to return. If <= 0 or unset, all transactions are returned.
  1828  		  refTxID (string): Optional. If set, the transactions before or after this tx (depending on the past argument)
  1829  		  will be returned.
  1830  		  past (bool): If true, the transactions before the reference tx will be returned. If false, the
  1831  		  transactions after the reference tx will be returned.`,
  1832  	},
  1833  	walletTxRoute: {
  1834  		argsShort:  `assetID txID`,
  1835  		cmdSummary: `Get a wallet transaction`,
  1836  		argsLong: `Args:
  1837  		  assetID (int): The asset's BIP-44 registered coin index.
  1838  		  txID (string): The transaction ID.`,
  1839  	},
  1840  	withdrawBchSpvRoute: {
  1841  		pwArgsShort: `"appPass"`,
  1842  		argsShort:   `recipient`,
  1843  		cmdSummary:  `Get a transaction that will withdraw all funds from the deprecated Bitcoin Cash SPV wallet`,
  1844  		argsLong: `Args:
  1845  		  recipient (string): The Bitcoin Cash address to withdraw the funds to`,
  1846  	},
  1847  }