From 36c748ba1212ad45bfd1eac4ebaffda65163998d Mon Sep 17 00:00:00 2001 From: Jamie Holdstock Date: Wed, 20 May 2020 15:18:24 +0100 Subject: [PATCH] Import xpub tweaks (#41) --- README.md | 3 ++ config.go | 34 ++++++++------------- main.go | 76 ++++++++++++++++++++++++++++++----------------- webapi/methods.go | 2 +- webapi/server.go | 9 +++--- 5 files changed, 70 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index b73689f..42b7799 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ stores it in the database. This key is used to sign all API responses, and the signature is included in the response header `VSP-Signature`. Error responses are not signed. +- An xpub key is provided to dcrvsp via config. The first time dcrvsp starts, it + imports this xpub to create a new wallet account. This account is used to + derive addresses for fee payments. - VSP API as described in [dcrstakepool #574](https://github.com/decred/dcrstakepool/issues/574) - Request fee amount (`GET /fee`) - Request fee address (`POST /feeaddress`) diff --git a/config.go b/config.go index ffdd07a..533b414 100644 --- a/config.go +++ b/config.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "io/ioutil" "net" @@ -31,8 +32,8 @@ type config struct { Listen string `long:"listen" ini-name:"listen" description:"The ip:port to listen for API requests."` LogLevel string `long:"loglevel" ini-name:"loglevel" description:"Logging level." choice:"trace" choice:"debug" choice:"info" choice:"warn" choice:"error" choice:"critical"` Network string `long:"network" ini-name:"network" description:"Decred network to use." choice:"testnet" choice:"mainnet" choice:"simnet"` - FeeXPub string `long:"feexpub" ini-name:"feexpub" description:"cold wallet xpub used for collecting fees"` - VSPFee float64 `long:"vspfee" ini-name:"vspfee" description:"The fee percentage charged for VSP use. eg. 0.01 (1%), 0.05 (5%)."` + FeeXPub string `long:"feexpub" ini-name:"feexpub" description:"Cold wallet xpub used for collecting fees."` + VSPFee float64 `long:"vspfee" ini-name:"vspfee" description:"Fee percentage charged for VSP use. eg. 0.01 (1%), 0.05 (5%)."` HomeDir string `long:"homedir" ini-name:"homedir" no-ini:"true" description:"Path to application home directory. Used for storing VSP database and logs."` ConfigFile string `long:"configfile" ini-name:"configfile" no-ini:"true" description:"Path to configuration file."` WalletHost string `long:"wallethost" ini-name:"wallethost" description:"The ip:port to establish a JSON-RPC connection with dcrwallet."` @@ -181,11 +182,9 @@ func loadConfig() (*config, error) { } // Create the home directory if it doesn't already exist. - funcName := "loadConfig" err = os.MkdirAll(cfg.HomeDir, 0700) if err != nil { - str := "%s: failed to create home directory: %v" - err := fmt.Errorf(str, funcName, err) + err := fmt.Errorf("failed to create home directory: %v", err) fmt.Fprintln(os.Stderr, err) return nil, err } @@ -233,23 +232,17 @@ func loadConfig() (*config, error) { // Ensure the dcrwallet RPC username is set. if cfg.WalletUser == "" { - str := "%s: the walletuser option is not set" - err := fmt.Errorf(str, funcName) - return nil, err + return nil, errors.New("the walletuser option is not set") } // Ensure the dcrwallet RPC password is set. if cfg.WalletPass == "" { - str := "%s: the walletpass option is not set" - err := fmt.Errorf(str, funcName) - return nil, err + return nil, errors.New("the walletpass option is not set") } // Ensure the dcrwallet RPC cert path is set. if cfg.WalletCert == "" { - str := "%s: the walletcert option is not set" - err := fmt.Errorf(str, funcName) - return nil, err + return nil, errors.New("the walletcert option is not set") } // Add default port for the active network if there is no port specified. @@ -259,18 +252,14 @@ func loadConfig() (*config, error) { cfg.WalletCert = cleanAndExpandPath(cfg.WalletCert) cfg.dcrwCert, err = ioutil.ReadFile(cfg.WalletCert) if err != nil { - str := "%s: failed to read dcrwallet cert file: %s" - err := fmt.Errorf(str, funcName, err) - return nil, err + return nil, fmt.Errorf("failed to read dcrwallet cert file: %v", err) } // Create the data directory. dataDir := filepath.Join(cfg.HomeDir, "data", cfg.netParams.Name) err = os.MkdirAll(dataDir, 0700) if err != nil { - str := "%s: failed to create data directory: %v" - err := fmt.Errorf(str, funcName, err) - return nil, err + return nil, fmt.Errorf("failed to create data directory: %v", err) } // Initialize loggers and log rotation. @@ -282,9 +271,12 @@ func loadConfig() (*config, error) { cfg.dbPath = filepath.Join(dataDir, "vsp.db") // Validate the cold wallet xpub. + if cfg.FeeXPub == "" { + return nil, errors.New("the feexpub option is not set") + } _, err = hdkeychain.NewKeyFromString(cfg.FeeXPub, cfg.netParams.Params) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to parse feexpub: %v", err) } return &cfg, nil diff --git a/main.go b/main.go index ca546aa..71abd13 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,11 @@ import ( "github.com/jholdstock/dcrvsp/database" "github.com/jholdstock/dcrvsp/rpc" "github.com/jholdstock/dcrvsp/webapi" + "github.com/jrick/wsrpc/v2" +) + +const ( + feeAccountName = "fees" ) func main() { @@ -68,42 +73,23 @@ func run(ctx context.Context) error { shutdownWg.Wait() return err } - - // Get the masterpubkey from the fees account, if it exists, and make - // sure it matches the configuration. - var existingXPub string - err = walletClient.Call(ctx, "getmasterpubkey", &existingXPub, "fees") + + // Ensure the wallet account for collecting fees exists and matches config. + err = setupFeeAccount(ctx, walletClient, cfg.FeeXPub) if err != nil { - // TODO - ignore account not found - log.Errorf("dcrwallet RPC error: %v", err) + log.Errorf("Fee account error: %v", err) requestShutdown() shutdownWg.Wait() return err } - - // account exists - make sure it matches the configuration. - if existingXPub != cfg.FeeXPub { - log.Errorf("fees account xpub differs: %s != %s", existingXPub, cfg.FeeXPub) - requestShutdown() - shutdownWg.Wait() - return err - } else { - // account does not exist - import xpub from configuration. - if err = walletClient.Call(ctx, "importxpub", nil, "fees"); err != nil { - log.Errorf("failed to import xpub: %v", err) - requestShutdown() - shutdownWg.Wait() - return err - } - } - // Create and start webapi server. apiCfg := webapi.Config{ - SignKey: signKey, - PubKey: pubKey, - VSPFee: cfg.VSPFee, - NetParams: cfg.netParams.Params, + SignKey: signKey, + PubKey: pubKey, + VSPFee: cfg.VSPFee, + NetParams: cfg.netParams.Params, + FeeAccountName: feeAccountName, } // TODO: Make releaseMode properly configurable. Release mode enables very // detailed webserver logging and live reloading of HTML templates. @@ -121,3 +107,37 @@ func run(ctx context.Context) error { return ctx.Err() } + +func setupFeeAccount(ctx context.Context, walletClient *wsrpc.Client, feeXpub string) error { + // Check if account for fee collection already exists. + var accounts map[string]float64 + err := walletClient.Call(ctx, "listaccounts", &accounts) + if err != nil { + return fmt.Errorf("dcrwallet RPC error: %v", err) + } + + if _, ok := accounts[feeAccountName]; ok { + // Account already exists. Check xpub matches xpub from config. + var existingXPub string + err = walletClient.Call(ctx, "getmasterpubkey", &existingXPub, feeAccountName) + if err != nil { + return fmt.Errorf("dcrwallet RPC error: %v", err) + } + + if existingXPub != feeXpub { + return fmt.Errorf("existing account xpub differs from config: %s != %s", existingXPub, feeXpub) + } + + log.Debugf("Using existing wallet account %q to collect fees", feeAccountName) + + } else { + // Account does not exist. Create it using xpub from config. + if err = walletClient.Call(ctx, "importxpub", nil, feeAccountName, feeXpub); err != nil { + log.Errorf("Failed to import xpub: %v", err) + return err + } + log.Debugf("Created new wallet account %q to collect fees", feeAccountName) + } + + return nil +} diff --git a/webapi/methods.go b/webapi/methods.go index 7745069..e09142e 100644 --- a/webapi/methods.go +++ b/webapi/methods.go @@ -205,7 +205,7 @@ func feeAddress(c *gin.Context) { } var newAddress string - err = walletClient.Call(ctx, "getnewaddress", &newAddress, "fees") + err = walletClient.Call(ctx, "getnewaddress", &newAddress, cfg.FeeAccountName) if err != nil { log.Errorf("GetNewAddress error: %v", err) sendErrorResponse("unable to generate fee address", http.StatusInternalServerError, c) diff --git a/webapi/server.go b/webapi/server.go index 588a79b..22810f6 100644 --- a/webapi/server.go +++ b/webapi/server.go @@ -16,10 +16,11 @@ import ( ) type Config struct { - SignKey ed25519.PrivateKey - PubKey ed25519.PublicKey - VSPFee float64 - NetParams *chaincfg.Params + SignKey ed25519.PrivateKey + PubKey ed25519.PublicKey + VSPFee float64 + NetParams *chaincfg.Params + FeeAccountName string } var cfg Config