diff --git a/background/background.go b/background/background.go index 41ded76..53c7af7 100644 --- a/background/background.go +++ b/background/background.go @@ -14,7 +14,7 @@ import ( type NotificationHandler struct { Ctx context.Context Db *database.VspDatabase - WalletConnect rpc.Connect + WalletConnect []rpc.Connect NetParams *chaincfg.Params closed chan struct{} dcrdClient *rpc.DcrdRPC @@ -108,18 +108,21 @@ func (n *NotificationHandler) Notify(method string, params json.RawMessage) erro return nil } - var walletClient *rpc.WalletRPC - walletConn, err := n.WalletConnect() - if err != nil { - log.Errorf("dcrwallet connection error: %v", err) - // If this fails, there is nothing more we can do. Return. - return nil - } - walletClient, err = rpc.WalletClient(n.Ctx, walletConn, n.NetParams) - if err != nil { - log.Errorf("dcrwallet client error: %v", err) - // If this fails, there is nothing more we can do. Return. - return nil + walletClients := make([]*rpc.WalletRPC, len(n.WalletConnect)) + for i := 0; i < len(n.WalletConnect); i++ { + walletConn, err := n.WalletConnect[i]() + if err != nil { + // TODO: what host? + log.Errorf("dcrwallet connection error: %v", err) + // If this fails, there is nothing more we can do. Return. + return nil + } + walletClients[i], err = rpc.WalletClient(n.Ctx, walletConn, n.NetParams) + if err != nil { + log.Errorf("dcrwallet '%s' client error: %v", walletConn.String(), err) + // If this fails, there is nothing more we can do. Return. + return nil + } } for _, ticket := range unconfirmedFees { @@ -147,26 +150,32 @@ func (n *NotificationHandler) Notify(method string, params json.RawMessage) erro log.Errorf("GetRawTransaction error: %v", err) continue } - err = walletClient.AddTransaction(rawTicket.BlockHash, rawTicket.Hex) - if err != nil { - log.Errorf("AddTransaction error: %v", err) - continue - } - err = walletClient.ImportPrivKey(ticket.VotingWIF) - if err != nil { - log.Errorf("ImportPrivKey error: %v", err) - continue - } - - // Update vote choices on voting wallets. - for agenda, choice := range ticket.VoteChoices { - err = walletClient.SetVoteChoice(agenda, choice, ticket.Hash) + for _, walletClient := range walletClients { + err = walletClient.AddTransaction(rawTicket.BlockHash, rawTicket.Hex) if err != nil { - log.Errorf("SetVoteChoice error: %v", err) + log.Errorf("AddTransaction error on dcrwallet '%s': %v", + walletClient.String(), err) continue } + err = walletClient.ImportPrivKey(ticket.VotingWIF) + if err != nil { + log.Errorf("ImportPrivKey error on dcrwallet '%s': %v", + walletClient.String(), err) + continue + } + + // Update vote choices on voting wallets. + for agenda, choice := range ticket.VoteChoices { + err = walletClient.SetVoteChoice(agenda, choice, ticket.Hash) + if err != nil { + log.Errorf("SetVoteChoice error on dcrwallet '%s': %v", + walletClient.String(), err) + continue + } + } + log.Debugf("Ticket added to voting wallet '%s': ticketHash=%s", + walletClient.String(), ticket.Hash) } - log.Debugf("Ticket added to voting wallet: ticketHash=%s", ticket.Hash) } } diff --git a/config.go b/config.go index 07713fb..6553560 100644 --- a/config.go +++ b/config.go @@ -45,7 +45,7 @@ type config struct { DcrdUser string `long:"dcrduser" ini-name:"dcrduser" description:"Username for dcrd RPC connections."` DcrdPass string `long:"dcrdpass" ini-name:"dcrdpass" description:"Password for dcrd RPC connections."` DcrdCert string `long:"dcrdcert" ini-name:"dcrdcert" description:"The dcrd RPC certificate file."` - WalletHost string `long:"wallethost" ini-name:"wallethost" description:"The ip:port to establish a JSON-RPC connection with voting dcrwallet."` + WalletHosts []string `long:"wallethost" ini-name:"wallethost" description:"Add an ip:port to establish a JSON-RPC connection with voting dcrwallet."` WalletUser string `long:"walletuser" ini-name:"walletuser" description:"Username for dcrwallet RPC connections."` WalletPass string `long:"walletpass" ini-name:"walletpass" description:"Password for dcrwallet RPC connections."` WalletCert string `long:"walletcert" ini-name:"walletcert" description:"The dcrwallet RPC certificate file."` @@ -155,7 +155,7 @@ func loadConfig() (*config, error) { HomeDir: defaultHomeDir, ConfigFile: defaultConfigFile, DcrdHost: defaultDcrdHost, - WalletHost: defaultWalletHost, + WalletHosts: []string{defaultWalletHost}, WebServerDebug: defaultWebServerDebug, BackupInterval: defaultBackupInterval, VspClosed: defaultVspClosed, @@ -239,11 +239,13 @@ func loadConfig() (*config, error) { } // Set the active network. + minRequired := 1 switch cfg.Network { case "testnet": cfg.netParams = &testNet3Params case "mainnet": cfg.netParams = &mainNetParams + minRequired = 3 case "simnet": cfg.netParams = &simNetParams } @@ -302,9 +304,17 @@ func loadConfig() (*config, error) { return nil, fmt.Errorf("failed to read dcrwallet cert file: %v", err) } + // Verify minimum number of voting wallets are configured. + if minRequired < len(cfg.WalletHosts) { + return nil, fmt.Errorf("minimum required voting wallets has not been met: %d < %d", + len(cfg.WalletHosts), minRequired) + } + // Add default port for the active network if there is no port specified. + for i := 0; i < len(cfg.WalletHosts); i++ { + cfg.WalletHosts[i] = normalizeAddress(cfg.WalletHosts[i], cfg.netParams.WalletRPCServerPort) + } cfg.DcrdHost = normalizeAddress(cfg.DcrdHost, cfg.netParams.DcrdRPCServerPort) - cfg.WalletHost = normalizeAddress(cfg.WalletHost, cfg.netParams.WalletRPCServerPort) // Create the data directory. dataDir := filepath.Join(cfg.HomeDir, "data", cfg.netParams.Name) diff --git a/main.go b/main.go index 6b8def8..e5779e9 100644 --- a/main.go +++ b/main.go @@ -84,21 +84,25 @@ func run(ctx context.Context) error { // Create RPC client for remote dcrwallet instance (used for voting). // Dial once just to validate config. - walletConnect := rpc.Setup(ctx, &shutdownWg, cfg.WalletUser, cfg.WalletPass, - cfg.WalletHost, cfg.walletCert, nil) - walletConn, err := walletConnect() - if err != nil { - log.Errorf("dcrwallet connection error: %v", err) - requestShutdown() - shutdownWg.Wait() - return err - } - _, err = rpc.WalletClient(ctx, walletConn, cfg.netParams.Params) - if err != nil { - log.Errorf("dcrwallet client error: %v", err) - requestShutdown() - shutdownWg.Wait() - return err + walletConnect := make([]rpc.Connect, len(cfg.WalletHosts)) + walletConn := make([]rpc.Caller, len(cfg.WalletHosts)) + for i := 0; i < len(cfg.WalletHosts); i++ { + walletConnect[i] = rpc.Setup(ctx, &shutdownWg, cfg.WalletUser, cfg.WalletPass, + cfg.WalletHosts[i], cfg.walletCert, nil) + walletConn[i], err = walletConnect[i]() + if err != nil { + log.Errorf("dcrwallet '%s' connection error: %v", cfg.WalletHosts[i], err) + requestShutdown() + shutdownWg.Wait() + return err + } + _, err = rpc.WalletClient(ctx, walletConn[i], cfg.netParams.Params) + if err != nil { + log.Errorf("dcrwallet '%s' client error: %v", cfg.WalletHosts[i], err) + requestShutdown() + shutdownWg.Wait() + return err + } } // Create a dcrd client with an attached notification handler which will run diff --git a/rpc/client.go b/rpc/client.go index c63a913..e816276 100644 --- a/rpc/client.go +++ b/rpc/client.go @@ -11,6 +11,9 @@ import ( // Caller provides a client interface to perform JSON-RPC remote procedure calls. type Caller interface { + // String returns the dialed URL. + String() string + // Call performs the remote procedure call defined by method and // waits for a response or a broken client connection. // Args provides positional parameters for the call. diff --git a/rpc/dcrwallet.go b/rpc/dcrwallet.go index 0a2129f..ffd0417 100644 --- a/rpc/dcrwallet.go +++ b/rpc/dcrwallet.go @@ -28,41 +28,46 @@ func WalletClient(ctx context.Context, c Caller, netParams *chaincfg.Params) (*W var verMap map[string]dcrdtypes.VersionResult err := c.Call(ctx, "version", &verMap) if err != nil { - return nil, fmt.Errorf("version check failed: %v", err) + return nil, fmt.Errorf("version check on dcrwallet '%s' failed: %v", + c.String(), err) } walletVersion, exists := verMap["dcrwalletjsonrpcapi"] if !exists { - return nil, fmt.Errorf("version response missing 'dcrwalletjsonrpcapi'") + return nil, fmt.Errorf("version response on dcrwallet '%s' missing 'dcrwalletjsonrpcapi'", + c.String()) } if walletVersion.VersionString != requiredWalletVersion { - return nil, fmt.Errorf("wrong dcrwallet RPC version: got %s, expected %s", - walletVersion.VersionString, requiredWalletVersion) + return nil, fmt.Errorf("dcrwallet '%s' has wrong RPC version: got %s, expected %s", + c.String(), walletVersion.VersionString, requiredWalletVersion) } // Verify dcrwallet is voting, unlocked, and is connected to dcrd (not SPV). var walletInfo wallettypes.WalletInfoResult err = c.Call(ctx, "walletinfo", &walletInfo) if err != nil { - return nil, fmt.Errorf("walletinfo check failed: %v", err) + return nil, fmt.Errorf("walletinfo check on dcrwallet '%s' failed: %v", + c.String(), err) } if !walletInfo.Voting { - return nil, fmt.Errorf("wallet has voting disabled") + return nil, fmt.Errorf("wallet '%s' has voting disabled", c.String()) } if !walletInfo.Unlocked { - return nil, fmt.Errorf("wallet is not unlocked") + return nil, fmt.Errorf("wallet '%s' is not unlocked", c.String()) } if !walletInfo.DaemonConnected { - return nil, fmt.Errorf("wallet is not connected to dcrd") + return nil, fmt.Errorf("wallet '%s' is not connected to dcrd", c.String()) } // Verify dcrwallet is on the correct network. var netID wire.CurrencyNet err = c.Call(ctx, "getcurrentnet", &netID) if err != nil { - return nil, fmt.Errorf("getcurrentnet check failed: %v", err) + return nil, fmt.Errorf("getcurrentnet check on dcrwallet '%s' failed: %v", + c.String(), err) } if netID != netParams.Net { - return nil, fmt.Errorf("dcrwallet running on %s, expected %s", netID, netParams.Net) + return nil, fmt.Errorf("dcrwallet '%s' running on %s, expected %s", + c.String(), netID, netParams.Net) } return &WalletRPC{c, ctx}, nil diff --git a/webapi/middleware.go b/webapi/middleware.go index 0d53095..4c745ae 100644 --- a/webapi/middleware.go +++ b/webapi/middleware.go @@ -37,19 +37,21 @@ func withDcrdClient() gin.HandlerFunc { // context for downstream handlers to make use of. func withWalletClient() gin.HandlerFunc { return func(c *gin.Context) { - walletConn, err := walletConnect() - if err != nil { - log.Errorf("dcrwallet connection error: %v", err) - sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) - return + walletClient := make([]*rpc.WalletRPC, len(walletConnect)) + for i := 0; i < len(walletConnect); i++ { + walletConn, err := walletConnect[i]() + if err != nil { + log.Errorf("dcrwallet '%s' connection error: %v", err) + sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) + return + } + walletClient[i], err = rpc.WalletClient(c, walletConn, cfg.NetParams) + if err != nil { + log.Errorf("dcrwallet '%s' client error: %v", walletClient[i].String(), err) + sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) + return + } } - walletClient, err := rpc.WalletClient(c, walletConn, cfg.NetParams) - if err != nil { - log.Errorf("dcrwallet client error: %v", err) - sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) - return - } - c.Set("WalletClient", walletClient) } } diff --git a/webapi/setvotechoices.go b/webapi/setvotechoices.go index a070155..249444a 100644 --- a/webapi/setvotechoices.go +++ b/webapi/setvotechoices.go @@ -17,7 +17,7 @@ func setVoteChoices(c *gin.Context) { rawRequest := c.MustGet("RawRequest").([]byte) ticket := c.MustGet("Ticket").(database.Ticket) knownTicket := c.MustGet("KnownTicket").(bool) - walletClient := c.MustGet("WalletClient").(*rpc.WalletRPC) + walletClients := c.MustGet("WalletClient").([]*rpc.WalletRPC) if !knownTicket { log.Warnf("Invalid ticket from %s", c.ClientIP()) @@ -56,11 +56,13 @@ func setVoteChoices(c *gin.Context) { // wallets if their fee is confirmed. if ticket.FeeConfirmed { for agenda, choice := range voteChoices { - err = walletClient.SetVoteChoice(agenda, choice, ticket.Hash) - if err != nil { - log.Errorf("SetVoteChoice failed: %v", err) - sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) - return + for _, walletClient := range walletClients { + err = walletClient.SetVoteChoice(agenda, choice, ticket.Hash) + if err != nil { + log.Errorf("SetVoteChoice failed: %v", err) + sendErrorResponse("dcrwallet RPC error", http.StatusInternalServerError, c) + return + } } } } diff --git a/webapi/webapi.go b/webapi/webapi.go index 66fe296..fe82c36 100644 --- a/webapi/webapi.go +++ b/webapi/webapi.go @@ -38,13 +38,13 @@ const ( var cfg Config var db *database.VspDatabase var dcrdConnect rpc.Connect -var walletConnect rpc.Connect +var walletConnect []rpc.Connect var addrGen *addressGenerator var signPrivKey ed25519.PrivateKey var signPubKey ed25519.PublicKey func Start(ctx context.Context, requestShutdownChan chan struct{}, shutdownWg *sync.WaitGroup, - listen string, vdb *database.VspDatabase, dConnect rpc.Connect, wConnect rpc.Connect, debugMode bool, feeXPub string, config Config) error { + listen string, vdb *database.VspDatabase, dConnect rpc.Connect, wConnect []rpc.Connect, debugMode bool, feeXPub string, config Config) error { cfg = config db = vdb