diff --git a/btcwallet.go b/btcwallet.go index 4ed5cbd..f23a14a 100644 --- a/btcwallet.go +++ b/btcwallet.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2014 The btcsuite developers + * Copyright (c) 2013-2015 The btcsuite developers * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -23,8 +23,12 @@ import ( _ "net/http/pprof" "os" "runtime" + "sync" "github.com/btcsuite/btcwallet/chain" + "github.com/btcsuite/btcwallet/rpc/legacyrpc" + "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/walletdb" ) var ( @@ -67,88 +71,169 @@ func walletMain() error { }() } - // Load the wallet database. It must have been created with the - // --create option already or this will return an appropriate error. - wallet, db, err := openWallet() - if err != nil { - log.Errorf("%v", err) - return err - } - defer db.Close() + dbDir := networkDir(cfg.DataDir, activeNet.Params) + loader := wallet.NewLoader(activeNet.Params, dbDir) // Create and start HTTP server to serve wallet client connections. // This will be updated with the wallet and chain server RPC client // created below after each is created. - server, err := newRPCServer(cfg.SvrListeners, cfg.RPCMaxClients, - cfg.RPCMaxWebsockets) + rpcs, legacyRPCServer, err := startRPCServers(loader) if err != nil { - log.Errorf("Unable to create HTTP server: %v", err) + log.Errorf("Unable to create RPC servers: %v", err) return err } - server.Start() - server.SetWallet(wallet) - // Shutdown the server if an interrupt signal is received. - addInterruptHandler(server.Stop) + // Create and start chain RPC client so it's ready to connect to + // the wallet when loaded later. + if !cfg.NoInitialLoad { + go rpcClientConnectLoop(legacyRPCServer, loader) + } - go func() { - for { - // Read CA certs and create the RPC client. - var certs []byte - if !cfg.DisableClientTLS { - certs, err = ioutil.ReadFile(cfg.CAFile) - if err != nil { - log.Warnf("Cannot open CA file: %v", err) - // If there's an error reading the CA file, continue - // with nil certs and without the client connection - certs = nil - } - } else { - log.Info("Client TLS is disabled") - } - rpcc, err := chain.NewClient(activeNet.Params, cfg.RPCConnect, - cfg.BtcdUsername, cfg.BtcdPassword, certs, cfg.DisableClientTLS) + var closeDB func() error + defer func() { + if closeDB != nil { + err := closeDB() if err != nil { - log.Errorf("Cannot create chain server RPC client: %v", err) - return - } - err = rpcc.Start() - if err != nil { - log.Warnf("Connection to Bitcoin RPC chain server " + - "unsuccessful -- available RPC methods will be limited") - } - // Even if Start errored, we still add the server disconnected. - // All client methods will then error, so it's obvious to a - // client that the there was a connection problem. - server.SetChainServer(rpcc) - - // Start wallet goroutines and handle RPC client notifications - // if the server is not shutting down. - select { - case <-server.quit: - return - default: - wallet.Start(rpcc) - } - - // Block goroutine until the client is finished. - rpcc.WaitForShutdown() - - wallet.SetChainSynced(false) - wallet.Stop() - - // Reconnect only if the server is not shutting down. - select { - case <-server.quit: - return - default: + log.Errorf("Unable to close wallet database: %v", err) } } }() + loader.RunAfterLoad(func(w *wallet.Wallet, db walletdb.DB) { + startWalletRPCServices(w, rpcs, legacyRPCServer) + closeDB = db.Close + }) - // Wait for the server to shutdown either due to a stop RPC request - // or an interrupt. - server.WaitForShutdown() + if !cfg.NoInitialLoad { + // Load the wallet database. It must have been created already + // or this will return an appropriate error. + _, err = loader.OpenExistingWallet([]byte(cfg.WalletPass), true) + if err != nil { + log.Error(err) + return err + } + } + + // Shutdown the server(s) when interrupt signal is received. + if rpcs != nil { + addInterruptHandler(func() { + // TODO: Does this need to wait for the grpc server to + // finish up any requests? + log.Warn("Stopping RPC server...") + rpcs.Stop() + log.Info("RPC server shutdown") + }) + } + if legacyRPCServer != nil { + go func() { + <-legacyRPCServer.RequestProcessShutdown() + simulateInterrupt() + }() + addInterruptHandler(func() { + log.Warn("Stopping legacy RPC server...") + legacyRPCServer.Stop() + log.Info("Legacy RPC server shutdown") + }) + } + + <-interruptHandlersDone log.Info("Shutdown complete") return nil } + +// rpcClientConnectLoop continuously attempts a connection to the consensus RPC +// server. When a connection is established, the client is used to sync the +// loaded wallet, either immediately or when loaded at a later time. +// +// The legacy RPC is optional. If set, the connected RPC client will be +// associated with the server for RPC passthrough and to enable additional +// methods. +func rpcClientConnectLoop(legacyRPCServer *legacyrpc.Server, loader *wallet.Loader) { + certs := readCAFile() + + for { + chainClient, err := startChainRPC(certs) + if err != nil { + log.Errorf("Unable to open connection to consensus RPC server: %v", err) + continue + } + + // Rather than inlining this logic directly into the loader + // callback, a function variable is used to avoid running any of + // this after the client disconnects by setting it to nil. This + // prevents the callback from associating a wallet loaded at a + // later time with a client that has already disconnected. A + // mutex is used to make this concurrent safe. + associateRPCClient := func(w *wallet.Wallet) { + w.SynchronizeRPC(chainClient) + if legacyRPCServer != nil { + legacyRPCServer.SetChainServer(chainClient) + } + } + mu := new(sync.Mutex) + loader.RunAfterLoad(func(w *wallet.Wallet, db walletdb.DB) { + mu.Lock() + associate := associateRPCClient + mu.Unlock() + if associate != nil { + associate(w) + } + }) + + chainClient.WaitForShutdown() + + mu.Lock() + associateRPCClient = nil + mu.Unlock() + + loadedWallet, ok := loader.LoadedWallet() + if ok { + // Do not attempt a reconnect when the wallet was + // explicitly stopped. + if loadedWallet.ShuttingDown() { + return + } + + loadedWallet.SetChainSynced(false) + + // TODO: Rework the wallet so changing the RPC client + // does not require stopping and restarting everything. + loadedWallet.Stop() + loadedWallet.WaitForShutdown() + loadedWallet.Start() + } + } +} + +func readCAFile() []byte { + // Read certificate file if TLS is not disabled. + var certs []byte + if !cfg.DisableClientTLS { + var err error + certs, err = ioutil.ReadFile(cfg.CAFile) + if err != nil { + log.Warnf("Cannot open CA file: %v", err) + // If there's an error reading the CA file, continue + // with nil certs and without the client connection. + certs = nil + } + } else { + log.Info("Chain server RPC TLS is disabled") + } + + return certs +} + +// startChainRPC opens a RPC client connection to a btcd server for blockchain +// services. This function uses the RPC options from the global config and +// there is no recovery in case the server is not available or if there is an +// authentication error. Instead, all requests to the client will simply error. +func startChainRPC(certs []byte) (*chain.RPCClient, error) { + log.Infof("Attempting RPC client connection to %v", cfg.RPCConnect) + rpcc, err := chain.NewRPCClient(activeNet.Params, cfg.RPCConnect, + cfg.BtcdUsername, cfg.BtcdPassword, certs, cfg.DisableClientTLS, 0) + if err != nil { + return nil, err + } + err = rpcc.Start() + return rpcc, err +} diff --git a/chain/chain.go b/chain/chain.go index ea50248..368a780 100644 --- a/chain/chain.go +++ b/chain/chain.go @@ -30,11 +30,13 @@ import ( "github.com/btcsuite/btcwallet/wtxmgr" ) -// Client represents a persistent client connection to a bitcoin RPC server +// RPCClient represents a persistent client connection to a bitcoin RPC server // for information regarding the current best block chain. -type Client struct { +type RPCClient struct { *btcrpcclient.Client - chainParams *chaincfg.Params + connConfig *btcrpcclient.ConnConfig // Work around unexported field + chainParams *chaincfg.Params + reconnectAttempts int enqueueNotification chan interface{} dequeueNotification chan interface{} @@ -46,21 +48,38 @@ type Client struct { quitMtx sync.Mutex } -// NewClient creates a client connection to the server described by the connect -// string. If disableTLS is false, the remote RPC certificate must be provided -// in the certs slice. The connection is not established immediately, but must -// be done using the Start method. If the remote server does not operate on -// the same bitcoin network as described by the passed chain parameters, the -// connection will be disconnected. -func NewClient(chainParams *chaincfg.Params, connect, user, pass string, certs []byte, disableTLS bool) (*Client, error) { - client := Client{ +// NewRPCClient creates a client connection to the server described by the +// connect string. If disableTLS is false, the remote RPC certificate must be +// provided in the certs slice. The connection is not established immediately, +// but must be done using the Start method. If the remote server does not +// operate on the same bitcoin network as described by the passed chain +// parameters, the connection will be disconnected. +func NewRPCClient(chainParams *chaincfg.Params, connect, user, pass string, certs []byte, + disableTLS bool, reconnectAttempts int) (*RPCClient, error) { + + if reconnectAttempts < 0 { + return nil, errors.New("reconnectAttempts must be positive") + } + + client := &RPCClient{ + connConfig: &btcrpcclient.ConnConfig{ + Host: connect, + Endpoint: "ws", + User: user, + Pass: pass, + Certificates: certs, + DisableAutoReconnect: true, + DisableConnectOnNew: true, + DisableTLS: disableTLS, + }, chainParams: chainParams, + reconnectAttempts: reconnectAttempts, enqueueNotification: make(chan interface{}), dequeueNotification: make(chan interface{}), currentBlock: make(chan *waddrmgr.BlockStamp), quit: make(chan struct{}), } - ntfnCallbacks := btcrpcclient.NotificationHandlers{ + ntfnCallbacks := &btcrpcclient.NotificationHandlers{ OnClientConnected: client.onClientConnect, OnBlockConnected: client.onBlockConnected, OnBlockDisconnected: client.onBlockDisconnected, @@ -69,22 +88,12 @@ func NewClient(chainParams *chaincfg.Params, connect, user, pass string, certs [ OnRescanFinished: client.onRescanFinished, OnRescanProgress: client.onRescanProgress, } - conf := btcrpcclient.ConnConfig{ - Host: connect, - Endpoint: "ws", - User: user, - Pass: pass, - Certificates: certs, - DisableAutoReconnect: true, - DisableConnectOnNew: true, - DisableTLS: disableTLS, - } - c, err := btcrpcclient.New(&conf, &ntfnCallbacks) + rpcClient, err := btcrpcclient.New(client.connConfig, ntfnCallbacks) if err != nil { return nil, err } - client.Client = c - return &client, nil + client.Client = rpcClient + return client, nil } // Start attempts to establish a client connection with the remote server. @@ -92,8 +101,8 @@ func NewClient(chainParams *chaincfg.Params, connect, user, pass string, certs [ // sent by the server. After a limited number of connection attempts, this // function gives up, and therefore will not block forever waiting for the // connection to be established to a server that may not exist. -func (c *Client) Start() error { - err := c.Connect(5) // attempt connection 5 tries at most +func (c *RPCClient) Start() error { + err := c.Connect(c.reconnectAttempts) if err != nil { return err } @@ -120,7 +129,7 @@ func (c *Client) Start() error { // Stop disconnects the client and signals the shutdown of all goroutines // started by Start. -func (c *Client) Stop() { +func (c *RPCClient) Stop() { c.quitMtx.Lock() select { case <-c.quit: @@ -137,7 +146,7 @@ func (c *Client) Stop() { // WaitForShutdown blocks until both the client has finished disconnecting // and all handlers have exited. -func (c *Client) WaitForShutdown() { +func (c *RPCClient) WaitForShutdown() { c.Client.WaitForShutdown() c.wg.Wait() } @@ -187,13 +196,13 @@ type ( // bitcoin RPC server. This channel must be continually read or the process // may abort for running out memory, as unread notifications are queued for // later reads. -func (c *Client) Notifications() <-chan interface{} { +func (c *RPCClient) Notifications() <-chan interface{} { return c.dequeueNotification } // BlockStamp returns the latest block notified by the client, or an error // if the client has been shut down. -func (c *Client) BlockStamp() (*waddrmgr.BlockStamp, error) { +func (c *RPCClient) BlockStamp() (*waddrmgr.BlockStamp, error) { select { case bs := <-c.currentBlock: return bs, nil @@ -223,14 +232,14 @@ func parseBlock(block *btcjson.BlockDetails) (*wtxmgr.BlockMeta, error) { return blk, nil } -func (c *Client) onClientConnect() { +func (c *RPCClient) onClientConnect() { select { case c.enqueueNotification <- ClientConnected{}: case <-c.quit: } } -func (c *Client) onBlockConnected(hash *wire.ShaHash, height int32, time time.Time) { +func (c *RPCClient) onBlockConnected(hash *wire.ShaHash, height int32, time time.Time) { select { case c.enqueueNotification <- BlockConnected{ Block: wtxmgr.Block{ @@ -243,7 +252,7 @@ func (c *Client) onBlockConnected(hash *wire.ShaHash, height int32, time time.Ti } } -func (c *Client) onBlockDisconnected(hash *wire.ShaHash, height int32, time time.Time) { +func (c *RPCClient) onBlockDisconnected(hash *wire.ShaHash, height int32, time time.Time) { select { case c.enqueueNotification <- BlockDisconnected{ Block: wtxmgr.Block{ @@ -256,7 +265,7 @@ func (c *Client) onBlockDisconnected(hash *wire.ShaHash, height int32, time time } } -func (c *Client) onRecvTx(tx *btcutil.Tx, block *btcjson.BlockDetails) { +func (c *RPCClient) onRecvTx(tx *btcutil.Tx, block *btcjson.BlockDetails) { blk, err := parseBlock(block) if err != nil { // Log and drop improper notification. @@ -276,19 +285,19 @@ func (c *Client) onRecvTx(tx *btcutil.Tx, block *btcjson.BlockDetails) { } } -func (c *Client) onRedeemingTx(tx *btcutil.Tx, block *btcjson.BlockDetails) { +func (c *RPCClient) onRedeemingTx(tx *btcutil.Tx, block *btcjson.BlockDetails) { // Handled exactly like recvtx notifications. c.onRecvTx(tx, block) } -func (c *Client) onRescanProgress(hash *wire.ShaHash, height int32, blkTime time.Time) { +func (c *RPCClient) onRescanProgress(hash *wire.ShaHash, height int32, blkTime time.Time) { select { case c.enqueueNotification <- &RescanProgress{hash, height, blkTime}: case <-c.quit: } } -func (c *Client) onRescanFinished(hash *wire.ShaHash, height int32, blkTime time.Time) { +func (c *RPCClient) onRescanFinished(hash *wire.ShaHash, height int32, blkTime time.Time) { select { case c.enqueueNotification <- &RescanFinished{hash, height, blkTime}: case <-c.quit: @@ -298,7 +307,7 @@ func (c *Client) onRescanFinished(hash *wire.ShaHash, height int32, blkTime time // handler maintains a queue of notifications and the current state (best // block) of the chain. -func (c *Client) handler() { +func (c *RPCClient) handler() { hash, height, err := c.GetBestBlock() if err != nil { log.Errorf("Failed to receive best block from chain server: %v", err) @@ -410,3 +419,10 @@ out: close(c.dequeueNotification) c.wg.Done() } + +// POSTClient creates the equivalent HTTP POST btcrpcclient.Client. +func (c *RPCClient) POSTClient() (*btcrpcclient.Client, error) { + configCopy := *c.connConfig + configCopy.HTTPPostMode = true + return btcrpcclient.New(&configCopy, nil) +} diff --git a/config.go b/config.go index 753cdc9..b613882 100644 --- a/config.go +++ b/config.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2014 The btcsuite developers + * Copyright (c) 2013-2016 The btcsuite developers * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -28,6 +28,7 @@ import ( "github.com/btcsuite/btcwallet/internal/cfgutil" "github.com/btcsuite/btcwallet/internal/legacy/keystore" "github.com/btcsuite/btcwallet/netparams" + "github.com/btcsuite/btcwallet/wallet" flags "github.com/btcsuite/go-flags" ) @@ -41,17 +42,6 @@ const ( defaultRPCMaxClients = 10 defaultRPCMaxWebsockets = 25 - // defaultPubPassphrase is the default public wallet passphrase which is - // used when the user indicates they do not want additional protection - // provided by having all public data in the wallet encrypted by a - // passphrase only known to them. - defaultPubPassphrase = "public" - - // maxEmptyAccounts is the number of accounts to scan even if they have no - // transaction history. This is a deviation from BIP044 to make account - // creation easier by allowing a limited number of empty accounts. - maxEmptyAccounts = 100 - walletDbName = "wallet.db" ) @@ -67,35 +57,58 @@ var ( ) type config struct { - ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"` - Create bool `long:"create" description:"Create the wallet if it does not exist"` - CreateTemp bool `long:"createtemp" description:"Create a temporary simulation wallet (pass=password) in the data directory indicated; must call with --datadir"` - CAFile string `long:"cafile" description:"File containing root certificates to authenticate a TLS connections with btcd"` - RPCConnect string `short:"c" long:"rpcconnect" description:"Hostname/IP and port of btcd RPC server to connect to (default localhost:18334, mainnet: localhost:8334, simnet: localhost:18556)"` - DebugLevel string `short:"d" long:"debuglevel" description:"Logging level {trace, debug, info, warn, error, critical}"` - ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"` - SvrListeners []string `long:"rpclisten" description:"Listen for RPC/websocket connections on this interface/port (default port: 18332, mainnet: 8332, simnet: 18554)"` - DataDir string `short:"D" long:"datadir" description:"Directory to store wallets and transactions"` - LogDir string `long:"logdir" description:"Directory to log output."` - Username string `short:"u" long:"username" description:"Username for client and btcd authorization"` - Password string `short:"P" long:"password" default-mask:"-" description:"Password for client and btcd authorization"` - BtcdUsername string `long:"btcdusername" description:"Alternative username for btcd authorization"` - BtcdPassword string `long:"btcdpassword" default-mask:"-" description:"Alternative password for btcd authorization"` - WalletPass string `long:"walletpass" default-mask:"-" description:"The public wallet password -- Only required if the wallet was created with one"` - RPCCert string `long:"rpccert" description:"File containing the certificate file"` - RPCKey string `long:"rpckey" description:"File containing the certificate key"` - RPCMaxClients int64 `long:"rpcmaxclients" description:"Max number of RPC clients for standard connections"` - RPCMaxWebsockets int64 `long:"rpcmaxwebsockets" description:"Max number of RPC websocket connections"` - DisableServerTLS bool `long:"noservertls" description:"Disable TLS for the RPC server -- NOTE: This is only allowed if the RPC server is bound to localhost"` - DisableClientTLS bool `long:"noclienttls" description:"Disable TLS for the RPC client -- NOTE: This is only allowed if the RPC client is connecting to localhost"` - MainNet bool `long:"mainnet" description:"Use the main Bitcoin network (default testnet3)"` - SimNet bool `long:"simnet" description:"Use the simulation test network (default testnet3)"` - KeypoolSize uint `short:"k" long:"keypoolsize" description:"DEPRECATED -- Maximum number of addresses in keypool"` - DisallowFree bool `long:"disallowfree" description:"Force transactions to always include a fee"` - Proxy string `long:"proxy" description:"Connect via SOCKS5 proxy (eg. 127.0.0.1:9050)"` - ProxyUser string `long:"proxyuser" description:"Username for proxy server"` - ProxyPass string `long:"proxypass" default-mask:"-" description:"Password for proxy server"` - Profile string `long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536"` + // General application behavior + ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"` + ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"` + Create bool `long:"create" description:"Create the wallet if it does not exist"` + CreateTemp bool `long:"createtemp" description:"Create a temporary simulation wallet (pass=password) in the data directory indicated; must call with --datadir"` + DataDir string `short:"D" long:"datadir" description:"Directory to store wallets and transactions"` + MainNet bool `long:"mainnet" description:"Use the main Bitcoin network (default testnet3)"` + SimNet bool `long:"simnet" description:"Use the simulation test network (default testnet3)"` + NoInitialLoad bool `long:"noinitialload" description:"Defer wallet creation/opening on startup and enable loading wallets over RPC"` + DebugLevel string `short:"d" long:"debuglevel" description:"Logging level {trace, debug, info, warn, error, critical}"` + LogDir string `long:"logdir" description:"Directory to log output."` + Profile string `long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536"` + + // Wallet options + WalletPass string `long:"walletpass" default-mask:"-" description:"The public wallet password -- Only required if the wallet was created with one"` + DisallowFree bool `long:"disallowfree" description:"Force transactions to always include a fee"` + + // RPC client options + RPCConnect string `short:"c" long:"rpcconnect" description:"Hostname/IP and port of btcd RPC server to connect to (default localhost:18334, mainnet: localhost:8334, simnet: localhost:18556)"` + CAFile string `long:"cafile" description:"File containing root certificates to authenticate a TLS connections with btcd"` + DisableClientTLS bool `long:"noclienttls" description:"Disable TLS for the RPC client -- NOTE: This is only allowed if the RPC client is connecting to localhost"` + BtcdUsername string `long:"btcdusername" description:"Username for btcd authentication"` + BtcdPassword string `long:"btcdpassword" default-mask:"-" description:"Password for btcd authentication"` + Proxy string `long:"proxy" description:"Connect via SOCKS5 proxy (eg. 127.0.0.1:9050)"` + ProxyUser string `long:"proxyuser" description:"Username for proxy server"` + ProxyPass string `long:"proxypass" default-mask:"-" description:"Password for proxy server"` + + // RPC server options + // + // The legacy server is still enabled by default (and eventually will be + // replaced with the experimental server) so prepare for that change by + // renaming the struct fields (but not the configuration options). + // + // Usernames can also be used for the consensus RPC client, so they + // aren't considered legacy. + RPCCert string `long:"rpccert" description:"File containing the certificate file"` + RPCKey string `long:"rpckey" description:"File containing the certificate key"` + DisableServerTLS bool `long:"noservertls" description:"Disable TLS for the RPC server -- NOTE: This is only allowed if the RPC server is bound to localhost"` + LegacyRPCListeners []string `long:"rpclisten" description:"Listen for legacy RPC connections on this interface/port (default port: 18332, mainnet: 8332, simnet: 18554)"` + LegacyRPCMaxClients int64 `long:"rpcmaxclients" description:"Max number of legacy RPC clients for standard connections"` + LegacyRPCMaxWebsockets int64 `long:"rpcmaxwebsockets" description:"Max number of legacy RPC websocket connections"` + Username string `short:"u" long:"username" description:"Username for legacy RPC and btcd authentication (if btcdusername is unset)"` + Password string `short:"P" long:"password" default-mask:"-" description:"Password for legacy RPC and btcd authentication (if btcdpassword is unset)"` + + // EXPERIMENTAL RPC server options + // + // These options will change (and require changes to config files, etc.) + // when the new gRPC server is enabled. + ExperimentalRPCListeners []string `long:"experimentalrpclisten" description:"Listen for RPC connections on this interface/port"` + + // Deprecated options + KeypoolSize uint `short:"k" long:"keypoolsize" description:"DEPRECATED -- Maximum number of addresses in keypool"` } // cleanAndExpandPath expands environement variables and leading ~ in the @@ -107,8 +120,9 @@ func cleanAndExpandPath(path string) string { path = strings.Replace(path, "~", homeDir, 1) } - // NOTE: The os.ExpandEnv doesn't work with Windows-style %VARIABLE%, - // but they variables can still be expanded via POSIX-style $VARIABLE. + // NOTE: The os.ExpandEnv doesn't work with Windows cmd.exe-style + // %VARIABLE%, but they variables can still be expanded via POSIX-style + // $VARIABLE. return filepath.Clean(os.ExpandEnv(path)) } @@ -211,16 +225,16 @@ func parseAndSetDebugLevels(debugLevel string) error { func loadConfig() (*config, []string, error) { // Default config. cfg := config{ - DebugLevel: defaultLogLevel, - ConfigFile: defaultConfigFile, - DataDir: defaultDataDir, - LogDir: defaultLogDir, - WalletPass: defaultPubPassphrase, - RPCKey: defaultRPCKeyFile, - RPCCert: defaultRPCCertFile, - DisallowFree: defaultDisallowFree, - RPCMaxClients: defaultRPCMaxClients, - RPCMaxWebsockets: defaultRPCMaxWebsockets, + DebugLevel: defaultLogLevel, + ConfigFile: defaultConfigFile, + DataDir: defaultDataDir, + LogDir: defaultLogDir, + WalletPass: wallet.InsecurePubPassphrase, + RPCKey: defaultRPCKeyFile, + RPCCert: defaultRPCCertFile, + DisallowFree: defaultDisallowFree, + LegacyRPCMaxClients: defaultRPCMaxClients, + LegacyRPCMaxWebsockets: defaultRPCMaxWebsockets, } // A config file in the current directory takes precedence. @@ -418,7 +432,7 @@ func loadConfig() (*config, []string, error) { // Created successfully, so exit now with success. os.Exit(0) - } else if !dbFileExists { + } else if !dbFileExists && !cfg.NoInitialLoad { keystorePath := filepath.Join(netDir, keystore.Filename) keystoreExists, err := cfgutil.FileExists(keystorePath) if err != nil { @@ -496,32 +510,64 @@ func loadConfig() (*config, []string, error) { } } - if len(cfg.SvrListeners) == 0 { + // Only set default RPC listeners when there are no listeners set for + // the experimental RPC server. This is required to prevent the old RPC + // server from sharing listen addresses, since it is impossible to + // remove defaults from go-flags slice options without assigning + // specific behavior to a particular string. + if len(cfg.ExperimentalRPCListeners) == 0 && len(cfg.LegacyRPCListeners) == 0 { addrs, err := net.LookupHost("localhost") if err != nil { return nil, nil, err } - cfg.SvrListeners = make([]string, 0, len(addrs)) + cfg.LegacyRPCListeners = make([]string, 0, len(addrs)) for _, addr := range addrs { addr = net.JoinHostPort(addr, activeNet.RPCServerPort) - cfg.SvrListeners = append(cfg.SvrListeners, addr) + cfg.LegacyRPCListeners = append(cfg.LegacyRPCListeners, addr) } } // Add default port to all rpc listener addresses if needed and remove // duplicate addresses. - cfg.SvrListeners, err = cfgutil.NormalizeAddresses( - cfg.SvrListeners, activeNet.RPCServerPort) + cfg.LegacyRPCListeners, err = cfgutil.NormalizeAddresses( + cfg.LegacyRPCListeners, activeNet.RPCServerPort) + if err != nil { + fmt.Fprintf(os.Stderr, + "Invalid network address in legacy RPC listeners: %v\n", err) + return nil, nil, err + } + cfg.ExperimentalRPCListeners, err = cfgutil.NormalizeAddresses( + cfg.ExperimentalRPCListeners, activeNet.RPCServerPort) if err != nil { fmt.Fprintf(os.Stderr, "Invalid network address in RPC listeners: %v\n", err) return nil, nil, err } - // Only allow server TLS to be disabled if the RPC is bound to localhost - // addresses. + // Both RPC servers may not listen on the same interface/port. + if len(cfg.LegacyRPCListeners) > 0 && len(cfg.ExperimentalRPCListeners) > 0 { + seenAddresses := make(map[string]struct{}, len(cfg.LegacyRPCListeners)) + for _, addr := range cfg.LegacyRPCListeners { + seenAddresses[addr] = struct{}{} + } + for _, addr := range cfg.ExperimentalRPCListeners { + _, seen := seenAddresses[addr] + if seen { + err := fmt.Errorf("Address `%s` may not be "+ + "used as a listener address for both "+ + "RPC servers", addr) + fmt.Fprintln(os.Stderr, err) + return nil, nil, err + } + } + } + + // Only allow server TLS to be disabled if the RPC server is bound to + // localhost addresses. if cfg.DisableServerTLS { - for _, addr := range cfg.SvrListeners { + allListeners := append(cfg.LegacyRPCListeners, + cfg.ExperimentalRPCListeners...) + for _, addr := range allListeners { host, _, err := net.SplitHostPort(addr) if err != nil { str := "%s: RPC listen interface '%s' is " + diff --git a/internal/cfgutil/file.go b/internal/cfgutil/file.go index 860054f..8a61f33 100644 --- a/internal/cfgutil/file.go +++ b/internal/cfgutil/file.go @@ -18,7 +18,7 @@ package cfgutil import "os" -// FilesExists reports whether the named file or directory exists. +// FileExists reports whether the named file or directory exists. func FileExists(filePath string) (bool, error) { _, err := os.Stat(filePath) if err != nil { diff --git a/internal/prompt/prompt.go b/internal/prompt/prompt.go new file mode 100644 index 0000000..c87c3d7 --- /dev/null +++ b/internal/prompt/prompt.go @@ -0,0 +1,320 @@ +package prompt + +import ( + "bufio" + "bytes" + "encoding/hex" + "fmt" + "os" + "strings" + + "golang.org/x/crypto/ssh/terminal" + + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/btcsuite/btcwallet/internal/legacy/keystore" +) + +// ProvideSeed is used to prompt for the wallet seed which maybe required during +// upgrades. +func ProvideSeed() ([]byte, error) { + reader := bufio.NewReader(os.Stdin) + for { + fmt.Print("Enter existing wallet seed: ") + seedStr, err := reader.ReadString('\n') + if err != nil { + return nil, err + } + seedStr = strings.TrimSpace(strings.ToLower(seedStr)) + + seed, err := hex.DecodeString(seedStr) + if err != nil || len(seed) < hdkeychain.MinSeedBytes || + len(seed) > hdkeychain.MaxSeedBytes { + + fmt.Printf("Invalid seed specified. Must be a "+ + "hexadecimal value that is at least %d bits and "+ + "at most %d bits\n", hdkeychain.MinSeedBytes*8, + hdkeychain.MaxSeedBytes*8) + continue + } + + return seed, nil + } +} + +// ProvidePrivPassphrase is used to prompt for the private passphrase which +// maybe required during upgrades. +func ProvidePrivPassphrase() ([]byte, error) { + prompt := "Enter the private passphrase of your wallet: " + for { + fmt.Print(prompt) + pass, err := terminal.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return nil, err + } + fmt.Print("\n") + pass = bytes.TrimSpace(pass) + if len(pass) == 0 { + continue + } + + return pass, nil + } +} + +// promptList prompts the user with the given prefix, list of valid responses, +// and default list entry to use. The function will repeat the prompt to the +// user until they enter a valid response. +func promptList(reader *bufio.Reader, prefix string, validResponses []string, defaultEntry string) (string, error) { + // Setup the prompt according to the parameters. + validStrings := strings.Join(validResponses, "/") + var prompt string + if defaultEntry != "" { + prompt = fmt.Sprintf("%s (%s) [%s]: ", prefix, validStrings, + defaultEntry) + } else { + prompt = fmt.Sprintf("%s (%s): ", prefix, validStrings) + } + + // Prompt the user until one of the valid responses is given. + for { + fmt.Print(prompt) + reply, err := reader.ReadString('\n') + if err != nil { + return "", err + } + reply = strings.TrimSpace(strings.ToLower(reply)) + if reply == "" { + reply = defaultEntry + } + + for _, validResponse := range validResponses { + if reply == validResponse { + return reply, nil + } + } + } +} + +// promptListBool prompts the user for a boolean (yes/no) with the given prefix. +// The function will repeat the prompt to the user until they enter a valid +// reponse. +func promptListBool(reader *bufio.Reader, prefix string, defaultEntry string) (bool, error) { + // Setup the valid responses. + valid := []string{"n", "no", "y", "yes"} + response, err := promptList(reader, prefix, valid, defaultEntry) + if err != nil { + return false, err + } + return response == "yes" || response == "y", nil +} + +// promptPass prompts the user for a passphrase with the given prefix. The +// function will ask the user to confirm the passphrase and will repeat the +// prompts until they enter a matching response. +func promptPass(reader *bufio.Reader, prefix string, confirm bool) ([]byte, error) { + // Prompt the user until they enter a passphrase. + prompt := fmt.Sprintf("%s: ", prefix) + for { + fmt.Print(prompt) + pass, err := terminal.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return nil, err + } + fmt.Print("\n") + pass = bytes.TrimSpace(pass) + if len(pass) == 0 { + continue + } + + if !confirm { + return pass, nil + } + + fmt.Print("Confirm passphrase: ") + confirm, err := terminal.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return nil, err + } + fmt.Print("\n") + confirm = bytes.TrimSpace(confirm) + if !bytes.Equal(pass, confirm) { + fmt.Println("The entered passphrases do not match") + continue + } + + return pass, nil + } +} + +// PrivatePass prompts the user for a private passphrase with varying behavior +// depending on whether the passed legacy keystore exists. When it does, the +// user is prompted for the existing passphrase which is then used to unlock it. +// On the other hand, when the legacy keystore is nil, the user is prompted for +// a new private passphrase. All prompts are repeated until the user enters a +// valid response. +func PrivatePass(reader *bufio.Reader, legacyKeyStore *keystore.Store) ([]byte, error) { + // When there is not an existing legacy wallet, simply prompt the user + // for a new private passphase and return it. + if legacyKeyStore == nil { + return promptPass(reader, "Enter the private "+ + "passphrase for your new wallet", true) + } + + // At this point, there is an existing legacy wallet, so prompt the user + // for the existing private passphrase and ensure it properly unlocks + // the legacy wallet so all of the addresses can later be imported. + fmt.Println("You have an existing legacy wallet. All addresses from " + + "your existing legacy wallet will be imported into the new " + + "wallet format.") + for { + privPass, err := promptPass(reader, "Enter the private "+ + "passphrase for your existing wallet", false) + if err != nil { + return nil, err + } + + // Keep prompting the user until the passphrase is correct. + if err := legacyKeyStore.Unlock([]byte(privPass)); err != nil { + if err == keystore.ErrWrongPassphrase { + fmt.Println(err) + continue + } + + return nil, err + } + + return privPass, nil + } +} + +// PublicPass prompts the user whether they want to add an additional layer of +// encryption to the wallet. When the user answers yes and there is already a +// public passphrase provided via the passed config, it prompts them whether or +// not to use that configured passphrase. It will also detect when the same +// passphrase is used for the private and public passphrase and prompt the user +// if they are sure they want to use the same passphrase for both. Finally, all +// prompts are repeated until the user enters a valid response. +func PublicPass(reader *bufio.Reader, privPass []byte, + defaultPubPassphrase, configPubPassphrase []byte) ([]byte, error) { + + pubPass := defaultPubPassphrase + usePubPass, err := promptListBool(reader, "Do you want "+ + "to add an additional layer of encryption for public "+ + "data?", "no") + if err != nil { + return nil, err + } + + if !usePubPass { + return pubPass, nil + } + + if !bytes.Equal(configPubPassphrase, pubPass) { + useExisting, err := promptListBool(reader, "Use the "+ + "existing configured public passphrase for encryption "+ + "of public data?", "no") + if err != nil { + return nil, err + } + + if useExisting { + return configPubPassphrase, nil + } + } + + for { + pubPass, err = promptPass(reader, "Enter the public "+ + "passphrase for your new wallet", true) + if err != nil { + return nil, err + } + + if bytes.Equal(pubPass, privPass) { + useSamePass, err := promptListBool(reader, + "Are you sure want to use the same passphrase "+ + "for public and private data?", "no") + if err != nil { + return nil, err + } + + if useSamePass { + break + } + + continue + } + + break + } + + fmt.Println("NOTE: Use the --walletpass option to configure your " + + "public passphrase.") + return pubPass, nil +} + +// Seed prompts the user whether they want to use an existing wallet generation +// seed. When the user answers no, a seed will be generated and displayed to +// the user along with prompting them for confirmation. When the user answers +// yes, a the user is prompted for it. All prompts are repeated until the user +// enters a valid response. +func Seed(reader *bufio.Reader) ([]byte, error) { + // Ascertain the wallet generation seed. + useUserSeed, err := promptListBool(reader, "Do you have an "+ + "existing wallet seed you want to use?", "no") + if err != nil { + return nil, err + } + if !useUserSeed { + seed, err := hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen) + if err != nil { + return nil, err + } + + fmt.Println("Your wallet generation seed is:") + fmt.Printf("%x\n", seed) + fmt.Println("IMPORTANT: Keep the seed in a safe place as you\n" + + "will NOT be able to restore your wallet without it.") + fmt.Println("Please keep in mind that anyone who has access\n" + + "to the seed can also restore your wallet thereby\n" + + "giving them access to all your funds, so it is\n" + + "imperative that you keep it in a secure location.") + + for { + fmt.Print(`Once you have stored the seed in a safe ` + + `and secure location, enter "OK" to continue: `) + confirmSeed, err := reader.ReadString('\n') + if err != nil { + return nil, err + } + confirmSeed = strings.TrimSpace(confirmSeed) + confirmSeed = strings.Trim(confirmSeed, `"`) + if confirmSeed == "OK" { + break + } + } + + return seed, nil + } + + for { + fmt.Print("Enter existing wallet seed: ") + seedStr, err := reader.ReadString('\n') + if err != nil { + return nil, err + } + seedStr = strings.TrimSpace(strings.ToLower(seedStr)) + + seed, err := hex.DecodeString(seedStr) + if err != nil || len(seed) < hdkeychain.MinSeedBytes || + len(seed) > hdkeychain.MaxSeedBytes { + + fmt.Printf("Invalid seed specified. Must be a "+ + "hexadecimal value that is at least %d bits and "+ + "at most %d bits\n", hdkeychain.MinSeedBytes*8, + hdkeychain.MaxSeedBytes*8) + continue + } + + return seed, nil + } +} diff --git a/internal/rpchelp/genrpcserverhelp.go b/internal/rpchelp/genrpcserverhelp.go index d1894bb..e22bb5b 100644 --- a/internal/rpchelp/genrpcserverhelp.go +++ b/internal/rpchelp/genrpcserverhelp.go @@ -85,9 +85,14 @@ func writeUsage() { func main() { defer outputFile.Close() + packageName := "main" + if len(os.Args) > 1 { + packageName = os.Args[1] + } + writefln("// AUTOGENERATED by internal/rpchelp/genrpcserverhelp.go; do not edit.") writefln("") - writefln("package main") + writefln("package %s", packageName) writefln("") for _, h := range rpchelp.HelpDescs { writeLocaleHelp(h.Locale, h.GoLocale, h.Descs) diff --git a/log.go b/log.go index 1c61283..4cce5d2 100644 --- a/log.go +++ b/log.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2014 The btcsuite developers + * Copyright (c) 2013-2015 The btcsuite developers * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -23,30 +23,25 @@ import ( "github.com/btcsuite/btclog" "github.com/btcsuite/btcrpcclient" "github.com/btcsuite/btcwallet/chain" + "github.com/btcsuite/btcwallet/rpc/legacyrpc" + "github.com/btcsuite/btcwallet/rpc/rpcserver" "github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/wtxmgr" "github.com/btcsuite/seelog" ) -const ( - // lockTimeThreshold is the number below which a lock time is - // interpreted to be a block number. Since an average of one block - // is generated per 10 minutes, this allows blocks for about 9,512 - // years. However, if the field is interpreted as a timestamp, given - // the lock time is a uint32, the max is sometime around 2106. - lockTimeThreshold uint32 = 5e8 // Tue Nov 5 00:53:20 1985 UTC -) - // Loggers per subsytem. Note that backendLog is a seelog logger that all of // the subsystem loggers route their messages to. When adding new subsystems, // add a reference here, to the subsystemLoggers map, and the useLogger // function. var ( - backendLog = seelog.Disabled - log = btclog.Disabled - walletLog = btclog.Disabled - txmgrLog = btclog.Disabled - chainLog = btclog.Disabled + backendLog = seelog.Disabled + log = btclog.Disabled + walletLog = btclog.Disabled + txmgrLog = btclog.Disabled + chainLog = btclog.Disabled + grpcLog = btclog.Disabled + legacyRPCLog = btclog.Disabled ) // subsystemLoggers maps each subsystem identifier to its associated logger. @@ -55,6 +50,8 @@ var subsystemLoggers = map[string]btclog.Logger{ "WLLT": walletLog, "TMGR": txmgrLog, "CHNS": chainLog, + "GRPC": grpcLog, + "RPCS": legacyRPCLog, } // logClosure is used to provide a closure over expensive logging operations @@ -94,6 +91,12 @@ func useLogger(subsystemID string, logger btclog.Logger) { chainLog = logger chain.UseLogger(logger) btcrpcclient.UseLogger(logger) + case "GRPC": + grpcLog = logger + rpcserver.UseLogger(logger) + case "RPCS": + legacyRPCLog = logger + legacyrpc.UseLogger(logger) } } diff --git a/rpc/api.proto b/rpc/api.proto new file mode 100644 index 0000000..7e883a7 --- /dev/null +++ b/rpc/api.proto @@ -0,0 +1,303 @@ +syntax = "proto3"; + +package walletrpc; + +service WalletService { + // Queries + rpc Ping (PingRequest) returns (PingResponse); + rpc Network (NetworkRequest) returns (NetworkResponse); + rpc AccountNumber (AccountNumberRequest) returns (AccountNumberResponse); + rpc Accounts (AccountsRequest) returns (AccountsResponse); + rpc Balance (BalanceRequest) returns (BalanceResponse); + rpc GetTransactions (GetTransactionsRequest) returns (GetTransactionsResponse); + + // Notifications + rpc TransactionNotifications (TransactionNotificationsRequest) returns (stream TransactionNotificationsResponse); + rpc SpentnessNotifications (SpentnessNotificationsRequest) returns (stream SpentnessNotificationsResponse); + rpc AccountNotifications (AccountNotificationsRequest) returns (stream AccountNotificationsResponse); + + // Control + rpc ChangePassphrase (ChangePassphraseRequest) returns (ChangePassphraseResponse); + rpc RenameAccount (RenameAccountRequest) returns (RenameAccountResponse); + rpc NextAccount (NextAccountRequest) returns (NextAccountResponse); + rpc NextAddress (NextAddressRequest) returns (NextAddressResponse); + rpc ImportPrivateKey (ImportPrivateKeyRequest) returns (ImportPrivateKeyResponse); + rpc FundTransaction (FundTransactionRequest) returns (FundTransactionResponse); + rpc SignTransaction (SignTransactionRequest) returns (SignTransactionResponse); + rpc PublishTransaction (PublishTransactionRequest) returns (PublishTransactionResponse); +} + +service WalletLoaderService { + rpc WalletExists (WalletExistsRequest) returns (WalletExistsResponse); + rpc CreateWallet (CreateWalletRequest) returns (CreateWalletResponse); + rpc OpenWallet (OpenWalletRequest) returns (OpenWalletResponse); + rpc CloseWallet (CloseWalletRequest) returns (CloseWalletResponse); + rpc StartBtcdRpc (StartBtcdRpcRequest) returns (StartBtcdRpcResponse); +} + +message TransactionDetails { + message Input { + uint32 index = 1; + uint32 previous_account = 2; + int64 previous_amount = 3; + } + message Output { + bool mine = 3; + + // These fields only relevant if mine==true. + uint32 account = 4; + bool internal = 5; + + // These fields only relevant if mine==false. + repeated string addresses = 6; // None if non-standard. + } + bytes hash = 1; + bytes transaction = 2; + repeated Input debits = 3; + repeated Output outputs = 4; + int64 fee = 5; + int64 timestamp = 6; // May be earlier than a block timestamp, but never later. +} + +message BlockDetails { + bytes hash = 1; + int32 height = 2; + int64 timestamp = 3; + repeated TransactionDetails transactions = 4; +} + +message AccountBalance { + uint32 account = 1; + int64 total_balance = 2; +} + +message PingRequest {} +message PingResponse {} + +message NetworkRequest {} +message NetworkResponse { + uint32 active_network = 1; +} + +message AccountNumberRequest { + string account_name = 1; +} +message AccountNumberResponse { + uint32 account_number = 1; +} + +message AccountsRequest {} +message AccountsResponse { + message Account { + uint32 account_number = 1; + string account_name = 2; + int64 total_balance = 3; + uint32 external_key_count = 4; + uint32 internal_key_count = 5; + uint32 imported_key_count = 6; + } + repeated Account accounts = 1; + bytes current_block_hash = 2; + int32 current_block_height = 3; +} + +message RenameAccountRequest { + uint32 account_number = 1; + string new_name = 2; +} +message RenameAccountResponse {} + +message NextAccountRequest { + bytes passphrase = 1; + string account_name = 2; +} +message NextAccountResponse { + uint32 account_number = 1; +} + +message NextAddressRequest { + uint32 account = 1; +} +message NextAddressResponse { + string address = 1; +} + +message ImportPrivateKeyRequest { + bytes passphrase = 1; + uint32 account = 2; + string private_key_wif = 3; + bool rescan = 4; +} +message ImportPrivateKeyResponse { +} + +message BalanceRequest { + uint32 account_number = 1; + int32 required_confirmations = 2; +} +message BalanceResponse { + int64 total = 1; + int64 spendable = 2; + int64 immature_reward = 3; +} + +message GetTransactionsRequest { + // Optionally specify the starting block from which to begin including all transactions. + // Either the starting block hash or height may be specified, but not both. + // If a block height is specified and is negative, the absolute value becomes the number of + // last blocks to include. That is, given a current chain height of 1000 and a starting block + // height of -3, transaction notifications will be created for blocks 998, 999, and 1000. + // If both options are excluded, transaction results are created for transactions since the + // genesis block. + bytes starting_block_hash = 1; + sint32 starting_block_height = 2; + + // Optionally specify the last block that transaction results may appear in. + // Either the ending block hash or height may be specified, but not both. + // If both are excluded, transaction results are created for all transactions + // through the best block, and include all unmined transactions. + bytes ending_block_hash = 3; + int32 ending_block_height = 4; + + // Include at least this many of the newest transactions if they exist. + // Cannot be used when the ending block hash is specified. + // + // TODO: remove until spec adds it back in some way. + int32 minimum_recent_transactions = 5; + + // TODO: limit max number of txs? +} +message GetTransactionsResponse { + repeated BlockDetails mined_transactions = 1; + repeated TransactionDetails unmined_transactions = 2; +} + +message ChangePassphraseRequest { + enum Key { + PRIVATE = 0; + PUBLIC = 1; + } + Key key = 1; + bytes old_passphrase = 2; + bytes new_passphrase = 3; +} +message ChangePassphraseResponse {} + +message FundTransactionRequest { + uint32 account = 1; + int64 target_amount = 2; + int32 required_confirmations = 3; + bool include_immature_coinbases = 4; + bool include_change_script = 5; +} +message FundTransactionResponse { + message PreviousOutput { + bytes transaction_hash = 1; + uint32 output_index = 2; + int64 amount = 3; + bytes pk_script = 4; + int64 receive_time = 5; + bool from_coinbase = 6; + } + repeated PreviousOutput selected_outputs = 1; + int64 total_amount = 2; + bytes change_pk_script = 3; +} + +message SignTransactionRequest { + bytes passphrase = 1; + + bytes serialized_transaction = 2; + + // If no indexes are specified, signatures scripts will be added for + // every input. If any input indexes are specified, only those inputs + // will be signed. Rather than returning an incompletely signed + // transaction if any of the inputs to be signed can not be, the RPC + // immediately errors. + repeated uint32 input_indexes = 3; +} +message SignTransactionResponse { + bytes transaction = 1; + repeated uint32 unsigned_input_indexes = 2; +} + +message PublishTransactionRequest { + bytes signed_transaction = 1; +} +message PublishTransactionResponse {} + +message TransactionNotificationsRequest {} +message TransactionNotificationsResponse { + // Sorted by increasing height. This is a repeated field so many new blocks + // in a new best chain can be notified at once during a reorganize. + repeated BlockDetails attached_blocks = 1; + + // If there was a chain reorganize, there may have been blocks with wallet + // transactions that are no longer in the best chain. These are those + // block's hashes. + repeated bytes detached_blocks = 2; + + // Any new unmined transactions are included here. These unmined transactions + // refer to the current best chain, so transactions from detached blocks may + // be moved to mempool and included here if they are not mined or double spent + // in the new chain. Additonally, if no new blocks were attached but a relevant + // unmined transaction is seen by the wallet, it will be reported here. + repeated TransactionDetails unmined_transactions = 3; + + // Instead of notifying all of the removed unmined transactions, + // just send all of the current hashes. + repeated bytes unmined_transaction_hashes = 4; +} + +message SpentnessNotificationsRequest { + uint32 account = 1; + bool no_notify_unspent = 2; + bool no_notify_spent = 3; +} + +message SpentnessNotificationsResponse { + bytes transaction_hash = 1; + uint32 output_index = 2; + message Spender { + bytes transaction_hash = 1; + uint32 input_index = 2; + } + Spender spender = 3; +} + +message AccountNotificationsRequest {} +message AccountNotificationsResponse { + uint32 account_number = 1; + string account_name = 2; + uint32 external_key_count = 3; + uint32 internal_key_count = 4; + uint32 imported_key_count = 5; +} + +message CreateWalletRequest { + bytes public_passphrase = 1; + bytes private_passphrase = 2; + bytes seed = 3; +} +message CreateWalletResponse {} + +message OpenWalletRequest { + bytes public_passphrase = 1; +} +message OpenWalletResponse {} + +message CloseWalletRequest {} +message CloseWalletResponse {} + +message WalletExistsRequest {} +message WalletExistsResponse { + bool exists = 1; +} + +message StartBtcdRpcRequest { + string network_address = 1; + string username = 2; + bytes password = 3; + bytes certificate = 4; +} +message StartBtcdRpcResponse {} diff --git a/rpc/documentation/README.md b/rpc/documentation/README.md new file mode 100644 index 0000000..a9155c7 --- /dev/null +++ b/rpc/documentation/README.md @@ -0,0 +1,16 @@ +# RPC Documentation + +This project provides a [gRPC](http://www.grpc.io/) server for Remote Procedure +Call (RPC) access from other processes. This is intended to be the primary +means by which users, through other client programs, interact with the wallet. + +These documents cover the documentation for both consumers of the server and +developers who must make changes or additions to the API and server +implementation: + +- [API specification](./api.md) +- [Client usage](./clientusage.md) +- [Making API changes](./serverchanges.md) + +A legacy RPC server based on the JSON-RPC API of Bitcoin Core's wallet is also +available, but documenting its usage it out of scope for these documents. diff --git a/rpc/documentation/api.md b/rpc/documentation/api.md new file mode 100644 index 0000000..12019bb --- /dev/null +++ b/rpc/documentation/api.md @@ -0,0 +1,941 @@ +# RPC API Specification + +Version: 0.1.0 + +**Note:** This document assumes the reader is familiar with gRPC concepts. +Refer to the [gRPC Concepts documentation](http://www.grpc.io/docs/guides/concepts.html) +for any unfamiliar terms. + +**Note:** The naming style used for autogenerated identifiers may differ +depending on the language being used. This document follows the naming style +used by Google in their Protocol Buffers and gRPC documentation as well as this +project's `.proto` files. That is, CamelCase is used for services, methods, and +messages, lower_snake_case for message fields, and SCREAMING_SNAKE_CASE for +enums. + +**Note:** The entierty of the RPC API is currently considered unstable and may +change anytime. Stability will be gradually added based on correctness, +perceived usefulness and ease-of-use over alternatives, and user feedback. + +This document is the authoritative source on the RPC API's definitions and +semantics. Any divergence from this document is an implementation error. API +fixes and additions require a version increase according to the rules of +[Semantic Versioning 2.0](http://semver.org/). + +Only optional proto3 message fields are used (the `required` keyword is never +used in the `.proto` file). If a message field must be set to something other +than the default value, or any other values are invalid, the error must occur in +the application's message handling. This prevents accidentally introducing +parsing errors if a previously optional field is missing or a new required field +is added. + +Functionality is grouped into gRPC services. Depending on what functions are +currently callable, different services will be running. As an example, the +server may be running without a loaded wallet, in which case the Wallet service +is not running and the Loader service must be used to create a new or load an +existing wallet. + +- [`LoaderService`](#loaderservice) +- [`WalletService`](#walletservice) + +## `LoaderService` + +The `LoaderService` service provides the caller with functions related to the +management of the wallet and its connection to the Bitcoin network. It has no +dependencies and is always running. + +**Methods:** + +- [`WalletExists`](#walletexists) +- [`CreateWallet`](#createwallet) +- [`OpenWallet`](#openwallet) +- [`CloseWallet`](#closewallet) +- [`StartBtcdRpc`](#startbtcdrpc) + +**Shared messages:** + +- [`BlockDetails`](#blockdetails) +- [`TransactionDetails`](#transactiondetails) + +### Methods + +#### `WalletExists` + +The `WalletExists` method returns whether a file at the wallet database's file +path exists. Clients that must load wallets with this service are expected to +call this RPC to query whether `OpenWallet` should be used to open an existing +wallet, or `CreateWallet` to create a new wallet. + +**Request:** `WalletExistsRequest` + +**Response:** `WalletExistsResponse` + +- `bool exists`: Whether the wallet file exists. + +**Expected errors:** None + +**Stability:** Unstable + +___ + +#### `CreateWallet` + +The `CreateWallet` method is used to create a wallet that is protected by two +levels of encryption: the public passphrase (for data that is made public on the +blockchain) and the private passphrase (for private keys). Since the seed is +not saved in the wallet database and clients should make their users backup the +seed, it needs to be passed as part of the request. + +After creating a wallet, the `WalletService` service begins running. + +**Request:** `CreateWalletRequest` + +- `bytes public_passphrase`: The passphrase used for the outer wallet + encryption. This passphrase protects data that is made public on the + blockchain. If this passphrase has zero length, an insecure default is used + instead. + +- `bytes private_passphrase`: The passphrase used for the inner wallet + encryption. This is the passphrase used for data that must always remain + private, such as private keys. The length of this field must not be zero. + +- `bytes seed`: The BIP0032 seed used to derive all wallet keys. The length of + this field must be between 16 and 64 bytes, inclusive. + +**Response:** `CreateWalletReponse` + +**Expected errors:** + +- `FailedPrecondition`: The wallet is currently open. + +- `AlreadyExists`: A file already exists at the wallet database file path. + +- `InvalidArgument`: A private passphrase was not included in the request, or + the seed is of incorrect length. + +**Stability:** Unstable: There needs to be a way to recover all keys and + transactions of a wallet being recovered by its seed. It is unclear whether + it should be part of this method or a `WalletService` method. + +___ + +#### `OpenWallet` + +The `OpenWallet` method is used to open an existing wallet database. If the +wallet is protected by a public passphrase, it can not be successfully opened if +the public passphrase parameter is missing or incorrect. + +After opening a wallet, the `WalletService` service begins running. + +**Request:** `OpenWalletRequest` + +- `bytes public_passphrase`: The passphrase used for the outer wallet + encryption. This passhprase protects data that is made public on the + blockchain. If this passphrase has zero length, an insecure default is used + instead. + +**Response:** `OpenWalletResponse` + +**Expected errors:** + +- `FailedPrecondition`: The wallet is currently open. + +- `NotFound`: The wallet database file does not exist. + +- `InvalidArgument`: The public encryption passphrase was missing or incorrect. + +**Stability:** Unstable + +___ + +#### `CloseWallet` + +The `CloseWallet` method is used to cleanly stop all wallet operations on a +loaded wallet and close the database. After closing, the `WalletService` +service will remain running but any operations that require the database will be +unusable. + +**Request:** `CloseWalletRequest` + +**Response:** `CloseWalletResponse` + +**Expected errors:** + +- `FailedPrecondition`: The wallet is not currently open. + +**Stability:** Unstable: It would be preferable to stop the `WalletService` + after closing, but there does not appear to be any way to do so currently. It + may also be a good idea to limit under what conditions a wallet can be closed, + such as only closing wallets loaded by `LoaderService` and/or using a secret + to authenticate the operation. + +___ + +#### `StartBtcdRpc` + +The `StartBtcdRpc` method is used to provide clients the ability to dynamically +start the btcd RPC client. This RPC client is used for wallet syncing and +publishing transactions to the Bitcoin network. + +**Request:** `StartBtcdRpcRequest` + +- `string network_address`: The host/IP and optional port of the RPC server to + connect to. IP addresses may be IPv4 or IPv6. If the port is missing, a + default port is chosen cooresponding to the default btcd RPC port of the + active Bitcoin network. + +- `string username`: The RPC username required to authenticate to the RPC + server. + +- `bytes password`: The RPC password required to authenticate to the RPC server. + +- `bytes certificate`: The consensus RPC server's TLS certificate. If this + field has zero length and the network address describes a loopback connection + (`localhost`, `127.0.0.1`, or `::1`) TLS will be disabled. + +**Response:** `StartBtcdRpcResponse` + +**Expected errors:** + +- `FailedPrecondition`: A consensus RPC client is already active. + +- `InvalidArgument`: The network address is ill-formatted or does not contain a + valid IP address. + +- `NotFound`: The consensus RPC server is unreachable. This condition may not + return `Unavailable` as that refers to `LoaderService` itself being + unavailable. + +- `InvalidArgument`: The username, password, or certificate are invalid. This + condition may not be return `Unauthenticated` as that refers to the client not + having the credentials to call this method. + +**Stability:** Unstable: It is unknown if the consensus RPC client will remain + used after the project gains SPV support. + +## `WalletService` + +The WalletService service provides RPCs for the wallet itself. The service +depends on a loaded wallet and does not run when the wallet has not been created +or opened yet. + +The service provides the following methods: + +- [`Ping`](#ping) +- [`Network`](#network) +- [`AccountNumber`](#accountnumber) +- [`Accounts`](#accounts) +- [`Balance`](#balance) +- [`GetTransactions`](#gettransactions) +- [`ChangePassphrase`](#changepassphrase) +- [`RenameAccount`](#renameaccount) +- [`NextAccount`](#nextaccount) +- [`NextAddress`](#nextaddress) +- [`ImportPrivateKey`](#importprivatekey) +- [`FundTransaction`](#fundtransaction) +- [`SignTransaction`](#signtransaction) +- [`PublishTransaction`](#publishtransaction) +- [`TransactionNotifications`](#transactionnotifications) +- [`SpentnessNotifications`](#spentnessnotifications) +- [`AccountNotifications`](#accountnotifications) + +#### `Ping` + +The `Ping` method checks whether the service is active. + +**Request:** `PingRequest` + +**Response:** `PingResponse` + +**Expected errors:** None + +**Stability:** Unstable: This may be moved to another service as it does not + depend on the wallet. + +___ + +#### `Network` + +The `Network` method returns the network identifier constant describing the +server's active network. + +**Request:** `NetworkRequest` + +**Response:** `NetworkResponse` + +- `uint32 active_network`: The network identifier. + +**Expected errors:** None + +**Stability:** Unstable: This may be moved to another service as it does not + depend on the wallet. + +___ + +#### `AccountNumber` + +The `AccountNumber` method looks up a BIP0044 account number by an account's +unique name. + +**Request:** `AccountNumberRequest` + +- `string account_name`: The name of the account being queried. + +**Response:** `AccountNumberResponse` + +- `uint32 account_number`: The BIP0044 account number. + +**Expected errors:** + +- `Aborted`: The wallet database is closed. + +- `NotFound`: No accounts exist by the name in the request. + +**Stability:** Unstable + +___ + +#### `Accounts` + +The `Accounts` method returns the current properties of all accounts managed in +the wallet. + +**Request:** `AccountsRequest` + +**Response:** `AccountsResponse` + +- `repeated Account accounts`: Account properties grouped into `Account` nested + message types, one per account, ordered by increasing account numbers. + + **Nested message:** `Account` + + - `uint32 account_number`: The BIP0044 account number. + + - `string account_name`: The name of the account. + + - `int64 total_balance`: The total (zero-conf and immature) balance, counted + in Satoshis. + + - `uint32 external_key_count`: The number of derived keys in the external + key chain. + + - `uint32 internal_key_count`: The number of derived keys in the internal + key chain. + + - `uint32 imported_key_count`: The number of imported keys. + +- `bytes current_block_hash`: The hash of the block wallet is considered to + be synced with. + +- `int32 current_block_height`: The height of the block wallet is considered + to be synced with. + +**Expected errors:** + +- `Aborted`: The wallet database is closed. + +**Stability:** Unstable + +___ + +#### `Balance` + +The `Balance` method queries the wallet for an account's balance. Balances are +returned as combination of total, spendable (by consensus and request policy), +and unspendable immature coinbase balances. + +**Request:** `BalanceRequest` + +- `uint32 account_number`: The account number to query. + +- `int32 required_confirmations`: The number of confirmations required before an + unspent transaction output's value is included in the spendable balance. This + may not be negative. + +**Response:** `BalanceResponse` + +- `int64 total`: The total (zero-conf and immature) balance, counted in + Satoshis. + +- `int64 spendable`: The spendable balance, given some number of required + confirmations, counted in Satoshis. This equals the total balance when the + required number of confirmations is zero and there are no immature coinbase + outputs. + +- `int64 immature_reward`: The total value of all immature coinbase outputs, + counted in Satoshis. + +**Expected errors:** + +- `InvalidArgument`: The required number of confirmations is negative. + +- `Aborted`: The wallet database is closed. + +- `NotFound`: The account does not exist. + +**Stability:** Unstable: It may prove useful to modify this RPC to query + multiple accounts together. + +___ + +#### `GetTransactions` + +The `GetTransactions` method queries the wallet for relevant transactions. The +query set may be specified using a block range, inclusive, with the heights or +hashes of the minimum and maximum block. Transaction results are grouped +grouped by the block they are mined in, or grouped together with other unmined +transactions. + +**Request:** `GetTransactionsRequest` + +- `bytes starting_block_hash`: The block hash of the block to begin including + transactions from. If this field is set to the default, the + `starting_block_height` field is used instead. If changed, the byte array + must have length 32 and `starting_block_height` must be zero. + +- `sint32 starting_block_height`: The block height to begin including + transactions from. If this field is non-zero, `starting_block_hash` must be + set to its default value to avoid ambiguity. If positive, the field is + interpreted as a block height. If negative, the height is subtracted from the + block wallet considers itself in sync with. + +- `bytes ending_block_hash`: The block hash of the last block to include + transactions from. If this default is set to the default, the + `ending_block_height` field is used instead. If changed, the byte array must + have length 32 and `ending_block_height` must be zero. + +- `int32 ending_block_height`: The block height of the last block to include + transactions from. If non-zero, the `ending_block_hash` field must be set to + its default value to avoid ambiguity. If both this field and + `ending_block_hash` are set to their default values, no upper block limit is + used and transactions through the best block and all unmined transactions are + included. + +**Response:** `GetTransactionsResponse` + +- `repeated BlockDetails mined_transactions`: All mined transactions, organized + by blocks in the order they appear in the blockchain. + + The `BlockDetails` message is used by other methods and is documented + [here](#blockdetails). + +- `repeated TransactionDetails unmined_transactions`: All unmined transactions. + The ordering is unspecified. + + The `TransactionDetails` message is used by other methods and is documented + [here](#transactiondetails). + +**Expected errors:** + +- `InvalidArgument`: A non-default block hash field did not have the correct length. + +- `Aborted`: The wallet database is closed. + +- `NotFound`: A block, specified by its height or hash, is unknown to the + wallet. + +**Stability:** Unstable + +- There is currently no way to get only unmined transactions due to the way + the block range is specified. + +- It would be useful to ignore the block range and return some minimum number of + the most recent transaction, but it is unclear if that should be added to this + method's request object, or to make a new method. + +- A specified ordering (such as dependency order) for all returned unmined + transactions would be useful. + +___ + +#### `ChangePassphrase` + +The `ChangePassphrase` method requests a change to either the public (outer) or +private (inner) encryption passphrases. + +**Request:** `ChangePassphraseRequest` + +- `Key key`: The key being changed. + + **Nested enum:** `Key` + + - `PRIVATE`: The request specifies to change the private (inner) encryption + passphrase. + + - `PUBLIC`: The request specifies to change the public (outer) encryption + passphrase. + +- `bytes old_passphrase`: The current passphrase for the encryption key. This + is the value being modified. If the public passphrase is being modified and + this value is the default value, an insecure default is used instead. + +- `bytes new_passphrase`: The replacement passphrase. This field may only have + zero length if the public passphrase is being changed, in which case an + insecure default will be used instead. + +**Response:** `ChangePassphraseResponse` + +**Expected errors:** + +- `InvalidArgument`: A zero length passphrase was specified when changing the + private passphrase, or the old passphrase was incorrect. + +- `Aborted`: The wallet database is closed. + +**Stability:** Unstable + +___ + +#### `RenameAccount` + +The `RenameAccount` method requests a change to an account's name property. + +**Request:** `RenameAccountRequest` + +- `uint32 account_number`: The number of the account being modified. + +- `string new_name`: The new name for the account. + +**Response:** `RenameAccountResponse` + +**Expected errors:** + +- `Aborted`: The wallet database is closed. + +- `InvalidArgument`: The new account name is a reserved name. + +- `NotFound`: The account does not exist. + +- `AlreadyExists`: An account by the same name already exists. + +**Stability:** Unstable: There should be a way to specify a starting block or + time to begin the rescan at. Additionally, since the client is expected to be + able to do asynchronous RPC, it may be useful for the response to block on the + rescan finishing before returning. + +___ + +#### `NextAccount` + +The `NextAccount` method generates the next BIP0044 account for the wallet. + +**Request:** `NextAccountRequest` + +- `bytes passphrase`: The private passphrase required to derive the next + account's key. + +- `string account_name`: The name to give the new account. + +**Response:** `NextAccountResponse` + +- `uint32 account_number`: The number of the newly-created account. + +**Expected errors:** + +- `Aborted`: The wallet database is closed. + +- `InvalidArgument`: The private passphrase is incorrect. + +- `InvalidArgument`: The new account name is a reserved name. + +- `AlreadyExists`: An account by the same name already exists. + +**Stability:** Unstable + +___ + +#### `NextAddress` + +The `NextAddress` method generates the next BIP0044 external account address for +the wallet. + +**Request:** `NextAddressRequest` + +- `uint32 account`: The number of the account to derive the next address for. + +**Response:** `NextAddressResponse` + +- `string address`: The payment address string. + +**Expected errors:** + +- `Aborted`: The wallet database is closed. + +- `NotFound`: The account does not exist. + +**Stability:** Unstable + +___ + +#### `ImportPrivateKey` + +The `ImportPrivateKey` method imports a private key in Wallet Import Format +(WIF) encoding to a wallet account. A rescan may optionally be started to +search for transactions involving the private key's associated payment address. + +**Request:** `ImportPrivateKeyRequest` + +- `bytes passphrase`: The wallet's private passphrase. + +- `uint32 account`: The account number to associate the imported key with. + +- `string private_key_wif`: The private key, encoded using WIF. + +- `bool rescan`: Whether or not to perform a blockchain rescan for the imported + key. + +**Response:** `ImportPrivateKeyResponse` + +**Expected errors:** + +- `InvalidArgument`: The private key WIF string is not a valid WIF encoding. + +- `Aborted`: The wallet database is closed. + +- `InvalidArgument`: The private passphrase is incorrect. + +- `NotFound`: The account does not exist. + +**Stability:** Unstable + +___ + +#### `FundTransaction` + +The `FundTransaction` method queries the wallet for unspent transaction outputs +controlled by some account. Results may be refined by setting a target output +amount and limiting the required confirmations. The selection algorithm is +unspecified. + +Output results are always created even if a minimum target output amount could +not be reached. This allows this method to behave similar to the `Balance` +method while also including the outputs that make up that balance. + +Change outputs can optionally be returned by this method as well. This can +provide the caller with everything necessary to construct an unsigned +transaction paying to already known addresses or scripts. + +**Request:** `FundTransactionRequest` + +- `uint32 account`: Account number containing the keys controlling the output + set to query. + +- `int64 target_amount`: If positive, the service may limit output results to + those that sum to at least this amount (counted in Satoshis). If zero, all + outputs not excluded by other arguments are returned. This may not be + negative. + +- `int32 required_confirmations`: The minimum number of block confirmations + needed to consider including an output in the return set. This may not be + negative. + +- `bool include_immature_coinbases`: If true, immature coinbase outputs will + also be included. + +- `bool include_change_script`: If true, a change script is included in the + response object. + +**Response:** `FundTransactionResponse` + +- `repeated PreviousOutput selected_outputs`: The output set returned as a list + of `PreviousOutput` nested message objects. + + **Nested message:** `PreviousOutput` + + - `bytes transaction_hash`: The hash of the transaction this output originates + from. + + - `uint32 output_index`: The output index of the transaction this output + originates from. + + - `int64 amount`: The output value (counted in Satoshis) of the unspent + transaction output. + + - `bytes pk_script`: The output script of the unspent transaction output. + + - `int64 receive_time`: The earliest Unix time the wallet became aware of the + transaction containing this output. + + - `bool from_coinbase`: Whether the output is a coinbase output. + +- `int64 total_amount`: The sum of all returned output amounts. This may be + less than a positive target amount if there were not enough eligible outputs + available. + +- `bytes change_pk_script`: A transaction output script used to pay the + remaining amount to a newly-generated change address for the account. This is + null if `include_change_script` was false or the target amount was not + exceeded. + +**Expected errors:** + +- `InvalidArgument`: The target amount is negative. + +- `InvalidArgument`: The required confirmations is negative. + +- `Aborted`: The wallet database is closed. + +- `NotFound`: The account does not exist. + +**Stability:** Unstable + +___ + +#### `SignTransaction` + +The `SignTransaction` method adds transaction input signatures to a serialized +transaction using a wallet private keys. + +**Request:** `SignTransactionRequest` + +- `bytes passphrase`: The wallet's private passphrase. + +- `bytes serialized_transaction`: The transaction to add input signatures to. + +- `repeated uint32 input_indexes`: The input indexes that signature scripts must + be created for. If there are no indexes, input scripts are created for every + input that is missing an input script. + +**Response:** `SignTransactionResponse` + +- `bytes transaction`: The serialized transaction with added input scripts. + +- `repeated uint32 unsigned_input_indexes`: The indexes of every input that an + input script could not be created for. + +**Expected errors:** + +- `InvalidArgument`: The serialized transaction can not be decoded. + +- `Aborted`: The wallet database is closed. + +- `InvalidArgument`: The private passphrase is incorrect. + +**Stability:** Unstable: It is unclear if the request should include an account, + and only secrets of that account are used when creating input scripts. It's + also missing options similar to Core's signrawtransaction, such as the sighash + flags and additional keys. + +___ + +#### `PublishTransaction` + +The `PublishTransaction` method publishes a signed, serialized transaction to +the Bitcoin network. If the transaction spends any of the wallet's unspent +outputs or creates a new output controlled by the wallet, it is saved by the +wallet and republished later if it or a double spend are not mined. + +**Request:** `PublishTransactionRequest` + +- `bytes signed_transaction`: The signed transaction to publish. + +**Response:** `PublishTransactionResponse` + +**Expected errors:** + +- `InvalidArgument`: The serialized transaction can not be decoded or is missing + input scripts. + +- `Aborted`: The wallet database is closed. + +**Stability:** Unstable + +___ + +#### `TransactionNotifications` + +The `TransactionNotifications` method returns a stream of notifications +regarding changes to the blockchain and transactions relevant to the wallet. + +**Request:** `TransactionNotificationsRequest` + +**Response:** `stream TransactionNotificationsResponse` + +- `repeated BlockDetails attached_blocks`: A list of blocks attached to the main + chain, sorted by increasing height. All newly mined transactions are included + in these messages, in the message cooresponding to the block that contains + them. If this field has zero length, the notification is due to an unmined + transaction being added to the wallet. + + The `BlockDetails` message is used by other methods and is documented + [here](#blockdetails). + +- `repeated bytes detached_blocks`: The hashes of every block that was + reorganized out of the main chain. These are sorted by heights in decreasing + order (newest blocks first). + +- `repeated TransactionDetails unmined_transactions`: All newly added unmined + transactions. When relevant transactions are reorganized out and not included + in (or double-spent by) the new chain, they are included here. + + The `TransactionDetails` message is used by other methods and is documented + [here](#transactiondetails). + +- `repeated bytes unmined_transaction_hashes`: The hashes of every + currently-unmined transaction. This differs from the `unmined_transactions` + field by including every unmined transaction, rather than those newly added to + the unmined set. + +**Expected errors:** + +- `Aborted`: The wallet database is closed. + +**Stability:** Unstable: This method could use a better name. + +___ + +#### `SpentnessNotifications` + +The `SpentnessNotifications` method returns a stream of notifications regarding +the spending of unspent outputs and/or the discovery of new unspent outputs for +an account. + +**Request:** `SpentnessNotificationsRequest` + +- `uint32 account`: The account to create notifications for. + +- `bool no_notify_unspent`: If true, do not send any notifications for + newly-discovered unspent outputs controlled by the account. + +- `bool no_notify_spent`: If true, do not send any notifications for newly-spent + transactions controlled by the account. + +**Response:** `stream SpentnessNotificationsResponse` + +- `bytes transaction_hash`: The hash of the serialized transaction containing + the output being reported. + +- `uint32 output_index`: The output index of the output being reported. + +- `Spender spender`: If null, the output is a newly-discovered unspent output. + If not null, the message records the transaction input that spends the + previously-unspent output. + + **Nested message:** `Spender` + + - `bytes transaction_hash`: The hash of the serialized transaction that spends + the reported output. + + - `uint32 input_index`: The index of the input that spends the reported + output. + +**Expected errors:** + +- `InvalidArgument`: The `no_notify_unspent` and `no_notify_spent` request + fields are both true. + +- `Aborted`: The wallet database is closed. + +**Stability:** Unstable + +___ + +#### `AccountNotifications` + +The `AccountNotifications` method returns a stream of notifications for account +property changes, such as name and key counts. + +**Request:** `AccountNotificationsRequest` + +**Response:** `stream AccountNotificationsResponse` + +- `uint32 account_number`: The BIP0044 account being reported. + +- `string account_name`: The current account name. + +- `uint32 external_key_count`: The current number of BIP0032 external keys + derived for the account. + +- `uint32 internal_key_count`: The current number of BIP0032 internal keys + derived for the account. + +- `uint32 imported_key_count`: The current number of private keys imported into + the account. + +**Expected errors:** + +- `Aborted`: The wallet database is closed. + +**Stability:** Unstable: This should probably share a message with the + `Accounts` method. + +___ + +### Shared messages + +The following messages are used by multiple methods. To avoid unnecessary +duplication, they are documented once here. + +#### `BlockDetails` + +The `BlockDetails` message is included in responses to report a block and the +wallet's relevant transactions contained therein. + +- `bytes hash`: The hash of the block being reported. + +- `int32 height`: The height of the block being reported. + +- `int64 timestamp`: The Unix time included in the block header. + +- `repeated TransactionDetails transactions`: All transactions relevant to the + wallet that are mined in this block. Transactions are sorted by their block + index in increasing order. + + The `TransactionDetails` message is used by other methods and is documented + [here](#transactiondetails). + +**Stability**: Unstable: This should probably include the block version. + +___ + +#### `TransactionDetails` + +The `TransactionDetails` message is included in responses to report transactions +relevant to the wallet. The message includes details such as which previous +wallet inputs are spent by this transaction, whether each output is controlled +by the wallet or not, the total fee (if calculable), and the earlist time the +transaction was seen. + +- `bytes hash`: The hash of the serialized transaction. + +- `bytes transaction`: The serialized transaction. + +- `repeated Input debits`: Properties for every previously-unspent wallet output + spent by this transaction. + + **Nested message:** `Input` + + - `uint32 index`: The transaction input index of the input being reported. + + - `uint32 previous_account`: The account that controlled the now-spent output. + + - `int64 previous_amount`: The previous output value. + +- `repeated Output outputs`: Properties for every transaction output. Every + transaction output has a cooresponding properties message in this repeated + field. + + **Nested message:** `Output` + + - `bool mine`: Whether the output is controlled by the wallet. + + - `uint32 account`: The account number of the controlled output. This field + is only relevant if `mine` is true. + + - `bool internal`: Whether the output pays to an address derived from the + account's internal key series. This often means the output is a change + output. This field is only relevant if `mine` is true. + + - `repeated string addresses`: The payment address string(s) this output pays + to. This is only relevant if `mine` is false. + +- `int64 fee`: The transaction fee, if calculable. The fee is only calculable + when every previous output spent by this transaction is also recorded by + wallet. Otherwise, this field is zero. + +- `int64 timestamp`: The Unix time of the earliest time this transaction was + seen. + +**Stability**: Unstable: Since the caller is expected to decode the serialized + transaction, and would have access to every output script, The output + properties could be changed to only include outputs controlled by the wallet. diff --git a/rpc/documentation/clientusage.md b/rpc/documentation/clientusage.md new file mode 100644 index 0000000..fd16e84 --- /dev/null +++ b/rpc/documentation/clientusage.md @@ -0,0 +1,438 @@ +# Client usage + +Clients use RPC to interact with the wallet. A client may be implemented in any +language directly supported by [gRPC](http://www.grpc.io/), languages capable of +performing [FFI](https://en.wikipedia.org/wiki/Foreign_function_interface) with +these, and languages that share a common runtime (e.g. Scala, Kotlin, and Ceylon +for the JVM, F# for the CLR, etc.). Exact instructions differ slightly +depending on the language being used, but the general process is the same for +each. In short summary, to call RPC server methods, a client must: + +1. Generate client bindings specific for the [wallet RPC server API](./api.md) +2. Import or include the gRPC dependency +3. (Optional) Wrap the client bindings with application-specific types +4. Open a gRPC channel using the wallet server's self-signed TLS certificate + +The only exception to these steps is if the client is being written in Go. In +that case, the first step may be omitted by importing the bindings from +btcwallet itself. + +The rest of this document provides short examples of how to quickly get started +by implementing a basic client that fetches the balance of the default account +(account 0) from a testnet3 wallet listening on `localhost:18332` in several +different languages: + +- [Go](#go) +- [C++](#cpp) +- [C#](#csharp) +- [Node.js](#nodejs) +- [Python](#python) + +Unless otherwise stated under the language example, it is assumed that +gRPC is already already installed. The gRPC installation procedure +can vary greatly depending on the operating system being used and +whether a gRPC source install is required. Follow the [gRPC install +instructions](https://github.com/grpc/grpc/blob/master/INSTALL) if +gRPC is not already installed. A full gRPC install also includes +[Protocol Buffers](https://github.com/google/protobuf) (compiled with +support for the proto3 language version), which contains the protoc +tool and language plugins used to compile this project's `.proto` +files to language-specific bindings. + +## Go + +The native gRPC library (gRPC Core) is not required for Go clients (a +pure Go implementation is used instead) and no additional setup is +required to generate Go bindings. + +```Go +package main + +import ( + "fmt" + "path/filepath" + + pb "github.com/btcsuite/btcwallet/rpc/walletrpc" + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + + "github.com/btcsuite/btcutil" +) + +var certificateFile = filepath.Join(btcutil.AppDataDir("btcwallet", false), "rpc.cert") + +func main() { + creds, err := credentials.NewClientTLSFromFile(certificateFile, "localhost") + if err != nil { + fmt.Println(err) + return + } + conn, err := grpc.Dial("localhost:18332", grpc.WithTransportCredentials(creds)) + if err != nil { + fmt.Println(err) + return + } + defer conn.Close() + c := pb.NewWalletServiceClient(conn) + + balanceRequest := &pb.BalanceRequest{ + AccountNumber: 0, + RequiredConfirmations: 1, + } + balanceResponse, err := c.Balance(context.Background(), balanceRequest) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println("Spendable balance: ", btcutil.Amount(balanceResponse.Spendable)) +} +``` + + +## C++ + +**Note:** Protocol Buffers and gRPC require at least C++11. The example client +is written using C++14. + +**Note:** The following instructions assume the client is being written on a +Unix-like platform (with instructions using the `sh` shell and Unix-isms in the +example source code) with a source gRPC install in `/usr/local`. + +First, generate the C++ language bindings by compiling the `.proto`: + +```bash +$ protoc -I/path/to/btcwallet/rpc --cpp_out=. --grpc_out=. \ + --plugin=protoc-gen-grpc=$(which grpc_cpp_plugin) \ + /path/to/btcwallet/rpc/api.proto +``` + +Once the `.proto` file has been compiled, the example client can be completed. +Note that the following code uses synchronous calls which will block the main +thread on all gRPC IO. + +```C++ +// example.cc +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include "api.grpc.pb.h" + +using namespace std::string_literals; + +struct NoHomeDirectoryException : std::exception { + char const* what() const noexcept override { + return "Failed to lookup home directory"; + } +}; + +auto read_file(std::string const& file_path) -> std::string { + std::ifstream in{file_path}; + std::stringstream ss{}; + ss << in.rdbuf(); + return ss.str(); +} + +auto main() -> int { + // Before the gRPC native library (gRPC Core) is lazily loaded and + // initialized, an environment variable must be set so OpenSSL is + // configured to use ECDSA TLS certificates (required by btcwallet). + setenv("GRPC_SSL_CIPHER_SUITES", "HIGH+ECDSA", 1); + + // Note: This path is operating system-dependent. This can be created + // portably using boost::filesystem or the experimental filesystem class + // expected to ship in C++17. + auto wallet_tls_cert_file = []{ + auto pw = getpwuid(getuid()); + if (pw == nullptr || pw->pw_dir == nullptr) { + throw NoHomeDirectoryException{}; + } + return pw->pw_dir + "/.btcwallet/rpc.cert"s; + }(); + + grpc::SslCredentialsOptions cred_options{ + .pem_root_certs = read_file(wallet_tls_cert_file), + }; + auto creds = grpc::SslCredentials(cred_options); + auto channel = grpc::CreateChannel("localhost:18332", creds); + auto stub = walletrpc::WalletService::NewStub(channel); + + grpc::ClientContext context{}; + + walletrpc::BalanceRequest request{}; + request.set_account_number(0); + request.set_required_confirmations(1); + + walletrpc::BalanceResponse response{}; + auto status = stub->Balance(&context, request, &response); + if (!status.ok()) { + std::cout << status.error_message() << std::endl; + } else { + std::cout << "Spendable balance: " << response.spendable() << " Satoshis" << std::endl; + } +} +``` + +The example can then be built with the following commands: + +```bash +$ c++ -std=c++14 -I/usr/local/include -pthread -c -o api.pb.o api.pb.cc +$ c++ -std=c++14 -I/usr/local/include -pthread -c -o api.grpc.pb.o api.grpc.pb.cc +$ c++ -std=c++14 -I/usr/local/include -pthread -c -o example.o example.cc +$ c++ *.o -L/usr/local/lib -lgrpc++ -lgrpc -lgpr -lprotobuf -lpthread -ldl -o example +``` + + +## C# + +The quickest way of generating client bindings in a Windows .NET environment is +by using the protoc binary included in the gRPC NuGet package. From the NuGet +package manager PowerShell console, this can be performed with: + +``` +PM> Install-Package Grpc +``` + +The protoc and C# plugin binaries can then be found in the packages directory. +For example, `.\packages\Google.Protobuf.x.x.x\tools\protoc.exe` and +`.\packages\Grpc.Tools.x.x.x\tools\grpc_csharp_plugin.exe`. + +When writing a client on other platforms (e.g. Mono on OS X), or when doing a +full gRPC source install on Windows, protoc and the C# plugin must be installed +by other means. Consult the [official documentation](https://github.com/grpc/grpc/blob/master/src/csharp/README.md) +for these steps. + +Once protoc and the C# plugin have been obtained, client bindings can be +generated. The following command generates the files `Api.cs` and `ApiGrpc.cs` +in the `Example` project directory using the `Walletrpc` namespace: + +```PowerShell +PS> & protoc.exe -I \Path\To\btcwallet\rpc --csharp_out=Example --grpc_out=Example ` + --plugin=protoc-gen-grpc=\Path\To\grpc_csharp_plugin.exe ` + \Path\To\btcwallet\rpc\api.proto +``` + +Once references have been added to the project for the `Google.Protobuf` and +`Grpc.Core` assemblies, the example client can be implemented. + +```C# +using Grpc.Core; +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Walletrpc; + +namespace Example +{ + static class Program + { + static void Main(string[] args) + { + ExampleAsync().Wait(); + } + + static async Task ExampleAsync() + { + // Before the gRPC native library (gRPC Core) is lazily loaded and initialized, + // an environment variable must be set so OpenSSL is configured to use ECDSA TLS + // certificates (required by btcwallet). + Environment.SetEnvironmentVariable("GRPC_SSL_CIPHER_SUITES", "HIGH+ECDSA"); + + var walletAppData = Portability.LocalAppData(Environment.OSVersion.Platform, "Btcwallet"); + var walletTlsCertFile = Path.Combine(walletAppData, "rpc.cert"); + var cert = await FileUtils.ReadFileAsync(walletTlsCertFile); + var channel = new Channel("localhost:18332", new SslCredentials(cert)); + try + { + var c = WalletService.NewClient(channel); + var balanceRequest = new BalanceRequest + { + AccountNumber = 0, + RequiredConfirmations = 1, + }; + var balanceResponse = await c.BalanceAsync(balanceRequest); + Console.WriteLine($"Spendable balance: {balanceResponse.Spendable} Satoshis"); + } + finally + { + await channel.ShutdownAsync(); + } + } + } + + static class FileUtils + { + public static async Task ReadFileAsync(string filePath) + { + using (var r = new StreamReader(filePath, Encoding.UTF8)) + { + return await r.ReadToEndAsync(); + } + } + } + + static class Portability + { + public static string LocalAppData(PlatformID platform, string processName) + { + if (processName == null) + throw new ArgumentNullException(nameof(processName)); + if (processName.Length == 0) + throw new ArgumentException(nameof(processName) + " may not have zero length"); + + switch (platform) + { + case PlatformID.Win32NT: + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + ToUpper(processName)); + case PlatformID.MacOSX: + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), + "Library", "Application Support", ToUpper(processName)); + case PlatformID.Unix: + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), + ToDotLower(processName)); + default: + throw new PlatformNotSupportedException($"PlatformID={platform}"); + } + } + + private static string ToUpper(string value) + { + var firstChar = value[0]; + if (char.IsUpper(firstChar)) + return value; + else + return char.ToUpper(firstChar) + value.Substring(1); + } + + private static string ToDotLower(string value) + { + var firstChar = value[0]; + return "." + char.ToLower(firstChar) + value.Substring(1); + } + } +} +``` + +## Node.js + +First, install gRPC (either by building the latest source release, or +by installing a gRPC binary development package through your operating +system's package manager). This is required to install the npm module +as it wraps the native C library (gRPC Core) with C++ bindings. +Installing the [grpc module](https://www.npmjs.com/package/grpc) to +your project can then be done by executing: + +``` +npm install grpc +``` + +A Node.js client does not require generating JavaScript stub files for +the wallet's API from the `.proto`. Instead, a call to `grpc.load` +with the `.proto` file path dynamically loads the Protobuf descriptor +and generates bindings for each service. Either copy the `.proto` to +the client project directory, or reference the file from the +`btcwallet` project directory. + +```JavaScript +var fs = require('fs'); +var path = require('path'); +var os = require('os'); +var grpc = require('grpc'); +var protoDescriptor = grpc.load('./api.proto'); +var walletrpc = protoDescriptor.walletrpc; + +// Before the gRPC native library (gRPC Core) is lazily loaded and +// initialized, an environment variable must be set so OpenSSL is +// configured to use ECDSA TLS certificates (required by btcwallet). +process.env['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA'; + +var certPath = path.join(process.env.HOME, '.btcwallet', 'rpc.cert'); +if (os.platform == 'win32') { + certPath = path.join(process.env.LOCALAPPDATA, 'Btcwallet', 'rpc.cert'); +} else if (os.platform == 'darwin') { + certPath = path.join(process.env.HOME, 'Library', 'Application Support', + 'Btcwallet', 'rpc.cert'); +} + +var cert = fs.readFileSync(certPath); +var creds = grpc.Credentials.createSsl(cert); +var client = new walletrpc.WalletService('localhost:18332', creds); + +var request = { + account_number: 0, + required_confirmations: 1 +}; +client.balance(request, function(err, response) { + if (err) { + console.error(err); + } else { + console.log('Spendable balance:', response.spendable, 'Satoshis'); + } +}); +``` + +## Python + +**Note:** gRPC requires Python 2.7. + +After installing gRPC Core and Python development headers, `pip` +should be used to install the `grpc` module and its dependencies. +Full instructions for this procedure can be found +[here](https://github.com/grpc/grpc/blob/master/src/python/README.md). + +Generate Python stubs from the `.proto`: + +```bash +$ protoc -I /path/to/btcsuite/btcwallet/rpc --python_out=. --grpc_out=. \ + --plugin=protoc-gen-grpc=$(which grpc_python_plugin) \ + /path/to/btcwallet/rpc/api.proto +``` + +Implement the client: + +```Python +import os +import platform +from grpc.beta import implementations + +import api_pb2 as walletrpc + +timeout = 1 # seconds + +def main(): + # Before the gRPC native library (gRPC Core) is lazily loaded and + # initialized, an environment variable must be set so OpenSSL is + # configured to use ECDSA TLS certificates (required by btcwallet). + os.environ['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA' + + cert_file_path = os.path.join(os.environ['HOME'], '.btcwallet', 'rpc.cert') + if platform.system() == 'Windows': + cert_file_path = os.path.join(os.environ['LOCALAPPDATA'], "Btcwallet", "rpc.cert") + elif platform.system() == 'Darwin': + cert_file_path = os.path.join(os.environ['HOME'], 'Library', 'Application Support', + 'Btcwallet', 'rpc.cert') + + with open(cert_file_path, 'r') as f: + cert = f.read() + creds = implementations.ssl_client_credentials(cert, None, None) + channel = implementations.secure_channel('localhost', 18332, creds) + stub = walletrpc.beta_create_WalletService_stub(channel) + + request = walletrpc.BalanceRequest(account_number = 0, required_confirmations = 1) + response = stub.Balance(request, timeout) + print 'Spendable balance: %d Satoshis' % response.spendable + +if __name__ == '__main__': + main() +``` diff --git a/rpc/documentation/serverchanges.md b/rpc/documentation/serverchanges.md new file mode 100644 index 0000000..32b9b07 --- /dev/null +++ b/rpc/documentation/serverchanges.md @@ -0,0 +1,94 @@ +# Making API Changes + +This document describes the process of how btcwallet developers must make +changes to the RPC API and server. Due to the use of gRPC and Protocol Buffers +for the RPC implementation, changes to this API require extra dependencies and +steps before changes to the server can be implemented. + +## Requirements + +- The Protocol Buffer compiler `protoc` installed with support for the `proto3` + language + + The `protoc` tool is part of the Protocol Buffers project. This can be + installed [from source](https://github.com/google/protobuf/blob/master/INSTALL.txt), + from an [official binary release](https://github.com/google/protobuf/releases), + or through an operating system's package manager. + +- The gRPC `protoc` plugin for Go + + This plugin is written in Go and can be installed using `go get`: + + ``` + go get github.com/golang/protobuf/protoc-gen-go + ``` + +- Knowledge of Protocol Buffers version 3 (proto3) + +Note that a full installation of gRPC Core is not required, and only the +`protoc` compiler and Go plugins are necessary. This is due to the project +using a pure Go gRPC implementation instead of wrapping the C library from gRPC +Core. + +## Step 1: Modify the `.proto` + +Once the developer dependencies have been met, changes can be made to the API by +modifying the Protocol Buffers descriptor file [`api.proto`](../api.proto). + +The API is versioned according to the rules of [Semantic Versioning +2.0](http://semver.org/). After any changes, bump the API version in the [API +specification](./api.md) and add the changes to the spec. + +Unless backwards compatibility is broken (and the version is bumped to represent +this change), message fields must never be removed or changed, and new fields +must always be appended. + +It is forbidden to use the `required` attribute on a message field as this can +cause errors during parsing when the new API is used by an older client. +Instead, the (implicit) optional attribute is used, and the server +implementation must return an appropiate error if the new request field is not +set to a valid value. + +## Step 2: Compile the `.proto` + +Once changes to the descriptor file and API specification have been made, the +`protoc` compiler must be used to compile the descriptor into a Go package. +This code contains interfaces (stubs) for each service (to be implemented by the +wallet) and message types used for each RPC. This same code can also be +imported by a Go client that then calls same interface methods to perform RPC +with the wallet. + +By committing the autogenerated package to the project repo, the `proto3` +compiler and plugin are not needed by users installing the project by source or +by other developers not making changes to the RPC API. + +A `sh` shell script is included to compile the Protocol Buffers descriptor. It +must be run from the `rpc` directory. + +```bash +$ sh regen.sh +``` + +If a `sh` shell is unavailable, the command can be run manually instead (again +from the `rpc` directory). + +``` +protoc -I. api.proto --go_out=plugins=grpc:walletrpc +``` + +TODO(jrick): This step could be simplified and be more portable by putting the +commands in a Go source file and executing them with `go generate`. It should, +however, only be run when API changes are performed (not with `go generate +./...` in the project root) since not all developers are expected to have +`protoc` installed. + +## Step 3: Implement the API change in the RPC server + +After the Go code for the API has been regenated, the necessary changes can be +implemented in the [`rpcserver`](../rpcserver/) package. + +## Additional Resources + +- [Protocol Buffers Language Guide (proto3)](https://developers.google.com/protocol-buffers/docs/proto3) +- [Protocol Buffers Basics: Go](https://developers.google.com/protocol-buffers/docs/gotutorial) +- [gRPC Basics: Go](http://www.grpc.io/docs/tutorials/basic/go.html) diff --git a/rpc/legacyrpc/config.go b/rpc/legacyrpc/config.go new file mode 100644 index 0000000..5a8333a --- /dev/null +++ b/rpc/legacyrpc/config.go @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2013-2015 The btcsuite developers + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package legacyrpc + +// Options contains the required options for running the legacy RPC server. +type Options struct { + Username string + Password string + + MaxPOSTClients int64 + MaxWebsocketClients int64 +} diff --git a/rpc/legacyrpc/errors.go b/rpc/legacyrpc/errors.go new file mode 100644 index 0000000..1ff3b5b --- /dev/null +++ b/rpc/legacyrpc/errors.go @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2013-2015 The btcsuite developers + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package legacyrpc + +import ( + "errors" + + "github.com/btcsuite/btcd/btcjson" +) + +// TODO(jrick): There are several error paths which 'replace' various errors +// with a more appropiate error from the btcjson package. Create a map of +// these replacements so they can be handled once after an RPC handler has +// returned and before the error is marshaled. + +// Error types to simplify the reporting of specific categories of +// errors, and their *btcjson.RPCError creation. +type ( + // DeserializationError describes a failed deserializaion due to bad + // user input. It cooresponds to btcjson.ErrRPCDeserialization. + DeserializationError struct { + error + } + + // InvalidParameterError describes an invalid parameter passed by + // the user. It cooresponds to btcjson.ErrRPCInvalidParameter. + InvalidParameterError struct { + error + } + + // ParseError describes a failed parse due to bad user input. It + // cooresponds to btcjson.ErrRPCParse. + ParseError struct { + error + } +) + +// Errors variables that are defined once here to avoid duplication below. +var ( + ErrNeedPositiveAmount = InvalidParameterError{ + errors.New("amount must be positive"), + } + + ErrNeedPositiveMinconf = InvalidParameterError{ + errors.New("minconf must be positive"), + } + + ErrAddressNotInWallet = btcjson.RPCError{ + Code: btcjson.ErrRPCWallet, + Message: "address not found in wallet", + } + + ErrAccountNameNotFound = btcjson.RPCError{ + Code: btcjson.ErrRPCWalletInvalidAccountName, + Message: "account name not found", + } + + ErrUnloadedWallet = btcjson.RPCError{ + Code: btcjson.ErrRPCWallet, + Message: "Request requires a wallet but wallet has not loaded yet", + } + + ErrWalletUnlockNeeded = btcjson.RPCError{ + Code: btcjson.ErrRPCWalletUnlockNeeded, + Message: "Enter the wallet passphrase with walletpassphrase first", + } + + ErrNotImportedAccount = btcjson.RPCError{ + Code: btcjson.ErrRPCWallet, + Message: "imported addresses must belong to the imported account", + } + + ErrNoTransactionInfo = btcjson.RPCError{ + Code: btcjson.ErrRPCNoTxInfo, + Message: "No information for transaction", + } + + ErrReservedAccountName = btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: "Account name is reserved by RPC server", + } +) diff --git a/rpc/legacyrpc/log.go b/rpc/legacyrpc/log.go new file mode 100644 index 0000000..13e9640 --- /dev/null +++ b/rpc/legacyrpc/log.go @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2013-2015 The btcsuite developers + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package legacyrpc + +import "github.com/btcsuite/btclog" + +var log = btclog.Disabled + +// UseLogger sets the package-wide logger. Any calls to this function must be +// made before a server is created and used (it is not concurrent safe). +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/rpc/legacyrpc/methods.go b/rpc/legacyrpc/methods.go new file mode 100644 index 0000000..ba656a8 --- /dev/null +++ b/rpc/legacyrpc/methods.go @@ -0,0 +1,1987 @@ +/* + * Copyright (c) 2013-2016 The btcsuite developers + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package legacyrpc + +import ( + "bytes" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "sync" + "time" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcrpcclient" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/chain" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/wtxmgr" +) + +const ( + // maxEmptyAccounts is the number of accounts to scan even if they have no + // transaction history. This is a deviation from BIP044 to make account + // creation easier by allowing a limited number of empty accounts. + maxEmptyAccounts = 100 +) + +// confirmed checks whether a transaction at height txHeight has met minconf +// confirmations for a blockchain at height curHeight. +func confirmed(minconf, txHeight, curHeight int32) bool { + return confirms(txHeight, curHeight) >= minconf +} + +// confirms returns the number of confirmations for a transaction in a block at +// height txHeight (or -1 for an unconfirmed tx) given the chain height +// curHeight. +func confirms(txHeight, curHeight int32) int32 { + switch { + case txHeight == -1, txHeight > curHeight: + return 0 + default: + return curHeight - txHeight + 1 + } +} + +// requestHandler is a handler function to handle an unmarshaled and parsed +// request into a marshalable response. If the error is a *btcjson.RPCError +// or any of the above special error classes, the server will respond with +// the JSON-RPC appropiate error code. All other errors use the wallet +// catch-all error code, btcjson.ErrRPCWallet. +type requestHandler func(interface{}, *wallet.Wallet) (interface{}, error) + +// requestHandlerChain is a requestHandler that also takes a parameter for +type requestHandlerChainRequired func(interface{}, *wallet.Wallet, *chain.RPCClient) (interface{}, error) + +var rpcHandlers = map[string]struct { + handler requestHandler + handlerWithChain requestHandlerChainRequired + + // Function variables cannot be compared against anything but nil, so + // use a boolean to record whether help generation is necessary. This + // is used by the tests to ensure that help can be generated for every + // implemented method. + // + // A single map and this bool is here is used rather than several maps + // for the unimplemented handlers so every method has exactly one + // handler function. + noHelp bool +}{ + // Reference implementation wallet methods (implemented) + "addmultisigaddress": {handler: AddMultiSigAddress}, + "createmultisig": {handler: CreateMultiSig}, + "dumpprivkey": {handler: DumpPrivKey}, + "getaccount": {handler: GetAccount}, + "getaccountaddress": {handler: GetAccountAddress}, + "getaddressesbyaccount": {handler: GetAddressesByAccount}, + "getbalance": {handler: GetBalance}, + "getbestblockhash": {handler: GetBestBlockHash}, + "getblockcount": {handler: GetBlockCount}, + "getinfo": {handlerWithChain: GetInfo}, + "getnewaddress": {handler: GetNewAddress}, + "getrawchangeaddress": {handler: GetRawChangeAddress}, + "getreceivedbyaccount": {handler: GetReceivedByAccount}, + "getreceivedbyaddress": {handler: GetReceivedByAddress}, + "gettransaction": {handler: GetTransaction}, + "help": {handler: HelpNoChainRPC, handlerWithChain: HelpWithChainRPC}, + "importprivkey": {handler: ImportPrivKey}, + "keypoolrefill": {handler: KeypoolRefill}, + "listaccounts": {handler: ListAccounts}, + "listlockunspent": {handler: ListLockUnspent}, + "listreceivedbyaccount": {handler: ListReceivedByAccount}, + "listreceivedbyaddress": {handler: ListReceivedByAddress}, + "listsinceblock": {handlerWithChain: ListSinceBlock}, + "listtransactions": {handler: ListTransactions}, + "listunspent": {handler: ListUnspent}, + "lockunspent": {handler: LockUnspent}, + "sendfrom": {handlerWithChain: SendFrom}, + "sendmany": {handler: SendMany}, + "sendtoaddress": {handler: SendToAddress}, + "settxfee": {handler: SetTxFee}, + "signmessage": {handler: SignMessage}, + "signrawtransaction": {handlerWithChain: SignRawTransaction}, + "validateaddress": {handler: ValidateAddress}, + "verifymessage": {handler: VerifyMessage}, + "walletlock": {handler: WalletLock}, + "walletpassphrase": {handler: WalletPassphrase}, + "walletpassphrasechange": {handler: WalletPassphraseChange}, + + // Reference implementation methods (still unimplemented) + "backupwallet": {handler: Unimplemented, noHelp: true}, + "dumpwallet": {handler: Unimplemented, noHelp: true}, + "getwalletinfo": {handler: Unimplemented, noHelp: true}, + "importwallet": {handler: Unimplemented, noHelp: true}, + "listaddressgroupings": {handler: Unimplemented, noHelp: true}, + + // Reference methods which can't be implemented by btcwallet due to + // design decision differences + "encryptwallet": {handler: Unsupported, noHelp: true}, + "move": {handler: Unsupported, noHelp: true}, + "setaccount": {handler: Unsupported, noHelp: true}, + + // Extensions to the reference client JSON-RPC API + "createnewaccount": {handler: CreateNewAccount}, + "exportwatchingwallet": {handler: ExportWatchingWallet}, + "getbestblock": {handler: GetBestBlock}, + // This was an extension but the reference implementation added it as + // well, but with a different API (no account parameter). It's listed + // here because it hasn't been update to use the reference + // implemenation's API. + "getunconfirmedbalance": {handler: GetUnconfirmedBalance}, + "listaddresstransactions": {handler: ListAddressTransactions}, + "listalltransactions": {handler: ListAllTransactions}, + "renameaccount": {handler: RenameAccount}, + "walletislocked": {handler: WalletIsLocked}, +} + +// Unimplemented handles an unimplemented RPC request with the +// appropiate error. +func Unimplemented(interface{}, *wallet.Wallet) (interface{}, error) { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCUnimplemented, + Message: "Method unimplemented", + } +} + +// Unsupported handles a standard bitcoind RPC request which is +// unsupported by btcwallet due to design differences. +func Unsupported(interface{}, *wallet.Wallet) (interface{}, error) { + return nil, &btcjson.RPCError{ + Code: -1, + Message: "Request unsupported by btcwallet", + } +} + +// lazyHandler is a closure over a requestHandler or passthrough request with +// the RPC server's wallet and chain server variables as part of the closure +// context. +type lazyHandler func() (interface{}, *btcjson.RPCError) + +// lazyApplyHandler looks up the best request handler func for the method, +// returning a closure that will execute it with the (required) wallet and +// (optional) consensus RPC server. If no handlers are found and the +// chainClient is not nil, the returned handler performs RPC passthrough. +func lazyApplyHandler(request *btcjson.Request, w *wallet.Wallet, chainClient *chain.RPCClient) lazyHandler { + handlerData, ok := rpcHandlers[request.Method] + if ok && handlerData.handlerWithChain != nil && w != nil && chainClient != nil { + return func() (interface{}, *btcjson.RPCError) { + cmd, err := btcjson.UnmarshalCmd(request) + if err != nil { + return nil, btcjson.ErrRPCInvalidRequest + } + resp, err := handlerData.handlerWithChain(cmd, w, chainClient) + if err != nil { + return nil, jsonError(err) + } + return resp, nil + } + } + if ok && handlerData.handler != nil && w != nil { + return func() (interface{}, *btcjson.RPCError) { + cmd, err := btcjson.UnmarshalCmd(request) + if err != nil { + return nil, btcjson.ErrRPCInvalidRequest + } + resp, err := handlerData.handler(cmd, w) + if err != nil { + return nil, jsonError(err) + } + return resp, nil + } + } + + // Fallback to RPC passthrough + return func() (interface{}, *btcjson.RPCError) { + if chainClient == nil { + return nil, &btcjson.RPCError{ + Code: -1, + Message: "Chain RPC is inactive", + } + } + resp, err := chainClient.RawRequest(request.Method, request.Params) + if err != nil { + return nil, jsonError(err) + } + return &resp, nil + } +} + +// makeResponse makes the JSON-RPC response struct for the result and error +// returned by a requestHandler. The returned response is not ready for +// marshaling and sending off to a client, but must be +func makeResponse(id, result interface{}, err error) btcjson.Response { + idPtr := idPointer(id) + if err != nil { + return btcjson.Response{ + ID: idPtr, + Error: jsonError(err), + } + } + resultBytes, err := json.Marshal(result) + if err != nil { + return btcjson.Response{ + ID: idPtr, + Error: &btcjson.RPCError{ + Code: btcjson.ErrRPCInternal.Code, + Message: "Unexpected error marshalling result", + }, + } + } + return btcjson.Response{ + ID: idPtr, + Result: json.RawMessage(resultBytes), + } +} + +// jsonError creates a JSON-RPC error from the Go error. +func jsonError(err error) *btcjson.RPCError { + if err == nil { + return nil + } + + code := btcjson.ErrRPCWallet + switch e := err.(type) { + case btcjson.RPCError: + return &e + case *btcjson.RPCError: + return e + case DeserializationError: + code = btcjson.ErrRPCDeserialization + case InvalidParameterError: + code = btcjson.ErrRPCInvalidParameter + case ParseError: + code = btcjson.ErrRPCParse.Code + case waddrmgr.ManagerError: + switch e.ErrorCode { + case waddrmgr.ErrWrongPassphrase: + code = btcjson.ErrRPCWalletPassphraseIncorrect + } + } + return &btcjson.RPCError{ + Code: code, + Message: err.Error(), + } +} + +// makeMultiSigScript is a helper function to combine common logic for +// AddMultiSig and CreateMultiSig. +func makeMultiSigScript(w *wallet.Wallet, keys []string, nRequired int) ([]byte, error) { + keysesPrecious := make([]*btcutil.AddressPubKey, len(keys)) + + // The address list will made up either of addreseses (pubkey hash), for + // which we need to look up the keys in wallet, straight pubkeys, or a + // mixture of the two. + for i, a := range keys { + // try to parse as pubkey address + a, err := decodeAddress(a, w.ChainParams()) + if err != nil { + return nil, err + } + + switch addr := a.(type) { + case *btcutil.AddressPubKey: + keysesPrecious[i] = addr + case *btcutil.AddressPubKeyHash: + ainfo, err := w.Manager.Address(addr) + if err != nil { + return nil, err + } + + apkinfo := ainfo.(waddrmgr.ManagedPubKeyAddress) + + // This will be an addresspubkey + a, err := decodeAddress(apkinfo.ExportPubKey(), + w.ChainParams()) + if err != nil { + return nil, err + } + + apk := a.(*btcutil.AddressPubKey) + keysesPrecious[i] = apk + default: + return nil, err + } + } + + return txscript.MultiSigScript(keysesPrecious, nRequired) +} + +// AddMultiSigAddress handles an addmultisigaddress request by adding a +// multisig address to the given wallet. +func AddMultiSigAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.AddMultisigAddressCmd) + + // If an account is specified, ensure that is the imported account. + if cmd.Account != nil && *cmd.Account != waddrmgr.ImportedAddrAccountName { + return nil, &ErrNotImportedAccount + } + + script, err := makeMultiSigScript(w, cmd.Keys, cmd.NRequired) + if err != nil { + return nil, ParseError{err} + } + + // TODO(oga) blockstamp current block? + bs := &waddrmgr.BlockStamp{ + Hash: *w.ChainParams().GenesisHash, + Height: 0, + } + + addr, err := w.Manager.ImportScript(script, bs) + if err != nil { + return nil, err + } + + return addr.Address().EncodeAddress(), nil +} + +// CreateMultiSig handles an createmultisig request by returning a +// multisig address for the given inputs. +func CreateMultiSig(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.CreateMultisigCmd) + + script, err := makeMultiSigScript(w, cmd.Keys, cmd.NRequired) + if err != nil { + return nil, ParseError{err} + } + + address, err := btcutil.NewAddressScriptHash(script, w.ChainParams()) + if err != nil { + // above is a valid script, shouldn't happen. + return nil, err + } + + return btcjson.CreateMultiSigResult{ + Address: address.EncodeAddress(), + RedeemScript: hex.EncodeToString(script), + }, nil +} + +// DumpPrivKey handles a dumpprivkey request with the private key +// for a single address, or an appropiate error if the wallet +// is locked. +func DumpPrivKey(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.DumpPrivKeyCmd) + + addr, err := decodeAddress(cmd.Address, w.ChainParams()) + if err != nil { + return nil, err + } + + key, err := w.DumpWIFPrivateKey(addr) + if waddrmgr.IsError(err, waddrmgr.ErrLocked) { + // Address was found, but the private key isn't + // accessible. + return nil, &ErrWalletUnlockNeeded + } + return key, err +} + +// DumpWallet handles a dumpwallet request by returning all private +// keys in a wallet, or an appropiate error if the wallet is locked. +// TODO: finish this to match bitcoind by writing the dump to a file. +func DumpWallet(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + keys, err := w.DumpPrivKeys() + if waddrmgr.IsError(err, waddrmgr.ErrLocked) { + return nil, &ErrWalletUnlockNeeded + } + + return keys, err +} + +// ExportWatchingWallet handles an exportwatchingwallet request by exporting the +// current wallet as a watching wallet (with no private keys), and returning +// base64-encoding of serialized account files. +func ExportWatchingWallet(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.ExportWatchingWalletCmd) + + if cmd.Account != nil && *cmd.Account != "*" { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCWallet, + Message: "Individual accounts can not be exported as watching-only", + } + } + + return w.ExportWatchingWallet() +} + +// GetAddressesByAccount handles a getaddressesbyaccount request by returning +// all addresses for an account, or an error if the requested account does +// not exist. +func GetAddressesByAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.GetAddressesByAccountCmd) + + account, err := w.Manager.LookupAccount(cmd.Account) + if err != nil { + return nil, err + } + + var addrStrs []string + err = w.Manager.ForEachAccountAddress(account, + func(maddr waddrmgr.ManagedAddress) error { + addrStrs = append(addrStrs, maddr.Address().EncodeAddress()) + return nil + }) + return addrStrs, err +} + +// GetBalance handles a getbalance request by returning the balance for an +// account (wallet), or an error if the requested account does not +// exist. +func GetBalance(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.GetBalanceCmd) + + var balance btcutil.Amount + var err error + accountName := "*" + if cmd.Account != nil { + accountName = *cmd.Account + } + if accountName == "*" { + balance, err = w.CalculateBalance(int32(*cmd.MinConf)) + if err != nil { + return nil, err + } + } else { + var account uint32 + account, err = w.Manager.LookupAccount(accountName) + if err != nil { + return nil, err + } + bals, err := w.CalculateAccountBalances(account, int32(*cmd.MinConf)) + if err != nil { + return nil, err + } + balance = bals.Spendable + } + return balance.ToBTC(), nil +} + +// GetBestBlock handles a getbestblock request by returning a JSON object +// with the height and hash of the most recently processed block. +func GetBestBlock(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + blk := w.Manager.SyncedTo() + result := &btcjson.GetBestBlockResult{ + Hash: blk.Hash.String(), + Height: blk.Height, + } + return result, nil +} + +// GetBestBlockHash handles a getbestblockhash request by returning the hash +// of the most recently processed block. +func GetBestBlockHash(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + blk := w.Manager.SyncedTo() + return blk.Hash.String(), nil +} + +// GetBlockCount handles a getblockcount request by returning the chain height +// of the most recently processed block. +func GetBlockCount(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + blk := w.Manager.SyncedTo() + return blk.Height, nil +} + +// GetInfo handles a getinfo request by returning the a structure containing +// information about the current state of btcwallet. +// exist. +func GetInfo(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient) (interface{}, error) { + // Call down to btcd for all of the information in this command known + // by them. + info, err := chainClient.GetInfo() + if err != nil { + return nil, err + } + + bal, err := w.CalculateBalance(1) + if err != nil { + return nil, err + } + + // TODO(davec): This should probably have a database version as opposed + // to using the manager version. + info.WalletVersion = int32(waddrmgr.LatestMgrVersion) + info.Balance = bal.ToBTC() + info.PaytxFee = w.FeeIncrement.ToBTC() + // We don't set the following since they don't make much sense in the + // wallet architecture: + // - unlocked_until + // - errors + + return info, nil +} + +func decodeAddress(s string, params *chaincfg.Params) (btcutil.Address, error) { + addr, err := btcutil.DecodeAddress(s, params) + if err != nil { + msg := fmt.Sprintf("Invalid address %q: decode failed with %#q", s, err) + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidAddressOrKey, + Message: msg, + } + } + if !addr.IsForNet(params) { + msg := fmt.Sprintf("Invalid address %q: not intended for use on %s", + addr, params.Name) + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidAddressOrKey, + Message: msg, + } + } + return addr, nil +} + +// GetAccount handles a getaccount request by returning the account name +// associated with a single address. +func GetAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.GetAccountCmd) + + addr, err := decodeAddress(cmd.Address, w.ChainParams()) + if err != nil { + return nil, err + } + + // Fetch the associated account + account, err := w.Manager.AddrAccount(addr) + if err != nil { + return nil, &ErrAddressNotInWallet + } + + acctName, err := w.Manager.AccountName(account) + if err != nil { + return nil, &ErrAccountNameNotFound + } + return acctName, nil +} + +// GetAccountAddress handles a getaccountaddress by returning the most +// recently-created chained address that has not yet been used (does not yet +// appear in the blockchain, or any tx that has arrived in the btcd mempool). +// If the most recently-requested address has been used, a new address (the +// next chained address in the keypool) is used. This can fail if the keypool +// runs out (and will return btcjson.ErrRPCWalletKeypoolRanOut if that happens). +func GetAccountAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.GetAccountAddressCmd) + + account, err := w.Manager.LookupAccount(cmd.Account) + if err != nil { + return nil, err + } + addr, err := w.CurrentAddress(account) + if err != nil { + return nil, err + } + + return addr.EncodeAddress(), err +} + +// GetUnconfirmedBalance handles a getunconfirmedbalance extension request +// by returning the current unconfirmed balance of an account. +func GetUnconfirmedBalance(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.GetUnconfirmedBalanceCmd) + + acctName := "default" + if cmd.Account != nil { + acctName = *cmd.Account + } + account, err := w.Manager.LookupAccount(acctName) + if err != nil { + return nil, err + } + bals, err := w.CalculateAccountBalances(account, 1) + if err != nil { + return nil, err + } + + return (bals.Total - bals.Spendable).ToBTC(), nil +} + +// ImportPrivKey handles an importprivkey request by parsing +// a WIF-encoded private key and adding it to an account. +func ImportPrivKey(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.ImportPrivKeyCmd) + + // Ensure that private keys are only imported to the correct account. + // + // Yes, Label is the account name. + if cmd.Label != nil && *cmd.Label != waddrmgr.ImportedAddrAccountName { + return nil, &ErrNotImportedAccount + } + + wif, err := btcutil.DecodeWIF(cmd.PrivKey) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidAddressOrKey, + Message: "WIF decode failed: " + err.Error(), + } + } + if !wif.IsForNet(w.ChainParams()) { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidAddressOrKey, + Message: "Key is not intended for " + w.ChainParams().Name, + } + } + + // Import the private key, handling any errors. + _, err = w.ImportPrivateKey(wif, nil, *cmd.Rescan) + switch { + case waddrmgr.IsError(err, waddrmgr.ErrDuplicateAddress): + // Do not return duplicate key errors to the client. + return nil, nil + case waddrmgr.IsError(err, waddrmgr.ErrLocked): + return nil, &ErrWalletUnlockNeeded + } + + return nil, err +} + +// KeypoolRefill handles the keypoolrefill command. Since we handle the keypool +// automatically this does nothing since refilling is never manually required. +func KeypoolRefill(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + return nil, nil +} + +// CreateNewAccount handles a createnewaccount request by creating and +// returning a new account. If the last account has no transaction history +// as per BIP 0044 a new account cannot be created so an error will be returned. +func CreateNewAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.CreateNewAccountCmd) + + // The wildcard * is reserved by the rpc server with the special meaning + // of "all accounts", so disallow naming accounts to this string. + if cmd.Account == "*" { + return nil, &ErrReservedAccountName + } + + // Check that we are within the maximum allowed non-empty accounts limit. + account, err := w.Manager.LastAccount() + if err != nil { + return nil, err + } + if account > maxEmptyAccounts { + used, err := w.AccountUsed(account) + if err != nil { + return nil, err + } + if !used { + return nil, errors.New("cannot create account: " + + "previous account has no transaction history") + } + } + + _, err = w.NextAccount(cmd.Account) + if waddrmgr.IsError(err, waddrmgr.ErrLocked) { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCWalletUnlockNeeded, + Message: "Creating an account requires the wallet to be unlocked. " + + "Enter the wallet passphrase with walletpassphrase to unlock", + } + } + return nil, err +} + +// RenameAccount handles a renameaccount request by renaming an account. +// If the account does not exist an appropiate error will be returned. +func RenameAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.RenameAccountCmd) + + // The wildcard * is reserved by the rpc server with the special meaning + // of "all accounts", so disallow naming accounts to this string. + if cmd.NewAccount == "*" { + return nil, &ErrReservedAccountName + } + + // Check that given account exists + account, err := w.Manager.LookupAccount(cmd.OldAccount) + if err != nil { + return nil, err + } + return nil, w.RenameAccount(account, cmd.NewAccount) +} + +// GetNewAddress handles a getnewaddress request by returning a new +// address for an account. If the account does not exist an appropiate +// error is returned. +// TODO: Follow BIP 0044 and warn if number of unused addresses exceeds +// the gap limit. +func GetNewAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.GetNewAddressCmd) + + acctName := "default" + if cmd.Account != nil { + acctName = *cmd.Account + } + account, err := w.Manager.LookupAccount(acctName) + if err != nil { + return nil, err + } + addr, err := w.NewAddress(account) + if err != nil { + return nil, err + } + + // Return the new payment address string. + return addr.EncodeAddress(), nil +} + +// GetRawChangeAddress handles a getrawchangeaddress request by creating +// and returning a new change address for an account. +// +// Note: bitcoind allows specifying the account as an optional parameter, +// but ignores the parameter. +func GetRawChangeAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.GetRawChangeAddressCmd) + + acctName := "default" + if cmd.Account != nil { + acctName = *cmd.Account + } + account, err := w.Manager.LookupAccount(acctName) + if err != nil { + return nil, err + } + addr, err := w.NewChangeAddress(account) + if err != nil { + return nil, err + } + + // Return the new payment address string. + return addr.EncodeAddress(), nil +} + +// GetReceivedByAccount handles a getreceivedbyaccount request by returning +// the total amount received by addresses of an account. +func GetReceivedByAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.GetReceivedByAccountCmd) + + account, err := w.Manager.LookupAccount(cmd.Account) + if err != nil { + return nil, err + } + + bal, _, err := w.TotalReceivedForAccount(account, int32(*cmd.MinConf)) + if err != nil { + return nil, err + } + + return bal.ToBTC(), nil +} + +// GetReceivedByAddress handles a getreceivedbyaddress request by returning +// the total amount received by a single address. +func GetReceivedByAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.GetReceivedByAddressCmd) + + addr, err := decodeAddress(cmd.Address, w.ChainParams()) + if err != nil { + return nil, err + } + total, err := w.TotalReceivedForAddr(addr, int32(*cmd.MinConf)) + if err != nil { + return nil, err + } + + return total.ToBTC(), nil +} + +// GetTransaction handles a gettransaction request by returning details about +// a single transaction saved by wallet. +func GetTransaction(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.GetTransactionCmd) + + txSha, err := wire.NewShaHashFromStr(cmd.Txid) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDecodeHexString, + Message: "Transaction hash string decode failed: " + err.Error(), + } + } + + details, err := w.TxStore.TxDetails(txSha) + if err != nil { + return nil, err + } + if details == nil { + return nil, &ErrNoTransactionInfo + } + + syncBlock := w.Manager.SyncedTo() + + // TODO: The serialized transaction is already in the DB, so + // reserializing can be avoided here. + var txBuf bytes.Buffer + txBuf.Grow(details.MsgTx.SerializeSize()) + err = details.MsgTx.Serialize(&txBuf) + if err != nil { + return nil, err + } + + // TODO: Add a "generated" field to this result type. "generated":true + // is only added if the transaction is a coinbase. + ret := btcjson.GetTransactionResult{ + TxID: cmd.Txid, + Hex: hex.EncodeToString(txBuf.Bytes()), + Time: details.Received.Unix(), + TimeReceived: details.Received.Unix(), + WalletConflicts: []string{}, // Not saved + //Generated: blockchain.IsCoinBaseTx(&details.MsgTx), + } + + if details.Block.Height != -1 { + ret.BlockHash = details.Block.Hash.String() + ret.BlockTime = details.Block.Time.Unix() + ret.Confirmations = int64(confirms(details.Block.Height, syncBlock.Height)) + } + + var ( + debitTotal btcutil.Amount + creditTotal btcutil.Amount // Excludes change + outputTotal btcutil.Amount + fee btcutil.Amount + feeF64 float64 + ) + for _, deb := range details.Debits { + debitTotal += deb.Amount + } + for _, cred := range details.Credits { + if !cred.Change { + creditTotal += cred.Amount + } + } + for _, output := range details.MsgTx.TxOut { + outputTotal -= btcutil.Amount(output.Value) + } + // Fee can only be determined if every input is a debit. + if len(details.Debits) == len(details.MsgTx.TxIn) { + fee = debitTotal - outputTotal + feeF64 = fee.ToBTC() + } + + if len(details.Debits) == 0 { + // Credits must be set later, but since we know the full length + // of the details slice, allocate it with the correct cap. + ret.Details = make([]btcjson.GetTransactionDetailsResult, 0, len(details.Credits)) + } else { + ret.Details = make([]btcjson.GetTransactionDetailsResult, 1, len(details.Credits)+1) + + ret.Details[0] = btcjson.GetTransactionDetailsResult{ + // Fields left zeroed: + // InvolvesWatchOnly + // Account + // Address + // Vout + // + // TODO(jrick): Address and Vout should always be set, + // but we're doing the wrong thing here by not matching + // core. Instead, gettransaction should only be adding + // details for transaction outputs, just like + // listtransactions (but using the short result format). + Category: "send", + Amount: (-debitTotal).ToBTC(), // negative since it is a send + Fee: &feeF64, + } + ret.Fee = feeF64 + } + + credCat := wallet.RecvCategory(details, syncBlock.Height).String() + for _, cred := range details.Credits { + // Change is ignored. + if cred.Change { + continue + } + + var addr string + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + details.MsgTx.TxOut[cred.Index].PkScript, w.ChainParams()) + if err == nil && len(addrs) == 1 { + addr = addrs[0].EncodeAddress() + } + + ret.Details = append(ret.Details, btcjson.GetTransactionDetailsResult{ + // Fields left zeroed: + // InvolvesWatchOnly + // Account + // Fee + Address: addr, + Category: credCat, + Amount: cred.Amount.ToBTC(), + Vout: cred.Index, + }) + } + + ret.Amount = creditTotal.ToBTC() + return ret, nil +} + +// These generators create the following global variables in this package: +// +// var localeHelpDescs map[string]func() map[string]string +// var requestUsages string +// +// localeHelpDescs maps from locale strings (e.g. "en_US") to a function that +// builds a map of help texts for each RPC server method. This prevents help +// text maps for every locale map from being rooted and created during init. +// Instead, the appropiate function is looked up when help text is first needed +// using the current locale and saved to the global below for futher reuse. +// +// requestUsages contains single line usages for every supported request, +// separated by newlines. It is set during init. These usages are used for all +// locales. +// +//go:generate go run ../../internal/rpchelp/genrpcserverhelp.go legacyrpc +//go:generate gofmt -w rpcserverhelp.go + +var helpDescs map[string]string +var helpDescsMu sync.Mutex // Help may execute concurrently, so synchronize access. + +// HelpWithChainRPC handles the help request when the RPC server has been +// associated with a consensus RPC client. The additional RPC client is used to +// include help messages for methods implemented by the consensus server via RPC +// passthrough. +func HelpWithChainRPC(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient) (interface{}, error) { + return help(icmd, w, chainClient) +} + +// HelpNoChainRPC handles the help request when the RPC server has not been +// associated with a consensus RPC client. No help messages are included for +// passthrough requests. +func HelpNoChainRPC(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + return help(icmd, w, nil) +} + +// help handles the help request by returning one line usage of all available +// methods, or full help for a specific method. The chainClient is optional, +// and this is simply a helper function for the HelpNoChainRPC and +// HelpWithChainRPC handlers. +func help(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient) (interface{}, error) { + cmd := icmd.(*btcjson.HelpCmd) + + // btcd returns different help messages depending on the kind of + // connection the client is using. Only methods availble to HTTP POST + // clients are available to be used by wallet clients, even though + // wallet itself is a websocket client to btcd. Therefore, create a + // POST client as needed. + // + // Returns nil if chainClient is currently nil or there is an error + // creating the client. + // + // This is hacky and is probably better handled by exposing help usage + // texts in a non-internal btcd package. + postClient := func() *btcrpcclient.Client { + if chainClient == nil { + return nil + } + c, err := chainClient.POSTClient() + if err != nil { + return nil + } + return c + } + if cmd.Command == nil || *cmd.Command == "" { + // Prepend chain server usage if it is available. + usages := requestUsages + client := postClient() + if client != nil { + rawChainUsage, err := client.RawRequest("help", nil) + var chainUsage string + if err == nil { + _ = json.Unmarshal([]byte(rawChainUsage), &chainUsage) + } + if chainUsage != "" { + usages = "Chain server usage:\n\n" + chainUsage + "\n\n" + + "Wallet server usage (overrides chain requests):\n\n" + + requestUsages + } + } + return usages, nil + } + + defer helpDescsMu.Unlock() + helpDescsMu.Lock() + + if helpDescs == nil { + // TODO: Allow other locales to be set via config or detemine + // this from environment variables. For now, hardcode US + // English. + helpDescs = localeHelpDescs["en_US"]() + } + + helpText, ok := helpDescs[*cmd.Command] + if ok { + return helpText, nil + } + + // Return the chain server's detailed help if possible. + var chainHelp string + client := postClient() + if client != nil { + param := make([]byte, len(*cmd.Command)+2) + param[0] = '"' + copy(param[1:], *cmd.Command) + param[len(param)-1] = '"' + rawChainHelp, err := client.RawRequest("help", []json.RawMessage{param}) + if err == nil { + _ = json.Unmarshal([]byte(rawChainHelp), &chainHelp) + } + } + if chainHelp != "" { + return chainHelp, nil + } + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: fmt.Sprintf("No help for method '%s'", *cmd.Command), + } +} + +// ListAccounts handles a listaccounts request by returning a map of account +// names to their balances. +func ListAccounts(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.ListAccountsCmd) + + accountBalances := map[string]float64{} + var accounts []uint32 + err := w.Manager.ForEachAccount(func(account uint32) error { + accounts = append(accounts, account) + return nil + }) + if err != nil { + return nil, err + } + minConf := int32(*cmd.MinConf) + for _, account := range accounts { + acctName, err := w.Manager.AccountName(account) + if err != nil { + return nil, &ErrAccountNameNotFound + } + bals, err := w.CalculateAccountBalances(account, minConf) + if err != nil { + return nil, err + } + accountBalances[acctName] = bals.Spendable.ToBTC() + } + // Return the map. This will be marshaled into a JSON object. + return accountBalances, nil +} + +// ListLockUnspent handles a listlockunspent request by returning an slice of +// all locked outpoints. +func ListLockUnspent(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + return w.LockedOutpoints(), nil +} + +// ListReceivedByAccount handles a listreceivedbyaccount request by returning +// a slice of objects, each one containing: +// "account": the receiving account; +// "amount": total amount received by the account; +// "confirmations": number of confirmations of the most recent transaction. +// It takes two parameters: +// "minconf": minimum number of confirmations to consider a transaction - +// default: one; +// "includeempty": whether or not to include addresses that have no transactions - +// default: false. +func ListReceivedByAccount(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.ListReceivedByAccountCmd) + + var accounts []uint32 + err := w.Manager.ForEachAccount(func(account uint32) error { + accounts = append(accounts, account) + return nil + }) + if err != nil { + return nil, err + } + + ret := make([]btcjson.ListReceivedByAccountResult, 0, len(accounts)) + minConf := int32(*cmd.MinConf) + for _, account := range accounts { + acctName, err := w.Manager.AccountName(account) + if err != nil { + return nil, &ErrAccountNameNotFound + } + bal, confirmations, err := w.TotalReceivedForAccount(account, + minConf) + if err != nil { + return nil, err + } + ret = append(ret, btcjson.ListReceivedByAccountResult{ + Account: acctName, + Amount: bal.ToBTC(), + Confirmations: uint64(confirmations), + }) + } + return ret, nil +} + +// ListReceivedByAddress handles a listreceivedbyaddress request by returning +// a slice of objects, each one containing: +// "account": the account of the receiving address; +// "address": the receiving address; +// "amount": total amount received by the address; +// "confirmations": number of confirmations of the most recent transaction. +// It takes two parameters: +// "minconf": minimum number of confirmations to consider a transaction - +// default: one; +// "includeempty": whether or not to include addresses that have no transactions - +// default: false. +func ListReceivedByAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.ListReceivedByAddressCmd) + + // Intermediate data for each address. + type AddrData struct { + // Total amount received. + amount btcutil.Amount + // Number of confirmations of the last transaction. + confirmations int32 + // Hashes of transactions which include an output paying to the address + tx []string + // Account which the address belongs to + account string + } + + syncBlock := w.Manager.SyncedTo() + + // Intermediate data for all addresses. + allAddrData := make(map[string]AddrData) + // Create an AddrData entry for each active address in the account. + // Otherwise we'll just get addresses from transactions later. + sortedAddrs, err := w.SortedActivePaymentAddresses() + if err != nil { + return nil, err + } + for _, address := range sortedAddrs { + // There might be duplicates, just overwrite them. + allAddrData[address] = AddrData{} + } + + minConf := *cmd.MinConf + var endHeight int32 + if minConf == 0 { + endHeight = -1 + } else { + endHeight = syncBlock.Height - int32(minConf) + 1 + } + err = w.TxStore.RangeTransactions(0, endHeight, func(details []wtxmgr.TxDetails) (bool, error) { + confirmations := confirms(details[0].Block.Height, syncBlock.Height) + for _, tx := range details { + for _, cred := range tx.Credits { + pkScript := tx.MsgTx.TxOut[cred.Index].PkScript + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + pkScript, w.ChainParams()) + if err != nil { + // Non standard script, skip. + continue + } + for _, addr := range addrs { + addrStr := addr.EncodeAddress() + addrData, ok := allAddrData[addrStr] + if ok { + addrData.amount += cred.Amount + // Always overwrite confirmations with newer ones. + addrData.confirmations = confirmations + } else { + addrData = AddrData{ + amount: cred.Amount, + confirmations: confirmations, + } + } + addrData.tx = append(addrData.tx, tx.Hash.String()) + allAddrData[addrStr] = addrData + } + } + } + return false, nil + }) + if err != nil { + return nil, err + } + + // Massage address data into output format. + numAddresses := len(allAddrData) + ret := make([]btcjson.ListReceivedByAddressResult, numAddresses, numAddresses) + idx := 0 + for address, addrData := range allAddrData { + ret[idx] = btcjson.ListReceivedByAddressResult{ + Address: address, + Amount: addrData.amount.ToBTC(), + Confirmations: uint64(addrData.confirmations), + TxIDs: addrData.tx, + } + idx++ + } + return ret, nil +} + +// ListSinceBlock handles a listsinceblock request by returning an array of maps +// with details of sent and received wallet transactions since the given block. +func ListSinceBlock(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient) (interface{}, error) { + cmd := icmd.(*btcjson.ListSinceBlockCmd) + + syncBlock := w.Manager.SyncedTo() + targetConf := int64(*cmd.TargetConfirmations) + + // For the result we need the block hash for the last block counted + // in the blockchain due to confirmations. We send this off now so that + // it can arrive asynchronously while we figure out the rest. + gbh := chainClient.GetBlockHashAsync(int64(syncBlock.Height) + 1 - targetConf) + + var start int32 + if cmd.BlockHash != nil { + hash, err := wire.NewShaHashFromStr(*cmd.BlockHash) + if err != nil { + return nil, DeserializationError{err} + } + block, err := chainClient.GetBlockVerbose(hash, false) + if err != nil { + return nil, err + } + start = int32(block.Height) + 1 + } + + txInfoList, err := w.ListSinceBlock(start, -1, syncBlock.Height) + if err != nil { + return nil, err + } + + // Done with work, get the response. + blockHash, err := gbh.Receive() + if err != nil { + return nil, err + } + + res := btcjson.ListSinceBlockResult{ + Transactions: txInfoList, + LastBlock: blockHash.String(), + } + return res, nil +} + +// ListTransactions handles a listtransactions request by returning an +// array of maps with details of sent and recevied wallet transactions. +func ListTransactions(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.ListTransactionsCmd) + + // TODO: ListTransactions does not currently understand the difference + // between transactions pertaining to one account from another. This + // will be resolved when wtxmgr is combined with the waddrmgr namespace. + + if cmd.Account != nil && *cmd.Account != "*" { + // For now, don't bother trying to continue if the user + // specified an account, since this can't be (easily or + // efficiently) calculated. + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCWallet, + Message: "Transactions are not yet grouped by account", + } + } + + return w.ListTransactions(*cmd.From, *cmd.Count) +} + +// ListAddressTransactions handles a listaddresstransactions request by +// returning an array of maps with details of spent and received wallet +// transactions. The form of the reply is identical to listtransactions, +// but the array elements are limited to transaction details which are +// about the addresess included in the request. +func ListAddressTransactions(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.ListAddressTransactionsCmd) + + if cmd.Account != nil && *cmd.Account != "*" { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: "Listing transactions for addresses may only be done for all accounts", + } + } + + // Decode addresses. + hash160Map := make(map[string]struct{}) + for _, addrStr := range cmd.Addresses { + addr, err := decodeAddress(addrStr, w.ChainParams()) + if err != nil { + return nil, err + } + hash160Map[string(addr.ScriptAddress())] = struct{}{} + } + + return w.ListAddressTransactions(hash160Map) +} + +// ListAllTransactions handles a listalltransactions request by returning +// a map with details of sent and recevied wallet transactions. This is +// similar to ListTransactions, except it takes only a single optional +// argument for the account name and replies with all transactions. +func ListAllTransactions(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.ListAllTransactionsCmd) + + if cmd.Account != nil && *cmd.Account != "*" { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidParameter, + Message: "Listing all transactions may only be done for all accounts", + } + } + + return w.ListAllTransactions() +} + +// ListUnspent handles the listunspent command. +func ListUnspent(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.ListUnspentCmd) + + var addresses map[string]struct{} + if cmd.Addresses != nil { + addresses = make(map[string]struct{}) + // confirm that all of them are good: + for _, as := range *cmd.Addresses { + a, err := decodeAddress(as, w.ChainParams()) + if err != nil { + return nil, err + } + addresses[a.EncodeAddress()] = struct{}{} + } + } + + return w.ListUnspent(int32(*cmd.MinConf), int32(*cmd.MaxConf), addresses) +} + +// LockUnspent handles the lockunspent command. +func LockUnspent(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.LockUnspentCmd) + + switch { + case cmd.Unlock && len(cmd.Transactions) == 0: + w.ResetLockedOutpoints() + default: + for _, input := range cmd.Transactions { + txSha, err := wire.NewShaHashFromStr(input.Txid) + if err != nil { + return nil, ParseError{err} + } + op := wire.OutPoint{Hash: *txSha, Index: input.Vout} + if cmd.Unlock { + w.UnlockOutpoint(op) + } else { + w.LockOutpoint(op) + } + } + } + return true, nil +} + +// sendPairs creates and sends payment transactions. +// It returns the transaction hash in string format upon success +// All errors are returned in btcjson.RPCError format +func sendPairs(w *wallet.Wallet, amounts map[string]btcutil.Amount, + account uint32, minconf int32) (string, error) { + txSha, err := w.SendPairs(amounts, account, minconf) + if err != nil { + if err == wallet.ErrNonPositiveAmount { + return "", ErrNeedPositiveAmount + } + if waddrmgr.IsError(err, waddrmgr.ErrLocked) { + return "", &ErrWalletUnlockNeeded + } + switch err.(type) { + case btcjson.RPCError: + return "", err + } + + return "", &btcjson.RPCError{ + Code: btcjson.ErrRPCInternal.Code, + Message: err.Error(), + } + } + + txShaStr := txSha.String() + log.Infof("Successfully sent transaction %v", txShaStr) + return txShaStr, nil +} + +// SendFrom handles a sendfrom RPC request by creating a new transaction +// spending unspent transaction outputs for a wallet to another payment +// address. Leftover inputs not sent to the payment address or a fee for +// the miner are sent back to a new address in the wallet. Upon success, +// the TxID for the created transaction is returned. +func SendFrom(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient) (interface{}, error) { + cmd := icmd.(*btcjson.SendFromCmd) + + // Transaction comments are not yet supported. Error instead of + // pretending to save them. + if cmd.Comment != nil || cmd.CommentTo != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCUnimplemented, + Message: "Transaction comments are not yet supported", + } + } + + account, err := w.Manager.LookupAccount(cmd.FromAccount) + if err != nil { + return nil, err + } + + // Check that signed integer parameters are positive. + if cmd.Amount < 0 { + return nil, ErrNeedPositiveAmount + } + minConf := int32(*cmd.MinConf) + if minConf < 0 { + return nil, ErrNeedPositiveMinconf + } + // Create map of address and amount pairs. + amt, err := btcutil.NewAmount(cmd.Amount) + if err != nil { + return nil, err + } + pairs := map[string]btcutil.Amount{ + cmd.ToAddress: amt, + } + + return sendPairs(w, pairs, account, minConf) +} + +// SendMany handles a sendmany RPC request by creating a new transaction +// spending unspent transaction outputs for a wallet to any number of +// payment addresses. Leftover inputs not sent to the payment address +// or a fee for the miner are sent back to a new address in the wallet. +// Upon success, the TxID for the created transaction is returned. +func SendMany(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.SendManyCmd) + + // Transaction comments are not yet supported. Error instead of + // pretending to save them. + if cmd.Comment != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCUnimplemented, + Message: "Transaction comments are not yet supported", + } + } + + account, err := w.Manager.LookupAccount(cmd.FromAccount) + if err != nil { + return nil, err + } + + // Check that minconf is positive. + minConf := int32(*cmd.MinConf) + if minConf < 0 { + return nil, ErrNeedPositiveMinconf + } + + // Recreate address/amount pairs, using btcutil.Amount. + pairs := make(map[string]btcutil.Amount, len(cmd.Amounts)) + for k, v := range cmd.Amounts { + amt, err := btcutil.NewAmount(v) + if err != nil { + return nil, err + } + pairs[k] = amt + } + + return sendPairs(w, pairs, account, minConf) +} + +// SendToAddress handles a sendtoaddress RPC request by creating a new +// transaction spending unspent transaction outputs for a wallet to another +// payment address. Leftover inputs not sent to the payment address or a fee +// for the miner are sent back to a new address in the wallet. Upon success, +// the TxID for the created transaction is returned. +func SendToAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.SendToAddressCmd) + + // Transaction comments are not yet supported. Error instead of + // pretending to save them. + if cmd.Comment != nil || cmd.CommentTo != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCUnimplemented, + Message: "Transaction comments are not yet supported", + } + } + + amt, err := btcutil.NewAmount(cmd.Amount) + if err != nil { + return nil, err + } + + // Check that signed integer parameters are positive. + if amt < 0 { + return nil, ErrNeedPositiveAmount + } + + // Mock up map of address and amount pairs. + pairs := map[string]btcutil.Amount{ + cmd.Address: amt, + } + + // sendtoaddress always spends from the default account, this matches bitcoind + return sendPairs(w, pairs, waddrmgr.DefaultAccountNum, 1) +} + +// SetTxFee sets the transaction fee per kilobyte added to transactions. +func SetTxFee(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.SetTxFeeCmd) + + // Check that amount is not negative. + if cmd.Amount < 0 { + return nil, ErrNeedPositiveAmount + } + + incr, err := btcutil.NewAmount(cmd.Amount) + if err != nil { + return nil, err + } + w.FeeIncrement = incr + + // A boolean true result is returned upon success. + return true, nil +} + +// SignMessage signs the given message with the private key for the given +// address +func SignMessage(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.SignMessageCmd) + + addr, err := decodeAddress(cmd.Address, w.ChainParams()) + if err != nil { + return nil, err + } + + ainfo, err := w.Manager.Address(addr) + if err != nil { + return nil, err + } + pka, ok := ainfo.(waddrmgr.ManagedPubKeyAddress) + if !ok { + msg := fmt.Sprintf("Address '%s' does not have an associated private key", addr) + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCInvalidAddressOrKey, + Message: msg, + } + } + privKey, err := pka.PrivKey() + if err != nil { + return nil, err + } + + var buf bytes.Buffer + wire.WriteVarString(&buf, 0, "Bitcoin Signed Message:\n") + wire.WriteVarString(&buf, 0, cmd.Message) + messageHash := wire.DoubleSha256(buf.Bytes()) + sigbytes, err := btcec.SignCompact(btcec.S256(), privKey, + messageHash, ainfo.Compressed()) + if err != nil { + return nil, err + } + + return base64.StdEncoding.EncodeToString(sigbytes), nil +} + +// pendingTx is used for async fetching of transaction dependancies in +// SignRawTransaction. +type pendingTx struct { + resp btcrpcclient.FutureGetRawTransactionResult + inputs []uint32 // list of inputs that care about this tx. +} + +// SignRawTransaction handles the signrawtransaction command. +func SignRawTransaction(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient) (interface{}, error) { + cmd := icmd.(*btcjson.SignRawTransactionCmd) + + serializedTx, err := decodeHexStr(cmd.RawTx) + if err != nil { + return nil, err + } + tx := wire.NewMsgTx() + err = tx.Deserialize(bytes.NewBuffer(serializedTx)) + if err != nil { + e := errors.New("TX decode failed") + return nil, DeserializationError{e} + } + + var hashType txscript.SigHashType + switch *cmd.Flags { + case "ALL": + hashType = txscript.SigHashAll + case "NONE": + hashType = txscript.SigHashNone + case "SINGLE": + hashType = txscript.SigHashSingle + case "ALL|ANYONECANPAY": + hashType = txscript.SigHashAll | txscript.SigHashAnyOneCanPay + case "NONE|ANYONECANPAY": + hashType = txscript.SigHashNone | txscript.SigHashAnyOneCanPay + case "SINGLE|ANYONECANPAY": + hashType = txscript.SigHashSingle | txscript.SigHashAnyOneCanPay + default: + e := errors.New("Invalid sighash parameter") + return nil, InvalidParameterError{e} + } + + // TODO: really we probably should look these up with btcd anyway to + // make sure that they match the blockchain if present. + inputs := make(map[wire.OutPoint][]byte) + scripts := make(map[string][]byte) + var cmdInputs []btcjson.RawTxInput + if cmd.Inputs != nil { + cmdInputs = *cmd.Inputs + } + for _, rti := range cmdInputs { + inputSha, err := wire.NewShaHashFromStr(rti.Txid) + if err != nil { + return nil, DeserializationError{err} + } + + script, err := decodeHexStr(rti.ScriptPubKey) + if err != nil { + return nil, err + } + + // redeemScript is only actually used iff the user provided + // private keys. In which case, it is used to get the scripts + // for signing. If the user did not provide keys then we always + // get scripts from the wallet. + // Empty strings are ok for this one and hex.DecodeString will + // DTRT. + if cmd.PrivKeys != nil && len(*cmd.PrivKeys) != 0 { + redeemScript, err := decodeHexStr(rti.RedeemScript) + if err != nil { + return nil, err + } + + addr, err := btcutil.NewAddressScriptHash(redeemScript, + w.ChainParams()) + if err != nil { + return nil, DeserializationError{err} + } + scripts[addr.String()] = redeemScript + } + inputs[wire.OutPoint{ + Hash: *inputSha, + Index: rti.Vout, + }] = script + } + + // Now we go and look for any inputs that we were not provided by + // querying btcd with getrawtransaction. We queue up a bunch of async + // requests and will wait for replies after we have checked the rest of + // the arguments. + requested := make(map[wire.ShaHash]*pendingTx) + for _, txIn := range tx.TxIn { + // Did we get this txin from the arguments? + if _, ok := inputs[txIn.PreviousOutPoint]; ok { + continue + } + + // Are we already fetching this tx? If so mark us as interested + // in this outpoint. (N.B. that any *sane* tx will only + // reference each outpoint once, since anything else is a double + // spend. We don't check this ourselves to save having to scan + // the array, it will fail later if so). + if ptx, ok := requested[txIn.PreviousOutPoint.Hash]; ok { + ptx.inputs = append(ptx.inputs, + txIn.PreviousOutPoint.Index) + continue + } + + // Never heard of this one before, request it. + prevHash := &txIn.PreviousOutPoint.Hash + requested[txIn.PreviousOutPoint.Hash] = &pendingTx{ + resp: chainClient.GetRawTransactionAsync(prevHash), + inputs: []uint32{txIn.PreviousOutPoint.Index}, + } + } + + // Parse list of private keys, if present. If there are any keys here + // they are the keys that we may use for signing. If empty we will + // use any keys known to us already. + var keys map[string]*btcutil.WIF + if cmd.PrivKeys != nil { + keys = make(map[string]*btcutil.WIF) + + for _, key := range *cmd.PrivKeys { + wif, err := btcutil.DecodeWIF(key) + if err != nil { + return nil, DeserializationError{err} + } + + if !wif.IsForNet(w.ChainParams()) { + s := "key network doesn't match wallet's" + return nil, DeserializationError{errors.New(s)} + } + + addr, err := btcutil.NewAddressPubKey(wif.SerializePubKey(), + w.ChainParams()) + if err != nil { + return nil, DeserializationError{err} + } + keys[addr.EncodeAddress()] = wif + } + } + + // We have checked the rest of the args. now we can collect the async + // txs. TODO: If we don't mind the possibility of wasting work we could + // move waiting to the following loop and be slightly more asynchronous. + for txid, ptx := range requested { + tx, err := ptx.resp.Receive() + if err != nil { + return nil, err + } + + for _, input := range ptx.inputs { + if input >= uint32(len(tx.MsgTx().TxOut)) { + e := fmt.Errorf("input %s:%d is not in tx", + txid.String(), input) + return nil, InvalidParameterError{e} + } + + inputs[wire.OutPoint{ + Hash: txid, + Index: input, + }] = tx.MsgTx().TxOut[input].PkScript + } + } + + // All args collected. Now we can sign all the inputs that we can. + // `complete' denotes that we successfully signed all outputs and that + // all scripts will run to completion. This is returned as part of the + // reply. + signErrs, err := w.SignTransaction(tx, hashType, inputs, keys, scripts) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + buf.Grow(tx.SerializeSize()) + + // All returned errors (not OOM, which panics) encounted during + // bytes.Buffer writes are unexpected. + if err = tx.Serialize(&buf); err != nil { + panic(err) + } + + signErrors := make([]btcjson.SignRawTransactionError, 0, len(signErrs)) + for _, e := range signErrs { + input := tx.TxIn[e.InputIndex] + signErrors = append(signErrors, btcjson.SignRawTransactionError{ + TxID: input.PreviousOutPoint.Hash.String(), + Vout: input.PreviousOutPoint.Index, + ScriptSig: hex.EncodeToString(input.SignatureScript), + Sequence: input.Sequence, + Error: e.Error.Error(), + }) + } + + return btcjson.SignRawTransactionResult{ + Hex: hex.EncodeToString(buf.Bytes()), + Complete: len(signErrors) == 0, + Errors: signErrors, + }, nil +} + +// ValidateAddress handles the validateaddress command. +func ValidateAddress(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.ValidateAddressCmd) + + result := btcjson.ValidateAddressWalletResult{} + addr, err := decodeAddress(cmd.Address, w.ChainParams()) + if err != nil { + // Use result zero value (IsValid=false). + return result, nil + } + + // We could put whether or not the address is a script here, + // by checking the type of "addr", however, the reference + // implementation only puts that information if the script is + // "ismine", and we follow that behaviour. + result.Address = addr.EncodeAddress() + result.IsValid = true + + ainfo, err := w.Manager.Address(addr) + if err != nil { + if waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) { + // No additional information available about the address. + return result, nil + } + return nil, err + } + + // The address lookup was successful which means there is further + // information about it available and it is "mine". + result.IsMine = true + acctName, err := w.Manager.AccountName(ainfo.Account()) + if err != nil { + return nil, &ErrAccountNameNotFound + } + result.Account = acctName + + switch ma := ainfo.(type) { + case waddrmgr.ManagedPubKeyAddress: + result.IsCompressed = ma.Compressed() + result.PubKey = ma.ExportPubKey() + + case waddrmgr.ManagedScriptAddress: + result.IsScript = true + + // The script is only available if the manager is unlocked, so + // just break out now if there is an error. + script, err := ma.Script() + if err != nil { + break + } + result.Hex = hex.EncodeToString(script) + + // This typically shouldn't fail unless an invalid script was + // imported. However, if it fails for any reason, there is no + // further information available, so just set the script type + // a non-standard and break out now. + class, addrs, reqSigs, err := txscript.ExtractPkScriptAddrs( + script, w.ChainParams()) + if err != nil { + result.Script = txscript.NonStandardTy.String() + break + } + + addrStrings := make([]string, len(addrs)) + for i, a := range addrs { + addrStrings[i] = a.EncodeAddress() + } + result.Addresses = addrStrings + + // Multi-signature scripts also provide the number of required + // signatures. + result.Script = class.String() + if class == txscript.MultiSigTy { + result.SigsRequired = int32(reqSigs) + } + } + + return result, nil +} + +// VerifyMessage handles the verifymessage command by verifying the provided +// compact signature for the given address and message. +func VerifyMessage(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.VerifyMessageCmd) + + addr, err := decodeAddress(cmd.Address, w.ChainParams()) + if err != nil { + return nil, err + } + + // decode base64 signature + sig, err := base64.StdEncoding.DecodeString(cmd.Signature) + if err != nil { + return nil, err + } + + // Validate the signature - this just shows that it was valid at all. + // we will compare it with the key next. + var buf bytes.Buffer + wire.WriteVarString(&buf, 0, "Bitcoin Signed Message:\n") + wire.WriteVarString(&buf, 0, cmd.Message) + expectedMessageHash := wire.DoubleSha256(buf.Bytes()) + pk, wasCompressed, err := btcec.RecoverCompact(btcec.S256(), sig, + expectedMessageHash) + if err != nil { + return nil, err + } + + var serializedPubKey []byte + if wasCompressed { + serializedPubKey = pk.SerializeCompressed() + } else { + serializedPubKey = pk.SerializeUncompressed() + } + // Verify that the signed-by address matches the given address + switch checkAddr := addr.(type) { + case *btcutil.AddressPubKeyHash: // ok + return bytes.Equal(btcutil.Hash160(serializedPubKey), checkAddr.Hash160()[:]), nil + case *btcutil.AddressPubKey: // ok + return string(serializedPubKey) == checkAddr.String(), nil + default: + return nil, errors.New("address type not supported") + } +} + +// WalletIsLocked handles the walletislocked extension request by +// returning the current lock state (false for unlocked, true for locked) +// of an account. +func WalletIsLocked(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + return w.Locked(), nil +} + +// WalletLock handles a walletlock request by locking the all account +// wallets, returning an error if any wallet is not encrypted (for example, +// a watching-only wallet). +func WalletLock(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + w.Lock() + return nil, nil +} + +// WalletPassphrase responds to the walletpassphrase request by unlocking +// the wallet. The decryption key is saved in the wallet until timeout +// seconds expires, after which the wallet is locked. +func WalletPassphrase(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.WalletPassphraseCmd) + + timeout := time.Second * time.Duration(cmd.Timeout) + err := w.Unlock([]byte(cmd.Passphrase), time.After(timeout)) + return nil, err +} + +// WalletPassphraseChange responds to the walletpassphrasechange request +// by unlocking all accounts with the provided old passphrase, and +// re-encrypting each private key with an AES key derived from the new +// passphrase. +// +// If the old passphrase is correct and the passphrase is changed, all +// wallets will be immediately locked. +func WalletPassphraseChange(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*btcjson.WalletPassphraseChangeCmd) + + err := w.ChangePassphrase([]byte(cmd.OldPassphrase), + []byte(cmd.NewPassphrase)) + if waddrmgr.IsError(err, waddrmgr.ErrWrongPassphrase) { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCWalletPassphraseIncorrect, + Message: "Incorrect passphrase", + } + } + return nil, err +} + +// decodeHexStr decodes the hex encoding of a string, possibly prepending a +// leading '0' character if there is an odd number of bytes in the hex string. +// This is to prevent an error for an invalid hex string when using an odd +// number of bytes when calling hex.Decode. +func decodeHexStr(hexStr string) ([]byte, error) { + if len(hexStr)%2 != 0 { + hexStr = "0" + hexStr + } + decoded, err := hex.DecodeString(hexStr) + if err != nil { + return nil, &btcjson.RPCError{ + Code: btcjson.ErrRPCDecodeHexString, + Message: "Hex string decode failed: " + err.Error(), + } + } + return decoded, nil +} diff --git a/rpcserver_test.go b/rpc/legacyrpc/rpcserver_test.go similarity index 98% rename from rpcserver_test.go rename to rpc/legacyrpc/rpcserver_test.go index 033bbf1..19fc8c3 100644 --- a/rpcserver_test.go +++ b/rpc/legacyrpc/rpcserver_test.go @@ -14,7 +14,7 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -package main +package legacyrpc import ( "net/http" diff --git a/rpcserverhelp.go b/rpc/legacyrpc/rpcserverhelp.go similarity index 99% rename from rpcserverhelp.go rename to rpc/legacyrpc/rpcserverhelp.go index 679d3fa..ed2da17 100644 --- a/rpcserverhelp.go +++ b/rpc/legacyrpc/rpcserverhelp.go @@ -1,6 +1,6 @@ // AUTOGENERATED by internal/rpchelp/genrpcserverhelp.go; do not edit. -package main +package legacyrpc func helpDescsEnUS() map[string]string { return map[string]string{ diff --git a/rpc/legacyrpc/server.go b/rpc/legacyrpc/server.go new file mode 100644 index 0000000..4cc8417 --- /dev/null +++ b/rpc/legacyrpc/server.go @@ -0,0 +1,964 @@ +/* + * Copyright (c) 2013-2015 The btcsuite developers + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package legacyrpc + +import ( + "crypto/subtle" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "sync" + "sync/atomic" + "time" + + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/chain" + "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/btcsuite/fastsha256" + "github.com/btcsuite/websocket" +) + +type websocketClient struct { + conn *websocket.Conn + authenticated bool + remoteAddr string + allRequests chan []byte + responses chan []byte + quit chan struct{} // closed on disconnect + wg sync.WaitGroup +} + +func newWebsocketClient(c *websocket.Conn, authenticated bool, remoteAddr string) *websocketClient { + return &websocketClient{ + conn: c, + authenticated: authenticated, + remoteAddr: remoteAddr, + allRequests: make(chan []byte), + responses: make(chan []byte), + quit: make(chan struct{}), + } +} + +func (c *websocketClient) send(b []byte) error { + select { + case c.responses <- b: + return nil + case <-c.quit: + return errors.New("websocket client disconnected") + } +} + +// Server holds the items the RPC server may need to access (auth, +// config, shutdown, etc.) +type Server struct { + httpServer http.Server + wallet *wallet.Wallet + walletLoader *wallet.Loader + chainClient *chain.RPCClient + handlerLookup func(string) (requestHandler, bool) + handlerMu sync.Mutex + + listeners []net.Listener + authsha [fastsha256.Size]byte + upgrader websocket.Upgrader + + maxPostClients int64 // Max concurrent HTTP POST clients. + maxWebsocketClients int64 // Max concurrent websocket clients. + + // Channels to register or unregister a websocket client for + // websocket notifications. + registerWSC chan *websocketClient + unregisterWSC chan *websocketClient + + // Channels read from other components from which notifications are + // created. + connectedBlocks <-chan wtxmgr.BlockMeta + disconnectedBlocks <-chan wtxmgr.BlockMeta + relevantTxs <-chan chain.RelevantTx + managerLocked <-chan bool + confirmedBalance <-chan btcutil.Amount + unconfirmedBalance <-chan btcutil.Amount + //chainServerConnected <-chan bool + registerWalletNtfns chan struct{} + + // enqueueNotification and dequeueNotification handle both sides of an + // infinitly growing queue for websocket client notifications. + enqueueNotification chan wsClientNotification + dequeueNotification chan wsClientNotification + + // notificationHandlerQuit is closed when the notification handler + // goroutine shuts down. After this is closed, no more notifications + // will be sent to any websocket client response channel. + notificationHandlerQuit chan struct{} + + wg sync.WaitGroup + quit chan struct{} + quitMtx sync.Mutex + + requestShutdownChan chan struct{} +} + +// NewServer creates a new server for serving legacy RPC client connections, +// both HTTP POST and websocket. +func NewServer(opts *Options, walletLoader *wallet.Loader, listeners []net.Listener) *Server { + serveMux := http.NewServeMux() + const rpcAuthTimeoutSeconds = 10 + + server := &Server{ + httpServer: http.Server{ + Handler: serveMux, + + // Timeout connections which don't complete the initial + // handshake within the allowed timeframe. + ReadTimeout: time.Second * rpcAuthTimeoutSeconds, + }, + walletLoader: walletLoader, + maxPostClients: opts.MaxPOSTClients, + maxWebsocketClients: opts.MaxWebsocketClients, + listeners: listeners, + // A hash of the HTTP basic auth string is used for a constant + // time comparison. + authsha: fastsha256.Sum256(httpBasicAuth(opts.Username, opts.Password)), + upgrader: websocket.Upgrader{ + // Allow all origins. + CheckOrigin: func(r *http.Request) bool { return true }, + }, + registerWSC: make(chan *websocketClient), + unregisterWSC: make(chan *websocketClient), + registerWalletNtfns: make(chan struct{}), + enqueueNotification: make(chan wsClientNotification), + dequeueNotification: make(chan wsClientNotification), + notificationHandlerQuit: make(chan struct{}), + quit: make(chan struct{}), + requestShutdownChan: make(chan struct{}, 1), + } + + serveMux.Handle("/", throttledFn(opts.MaxPOSTClients, + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Connection", "close") + w.Header().Set("Content-Type", "application/json") + r.Close = true + + if err := server.checkAuthHeader(r); err != nil { + log.Warnf("Unauthorized client connection attempt") + http.Error(w, "401 Unauthorized.", http.StatusUnauthorized) + return + } + server.wg.Add(1) + server.PostClientRPC(w, r) + server.wg.Done() + })) + + serveMux.Handle("/ws", throttledFn(opts.MaxWebsocketClients, + func(w http.ResponseWriter, r *http.Request) { + authenticated := false + switch server.checkAuthHeader(r) { + case nil: + authenticated = true + case ErrNoAuth: + // nothing + default: + // If auth was supplied but incorrect, rather than simply + // being missing, immediately terminate the connection. + log.Warnf("Disconnecting improperly authorized " + + "websocket client") + http.Error(w, "401 Unauthorized.", http.StatusUnauthorized) + return + } + + conn, err := server.upgrader.Upgrade(w, r, nil) + if err != nil { + log.Warnf("Cannot websocket upgrade client %s: %v", + r.RemoteAddr, err) + return + } + wsc := newWebsocketClient(conn, authenticated, r.RemoteAddr) + server.websocketClientRPC(wsc) + })) + + server.wg.Add(3) + go server.notificationListener() + go server.notificationQueue() + go server.notificationHandler() + + for _, lis := range listeners { + server.serve(lis) + } + + return server +} + +// httpBasicAuth returns the UTF-8 bytes of the HTTP Basic authentication +// string: +// +// "Basic " + base64(username + ":" + password) +func httpBasicAuth(username, password string) []byte { + const header = "Basic " + base64 := base64.StdEncoding + + b64InputLen := len(username) + len(":") + len(password) + b64Input := make([]byte, 0, b64InputLen) + b64Input = append(b64Input, username...) + b64Input = append(b64Input, ':') + b64Input = append(b64Input, password...) + + output := make([]byte, len(header)+base64.EncodedLen(b64InputLen)) + copy(output, header) + base64.Encode(output[len(header):], b64Input) + return output +} + +// serve serves HTTP POST and websocket RPC for the legacy JSON-RPC RPC server. +// This function does not block on lis.Accept. +func (s *Server) serve(lis net.Listener) { + s.wg.Add(1) + go func() { + log.Infof("Listening on %s", lis.Addr()) + err := s.httpServer.Serve(lis) + log.Tracef("Finished serving RPC: %v", err) + s.wg.Done() + }() +} + +// RegisterWallet associates the legacy RPC server with the wallet. This +// function must be called before any wallet RPCs can be called by clients. +func (s *Server) RegisterWallet(w *wallet.Wallet) { + s.handlerMu.Lock() + s.wallet = w + s.registerWalletNtfns <- struct{}{} + s.handlerMu.Unlock() +} + +// Stop gracefully shuts down the rpc server by stopping and disconnecting all +// clients, disconnecting the chain server connection, and closing the wallet's +// account files. This blocks until shutdown completes. +func (s *Server) Stop() { + s.quitMtx.Lock() + select { + case <-s.quit: + s.quitMtx.Unlock() + return + default: + } + + // Stop the connected wallet and chain server, if any. + s.handlerMu.Lock() + wallet := s.wallet + chainClient := s.chainClient + s.handlerMu.Unlock() + if wallet != nil { + wallet.Stop() + } + if chainClient != nil { + chainClient.Stop() + } + + // Stop all the listeners. + for _, listener := range s.listeners { + err := listener.Close() + if err != nil { + log.Errorf("Cannot close listener `%s`: %v", + listener.Addr(), err) + } + } + + // Signal the remaining goroutines to stop. + close(s.quit) + s.quitMtx.Unlock() + + // First wait for the wallet and chain server to stop, if they + // were ever set. + if wallet != nil { + wallet.WaitForShutdown() + } + if chainClient != nil { + chainClient.WaitForShutdown() + } + + // Wait for all remaining goroutines to exit. + s.wg.Wait() +} + +// SetChainServer sets the chain server client component needed to run a fully +// functional bitcoin wallet RPC server. This can be called to enable RPC +// passthrough even before a loaded wallet is set, but the wallet's RPC client +// is preferred. +func (s *Server) SetChainServer(chainClient *chain.RPCClient) { + s.handlerMu.Lock() + s.chainClient = chainClient + s.handlerMu.Unlock() +} + +// handlerClosure creates a closure function for handling requests of the given +// method. This may be a request that is handled directly by btcwallet, or +// a chain server request that is handled by passing the request down to btcd. +// +// NOTE: These handlers do not handle special cases, such as the authenticate +// method. Each of these must be checked beforehand (the method is already +// known) and handled accordingly. +func (s *Server) handlerClosure(request *btcjson.Request) lazyHandler { + s.handlerMu.Lock() + // With the lock held, make copies of these pointers for the closure. + wallet := s.wallet + chainClient := s.chainClient + if wallet != nil && chainClient == nil { + chainClient = wallet.ChainClient() + s.chainClient = chainClient + } + s.handlerMu.Unlock() + + return lazyApplyHandler(request, wallet, chainClient) +} + +// ErrNoAuth represents an error where authentication could not succeed +// due to a missing Authorization HTTP header. +var ErrNoAuth = errors.New("no auth") + +// checkAuthHeader checks the HTTP Basic authentication supplied by a client +// in the HTTP request r. It errors with ErrNoAuth if the request does not +// contain the Authorization header, or another non-nil error if the +// authentication was provided but incorrect. +// +// This check is time-constant. +func (s *Server) checkAuthHeader(r *http.Request) error { + authhdr := r.Header["Authorization"] + if len(authhdr) == 0 { + return ErrNoAuth + } + + authsha := fastsha256.Sum256([]byte(authhdr[0])) + cmp := subtle.ConstantTimeCompare(authsha[:], s.authsha[:]) + if cmp != 1 { + return errors.New("bad auth") + } + return nil +} + +// throttledFn wraps an http.HandlerFunc with throttling of concurrent active +// clients by responding with an HTTP 429 when the threshold is crossed. +func throttledFn(threshold int64, f http.HandlerFunc) http.Handler { + return throttled(threshold, f) +} + +// throttled wraps an http.Handler with throttling of concurrent active +// clients by responding with an HTTP 429 when the threshold is crossed. +func throttled(threshold int64, h http.Handler) http.Handler { + var active int64 + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + current := atomic.AddInt64(&active, 1) + defer atomic.AddInt64(&active, -1) + + if current-1 >= threshold { + log.Warnf("Reached threshold of %d concurrent active clients", threshold) + http.Error(w, "429 Too Many Requests", 429) + return + } + + h.ServeHTTP(w, r) + }) +} + +// sanitizeRequest returns a sanitized string for the request which may be +// safely logged. It is intended to strip private keys, passphrases, and any +// other secrets from request parameters before they may be saved to a log file. +func sanitizeRequest(r *btcjson.Request) string { + // These are considered unsafe to log, so sanitize parameters. + switch r.Method { + case "encryptwallet", "importprivkey", "importwallet", + "signrawtransaction", "walletpassphrase", + "walletpassphrasechange": + + return fmt.Sprintf(`{"id":%v,"method":"%s","params":SANITIZED %d parameters}`, + r.ID, r.Method, len(r.Params)) + } + + return fmt.Sprintf(`{"id":%v,"method":"%s","params":%v}`, r.ID, + r.Method, r.Params) +} + +// idPointer returns a pointer to the passed ID, or nil if the interface is nil. +// Interface pointers are usually a red flag of doing something incorrectly, +// but this is only implemented here to work around an oddity with btcjson, +// which uses empty interface pointers for response IDs. +func idPointer(id interface{}) (p *interface{}) { + if id != nil { + p = &id + } + return +} + +// invalidAuth checks whether a websocket request is a valid (parsable) +// authenticate request and checks the supplied username and passphrase +// against the server auth. +func (s *Server) invalidAuth(req *btcjson.Request) bool { + cmd, err := btcjson.UnmarshalCmd(req) + if err != nil { + return false + } + authCmd, ok := cmd.(*btcjson.AuthenticateCmd) + if !ok { + return false + } + // Check credentials. + login := authCmd.Username + ":" + authCmd.Passphrase + auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login)) + authSha := fastsha256.Sum256([]byte(auth)) + return subtle.ConstantTimeCompare(authSha[:], s.authsha[:]) != 1 +} + +func (s *Server) websocketClientRead(wsc *websocketClient) { + for { + _, request, err := wsc.conn.ReadMessage() + if err != nil { + if err != io.EOF && err != io.ErrUnexpectedEOF { + log.Warnf("Websocket receive failed from client %s: %v", + wsc.remoteAddr, err) + } + close(wsc.allRequests) + break + } + wsc.allRequests <- request + } +} + +func (s *Server) websocketClientRespond(wsc *websocketClient) { + // A for-select with a read of the quit channel is used instead of a + // for-range to provide clean shutdown. This is necessary due to + // WebsocketClientRead (which sends to the allRequests chan) not closing + // allRequests during shutdown if the remote websocket client is still + // connected. +out: + for { + select { + case reqBytes, ok := <-wsc.allRequests: + if !ok { + // client disconnected + break out + } + + var req btcjson.Request + err := json.Unmarshal(reqBytes, &req) + if err != nil { + if !wsc.authenticated { + // Disconnect immediately. + break out + } + resp := makeResponse(req.ID, nil, + btcjson.ErrRPCInvalidRequest) + mresp, err := json.Marshal(resp) + // We expect the marshal to succeed. If it + // doesn't, it indicates some non-marshalable + // type in the response. + if err != nil { + panic(err) + } + err = wsc.send(mresp) + if err != nil { + break out + } + continue + } + + if req.Method == "authenticate" { + if wsc.authenticated || s.invalidAuth(&req) { + // Disconnect immediately. + break out + } + wsc.authenticated = true + resp := makeResponse(req.ID, nil, nil) + // Expected to never fail. + mresp, err := json.Marshal(resp) + if err != nil { + panic(err) + } + err = wsc.send(mresp) + if err != nil { + break out + } + continue + } + + if !wsc.authenticated { + // Disconnect immediately. + break out + } + + switch req.Method { + case "stop": + resp := makeResponse(req.ID, + "btcwallet stopping.", nil) + mresp, err := json.Marshal(resp) + // Expected to never fail. + if err != nil { + panic(err) + } + err = wsc.send(mresp) + if err != nil { + break out + } + s.requestProcessShutdown() + break + + default: + req := req // Copy for the closure + f := s.handlerClosure(&req) + wsc.wg.Add(1) + go func() { + resp, jsonErr := f() + mresp, err := btcjson.MarshalResponse(req.ID, resp, jsonErr) + if err != nil { + log.Errorf("Unable to marshal response: %v", err) + } else { + _ = wsc.send(mresp) + } + wsc.wg.Done() + }() + } + + case <-s.quit: + break out + } + } + + // Remove websocket client from notification group, or if the server is + // shutting down, wait until the notification handler has finished + // running. This is needed to ensure that no more notifications will be + // sent to the client's responses chan before it's closed below. + select { + case s.unregisterWSC <- wsc: + case <-s.quit: + <-s.notificationHandlerQuit + } + + // allow client to disconnect after all handler goroutines are done + wsc.wg.Wait() + close(wsc.responses) + s.wg.Done() +} + +func (s *Server) websocketClientSend(wsc *websocketClient) { + const deadline time.Duration = 2 * time.Second +out: + for { + select { + case response, ok := <-wsc.responses: + if !ok { + // client disconnected + break out + } + err := wsc.conn.SetWriteDeadline(time.Now().Add(deadline)) + if err != nil { + log.Warnf("Cannot set write deadline on "+ + "client %s: %v", wsc.remoteAddr, err) + } + err = wsc.conn.WriteMessage(websocket.TextMessage, + response) + if err != nil { + log.Warnf("Failed websocket send to client "+ + "%s: %v", wsc.remoteAddr, err) + break out + } + + case <-s.quit: + break out + } + } + close(wsc.quit) + log.Infof("Disconnected websocket client %s", wsc.remoteAddr) + s.wg.Done() +} + +// websocketClientRPC starts the goroutines to serve JSON-RPC requests and +// notifications over a websocket connection for a single client. +func (s *Server) websocketClientRPC(wsc *websocketClient) { + log.Infof("New websocket client %s", wsc.remoteAddr) + + // Clear the read deadline set before the websocket hijacked + // the connection. + if err := wsc.conn.SetReadDeadline(time.Time{}); err != nil { + log.Warnf("Cannot remove read deadline: %v", err) + } + + // Add client context so notifications duplicated to each + // client are received by this client. + select { + case s.registerWSC <- wsc: + case <-s.quit: + return + } + + // WebsocketClientRead is intentionally not run with the waitgroup + // so it is ignored during shutdown. This is to prevent a hang during + // shutdown where the goroutine is blocked on a read of the + // websocket connection if the client is still connected. + go s.websocketClientRead(wsc) + + s.wg.Add(2) + go s.websocketClientRespond(wsc) + go s.websocketClientSend(wsc) + + <-wsc.quit +} + +// maxRequestSize specifies the maximum number of bytes in the request body +// that may be read from a client. This is currently limited to 4MB. +const maxRequestSize = 1024 * 1024 * 4 + +// PostClientRPC processes and replies to a JSON-RPC client request. +func (s *Server) PostClientRPC(w http.ResponseWriter, r *http.Request) { + body := http.MaxBytesReader(w, r.Body, maxRequestSize) + rpcRequest, err := ioutil.ReadAll(body) + if err != nil { + // TODO: what if the underlying reader errored? + http.Error(w, "413 Request Too Large.", + http.StatusRequestEntityTooLarge) + return + } + + // First check whether wallet has a handler for this request's method. + // If unfound, the request is sent to the chain server for further + // processing. While checking the methods, disallow authenticate + // requests, as they are invalid for HTTP POST clients. + var req btcjson.Request + err = json.Unmarshal(rpcRequest, &req) + if err != nil { + resp, err := btcjson.MarshalResponse(req.ID, nil, btcjson.ErrRPCInvalidRequest) + if err != nil { + log.Errorf("Unable to marshal response: %v", err) + http.Error(w, "500 Internal Server Error", + http.StatusInternalServerError) + return + } + _, err = w.Write(resp) + if err != nil { + log.Warnf("Cannot write invalid request request to "+ + "client: %v", err) + } + return + } + + // Create the response and error from the request. Two special cases + // are handled for the authenticate and stop request methods. + var res interface{} + var jsonErr *btcjson.RPCError + var stop bool + switch req.Method { + case "authenticate": + // Drop it. + return + case "stop": + stop = true + res = "btcwallet stopping" + default: + res, jsonErr = s.handlerClosure(&req)() + } + + // Marshal and send. + mresp, err := btcjson.MarshalResponse(req.ID, res, jsonErr) + if err != nil { + log.Errorf("Unable to marshal response: %v", err) + http.Error(w, "500 Internal Server Error", http.StatusInternalServerError) + return + } + _, err = w.Write(mresp) + if err != nil { + log.Warnf("Unable to respond to client: %v", err) + } + + if stop { + s.requestProcessShutdown() + } +} + +func (s *Server) requestProcessShutdown() { + select { + case s.requestShutdownChan <- struct{}{}: + default: + } +} + +// RequestProcessShutdown returns a channel that is sent to when an authorized +// client requests remote shutdown. +func (s *Server) RequestProcessShutdown() <-chan struct{} { + return s.requestShutdownChan +} + +// Notification messages for websocket clients. +type ( + wsClientNotification interface { + // This returns a slice only because some of these types result + // in multpile client notifications. + notificationCmds(w *wallet.Wallet) []interface{} + } + + blockConnected wtxmgr.BlockMeta + blockDisconnected wtxmgr.BlockMeta + + relevantTx chain.RelevantTx + + managerLocked bool + + confirmedBalance btcutil.Amount + unconfirmedBalance btcutil.Amount + + btcdConnected bool +) + +func (b blockConnected) notificationCmds(w *wallet.Wallet) []interface{} { + n := btcjson.NewBlockConnectedNtfn(b.Hash.String(), b.Height, b.Time.Unix()) + return []interface{}{n} +} + +func (b blockDisconnected) notificationCmds(w *wallet.Wallet) []interface{} { + n := btcjson.NewBlockDisconnectedNtfn(b.Hash.String(), b.Height, b.Time.Unix()) + return []interface{}{n} +} + +func (t relevantTx) notificationCmds(w *wallet.Wallet) []interface{} { + syncBlock := w.Manager.SyncedTo() + + var block *wtxmgr.Block + if t.Block != nil { + block = &t.Block.Block + } + details, err := w.TxStore.UniqueTxDetails(&t.TxRecord.Hash, block) + if err != nil { + log.Errorf("Cannot fetch transaction details for "+ + "client notification: %v", err) + return nil + } + if details == nil { + log.Errorf("No details found for client transaction notification") + return nil + } + + ltr := wallet.ListTransactions(details, syncBlock.Height, w.ChainParams()) + ntfns := make([]interface{}, len(ltr)) + for i := range ntfns { + ntfns[i] = btcjson.NewNewTxNtfn(ltr[i].Account, ltr[i]) + } + return ntfns +} + +func (l managerLocked) notificationCmds(w *wallet.Wallet) []interface{} { + n := btcjson.NewWalletLockStateNtfn(bool(l)) + return []interface{}{n} +} + +func (b confirmedBalance) notificationCmds(w *wallet.Wallet) []interface{} { + n := btcjson.NewAccountBalanceNtfn("", + btcutil.Amount(b).ToBTC(), true) + return []interface{}{n} +} + +func (b unconfirmedBalance) notificationCmds(w *wallet.Wallet) []interface{} { + n := btcjson.NewAccountBalanceNtfn("", + btcutil.Amount(b).ToBTC(), false) + return []interface{}{n} +} + +func (b btcdConnected) notificationCmds(w *wallet.Wallet) []interface{} { + n := btcjson.NewBtcdConnectedNtfn(bool(b)) + return []interface{}{n} +} + +func (s *Server) notificationListener() { +out: + for { + select { + case n := <-s.connectedBlocks: + s.enqueueNotification <- blockConnected(n) + case n := <-s.disconnectedBlocks: + s.enqueueNotification <- blockDisconnected(n) + case n := <-s.relevantTxs: + s.enqueueNotification <- relevantTx(n) + case n := <-s.managerLocked: + s.enqueueNotification <- managerLocked(n) + case n := <-s.confirmedBalance: + s.enqueueNotification <- confirmedBalance(n) + case n := <-s.unconfirmedBalance: + s.enqueueNotification <- unconfirmedBalance(n) + + // Registration of all notifications is done by the handler so + // it doesn't require another Server mutex. + case <-s.registerWalletNtfns: + connectedBlocks, err := s.wallet.ListenConnectedBlocks() + if err != nil { + log.Errorf("Could not register for new "+ + "connected block notifications: %v", + err) + continue + } + disconnectedBlocks, err := s.wallet.ListenDisconnectedBlocks() + if err != nil { + log.Errorf("Could not register for new "+ + "disconnected block notifications: %v", + err) + continue + } + relevantTxs, err := s.wallet.ListenRelevantTxs() + if err != nil { + log.Errorf("Could not register for new relevant "+ + "transaction notifications: %v", err) + continue + } + managerLocked, err := s.wallet.ListenLockStatus() + if err != nil { + log.Errorf("Could not register for manager "+ + "lock state changes: %v", err) + continue + } + confirmedBalance, err := s.wallet.ListenConfirmedBalance() + if err != nil { + log.Errorf("Could not register for confirmed "+ + "balance changes: %v", err) + continue + } + unconfirmedBalance, err := s.wallet.ListenUnconfirmedBalance() + if err != nil { + log.Errorf("Could not register for unconfirmed "+ + "balance changes: %v", err) + continue + } + s.connectedBlocks = connectedBlocks + s.disconnectedBlocks = disconnectedBlocks + s.relevantTxs = relevantTxs + s.managerLocked = managerLocked + s.confirmedBalance = confirmedBalance + s.unconfirmedBalance = unconfirmedBalance + + case <-s.quit: + break out + } + } + close(s.enqueueNotification) + go s.drainNotifications() + s.wg.Done() +} + +func (s *Server) drainNotifications() { + for { + select { + case <-s.connectedBlocks: + case <-s.disconnectedBlocks: + case <-s.relevantTxs: + case <-s.managerLocked: + case <-s.confirmedBalance: + case <-s.unconfirmedBalance: + case <-s.registerWalletNtfns: + } + } +} + +// notificationQueue manages an infinitly-growing queue of notifications that +// wallet websocket clients may be interested in. It quits when the +// enqueueNotification channel is closed, dropping any still pending +// notifications. +func (s *Server) notificationQueue() { + var q []wsClientNotification + var dequeue chan<- wsClientNotification + skipQueue := s.dequeueNotification + var next wsClientNotification +out: + for { + select { + case n, ok := <-s.enqueueNotification: + if !ok { + // Sender closed input channel. + break out + } + + // Either send to out immediately if skipQueue is + // non-nil (queue is empty) and reader is ready, + // or append to the queue and send later. + select { + case skipQueue <- n: + default: + q = append(q, n) + dequeue = s.dequeueNotification + skipQueue = nil + next = q[0] + } + + case dequeue <- next: + q[0] = nil // avoid leak + q = q[1:] + if len(q) == 0 { + dequeue = nil + skipQueue = s.dequeueNotification + } else { + next = q[0] + } + } + } + close(s.dequeueNotification) + s.wg.Done() +} + +func (s *Server) notificationHandler() { + clients := make(map[chan struct{}]*websocketClient) +out: + for { + select { + case c := <-s.registerWSC: + clients[c.quit] = c + + case c := <-s.unregisterWSC: + delete(clients, c.quit) + + case nmsg, ok := <-s.dequeueNotification: + // No more notifications. + if !ok { + break out + } + + // Ignore if there are no clients to receive the + // notification. + if len(clients) == 0 { + continue + } + + ns := nmsg.notificationCmds(s.wallet) + for _, n := range ns { + mn, err := btcjson.MarshalCmd(nil, n) + // All notifications are expected to be + // marshalable. + if err != nil { + panic(err) + } + for _, c := range clients { + if err := c.send(mn); err != nil { + delete(clients, c.quit) + } + } + } + + case <-s.quit: + break out + } + } + close(s.notificationHandlerQuit) + s.wg.Done() +} diff --git a/rpc/regen.sh b/rpc/regen.sh new file mode 100644 index 0000000..79c6169 --- /dev/null +++ b/rpc/regen.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +protoc -I. api.proto --go_out=plugins=grpc:walletrpc diff --git a/rpc/rpcserver/log.go b/rpc/rpcserver/log.go new file mode 100644 index 0000000..d25317a --- /dev/null +++ b/rpc/rpcserver/log.go @@ -0,0 +1,81 @@ +// Copyright (c) 2015-2016 The btcsuite developers +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package rpcserver + +import ( + "os" + "strings" + + "google.golang.org/grpc/grpclog" + + "github.com/btcsuite/btclog" +) + +// UseLogger sets the logger to use for the gRPC server. +func UseLogger(l btclog.Logger) { + grpclog.SetLogger(logger{l}) +} + +// logger uses a btclog.Logger to implement the grpclog.Logger interface. +type logger struct { + btclog.Logger +} + +// stripGrpcPrefix removes the package prefix for all logs made to the grpc +// logger, since these are already included as the btclog subsystem name. +func stripGrpcPrefix(logstr string) string { + return strings.TrimPrefix(logstr, "grpc: ") +} + +// stripGrpcPrefixArgs removes the package prefix from the first argument, if it +// exists and is a string, returning the same arg slice after reassigning the +// first arg. +func stripGrpcPrefixArgs(args ...interface{}) []interface{} { + if len(args) == 0 { + return args + } + firstArgStr, ok := args[0].(string) + if ok { + args[0] = stripGrpcPrefix(firstArgStr) + } + return args +} + +func (l logger) Fatal(args ...interface{}) { + l.Critical(stripGrpcPrefixArgs(args)...) + os.Exit(1) +} + +func (l logger) Fatalf(format string, args ...interface{}) { + l.Criticalf(stripGrpcPrefix(format), args...) + os.Exit(1) +} + +func (l logger) Fatalln(args ...interface{}) { + l.Critical(stripGrpcPrefixArgs(args)...) + os.Exit(1) +} + +func (l logger) Print(args ...interface{}) { + l.Info(stripGrpcPrefixArgs(args)...) +} + +func (l logger) Printf(format string, args ...interface{}) { + l.Infof(stripGrpcPrefix(format), args...) +} + +func (l logger) Println(args ...interface{}) { + l.Info(stripGrpcPrefixArgs(args)...) +} diff --git a/rpc/rpcserver/server.go b/rpc/rpcserver/server.go new file mode 100644 index 0000000..51072af --- /dev/null +++ b/rpc/rpcserver/server.go @@ -0,0 +1,845 @@ +// Copyright (c) 2015-2016 The btcsuite developers +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +// This package implements the RPC API and is used by the main package to start +// gRPC services. +// +// Full documentation of the API implemented by this package is maintained in a +// language-agnostic document: +// +// https://github.com/btcsuite/btcwallet/blob/master/rpc/documentation/api.md +// +// Any API changes must be performed according to the steps listed here: +// +// https://github.com/btcsuite/btcwallet/blob/master/rpc/documentation/serverchanges.md +package rpcserver + +import ( + "bytes" + "errors" + "sync" + "time" + + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + + "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcrpcclient" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/btcsuite/btcwallet/chain" + "github.com/btcsuite/btcwallet/internal/cfgutil" + "github.com/btcsuite/btcwallet/internal/zero" + "github.com/btcsuite/btcwallet/netparams" + pb "github.com/btcsuite/btcwallet/rpc/walletrpc" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/walletdb" +) + +// translateError creates a new gRPC error with an appropiate error code for +// recognized errors. +// +// This function is by no means complete and should be expanded based on other +// known errors. Any RPC handler not returning a gRPC error (with grpc.Errorf) +// should return this result instead. +func translateError(err error) error { + code := errorCode(err) + return grpc.Errorf(code, "%s", err.Error()) +} + +func errorCode(err error) codes.Code { + // waddrmgr.IsError is convenient, but not granular enough when the + // underlying error has to be checked. Unwrap the underlying error + // if it exists. + if e, ok := err.(waddrmgr.ManagerError); ok { + // For these waddrmgr error codes, the underlying error isn't + // needed to determine the grpc error code. + switch e.ErrorCode { + case waddrmgr.ErrWrongPassphrase: // public and private + return codes.InvalidArgument + case waddrmgr.ErrAccountNotFound: + return codes.NotFound + case waddrmgr.ErrInvalidAccount: // reserved account + return codes.InvalidArgument + case waddrmgr.ErrDuplicateAccount: + return codes.AlreadyExists + } + + err = e.Err + } + + switch err { + case wallet.ErrLoaded: + return codes.FailedPrecondition + case walletdb.ErrDbNotOpen: + return codes.Aborted + case walletdb.ErrDbExists: + return codes.AlreadyExists + case walletdb.ErrDbDoesNotExist: + return codes.NotFound + case hdkeychain.ErrInvalidSeedLen: + return codes.InvalidArgument + default: + return codes.Unknown + } +} + +// walletServer provides wallet services for RPC clients. +type walletServer struct { + wallet *wallet.Wallet +} + +// loaderServer provides RPC clients with the ability to load and close wallets, +// as well as establishing a RPC connection to a btcd consensus server. +type loaderServer struct { + loader *wallet.Loader + activeNet *netparams.Params + rpcClient *chain.RPCClient + mu sync.Mutex +} + +// StartWalletService creates a implementation of the WalletService and +// registers it with the gRPC server. +func StartWalletService(server *grpc.Server, wallet *wallet.Wallet) { + service := &walletServer{wallet} + pb.RegisterWalletServiceServer(server, service) +} + +func (s *walletServer) Ping(ctx context.Context, req *pb.PingRequest) (*pb.PingResponse, error) { + return &pb.PingResponse{}, nil +} + +func (s *walletServer) Network(ctx context.Context, req *pb.NetworkRequest) ( + *pb.NetworkResponse, error) { + + return &pb.NetworkResponse{ActiveNetwork: uint32(s.wallet.ChainParams().Net)}, nil +} + +func (s *walletServer) AccountNumber(ctx context.Context, req *pb.AccountNumberRequest) ( + *pb.AccountNumberResponse, error) { + + accountNum, err := s.wallet.Manager.LookupAccount(req.AccountName) + if err != nil { + return nil, translateError(err) + } + + return &pb.AccountNumberResponse{AccountNumber: accountNum}, nil +} + +func (s *walletServer) Accounts(ctx context.Context, req *pb.AccountsRequest) ( + *pb.AccountsResponse, error) { + + resp, err := s.wallet.Accounts() + if err != nil { + return nil, translateError(err) + } + accounts := make([]*pb.AccountsResponse_Account, len(resp.Accounts)) + for i := range resp.Accounts { + a := &resp.Accounts[i] + accounts[i] = &pb.AccountsResponse_Account{ + AccountNumber: a.AccountNumber, + AccountName: a.AccountName, + TotalBalance: int64(a.TotalBalance), + ExternalKeyCount: a.ExternalKeyCount, + InternalKeyCount: a.InternalKeyCount, + ImportedKeyCount: a.ImportedKeyCount, + } + } + return &pb.AccountsResponse{ + Accounts: accounts, + CurrentBlockHash: resp.CurrentBlockHash[:], + CurrentBlockHeight: resp.CurrentBlockHeight, + }, nil +} + +func (s *walletServer) RenameAccount(ctx context.Context, req *pb.RenameAccountRequest) ( + *pb.RenameAccountResponse, error) { + + err := s.wallet.RenameAccount(req.AccountNumber, req.NewName) + if err != nil { + return nil, translateError(err) + } + + return &pb.RenameAccountResponse{}, nil +} + +func (s *walletServer) NextAccount(ctx context.Context, req *pb.NextAccountRequest) ( + *pb.NextAccountResponse, error) { + + defer zero.Bytes(req.Passphrase) + + if req.AccountName == "" { + return nil, grpc.Errorf(codes.InvalidArgument, "account name may not be empty") + } + + lock := make(chan time.Time, 1) + defer func() { + lock <- time.Time{} // send matters, not the value + }() + err := s.wallet.Unlock(req.Passphrase, lock) + if err != nil { + return nil, translateError(err) + } + + account, err := s.wallet.NextAccount(req.AccountName) + if err != nil { + return nil, translateError(err) + } + + return &pb.NextAccountResponse{AccountNumber: account}, nil +} + +func (s *walletServer) NextAddress(ctx context.Context, req *pb.NextAddressRequest) ( + *pb.NextAddressResponse, error) { + + addr, err := s.wallet.NewAddress(req.Account) + if err != nil { + return nil, translateError(err) + } + + return &pb.NextAddressResponse{Address: addr.EncodeAddress()}, nil +} + +func (s *walletServer) ImportPrivateKey(ctx context.Context, req *pb.ImportPrivateKeyRequest) ( + *pb.ImportPrivateKeyResponse, error) { + + defer zero.Bytes(req.Passphrase) + + wif, err := btcutil.DecodeWIF(req.PrivateKeyWif) + if err != nil { + return nil, grpc.Errorf(codes.InvalidArgument, + "Invalid WIF-encoded private key: %v", err) + } + + lock := make(chan time.Time, 1) + defer func() { + lock <- time.Time{} // send matters, not the value + }() + err = s.wallet.Unlock(req.Passphrase, lock) + if err != nil { + return nil, translateError(err) + } + + // At the moment, only the special-cased import account can be used to + // import keys. + if req.Account != waddrmgr.ImportedAddrAccount { + return nil, grpc.Errorf(codes.InvalidArgument, + "Only the imported account accepts private key imports") + } + + _, err = s.wallet.ImportPrivateKey(wif, nil, req.Rescan) + if err != nil { + return nil, translateError(err) + } + + return &pb.ImportPrivateKeyResponse{}, nil +} + +func (s *walletServer) Balance(ctx context.Context, req *pb.BalanceRequest) ( + *pb.BalanceResponse, error) { + + account := req.AccountNumber + reqConfs := req.RequiredConfirmations + bals, err := s.wallet.CalculateAccountBalances(account, reqConfs) + if err != nil { + return nil, translateError(err) + } + + // TODO: Spendable currently includes multisig outputs that may not + // actually be spendable without additional keys. + resp := &pb.BalanceResponse{ + Total: int64(bals.Total), + Spendable: int64(bals.Spendable), + ImmatureReward: int64(bals.ImmatureReward), + } + return resp, nil +} + +// confirmed checks whether a transaction at height txHeight has met minconf +// confirmations for a blockchain at height curHeight. +func confirmed(minconf, txHeight, curHeight int32) bool { + return confirms(txHeight, curHeight) >= minconf +} + +// confirms returns the number of confirmations for a transaction in a block at +// height txHeight (or -1 for an unconfirmed tx) given the chain height +// curHeight. +func confirms(txHeight, curHeight int32) int32 { + switch { + case txHeight == -1, txHeight > curHeight: + return 0 + default: + return curHeight - txHeight + 1 + } +} + +func (s *walletServer) FundTransaction(ctx context.Context, req *pb.FundTransactionRequest) ( + *pb.FundTransactionResponse, error) { + + // TODO: A predicate function for selecting outputs should be created + // and passed to a database view of just a particular account's utxos to + // prevent reading every unspent transaction output from every account + // into memory at once. + + syncBlock := s.wallet.Manager.SyncedTo() + + outputs, err := s.wallet.TxStore.UnspentOutputs() + if err != nil { + return nil, translateError(err) + } + + selectedOutputs := make([]*pb.FundTransactionResponse_PreviousOutput, 0, len(outputs)) + var totalAmount btcutil.Amount + for i := range outputs { + output := &outputs[i] + + if !confirmed(req.RequiredConfirmations, output.Height, syncBlock.Height) { + continue + } + if !req.IncludeImmatureCoinbases && output.FromCoinBase && + !confirmed(blockchain.CoinbaseMaturity, output.Height, syncBlock.Height) { + continue + } + + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + output.PkScript, s.wallet.ChainParams()) + if err != nil || len(addrs) == 0 { + // Cannot determine which account this belongs to + // without a valid address. Fix this by saving + // outputs per account (per-account wtxmgr). + continue + } + outputAcct, err := s.wallet.Manager.AddrAccount(addrs[0]) + if err != nil { + return nil, translateError(err) + } + if outputAcct != req.Account { + continue + } + + selectedOutputs = append(selectedOutputs, &pb.FundTransactionResponse_PreviousOutput{ + TransactionHash: output.OutPoint.Hash[:], + OutputIndex: output.Index, + Amount: int64(output.Amount), + PkScript: output.PkScript, + ReceiveTime: output.Received.Unix(), + FromCoinbase: output.FromCoinBase, + }) + totalAmount += output.Amount + + if req.TargetAmount != 0 && totalAmount > btcutil.Amount(req.TargetAmount) { + break + } + + } + + if req.TargetAmount != 0 && totalAmount < btcutil.Amount(req.TargetAmount) { + return nil, errors.New("insufficient output value to reach target") + } + + var changeScript []byte + if req.IncludeChangeScript && totalAmount > btcutil.Amount(req.TargetAmount) { + changeAddr, err := s.wallet.NewChangeAddress(req.Account) + if err != nil { + return nil, translateError(err) + } + changeScript, err = txscript.PayToAddrScript(changeAddr) + if err != nil { + return nil, translateError(err) + } + } + + return &pb.FundTransactionResponse{ + SelectedOutputs: selectedOutputs, + TotalAmount: int64(totalAmount), + ChangePkScript: changeScript, + }, nil +} + +func marshalGetTransactionsResult(wresp *wallet.GetTransactionsResult) ( + *pb.GetTransactionsResponse, error) { + + resp := &pb.GetTransactionsResponse{ + MinedTransactions: marshalBlocks(wresp.MinedTransactions), + UnminedTransactions: marshalTransactionDetails(wresp.UnminedTransactions), + } + return resp, nil +} + +// BUGS: +// - MinimumRecentTransactions is ignored. +// - Wrong error codes when a block height or hash is not recognized +func (s *walletServer) GetTransactions(ctx context.Context, req *pb.GetTransactionsRequest) ( + resp *pb.GetTransactionsResponse, err error) { + + var startBlock, endBlock *wallet.BlockIdentifier + if req.StartingBlockHash != nil && req.StartingBlockHeight != 0 { + return nil, errors.New( + "starting block hash and height may not be specified simultaneously") + } else if req.StartingBlockHash != nil { + startBlockHash, err := wire.NewShaHash(req.StartingBlockHash) + if err != nil { + return nil, grpc.Errorf(codes.InvalidArgument, "%s", err.Error()) + } + startBlock = wallet.NewBlockIdentifierFromHash(startBlockHash) + } else if req.StartingBlockHeight != 0 { + startBlock = wallet.NewBlockIdentifierFromHeight(req.StartingBlockHeight) + } + + if req.EndingBlockHash != nil && req.EndingBlockHeight != 0 { + return nil, grpc.Errorf(codes.InvalidArgument, + "ending block hash and height may not be specified simultaneously") + } else if req.EndingBlockHash != nil { + endBlockHash, err := wire.NewShaHash(req.EndingBlockHash) + if err != nil { + return nil, grpc.Errorf(codes.InvalidArgument, "%s", err.Error()) + } + endBlock = wallet.NewBlockIdentifierFromHash(endBlockHash) + } else if req.EndingBlockHeight != 0 { + endBlock = wallet.NewBlockIdentifierFromHeight(req.EndingBlockHeight) + } + + var minRecentTxs int + if req.MinimumRecentTransactions != 0 { + if endBlock != nil { + return nil, grpc.Errorf(codes.InvalidArgument, + "ending block and minimum number of recent transactions "+ + "may not be specified simultaneously") + } + minRecentTxs = int(req.MinimumRecentTransactions) + if minRecentTxs < 0 { + return nil, grpc.Errorf(codes.InvalidArgument, + "minimum number of recent transactions may not be negative") + } + } + + _ = minRecentTxs + + gtr, err := s.wallet.GetTransactions(startBlock, endBlock, ctx.Done()) + if err != nil { + return nil, translateError(err) + } + return marshalGetTransactionsResult(gtr) +} + +func (s *walletServer) ChangePassphrase(ctx context.Context, req *pb.ChangePassphraseRequest) ( + *pb.ChangePassphraseResponse, error) { + + defer func() { + zero.Bytes(req.OldPassphrase) + zero.Bytes(req.NewPassphrase) + }() + + err := s.wallet.Manager.ChangePassphrase(req.OldPassphrase, req.NewPassphrase, + req.Key != pb.ChangePassphraseRequest_PUBLIC, &waddrmgr.DefaultScryptOptions) + if err != nil { + return nil, translateError(err) + } + + return &pb.ChangePassphraseResponse{}, nil +} + +// BUGS: +// - InputIndexes request field is ignored. +func (s *walletServer) SignTransaction(ctx context.Context, req *pb.SignTransactionRequest) ( + *pb.SignTransactionResponse, error) { + + defer zero.Bytes(req.Passphrase) + + var tx wire.MsgTx + err := tx.Deserialize(bytes.NewReader(req.SerializedTransaction)) + if err != nil { + return nil, grpc.Errorf(codes.InvalidArgument, + "Bytes do not represent a valid raw transaction: %v", err) + } + + lock := make(chan time.Time, 1) + defer func() { + lock <- time.Time{} // send matters, not the value + }() + err = s.wallet.Unlock(req.Passphrase, lock) + if err != nil { + return nil, translateError(err) + } + + invalidSigs, err := s.wallet.SignTransaction(&tx, txscript.SigHashAll, nil, nil, nil) + if err != nil { + return nil, translateError(err) + } + + invalidInputIndexes := make([]uint32, len(invalidSigs)) + for i, e := range invalidSigs { + invalidInputIndexes[i] = e.InputIndex + } + + var serializedTransaction bytes.Buffer + serializedTransaction.Grow(tx.SerializeSize()) + err = tx.Serialize(&serializedTransaction) + if err != nil { + return nil, translateError(err) + } + + resp := &pb.SignTransactionResponse{ + Transaction: serializedTransaction.Bytes(), + UnsignedInputIndexes: invalidInputIndexes, + } + return resp, nil +} + +// BUGS: +// - The transaction is not inspected to be relevant before publishing using +// sendrawtransaction, so connection errors to btcd could result in the tx +// never being added to the wallet database. +// - Once the above bug is fixed, wallet will require a way to purge invalid +// transactions from the database when they are rejected by the network, other +// than double spending them. +func (s *walletServer) PublishTransaction(ctx context.Context, req *pb.PublishTransactionRequest) ( + *pb.PublishTransactionResponse, error) { + + var msgTx wire.MsgTx + err := msgTx.Deserialize(bytes.NewReader(req.SignedTransaction)) + if err != nil { + return nil, grpc.Errorf(codes.InvalidArgument, + "Bytes do not represent a valid raw transaction: %v", err) + } + + err = s.wallet.PublishTransaction(&msgTx) + if err != nil { + return nil, translateError(err) + } + + return &pb.PublishTransactionResponse{}, nil +} + +func marshalTransactionInputs(v []wallet.TransactionSummaryInput) []*pb.TransactionDetails_Input { + inputs := make([]*pb.TransactionDetails_Input, len(v)) + for i := range v { + input := &v[i] + inputs[i] = &pb.TransactionDetails_Input{ + Index: input.Index, + PreviousAccount: input.PreviousAccount, + PreviousAmount: int64(input.PreviousAmount), + } + } + return inputs +} + +func marshalTransactionOutputs(v []wallet.TransactionSummaryOutput) []*pb.TransactionDetails_Output { + outputs := make([]*pb.TransactionDetails_Output, len(v)) + for i := range v { + output := &v[i] + + var addresses []string + if len(output.Addresses) != 0 { + addresses = make([]string, 0, len(output.Addresses)) + for _, a := range output.Addresses { + addresses = append(addresses, a.EncodeAddress()) + } + } + + outputs[i] = &pb.TransactionDetails_Output{ + Mine: output.Mine, + Account: output.Account, + Internal: output.Internal, + Addresses: addresses, + } + } + return outputs +} + +func marshalTransactionDetails(v []wallet.TransactionSummary) []*pb.TransactionDetails { + txs := make([]*pb.TransactionDetails, len(v)) + for i := range v { + tx := &v[i] + txs[i] = &pb.TransactionDetails{ + Hash: tx.Hash[:], + Transaction: tx.Transaction, + Debits: marshalTransactionInputs(tx.MyInputs), + Outputs: marshalTransactionOutputs(tx.MyOutputs), + Fee: int64(tx.Fee), + Timestamp: tx.Timestamp, + } + } + return txs +} + +func marshalBlocks(v []wallet.Block) []*pb.BlockDetails { + blocks := make([]*pb.BlockDetails, len(v)) + for i := range v { + block := &v[i] + blocks[i] = &pb.BlockDetails{ + Hash: block.Hash[:], + Height: block.Height, + Timestamp: block.Timestamp, + Transactions: marshalTransactionDetails(block.Transactions), + } + } + return blocks +} + +func marshalHashes(v []*wire.ShaHash) [][]byte { + hashes := make([][]byte, len(v)) + for i, hash := range v { + hashes[i] = hash[:] + } + return hashes +} + +func marshalAccountBalances(v []wallet.AccountBalance) []*pb.AccountBalance { + balances := make([]*pb.AccountBalance, len(v)) + for i := range v { + balance := &v[i] + balances[i] = &pb.AccountBalance{ + Account: balance.Account, + TotalBalance: int64(balance.TotalBalance), + } + } + return balances +} + +func (s *walletServer) TransactionNotifications(req *pb.TransactionNotificationsRequest, + svr pb.WalletService_TransactionNotificationsServer) error { + + n := s.wallet.NtfnServer.TransactionNotifications() + defer n.Done() + + ctxDone := svr.Context().Done() + for { + select { + case v := <-n.C: + resp := pb.TransactionNotificationsResponse{ + AttachedBlocks: marshalBlocks(v.AttachedBlocks), + DetachedBlocks: marshalHashes(v.DetachedBlocks), + UnminedTransactions: marshalTransactionDetails(v.UnminedTransactions), + UnminedTransactionHashes: marshalHashes(v.UnminedTransactionHashes), + } + err := svr.Send(&resp) + if err != nil { + return translateError(err) + } + + case <-ctxDone: + return nil + } + } +} + +func (s *walletServer) SpentnessNotifications(req *pb.SpentnessNotificationsRequest, + svr pb.WalletService_SpentnessNotificationsServer) error { + + if req.NoNotifyUnspent && req.NoNotifySpent { + return grpc.Errorf(codes.InvalidArgument, + "no_notify_unspent and no_notify_spent may not both be true") + } + + n := s.wallet.NtfnServer.AccountSpentnessNotifications(req.Account) + defer n.Done() + + ctxDone := svr.Context().Done() + for { + select { + case v := <-n.C: + spenderHash, spenderIndex, spent := v.Spender() + if (spent && req.NoNotifySpent) || (!spent && req.NoNotifyUnspent) { + continue + } + index := v.Index() + resp := pb.SpentnessNotificationsResponse{ + TransactionHash: v.Hash()[:], + OutputIndex: index, + } + if spent { + resp.Spender = &pb.SpentnessNotificationsResponse_Spender{ + TransactionHash: spenderHash[:], + InputIndex: spenderIndex, + } + } + err := svr.Send(&resp) + if err != nil { + return translateError(err) + } + + case <-ctxDone: + return nil + } + } +} + +func (s *walletServer) AccountNotifications(req *pb.AccountNotificationsRequest, + svr pb.WalletService_AccountNotificationsServer) error { + + n := s.wallet.NtfnServer.AccountNotifications() + defer n.Done() + + ctxDone := svr.Context().Done() + for { + select { + case v := <-n.C: + resp := pb.AccountNotificationsResponse{ + AccountNumber: v.AccountNumber, + AccountName: v.AccountName, + ExternalKeyCount: v.ExternalKeyCount, + InternalKeyCount: v.InternalKeyCount, + ImportedKeyCount: v.ImportedKeyCount, + } + err := svr.Send(&resp) + if err != nil { + return translateError(err) + } + + case <-ctxDone: + return nil + } + } +} + +// StartWalletLoaderService creates a implementation of the WalletLoaderService +// and registers it with the gRPC server. +func StartWalletLoaderService(server *grpc.Server, loader *wallet.Loader, + activeNet *netparams.Params) { + + service := &loaderServer{loader: loader, activeNet: activeNet} + pb.RegisterWalletLoaderServiceServer(server, service) +} + +func (s *loaderServer) CreateWallet(ctx context.Context, req *pb.CreateWalletRequest) ( + *pb.CreateWalletResponse, error) { + + defer func() { + zero.Bytes(req.PrivatePassphrase) + zero.Bytes(req.Seed) + }() + + // Use an insecure public passphrase when the request's is empty. + pubPassphrase := req.PublicPassphrase + if len(pubPassphrase) == 0 { + pubPassphrase = []byte(wallet.InsecurePubPassphrase) + } + + wallet, err := s.loader.CreateNewWallet(pubPassphrase, req.PrivatePassphrase, req.Seed) + if err != nil { + return nil, translateError(err) + } + + s.mu.Lock() + if s.rpcClient != nil { + wallet.SynchronizeRPC(s.rpcClient) + } + s.mu.Unlock() + + return &pb.CreateWalletResponse{}, nil +} + +func (s *loaderServer) OpenWallet(ctx context.Context, req *pb.OpenWalletRequest) ( + *pb.OpenWalletResponse, error) { + + // Use an insecure public passphrase when the request's is empty. + pubPassphrase := req.PublicPassphrase + if len(pubPassphrase) == 0 { + pubPassphrase = []byte(wallet.InsecurePubPassphrase) + } + + wallet, err := s.loader.OpenExistingWallet(pubPassphrase, false) + if err != nil { + return nil, translateError(err) + } + + s.mu.Lock() + if s.rpcClient != nil { + wallet.SynchronizeRPC(s.rpcClient) + } + s.mu.Unlock() + + return &pb.OpenWalletResponse{}, nil +} + +func (s *loaderServer) WalletExists(ctx context.Context, req *pb.WalletExistsRequest) ( + *pb.WalletExistsResponse, error) { + + exists, err := s.loader.WalletExists() + if err != nil { + return nil, translateError(err) + } + return &pb.WalletExistsResponse{Exists: exists}, nil +} + +func (s *loaderServer) CloseWallet(ctx context.Context, req *pb.CloseWalletRequest) ( + *pb.CloseWalletResponse, error) { + + loadedWallet, ok := s.loader.LoadedWallet() + if !ok { + return nil, grpc.Errorf(codes.FailedPrecondition, "wallet is not loaded") + } + + loadedWallet.Stop() + loadedWallet.WaitForShutdown() + + return &pb.CloseWalletResponse{}, nil +} + +func (s *loaderServer) StartBtcdRpc(ctx context.Context, req *pb.StartBtcdRpcRequest) ( + *pb.StartBtcdRpcResponse, error) { + + defer zero.Bytes(req.Password) + + defer s.mu.Unlock() + s.mu.Lock() + + if s.rpcClient != nil { + return nil, grpc.Errorf(codes.FailedPrecondition, "RPC client already created") + } + + networkAddress, err := cfgutil.NormalizeAddress(req.NetworkAddress, + s.activeNet.RPCClientPort) + if err != nil { + return nil, grpc.Errorf(codes.InvalidArgument, + "Network address is ill-formed: %v", err) + } + + // Error if the wallet is already syncing with the network. + wallet, walletLoaded := s.loader.LoadedWallet() + if walletLoaded && wallet.SynchronizingToNetwork() { + return nil, grpc.Errorf(codes.FailedPrecondition, + "wallet is loaded and already synchronizing") + } + + rpcClient, err := chain.NewRPCClient(s.activeNet.Params, networkAddress, req.Username, + string(req.Password), req.Certificate, len(req.Certificate) == 0, 1) + if err != nil { + return nil, translateError(err) + } + + err = rpcClient.Start() + if err != nil { + if err == btcrpcclient.ErrInvalidAuth { + return nil, grpc.Errorf(codes.InvalidArgument, + "Invalid RPC credentials: %v", err) + } else { + return nil, grpc.Errorf(codes.NotFound, + "Connection to RPC server failed: %v", err) + } + } + + s.rpcClient = rpcClient + + if walletLoaded { + wallet.SynchronizeRPC(rpcClient) + } + + return &pb.StartBtcdRpcResponse{}, nil +} diff --git a/rpc/walletrpc/api.pb.go b/rpc/walletrpc/api.pb.go new file mode 100644 index 0000000..4ca3700 --- /dev/null +++ b/rpc/walletrpc/api.pb.go @@ -0,0 +1,1437 @@ +// Code generated by protoc-gen-go. +// source: api.proto +// DO NOT EDIT! + +/* +Package walletrpc is a generated protocol buffer package. + +It is generated from these files: + api.proto + +It has these top-level messages: + TransactionDetails + BlockDetails + AccountBalance + PingRequest + PingResponse + NetworkRequest + NetworkResponse + AccountNumberRequest + AccountNumberResponse + AccountsRequest + AccountsResponse + RenameAccountRequest + RenameAccountResponse + NextAccountRequest + NextAccountResponse + NextAddressRequest + NextAddressResponse + ImportPrivateKeyRequest + ImportPrivateKeyResponse + BalanceRequest + BalanceResponse + GetTransactionsRequest + GetTransactionsResponse + ChangePassphraseRequest + ChangePassphraseResponse + FundTransactionRequest + FundTransactionResponse + SignTransactionRequest + SignTransactionResponse + PublishTransactionRequest + PublishTransactionResponse + TransactionNotificationsRequest + TransactionNotificationsResponse + SpentnessNotificationsRequest + SpentnessNotificationsResponse + AccountNotificationsRequest + AccountNotificationsResponse + CreateWalletRequest + CreateWalletResponse + OpenWalletRequest + OpenWalletResponse + CloseWalletRequest + CloseWalletResponse + WalletExistsRequest + WalletExistsResponse + StartBtcdRpcRequest + StartBtcdRpcResponse +*/ +package walletrpc + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +import ( + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +type ChangePassphraseRequest_Key int32 + +const ( + ChangePassphraseRequest_PRIVATE ChangePassphraseRequest_Key = 0 + ChangePassphraseRequest_PUBLIC ChangePassphraseRequest_Key = 1 +) + +var ChangePassphraseRequest_Key_name = map[int32]string{ + 0: "PRIVATE", + 1: "PUBLIC", +} +var ChangePassphraseRequest_Key_value = map[string]int32{ + "PRIVATE": 0, + "PUBLIC": 1, +} + +func (x ChangePassphraseRequest_Key) String() string { + return proto.EnumName(ChangePassphraseRequest_Key_name, int32(x)) +} + +type TransactionDetails struct { + Hash []byte `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"` + Transaction []byte `protobuf:"bytes,2,opt,name=transaction,proto3" json:"transaction,omitempty"` + Debits []*TransactionDetails_Input `protobuf:"bytes,3,rep,name=debits" json:"debits,omitempty"` + Outputs []*TransactionDetails_Output `protobuf:"bytes,4,rep,name=outputs" json:"outputs,omitempty"` + Fee int64 `protobuf:"varint,5,opt,name=fee" json:"fee,omitempty"` + Timestamp int64 `protobuf:"varint,6,opt,name=timestamp" json:"timestamp,omitempty"` +} + +func (m *TransactionDetails) Reset() { *m = TransactionDetails{} } +func (m *TransactionDetails) String() string { return proto.CompactTextString(m) } +func (*TransactionDetails) ProtoMessage() {} + +func (m *TransactionDetails) GetDebits() []*TransactionDetails_Input { + if m != nil { + return m.Debits + } + return nil +} + +func (m *TransactionDetails) GetOutputs() []*TransactionDetails_Output { + if m != nil { + return m.Outputs + } + return nil +} + +type TransactionDetails_Input struct { + Index uint32 `protobuf:"varint,1,opt,name=index" json:"index,omitempty"` + PreviousAccount uint32 `protobuf:"varint,2,opt,name=previous_account" json:"previous_account,omitempty"` + PreviousAmount int64 `protobuf:"varint,3,opt,name=previous_amount" json:"previous_amount,omitempty"` +} + +func (m *TransactionDetails_Input) Reset() { *m = TransactionDetails_Input{} } +func (m *TransactionDetails_Input) String() string { return proto.CompactTextString(m) } +func (*TransactionDetails_Input) ProtoMessage() {} + +type TransactionDetails_Output struct { + Mine bool `protobuf:"varint,3,opt,name=mine" json:"mine,omitempty"` + // These fields only relevant if mine==true. + Account uint32 `protobuf:"varint,4,opt,name=account" json:"account,omitempty"` + Internal bool `protobuf:"varint,5,opt,name=internal" json:"internal,omitempty"` + // These fields only relevant if mine==false. + Addresses []string `protobuf:"bytes,6,rep,name=addresses" json:"addresses,omitempty"` +} + +func (m *TransactionDetails_Output) Reset() { *m = TransactionDetails_Output{} } +func (m *TransactionDetails_Output) String() string { return proto.CompactTextString(m) } +func (*TransactionDetails_Output) ProtoMessage() {} + +type BlockDetails struct { + Hash []byte `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"` + Height int32 `protobuf:"varint,2,opt,name=height" json:"height,omitempty"` + Timestamp int64 `protobuf:"varint,3,opt,name=timestamp" json:"timestamp,omitempty"` + Transactions []*TransactionDetails `protobuf:"bytes,4,rep,name=transactions" json:"transactions,omitempty"` +} + +func (m *BlockDetails) Reset() { *m = BlockDetails{} } +func (m *BlockDetails) String() string { return proto.CompactTextString(m) } +func (*BlockDetails) ProtoMessage() {} + +func (m *BlockDetails) GetTransactions() []*TransactionDetails { + if m != nil { + return m.Transactions + } + return nil +} + +type AccountBalance struct { + Account uint32 `protobuf:"varint,1,opt,name=account" json:"account,omitempty"` + TotalBalance int64 `protobuf:"varint,2,opt,name=total_balance" json:"total_balance,omitempty"` +} + +func (m *AccountBalance) Reset() { *m = AccountBalance{} } +func (m *AccountBalance) String() string { return proto.CompactTextString(m) } +func (*AccountBalance) ProtoMessage() {} + +type PingRequest struct { +} + +func (m *PingRequest) Reset() { *m = PingRequest{} } +func (m *PingRequest) String() string { return proto.CompactTextString(m) } +func (*PingRequest) ProtoMessage() {} + +type PingResponse struct { +} + +func (m *PingResponse) Reset() { *m = PingResponse{} } +func (m *PingResponse) String() string { return proto.CompactTextString(m) } +func (*PingResponse) ProtoMessage() {} + +type NetworkRequest struct { +} + +func (m *NetworkRequest) Reset() { *m = NetworkRequest{} } +func (m *NetworkRequest) String() string { return proto.CompactTextString(m) } +func (*NetworkRequest) ProtoMessage() {} + +type NetworkResponse struct { + ActiveNetwork uint32 `protobuf:"varint,1,opt,name=active_network" json:"active_network,omitempty"` +} + +func (m *NetworkResponse) Reset() { *m = NetworkResponse{} } +func (m *NetworkResponse) String() string { return proto.CompactTextString(m) } +func (*NetworkResponse) ProtoMessage() {} + +type AccountNumberRequest struct { + AccountName string `protobuf:"bytes,1,opt,name=account_name" json:"account_name,omitempty"` +} + +func (m *AccountNumberRequest) Reset() { *m = AccountNumberRequest{} } +func (m *AccountNumberRequest) String() string { return proto.CompactTextString(m) } +func (*AccountNumberRequest) ProtoMessage() {} + +type AccountNumberResponse struct { + AccountNumber uint32 `protobuf:"varint,1,opt,name=account_number" json:"account_number,omitempty"` +} + +func (m *AccountNumberResponse) Reset() { *m = AccountNumberResponse{} } +func (m *AccountNumberResponse) String() string { return proto.CompactTextString(m) } +func (*AccountNumberResponse) ProtoMessage() {} + +type AccountsRequest struct { +} + +func (m *AccountsRequest) Reset() { *m = AccountsRequest{} } +func (m *AccountsRequest) String() string { return proto.CompactTextString(m) } +func (*AccountsRequest) ProtoMessage() {} + +type AccountsResponse struct { + Accounts []*AccountsResponse_Account `protobuf:"bytes,1,rep,name=accounts" json:"accounts,omitempty"` + CurrentBlockHash []byte `protobuf:"bytes,2,opt,name=current_block_hash,proto3" json:"current_block_hash,omitempty"` + CurrentBlockHeight int32 `protobuf:"varint,3,opt,name=current_block_height" json:"current_block_height,omitempty"` +} + +func (m *AccountsResponse) Reset() { *m = AccountsResponse{} } +func (m *AccountsResponse) String() string { return proto.CompactTextString(m) } +func (*AccountsResponse) ProtoMessage() {} + +func (m *AccountsResponse) GetAccounts() []*AccountsResponse_Account { + if m != nil { + return m.Accounts + } + return nil +} + +type AccountsResponse_Account struct { + AccountNumber uint32 `protobuf:"varint,1,opt,name=account_number" json:"account_number,omitempty"` + AccountName string `protobuf:"bytes,2,opt,name=account_name" json:"account_name,omitempty"` + TotalBalance int64 `protobuf:"varint,3,opt,name=total_balance" json:"total_balance,omitempty"` + ExternalKeyCount uint32 `protobuf:"varint,4,opt,name=external_key_count" json:"external_key_count,omitempty"` + InternalKeyCount uint32 `protobuf:"varint,5,opt,name=internal_key_count" json:"internal_key_count,omitempty"` + ImportedKeyCount uint32 `protobuf:"varint,6,opt,name=imported_key_count" json:"imported_key_count,omitempty"` +} + +func (m *AccountsResponse_Account) Reset() { *m = AccountsResponse_Account{} } +func (m *AccountsResponse_Account) String() string { return proto.CompactTextString(m) } +func (*AccountsResponse_Account) ProtoMessage() {} + +type RenameAccountRequest struct { + AccountNumber uint32 `protobuf:"varint,1,opt,name=account_number" json:"account_number,omitempty"` + NewName string `protobuf:"bytes,2,opt,name=new_name" json:"new_name,omitempty"` +} + +func (m *RenameAccountRequest) Reset() { *m = RenameAccountRequest{} } +func (m *RenameAccountRequest) String() string { return proto.CompactTextString(m) } +func (*RenameAccountRequest) ProtoMessage() {} + +type RenameAccountResponse struct { +} + +func (m *RenameAccountResponse) Reset() { *m = RenameAccountResponse{} } +func (m *RenameAccountResponse) String() string { return proto.CompactTextString(m) } +func (*RenameAccountResponse) ProtoMessage() {} + +type NextAccountRequest struct { + Passphrase []byte `protobuf:"bytes,1,opt,name=passphrase,proto3" json:"passphrase,omitempty"` + AccountName string `protobuf:"bytes,2,opt,name=account_name" json:"account_name,omitempty"` +} + +func (m *NextAccountRequest) Reset() { *m = NextAccountRequest{} } +func (m *NextAccountRequest) String() string { return proto.CompactTextString(m) } +func (*NextAccountRequest) ProtoMessage() {} + +type NextAccountResponse struct { + AccountNumber uint32 `protobuf:"varint,1,opt,name=account_number" json:"account_number,omitempty"` +} + +func (m *NextAccountResponse) Reset() { *m = NextAccountResponse{} } +func (m *NextAccountResponse) String() string { return proto.CompactTextString(m) } +func (*NextAccountResponse) ProtoMessage() {} + +type NextAddressRequest struct { + Account uint32 `protobuf:"varint,1,opt,name=account" json:"account,omitempty"` +} + +func (m *NextAddressRequest) Reset() { *m = NextAddressRequest{} } +func (m *NextAddressRequest) String() string { return proto.CompactTextString(m) } +func (*NextAddressRequest) ProtoMessage() {} + +type NextAddressResponse struct { + Address string `protobuf:"bytes,1,opt,name=address" json:"address,omitempty"` +} + +func (m *NextAddressResponse) Reset() { *m = NextAddressResponse{} } +func (m *NextAddressResponse) String() string { return proto.CompactTextString(m) } +func (*NextAddressResponse) ProtoMessage() {} + +type ImportPrivateKeyRequest struct { + Passphrase []byte `protobuf:"bytes,1,opt,name=passphrase,proto3" json:"passphrase,omitempty"` + Account uint32 `protobuf:"varint,2,opt,name=account" json:"account,omitempty"` + PrivateKeyWif string `protobuf:"bytes,3,opt,name=private_key_wif" json:"private_key_wif,omitempty"` + Rescan bool `protobuf:"varint,4,opt,name=rescan" json:"rescan,omitempty"` +} + +func (m *ImportPrivateKeyRequest) Reset() { *m = ImportPrivateKeyRequest{} } +func (m *ImportPrivateKeyRequest) String() string { return proto.CompactTextString(m) } +func (*ImportPrivateKeyRequest) ProtoMessage() {} + +type ImportPrivateKeyResponse struct { +} + +func (m *ImportPrivateKeyResponse) Reset() { *m = ImportPrivateKeyResponse{} } +func (m *ImportPrivateKeyResponse) String() string { return proto.CompactTextString(m) } +func (*ImportPrivateKeyResponse) ProtoMessage() {} + +type BalanceRequest struct { + AccountNumber uint32 `protobuf:"varint,1,opt,name=account_number" json:"account_number,omitempty"` + RequiredConfirmations int32 `protobuf:"varint,2,opt,name=required_confirmations" json:"required_confirmations,omitempty"` +} + +func (m *BalanceRequest) Reset() { *m = BalanceRequest{} } +func (m *BalanceRequest) String() string { return proto.CompactTextString(m) } +func (*BalanceRequest) ProtoMessage() {} + +type BalanceResponse struct { + Total int64 `protobuf:"varint,1,opt,name=total" json:"total,omitempty"` + Spendable int64 `protobuf:"varint,2,opt,name=spendable" json:"spendable,omitempty"` + ImmatureReward int64 `protobuf:"varint,3,opt,name=immature_reward" json:"immature_reward,omitempty"` +} + +func (m *BalanceResponse) Reset() { *m = BalanceResponse{} } +func (m *BalanceResponse) String() string { return proto.CompactTextString(m) } +func (*BalanceResponse) ProtoMessage() {} + +type GetTransactionsRequest struct { + // Optionally specify the starting block from which to begin including all transactions. + // Either the starting block hash or height may be specified, but not both. + // If a block height is specified and is negative, the absolute value becomes the number of + // last blocks to include. That is, given a current chain height of 1000 and a starting block + // height of -3, transaction notifications will be created for blocks 998, 999, and 1000. + // If both options are excluded, transaction results are created for transactions since the + // genesis block. + StartingBlockHash []byte `protobuf:"bytes,1,opt,name=starting_block_hash,proto3" json:"starting_block_hash,omitempty"` + StartingBlockHeight int32 `protobuf:"zigzag32,2,opt,name=starting_block_height" json:"starting_block_height,omitempty"` + // Optionally specify the last block that transaction results may appear in. + // Either the ending block hash or height may be specified, but not both. + // If both are excluded, transaction results are created for all transactions + // through the best block, and include all unmined transactions. + EndingBlockHash []byte `protobuf:"bytes,3,opt,name=ending_block_hash,proto3" json:"ending_block_hash,omitempty"` + EndingBlockHeight int32 `protobuf:"varint,4,opt,name=ending_block_height" json:"ending_block_height,omitempty"` + // Include at least this many of the newest transactions if they exist. + // Cannot be used when the ending block hash is specified. + // + // TODO: remove until spec adds it back in some way. + MinimumRecentTransactions int32 `protobuf:"varint,5,opt,name=minimum_recent_transactions" json:"minimum_recent_transactions,omitempty"` +} + +func (m *GetTransactionsRequest) Reset() { *m = GetTransactionsRequest{} } +func (m *GetTransactionsRequest) String() string { return proto.CompactTextString(m) } +func (*GetTransactionsRequest) ProtoMessage() {} + +type GetTransactionsResponse struct { + MinedTransactions []*BlockDetails `protobuf:"bytes,1,rep,name=mined_transactions" json:"mined_transactions,omitempty"` + UnminedTransactions []*TransactionDetails `protobuf:"bytes,2,rep,name=unmined_transactions" json:"unmined_transactions,omitempty"` +} + +func (m *GetTransactionsResponse) Reset() { *m = GetTransactionsResponse{} } +func (m *GetTransactionsResponse) String() string { return proto.CompactTextString(m) } +func (*GetTransactionsResponse) ProtoMessage() {} + +func (m *GetTransactionsResponse) GetMinedTransactions() []*BlockDetails { + if m != nil { + return m.MinedTransactions + } + return nil +} + +func (m *GetTransactionsResponse) GetUnminedTransactions() []*TransactionDetails { + if m != nil { + return m.UnminedTransactions + } + return nil +} + +type ChangePassphraseRequest struct { + Key ChangePassphraseRequest_Key `protobuf:"varint,1,opt,name=key,enum=walletrpc.ChangePassphraseRequest_Key" json:"key,omitempty"` + OldPassphrase []byte `protobuf:"bytes,2,opt,name=old_passphrase,proto3" json:"old_passphrase,omitempty"` + NewPassphrase []byte `protobuf:"bytes,3,opt,name=new_passphrase,proto3" json:"new_passphrase,omitempty"` +} + +func (m *ChangePassphraseRequest) Reset() { *m = ChangePassphraseRequest{} } +func (m *ChangePassphraseRequest) String() string { return proto.CompactTextString(m) } +func (*ChangePassphraseRequest) ProtoMessage() {} + +type ChangePassphraseResponse struct { +} + +func (m *ChangePassphraseResponse) Reset() { *m = ChangePassphraseResponse{} } +func (m *ChangePassphraseResponse) String() string { return proto.CompactTextString(m) } +func (*ChangePassphraseResponse) ProtoMessage() {} + +type FundTransactionRequest struct { + Account uint32 `protobuf:"varint,1,opt,name=account" json:"account,omitempty"` + TargetAmount int64 `protobuf:"varint,2,opt,name=target_amount" json:"target_amount,omitempty"` + RequiredConfirmations int32 `protobuf:"varint,3,opt,name=required_confirmations" json:"required_confirmations,omitempty"` + IncludeImmatureCoinbases bool `protobuf:"varint,4,opt,name=include_immature_coinbases" json:"include_immature_coinbases,omitempty"` + IncludeChangeScript bool `protobuf:"varint,5,opt,name=include_change_script" json:"include_change_script,omitempty"` +} + +func (m *FundTransactionRequest) Reset() { *m = FundTransactionRequest{} } +func (m *FundTransactionRequest) String() string { return proto.CompactTextString(m) } +func (*FundTransactionRequest) ProtoMessage() {} + +type FundTransactionResponse struct { + SelectedOutputs []*FundTransactionResponse_PreviousOutput `protobuf:"bytes,1,rep,name=selected_outputs" json:"selected_outputs,omitempty"` + TotalAmount int64 `protobuf:"varint,2,opt,name=total_amount" json:"total_amount,omitempty"` + ChangePkScript []byte `protobuf:"bytes,3,opt,name=change_pk_script,proto3" json:"change_pk_script,omitempty"` +} + +func (m *FundTransactionResponse) Reset() { *m = FundTransactionResponse{} } +func (m *FundTransactionResponse) String() string { return proto.CompactTextString(m) } +func (*FundTransactionResponse) ProtoMessage() {} + +func (m *FundTransactionResponse) GetSelectedOutputs() []*FundTransactionResponse_PreviousOutput { + if m != nil { + return m.SelectedOutputs + } + return nil +} + +type FundTransactionResponse_PreviousOutput struct { + TransactionHash []byte `protobuf:"bytes,1,opt,name=transaction_hash,proto3" json:"transaction_hash,omitempty"` + OutputIndex uint32 `protobuf:"varint,2,opt,name=output_index" json:"output_index,omitempty"` + Amount int64 `protobuf:"varint,3,opt,name=amount" json:"amount,omitempty"` + PkScript []byte `protobuf:"bytes,4,opt,name=pk_script,proto3" json:"pk_script,omitempty"` + ReceiveTime int64 `protobuf:"varint,5,opt,name=receive_time" json:"receive_time,omitempty"` + FromCoinbase bool `protobuf:"varint,6,opt,name=from_coinbase" json:"from_coinbase,omitempty"` +} + +func (m *FundTransactionResponse_PreviousOutput) Reset() { + *m = FundTransactionResponse_PreviousOutput{} +} +func (m *FundTransactionResponse_PreviousOutput) String() string { return proto.CompactTextString(m) } +func (*FundTransactionResponse_PreviousOutput) ProtoMessage() {} + +type SignTransactionRequest struct { + Passphrase []byte `protobuf:"bytes,1,opt,name=passphrase,proto3" json:"passphrase,omitempty"` + SerializedTransaction []byte `protobuf:"bytes,2,opt,name=serialized_transaction,proto3" json:"serialized_transaction,omitempty"` + // If no indexes are specified, signatures scripts will be added for + // every input. If any input indexes are specified, only those inputs + // will be signed. Rather than returning an incompletely signed + // transaction if any of the inputs to be signed can not be, the RPC + // immediately errors. + InputIndexes []uint32 `protobuf:"varint,3,rep,name=input_indexes" json:"input_indexes,omitempty"` +} + +func (m *SignTransactionRequest) Reset() { *m = SignTransactionRequest{} } +func (m *SignTransactionRequest) String() string { return proto.CompactTextString(m) } +func (*SignTransactionRequest) ProtoMessage() {} + +type SignTransactionResponse struct { + Transaction []byte `protobuf:"bytes,1,opt,name=transaction,proto3" json:"transaction,omitempty"` + UnsignedInputIndexes []uint32 `protobuf:"varint,2,rep,name=unsigned_input_indexes" json:"unsigned_input_indexes,omitempty"` +} + +func (m *SignTransactionResponse) Reset() { *m = SignTransactionResponse{} } +func (m *SignTransactionResponse) String() string { return proto.CompactTextString(m) } +func (*SignTransactionResponse) ProtoMessage() {} + +type PublishTransactionRequest struct { + SignedTransaction []byte `protobuf:"bytes,1,opt,name=signed_transaction,proto3" json:"signed_transaction,omitempty"` +} + +func (m *PublishTransactionRequest) Reset() { *m = PublishTransactionRequest{} } +func (m *PublishTransactionRequest) String() string { return proto.CompactTextString(m) } +func (*PublishTransactionRequest) ProtoMessage() {} + +type PublishTransactionResponse struct { +} + +func (m *PublishTransactionResponse) Reset() { *m = PublishTransactionResponse{} } +func (m *PublishTransactionResponse) String() string { return proto.CompactTextString(m) } +func (*PublishTransactionResponse) ProtoMessage() {} + +type TransactionNotificationsRequest struct { +} + +func (m *TransactionNotificationsRequest) Reset() { *m = TransactionNotificationsRequest{} } +func (m *TransactionNotificationsRequest) String() string { return proto.CompactTextString(m) } +func (*TransactionNotificationsRequest) ProtoMessage() {} + +type TransactionNotificationsResponse struct { + // Sorted by increasing height. This is a repeated field so many new blocks + // in a new best chain can be notified at once during a reorganize. + AttachedBlocks []*BlockDetails `protobuf:"bytes,1,rep,name=attached_blocks" json:"attached_blocks,omitempty"` + // If there was a chain reorganize, there may have been blocks with wallet + // transactions that are no longer in the best chain. These are those + // block's hashes. + DetachedBlocks [][]byte `protobuf:"bytes,2,rep,name=detached_blocks,proto3" json:"detached_blocks,omitempty"` + // Any new unmined transactions are included here. These unmined transactions + // refer to the current best chain, so transactions from detached blocks may + // be moved to mempool and included here if they are not mined or double spent + // in the new chain. Additonally, if no new blocks were attached but a relevant + // unmined transaction is seen by the wallet, it will be reported here. + UnminedTransactions []*TransactionDetails `protobuf:"bytes,3,rep,name=unmined_transactions" json:"unmined_transactions,omitempty"` + // Instead of notifying all of the removed unmined transactions, + // just send all of the current hashes. + UnminedTransactionHashes [][]byte `protobuf:"bytes,4,rep,name=unmined_transaction_hashes,proto3" json:"unmined_transaction_hashes,omitempty"` +} + +func (m *TransactionNotificationsResponse) Reset() { *m = TransactionNotificationsResponse{} } +func (m *TransactionNotificationsResponse) String() string { return proto.CompactTextString(m) } +func (*TransactionNotificationsResponse) ProtoMessage() {} + +func (m *TransactionNotificationsResponse) GetAttachedBlocks() []*BlockDetails { + if m != nil { + return m.AttachedBlocks + } + return nil +} + +func (m *TransactionNotificationsResponse) GetUnminedTransactions() []*TransactionDetails { + if m != nil { + return m.UnminedTransactions + } + return nil +} + +type SpentnessNotificationsRequest struct { + Account uint32 `protobuf:"varint,1,opt,name=account" json:"account,omitempty"` + NoNotifyUnspent bool `protobuf:"varint,2,opt,name=no_notify_unspent" json:"no_notify_unspent,omitempty"` + NoNotifySpent bool `protobuf:"varint,3,opt,name=no_notify_spent" json:"no_notify_spent,omitempty"` +} + +func (m *SpentnessNotificationsRequest) Reset() { *m = SpentnessNotificationsRequest{} } +func (m *SpentnessNotificationsRequest) String() string { return proto.CompactTextString(m) } +func (*SpentnessNotificationsRequest) ProtoMessage() {} + +type SpentnessNotificationsResponse struct { + TransactionHash []byte `protobuf:"bytes,1,opt,name=transaction_hash,proto3" json:"transaction_hash,omitempty"` + OutputIndex uint32 `protobuf:"varint,2,opt,name=output_index" json:"output_index,omitempty"` + Spender *SpentnessNotificationsResponse_Spender `protobuf:"bytes,3,opt,name=spender" json:"spender,omitempty"` +} + +func (m *SpentnessNotificationsResponse) Reset() { *m = SpentnessNotificationsResponse{} } +func (m *SpentnessNotificationsResponse) String() string { return proto.CompactTextString(m) } +func (*SpentnessNotificationsResponse) ProtoMessage() {} + +func (m *SpentnessNotificationsResponse) GetSpender() *SpentnessNotificationsResponse_Spender { + if m != nil { + return m.Spender + } + return nil +} + +type SpentnessNotificationsResponse_Spender struct { + TransactionHash []byte `protobuf:"bytes,1,opt,name=transaction_hash,proto3" json:"transaction_hash,omitempty"` + InputIndex uint32 `protobuf:"varint,2,opt,name=input_index" json:"input_index,omitempty"` +} + +func (m *SpentnessNotificationsResponse_Spender) Reset() { + *m = SpentnessNotificationsResponse_Spender{} +} +func (m *SpentnessNotificationsResponse_Spender) String() string { return proto.CompactTextString(m) } +func (*SpentnessNotificationsResponse_Spender) ProtoMessage() {} + +type AccountNotificationsRequest struct { +} + +func (m *AccountNotificationsRequest) Reset() { *m = AccountNotificationsRequest{} } +func (m *AccountNotificationsRequest) String() string { return proto.CompactTextString(m) } +func (*AccountNotificationsRequest) ProtoMessage() {} + +type AccountNotificationsResponse struct { + AccountNumber uint32 `protobuf:"varint,1,opt,name=account_number" json:"account_number,omitempty"` + AccountName string `protobuf:"bytes,2,opt,name=account_name" json:"account_name,omitempty"` + ExternalKeyCount uint32 `protobuf:"varint,3,opt,name=external_key_count" json:"external_key_count,omitempty"` + InternalKeyCount uint32 `protobuf:"varint,4,opt,name=internal_key_count" json:"internal_key_count,omitempty"` + ImportedKeyCount uint32 `protobuf:"varint,5,opt,name=imported_key_count" json:"imported_key_count,omitempty"` +} + +func (m *AccountNotificationsResponse) Reset() { *m = AccountNotificationsResponse{} } +func (m *AccountNotificationsResponse) String() string { return proto.CompactTextString(m) } +func (*AccountNotificationsResponse) ProtoMessage() {} + +type CreateWalletRequest struct { + PublicPassphrase []byte `protobuf:"bytes,1,opt,name=public_passphrase,proto3" json:"public_passphrase,omitempty"` + PrivatePassphrase []byte `protobuf:"bytes,2,opt,name=private_passphrase,proto3" json:"private_passphrase,omitempty"` + Seed []byte `protobuf:"bytes,3,opt,name=seed,proto3" json:"seed,omitempty"` +} + +func (m *CreateWalletRequest) Reset() { *m = CreateWalletRequest{} } +func (m *CreateWalletRequest) String() string { return proto.CompactTextString(m) } +func (*CreateWalletRequest) ProtoMessage() {} + +type CreateWalletResponse struct { +} + +func (m *CreateWalletResponse) Reset() { *m = CreateWalletResponse{} } +func (m *CreateWalletResponse) String() string { return proto.CompactTextString(m) } +func (*CreateWalletResponse) ProtoMessage() {} + +type OpenWalletRequest struct { + PublicPassphrase []byte `protobuf:"bytes,1,opt,name=public_passphrase,proto3" json:"public_passphrase,omitempty"` +} + +func (m *OpenWalletRequest) Reset() { *m = OpenWalletRequest{} } +func (m *OpenWalletRequest) String() string { return proto.CompactTextString(m) } +func (*OpenWalletRequest) ProtoMessage() {} + +type OpenWalletResponse struct { +} + +func (m *OpenWalletResponse) Reset() { *m = OpenWalletResponse{} } +func (m *OpenWalletResponse) String() string { return proto.CompactTextString(m) } +func (*OpenWalletResponse) ProtoMessage() {} + +type CloseWalletRequest struct { +} + +func (m *CloseWalletRequest) Reset() { *m = CloseWalletRequest{} } +func (m *CloseWalletRequest) String() string { return proto.CompactTextString(m) } +func (*CloseWalletRequest) ProtoMessage() {} + +type CloseWalletResponse struct { +} + +func (m *CloseWalletResponse) Reset() { *m = CloseWalletResponse{} } +func (m *CloseWalletResponse) String() string { return proto.CompactTextString(m) } +func (*CloseWalletResponse) ProtoMessage() {} + +type WalletExistsRequest struct { +} + +func (m *WalletExistsRequest) Reset() { *m = WalletExistsRequest{} } +func (m *WalletExistsRequest) String() string { return proto.CompactTextString(m) } +func (*WalletExistsRequest) ProtoMessage() {} + +type WalletExistsResponse struct { + Exists bool `protobuf:"varint,1,opt,name=exists" json:"exists,omitempty"` +} + +func (m *WalletExistsResponse) Reset() { *m = WalletExistsResponse{} } +func (m *WalletExistsResponse) String() string { return proto.CompactTextString(m) } +func (*WalletExistsResponse) ProtoMessage() {} + +type StartBtcdRpcRequest struct { + NetworkAddress string `protobuf:"bytes,1,opt,name=network_address" json:"network_address,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username" json:"username,omitempty"` + Password []byte `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"` + Certificate []byte `protobuf:"bytes,4,opt,name=certificate,proto3" json:"certificate,omitempty"` +} + +func (m *StartBtcdRpcRequest) Reset() { *m = StartBtcdRpcRequest{} } +func (m *StartBtcdRpcRequest) String() string { return proto.CompactTextString(m) } +func (*StartBtcdRpcRequest) ProtoMessage() {} + +type StartBtcdRpcResponse struct { +} + +func (m *StartBtcdRpcResponse) Reset() { *m = StartBtcdRpcResponse{} } +func (m *StartBtcdRpcResponse) String() string { return proto.CompactTextString(m) } +func (*StartBtcdRpcResponse) ProtoMessage() {} + +func init() { + proto.RegisterEnum("walletrpc.ChangePassphraseRequest_Key", ChangePassphraseRequest_Key_name, ChangePassphraseRequest_Key_value) +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// Client API for WalletService service + +type WalletServiceClient interface { + // Queries + Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) + Network(ctx context.Context, in *NetworkRequest, opts ...grpc.CallOption) (*NetworkResponse, error) + AccountNumber(ctx context.Context, in *AccountNumberRequest, opts ...grpc.CallOption) (*AccountNumberResponse, error) + Accounts(ctx context.Context, in *AccountsRequest, opts ...grpc.CallOption) (*AccountsResponse, error) + Balance(ctx context.Context, in *BalanceRequest, opts ...grpc.CallOption) (*BalanceResponse, error) + GetTransactions(ctx context.Context, in *GetTransactionsRequest, opts ...grpc.CallOption) (*GetTransactionsResponse, error) + // Notifications + TransactionNotifications(ctx context.Context, in *TransactionNotificationsRequest, opts ...grpc.CallOption) (WalletService_TransactionNotificationsClient, error) + SpentnessNotifications(ctx context.Context, in *SpentnessNotificationsRequest, opts ...grpc.CallOption) (WalletService_SpentnessNotificationsClient, error) + AccountNotifications(ctx context.Context, in *AccountNotificationsRequest, opts ...grpc.CallOption) (WalletService_AccountNotificationsClient, error) + // Control + ChangePassphrase(ctx context.Context, in *ChangePassphraseRequest, opts ...grpc.CallOption) (*ChangePassphraseResponse, error) + RenameAccount(ctx context.Context, in *RenameAccountRequest, opts ...grpc.CallOption) (*RenameAccountResponse, error) + NextAccount(ctx context.Context, in *NextAccountRequest, opts ...grpc.CallOption) (*NextAccountResponse, error) + NextAddress(ctx context.Context, in *NextAddressRequest, opts ...grpc.CallOption) (*NextAddressResponse, error) + ImportPrivateKey(ctx context.Context, in *ImportPrivateKeyRequest, opts ...grpc.CallOption) (*ImportPrivateKeyResponse, error) + FundTransaction(ctx context.Context, in *FundTransactionRequest, opts ...grpc.CallOption) (*FundTransactionResponse, error) + SignTransaction(ctx context.Context, in *SignTransactionRequest, opts ...grpc.CallOption) (*SignTransactionResponse, error) + PublishTransaction(ctx context.Context, in *PublishTransactionRequest, opts ...grpc.CallOption) (*PublishTransactionResponse, error) +} + +type walletServiceClient struct { + cc *grpc.ClientConn +} + +func NewWalletServiceClient(cc *grpc.ClientConn) WalletServiceClient { + return &walletServiceClient{cc} +} + +func (c *walletServiceClient) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) { + out := new(PingResponse) + err := grpc.Invoke(ctx, "/walletrpc.WalletService/Ping", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletServiceClient) Network(ctx context.Context, in *NetworkRequest, opts ...grpc.CallOption) (*NetworkResponse, error) { + out := new(NetworkResponse) + err := grpc.Invoke(ctx, "/walletrpc.WalletService/Network", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletServiceClient) AccountNumber(ctx context.Context, in *AccountNumberRequest, opts ...grpc.CallOption) (*AccountNumberResponse, error) { + out := new(AccountNumberResponse) + err := grpc.Invoke(ctx, "/walletrpc.WalletService/AccountNumber", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletServiceClient) Accounts(ctx context.Context, in *AccountsRequest, opts ...grpc.CallOption) (*AccountsResponse, error) { + out := new(AccountsResponse) + err := grpc.Invoke(ctx, "/walletrpc.WalletService/Accounts", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletServiceClient) Balance(ctx context.Context, in *BalanceRequest, opts ...grpc.CallOption) (*BalanceResponse, error) { + out := new(BalanceResponse) + err := grpc.Invoke(ctx, "/walletrpc.WalletService/Balance", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletServiceClient) GetTransactions(ctx context.Context, in *GetTransactionsRequest, opts ...grpc.CallOption) (*GetTransactionsResponse, error) { + out := new(GetTransactionsResponse) + err := grpc.Invoke(ctx, "/walletrpc.WalletService/GetTransactions", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletServiceClient) TransactionNotifications(ctx context.Context, in *TransactionNotificationsRequest, opts ...grpc.CallOption) (WalletService_TransactionNotificationsClient, error) { + stream, err := grpc.NewClientStream(ctx, &_WalletService_serviceDesc.Streams[0], c.cc, "/walletrpc.WalletService/TransactionNotifications", opts...) + if err != nil { + return nil, err + } + x := &walletServiceTransactionNotificationsClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type WalletService_TransactionNotificationsClient interface { + Recv() (*TransactionNotificationsResponse, error) + grpc.ClientStream +} + +type walletServiceTransactionNotificationsClient struct { + grpc.ClientStream +} + +func (x *walletServiceTransactionNotificationsClient) Recv() (*TransactionNotificationsResponse, error) { + m := new(TransactionNotificationsResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *walletServiceClient) SpentnessNotifications(ctx context.Context, in *SpentnessNotificationsRequest, opts ...grpc.CallOption) (WalletService_SpentnessNotificationsClient, error) { + stream, err := grpc.NewClientStream(ctx, &_WalletService_serviceDesc.Streams[1], c.cc, "/walletrpc.WalletService/SpentnessNotifications", opts...) + if err != nil { + return nil, err + } + x := &walletServiceSpentnessNotificationsClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type WalletService_SpentnessNotificationsClient interface { + Recv() (*SpentnessNotificationsResponse, error) + grpc.ClientStream +} + +type walletServiceSpentnessNotificationsClient struct { + grpc.ClientStream +} + +func (x *walletServiceSpentnessNotificationsClient) Recv() (*SpentnessNotificationsResponse, error) { + m := new(SpentnessNotificationsResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *walletServiceClient) AccountNotifications(ctx context.Context, in *AccountNotificationsRequest, opts ...grpc.CallOption) (WalletService_AccountNotificationsClient, error) { + stream, err := grpc.NewClientStream(ctx, &_WalletService_serviceDesc.Streams[2], c.cc, "/walletrpc.WalletService/AccountNotifications", opts...) + if err != nil { + return nil, err + } + x := &walletServiceAccountNotificationsClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type WalletService_AccountNotificationsClient interface { + Recv() (*AccountNotificationsResponse, error) + grpc.ClientStream +} + +type walletServiceAccountNotificationsClient struct { + grpc.ClientStream +} + +func (x *walletServiceAccountNotificationsClient) Recv() (*AccountNotificationsResponse, error) { + m := new(AccountNotificationsResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *walletServiceClient) ChangePassphrase(ctx context.Context, in *ChangePassphraseRequest, opts ...grpc.CallOption) (*ChangePassphraseResponse, error) { + out := new(ChangePassphraseResponse) + err := grpc.Invoke(ctx, "/walletrpc.WalletService/ChangePassphrase", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletServiceClient) RenameAccount(ctx context.Context, in *RenameAccountRequest, opts ...grpc.CallOption) (*RenameAccountResponse, error) { + out := new(RenameAccountResponse) + err := grpc.Invoke(ctx, "/walletrpc.WalletService/RenameAccount", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletServiceClient) NextAccount(ctx context.Context, in *NextAccountRequest, opts ...grpc.CallOption) (*NextAccountResponse, error) { + out := new(NextAccountResponse) + err := grpc.Invoke(ctx, "/walletrpc.WalletService/NextAccount", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletServiceClient) NextAddress(ctx context.Context, in *NextAddressRequest, opts ...grpc.CallOption) (*NextAddressResponse, error) { + out := new(NextAddressResponse) + err := grpc.Invoke(ctx, "/walletrpc.WalletService/NextAddress", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletServiceClient) ImportPrivateKey(ctx context.Context, in *ImportPrivateKeyRequest, opts ...grpc.CallOption) (*ImportPrivateKeyResponse, error) { + out := new(ImportPrivateKeyResponse) + err := grpc.Invoke(ctx, "/walletrpc.WalletService/ImportPrivateKey", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletServiceClient) FundTransaction(ctx context.Context, in *FundTransactionRequest, opts ...grpc.CallOption) (*FundTransactionResponse, error) { + out := new(FundTransactionResponse) + err := grpc.Invoke(ctx, "/walletrpc.WalletService/FundTransaction", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletServiceClient) SignTransaction(ctx context.Context, in *SignTransactionRequest, opts ...grpc.CallOption) (*SignTransactionResponse, error) { + out := new(SignTransactionResponse) + err := grpc.Invoke(ctx, "/walletrpc.WalletService/SignTransaction", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletServiceClient) PublishTransaction(ctx context.Context, in *PublishTransactionRequest, opts ...grpc.CallOption) (*PublishTransactionResponse, error) { + out := new(PublishTransactionResponse) + err := grpc.Invoke(ctx, "/walletrpc.WalletService/PublishTransaction", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for WalletService service + +type WalletServiceServer interface { + // Queries + Ping(context.Context, *PingRequest) (*PingResponse, error) + Network(context.Context, *NetworkRequest) (*NetworkResponse, error) + AccountNumber(context.Context, *AccountNumberRequest) (*AccountNumberResponse, error) + Accounts(context.Context, *AccountsRequest) (*AccountsResponse, error) + Balance(context.Context, *BalanceRequest) (*BalanceResponse, error) + GetTransactions(context.Context, *GetTransactionsRequest) (*GetTransactionsResponse, error) + // Notifications + TransactionNotifications(*TransactionNotificationsRequest, WalletService_TransactionNotificationsServer) error + SpentnessNotifications(*SpentnessNotificationsRequest, WalletService_SpentnessNotificationsServer) error + AccountNotifications(*AccountNotificationsRequest, WalletService_AccountNotificationsServer) error + // Control + ChangePassphrase(context.Context, *ChangePassphraseRequest) (*ChangePassphraseResponse, error) + RenameAccount(context.Context, *RenameAccountRequest) (*RenameAccountResponse, error) + NextAccount(context.Context, *NextAccountRequest) (*NextAccountResponse, error) + NextAddress(context.Context, *NextAddressRequest) (*NextAddressResponse, error) + ImportPrivateKey(context.Context, *ImportPrivateKeyRequest) (*ImportPrivateKeyResponse, error) + FundTransaction(context.Context, *FundTransactionRequest) (*FundTransactionResponse, error) + SignTransaction(context.Context, *SignTransactionRequest) (*SignTransactionResponse, error) + PublishTransaction(context.Context, *PublishTransactionRequest) (*PublishTransactionResponse, error) +} + +func RegisterWalletServiceServer(s *grpc.Server, srv WalletServiceServer) { + s.RegisterService(&_WalletService_serviceDesc, srv) +} + +func _WalletService_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(PingRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(WalletServiceServer).Ping(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _WalletService_Network_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(NetworkRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(WalletServiceServer).Network(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _WalletService_AccountNumber_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(AccountNumberRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(WalletServiceServer).AccountNumber(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _WalletService_Accounts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(AccountsRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(WalletServiceServer).Accounts(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _WalletService_Balance_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(BalanceRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(WalletServiceServer).Balance(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _WalletService_GetTransactions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(GetTransactionsRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(WalletServiceServer).GetTransactions(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _WalletService_TransactionNotifications_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(TransactionNotificationsRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(WalletServiceServer).TransactionNotifications(m, &walletServiceTransactionNotificationsServer{stream}) +} + +type WalletService_TransactionNotificationsServer interface { + Send(*TransactionNotificationsResponse) error + grpc.ServerStream +} + +type walletServiceTransactionNotificationsServer struct { + grpc.ServerStream +} + +func (x *walletServiceTransactionNotificationsServer) Send(m *TransactionNotificationsResponse) error { + return x.ServerStream.SendMsg(m) +} + +func _WalletService_SpentnessNotifications_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(SpentnessNotificationsRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(WalletServiceServer).SpentnessNotifications(m, &walletServiceSpentnessNotificationsServer{stream}) +} + +type WalletService_SpentnessNotificationsServer interface { + Send(*SpentnessNotificationsResponse) error + grpc.ServerStream +} + +type walletServiceSpentnessNotificationsServer struct { + grpc.ServerStream +} + +func (x *walletServiceSpentnessNotificationsServer) Send(m *SpentnessNotificationsResponse) error { + return x.ServerStream.SendMsg(m) +} + +func _WalletService_AccountNotifications_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(AccountNotificationsRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(WalletServiceServer).AccountNotifications(m, &walletServiceAccountNotificationsServer{stream}) +} + +type WalletService_AccountNotificationsServer interface { + Send(*AccountNotificationsResponse) error + grpc.ServerStream +} + +type walletServiceAccountNotificationsServer struct { + grpc.ServerStream +} + +func (x *walletServiceAccountNotificationsServer) Send(m *AccountNotificationsResponse) error { + return x.ServerStream.SendMsg(m) +} + +func _WalletService_ChangePassphrase_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(ChangePassphraseRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(WalletServiceServer).ChangePassphrase(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _WalletService_RenameAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(RenameAccountRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(WalletServiceServer).RenameAccount(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _WalletService_NextAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(NextAccountRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(WalletServiceServer).NextAccount(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _WalletService_NextAddress_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(NextAddressRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(WalletServiceServer).NextAddress(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _WalletService_ImportPrivateKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(ImportPrivateKeyRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(WalletServiceServer).ImportPrivateKey(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _WalletService_FundTransaction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(FundTransactionRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(WalletServiceServer).FundTransaction(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _WalletService_SignTransaction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(SignTransactionRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(WalletServiceServer).SignTransaction(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _WalletService_PublishTransaction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(PublishTransactionRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(WalletServiceServer).PublishTransaction(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +var _WalletService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "walletrpc.WalletService", + HandlerType: (*WalletServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Ping", + Handler: _WalletService_Ping_Handler, + }, + { + MethodName: "Network", + Handler: _WalletService_Network_Handler, + }, + { + MethodName: "AccountNumber", + Handler: _WalletService_AccountNumber_Handler, + }, + { + MethodName: "Accounts", + Handler: _WalletService_Accounts_Handler, + }, + { + MethodName: "Balance", + Handler: _WalletService_Balance_Handler, + }, + { + MethodName: "GetTransactions", + Handler: _WalletService_GetTransactions_Handler, + }, + { + MethodName: "ChangePassphrase", + Handler: _WalletService_ChangePassphrase_Handler, + }, + { + MethodName: "RenameAccount", + Handler: _WalletService_RenameAccount_Handler, + }, + { + MethodName: "NextAccount", + Handler: _WalletService_NextAccount_Handler, + }, + { + MethodName: "NextAddress", + Handler: _WalletService_NextAddress_Handler, + }, + { + MethodName: "ImportPrivateKey", + Handler: _WalletService_ImportPrivateKey_Handler, + }, + { + MethodName: "FundTransaction", + Handler: _WalletService_FundTransaction_Handler, + }, + { + MethodName: "SignTransaction", + Handler: _WalletService_SignTransaction_Handler, + }, + { + MethodName: "PublishTransaction", + Handler: _WalletService_PublishTransaction_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "TransactionNotifications", + Handler: _WalletService_TransactionNotifications_Handler, + ServerStreams: true, + }, + { + StreamName: "SpentnessNotifications", + Handler: _WalletService_SpentnessNotifications_Handler, + ServerStreams: true, + }, + { + StreamName: "AccountNotifications", + Handler: _WalletService_AccountNotifications_Handler, + ServerStreams: true, + }, + }, +} + +// Client API for WalletLoaderService service + +type WalletLoaderServiceClient interface { + WalletExists(ctx context.Context, in *WalletExistsRequest, opts ...grpc.CallOption) (*WalletExistsResponse, error) + CreateWallet(ctx context.Context, in *CreateWalletRequest, opts ...grpc.CallOption) (*CreateWalletResponse, error) + OpenWallet(ctx context.Context, in *OpenWalletRequest, opts ...grpc.CallOption) (*OpenWalletResponse, error) + CloseWallet(ctx context.Context, in *CloseWalletRequest, opts ...grpc.CallOption) (*CloseWalletResponse, error) + StartBtcdRpc(ctx context.Context, in *StartBtcdRpcRequest, opts ...grpc.CallOption) (*StartBtcdRpcResponse, error) +} + +type walletLoaderServiceClient struct { + cc *grpc.ClientConn +} + +func NewWalletLoaderServiceClient(cc *grpc.ClientConn) WalletLoaderServiceClient { + return &walletLoaderServiceClient{cc} +} + +func (c *walletLoaderServiceClient) WalletExists(ctx context.Context, in *WalletExistsRequest, opts ...grpc.CallOption) (*WalletExistsResponse, error) { + out := new(WalletExistsResponse) + err := grpc.Invoke(ctx, "/walletrpc.WalletLoaderService/WalletExists", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletLoaderServiceClient) CreateWallet(ctx context.Context, in *CreateWalletRequest, opts ...grpc.CallOption) (*CreateWalletResponse, error) { + out := new(CreateWalletResponse) + err := grpc.Invoke(ctx, "/walletrpc.WalletLoaderService/CreateWallet", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletLoaderServiceClient) OpenWallet(ctx context.Context, in *OpenWalletRequest, opts ...grpc.CallOption) (*OpenWalletResponse, error) { + out := new(OpenWalletResponse) + err := grpc.Invoke(ctx, "/walletrpc.WalletLoaderService/OpenWallet", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletLoaderServiceClient) CloseWallet(ctx context.Context, in *CloseWalletRequest, opts ...grpc.CallOption) (*CloseWalletResponse, error) { + out := new(CloseWalletResponse) + err := grpc.Invoke(ctx, "/walletrpc.WalletLoaderService/CloseWallet", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *walletLoaderServiceClient) StartBtcdRpc(ctx context.Context, in *StartBtcdRpcRequest, opts ...grpc.CallOption) (*StartBtcdRpcResponse, error) { + out := new(StartBtcdRpcResponse) + err := grpc.Invoke(ctx, "/walletrpc.WalletLoaderService/StartBtcdRpc", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for WalletLoaderService service + +type WalletLoaderServiceServer interface { + WalletExists(context.Context, *WalletExistsRequest) (*WalletExistsResponse, error) + CreateWallet(context.Context, *CreateWalletRequest) (*CreateWalletResponse, error) + OpenWallet(context.Context, *OpenWalletRequest) (*OpenWalletResponse, error) + CloseWallet(context.Context, *CloseWalletRequest) (*CloseWalletResponse, error) + StartBtcdRpc(context.Context, *StartBtcdRpcRequest) (*StartBtcdRpcResponse, error) +} + +func RegisterWalletLoaderServiceServer(s *grpc.Server, srv WalletLoaderServiceServer) { + s.RegisterService(&_WalletLoaderService_serviceDesc, srv) +} + +func _WalletLoaderService_WalletExists_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(WalletExistsRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(WalletLoaderServiceServer).WalletExists(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _WalletLoaderService_CreateWallet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(CreateWalletRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(WalletLoaderServiceServer).CreateWallet(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _WalletLoaderService_OpenWallet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(OpenWalletRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(WalletLoaderServiceServer).OpenWallet(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _WalletLoaderService_CloseWallet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(CloseWalletRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(WalletLoaderServiceServer).CloseWallet(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +func _WalletLoaderService_StartBtcdRpc_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { + in := new(StartBtcdRpcRequest) + if err := dec(in); err != nil { + return nil, err + } + out, err := srv.(WalletLoaderServiceServer).StartBtcdRpc(ctx, in) + if err != nil { + return nil, err + } + return out, nil +} + +var _WalletLoaderService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "walletrpc.WalletLoaderService", + HandlerType: (*WalletLoaderServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "WalletExists", + Handler: _WalletLoaderService_WalletExists_Handler, + }, + { + MethodName: "CreateWallet", + Handler: _WalletLoaderService_CreateWallet_Handler, + }, + { + MethodName: "OpenWallet", + Handler: _WalletLoaderService_OpenWallet_Handler, + }, + { + MethodName: "CloseWallet", + Handler: _WalletLoaderService_CloseWallet_Handler, + }, + { + MethodName: "StartBtcdRpc", + Handler: _WalletLoaderService_StartBtcdRpc_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, +} diff --git a/rpchelp_test.go b/rpchelp_test.go deleted file mode 100644 index 697f52d..0000000 --- a/rpchelp_test.go +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) 2015 The btcsuite developers -// -// Permission to use, copy, modify, and distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -package main - -import ( - "strings" - "testing" - - "github.com/btcsuite/btcd/btcjson" - "github.com/btcsuite/btcwallet/internal/rpchelp" -) - -func serverMethods() map[string]struct{} { - m := make(map[string]struct{}) - for method, handlerData := range rpcHandlers { - if !handlerData.noHelp { - m[method] = struct{}{} - } - } - return m -} - -// TestRPCMethodHelpGeneration ensures that help text can be generated for every -// method of the RPC server for every supported locale. -func TestRPCMethodHelpGeneration(t *testing.T) { - needsGenerate := false - - defer func() { - if needsGenerate && !t.Failed() { - t.Error("Generated help texts are out of date: run 'go generate'") - return - } - if t.Failed() { - t.Log("Regenerate help texts with 'go generate' after fixing") - } - }() - - for i := range rpchelp.HelpDescs { - svrMethods := serverMethods() - locale := rpchelp.HelpDescs[i].Locale - generatedDescs := localeHelpDescs[locale]() - for _, m := range rpchelp.Methods { - delete(svrMethods, m.Method) - - helpText, err := btcjson.GenerateHelp(m.Method, rpchelp.HelpDescs[i].Descs, m.ResultTypes...) - if err != nil { - t.Errorf("Cannot generate '%s' help for method '%s': missing description for '%s'", - locale, m.Method, err) - continue - } - if !needsGenerate && helpText != generatedDescs[m.Method] { - needsGenerate = true - } - } - - for m := range svrMethods { - t.Errorf("Missing '%s' help for method '%s'", locale, m) - } - } -} - -// TestRPCMethodUsageGeneration ensures that single line usage text can be -// generated for every supported request of the RPC server. -func TestRPCMethodUsageGeneration(t *testing.T) { - needsGenerate := false - - defer func() { - if needsGenerate && !t.Failed() { - t.Error("Generated help usages are out of date: run 'go generate'") - return - } - if t.Failed() { - t.Log("Regenerate help usage with 'go generate' after fixing") - } - }() - - svrMethods := serverMethods() - usageStrs := make([]string, 0, len(rpchelp.Methods)) - for _, m := range rpchelp.Methods { - delete(svrMethods, m.Method) - - usage, err := btcjson.MethodUsageText(m.Method) - if err != nil { - t.Errorf("Cannot generate single line usage for method '%s': %v", - m.Method, err) - } - - if !t.Failed() { - usageStrs = append(usageStrs, usage) - } - } - - if !t.Failed() { - usages := strings.Join(usageStrs, "\n") - needsGenerate = usages != requestUsages - } -} diff --git a/rpcserver.go b/rpcserver.go index 1b6f809..ebfac9e 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -17,181 +17,180 @@ package main import ( - "bytes" - "crypto/sha256" - "crypto/subtle" "crypto/tls" - "encoding/base64" - "encoding/hex" - "encoding/json" "errors" - "fmt" - "io" "io/ioutil" "net" - "net/http" "os" "path/filepath" "runtime" "strings" - "sync" - "sync/atomic" "time" - "github.com/btcsuite/btcd/btcec" - "github.com/btcsuite/btcd/btcjson" - "github.com/btcsuite/btcd/chaincfg" - "github.com/btcsuite/btcd/txscript" - "github.com/btcsuite/btcd/wire" - "github.com/btcsuite/btcrpcclient" "github.com/btcsuite/btcutil" - "github.com/btcsuite/btcwallet/chain" - "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/rpc/legacyrpc" + "github.com/btcsuite/btcwallet/rpc/rpcserver" "github.com/btcsuite/btcwallet/wallet" - "github.com/btcsuite/btcwallet/wtxmgr" - "github.com/btcsuite/websocket" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" ) -// Error types to simplify the reporting of specific categories of -// errors, and their *btcjson.RPCError creation. -type ( - // DeserializationError describes a failed deserializaion due to bad - // user input. It cooresponds to btcjson.ErrRPCDeserialization. - DeserializationError struct { - error +// openRPCKeyPair creates or loads the RPC TLS keypair specified by the +// application config. +func openRPCKeyPair() (tls.Certificate, error) { + // Check for existence of cert file and key file. Generate a new + // keypair if both are missing. If one exists but not the other, the + // error will occur in LoadX509KeyPair. + _, e1 := os.Stat(cfg.RPCKey) + _, e2 := os.Stat(cfg.RPCCert) + if os.IsNotExist(e1) && os.IsNotExist(e2) { + return generateRPCKeyPair() } - - // InvalidParameterError describes an invalid parameter passed by - // the user. It cooresponds to btcjson.ErrRPCInvalidParameter. - InvalidParameterError struct { - error - } - - // ParseError describes a failed parse due to bad user input. It - // cooresponds to btcjson.ErrRPCParse. - ParseError struct { - error - } -) - -// Errors variables that are defined once here to avoid duplication below. -var ( - ErrNeedPositiveAmount = InvalidParameterError{ - errors.New("amount must be positive"), - } - - ErrNeedPositiveMinconf = InvalidParameterError{ - errors.New("minconf must be positive"), - } - - ErrAddressNotInWallet = btcjson.RPCError{ - Code: btcjson.ErrRPCWallet, - Message: "address not found in wallet", - } - - ErrAccountNameNotFound = btcjson.RPCError{ - Code: btcjson.ErrRPCWalletInvalidAccountName, - Message: "account name not found", - } - - ErrUnloadedWallet = btcjson.RPCError{ - Code: btcjson.ErrRPCWallet, - Message: "Request requires a wallet but wallet has not loaded yet", - } - - ErrWalletUnlockNeeded = btcjson.RPCError{ - Code: btcjson.ErrRPCWalletUnlockNeeded, - Message: "Enter the wallet passphrase with walletpassphrase first", - } - - ErrNotImportedAccount = btcjson.RPCError{ - Code: btcjson.ErrRPCWallet, - Message: "imported addresses must belong to the imported account", - } - - ErrNoTransactionInfo = btcjson.RPCError{ - Code: btcjson.ErrRPCNoTxInfo, - Message: "No information for transaction", - } - - ErrReservedAccountName = btcjson.RPCError{ - Code: btcjson.ErrRPCInvalidParameter, - Message: "Account name is reserved by RPC server", - } -) - -// TODO(jrick): There are several error paths which 'replace' various errors -// with a more appropiate error from the btcjson package. Create a map of -// these replacements so they can be handled once after an RPC handler has -// returned and before the error is marshaled. - -// confirmed checks whether a transaction at height txHeight has met minconf -// confirmations for a blockchain at height curHeight. -func confirmed(minconf, txHeight, curHeight int32) bool { - return confirms(txHeight, curHeight) >= minconf + return tls.LoadX509KeyPair(cfg.RPCCert, cfg.RPCKey) } -// confirms returns the number of confirmations for a transaction in a block at -// height txHeight (or -1 for an unconfirmed tx) given the chain height -// curHeight. -func confirms(txHeight, curHeight int32) int32 { - switch { - case txHeight == -1, txHeight > curHeight: - return 0 - default: - return curHeight - txHeight + 1 +// generateRPCKeyPair generates a new RPC TLS keypair and writes the pair in PEM +// format to the paths specified by the config. If successful, the new keypair +// is returned. +func generateRPCKeyPair() (tls.Certificate, error) { + log.Infof("Generating TLS certificates...") + + // Create directories for cert and key files if they do not yet exist. + certDir, _ := filepath.Split(cfg.RPCCert) + keyDir, _ := filepath.Split(cfg.RPCKey) + err := os.MkdirAll(certDir, 0700) + if err != nil { + return tls.Certificate{}, err } -} - -type websocketClient struct { - conn *websocket.Conn - authenticated bool - remoteAddr string - allRequests chan []byte - responses chan []byte - quit chan struct{} // closed on disconnect - wg sync.WaitGroup -} - -func newWebsocketClient(c *websocket.Conn, authenticated bool, remoteAddr string) *websocketClient { - return &websocketClient{ - conn: c, - authenticated: authenticated, - remoteAddr: remoteAddr, - allRequests: make(chan []byte), - responses: make(chan []byte), - quit: make(chan struct{}), + err = os.MkdirAll(keyDir, 0700) + if err != nil { + return tls.Certificate{}, err } -} -func (c *websocketClient) send(b []byte) error { - select { - case c.responses <- b: - return nil - case <-c.quit: - return errors.New("websocket client disconnected") + // Generate cert pair. + org := "btcwallet autogenerated cert" + validUntil := time.Now().Add(time.Hour * 24 * 365 * 10) + cert, key, err := btcutil.NewTLSCertPair(org, validUntil, nil) + if err != nil { + return tls.Certificate{}, err } + keyPair, err := tls.X509KeyPair(cert, key) + if err != nil { + return tls.Certificate{}, err + } + + // Write cert and key files. + err = ioutil.WriteFile(cfg.RPCCert, cert, 0600) + if err != nil { + return tls.Certificate{}, err + } + err = ioutil.WriteFile(cfg.RPCKey, key, 0600) + if err != nil { + rmErr := os.Remove(cfg.RPCCert) + if rmErr != nil { + log.Warnf("Cannot remove written certificates: %v", rmErr) + } + return tls.Certificate{}, err + } + + log.Info("Done generating TLS certificates") + return keyPair, nil } -// parseListeners splits the list of listen addresses passed in addrs into -// IPv4 and IPv6 slices and returns them. This allows easy creation of the -// listeners on the correct interface "tcp4" and "tcp6". It also properly -// detects addresses which apply to "all interfaces" and adds the address to -// both slices. -func parseListeners(addrs []string) ([]string, []string, error) { - ipv4ListenAddrs := make([]string, 0, len(addrs)*2) - ipv6ListenAddrs := make([]string, 0, len(addrs)*2) - for _, addr := range addrs { +func startRPCServers(walletLoader *wallet.Loader) (*grpc.Server, *legacyrpc.Server, error) { + var ( + server *grpc.Server + legacyServer *legacyrpc.Server + legacyListen = net.Listen + keyPair tls.Certificate + err error + ) + if cfg.DisableServerTLS { + log.Info("Server TLS is disabled. Only legacy RPC may be used") + } else { + keyPair, err = openRPCKeyPair() + if err != nil { + return nil, nil, err + } + + // Change the standard net.Listen function to the tls one. + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{keyPair}, + MinVersion: tls.VersionTLS12, + NextProtos: []string{"h2"}, // HTTP/2 over TLS + } + legacyListen = func(net string, laddr string) (net.Listener, error) { + return tls.Listen(net, laddr, tlsConfig) + } + + if len(cfg.ExperimentalRPCListeners) != 0 { + listeners := makeListeners(cfg.ExperimentalRPCListeners, net.Listen) + if len(listeners) == 0 { + err := errors.New("failed to create listeners for RPC server") + return nil, nil, err + } + creds := credentials.NewServerTLSFromCert(&keyPair) + server = grpc.NewServer(grpc.Creds(creds)) + rpcserver.StartWalletLoaderService(server, walletLoader, activeNet) + for _, lis := range listeners { + lis := lis + go func() { + log.Infof("Experimental RPC server listening on %s", + lis.Addr()) + err := server.Serve(lis) + log.Tracef("Finished serving expimental RPC: %v", + err) + }() + } + } + } + + if cfg.Username == "" || cfg.Password == "" { + log.Info("Legacy RPC server disabled (requires username and password)") + } else if len(cfg.LegacyRPCListeners) != 0 { + listeners := makeListeners(cfg.LegacyRPCListeners, legacyListen) + if len(listeners) == 0 { + err := errors.New("failed to create listeners for legacy RPC server") + return nil, nil, err + } + opts := legacyrpc.Options{ + Username: cfg.Username, + Password: cfg.Password, + MaxPOSTClients: cfg.LegacyRPCMaxClients, + MaxWebsocketClients: cfg.LegacyRPCMaxWebsockets, + } + legacyServer = legacyrpc.NewServer(&opts, walletLoader, listeners) + } + + // Error when neither the GRPC nor legacy RPC servers can be started. + if server == nil && legacyServer == nil { + return nil, nil, errors.New("no suitable RPC services can be started") + } + + return server, legacyServer, nil +} + +type listenFunc func(net string, laddr string) (net.Listener, error) + +// makeListeners splits the normalized listen addresses into IPv4 and IPv6 +// addresses and creates new net.Listeners for each with the passed listen func. +// Invalid addresses are logged and skipped. +func makeListeners(normalizedListenAddrs []string, listen listenFunc) []net.Listener { + ipv4Addrs := make([]string, 0, len(normalizedListenAddrs)*2) + ipv6Addrs := make([]string, 0, len(normalizedListenAddrs)*2) + for _, addr := range normalizedListenAddrs { host, _, err := net.SplitHostPort(addr) if err != nil { // Shouldn't happen due to already being normalized. - return nil, nil, err + log.Errorf("`%s` is not a normalized "+ + "listener address", addr) + continue } // Empty host or host of * on plan9 is both IPv4 and IPv6. if host == "" || (host == "*" && runtime.GOOS == "plan9") { - ipv4ListenAddrs = append(ipv4ListenAddrs, addr) - ipv6ListenAddrs = append(ipv6ListenAddrs, addr) + ipv4Addrs = append(ipv4Addrs, addr) + ipv6Addrs = append(ipv6Addrs, addr) continue } @@ -205,3043 +204,45 @@ func parseListeners(addrs []string) ([]string, []string, error) { host = host[:zoneIndex] } - // Parse the IP. ip := net.ParseIP(host) - if ip == nil { - return nil, nil, fmt.Errorf("'%s' is not a valid IP "+ - "address", host) - } - - // To4 returns nil when the IP is not an IPv4 address, so use - // this determine the address type. - if ip.To4() == nil { - ipv6ListenAddrs = append(ipv6ListenAddrs, addr) - } else { - ipv4ListenAddrs = append(ipv4ListenAddrs, addr) - } - } - return ipv4ListenAddrs, ipv6ListenAddrs, nil -} - -// genCertPair generates a key/cert pair to the paths provided. -func genCertPair(certFile, keyFile string) error { - log.Infof("Generating TLS certificates...") - - // Create directories for cert and key files if they do not yet exist. - certDir, _ := filepath.Split(certFile) - keyDir, _ := filepath.Split(keyFile) - if err := os.MkdirAll(certDir, 0700); err != nil { - return err - } - if err := os.MkdirAll(keyDir, 0700); err != nil { - return err - } - - // Generate cert pair. - org := "btcwallet autogenerated cert" - validUntil := time.Now().Add(time.Hour * 24 * 365 * 10) - cert, key, err := btcutil.NewTLSCertPair(org, validUntil, nil) - if err != nil { - return err - } - - // Write cert and key files. - if err = ioutil.WriteFile(certFile, cert, 0666); err != nil { - return err - } - if err = ioutil.WriteFile(keyFile, key, 0600); err != nil { - if rmErr := os.Remove(certFile); rmErr != nil { - log.Warnf("Cannot remove written certificates: %v", rmErr) - } - return err - } - - log.Info("Done generating TLS certificates") - return nil -} - -// rpcServer holds the items the RPC server may need to access (auth, -// config, shutdown, etc.) -type rpcServer struct { - wallet *wallet.Wallet - chainSvr *chain.Client - createOK bool - handlerLookup func(string) (requestHandler, bool) - handlerMu sync.Mutex - - listeners []net.Listener - authsha [sha256.Size]byte - upgrader websocket.Upgrader - - maxPostClients int64 // Max concurrent HTTP POST clients. - maxWebsocketClients int64 // Max concurrent websocket clients. - - // Channels to register or unregister a websocket client for - // websocket notifications. - registerWSC chan *websocketClient - unregisterWSC chan *websocketClient - - // Channels read from other components from which notifications are - // created. - connectedBlocks <-chan wtxmgr.BlockMeta - disconnectedBlocks <-chan wtxmgr.BlockMeta - relevantTxs <-chan chain.RelevantTx - managerLocked <-chan bool - confirmedBalance <-chan btcutil.Amount - unconfirmedBalance <-chan btcutil.Amount - //chainServerConnected <-chan bool - registerWalletNtfns chan struct{} - - // enqueueNotification and dequeueNotification handle both sides of an - // infinitly growing queue for websocket client notifications. - enqueueNotification chan wsClientNotification - dequeueNotification chan wsClientNotification - - // notificationHandlerQuit is closed when the notification handler - // goroutine shuts down. After this is closed, no more notifications - // will be sent to any websocket client response channel. - notificationHandlerQuit chan struct{} - - wg sync.WaitGroup - quit chan struct{} - quitMtx sync.Mutex -} - -// newRPCServer creates a new server for serving RPC client connections, both -// HTTP POST and websocket. -func newRPCServer(listenAddrs []string, maxPost, maxWebsockets int64) (*rpcServer, error) { - login := cfg.Username + ":" + cfg.Password - auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login)) - s := rpcServer{ - handlerLookup: unloadedWalletHandlerFunc, - authsha: sha256.Sum256([]byte(auth)), - maxPostClients: maxPost, - maxWebsocketClients: maxWebsockets, - upgrader: websocket.Upgrader{ - // Allow all origins. - CheckOrigin: func(r *http.Request) bool { return true }, - }, - registerWSC: make(chan *websocketClient), - unregisterWSC: make(chan *websocketClient), - registerWalletNtfns: make(chan struct{}), - enqueueNotification: make(chan wsClientNotification), - dequeueNotification: make(chan wsClientNotification), - notificationHandlerQuit: make(chan struct{}), - quit: make(chan struct{}), - } - - // Setup TLS if not disabled. - listenFunc := net.Listen - if !cfg.DisableServerTLS { - // Check for existence of cert file and key file. Generate a - // new keypair if both are missing. - _, e1 := os.Stat(cfg.RPCKey) - _, e2 := os.Stat(cfg.RPCCert) - if os.IsNotExist(e1) && os.IsNotExist(e2) { - err := genCertPair(cfg.RPCCert, cfg.RPCKey) - if err != nil { - return nil, err - } - } - keypair, err := tls.LoadX509KeyPair(cfg.RPCCert, cfg.RPCKey) - if err != nil { - return nil, err - } - - tlsConfig := tls.Config{ - Certificates: []tls.Certificate{keypair}, - MinVersion: tls.VersionTLS12, - } - - // Change the standard net.Listen function to the tls one. - listenFunc = func(net string, laddr string) (net.Listener, error) { - return tls.Listen(net, laddr, &tlsConfig) - } - } else { - log.Info("Server TLS is disabled") - } - - ipv4ListenAddrs, ipv6ListenAddrs, err := parseListeners(listenAddrs) - if err != nil { - return nil, err - } - listeners := make([]net.Listener, 0, - len(ipv6ListenAddrs)+len(ipv4ListenAddrs)) - for _, addr := range ipv4ListenAddrs { - listener, err := listenFunc("tcp4", addr) - if err != nil { - log.Warnf("RPCS: Can't listen on %s: %v", addr, - err) - continue - } - listeners = append(listeners, listener) - } - - for _, addr := range ipv6ListenAddrs { - listener, err := listenFunc("tcp6", addr) - if err != nil { - log.Warnf("RPCS: Can't listen on %s: %v", addr, - err) - continue - } - listeners = append(listeners, listener) - } - if len(listeners) == 0 { - return nil, errors.New("no valid listen address") - } - - s.listeners = listeners - - return &s, nil -} - -// Start starts a HTTP server to provide standard RPC and extension -// websocket connections for any number of btcwallet clients. -func (s *rpcServer) Start() { - s.wg.Add(3) - go s.notificationListener() - go s.notificationQueue() - go s.notificationHandler() - - log.Trace("Starting RPC server") - - serveMux := http.NewServeMux() - const rpcAuthTimeoutSeconds = 10 - - httpServer := &http.Server{ - Handler: serveMux, - - // Timeout connections which don't complete the initial - // handshake within the allowed timeframe. - ReadTimeout: time.Second * rpcAuthTimeoutSeconds, - } - - serveMux.Handle("/", throttledFn(s.maxPostClients, - func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Connection", "close") - w.Header().Set("Content-Type", "application/json") - r.Close = true - - if err := s.checkAuthHeader(r); err != nil { - log.Warnf("Unauthorized client connection attempt") - http.Error(w, "401 Unauthorized.", http.StatusUnauthorized) - return - } - s.wg.Add(1) - s.PostClientRPC(w, r) - s.wg.Done() - })) - - serveMux.Handle("/ws", throttledFn(s.maxWebsocketClients, - func(w http.ResponseWriter, r *http.Request) { - authenticated := false - switch s.checkAuthHeader(r) { - case nil: - authenticated = true - case ErrNoAuth: - // nothing - default: - // If auth was supplied but incorrect, rather than simply - // being missing, immediately terminate the connection. - log.Warnf("Disconnecting improperly authorized " + - "websocket client") - http.Error(w, "401 Unauthorized.", http.StatusUnauthorized) - return - } - - conn, err := s.upgrader.Upgrade(w, r, nil) - if err != nil { - log.Warnf("Cannot websocket upgrade client %s: %v", - r.RemoteAddr, err) - return - } - wsc := newWebsocketClient(conn, authenticated, r.RemoteAddr) - s.WebsocketClientRPC(wsc) - })) - - for _, listener := range s.listeners { - s.wg.Add(1) - go func(listener net.Listener) { - log.Infof("RPCS: RPC server listening on %s", listener.Addr()) - _ = httpServer.Serve(listener) - log.Tracef("RPCS: RPC listener done for %s", listener.Addr()) - s.wg.Done() - }(listener) - } -} - -// Stop gracefully shuts down the rpc server by stopping and disconnecting all -// clients, disconnecting the chain server connection, and closing the wallet's -// account files. -func (s *rpcServer) Stop() { - s.quitMtx.Lock() - defer s.quitMtx.Unlock() - - select { - case <-s.quit: - return - default: - } - - log.Warn("Server shutting down") - - // Stop the connected wallet and chain server, if any. - s.handlerMu.Lock() - if s.wallet != nil { - s.wallet.Stop() - } - if s.chainSvr != nil { - s.chainSvr.Stop() - } - s.handlerMu.Unlock() - - // Stop all the listeners. - for _, listener := range s.listeners { - err := listener.Close() - if err != nil { - log.Errorf("Cannot close listener %s: %v", - listener.Addr(), err) - } - } - - // Signal the remaining goroutines to stop. - close(s.quit) -} - -func (s *rpcServer) WaitForShutdown() { - // First wait for the wallet and chain server to stop, if they - // were ever set. - s.handlerMu.Lock() - if s.wallet != nil { - s.wallet.WaitForShutdown() - } - if s.chainSvr != nil { - s.chainSvr.WaitForShutdown() - } - s.handlerMu.Unlock() - - s.wg.Wait() -} - -// SetWallet sets the wallet dependency component needed to run a fully -// functional bitcoin wallet RPC server. If wallet is nil, this informs the -// server that the createencryptedwallet RPC method is valid and must be called -// by a client before any other wallet methods are allowed. -func (s *rpcServer) SetWallet(wallet *wallet.Wallet) { - defer s.handlerMu.Unlock() - s.handlerMu.Lock() - - if wallet == nil { - s.handlerLookup = missingWalletHandlerFunc - s.createOK = true - return - } - - s.wallet = wallet - s.registerWalletNtfns <- struct{}{} - - if s.chainSvr != nil { - // With both the wallet and chain server set, all handlers are - // ok to run. - s.handlerLookup = lookupAnyHandler - } -} - -// SetChainServer sets the chain server client component needed to run a fully -// functional bitcoin wallet RPC server. This should be set even before the -// client is connected, as any request handlers should return the error for -// a never connected client, rather than panicking (or never being looked up) -// if the client was never conneceted and added. -func (s *rpcServer) SetChainServer(chainSvr *chain.Client) { - defer s.handlerMu.Unlock() - s.handlerMu.Lock() - - s.chainSvr = chainSvr - - if s.wallet != nil { - // With both the chain server and wallet set, all handlers are - // ok to run. - s.handlerLookup = lookupAnyHandler - } -} - -// HandlerClosure creates a closure function for handling requests of the given -// method. This may be a request that is handled directly by btcwallet, or -// a chain server request that is handled by passing the request down to btcd. -// -// NOTE: These handlers do not handle special cases, such as the authenticate -// method. Each of these must be checked beforehand (the method is already -// known) and handled accordingly. -func (s *rpcServer) HandlerClosure(method string) requestHandlerClosure { - defer s.handlerMu.Unlock() - s.handlerMu.Lock() - - // With the lock held, make copies of these pointers for the closure. - wallet := s.wallet - chainSvr := s.chainSvr - - if handler, ok := s.handlerLookup(method); ok { - return func(req *btcjson.Request) (interface{}, *btcjson.RPCError) { - cmd, err := btcjson.UnmarshalCmd(req) - if err != nil { - return nil, btcjson.ErrRPCInvalidRequest - } - res, err := handler(wallet, chainSvr, cmd) - if err != nil { - return nil, jsonError(err) - } - return res, nil - } - } - - return func(req *btcjson.Request) (interface{}, *btcjson.RPCError) { - if chainSvr == nil { - return nil, &btcjson.RPCError{ - Code: -1, - Message: "Chain server is disconnected", - } - } - res, err := chainSvr.RawRequest(req.Method, req.Params) - if err != nil { - return nil, jsonError(err) - } - return &res, nil - } -} - -// ErrNoAuth represents an error where authentication could not succeed -// due to a missing Authorization HTTP header. -var ErrNoAuth = errors.New("no auth") - -// checkAuthHeader checks the HTTP Basic authentication supplied by a client -// in the HTTP request r. It errors with ErrNoAuth if the request does not -// contain the Authorization header, or another non-nil error if the -// authentication was provided but incorrect. -// -// This check is time-constant. -func (s *rpcServer) checkAuthHeader(r *http.Request) error { - authhdr := r.Header["Authorization"] - if len(authhdr) == 0 { - return ErrNoAuth - } - - authsha := sha256.Sum256([]byte(authhdr[0])) - cmp := subtle.ConstantTimeCompare(authsha[:], s.authsha[:]) - if cmp != 1 { - return errors.New("bad auth") - } - return nil -} - -// throttledFn wraps an http.HandlerFunc with throttling of concurrent active -// clients by responding with an HTTP 429 when the threshold is crossed. -func throttledFn(threshold int64, f http.HandlerFunc) http.Handler { - return throttled(threshold, f) -} - -// throttled wraps an http.Handler with throttling of concurrent active -// clients by responding with an HTTP 429 when the threshold is crossed. -func throttled(threshold int64, h http.Handler) http.Handler { - var active int64 - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - current := atomic.AddInt64(&active, 1) - defer atomic.AddInt64(&active, -1) - - if current-1 >= threshold { - log.Warnf("Reached threshold of %d concurrent active clients", threshold) - http.Error(w, "429 Too Many Requests", 429) - return - } - - h.ServeHTTP(w, r) - }) -} - -// sanitizeRequest returns a sanitized string for the request which may be -// safely logged. It is intended to strip private keys, passphrases, and any -// other secrets from request parameters before they may be saved to a log file. -func sanitizeRequest(r *btcjson.Request) string { - // These are considered unsafe to log, so sanitize parameters. - switch r.Method { - case "encryptwallet", "importprivkey", "importwallet", - "signrawtransaction", "walletpassphrase", - "walletpassphrasechange": - - return fmt.Sprintf(`{"id":%v,"method":"%s","params":SANITIZED %d parameters}`, - r.ID, r.Method, len(r.Params)) - } - - return fmt.Sprintf(`{"id":%v,"method":"%s","params":%v}`, r.ID, - r.Method, r.Params) -} - -// idPointer returns a pointer to the passed ID, or nil if the interface is nil. -// Interface pointers are usually a red flag of doing something incorrectly, -// but this is only implemented here to work around an oddity with btcjson, -// which uses empty interface pointers for response IDs. -func idPointer(id interface{}) (p *interface{}) { - if id != nil { - p = &id - } - return -} - -// invalidAuth checks whether a websocket request is a valid (parsable) -// authenticate request and checks the supplied username and passphrase -// against the server auth. -func (s *rpcServer) invalidAuth(req *btcjson.Request) bool { - cmd, err := btcjson.UnmarshalCmd(req) - if err != nil { - return false - } - authCmd, ok := cmd.(*btcjson.AuthenticateCmd) - if !ok { - return false - } - // Check credentials. - login := authCmd.Username + ":" + authCmd.Passphrase - auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(login)) - authSha := sha256.Sum256([]byte(auth)) - return subtle.ConstantTimeCompare(authSha[:], s.authsha[:]) != 1 -} - -func (s *rpcServer) WebsocketClientRead(wsc *websocketClient) { - for { - _, request, err := wsc.conn.ReadMessage() - if err != nil { - if err != io.EOF && err != io.ErrUnexpectedEOF { - log.Warnf("Websocket receive failed from client %s: %v", - wsc.remoteAddr, err) - } - close(wsc.allRequests) - break - } - wsc.allRequests <- request - } -} - -func (s *rpcServer) WebsocketClientRespond(wsc *websocketClient) { - // A for-select with a read of the quit channel is used instead of a - // for-range to provide clean shutdown. This is necessary due to - // WebsocketClientRead (which sends to the allRequests chan) not closing - // allRequests during shutdown if the remote websocket client is still - // connected. -out: - for { - select { - case reqBytes, ok := <-wsc.allRequests: - if !ok { - // client disconnected - break out - } - - var req btcjson.Request - err := json.Unmarshal(reqBytes, &req) - if err != nil { - if !wsc.authenticated { - // Disconnect immediately. - break out - } - resp := makeResponse(req.ID, nil, - btcjson.ErrRPCInvalidRequest) - mresp, err := json.Marshal(resp) - // We expect the marshal to succeed. If it - // doesn't, it indicates some non-marshalable - // type in the response. - if err != nil { - panic(err) - } - err = wsc.send(mresp) - if err != nil { - break out - } - continue - } - - if req.Method == "authenticate" { - if wsc.authenticated || s.invalidAuth(&req) { - // Disconnect immediately. - break out - } - wsc.authenticated = true - resp := makeResponse(req.ID, nil, nil) - // Expected to never fail. - mresp, err := json.Marshal(resp) - if err != nil { - panic(err) - } - err = wsc.send(mresp) - if err != nil { - break out - } - continue - } - - if !wsc.authenticated { - // Disconnect immediately. - break out - } - - switch req.Method { - case "stop": - s.Stop() - resp := makeResponse(req.ID, - "btcwallet stopping.", nil) - mresp, err := json.Marshal(resp) - // Expected to never fail. - if err != nil { - panic(err) - } - err = wsc.send(mresp) - if err != nil { - break out - } - - default: - req := req // Copy for the closure - f := s.HandlerClosure(req.Method) - wsc.wg.Add(1) - go func() { - resp, jsonErr := f(&req) - mresp, err := btcjson.MarshalResponse(req.ID, resp, jsonErr) - if err != nil { - log.Errorf("Unable to marshal response: %v", err) - } else { - _ = wsc.send(mresp) - } - wsc.wg.Done() - }() - } - - case <-s.quit: - break out - } - } - - // Remove websocket client from notification group, or if the server is - // shutting down, wait until the notification handler has finished - // running. This is needed to ensure that no more notifications will be - // sent to the client's responses chan before it's closed below. - select { - case s.unregisterWSC <- wsc: - case <-s.quit: - <-s.notificationHandlerQuit - } - - // allow client to disconnect after all handler goroutines are done - wsc.wg.Wait() - close(wsc.responses) - s.wg.Done() -} - -func (s *rpcServer) WebsocketClientSend(wsc *websocketClient) { - const deadline time.Duration = 2 * time.Second -out: - for { - select { - case response, ok := <-wsc.responses: - if !ok { - // client disconnected - break out - } - err := wsc.conn.SetWriteDeadline(time.Now().Add(deadline)) - if err != nil { - log.Warnf("Cannot set write deadline on "+ - "client %s: %v", wsc.remoteAddr, err) - } - err = wsc.conn.WriteMessage(websocket.TextMessage, - response) - if err != nil { - log.Warnf("Failed websocket send to client "+ - "%s: %v", wsc.remoteAddr, err) - break out - } - - case <-s.quit: - break out - } - } - close(wsc.quit) - log.Infof("Disconnected websocket client %s", wsc.remoteAddr) - s.wg.Done() -} - -// WebsocketClientRPC starts the goroutines to serve JSON-RPC requests and -// notifications over a websocket connection for a single client. -func (s *rpcServer) WebsocketClientRPC(wsc *websocketClient) { - log.Infof("New websocket client %s", wsc.remoteAddr) - - // Clear the read deadline set before the websocket hijacked - // the connection. - if err := wsc.conn.SetReadDeadline(time.Time{}); err != nil { - log.Warnf("Cannot remove read deadline: %v", err) - } - - // Add client context so notifications duplicated to each - // client are received by this client. - select { - case s.registerWSC <- wsc: - case <-s.quit: - return - } - - // WebsocketClientRead is intentionally not run with the waitgroup - // so it is ignored during shutdown. This is to prevent a hang during - // shutdown where the goroutine is blocked on a read of the - // websocket connection if the client is still connected. - go s.WebsocketClientRead(wsc) - - s.wg.Add(2) - go s.WebsocketClientRespond(wsc) - go s.WebsocketClientSend(wsc) - - <-wsc.quit -} - -// maxRequestSize specifies the maximum number of bytes in the request body -// that may be read from a client. This is currently limited to 4MB. -const maxRequestSize = 1024 * 1024 * 4 - -// PostClientRPC processes and replies to a JSON-RPC client request. -func (s *rpcServer) PostClientRPC(w http.ResponseWriter, r *http.Request) { - body := http.MaxBytesReader(w, r.Body, maxRequestSize) - rpcRequest, err := ioutil.ReadAll(body) - if err != nil { - // TODO: what if the underlying reader errored? - http.Error(w, "413 Request Too Large.", - http.StatusRequestEntityTooLarge) - return - } - - // First check whether wallet has a handler for this request's method. - // If unfound, the request is sent to the chain server for further - // processing. While checking the methods, disallow authenticate - // requests, as they are invalid for HTTP POST clients. - var req btcjson.Request - err = json.Unmarshal(rpcRequest, &req) - if err != nil { - resp, err := btcjson.MarshalResponse(req.ID, nil, btcjson.ErrRPCInvalidRequest) - if err != nil { - log.Errorf("Unable to marshal response: %v", err) - http.Error(w, "500 Internal Server Error", - http.StatusInternalServerError) - return - } - _, err = w.Write(resp) - if err != nil { - log.Warnf("Cannot write invalid request request to "+ - "client: %v", err) - } - return - } - - // Create the response and error from the request. Two special cases - // are handled for the authenticate and stop request methods. - var res interface{} - var jsonErr *btcjson.RPCError - switch req.Method { - case "authenticate": - // Drop it. - return - case "stop": - s.Stop() - res = "btcwallet stopping" - default: - res, jsonErr = s.HandlerClosure(req.Method)(&req) - } - - // Marshal and send. - mresp, err := btcjson.MarshalResponse(req.ID, res, jsonErr) - if err != nil { - log.Errorf("Unable to marshal response: %v", err) - http.Error(w, "500 Internal Server Error", http.StatusInternalServerError) - return - } - _, err = w.Write(mresp) - if err != nil { - log.Warnf("Unable to respond to client: %v", err) - } -} - -// Notification messages for websocket clients. -type ( - wsClientNotification interface { - // This returns a slice only because some of these types result - // in multpile client notifications. - notificationCmds(w *wallet.Wallet) []interface{} - } - - blockConnected wtxmgr.BlockMeta - blockDisconnected wtxmgr.BlockMeta - - relevantTx chain.RelevantTx - - managerLocked bool - - confirmedBalance btcutil.Amount - unconfirmedBalance btcutil.Amount - - btcdConnected bool -) - -func (b blockConnected) notificationCmds(w *wallet.Wallet) []interface{} { - n := btcjson.NewBlockConnectedNtfn(b.Hash.String(), b.Height, b.Time.Unix()) - return []interface{}{n} -} - -func (b blockDisconnected) notificationCmds(w *wallet.Wallet) []interface{} { - n := btcjson.NewBlockDisconnectedNtfn(b.Hash.String(), b.Height, b.Time.Unix()) - return []interface{}{n} -} - -func (t relevantTx) notificationCmds(w *wallet.Wallet) []interface{} { - syncBlock := w.Manager.SyncedTo() - - var block *wtxmgr.Block - if t.Block != nil { - block = &t.Block.Block - } - details, err := w.TxStore.UniqueTxDetails(&t.TxRecord.Hash, block) - if err != nil { - log.Errorf("Cannot fetch transaction details for "+ - "client notification: %v", err) - return nil - } - if details == nil { - log.Errorf("No details found for client transaction notification") - return nil - } - - ltr := wallet.ListTransactions(details, syncBlock.Height, activeNet.Params) - ntfns := make([]interface{}, len(ltr)) - for i := range ntfns { - ntfns[i] = btcjson.NewNewTxNtfn(ltr[i].Account, ltr[i]) - } - return ntfns -} - -func (l managerLocked) notificationCmds(w *wallet.Wallet) []interface{} { - n := btcjson.NewWalletLockStateNtfn(bool(l)) - return []interface{}{n} -} - -func (b confirmedBalance) notificationCmds(w *wallet.Wallet) []interface{} { - n := btcjson.NewAccountBalanceNtfn("", - btcutil.Amount(b).ToBTC(), true) - return []interface{}{n} -} - -func (b unconfirmedBalance) notificationCmds(w *wallet.Wallet) []interface{} { - n := btcjson.NewAccountBalanceNtfn("", - btcutil.Amount(b).ToBTC(), false) - return []interface{}{n} -} - -func (b btcdConnected) notificationCmds(w *wallet.Wallet) []interface{} { - n := btcjson.NewBtcdConnectedNtfn(bool(b)) - return []interface{}{n} -} - -func (s *rpcServer) notificationListener() { -out: - for { - select { - case n := <-s.connectedBlocks: - s.enqueueNotification <- blockConnected(n) - case n := <-s.disconnectedBlocks: - s.enqueueNotification <- blockDisconnected(n) - case n := <-s.relevantTxs: - s.enqueueNotification <- relevantTx(n) - case n := <-s.managerLocked: - s.enqueueNotification <- managerLocked(n) - case n := <-s.confirmedBalance: - s.enqueueNotification <- confirmedBalance(n) - case n := <-s.unconfirmedBalance: - s.enqueueNotification <- unconfirmedBalance(n) - - // Registration of all notifications is done by the handler so - // it doesn't require another rpcServer mutex. - case <-s.registerWalletNtfns: - connectedBlocks, err := s.wallet.ListenConnectedBlocks() - if err != nil { - log.Errorf("Could not register for new "+ - "connected block notifications: %v", - err) - continue - } - disconnectedBlocks, err := s.wallet.ListenDisconnectedBlocks() - if err != nil { - log.Errorf("Could not register for new "+ - "disconnected block notifications: %v", - err) - continue - } - relevantTxs, err := s.wallet.ListenRelevantTxs() - if err != nil { - log.Errorf("Could not register for new relevant "+ - "transaction notifications: %v", err) - continue - } - managerLocked, err := s.wallet.ListenLockStatus() - if err != nil { - log.Errorf("Could not register for manager "+ - "lock state changes: %v", err) - continue - } - confirmedBalance, err := s.wallet.ListenConfirmedBalance() - if err != nil { - log.Errorf("Could not register for confirmed "+ - "balance changes: %v", err) - continue - } - unconfirmedBalance, err := s.wallet.ListenUnconfirmedBalance() - if err != nil { - log.Errorf("Could not register for unconfirmed "+ - "balance changes: %v", err) - continue - } - s.connectedBlocks = connectedBlocks - s.disconnectedBlocks = disconnectedBlocks - s.relevantTxs = relevantTxs - s.managerLocked = managerLocked - s.confirmedBalance = confirmedBalance - s.unconfirmedBalance = unconfirmedBalance - - case <-s.quit: - break out - } - } - close(s.enqueueNotification) - go s.drainNotifications() - s.wg.Done() -} - -func (s *rpcServer) drainNotifications() { - for { - select { - case <-s.connectedBlocks: - case <-s.disconnectedBlocks: - case <-s.relevantTxs: - case <-s.managerLocked: - case <-s.confirmedBalance: - case <-s.unconfirmedBalance: - case <-s.registerWalletNtfns: - } - } -} - -// notificationQueue manages an infinitly-growing queue of notifications that -// wallet websocket clients may be interested in. It quits when the -// enqueueNotification channel is closed, dropping any still pending -// notifications. -func (s *rpcServer) notificationQueue() { - var q []wsClientNotification - var dequeue chan<- wsClientNotification - skipQueue := s.dequeueNotification - var next wsClientNotification -out: - for { - select { - case n, ok := <-s.enqueueNotification: - if !ok { - // Sender closed input channel. - break out - } - - // Either send to out immediately if skipQueue is - // non-nil (queue is empty) and reader is ready, - // or append to the queue and send later. - select { - case skipQueue <- n: - default: - q = append(q, n) - dequeue = s.dequeueNotification - skipQueue = nil - next = q[0] - } - - case dequeue <- next: - q[0] = nil // avoid leak - q = q[1:] - if len(q) == 0 { - dequeue = nil - skipQueue = s.dequeueNotification - } else { - next = q[0] - } - } - } - close(s.dequeueNotification) - s.wg.Done() -} - -func (s *rpcServer) notificationHandler() { - clients := make(map[chan struct{}]*websocketClient) -out: - for { - select { - case c := <-s.registerWSC: - clients[c.quit] = c - - case c := <-s.unregisterWSC: - delete(clients, c.quit) - - case nmsg, ok := <-s.dequeueNotification: - // No more notifications. - if !ok { - break out - } - - // Ignore if there are no clients to receive the - // notification. - if len(clients) == 0 { - continue - } - - ns := nmsg.notificationCmds(s.wallet) - for _, n := range ns { - mn, err := btcjson.MarshalCmd(nil, n) - // All notifications are expected to be - // marshalable. - if err != nil { - panic(err) - } - for _, c := range clients { - if err := c.send(mn); err != nil { - delete(clients, c.quit) - } - } - } - - case <-s.quit: - break out - } - } - close(s.notificationHandlerQuit) - s.wg.Done() -} - -// requestHandler is a handler function to handle an unmarshaled and parsed -// request into a marshalable response. If the error is a *btcjson.RPCError -// or any of the above special error classes, the server will respond with -// the JSON-RPC appropiate error code. All other errors use the wallet -// catch-all error code, btcjson.ErrRPCWallet. -type requestHandler func(*wallet.Wallet, *chain.Client, interface{}) (interface{}, error) - -var rpcHandlers = map[string]struct { - handler requestHandler - - // Function variables cannot be compared against anything but nil, so - // use a boolean to record whether help generation is necessary. This - // is used by the tests to ensure that help can be generated for every - // implemented method. - // - // A single map and this bool is here is used rather than several maps - // for the unimplemented handlers so every method has exactly one - // handler function. - noHelp bool -}{ - // Reference implementation wallet methods (implemented) - "addmultisigaddress": {handler: AddMultiSigAddress}, - "createmultisig": {handler: CreateMultiSig}, - "dumpprivkey": {handler: DumpPrivKey}, - "getaccount": {handler: GetAccount}, - "getaccountaddress": {handler: GetAccountAddress}, - "getaddressesbyaccount": {handler: GetAddressesByAccount}, - "getbalance": {handler: GetBalance}, - "getbestblockhash": {handler: GetBestBlockHash}, - "getblockcount": {handler: GetBlockCount}, - "getinfo": {handler: GetInfo}, - "getnewaddress": {handler: GetNewAddress}, - "getrawchangeaddress": {handler: GetRawChangeAddress}, - "getreceivedbyaccount": {handler: GetReceivedByAccount}, - "getreceivedbyaddress": {handler: GetReceivedByAddress}, - "gettransaction": {handler: GetTransaction}, - "help": {handler: Help}, - "importprivkey": {handler: ImportPrivKey}, - "keypoolrefill": {handler: KeypoolRefill}, - "listaccounts": {handler: ListAccounts}, - "listlockunspent": {handler: ListLockUnspent}, - "listreceivedbyaccount": {handler: ListReceivedByAccount}, - "listreceivedbyaddress": {handler: ListReceivedByAddress}, - "listsinceblock": {handler: ListSinceBlock}, - "listtransactions": {handler: ListTransactions}, - "listunspent": {handler: ListUnspent}, - "lockunspent": {handler: LockUnspent}, - "sendfrom": {handler: SendFrom}, - "sendmany": {handler: SendMany}, - "sendtoaddress": {handler: SendToAddress}, - "settxfee": {handler: SetTxFee}, - "signmessage": {handler: SignMessage}, - "signrawtransaction": {handler: SignRawTransaction}, - "validateaddress": {handler: ValidateAddress}, - "verifymessage": {handler: VerifyMessage}, - "walletlock": {handler: WalletLock}, - "walletpassphrase": {handler: WalletPassphrase}, - "walletpassphrasechange": {handler: WalletPassphraseChange}, - - // Reference implementation methods (still unimplemented) - "backupwallet": {handler: Unimplemented, noHelp: true}, - "dumpwallet": {handler: Unimplemented, noHelp: true}, - "getwalletinfo": {handler: Unimplemented, noHelp: true}, - "importwallet": {handler: Unimplemented, noHelp: true}, - "listaddressgroupings": {handler: Unimplemented, noHelp: true}, - - // Reference methods which can't be implemented by btcwallet due to - // design decision differences - "encryptwallet": {handler: Unsupported, noHelp: true}, - "move": {handler: Unsupported, noHelp: true}, - "setaccount": {handler: Unsupported, noHelp: true}, - - // Extensions to the reference client JSON-RPC API - "createnewaccount": {handler: CreateNewAccount}, - "exportwatchingwallet": {handler: ExportWatchingWallet}, - "getbestblock": {handler: GetBestBlock}, - // This was an extension but the reference implementation added it as - // well, but with a different API (no account parameter). It's listed - // here because it hasn't been update to use the reference - // implemenation's API. - "getunconfirmedbalance": {handler: GetUnconfirmedBalance}, - "listaddresstransactions": {handler: ListAddressTransactions}, - "listalltransactions": {handler: ListAllTransactions}, - "renameaccount": {handler: RenameAccount}, - "walletislocked": {handler: WalletIsLocked}, -} - -// Unimplemented handles an unimplemented RPC request with the -// appropiate error. -func Unimplemented(*wallet.Wallet, *chain.Client, interface{}) (interface{}, error) { - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCUnimplemented, - Message: "Method unimplemented", - } -} - -// Unsupported handles a standard bitcoind RPC request which is -// unsupported by btcwallet due to design differences. -func Unsupported(*wallet.Wallet, *chain.Client, interface{}) (interface{}, error) { - return nil, &btcjson.RPCError{ - Code: -1, - Message: "Request unsupported by btcwallet", - } -} - -// UnloadedWallet is the handler func that is run when a wallet has not been -// loaded yet when trying to execute a wallet RPC. -func UnloadedWallet(*wallet.Wallet, *chain.Client, interface{}) (interface{}, error) { - return nil, &ErrUnloadedWallet -} - -// NoEncryptedWallet is the handler func that is run when no wallet has been -// created by the user yet. -// loaded yet when trying to execute a wallet RPC. -func NoEncryptedWallet(*wallet.Wallet, *chain.Client, interface{}) (interface{}, error) { - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCWallet, - Message: "Request requires a wallet but no wallet has been " + - "created -- use createencryptedwallet to recover", - } -} - -// TODO(jrick): may be a good idea to add handlers for passthrough to the chain -// server. If a handler can not be looked up in one of the above maps, use this -// passthrough handler instead. This isn't done at the moment since all -// requests are executed serialized, and blocking all requests, and even just -// requests from the same client, on the result of a btcd RPC can result is too -// much waiting for the round trip. - -// lookupAnyHandler looks up a request handler func for the passed method from -// the http post and (if the request is from a websocket connection) websocket -// handler maps. If a suitable handler could not be found, ok is false. -func lookupAnyHandler(method string) (f requestHandler, ok bool) { - handlerData, ok := rpcHandlers[method] - f = handlerData.handler - return -} - -// unloadedWalletHandlerFunc looks up whether a request requires a wallet, and -// if so, returns a specialized handler func to return errors for an unloaded -// wallet component necessary to complete the request. If ok is false, the -// function is invalid and should be passed through instead. -func unloadedWalletHandlerFunc(method string) (f requestHandler, ok bool) { - _, ok = rpcHandlers[method] - if ok { - f = UnloadedWallet - } - return -} - -// missingWalletHandlerFunc looks up whether a request requires a wallet, and -// if so, returns a specialized handler func to return errors for no wallets -// being created yet with the createencryptedwallet RPC. If ok is false, the -// function is invalid and should be passed through instead. -func missingWalletHandlerFunc(method string) (f requestHandler, ok bool) { - _, ok = rpcHandlers[method] - if ok { - f = NoEncryptedWallet - } - return -} - -// requestHandlerClosure is a closure over a requestHandler or passthrough -// request with the RPC server's wallet and chain server variables as part -// of the closure context. -type requestHandlerClosure func(*btcjson.Request) (interface{}, *btcjson.RPCError) - -// makeResponse makes the JSON-RPC response struct for the result and error -// returned by a requestHandler. The returned response is not ready for -// marshaling and sending off to a client, but must be -func makeResponse(id, result interface{}, err error) btcjson.Response { - idPtr := idPointer(id) - if err != nil { - return btcjson.Response{ - ID: idPtr, - Error: jsonError(err), - } - } - resultBytes, err := json.Marshal(result) - if err != nil { - return btcjson.Response{ - ID: idPtr, - Error: &btcjson.RPCError{ - Code: btcjson.ErrRPCInternal.Code, - Message: "Unexpected error marshalling result", - }, - } - } - return btcjson.Response{ - ID: idPtr, - Result: json.RawMessage(resultBytes), - } -} - -// jsonError creates a JSON-RPC error from the Go error. -func jsonError(err error) *btcjson.RPCError { - if err == nil { - return nil - } - - code := btcjson.ErrRPCWallet - switch e := err.(type) { - case btcjson.RPCError: - return &e - case *btcjson.RPCError: - return e - case DeserializationError: - code = btcjson.ErrRPCDeserialization - case InvalidParameterError: - code = btcjson.ErrRPCInvalidParameter - case ParseError: - code = btcjson.ErrRPCParse.Code - case waddrmgr.ManagerError: - switch e.ErrorCode { - case waddrmgr.ErrWrongPassphrase: - code = btcjson.ErrRPCWalletPassphraseIncorrect - } - } - return &btcjson.RPCError{ - Code: code, - Message: err.Error(), - } -} - -// makeMultiSigScript is a helper function to combine common logic for -// AddMultiSig and CreateMultiSig. -// all error codes are rpc parse error here to match bitcoind which just throws -// a runtime exception. *sigh*. -func makeMultiSigScript(w *wallet.Wallet, keys []string, nRequired int) ([]byte, error) { - keysesPrecious := make([]*btcutil.AddressPubKey, len(keys)) - - // The address list will made up either of addreseses (pubkey hash), for - // which we need to look up the keys in wallet, straight pubkeys, or a - // mixture of the two. - for i, a := range keys { - // try to parse as pubkey address - a, err := decodeAddress(a, activeNet.Params) - if err != nil { - return nil, err - } - - switch addr := a.(type) { - case *btcutil.AddressPubKey: - keysesPrecious[i] = addr - case *btcutil.AddressPubKeyHash: - ainfo, err := w.Manager.Address(addr) - if err != nil { - return nil, err - } - - apkinfo := ainfo.(waddrmgr.ManagedPubKeyAddress) - - // This will be an addresspubkey - a, err := decodeAddress(apkinfo.ExportPubKey(), - activeNet.Params) - if err != nil { - return nil, err - } - - apk := a.(*btcutil.AddressPubKey) - keysesPrecious[i] = apk + switch { + case ip == nil: + log.Warnf("`%s` is not a valid IP address", host) + case ip.To4() == nil: + ipv6Addrs = append(ipv6Addrs, addr) default: - return nil, err + ipv4Addrs = append(ipv4Addrs, addr) } } - - return txscript.MultiSigScript(keysesPrecious, nRequired) -} - -// AddMultiSigAddress handles an addmultisigaddress request by adding a -// multisig address to the given wallet. -func AddMultiSigAddress(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.AddMultisigAddressCmd) - - // If an account is specified, ensure that is the imported account. - if cmd.Account != nil && *cmd.Account != waddrmgr.ImportedAddrAccountName { - return nil, &ErrNotImportedAccount - } - - script, err := makeMultiSigScript(w, cmd.Keys, cmd.NRequired) - if err != nil { - return nil, ParseError{err} - } - - // TODO(oga) blockstamp current block? - bs := &waddrmgr.BlockStamp{ - Hash: *activeNet.Params.GenesisHash, - Height: 0, - } - - addr, err := w.Manager.ImportScript(script, bs) - if err != nil { - return nil, err - } - - return addr.Address().EncodeAddress(), nil -} - -// CreateMultiSig handles an createmultisig request by returning a -// multisig address for the given inputs. -func CreateMultiSig(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.CreateMultisigCmd) - - script, err := makeMultiSigScript(w, cmd.Keys, cmd.NRequired) - if err != nil { - return nil, ParseError{err} - } - - address, err := btcutil.NewAddressScriptHash(script, activeNet.Params) - if err != nil { - // above is a valid script, shouldn't happen. - return nil, err - } - - return btcjson.CreateMultiSigResult{ - Address: address.EncodeAddress(), - RedeemScript: hex.EncodeToString(script), - }, nil -} - -// DumpPrivKey handles a dumpprivkey request with the private key -// for a single address, or an appropiate error if the wallet -// is locked. -func DumpPrivKey(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.DumpPrivKeyCmd) - - addr, err := decodeAddress(cmd.Address, activeNet.Params) - if err != nil { - return nil, err - } - - key, err := w.DumpWIFPrivateKey(addr) - if waddrmgr.IsError(err, waddrmgr.ErrLocked) { - // Address was found, but the private key isn't - // accessible. - return nil, &ErrWalletUnlockNeeded - } - return key, err -} - -// DumpWallet handles a dumpwallet request by returning all private -// keys in a wallet, or an appropiate error if the wallet is locked. -// TODO: finish this to match bitcoind by writing the dump to a file. -func DumpWallet(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - keys, err := w.DumpPrivKeys() - if waddrmgr.IsError(err, waddrmgr.ErrLocked) { - return nil, &ErrWalletUnlockNeeded - } - - return keys, err -} - -// ExportWatchingWallet handles an exportwatchingwallet request by exporting the -// current wallet as a watching wallet (with no private keys), and returning -// base64-encoding of serialized account files. -func ExportWatchingWallet(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.ExportWatchingWalletCmd) - - if cmd.Account != nil && *cmd.Account != "*" { - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCWallet, - Message: "Individual accounts can not be exported as watching-only", - } - } - - return w.ExportWatchingWallet(cfg.WalletPass) -} - -// GetAddressesByAccount handles a getaddressesbyaccount request by returning -// all addresses for an account, or an error if the requested account does -// not exist. -func GetAddressesByAccount(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.GetAddressesByAccountCmd) - - account, err := w.Manager.LookupAccount(cmd.Account) - if err != nil { - return nil, err - } - - var addrStrs []string - err = w.Manager.ForEachAccountAddress(account, - func(maddr waddrmgr.ManagedAddress) error { - addrStrs = append(addrStrs, maddr.Address().EncodeAddress()) - return nil - }) - return addrStrs, err -} - -// GetBalance handles a getbalance request by returning the balance for an -// account (wallet), or an error if the requested account does not -// exist. -func GetBalance(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.GetBalanceCmd) - - var balance btcutil.Amount - var err error - accountName := "*" - if cmd.Account != nil { - accountName = *cmd.Account - } - if accountName == "*" { - balance, err = w.CalculateBalance(int32(*cmd.MinConf)) - } else { - var account uint32 - account, err = w.Manager.LookupAccount(accountName) + listeners := make([]net.Listener, 0, len(ipv6Addrs)+len(ipv4Addrs)) + for _, addr := range ipv4Addrs { + listener, err := listen("tcp4", addr) if err != nil { - return nil, err - } - balance, err = w.CalculateAccountBalance(account, int32(*cmd.MinConf)) - } - if err != nil { - return nil, err - } - return balance.ToBTC(), nil -} - -// GetBestBlock handles a getbestblock request by returning a JSON object -// with the height and hash of the most recently processed block. -func GetBestBlock(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - blk := w.Manager.SyncedTo() - result := &btcjson.GetBestBlockResult{ - Hash: blk.Hash.String(), - Height: blk.Height, - } - return result, nil -} - -// GetBestBlockHash handles a getbestblockhash request by returning the hash -// of the most recently processed block. -func GetBestBlockHash(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - blk := w.Manager.SyncedTo() - return blk.Hash.String(), nil -} - -// GetBlockCount handles a getblockcount request by returning the chain height -// of the most recently processed block. -func GetBlockCount(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - blk := w.Manager.SyncedTo() - return blk.Height, nil -} - -// GetInfo handles a getinfo request by returning the a structure containing -// information about the current state of btcwallet. -// exist. -func GetInfo(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - // Call down to btcd for all of the information in this command known - // by them. - info, err := chainSvr.GetInfo() - if err != nil { - return nil, err - } - - bal, err := w.CalculateBalance(1) - if err != nil { - return nil, err - } - - // TODO(davec): This should probably have a database version as opposed - // to using the manager version. - info.WalletVersion = int32(waddrmgr.LatestMgrVersion) - info.Balance = bal.ToBTC() - info.PaytxFee = w.FeeIncrement.ToBTC() - // We don't set the following since they don't make much sense in the - // wallet architecture: - // - unlocked_until - // - errors - - return info, nil -} - -func decodeAddress(s string, params *chaincfg.Params) (btcutil.Address, error) { - addr, err := btcutil.DecodeAddress(s, params) - if err != nil { - msg := fmt.Sprintf("Invalid address %q: decode failed with %#q", s, err) - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCInvalidAddressOrKey, - Message: msg, - } - } - if !addr.IsForNet(activeNet.Params) { - msg := fmt.Sprintf("Invalid address %q: not intended for use on %s", - addr, params.Name) - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCInvalidAddressOrKey, - Message: msg, - } - } - return addr, nil -} - -// GetAccount handles a getaccount request by returning the account name -// associated with a single address. -func GetAccount(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.GetAccountCmd) - - addr, err := decodeAddress(cmd.Address, activeNet.Params) - if err != nil { - return nil, err - } - - // Fetch the associated account - account, err := w.Manager.AddrAccount(addr) - if err != nil { - return nil, &ErrAddressNotInWallet - } - - acctName, err := w.Manager.AccountName(account) - if err != nil { - return nil, &ErrAccountNameNotFound - } - return acctName, nil -} - -// GetAccountAddress handles a getaccountaddress by returning the most -// recently-created chained address that has not yet been used (does not yet -// appear in the blockchain, or any tx that has arrived in the btcd mempool). -// If the most recently-requested address has been used, a new address (the -// next chained address in the keypool) is used. This can fail if the keypool -// runs out (and will return btcjson.ErrRPCWalletKeypoolRanOut if that happens). -func GetAccountAddress(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.GetAccountAddressCmd) - - account, err := w.Manager.LookupAccount(cmd.Account) - if err != nil { - return nil, err - } - addr, err := w.CurrentAddress(account) - if err != nil { - return nil, err - } - - return addr.EncodeAddress(), err -} - -// GetUnconfirmedBalance handles a getunconfirmedbalance extension request -// by returning the current unconfirmed balance of an account. -func GetUnconfirmedBalance(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.GetUnconfirmedBalanceCmd) - - acctName := "default" - if cmd.Account != nil { - acctName = *cmd.Account - } - account, err := w.Manager.LookupAccount(acctName) - if err != nil { - return nil, err - } - unconfirmed, err := w.CalculateAccountBalance(account, 0) - if err != nil { - return nil, err - } - confirmed, err := w.CalculateAccountBalance(account, 1) - if err != nil { - return nil, err - } - - return (unconfirmed - confirmed).ToBTC(), nil -} - -// ImportPrivKey handles an importprivkey request by parsing -// a WIF-encoded private key and adding it to an account. -func ImportPrivKey(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.ImportPrivKeyCmd) - - // Ensure that private keys are only imported to the correct account. - // - // Yes, Label is the account name. - if cmd.Label != nil && *cmd.Label != waddrmgr.ImportedAddrAccountName { - return nil, &ErrNotImportedAccount - } - - wif, err := btcutil.DecodeWIF(cmd.PrivKey) - if err != nil { - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCInvalidAddressOrKey, - Message: "WIF decode failed: " + err.Error(), - } - } - if !wif.IsForNet(activeNet.Params) { - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCInvalidAddressOrKey, - Message: "Key is not intended for " + activeNet.Params.Name, - } - } - - // Import the private key, handling any errors. - _, err = w.ImportPrivateKey(wif, nil, *cmd.Rescan) - switch { - case waddrmgr.IsError(err, waddrmgr.ErrDuplicateAddress): - // Do not return duplicate key errors to the client. - return nil, nil - case waddrmgr.IsError(err, waddrmgr.ErrLocked): - return nil, &ErrWalletUnlockNeeded - } - - return nil, err -} - -// KeypoolRefill handles the keypoolrefill command. Since we handle the keypool -// automatically this does nothing since refilling is never manually required. -func KeypoolRefill(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - return nil, nil -} - -// CreateNewAccount handles a createnewaccount request by creating and -// returning a new account. If the last account has no transaction history -// as per BIP 0044 a new account cannot be created so an error will be returned. -func CreateNewAccount(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.CreateNewAccountCmd) - - // The wildcard * is reserved by the rpc server with the special meaning - // of "all accounts", so disallow naming accounts to this string. - if cmd.Account == "*" { - return nil, &ErrReservedAccountName - } - - // Check that we are within the maximum allowed non-empty accounts limit. - account, err := w.Manager.LastAccount() - if err != nil { - return nil, err - } - if account > maxEmptyAccounts { - used, err := w.AccountUsed(account) - if err != nil { - return nil, err - } - if !used { - return nil, errors.New("cannot create account: " + - "previous account has no transaction history") - } - } - - _, err = w.Manager.NewAccount(cmd.Account) - if waddrmgr.IsError(err, waddrmgr.ErrLocked) { - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCWalletUnlockNeeded, - Message: "Creating an account requires the wallet to be unlocked. " + - "Enter the wallet passphrase with walletpassphrase to unlock", - } - } - return nil, err -} - -// RenameAccount handles a renameaccount request by renaming an account. -// If the account does not exist an appropiate error will be returned. -func RenameAccount(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.RenameAccountCmd) - - // The wildcard * is reserved by the rpc server with the special meaning - // of "all accounts", so disallow naming accounts to this string. - if cmd.NewAccount == "*" { - return nil, &ErrReservedAccountName - } - - // Check that given account exists - account, err := w.Manager.LookupAccount(cmd.OldAccount) - if err != nil { - return nil, err - } - return nil, w.Manager.RenameAccount(account, cmd.NewAccount) -} - -// GetNewAddress handles a getnewaddress request by returning a new -// address for an account. If the account does not exist an appropiate -// error is returned. -// TODO: Follow BIP 0044 and warn if number of unused addresses exceeds -// the gap limit. -func GetNewAddress(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.GetNewAddressCmd) - - acctName := "default" - if cmd.Account != nil { - acctName = *cmd.Account - } - account, err := w.Manager.LookupAccount(acctName) - if err != nil { - return nil, err - } - addr, err := w.NewAddress(account) - if err != nil { - return nil, err - } - - // Return the new payment address string. - return addr.EncodeAddress(), nil -} - -// GetRawChangeAddress handles a getrawchangeaddress request by creating -// and returning a new change address for an account. -// -// Note: bitcoind allows specifying the account as an optional parameter, -// but ignores the parameter. -func GetRawChangeAddress(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.GetRawChangeAddressCmd) - - acctName := "default" - if cmd.Account != nil { - acctName = *cmd.Account - } - account, err := w.Manager.LookupAccount(acctName) - if err != nil { - return nil, err - } - addr, err := w.NewChangeAddress(account) - if err != nil { - return nil, err - } - - // Return the new payment address string. - return addr.EncodeAddress(), nil -} - -// GetReceivedByAccount handles a getreceivedbyaccount request by returning -// the total amount received by addresses of an account. -func GetReceivedByAccount(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.GetReceivedByAccountCmd) - - account, err := w.Manager.LookupAccount(cmd.Account) - if err != nil { - return nil, err - } - - bal, _, err := w.TotalReceivedForAccount(account, int32(*cmd.MinConf)) - if err != nil { - return nil, err - } - - return bal.ToBTC(), nil -} - -// GetReceivedByAddress handles a getreceivedbyaddress request by returning -// the total amount received by a single address. -func GetReceivedByAddress(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.GetReceivedByAddressCmd) - - addr, err := decodeAddress(cmd.Address, activeNet.Params) - if err != nil { - return nil, err - } - total, err := w.TotalReceivedForAddr(addr, int32(*cmd.MinConf)) - if err != nil { - return nil, err - } - - return total.ToBTC(), nil -} - -// GetTransaction handles a gettransaction request by returning details about -// a single transaction saved by wallet. -func GetTransaction(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.GetTransactionCmd) - - txSha, err := wire.NewShaHashFromStr(cmd.Txid) - if err != nil { - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCDecodeHexString, - Message: "Transaction hash string decode failed: " + err.Error(), - } - } - - details, err := w.TxStore.TxDetails(txSha) - if err != nil { - return nil, err - } - if details == nil { - return nil, &ErrNoTransactionInfo - } - - syncBlock := w.Manager.SyncedTo() - - // TODO: The serialized transaction is already in the DB, so - // reserializing can be avoided here. - var txBuf bytes.Buffer - txBuf.Grow(details.MsgTx.SerializeSize()) - err = details.MsgTx.Serialize(&txBuf) - if err != nil { - return nil, err - } - - // TODO: Add a "generated" field to this result type. "generated":true - // is only added if the transaction is a coinbase. - ret := btcjson.GetTransactionResult{ - TxID: cmd.Txid, - Hex: hex.EncodeToString(txBuf.Bytes()), - Time: details.Received.Unix(), - TimeReceived: details.Received.Unix(), - WalletConflicts: []string{}, // Not saved - //Generated: blockchain.IsCoinBaseTx(&details.MsgTx), - } - - if details.Block.Height != -1 { - ret.BlockHash = details.Block.Hash.String() - ret.BlockTime = details.Block.Time.Unix() - ret.Confirmations = int64(confirms(details.Block.Height, syncBlock.Height)) - } - - var ( - debitTotal btcutil.Amount - creditTotal btcutil.Amount // Excludes change - outputTotal btcutil.Amount - fee btcutil.Amount - feeF64 float64 - ) - for _, deb := range details.Debits { - debitTotal += deb.Amount - } - for _, cred := range details.Credits { - if !cred.Change { - creditTotal += cred.Amount - } - } - for _, output := range details.MsgTx.TxOut { - outputTotal -= btcutil.Amount(output.Value) - } - // Fee can only be determined if every input is a debit. - if len(details.Debits) == len(details.MsgTx.TxIn) { - fee = debitTotal - outputTotal - feeF64 = fee.ToBTC() - } - - if len(details.Debits) == 0 { - // Credits must be set later, but since we know the full length - // of the details slice, allocate it with the correct cap. - ret.Details = make([]btcjson.GetTransactionDetailsResult, 0, len(details.Credits)) - } else { - ret.Details = make([]btcjson.GetTransactionDetailsResult, 1, len(details.Credits)+1) - - ret.Details[0] = btcjson.GetTransactionDetailsResult{ - // Fields left zeroed: - // InvolvesWatchOnly - // Account - // Address - // Vout - // - // TODO(jrick): Address and Vout should always be set, - // but we're doing the wrong thing here by not matching - // core. Instead, gettransaction should only be adding - // details for transaction outputs, just like - // listtransactions (but using the short result format). - Category: "send", - Amount: (-debitTotal).ToBTC(), // negative since it is a send - Fee: &feeF64, - } - ret.Fee = feeF64 - } - - credCat := wallet.RecvCategory(details, syncBlock.Height).String() - for _, cred := range details.Credits { - // Change is ignored. - if cred.Change { + log.Warnf("Can't listen on %s: %v", addr, err) continue } - - var addr string - _, addrs, _, err := txscript.ExtractPkScriptAddrs( - details.MsgTx.TxOut[cred.Index].PkScript, activeNet.Params) - if err == nil && len(addrs) == 1 { - addr = addrs[0].EncodeAddress() - } - - ret.Details = append(ret.Details, btcjson.GetTransactionDetailsResult{ - // Fields left zeroed: - // InvolvesWatchOnly - // Account - // Fee - Address: addr, - Category: credCat, - Amount: cred.Amount.ToBTC(), - Vout: cred.Index, - }) + listeners = append(listeners, listener) } - - ret.Amount = creditTotal.ToBTC() - return ret, nil -} - -// These generators create the following global variables in this package: -// -// var localeHelpDescs map[string]func() map[string]string -// var requestUsages string -// -// localeHelpDescs maps from locale strings (e.g. "en_US") to a function that -// builds a map of help texts for each RPC server method. This prevents help -// text maps for every locale map from being rooted and created during init. -// Instead, the appropiate function is looked up when help text is first needed -// using the current locale and saved to the global below for futher reuse. -// -// requestUsages contains single line usages for every supported request, -// separated by newlines. It is set during init. These usages are used for all -// locales. -// -//go:generate go run internal/rpchelp/genrpcserverhelp.go -tags generate -//go:generate gofmt -w rpcserverhelp.go - -var helpDescs map[string]string -var helpDescsMu sync.Mutex // Help may execute concurrently, so synchronize access. - -// Help handles the help request by returning one line usage of all available -// methods, or full help for a specific method. -func Help(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.HelpCmd) - - // btcd returns different help messages depending on the kind of - // connection the client is using. Only methods availble to HTTP POST - // clients are available to be used by wallet clients, even though - // wallet itself is a websocket client to btcd. Therefore, create a - // POST client as needed. - // - // Returns nil if chainSvr is currently nil or there is an error - // creating the client. - // - // This is hacky and is probably better handled by exposing help usage - // texts in a non-internal btcd package. - postClient := func() *btcrpcclient.Client { - if chainSvr == nil { - return nil - } - var certs []byte - if !cfg.DisableClientTLS { - var err error - certs, err = ioutil.ReadFile(cfg.CAFile) - if err != nil { - return nil - } - } - conf := btcrpcclient.ConnConfig{ - Host: cfg.RPCConnect, - User: cfg.BtcdUsername, - Pass: cfg.BtcdPassword, - DisableTLS: cfg.DisableClientTLS, - Certificates: certs, - HTTPPostMode: true, - } - client, err := btcrpcclient.New(&conf, nil) + for _, addr := range ipv6Addrs { + listener, err := listen("tcp6", addr) if err != nil { - return nil - } - return client - } - - if cmd.Command == nil || *cmd.Command == "" { - // Prepend chain server usage if it is available. - usages := requestUsages - client := postClient() - if client != nil { - rawChainUsage, err := client.RawRequest("help", nil) - var chainUsage string - if err == nil { - _ = json.Unmarshal([]byte(rawChainUsage), &chainUsage) - } - if chainUsage != "" { - usages = "Chain server usage:\n\n" + chainUsage + "\n\n" + - "Wallet server usage (overrides chain requests):\n\n" + - requestUsages - } - } - return usages, nil - } - - defer helpDescsMu.Unlock() - helpDescsMu.Lock() - - if helpDescs == nil { - // TODO: Allow other locales to be set via config or detemine - // this from environment variables. For now, hardcode US - // English. - helpDescs = localeHelpDescs["en_US"]() - } - - helpText, ok := helpDescs[*cmd.Command] - if ok { - return helpText, nil - } - - // Return the chain server's detailed help if possible. - var chainHelp string - client := postClient() - if client != nil { - param := make([]byte, len(*cmd.Command)+2) - param[0] = '"' - copy(param[1:], *cmd.Command) - param[len(param)-1] = '"' - rawChainHelp, err := client.RawRequest("help", []json.RawMessage{param}) - if err == nil { - _ = json.Unmarshal([]byte(rawChainHelp), &chainHelp) - } - } - if chainHelp != "" { - return chainHelp, nil - } - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCInvalidParameter, - Message: fmt.Sprintf("No help for method '%s'", *cmd.Command), - } -} - -// ListAccounts handles a listaccounts request by returning a map of account -// names to their balances. -func ListAccounts(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.ListAccountsCmd) - - accountBalances := map[string]float64{} - var accounts []uint32 - err := w.Manager.ForEachAccount(func(account uint32) error { - accounts = append(accounts, account) - return nil - }) - if err != nil { - return nil, err - } - minConf := int32(*cmd.MinConf) - for _, account := range accounts { - acctName, err := w.Manager.AccountName(account) - if err != nil { - return nil, &ErrAccountNameNotFound - } - bal, err := w.CalculateAccountBalance(account, minConf) - if err != nil { - return nil, err - } - accountBalances[acctName] = bal.ToBTC() - } - // Return the map. This will be marshaled into a JSON object. - return accountBalances, nil -} - -// ListLockUnspent handles a listlockunspent request by returning an slice of -// all locked outpoints. -func ListLockUnspent(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - return w.LockedOutpoints(), nil -} - -// ListReceivedByAccount handles a listreceivedbyaccount request by returning -// a slice of objects, each one containing: -// "account": the receiving account; -// "amount": total amount received by the account; -// "confirmations": number of confirmations of the most recent transaction. -// It takes two parameters: -// "minconf": minimum number of confirmations to consider a transaction - -// default: one; -// "includeempty": whether or not to include addresses that have no transactions - -// default: false. -func ListReceivedByAccount(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.ListReceivedByAccountCmd) - - var accounts []uint32 - err := w.Manager.ForEachAccount(func(account uint32) error { - accounts = append(accounts, account) - return nil - }) - if err != nil { - return nil, err - } - - ret := make([]btcjson.ListReceivedByAccountResult, 0, len(accounts)) - minConf := int32(*cmd.MinConf) - for _, account := range accounts { - acctName, err := w.Manager.AccountName(account) - if err != nil { - return nil, &ErrAccountNameNotFound - } - bal, confirmations, err := w.TotalReceivedForAccount(account, - minConf) - if err != nil { - return nil, err - } - ret = append(ret, btcjson.ListReceivedByAccountResult{ - Account: acctName, - Amount: bal.ToBTC(), - Confirmations: uint64(confirmations), - }) - } - return ret, nil -} - -// ListReceivedByAddress handles a listreceivedbyaddress request by returning -// a slice of objects, each one containing: -// "account": the account of the receiving address; -// "address": the receiving address; -// "amount": total amount received by the address; -// "confirmations": number of confirmations of the most recent transaction. -// It takes two parameters: -// "minconf": minimum number of confirmations to consider a transaction - -// default: one; -// "includeempty": whether or not to include addresses that have no transactions - -// default: false. -func ListReceivedByAddress(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.ListReceivedByAddressCmd) - - // Intermediate data for each address. - type AddrData struct { - // Total amount received. - amount btcutil.Amount - // Number of confirmations of the last transaction. - confirmations int32 - // Hashes of transactions which include an output paying to the address - tx []string - // Account which the address belongs to - account string - } - - syncBlock := w.Manager.SyncedTo() - - // Intermediate data for all addresses. - allAddrData := make(map[string]AddrData) - // Create an AddrData entry for each active address in the account. - // Otherwise we'll just get addresses from transactions later. - sortedAddrs, err := w.SortedActivePaymentAddresses() - if err != nil { - return nil, err - } - for _, address := range sortedAddrs { - // There might be duplicates, just overwrite them. - allAddrData[address] = AddrData{} - } - - minConf := *cmd.MinConf - var endHeight int32 - if minConf == 0 { - endHeight = -1 - } else { - endHeight = syncBlock.Height - int32(minConf) + 1 - } - err = w.TxStore.RangeTransactions(0, endHeight, func(details []wtxmgr.TxDetails) (bool, error) { - confirmations := confirms(details[0].Block.Height, syncBlock.Height) - for _, tx := range details { - for _, cred := range tx.Credits { - pkScript := tx.MsgTx.TxOut[cred.Index].PkScript - _, addrs, _, err := txscript.ExtractPkScriptAddrs( - pkScript, activeNet.Params) - if err != nil { - // Non standard script, skip. - continue - } - for _, addr := range addrs { - addrStr := addr.EncodeAddress() - addrData, ok := allAddrData[addrStr] - if ok { - addrData.amount += cred.Amount - // Always overwrite confirmations with newer ones. - addrData.confirmations = confirmations - } else { - addrData = AddrData{ - amount: cred.Amount, - confirmations: confirmations, - } - } - addrData.tx = append(addrData.tx, tx.Hash.String()) - allAddrData[addrStr] = addrData - } - } - } - return false, nil - }) - if err != nil { - return nil, err - } - - // Massage address data into output format. - numAddresses := len(allAddrData) - ret := make([]btcjson.ListReceivedByAddressResult, numAddresses, numAddresses) - idx := 0 - for address, addrData := range allAddrData { - ret[idx] = btcjson.ListReceivedByAddressResult{ - Address: address, - Amount: addrData.amount.ToBTC(), - Confirmations: uint64(addrData.confirmations), - TxIDs: addrData.tx, - } - idx++ - } - return ret, nil -} - -// ListSinceBlock handles a listsinceblock request by returning an array of maps -// with details of sent and received wallet transactions since the given block. -func ListSinceBlock(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.ListSinceBlockCmd) - - syncBlock := w.Manager.SyncedTo() - targetConf := int64(*cmd.TargetConfirmations) - - // For the result we need the block hash for the last block counted - // in the blockchain due to confirmations. We send this off now so that - // it can arrive asynchronously while we figure out the rest. - gbh := chainSvr.GetBlockHashAsync(int64(syncBlock.Height) + 1 - targetConf) - - var start int32 - if cmd.BlockHash != nil { - hash, err := wire.NewShaHashFromStr(*cmd.BlockHash) - if err != nil { - return nil, DeserializationError{err} - } - block, err := chainSvr.GetBlockVerbose(hash, false) - if err != nil { - return nil, err - } - start = int32(block.Height) + 1 - } - - txInfoList, err := w.ListSinceBlock(start, -1, syncBlock.Height) - if err != nil { - return nil, err - } - - // Done with work, get the response. - blockHash, err := gbh.Receive() - if err != nil { - return nil, err - } - - res := btcjson.ListSinceBlockResult{ - Transactions: txInfoList, - LastBlock: blockHash.String(), - } - return res, nil -} - -// ListTransactions handles a listtransactions request by returning an -// array of maps with details of sent and recevied wallet transactions. -func ListTransactions(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.ListTransactionsCmd) - - // TODO: ListTransactions does not currently understand the difference - // between transactions pertaining to one account from another. This - // will be resolved when wtxmgr is combined with the waddrmgr namespace. - - if cmd.Account != nil && *cmd.Account != "*" { - // For now, don't bother trying to continue if the user - // specified an account, since this can't be (easily or - // efficiently) calculated. - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCWallet, - Message: "Transactions are not yet grouped by account", - } - } - - return w.ListTransactions(*cmd.From, *cmd.Count) -} - -// ListAddressTransactions handles a listaddresstransactions request by -// returning an array of maps with details of spent and received wallet -// transactions. The form of the reply is identical to listtransactions, -// but the array elements are limited to transaction details which are -// about the addresess included in the request. -func ListAddressTransactions(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.ListAddressTransactionsCmd) - - if cmd.Account != nil && *cmd.Account != "*" { - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCInvalidParameter, - Message: "Listing transactions for addresses may only be done for all accounts", - } - } - - // Decode addresses. - hash160Map := make(map[string]struct{}) - for _, addrStr := range cmd.Addresses { - addr, err := decodeAddress(addrStr, activeNet.Params) - if err != nil { - return nil, err - } - hash160Map[string(addr.ScriptAddress())] = struct{}{} - } - - return w.ListAddressTransactions(hash160Map) -} - -// ListAllTransactions handles a listalltransactions request by returning -// a map with details of sent and recevied wallet transactions. This is -// similar to ListTransactions, except it takes only a single optional -// argument for the account name and replies with all transactions. -func ListAllTransactions(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.ListAllTransactionsCmd) - - if cmd.Account != nil && *cmd.Account != "*" { - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCInvalidParameter, - Message: "Listing all transactions may only be done for all accounts", - } - } - - return w.ListAllTransactions() -} - -// ListUnspent handles the listunspent command. -func ListUnspent(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.ListUnspentCmd) - - var addresses map[string]struct{} - if cmd.Addresses != nil { - addresses = make(map[string]struct{}) - // confirm that all of them are good: - for _, as := range *cmd.Addresses { - a, err := decodeAddress(as, activeNet.Params) - if err != nil { - return nil, err - } - addresses[a.EncodeAddress()] = struct{}{} - } - } - - return w.ListUnspent(int32(*cmd.MinConf), int32(*cmd.MaxConf), addresses) -} - -// LockUnspent handles the lockunspent command. -func LockUnspent(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.LockUnspentCmd) - - switch { - case cmd.Unlock && len(cmd.Transactions) == 0: - w.ResetLockedOutpoints() - default: - for _, input := range cmd.Transactions { - txSha, err := wire.NewShaHashFromStr(input.Txid) - if err != nil { - return nil, ParseError{err} - } - op := wire.OutPoint{Hash: *txSha, Index: input.Vout} - if cmd.Unlock { - w.UnlockOutpoint(op) - } else { - w.LockOutpoint(op) - } - } - } - return true, nil -} - -// sendPairs creates and sends payment transactions. -// It returns the transaction hash in string format upon success -// All errors are returned in btcjson.RPCError format -func sendPairs(w *wallet.Wallet, amounts map[string]btcutil.Amount, - account uint32, minconf int32) (string, error) { - txSha, err := w.SendPairs(amounts, account, minconf) - if err != nil { - if err == wallet.ErrNonPositiveAmount { - return "", ErrNeedPositiveAmount - } - if waddrmgr.IsError(err, waddrmgr.ErrLocked) { - return "", &ErrWalletUnlockNeeded - } - switch err.(type) { - case btcjson.RPCError: - return "", err - } - - return "", &btcjson.RPCError{ - Code: btcjson.ErrRPCInternal.Code, - Message: err.Error(), - } - } - - txShaStr := txSha.String() - log.Infof("Successfully sent transaction %v", txShaStr) - return txShaStr, nil -} - -// SendFrom handles a sendfrom RPC request by creating a new transaction -// spending unspent transaction outputs for a wallet to another payment -// address. Leftover inputs not sent to the payment address or a fee for -// the miner are sent back to a new address in the wallet. Upon success, -// the TxID for the created transaction is returned. -func SendFrom(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.SendFromCmd) - - // Transaction comments are not yet supported. Error instead of - // pretending to save them. - if cmd.Comment != nil || cmd.CommentTo != nil { - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCUnimplemented, - Message: "Transaction comments are not yet supported", - } - } - - account, err := w.Manager.LookupAccount(cmd.FromAccount) - if err != nil { - return nil, err - } - - // Check that signed integer parameters are positive. - if cmd.Amount < 0 { - return nil, ErrNeedPositiveAmount - } - minConf := int32(*cmd.MinConf) - if minConf < 0 { - return nil, ErrNeedPositiveMinconf - } - // Create map of address and amount pairs. - amt, err := btcutil.NewAmount(cmd.Amount) - if err != nil { - return nil, err - } - pairs := map[string]btcutil.Amount{ - cmd.ToAddress: amt, - } - - return sendPairs(w, pairs, account, minConf) -} - -// SendMany handles a sendmany RPC request by creating a new transaction -// spending unspent transaction outputs for a wallet to any number of -// payment addresses. Leftover inputs not sent to the payment address -// or a fee for the miner are sent back to a new address in the wallet. -// Upon success, the TxID for the created transaction is returned. -func SendMany(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.SendManyCmd) - - // Transaction comments are not yet supported. Error instead of - // pretending to save them. - if cmd.Comment != nil { - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCUnimplemented, - Message: "Transaction comments are not yet supported", - } - } - - account, err := w.Manager.LookupAccount(cmd.FromAccount) - if err != nil { - return nil, err - } - - // Check that minconf is positive. - minConf := int32(*cmd.MinConf) - if minConf < 0 { - return nil, ErrNeedPositiveMinconf - } - - // Recreate address/amount pairs, using btcutil.Amount. - pairs := make(map[string]btcutil.Amount, len(cmd.Amounts)) - for k, v := range cmd.Amounts { - amt, err := btcutil.NewAmount(v) - if err != nil { - return nil, err - } - pairs[k] = amt - } - - return sendPairs(w, pairs, account, minConf) -} - -// SendToAddress handles a sendtoaddress RPC request by creating a new -// transaction spending unspent transaction outputs for a wallet to another -// payment address. Leftover inputs not sent to the payment address or a fee -// for the miner are sent back to a new address in the wallet. Upon success, -// the TxID for the created transaction is returned. -func SendToAddress(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.SendToAddressCmd) - - // Transaction comments are not yet supported. Error instead of - // pretending to save them. - if cmd.Comment != nil || cmd.CommentTo != nil { - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCUnimplemented, - Message: "Transaction comments are not yet supported", - } - } - - amt, err := btcutil.NewAmount(cmd.Amount) - if err != nil { - return nil, err - } - - // Check that signed integer parameters are positive. - if amt < 0 { - return nil, ErrNeedPositiveAmount - } - - // Mock up map of address and amount pairs. - pairs := map[string]btcutil.Amount{ - cmd.Address: amt, - } - - // sendtoaddress always spends from the default account, this matches bitcoind - return sendPairs(w, pairs, waddrmgr.DefaultAccountNum, 1) -} - -// SetTxFee sets the transaction fee per kilobyte added to transactions. -func SetTxFee(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.SetTxFeeCmd) - - // Check that amount is not negative. - if cmd.Amount < 0 { - return nil, ErrNeedPositiveAmount - } - - incr, err := btcutil.NewAmount(cmd.Amount) - if err != nil { - return nil, err - } - w.FeeIncrement = incr - - // A boolean true result is returned upon success. - return true, nil -} - -// SignMessage signs the given message with the private key for the given -// address -func SignMessage(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.SignMessageCmd) - - addr, err := decodeAddress(cmd.Address, activeNet.Params) - if err != nil { - return nil, err - } - - ainfo, err := w.Manager.Address(addr) - if err != nil { - return nil, err - } - pka, ok := ainfo.(waddrmgr.ManagedPubKeyAddress) - if !ok { - msg := fmt.Sprintf("Address '%s' does not have an associated private key", addr) - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCInvalidAddressOrKey, - Message: msg, - } - } - privKey, err := pka.PrivKey() - if err != nil { - return nil, err - } - - var buf bytes.Buffer - wire.WriteVarString(&buf, 0, "Bitcoin Signed Message:\n") - wire.WriteVarString(&buf, 0, cmd.Message) - messageHash := wire.DoubleSha256(buf.Bytes()) - sigbytes, err := btcec.SignCompact(btcec.S256(), privKey, - messageHash, ainfo.Compressed()) - if err != nil { - return nil, err - } - - return base64.StdEncoding.EncodeToString(sigbytes), nil -} - -// pendingTx is used for async fetching of transaction dependancies in -// SignRawTransaction. -type pendingTx struct { - resp btcrpcclient.FutureGetRawTransactionResult - inputs []uint32 // list of inputs that care about this tx. -} - -// SignRawTransaction handles the signrawtransaction command. -func SignRawTransaction(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.SignRawTransactionCmd) - - serializedTx, err := decodeHexStr(cmd.RawTx) - if err != nil { - return nil, err - } - msgTx := wire.NewMsgTx() - err = msgTx.Deserialize(bytes.NewBuffer(serializedTx)) - if err != nil { - e := errors.New("TX decode failed") - return nil, DeserializationError{e} - } - - // First we add the stuff we have been given. - // TODO(oga) really we probably should look these up with btcd anyway - // to make sure that they match the blockchain if present. - inputs := make(map[wire.OutPoint][]byte) - scripts := make(map[string][]byte) - var cmdInputs []btcjson.RawTxInput - if cmd.Inputs != nil { - cmdInputs = *cmd.Inputs - } - for _, rti := range cmdInputs { - inputSha, err := wire.NewShaHashFromStr(rti.Txid) - if err != nil { - return nil, DeserializationError{err} - } - - script, err := decodeHexStr(rti.ScriptPubKey) - if err != nil { - return nil, err - } - - // redeemScript is only actually used iff the user provided - // private keys. In which case, it is used to get the scripts - // for signing. If the user did not provide keys then we always - // get scripts from the wallet. - // Empty strings are ok for this one and hex.DecodeString will - // DTRT. - if cmd.PrivKeys != nil && len(*cmd.PrivKeys) != 0 { - redeemScript, err := decodeHexStr(rti.RedeemScript) - if err != nil { - return nil, err - } - - addr, err := btcutil.NewAddressScriptHash(redeemScript, - activeNet.Params) - if err != nil { - return nil, DeserializationError{err} - } - scripts[addr.String()] = redeemScript - } - inputs[wire.OutPoint{ - Hash: *inputSha, - Index: rti.Vout, - }] = script - } - - // Now we go and look for any inputs that we were not provided by - // querying btcd with getrawtransaction. We queue up a bunch of async - // requests and will wait for replies after we have checked the rest of - // the arguments. - requested := make(map[wire.ShaHash]*pendingTx) - for _, txIn := range msgTx.TxIn { - // Did we get this txin from the arguments? - if _, ok := inputs[txIn.PreviousOutPoint]; ok { + log.Warnf("Can't listen on %s: %v", addr, err) continue } - - // Are we already fetching this tx? If so mark us as interested - // in this outpoint. (N.B. that any *sane* tx will only - // reference each outpoint once, since anything else is a double - // spend. We don't check this ourselves to save having to scan - // the array, it will fail later if so). - if ptx, ok := requested[txIn.PreviousOutPoint.Hash]; ok { - ptx.inputs = append(ptx.inputs, - txIn.PreviousOutPoint.Index) - continue - } - - // Never heard of this one before, request it. - prevHash := &txIn.PreviousOutPoint.Hash - requested[txIn.PreviousOutPoint.Hash] = &pendingTx{ - resp: chainSvr.GetRawTransactionAsync(prevHash), - inputs: []uint32{txIn.PreviousOutPoint.Index}, - } + listeners = append(listeners, listener) } - - // Parse list of private keys, if present. If there are any keys here - // they are the keys that we may use for signing. If empty we will - // use any keys known to us already. - var keys map[string]*btcutil.WIF - if cmd.PrivKeys != nil { - keys = make(map[string]*btcutil.WIF) - - for _, key := range *cmd.PrivKeys { - wif, err := btcutil.DecodeWIF(key) - if err != nil { - return nil, DeserializationError{err} - } - - if !wif.IsForNet(activeNet.Params) { - s := "key network doesn't match wallet's" - return nil, DeserializationError{errors.New(s)} - } - - addr, err := btcutil.NewAddressPubKey(wif.SerializePubKey(), - activeNet.Params) - if err != nil { - return nil, DeserializationError{err} - } - keys[addr.EncodeAddress()] = wif - } - } - - var hashType txscript.SigHashType - switch *cmd.Flags { - case "ALL": - hashType = txscript.SigHashAll - case "NONE": - hashType = txscript.SigHashNone - case "SINGLE": - hashType = txscript.SigHashSingle - case "ALL|ANYONECANPAY": - hashType = txscript.SigHashAll | txscript.SigHashAnyOneCanPay - case "NONE|ANYONECANPAY": - hashType = txscript.SigHashNone | txscript.SigHashAnyOneCanPay - case "SINGLE|ANYONECANPAY": - hashType = txscript.SigHashSingle | txscript.SigHashAnyOneCanPay - default: - e := errors.New("Invalid sighash parameter") - return nil, InvalidParameterError{e} - } - - // We have checked the rest of the args. now we can collect the async - // txs. TODO(oga) If we don't mind the possibility of wasting work we - // could move waiting to the following loop and be slightly more - // asynchronous. - for txid, ptx := range requested { - tx, err := ptx.resp.Receive() - if err != nil { - return nil, err - } - - for _, input := range ptx.inputs { - if input >= uint32(len(tx.MsgTx().TxOut)) { - e := fmt.Errorf("input %s:%d is not in tx", - txid.String(), input) - return nil, InvalidParameterError{e} - } - - inputs[wire.OutPoint{ - Hash: txid, - Index: input, - }] = tx.MsgTx().TxOut[input].PkScript - } - } - - // All args collected. Now we can sign all the inputs that we can. - // `complete' denotes that we successfully signed all outputs and that - // all scripts will run to completion. This is returned as part of the - // reply. - var signErrors []btcjson.SignRawTransactionError - for i, txIn := range msgTx.TxIn { - input, ok := inputs[txIn.PreviousOutPoint] - if !ok { - // failure to find previous is actually an error since - // we failed above if we don't have all the inputs. - return nil, fmt.Errorf("%s:%d not found", - txIn.PreviousOutPoint.Hash, - txIn.PreviousOutPoint.Index) - } - - // Set up our callbacks that we pass to txscript so it can - // look up the appropriate keys and scripts by address. - getKey := txscript.KeyClosure(func(addr btcutil.Address) ( - *btcec.PrivateKey, bool, error) { - if len(keys) != 0 { - wif, ok := keys[addr.EncodeAddress()] - if !ok { - return nil, false, - errors.New("no key for address") - } - return wif.PrivKey, wif.CompressPubKey, nil - } - address, err := w.Manager.Address(addr) - if err != nil { - return nil, false, err - } - - pka, ok := address.(waddrmgr.ManagedPubKeyAddress) - if !ok { - return nil, false, errors.New("address is not " + - "a pubkey address") - } - - key, err := pka.PrivKey() - if err != nil { - return nil, false, err - } - - return key, pka.Compressed(), nil - }) - - getScript := txscript.ScriptClosure(func( - addr btcutil.Address) ([]byte, error) { - // If keys were provided then we can only use the - // scripts provided with our inputs, too. - if len(keys) != 0 { - script, ok := scripts[addr.EncodeAddress()] - if !ok { - return nil, errors.New("no script for " + - "address") - } - return script, nil - } - address, err := w.Manager.Address(addr) - if err != nil { - return nil, err - } - sa, ok := address.(waddrmgr.ManagedScriptAddress) - if !ok { - return nil, errors.New("address is not a script" + - " address") - } - - return sa.Script() - }) - - // SigHashSingle inputs can only be signed if there's a - // corresponding output. However this could be already signed, - // so we always verify the output. - if (hashType&txscript.SigHashSingle) != - txscript.SigHashSingle || i < len(msgTx.TxOut) { - - script, err := txscript.SignTxOutput(activeNet.Params, - msgTx, i, input, hashType, getKey, - getScript, txIn.SignatureScript) - // Failure to sign isn't an error, it just means that - // the tx isn't complete. - if err != nil { - signErrors = append(signErrors, - btcjson.SignRawTransactionError{ - TxID: txIn.PreviousOutPoint.Hash.String(), - Vout: txIn.PreviousOutPoint.Index, - ScriptSig: hex.EncodeToString(txIn.SignatureScript), - Sequence: txIn.Sequence, - Error: err.Error(), - }) - continue - } - txIn.SignatureScript = script - } - - // Either it was already signed or we just signed it. - // Find out if it is completely satisfied or still needs more. - vm, err := txscript.NewEngine(input, msgTx, i, - txscript.StandardVerifyFlags, nil) - if err == nil { - err = vm.Execute() - } - if err != nil { - signErrors = append(signErrors, - btcjson.SignRawTransactionError{ - TxID: txIn.PreviousOutPoint.Hash.String(), - Vout: txIn.PreviousOutPoint.Index, - ScriptSig: hex.EncodeToString(txIn.SignatureScript), - Sequence: txIn.Sequence, - Error: err.Error(), - }) - } - } - - var buf bytes.Buffer - buf.Grow(msgTx.SerializeSize()) - - // All returned errors (not OOM, which panics) encounted during - // bytes.Buffer writes are unexpected. - if err = msgTx.Serialize(&buf); err != nil { - panic(err) - } - - return btcjson.SignRawTransactionResult{ - Hex: hex.EncodeToString(buf.Bytes()), - Complete: len(signErrors) == 0, - Errors: signErrors, - }, nil + return listeners } -// ValidateAddress handles the validateaddress command. -func ValidateAddress(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.ValidateAddressCmd) - - result := btcjson.ValidateAddressWalletResult{} - addr, err := decodeAddress(cmd.Address, activeNet.Params) - if err != nil { - // Use result zero value (IsValid=false). - return result, nil +// startWalletRPCServices associates each of the (optionally-nil) RPC servers +// with a wallet to enable remote wallet access. For the GRPC server, this +// registers the WalletService service, and for the legacy JSON-RPC server it +// enables methods that require a loaded wallet. +func startWalletRPCServices(wallet *wallet.Wallet, server *grpc.Server, legacyServer *legacyrpc.Server) { + if server != nil { + rpcserver.StartWalletService(server, wallet) } - - // We could put whether or not the address is a script here, - // by checking the type of "addr", however, the reference - // implementation only puts that information if the script is - // "ismine", and we follow that behaviour. - result.Address = addr.EncodeAddress() - result.IsValid = true - - ainfo, err := w.Manager.Address(addr) - if err != nil { - if waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) { - // No additional information available about the address. - return result, nil - } - return nil, err - } - - // The address lookup was successful which means there is further - // information about it available and it is "mine". - result.IsMine = true - acctName, err := w.Manager.AccountName(ainfo.Account()) - if err != nil { - return nil, &ErrAccountNameNotFound - } - result.Account = acctName - - switch ma := ainfo.(type) { - case waddrmgr.ManagedPubKeyAddress: - result.IsCompressed = ma.Compressed() - result.PubKey = ma.ExportPubKey() - - case waddrmgr.ManagedScriptAddress: - result.IsScript = true - - // The script is only available if the manager is unlocked, so - // just break out now if there is an error. - script, err := ma.Script() - if err != nil { - break - } - result.Hex = hex.EncodeToString(script) - - // This typically shouldn't fail unless an invalid script was - // imported. However, if it fails for any reason, there is no - // further information available, so just set the script type - // a non-standard and break out now. - class, addrs, reqSigs, err := txscript.ExtractPkScriptAddrs( - script, activeNet.Params) - if err != nil { - result.Script = txscript.NonStandardTy.String() - break - } - - addrStrings := make([]string, len(addrs)) - for i, a := range addrs { - addrStrings[i] = a.EncodeAddress() - } - result.Addresses = addrStrings - - // Multi-signature scripts also provide the number of required - // signatures. - result.Script = class.String() - if class == txscript.MultiSigTy { - result.SigsRequired = int32(reqSigs) - } - } - - return result, nil -} - -// VerifyMessage handles the verifymessage command by verifying the provided -// compact signature for the given address and message. -func VerifyMessage(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.VerifyMessageCmd) - - addr, err := decodeAddress(cmd.Address, activeNet.Params) - if err != nil { - return nil, err - } - - // decode base64 signature - sig, err := base64.StdEncoding.DecodeString(cmd.Signature) - if err != nil { - return nil, err - } - - // Validate the signature - this just shows that it was valid at all. - // we will compare it with the key next. - var buf bytes.Buffer - wire.WriteVarString(&buf, 0, "Bitcoin Signed Message:\n") - wire.WriteVarString(&buf, 0, cmd.Message) - expectedMessageHash := wire.DoubleSha256(buf.Bytes()) - pk, wasCompressed, err := btcec.RecoverCompact(btcec.S256(), sig, - expectedMessageHash) - if err != nil { - return nil, err - } - - var serializedPubKey []byte - if wasCompressed { - serializedPubKey = pk.SerializeCompressed() - } else { - serializedPubKey = pk.SerializeUncompressed() - } - // Verify that the signed-by address matches the given address - switch checkAddr := addr.(type) { - case *btcutil.AddressPubKeyHash: // ok - return bytes.Equal(btcutil.Hash160(serializedPubKey), checkAddr.Hash160()[:]), nil - case *btcutil.AddressPubKey: // ok - return string(serializedPubKey) == checkAddr.String(), nil - default: - return nil, errors.New("address type not supported") + if legacyServer != nil { + legacyServer.RegisterWallet(wallet) } } - -// WalletIsLocked handles the walletislocked extension request by -// returning the current lock state (false for unlocked, true for locked) -// of an account. -func WalletIsLocked(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - return w.Locked(), nil -} - -// WalletLock handles a walletlock request by locking the all account -// wallets, returning an error if any wallet is not encrypted (for example, -// a watching-only wallet). -func WalletLock(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - w.Lock() - return nil, nil -} - -// WalletPassphrase responds to the walletpassphrase request by unlocking -// the wallet. The decryption key is saved in the wallet until timeout -// seconds expires, after which the wallet is locked. -func WalletPassphrase(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.WalletPassphraseCmd) - - timeout := time.Second * time.Duration(cmd.Timeout) - err := w.Unlock([]byte(cmd.Passphrase), timeout) - return nil, err -} - -// WalletPassphraseChange responds to the walletpassphrasechange request -// by unlocking all accounts with the provided old passphrase, and -// re-encrypting each private key with an AES key derived from the new -// passphrase. -// -// If the old passphrase is correct and the passphrase is changed, all -// wallets will be immediately locked. -func WalletPassphraseChange(w *wallet.Wallet, chainSvr *chain.Client, icmd interface{}) (interface{}, error) { - cmd := icmd.(*btcjson.WalletPassphraseChangeCmd) - - err := w.ChangePassphrase([]byte(cmd.OldPassphrase), - []byte(cmd.NewPassphrase)) - if waddrmgr.IsError(err, waddrmgr.ErrWrongPassphrase) { - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCWalletPassphraseIncorrect, - Message: "Incorrect passphrase", - } - } - return nil, err -} - -// decodeHexStr decodes the hex encoding of a string, possibly prepending a -// leading '0' character if there is an odd number of bytes in the hex string. -// This is to prevent an error for an invalid hex string when using an odd -// number of bytes when calling hex.Decode. -func decodeHexStr(hexStr string) ([]byte, error) { - if len(hexStr)%2 != 0 { - hexStr = "0" + hexStr - } - decoded, err := hex.DecodeString(hexStr) - if err != nil { - return nil, &btcjson.RPCError{ - Code: btcjson.ErrRPCDecodeHexString, - Message: "Hex string decode failed: " + err.Error(), - } - } - return decoded, nil -} diff --git a/sample-btcwallet.conf b/sample-btcwallet.conf index e4b0de5..d773b2a 100644 --- a/sample-btcwallet.conf +++ b/sample-btcwallet.conf @@ -15,7 +15,7 @@ ; directory for mainnet and testnet wallets, respectively. ; datadir=~/.btcwallet -; Maximum number of addresses to generate for the keypool +; Maximum number of addresses to generate for the keypool (DEPRECATED) ; keypoolsize=100 ; Whether transactions must be created with some minimum fee, even if the @@ -69,6 +69,15 @@ ; rpclisten=0.0.0.0:18337 ; all ipv4 interfaces on non-standard port 18337 ; rpclisten=[::]:18337 ; all ipv6 interfaces on non-standard port 18337 +; Legacy (Bitcoin Core-compatible) RPC listener addresses. Addresses without a +; port specified use the same default port as the new server. Listeners cannot +; be shared between both RPC servers. +; +; Adding any legacy RPC listen addresses disable all default rpclisten options. +; If both servers must run, all listen addresses must be manually specified for +; each. +; legacyrpclisten= + ; ------------------------------------------------------------------------------ diff --git a/signal.go b/signal.go index 3953166..1200bb2 100644 --- a/signal.go +++ b/signal.go @@ -28,6 +28,21 @@ var interruptChannel chan os.Signal // to be invoked on SIGINT (Ctrl+C) signals. var addHandlerChannel = make(chan func()) +// interruptHandlersDone is closed after all interrupt handlers run the first +// time an interrupt is signaled. +var interruptHandlersDone = make(chan struct{}) + +var simulateInterruptChannel = make(chan struct{}, 1) + +// simulateInterrupt requests invoking the clean termination process by an +// internal component instead of a SIGINT. +func simulateInterrupt() { + select { + case simulateInterruptChannel <- struct{}{}: + default: + } +} + // mainInterruptHandler listens for SIGINT (Ctrl+C) signals on the // interruptChannel and invokes the registered interruptCallbacks accordingly. // It also listens for callback registration. It must be run as a goroutine. @@ -35,16 +50,25 @@ func mainInterruptHandler() { // interruptCallbacks is a list of callbacks to invoke when a // SIGINT (Ctrl+C) is received. var interruptCallbacks []func() + invokeCallbacks := func() { + // run handlers in LIFO order. + for i := range interruptCallbacks { + idx := len(interruptCallbacks) - 1 - i + interruptCallbacks[idx]() + } + close(interruptHandlersDone) + } for { select { case <-interruptChannel: log.Info("Received SIGINT (Ctrl+C). Shutting down...") - // run handlers in LIFO order. - for i := range interruptCallbacks { - idx := len(interruptCallbacks) - 1 - i - interruptCallbacks[idx]() - } + invokeCallbacks() + return + case <-simulateInterruptChannel: + log.Info("Received shutdown request. Shutting down...") + invokeCallbacks() + return case handler := <-addHandlerChannel: interruptCallbacks = append(interruptCallbacks, handler) diff --git a/waddrmgr/error.go b/waddrmgr/error.go index a3d158b..3752189 100644 --- a/waddrmgr/error.go +++ b/waddrmgr/error.go @@ -135,6 +135,10 @@ const ( // ErrCallBackBreak is used to break from a callback function passed // down to the manager. ErrCallBackBreak + + // ErrEmptyPassphrase indicates that the private passphrase was refused + // due to being empty. + ErrEmptyPassphrase ) // Map of ErrorCode values back to their constant names for pretty printing. @@ -159,6 +163,7 @@ var errorCodeStrings = map[ErrorCode]string{ ErrWrongPassphrase: "ErrWrongPassphrase", ErrWrongNet: "ErrWrongNet", ErrCallBackBreak: "ErrCallBackBreak", + ErrEmptyPassphrase: "ErrEmptyPassphrase", } // String returns the ErrorCode as a human-readable name. diff --git a/waddrmgr/error_test.go b/waddrmgr/error_test.go index b6e694e..9c0c408 100644 --- a/waddrmgr/error_test.go +++ b/waddrmgr/error_test.go @@ -49,6 +49,8 @@ func TestErrorCodeStringer(t *testing.T) { {waddrmgr.ErrTooManyAddresses, "ErrTooManyAddresses"}, {waddrmgr.ErrWrongPassphrase, "ErrWrongPassphrase"}, {waddrmgr.ErrWrongNet, "ErrWrongNet"}, + {waddrmgr.ErrCallBackBreak, "ErrCallBackBreak"}, + {waddrmgr.ErrEmptyPassphrase, "ErrEmptyPassphrase"}, {0xffff, "Unknown ErrorCode (65535)"}, } t.Logf("Running %d tests", len(tests)) diff --git a/waddrmgr/manager.go b/waddrmgr/manager.go index d29346b..e456eab 100644 --- a/waddrmgr/manager.go +++ b/waddrmgr/manager.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 The btcsuite developers + * Copyright (c) 2014-2016 The btcsuite developers * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -147,6 +147,8 @@ type addrKey string // private extended key so the unencrypted versions can be cleared from memory // when the address manager is locked. type accountInfo struct { + acctName string + // The account key is used to derive the branches which in turn derive // the internal and external addresses. // The accountKeyPriv will be nil when the address manager is locked. @@ -165,6 +167,16 @@ type accountInfo struct { lastInternalAddr ManagedAddress } +// AccountProperties contains properties associated with each account, such as +// the account name, number, and the nubmer of derived and imported keys. +type AccountProperties struct { + AccountNumber uint32 + AccountName string + ExternalKeyCount uint32 + InternalKeyCount uint32 + ImportedKeyCount uint32 +} + // unlockDeriveInfo houses the information needed to derive a private key for a // managed address when the address manager is unlocked. See the deriveOnUnlock // field in the Manager struct for more details on how this is used. @@ -487,6 +499,7 @@ func (m *Manager) loadAccountInfo(account uint32) (*accountInfo, error) { // Create the new account info with the known information. The rest // of the fields are filled out below. acctInfo := &accountInfo{ + acctName: row.name, acctKeyEncrypted: row.privKeyEncrypted, acctKeyPub: acctKeyPub, nextExternalIndex: row.nextExternalIndex, @@ -547,6 +560,60 @@ func (m *Manager) loadAccountInfo(account uint32) (*accountInfo, error) { return acctInfo, nil } +// AccountProperties returns properties associated with the account, such as the +// account number, name, and the number of derived and imported keys. +// +// TODO: Instead of opening a second read transaction after making a change, and +// then fetching the account properties with a new read tx, this can be made +// more performant by simply returning the new account properties during the +// change. +func (m *Manager) AccountProperties(account uint32) (*AccountProperties, error) { + defer m.mtx.RUnlock() + m.mtx.RLock() + + props := &AccountProperties{AccountNumber: account} + + // Until keys can be imported into any account, special handling is + // required for the imported account. + // + // loadAccountInfo errors when using it on the imported account since + // the accountInfo struct is filled with a BIP0044 account's extended + // keys, and the imported accounts has none. + // + // Since only the imported account allows imports currently, the number + // of imported keys for any other account is zero, and since the + // imported account cannot contain non-imported keys, the external and + // internal key counts for it are zero. + if account != ImportedAddrAccount { + acctInfo, err := m.loadAccountInfo(account) + if err != nil { + return nil, err + } + props.AccountName = acctInfo.acctName + props.ExternalKeyCount = acctInfo.nextExternalIndex + props.InternalKeyCount = acctInfo.nextInternalIndex + } else { + props.AccountName = ImportedAddrAccountName // reserved, nonchangable + + // Could be more efficient if this was tracked by the db. + var importedKeyCount uint32 + err := m.namespace.View(func(tx walletdb.Tx) error { + count := func(interface{}) error { + importedKeyCount++ + return nil + } + return forEachAccountAddress(tx, ImportedAddrAccount, + count) + }) + if err != nil { + return nil, err + } + props.ImportedKeyCount = importedKeyCount + } + + return props, nil +} + // deriveKeyFromPath returns either a public or private derived extended key // based on the private flag for the given an account, branch, and index. // @@ -1804,7 +1871,7 @@ func (m *Manager) RenameAccount(account uint32, name string) error { if err = deleteAccountIDIndex(tx, account); err != nil { return err } - // Remove the old name key from the accout name index + // Remove the old name key from the account name index if err = deleteAccountNameIndex(tx, row.name); err != nil { return err } @@ -1812,6 +1879,15 @@ func (m *Manager) RenameAccount(account uint32, name string) error { row.privKeyEncrypted, row.nextExternalIndex, row.nextInternalIndex, name) return err }) + + // Update in-memory account info with new name if cached and the db + // write was successful. + if err == nil { + if acctInfo, ok := m.acctInfo[account]; ok { + acctInfo.acctName = name + } + } + return err } @@ -2232,6 +2308,12 @@ func Create(namespace walletdb.Namespace, seed, pubPassphrase, privPassphrase [] return nil, managerError(ErrAlreadyExists, errAlreadyExists, nil) } + // Ensure the private passphrase is not empty. + if len(privPassphrase) == 0 { + str := "private passphrase may not be empty" + return nil, managerError(ErrEmptyPassphrase, str, nil) + } + // Perform the initial bucket creation and database namespace setup. if err := createManagerNS(namespace); err != nil { return nil, err diff --git a/wallet/chainntfns.go b/wallet/chainntfns.go index 13fdb0e..3a7cd14 100644 --- a/wallet/chainntfns.go +++ b/wallet/chainntfns.go @@ -24,6 +24,13 @@ import ( ) func (w *Wallet) handleChainNotifications() { + chainClient, err := w.requireChainClient() + if err != nil { + log.Errorf("handleChainNotifications called without RPC client") + w.wg.Done() + return + } + sync := func(w *Wallet) { // At the moment there is no recourse if the rescan fails for // some reason, however, the wallet will not be marked synced @@ -35,7 +42,7 @@ func (w *Wallet) handleChainNotifications() { } } - for n := range w.chainSvr.Notifications() { + for n := range chainClient.Notifications() { var err error switch n := n.(type) { case chain.ClientConnected: @@ -64,10 +71,6 @@ func (w *Wallet) handleChainNotifications() { // that's currently in-sync with the chain server as being synced up to // the passed block. func (w *Wallet) connectBlock(b wtxmgr.BlockMeta) { - if !w.ChainSynced() { - return - } - bs := waddrmgr.BlockStamp{ Height: b.Height, Hash: b.Hash, @@ -77,9 +80,13 @@ func (w *Wallet) connectBlock(b wtxmgr.BlockMeta) { "connect block for hash %v (height %d): %v", b.Hash, b.Height, err) } - w.notifyConnectedBlock(b) - w.notifyBalances(bs.Height) + // Notify interested clients of the connected block. + w.NtfnServer.notifyAttachedBlock(&b) + + // Legacy JSON-RPC notifications + w.notifyConnectedBlock(b) + w.notifyBalances(b.Height) } // disconnectBlock handles a chain server reorganize by rolling back all @@ -116,6 +123,10 @@ func (w *Wallet) disconnectBlock(b wtxmgr.BlockMeta) error { } } + // Notify interested clients of the disconnected block. + w.NtfnServer.notifyDetachedBlock(&b.Hash) + + // Legacy JSON-RPC notifications w.notifyDisconnectedBlock(b) w.notifyBalances(b.Height - 1) @@ -201,11 +212,36 @@ func (w *Wallet) addRelevantTx(rec *wtxmgr.TxRecord, block *wtxmgr.BlockMeta) er } } - // TODO: Notify connected clients of the added transaction. + // Send notification of mined or unmined transaction to any interested + // clients. + // + // TODO: Avoid the extra db hits. + if block == nil { + details, err := w.TxStore.UniqueTxDetails(&rec.Hash, nil) + if err != nil { + log.Errorf("Cannot query transaction details for notifiation: %v", err) + } else { + w.NtfnServer.notifyUnminedTransaction(details) + } + } else { + details, err := w.TxStore.UniqueTxDetails(&rec.Hash, &block.Block) + if err != nil { + log.Errorf("Cannot query transaction details for notifiation: %v", err) + } else { + w.NtfnServer.notifyMinedTransaction(details, block) + } + } - bs, err := w.chainSvr.BlockStamp() + // Legacy JSON-RPC notifications + // + // TODO: Synced-to information should be handled by the wallet, not the + // RPC client. + chainClient, err := w.requireChainClient() if err == nil { - w.notifyBalances(bs.Height) + bs, err := chainClient.BlockStamp() + if err == nil { + w.notifyBalances(bs.Height) + } } return nil diff --git a/wallet/createtx.go b/wallet/createtx.go index af5d86b..7e433e4 100644 --- a/wallet/createtx.go +++ b/wallet/createtx.go @@ -112,6 +112,7 @@ type CreatedTx struct { MsgTx *wire.MsgTx ChangeAddr btcutil.Address ChangeIndex int // negative if no change + Fee btcutil.Amount } // ByAmount defines the methods needed to satisify sort.Interface to @@ -140,8 +141,13 @@ func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, account uint32, minc } defer heldUnlock.Release() + chainClient, err := w.requireChainClient() + if err != nil { + return nil, err + } + // Get current block's height and hash. - bs, err := w.chainSvr.BlockStamp() + bs, err := chainClient.BlockStamp() if err != nil { return nil, err } @@ -270,6 +276,7 @@ func createTx(eligible []wtxmgr.Credit, MsgTx: msgtx, ChangeAddr: changeAddr, ChangeIndex: changeIdx, + Fee: feeEst, // Last estimate is the actual fee } return info, nil } diff --git a/wallet/loader.go b/wallet/loader.go new file mode 100644 index 0000000..7c5d5e2 --- /dev/null +++ b/wallet/loader.go @@ -0,0 +1,238 @@ +package wallet + +import ( + "errors" + "os" + "path/filepath" + "sync" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/btcsuite/btcwallet/internal/prompt" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/walletdb" + "github.com/btcsuite/btcwallet/wtxmgr" +) + +const ( + walletDbName = "wallet.db" +) + +var ( + // ErrLoaded describes the error condition of attempting to load or + // create a wallet when the loader has already done so. + ErrLoaded = errors.New("wallet already loaded") + + // ErrExists describes the error condition of attempting to create a new + // wallet when one exists already. + ErrExists = errors.New("wallet already exists") +) + +// Loader implements the creating of new and opening of existing wallets, while +// providing a callback system for other subsystems to handle the loading of a +// wallet. This is primarely intended for use by the RPC servers, to enable +// methods and services which require the wallet when the wallet is loaded by +// another subsystem. +// +// Loader is safe for concurrent access. +type Loader struct { + callbacks []func(*Wallet, walletdb.DB) + chainParams *chaincfg.Params + dbDirPath string + wallet *Wallet + db walletdb.DB + mu sync.Mutex +} + +// NewLoader constructs a Loader. +func NewLoader(chainParams *chaincfg.Params, dbDirPath string) *Loader { + return &Loader{ + chainParams: chainParams, + dbDirPath: dbDirPath, + } +} + +// onLoaded executes each added callback and prevents loader from loading any +// additional wallets. Requires mutex to be locked. +func (l *Loader) onLoaded(w *Wallet, db walletdb.DB) { + for _, fn := range l.callbacks { + fn(w, db) + } + + l.wallet = w + l.db = db + l.callbacks = nil // not needed anymore +} + +// RunAfterLoad adds a function to be executed when the loader creates or opens +// a wallet. Functions are executed in a single goroutine in the order they are +// added. +func (l *Loader) RunAfterLoad(fn func(*Wallet, walletdb.DB)) { + l.mu.Lock() + if l.wallet != nil { + w := l.wallet + db := l.db + l.mu.Unlock() + fn(w, db) + } else { + l.callbacks = append(l.callbacks, fn) + l.mu.Unlock() + } +} + +// CreateNewWallet creates a new wallet using the provided public and private +// passphrases. The seed is optional. If non-nil, addresses are derived from +// this seed. If nil, a secure random seed is generated. +func (l *Loader) CreateNewWallet(pubPassphrase, privPassphrase, seed []byte) (*Wallet, error) { + defer l.mu.Unlock() + l.mu.Lock() + + if l.wallet != nil { + return nil, ErrLoaded + } + + dbPath := filepath.Join(l.dbDirPath, walletDbName) + exists, err := fileExists(dbPath) + if err != nil { + return nil, err + } + if exists { + return nil, ErrExists + } + + // Create the wallet database backed by bolt db. + err = os.MkdirAll(l.dbDirPath, 0700) + if err != nil { + return nil, err + } + db, err := walletdb.Create("bdb", dbPath) + if err != nil { + return nil, err + } + + // Create the address manager. + if seed != nil { + if len(seed) < hdkeychain.MinSeedBytes || + len(seed) > hdkeychain.MaxSeedBytes { + + return nil, hdkeychain.ErrInvalidSeedLen + } + } + addrMgrNamespace, err := db.Namespace(waddrmgrNamespaceKey) + if err != nil { + return nil, err + } + _, err = waddrmgr.Create(addrMgrNamespace, seed, pubPassphrase, + privPassphrase, l.chainParams, nil) + if err != nil { + return nil, err + } + + // Create empty transaction manager. + txMgrNamespace, err := db.Namespace(wtxmgrNamespaceKey) + if err != nil { + return nil, err + } + _, err = wtxmgr.Create(txMgrNamespace) + if err != nil { + return nil, err + } + + // Open the newly-created wallet. + w, err := Open(pubPassphrase, l.chainParams, db, addrMgrNamespace, txMgrNamespace, nil) + if err != nil { + return nil, err + } + + l.onLoaded(w, db) + return w, nil +} + +var errNoConsole = errors.New("db upgrade requires console access for additional input") + +func noConsole() ([]byte, error) { + return nil, errNoConsole +} + +// OpenExistingWallet opens the wallet from the loader's wallet database path +// and the public passphrase. If the loader is being called by a context where +// standard input prompts may be used during wallet upgrades, setting +// canConsolePrompt will enables these prompts. +func (l *Loader) OpenExistingWallet(pubPassphrase []byte, canConsolePrompt bool) (*Wallet, error) { + defer l.mu.Unlock() + l.mu.Lock() + + if l.wallet != nil { + return nil, ErrLoaded + } + + // Ensure that the network directory exists. + if err := checkCreateDir(l.dbDirPath); err != nil { + return nil, err + } + + // Open the database using the boltdb backend. + dbPath := filepath.Join(l.dbDirPath, walletDbName) + db, err := walletdb.Open("bdb", dbPath) + if err != nil { + log.Errorf("Failed to open database: %v", err) + return nil, err + } + + addrMgrNS, err := db.Namespace(waddrmgrNamespaceKey) + if err != nil { + return nil, err + } + txMgrNS, err := db.Namespace(wtxmgrNamespaceKey) + if err != nil { + return nil, err + } + var cbs *waddrmgr.OpenCallbacks + if canConsolePrompt { + cbs = &waddrmgr.OpenCallbacks{ + ObtainSeed: prompt.ProvideSeed, + ObtainPrivatePass: prompt.ProvidePrivPassphrase, + } + } else { + cbs = &waddrmgr.OpenCallbacks{ + ObtainSeed: noConsole, + ObtainPrivatePass: noConsole, + } + } + w, err := Open(pubPassphrase, l.chainParams, db, addrMgrNS, txMgrNS, cbs) + if err != nil { + return nil, err + } + w.Start() + + l.onLoaded(w, db) + return w, nil +} + +// WalletExists returns whether a file exists at the loader's database path. +// This may return an error for unexpected I/O failures. +func (l *Loader) WalletExists() (bool, error) { + dbPath := filepath.Join(l.dbDirPath, walletDbName) + return fileExists(dbPath) +} + +// LoadedWallet returns the loaded wallet, if any, and a bool for whether the +// wallet has been loaded or not. If true, the wallet pointer should be safe to +// dereference. +func (l *Loader) LoadedWallet() (*Wallet, bool) { + l.mu.Lock() + w := l.wallet + l.mu.Unlock() + return w, w != nil +} + +func fileExists(filePath string) (bool, error) { + _, err := os.Stat(filePath) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, err + } + return true, nil +} diff --git a/wallet/notifications.go b/wallet/notifications.go new file mode 100644 index 0000000..4c1b806 --- /dev/null +++ b/wallet/notifications.go @@ -0,0 +1,652 @@ +// Copyright (c) 2015 The btcsuite developers +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package wallet + +import ( + "bytes" + "sync" + + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wtxmgr" +) + +// TODO: It would be good to send errors during notification creation to the rpc +// server instead of just logging them here so the client is aware that wallet +// isn't working correctly and notifications are missing. + +// TODO: Anything dealing with accounts here is expensive because the database +// is not organized correctly for true account support, but do the slow thing +// instead of the easy thing since the db can be fixed later, and we want the +// api correct now. + +// NotificationServer is a server that interested clients may hook into to +// receive notifications of changes in a wallet. A client is created for each +// registered notification. Clients are guaranteed to receive messages in the +// order wallet created them, but there is no guaranteed synchronization between +// different clients. +type NotificationServer struct { + transactions []chan *TransactionNotifications + currentTxNtfn *TransactionNotifications // coalesce this since wallet does not add mined txs together + spentness map[uint32][]chan *SpentnessNotifications + accountClients []chan *AccountNotification + mu sync.Mutex // Only protects registered client channels + wallet *Wallet // smells like hacks +} + +func newNotificationServer(wallet *Wallet) *NotificationServer { + return &NotificationServer{ + spentness: make(map[uint32][]chan *SpentnessNotifications), + wallet: wallet, + } +} + +func lookupInputAccount(w *Wallet, details *wtxmgr.TxDetails, deb wtxmgr.DebitRecord) uint32 { + // TODO: Debits should record which account(s?) they + // debit from so this doesn't need to be looked up. + prevOP := &details.MsgTx.TxIn[deb.Index].PreviousOutPoint + prev, err := w.TxStore.TxDetails(&prevOP.Hash) + if err != nil { + log.Errorf("Cannot query previous transaction details for %v: %v", prevOP.Hash, err) + return 0 + } + if prev == nil { + log.Errorf("Missing previous transaction %v", prevOP.Hash) + return 0 + } + prevOut := prev.MsgTx.TxOut[prevOP.Index] + _, addrs, _, err := txscript.ExtractPkScriptAddrs(prevOut.PkScript, w.chainParams) + var inputAcct uint32 + if err == nil && len(addrs) > 0 { + inputAcct, err = w.Manager.AddrAccount(addrs[0]) + } + if err != nil { + log.Errorf("Cannot fetch account for previous output %v: %v", prevOP, err) + inputAcct = 0 + } + return inputAcct +} + +func lookupOutputChain(w *Wallet, details *wtxmgr.TxDetails, cred wtxmgr.CreditRecord) (account uint32, internal bool) { + output := details.MsgTx.TxOut[cred.Index] + _, addrs, _, err := txscript.ExtractPkScriptAddrs(output.PkScript, w.chainParams) + var ma waddrmgr.ManagedAddress + if err == nil && len(addrs) > 0 { + ma, err = w.Manager.Address(addrs[0]) + } + if err != nil { + log.Errorf("Cannot fetch account for wallet output: %v", err) + } else { + account = ma.Account() + internal = ma.Internal() + } + return +} + +func makeTxSummary(w *Wallet, details *wtxmgr.TxDetails) TransactionSummary { + serializedTx := details.SerializedTx + if serializedTx == nil { + var buf bytes.Buffer + err := details.MsgTx.Serialize(&buf) + if err != nil { + log.Errorf("Transaction serialization: %v", err) + } + serializedTx = buf.Bytes() + } + var fee btcutil.Amount + if len(details.Debits) == len(details.MsgTx.TxIn) { + for _, deb := range details.Debits { + fee += deb.Amount + } + for _, txOut := range details.MsgTx.TxOut { + fee -= btcutil.Amount(txOut.Value) + } + } + var inputs []TransactionSummaryInput + if len(details.Debits) != 0 { + inputs = make([]TransactionSummaryInput, len(details.Debits)) + for i, d := range details.Debits { + inputs[i] = TransactionSummaryInput{ + Index: d.Index, + PreviousAccount: lookupInputAccount(w, details, d), + PreviousAmount: d.Amount, + } + } + } + outputs := make([]TransactionSummaryOutput, 0, len(details.MsgTx.TxOut)) + var credIndex int + for i, txOut := range details.MsgTx.TxOut { + mine := len(details.Credits) > credIndex && details.Credits[credIndex].Index == uint32(i) + if !mine && len(details.Debits) == 0 { + continue + } + output := TransactionSummaryOutput{ + Index: uint32(i), + Amount: btcutil.Amount(txOut.Value), + Mine: mine, + } + if mine { + acct, internal := lookupOutputChain(w, details, details.Credits[credIndex]) + output.Account = acct + output.Internal = internal + credIndex++ + } else { + _, addresses, _, err := txscript.ExtractPkScriptAddrs(txOut.PkScript, w.chainParams) + if err == nil { + output.Addresses = addresses + } + } + outputs = append(outputs, output) + } + return TransactionSummary{ + Hash: &details.Hash, + Transaction: serializedTx, + MyInputs: inputs, + MyOutputs: outputs, + Fee: fee, + Timestamp: details.Received.Unix(), + } +} + +func totalBalances(w *Wallet, m map[uint32]btcutil.Amount) error { + unspent, err := w.TxStore.UnspentOutputs() + if err != nil { + return err + } + for i := range unspent { + output := &unspent[i] + var outputAcct uint32 + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + output.PkScript, w.chainParams) + if err == nil && len(addrs) > 0 { + outputAcct, err = w.Manager.AddrAccount(addrs[0]) + } + if err == nil { + _, ok := m[outputAcct] + if ok { + m[outputAcct] += output.Amount + } + } + } + return nil +} + +func flattenBalanceMap(m map[uint32]btcutil.Amount) []AccountBalance { + s := make([]AccountBalance, 0, len(m)) + for k, v := range m { + s = append(s, AccountBalance{Account: k, TotalBalance: v}) + } + return s +} + +func relevantAccounts(w *Wallet, m map[uint32]btcutil.Amount, txs []TransactionSummary) { + for _, tx := range txs { + for _, d := range tx.MyInputs { + m[d.PreviousAccount] = 0 + } + for _, c := range tx.MyOutputs { + m[c.Account] = 0 + } + } +} + +func (s *NotificationServer) notifyUnminedTransaction(details *wtxmgr.TxDetails) { + // Sanity check: should not be currently coalescing a notification for + // mined transactions at the same time that an unmined tx is notified. + if s.currentTxNtfn != nil { + log.Errorf("Notifying unmined tx notification while creating notification for blocks") + } + + defer s.mu.Unlock() + s.mu.Lock() + clients := s.transactions + if len(clients) == 0 { + return + } + + unminedTxs := []TransactionSummary{makeTxSummary(s.wallet, details)} + unminedHashes, err := s.wallet.TxStore.UnminedTxHashes() + if err != nil { + log.Errorf("Cannot fetch unmined transaction hashes: %v", err) + return + } + bals := make(map[uint32]btcutil.Amount) + relevantAccounts(s.wallet, bals, unminedTxs) + err = totalBalances(s.wallet, bals) + if err != nil { + log.Errorf("Cannot determine balances for relevant accounts: %v", err) + return + } + n := &TransactionNotifications{ + UnminedTransactions: unminedTxs, + UnminedTransactionHashes: unminedHashes, + NewBalances: flattenBalanceMap(bals), + } + for _, c := range clients { + c <- n + } +} + +func (s *NotificationServer) notifyDetachedBlock(hash *wire.ShaHash) { + if s.currentTxNtfn == nil { + s.currentTxNtfn = &TransactionNotifications{} + } + s.currentTxNtfn.DetachedBlocks = append(s.currentTxNtfn.DetachedBlocks, hash) +} + +func (s *NotificationServer) notifyMinedTransaction(details *wtxmgr.TxDetails, block *wtxmgr.BlockMeta) { + if s.currentTxNtfn == nil { + s.currentTxNtfn = &TransactionNotifications{} + } + n := len(s.currentTxNtfn.AttachedBlocks) + if n == 0 || *s.currentTxNtfn.AttachedBlocks[n-1].Hash != block.Hash { + s.currentTxNtfn.AttachedBlocks = append(s.currentTxNtfn.AttachedBlocks, Block{ + Hash: &block.Hash, + Height: block.Height, + Timestamp: block.Time.Unix(), + }) + n++ + } + txs := s.currentTxNtfn.AttachedBlocks[n-1].Transactions + s.currentTxNtfn.AttachedBlocks[n-1].Transactions = append(txs, makeTxSummary(s.wallet, details)) +} + +func (s *NotificationServer) notifyAttachedBlock(block *wtxmgr.BlockMeta) { + if s.currentTxNtfn == nil { + s.currentTxNtfn = &TransactionNotifications{} + } + + // Add block details if it wasn't already included for previously + // notified mined transactions. + n := len(s.currentTxNtfn.AttachedBlocks) + if n == 0 || *s.currentTxNtfn.AttachedBlocks[n-1].Hash != block.Hash { + s.currentTxNtfn.AttachedBlocks = append(s.currentTxNtfn.AttachedBlocks, Block{ + Hash: &block.Hash, + Height: block.Height, + Timestamp: block.Time.Unix(), + }) + } + + // For now (until notification coalescing isn't necessary) just use + // chain length to determine if this is the new best block. + if s.wallet.ChainSynced() { + if len(s.currentTxNtfn.DetachedBlocks) >= len(s.currentTxNtfn.AttachedBlocks) { + return + } + } + + defer s.mu.Unlock() + s.mu.Lock() + clients := s.transactions + if len(clients) == 0 { + s.currentTxNtfn = nil + return + } + + // The UnminedTransactions field is intentionally not set. Since the + // hashes of all detached blocks are reported, and all transactions + // moved from a mined block back to unconfirmed are either in the + // UnminedTransactionHashes slice or don't exist due to conflicting with + // a mined transaction in the new best chain, there is no possiblity of + // a new, previously unseen transaction appearing in unconfirmed. + + unminedHashes, err := s.wallet.TxStore.UnminedTxHashes() + if err != nil { + log.Errorf("Cannot fetch unmined transaction hashes: %v", err) + return + } + s.currentTxNtfn.UnminedTransactionHashes = unminedHashes + + bals := make(map[uint32]btcutil.Amount) + for _, b := range s.currentTxNtfn.AttachedBlocks { + relevantAccounts(s.wallet, bals, b.Transactions) + } + err = totalBalances(s.wallet, bals) + if err != nil { + log.Errorf("Cannot determine balances for relevant accounts: %v", err) + return + } + s.currentTxNtfn.NewBalances = flattenBalanceMap(bals) + + for _, c := range clients { + c <- s.currentTxNtfn + } + s.currentTxNtfn = nil +} + +// TransactionNotifications is a notification of changes to the wallet's +// transaction set and the current chain tip that wallet is considered to be +// synced with. All transactions added to the blockchain are organized by the +// block they were mined in. +// +// During a chain switch, all removed block hashes are included. Detached +// blocks are sorted in the reverse order they were mined. Attached blocks are +// sorted in the order mined. +// +// All newly added unmined transactions are included. Removed unmined +// transactions are not explicitly included. Instead, the hashes of all +// transactions still unmined are included. +// +// If any transactions were involved, each affected account's new total balance +// is included. +// +// TODO: Because this includes stuff about blocks and can be fired without any +// changes to transactions, it needs a better name. +type TransactionNotifications struct { + AttachedBlocks []Block + DetachedBlocks []*wire.ShaHash + UnminedTransactions []TransactionSummary + UnminedTransactionHashes []*wire.ShaHash + NewBalances []AccountBalance +} + +// Block contains the properties and all relevant transactions of an attached +// block. +type Block struct { + Hash *wire.ShaHash + Height int32 + Timestamp int64 + Transactions []TransactionSummary +} + +// TransactionSummary contains a transaction relevant to the wallet and marks +// which inputs and outputs were relevant. +type TransactionSummary struct { + Hash *wire.ShaHash + Transaction []byte + MyInputs []TransactionSummaryInput + MyOutputs []TransactionSummaryOutput + Fee btcutil.Amount + Timestamp int64 +} + +// TransactionSummaryInput describes a transaction input that is relevant to the +// wallet. The Index field marks the transaction input index of the transaction +// (not included here). The PreviousAccount and PreviousAmount fields describe +// how much this input debits from a wallet account. +type TransactionSummaryInput struct { + Index uint32 + PreviousAccount uint32 + PreviousAmount btcutil.Amount +} + +// TransactionSummaryOutput describes a transaction output of a relevant +// transaction. When the transaction is authored by this wallet, all +// transaction outputs are considered relevant. The Mine field describes +// whether outputs to these authored transactions pay back to the wallet +// (e.g. change) or create an uncontrolled output. For convenience, the +// addresses (if any) of an uncontrolled output are included. +type TransactionSummaryOutput struct { + Index uint32 + Amount btcutil.Amount + Mine bool + + // Only relevant if mine==true. + Account uint32 + Internal bool + + // Only relevant if mine==false. + Addresses []btcutil.Address +} + +// AccountBalance associates a total (zero confirmation) balance with an +// account. Balances for other minimum confirmation counts require more +// expensive logic and it is not clear which minimums a client is interested in, +// so they are not included. +type AccountBalance struct { + Account uint32 + TotalBalance btcutil.Amount +} + +// TransactionNotificationsClient receives TransactionNotifications from the +// NotificationServer over the channel C. +type TransactionNotificationsClient struct { + C <-chan *TransactionNotifications + server *NotificationServer +} + +// TransactionNotifications returns a client for receiving +// TransactionNotifiations notifications over a channel. The channel is +// unbuffered. +// +// When finished, the Done method should be called on the client to disassociate +// it from the server. +func (s *NotificationServer) TransactionNotifications() TransactionNotificationsClient { + c := make(chan *TransactionNotifications) + s.mu.Lock() + s.transactions = append(s.transactions, c) + s.mu.Unlock() + return TransactionNotificationsClient{ + C: c, + server: s, + } +} + +// Done deregisters the client from the server and drains any remaining +// messages. It must be called exactly once when the client is finished +// receiving notifications. +func (c *TransactionNotificationsClient) Done() { + go func() { + // Drain notifications until the client channel is removed from + // the server and closed. + for range c.C { + } + }() + go func() { + s := c.server + s.mu.Lock() + clients := s.transactions + for i, ch := range clients { + if c.C == ch { + clients[i] = clients[len(clients)-1] + s.transactions = clients[:len(clients)-1] + close(ch) + break + } + } + s.mu.Unlock() + }() +} + +// SpentnessNotifications is a notification that is fired for transaction +// outputs controlled by some account's keys. The notification may be about a +// newly added unspent transaction output or that a previously unspent output is +// now spent. When spent, the notification includes the spending transaction's +// hash and input index. +type SpentnessNotifications struct { + hash *wire.ShaHash + spenderHash *wire.ShaHash + index uint32 + spenderIndex uint32 +} + +// Hash returns the transaction hash of the spent output. +func (n *SpentnessNotifications) Hash() *wire.ShaHash { + return n.hash +} + +// Index returns the transaction output index of the spent output. +func (n *SpentnessNotifications) Index() uint32 { + return n.index +} + +// Spender returns the spending transction's hash and input index, if any. If +// the output is unspent, the final bool return is false. +func (n *SpentnessNotifications) Spender() (*wire.ShaHash, uint32, bool) { + return n.spenderHash, n.spenderIndex, n.spenderHash != nil +} + +// notifyUnspentOutput notifies registered clients of a new unspent output that +// is controlled by the wallet. +func (s *NotificationServer) notifyUnspentOutput(account uint32, hash *wire.ShaHash, index uint32) { + defer s.mu.Unlock() + s.mu.Lock() + clients := s.spentness[account] + if len(clients) == 0 { + return + } + n := &SpentnessNotifications{ + hash: hash, + index: index, + } + for _, c := range clients { + c <- n + } +} + +// notifySpentOutput notifies registered clients that a previously-unspent +// output is now spent, and includes the spender hash and input index in the +// notification. +func (s *NotificationServer) notifySpentOutput(account uint32, op *wire.OutPoint, spenderHash *wire.ShaHash, spenderIndex uint32) { + defer s.mu.Unlock() + s.mu.Lock() + clients := s.spentness[account] + if len(clients) == 0 { + return + } + n := &SpentnessNotifications{ + hash: &op.Hash, + index: op.Index, + spenderHash: spenderHash, + spenderIndex: spenderIndex, + } + for _, c := range clients { + c <- n + } +} + +// SpentnessNotificationsClient receives SpentnessNotifications from the +// NotificationServer over the channel C. +type SpentnessNotificationsClient struct { + C <-chan *SpentnessNotifications + account uint32 + server *NotificationServer +} + +// AccountSpentnessNotifications registers a client for spentness changes of +// outputs controlled by the account. +func (s *NotificationServer) AccountSpentnessNotifications(account uint32) SpentnessNotificationsClient { + c := make(chan *SpentnessNotifications) + s.mu.Lock() + s.spentness[account] = append(s.spentness[account], c) + s.mu.Unlock() + return SpentnessNotificationsClient{ + C: c, + account: account, + server: s, + } +} + +// Done deregisters the client from the server and drains any remaining +// messages. It must be called exactly once when the client is finished +// receiving notifications. +func (c *SpentnessNotificationsClient) Done() { + go func() { + // Drain notifications until the client channel is removed from + // the server and closed. + for range c.C { + } + }() + go func() { + s := c.server + s.mu.Lock() + clients := s.spentness[c.account] + for i, ch := range clients { + if c.C == ch { + clients[i] = clients[len(clients)-1] + s.spentness[c.account] = clients[:len(clients)-1] + close(ch) + break + } + } + s.mu.Unlock() + }() +} + +// AccountNotification contains properties regarding an account, such as its +// name and the number of derived and imported keys. When any of these +// properties change, the notification is fired. +type AccountNotification struct { + AccountNumber uint32 + AccountName string + ExternalKeyCount uint32 + InternalKeyCount uint32 + ImportedKeyCount uint32 +} + +func (s *NotificationServer) notifyAccountProperties(props *waddrmgr.AccountProperties) { + defer s.mu.Unlock() + s.mu.Lock() + clients := s.accountClients + if len(clients) == 0 { + return + } + n := &AccountNotification{ + AccountNumber: props.AccountNumber, + AccountName: props.AccountName, + ExternalKeyCount: props.ExternalKeyCount, + InternalKeyCount: props.InternalKeyCount, + ImportedKeyCount: props.ImportedKeyCount, + } + for _, c := range clients { + c <- n + } +} + +// AccountNotificationsClient receives AccountNotifications over the channel C. +type AccountNotificationsClient struct { + C chan *AccountNotification + server *NotificationServer +} + +// AccountNotifications returns a client for receiving AccountNotifications over +// a channel. The channel is unbuffered. When finished, the client's Done +// method should be called to disassociate the client from the server. +func (s *NotificationServer) AccountNotifications() AccountNotificationsClient { + c := make(chan *AccountNotification) + s.mu.Lock() + s.accountClients = append(s.accountClients, c) + s.mu.Unlock() + return AccountNotificationsClient{ + C: c, + server: s, + } +} + +// Done deregisters the client from the server and drains any remaining +// messages. It must be called exactly once when the client is finished +// receiving notifications. +func (c *AccountNotificationsClient) Done() { + go func() { + for range c.C { + } + }() + go func() { + s := c.server + s.mu.Lock() + clients := s.accountClients + for i, ch := range clients { + if c.C == ch { + clients[i] = clients[len(clients)-1] + s.accountClients = clients[:len(clients)-1] + close(ch) + break + } + } + s.mu.Unlock() + }() +} diff --git a/wallet/rescan.go b/wallet/rescan.go index fa73437..7d5d9e4 100644 --- a/wallet/rescan.go +++ b/wallet/rescan.go @@ -238,6 +238,13 @@ out: // RPC requests to perform a rescan. New jobs are not read until a rescan // finishes. func (w *Wallet) rescanRPCHandler() { + chainClient, err := w.requireChainClient() + if err != nil { + log.Errorf("rescanRPCHandler called without an RPC client") + w.wg.Done() + return + } + quit := w.quitChan() out: @@ -250,7 +257,7 @@ out: log.Infof("Started rescan from block %v (height %d) for %d %s", batch.bs.Hash, batch.bs.Height, numAddrs, noun) - err := w.chainSvr.Rescan(&batch.bs.Hash, batch.addrs, + err := chainClient.Rescan(&batch.bs.Hash, batch.addrs, batch.outpoints) if err != nil { log.Errorf("Rescan for %d %s failed: %v", numAddrs, diff --git a/wallet/wallet.go b/wallet/wallet.go index 690cd29..e877ab8 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013-2015 The btcsuite developers + * Copyright (c) 2013-2016 The btcsuite developers * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -30,10 +30,12 @@ import ( "time" "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcrpcclient" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcwallet/chain" "github.com/btcsuite/btcwallet/waddrmgr" @@ -42,6 +44,16 @@ import ( ) const ( + // InsecurePubPassphrase is the default outer encryption passphrase used + // for public data (everything but private keys). Using a non-default + // public passphrase can prevent an attacker without the public + // passphrase from discovering all past and future wallet addresses if + // they gain access to the wallet database. + // + // NOTE: at time of writing, public encryption only applies to public + // data in the waddrmgr namespace. Transactions are not yet encrypted. + InsecurePubPassphrase = "public" + walletDbWatchingOnlyName = "wowallet.db" ) @@ -60,15 +72,17 @@ var ( // complete wallet. It contains the Armory-style key store // addresses and keys), type Wallet struct { + publicPassphrase []byte + // Data stores db walletdb.DB Manager *waddrmgr.Manager TxStore *wtxmgr.Store - chainSvr *chain.Client - chainSvrLock sync.Mutex - chainSvrSynced bool - chainSvrSyncMtx sync.Mutex + chainClient *chain.RPCClient + chainClientLock sync.Mutex + chainClientSynced bool + chainClientSyncMtx sync.Mutex lockedOutpoints map[wire.OutPoint]struct{} FeeIncrement btcutil.Amount @@ -93,9 +107,14 @@ type Wallet struct { lockState chan bool changePassphrase chan changePassphraseRequest - // Notification channels so other components can listen in on wallet - // activity. These are initialized as nil, and must be created by - // calling one of the Listen* methods. + NtfnServer *NotificationServer + + // Legacy notification channels so other components can listen in on + // wallet activity. These are initialized as nil, and must be created + // by calling one of the Listen* methods. + // + // These channels and the features needed by them are on a fast path to + // deletion. Use the server instead. connectedBlocks chan wtxmgr.BlockMeta disconnectedBlocks chan wtxmgr.BlockMeta relevantTxs chan chain.RelevantTx @@ -263,7 +282,7 @@ func (w *Wallet) notifyRelevantTx(relevantTx chain.RelevantTx) { } // Start starts the goroutines necessary to manage a wallet. -func (w *Wallet) Start(chainServer *chain.Client) { +func (w *Wallet) Start() { w.quitMu.Lock() select { case <-w.quit: @@ -280,19 +299,74 @@ func (w *Wallet) Start(chainServer *chain.Client) { } w.quitMu.Unlock() - w.chainSvrLock.Lock() - w.chainSvr = chainServer - w.chainSvrLock.Unlock() - - w.wg.Add(6) - go w.handleChainNotifications() + w.wg.Add(2) go w.txCreator() go w.walletLocker() +} + +// SynchronizeRPC associates the wallet with the consensus RPC client, +// synchronizes the wallet with the latest changes to the blockchain, and +// continuously updates the wallet through RPC notifications. +// +// This method is unstable and will be removed when all syncing logic is moved +// outside of the wallet package. +func (w *Wallet) SynchronizeRPC(chainClient *chain.RPCClient) { + w.quitMu.Lock() + select { + case <-w.quit: + w.quitMu.Unlock() + return + default: + } + w.quitMu.Unlock() + + // TODO: Ignoring the new client when one is already set breaks callers + // who are replacing the client, perhaps after a disconnect. + w.chainClientLock.Lock() + if w.chainClient != nil { + w.chainClientLock.Unlock() + return + } + w.chainClient = chainClient + w.chainClientLock.Unlock() + + // TODO: It would be preferable to either run these goroutines + // separately from the wallet (use wallet mutator functions to + // make changes from the RPC client) and not have to stop and + // restart them each time the client disconnects and reconnets. + w.wg.Add(4) + go w.handleChainNotifications() go w.rescanBatchHandler() go w.rescanProgressHandler() go w.rescanRPCHandler() } +// requireChainClient marks that a wallet method can only be completed when the +// consensus RPC server is set. This function and all functions that call it +// are unstable and will need to be moved when the syncing code is moved out of +// the wallet. +func (w *Wallet) requireChainClient() (*chain.RPCClient, error) { + w.chainClientLock.Lock() + chainClient := w.chainClient + w.chainClientLock.Unlock() + if chainClient == nil { + return nil, errors.New("blockchain RPC is inactive") + } + return chainClient, nil +} + +// ChainClient returns the optional consensus RPC client associated with the +// wallet. +// +// This function is unstable and will be removed once sync logic is moved out of +// the wallet. +func (w *Wallet) ChainClient() *chain.RPCClient { + w.chainClientLock.Lock() + chainClient := w.chainClient + w.chainClientLock.Unlock() + return chainClient +} + // quitChan atomically reads the quit channel. func (w *Wallet) quitChan() <-chan struct{} { w.quitMu.Lock() @@ -311,11 +385,12 @@ func (w *Wallet) Stop() { case <-quit: default: close(quit) - w.chainSvrLock.Lock() - if w.chainSvr != nil { - w.chainSvr.Stop() + w.chainClientLock.Lock() + if w.chainClient != nil { + w.chainClient.Stop() + w.chainClient = nil } - w.chainSvrLock.Unlock() + w.chainClientLock.Unlock() } } @@ -332,20 +407,33 @@ func (w *Wallet) ShuttingDown() bool { // WaitForShutdown blocks until all wallet goroutines have finished executing. func (w *Wallet) WaitForShutdown() { - w.chainSvrLock.Lock() - if w.chainSvr != nil { - w.chainSvr.WaitForShutdown() + w.chainClientLock.Lock() + if w.chainClient != nil { + w.chainClient.WaitForShutdown() } - w.chainSvrLock.Unlock() + w.chainClientLock.Unlock() w.wg.Wait() } +// SynchronizingToNetwork returns whether the wallet is currently synchronizing +// with the Bitcoin network. +func (w *Wallet) SynchronizingToNetwork() bool { + // At the moment, RPC is the only synchronization method. In the + // future, when SPV is added, a separate check will also be needed, or + // SPV could always be enabled if RPC was not explicitly specified when + // creating the wallet. + w.chainClientSyncMtx.Lock() + syncing := w.chainClient != nil + w.chainClientSyncMtx.Unlock() + return syncing +} + // ChainSynced returns whether the wallet has been attached to a chain server // and synced up to the best block on the main chain. func (w *Wallet) ChainSynced() bool { - w.chainSvrSyncMtx.Lock() - synced := w.chainSvrSynced - w.chainSvrSyncMtx.Unlock() + w.chainClientSyncMtx.Lock() + synced := w.chainClientSynced + w.chainClientSyncMtx.Unlock() return synced } @@ -357,9 +445,9 @@ func (w *Wallet) ChainSynced() bool { // until the reconnect notification is received, at which point the wallet can be // marked out of sync again until after the next rescan completes. func (w *Wallet) SetChainSynced(synced bool) { - w.chainSvrSyncMtx.Lock() - w.chainSvrSynced = synced - w.chainSvrSyncMtx.Unlock() + w.chainClientSyncMtx.Lock() + w.chainClientSynced = synced + w.chainClientSyncMtx.Unlock() } // activeData returns the currently-active receiving addresses and all unspent @@ -383,6 +471,11 @@ func (w *Wallet) activeData() ([]btcutil.Address, []wtxmgr.Credit, error) { // finished. // func (w *Wallet) syncWithChain() error { + chainClient, err := w.requireChainClient() + if err != nil { + return err + } + // Request notifications for connected and disconnected blocks. // // TODO(jrick): Either request this notification only once, or when @@ -391,7 +484,7 @@ func (w *Wallet) syncWithChain() error { // as well. I am leaning towards allowing off all btcrpcclient // notification re-registrations, in which case the code here should be // left as is. - err := w.chainSvr.NotifyBlocks() + err = chainClient.NotifyBlocks() if err != nil { return err } @@ -406,6 +499,28 @@ func (w *Wallet) syncWithChain() error { // TODO(jrick): How should this handle a synced height earlier than // the chain server best block? + // When no addresses have been generated for the wallet, the rescan can + // be skipped. + // + // TODO: This is only correct because activeData above returns all + // addresses ever created, including those that don't need to be watched + // anymore. This code should be updated when this assumption is no + // longer true, but worst case would result in an unnecessary rescan. + if len(addrs) == 0 && len(unspent) == 0 { + // TODO: It would be ideal if on initial sync wallet saved the + // last several recent blocks rather than just one. This would + // avoid a full rescan for a one block reorg of the current + // chain tip. + hash, height, err := chainClient.GetBestBlock() + if err != nil { + return err + } + return w.Manager.SetSyncedTo(&waddrmgr.BlockStamp{ + Hash: *hash, + Height: height, + }) + } + // Compare previously-seen blocks against the chain server. If any of // these blocks no longer exist, rollback all of the missing blocks // before catching up with the rescan. @@ -419,7 +534,7 @@ func (w *Wallet) syncWithChain() error { bs := iter.BlockStamp() log.Debugf("Checking for previous saved block with height %v hash %v", bs.Height, bs.Hash) - _, err = w.chainSvr.GetBlock(&bs.Hash) + _, err = chainClient.GetBlock(&bs.Hash) if err != nil { rollback = true continue @@ -508,7 +623,7 @@ func (w *Wallet) CreateSimpleTx(account uint32, pairs map[string]btcutil.Amount, type ( unlockRequest struct { passphrase []byte - timeout time.Duration // Zero value prevents the timeout. + lockAfter <-chan time.Time // nil prevents the timeout. err chan error } @@ -540,11 +655,7 @@ out: continue } w.notifyLockStateChange(false) - if req.timeout == 0 { - timeout = nil - } else { - timeout = time.After(req.timeout) - } + timeout = req.lockAfter req.err <- nil continue @@ -582,6 +693,7 @@ out: break out case <-w.lockRequests: + timeout = nil case <-timeout: } @@ -605,11 +717,11 @@ out: // correct, the current timeout is replaced with the new one. The wallet will // be locked if the passphrase is incorrect or any other error occurs during the // unlock. -func (w *Wallet) Unlock(passphrase []byte, timeout time.Duration) error { +func (w *Wallet) Unlock(passphrase []byte, lock <-chan time.Time) error { err := make(chan error, 1) w.unlockRequests <- unlockRequest{ passphrase: passphrase, - timeout: timeout, + lockAfter: lock, err: err, } return <-err @@ -703,10 +815,22 @@ func (w *Wallet) CalculateBalance(confirms int32) (btcutil.Amount, error) { return w.TxStore.Balance(confirms, blk.Height) } -// CalculateAccountBalance sums the amounts of all unspent transaction +// Balances records total, spendable (by policy), and immature coinbase +// reward balance amounts. +type Balances struct { + Total btcutil.Amount + Spendable btcutil.Amount + ImmatureReward btcutil.Amount +} + +// CalculateAccountBalances sums the amounts of all unspent transaction // outputs to the given account of a wallet and returns the balance. -func (w *Wallet) CalculateAccountBalance(account uint32, confirms int32) (btcutil.Amount, error) { - var bal btcutil.Amount +// +// This function is much slower than it needs to be since transactions outputs +// are not indexed by the accounts they credit to, and all unspent transaction +// outputs must be iterated. +func (w *Wallet) CalculateAccountBalances(account uint32, confirms int32) (Balances, error) { + var bals Balances // Get current block. The block height used for calculating // the number of tx confirmations. @@ -714,32 +838,32 @@ func (w *Wallet) CalculateAccountBalance(account uint32, confirms int32) (btcuti unspent, err := w.TxStore.UnspentOutputs() if err != nil { - return 0, err + return bals, err } for i := range unspent { output := &unspent[i] - if !confirmed(confirms, output.Height, syncBlock.Height) { - continue - } - if output.FromCoinBase { - const target = blockchain.CoinbaseMaturity - if !confirmed(target, output.Height, syncBlock.Height) { - continue - } - } - var outputAcct uint32 _, addrs, _, err := txscript.ExtractPkScriptAddrs( output.PkScript, w.chainParams) if err == nil && len(addrs) > 0 { outputAcct, err = w.Manager.AddrAccount(addrs[0]) } - if err == nil && outputAcct == account { - bal += output.Amount + if err != nil || outputAcct != account { + continue + } + + bals.Total += output.Amount + if output.FromCoinBase { + const target = blockchain.CoinbaseMaturity + if !confirmed(target, output.Height, syncBlock.Height) { + bals.ImmatureReward += output.Amount + } + } else if confirmed(confirms, output.Height, syncBlock.Height) { + bals.Spendable += output.Amount } } - return bal, nil + return bals, nil } // CurrentAddress gets the most recently requested Bitcoin payment address @@ -768,6 +892,43 @@ func (w *Wallet) CurrentAddress(account uint32) (btcutil.Address, error) { return addr.Address(), nil } +// RenameAccount sets the name for an account number to newName. +func (w *Wallet) RenameAccount(account uint32, newName string) error { + err := w.Manager.RenameAccount(account, newName) + if err != nil { + return err + } + + props, err := w.Manager.AccountProperties(account) + if err != nil { + log.Errorf("Cannot fetch new account properties for notification "+ + "during account rename: %v", err) + } else { + w.NtfnServer.notifyAccountProperties(props) + } + + return nil +} + +// NextAccount creates the next account and returns its account number. The +// name must be unique to the account. +func (w *Wallet) NextAccount(name string) (uint32, error) { + account, err := w.Manager.NewAccount(name) + if err != nil { + return 0, err + } + + props, err := w.Manager.AccountProperties(account) + if err != nil { + log.Errorf("Cannot fetch new account properties for notification "+ + "after account creation: %v", err) + } else { + w.NtfnServer.notifyAccountProperties(props) + } + + return account, nil +} + // CreditCategory describes the type of wallet transaction output. The category // of "sent transactions" (debits) is always "send", and is not expressed by // this type. @@ -1066,6 +1227,189 @@ func (w *Wallet) ListAllTransactions() ([]btcjson.ListTransactionsResult, error) return txList, err } +// BlockIdentifier identifies a block by either a height or a hash. +type BlockIdentifier struct { + height int32 + hash *wire.ShaHash +} + +// NewBlockIdentifierFromHeight constructs a BlockIdentifier for a block height. +func NewBlockIdentifierFromHeight(height int32) *BlockIdentifier { + return &BlockIdentifier{height: height} +} + +// NewBlockIdentifierFromHash constructs a BlockIdentifier for a block hash. +func NewBlockIdentifierFromHash(hash *wire.ShaHash) *BlockIdentifier { + return &BlockIdentifier{hash: hash} +} + +// GetTransactionsResult is the result of the wallet's GetTransactions method. +// See GetTransactions for more details. +type GetTransactionsResult struct { + MinedTransactions []Block + UnminedTransactions []TransactionSummary +} + +// GetTransactions returns transaction results between a starting and ending +// block. Blocks in the block range may be specified by either a height or a +// hash. +// +// Because this is a possibly lenghtly operation, a cancel channel is provided +// to cancel the task. If this channel unblocks, the results created thus far +// will be returned. +// +// Transaction results are organized by blocks in ascending order and unmined +// transactions in an unspecified order. Mined transactions are saved in a +// Block structure which records properties about the block. +func (w *Wallet) GetTransactions(startBlock, endBlock *BlockIdentifier, cancel <-chan struct{}) (*GetTransactionsResult, error) { + var start, end int32 = 0, -1 + + w.chainClientLock.Lock() + chainClient := w.chainClient + w.chainClientLock.Unlock() + + // TODO: Fetching block heights by their hashes is inherently racy + // because not all block headers are saved but when they are for SPV the + // db can be queried directly without this. + var startResp, endResp btcrpcclient.FutureGetBlockVerboseResult + if startBlock != nil { + if startBlock.hash == nil { + start = startBlock.height + } else { + if chainClient == nil { + return nil, errors.New("no chain server client") + } + startResp = chainClient.GetBlockVerboseAsync(startBlock.hash, false) + } + } + if endBlock != nil { + if endBlock.hash == nil { + end = endBlock.height + } else { + if chainClient == nil { + return nil, errors.New("no chain server client") + } + endResp = chainClient.GetBlockVerboseAsync(endBlock.hash, false) + } + } + if startResp != nil { + resp, err := startResp.Receive() + if err != nil { + return nil, err + } + start = int32(resp.Height) + } + if endResp != nil { + resp, err := endResp.Receive() + if err != nil { + return nil, err + } + end = int32(resp.Height) + } + + var res GetTransactionsResult + err := w.TxStore.RangeTransactions(start, end, func(details []wtxmgr.TxDetails) (bool, error) { + // TODO: probably should make RangeTransactions not reuse the + // details backing array memory. + dets := make([]wtxmgr.TxDetails, len(details)) + copy(dets, details) + details = dets + + txs := make([]TransactionSummary, 0, len(details)) + for i := range details { + txs = append(txs, makeTxSummary(w, &details[i])) + } + + if details[0].Block.Height != -1 { + blockHash := details[0].Block.Hash + res.MinedTransactions = append(res.MinedTransactions, Block{ + Hash: &blockHash, + Height: details[0].Block.Height, + Timestamp: details[0].Block.Time.Unix(), + Transactions: txs, + }) + } else { + res.UnminedTransactions = txs + } + + select { + case <-cancel: + return true, nil + default: + return false, nil + } + }) + return &res, err +} + +// AccountResult is a single account result for the AccountsResult type. +type AccountResult struct { + waddrmgr.AccountProperties + TotalBalance btcutil.Amount +} + +// AccountsResult is the resutl of the wallet's Accounts method. See that +// method for more details. +type AccountsResult struct { + Accounts []AccountResult + CurrentBlockHash *wire.ShaHash + CurrentBlockHeight int32 +} + +// Accounts returns the current names, numbers, and total balances of all +// accounts in the wallet. The current chain tip is included in the result for +// atomicity reasons. +// +// TODO(jrick): Is the chain tip really needed, since only the total balances +// are included? +func (w *Wallet) Accounts() (*AccountsResult, error) { + var accounts []AccountResult + syncBlock := w.Manager.SyncedTo() + unspent, err := w.TxStore.UnspentOutputs() + if err != nil { + return nil, err + } + err = w.Manager.ForEachAccount(func(acct uint32) error { + props, err := w.Manager.AccountProperties(acct) + if err != nil { + return err + } + accounts = append(accounts, AccountResult{ + AccountProperties: *props, + // TotalBalance set below + }) + return nil + }) + if err != nil { + return nil, err + } + m := make(map[uint32]*btcutil.Amount) + for i := range accounts { + a := &accounts[i] + m[a.AccountNumber] = &a.TotalBalance + } + for i := range unspent { + output := &unspent[i] + var outputAcct uint32 + _, addrs, _, err := txscript.ExtractPkScriptAddrs( + output.PkScript, w.chainParams) + if err == nil && len(addrs) > 0 { + outputAcct, err = w.Manager.AddrAccount(addrs[0]) + } + if err == nil { + amt, ok := m[outputAcct] + if ok { + *amt += output.Amount + } + } + } + return &AccountsResult{ + Accounts: accounts, + CurrentBlockHash: &syncBlock.Hash, + CurrentBlockHeight: syncBlock.Height, + }, nil +} + // creditSlice satisifies the sort.Interface interface to provide sorting // transaction credits from oldest to newest. Credits with the same receive // time and mined in the same block are not guaranteed to be sorted by the order @@ -1331,13 +1675,21 @@ func (w *Wallet) ImportPrivateKey(wif *btcutil.WIF, bs *waddrmgr.BlockStamp, addrStr := addr.Address().EncodeAddress() log.Infof("Imported payment address %s", addrStr) + props, err := w.Manager.AccountProperties(waddrmgr.ImportedAddrAccount) + if err != nil { + log.Errorf("Cannot fetch account properties for imported "+ + "account after importing key: %v", err) + } else { + w.NtfnServer.notifyAccountProperties(props) + } + // Return the payment address string of the imported private key. return addrStr, nil } // ExportWatchingWallet returns a watching-only version of the wallet serialized // database as a base64-encoded string. -func (w *Wallet) ExportWatchingWallet(pubPass string) (string, error) { +func (w *Wallet) ExportWatchingWallet() (string, error) { tmpDir, err := ioutil.TempDir("", "btcwallet") if err != nil { return "", err @@ -1370,7 +1722,7 @@ func (w *Wallet) ExportWatchingWallet(pubPass string) (string, error) { if err != nil { return "", err } - woMgr, err := waddrmgr.Open(namespace, []byte(pubPass), + woMgr, err := waddrmgr.Open(namespace, w.publicPassphrase, w.chainParams, nil) if err != nil { return "", err @@ -1449,13 +1801,19 @@ func (w *Wallet) LockedOutpoints() []btcjson.TransactionInput { // credits that are not known to have been mined into a block, and attempts // to send each to the chain server for relay. func (w *Wallet) ResendUnminedTxs() { + chainClient, err := w.requireChainClient() + if err != nil { + log.Errorf("No chain server available to resend unmined transactions") + return + } + txs, err := w.TxStore.UnminedTxs() if err != nil { log.Errorf("Cannot load unmined transactions for resending: %v", err) return } for _, tx := range txs { - resp, err := w.chainSvr.SendRawTransaction(tx, false) + resp, err := chainClient.SendRawTransaction(tx, false) if err != nil { // TODO(jrick): Check error for if this tx is a double spend, // remove it if so. @@ -1496,8 +1854,22 @@ func (w *Wallet) NewAddress(account uint32) (btcutil.Address, error) { for i, addr := range addrs { utilAddrs[i] = addr.Address() } - if err := w.chainSvr.NotifyReceived(utilAddrs); err != nil { - return nil, err + w.chainClientLock.Lock() + chainClient := w.chainClient + w.chainClientLock.Unlock() + if chainClient != nil { + err := chainClient.NotifyReceived(utilAddrs) + if err != nil { + return nil, err + } + } + + props, err := w.Manager.AccountProperties(account) + if err != nil { + log.Errorf("Cannot fetch account properties for notification "+ + "after deriving next external address: %v", err) + } else { + w.NtfnServer.notifyAccountProperties(props) } return utilAddrs[0], nil @@ -1517,8 +1889,12 @@ func (w *Wallet) NewChangeAddress(account uint32) (btcutil.Address, error) { utilAddrs[i] = addr.Address() } - if err := w.chainSvr.NotifyReceived(utilAddrs); err != nil { - return nil, err + chainClient, err := w.requireChainClient() + if err == nil { + err = chainClient.NotifyReceived(utilAddrs) + if err != nil { + return nil, err + } } return utilAddrs[0], nil @@ -1629,6 +2005,11 @@ func (w *Wallet) TotalReceivedForAddr(addr btcutil.Address, minConf int32) (btcu func (w *Wallet) SendPairs(amounts map[string]btcutil.Amount, account uint32, minconf int32) (*wire.ShaHash, error) { + chainClient, err := w.requireChainClient() + if err != nil { + return nil, err + } + // Create transaction, replying with an error if the creation // was not successful. createdTx, err := w.CreateSimpleTx(account, amounts, minconf) @@ -1659,7 +2040,160 @@ func (w *Wallet) SendPairs(amounts map[string]btcutil.Amount, account uint32, // TODO: The record already has the serialized tx, so no need to // serialize it again. - return w.chainSvr.SendRawTransaction(&rec.MsgTx, false) + return chainClient.SendRawTransaction(&rec.MsgTx, false) +} + +// SignatureError records the underlying error when validating a transaction +// input signature. +type SignatureError struct { + InputIndex uint32 + Error error +} + +// SignTransaction uses secrets of the wallet, as well as additional secrets +// passed in by the caller, to create and add input signatures to a transaction. +// +// Transaction input script validation is used to confirm that all signatures +// are valid. For any invalid input, a SignatureError is added to the returns. +// The final error return is reserved for unexpected or fatal errors, such as +// being unable to determine a previous output script to redeem. +// +// The transaction pointed to by tx is modified by this function. +func (w *Wallet) SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType, + additionalPrevScripts map[wire.OutPoint][]byte, + additionalKeysByAddress map[string]*btcutil.WIF, + p2shRedeemScriptsByAddress map[string][]byte) ([]SignatureError, error) { + + var signErrors []SignatureError + for i, txIn := range tx.TxIn { + prevOutScript, ok := additionalPrevScripts[txIn.PreviousOutPoint] + if !ok { + prevHash := &txIn.PreviousOutPoint.Hash + prevIndex := txIn.PreviousOutPoint.Index + txDetails, err := w.TxStore.TxDetails(prevHash) + if err != nil { + return nil, fmt.Errorf("%v not found", + txIn.PreviousOutPoint) + } + prevOutScript = txDetails.MsgTx.TxOut[prevIndex].PkScript + } + + // Set up our callbacks that we pass to txscript so it can + // look up the appropriate keys and scripts by address. + getKey := txscript.KeyClosure(func(addr btcutil.Address) ( + *btcec.PrivateKey, bool, error) { + if len(additionalKeysByAddress) != 0 { + addrStr := addr.EncodeAddress() + wif, ok := additionalKeysByAddress[addrStr] + if !ok { + return nil, false, + errors.New("no key for address") + } + return wif.PrivKey, wif.CompressPubKey, nil + } + address, err := w.Manager.Address(addr) + if err != nil { + return nil, false, err + } + + pka, ok := address.(waddrmgr.ManagedPubKeyAddress) + if !ok { + return nil, false, errors.New("address is not " + + "a pubkey address") + } + + key, err := pka.PrivKey() + if err != nil { + return nil, false, err + } + + return key, pka.Compressed(), nil + }) + + getScript := txscript.ScriptClosure(func( + addr btcutil.Address) ([]byte, error) { + // If keys were provided then we can only use the + // redeem scripts provided with our inputs, too. + if len(additionalKeysByAddress) != 0 { + addrStr := addr.EncodeAddress() + script, ok := p2shRedeemScriptsByAddress[addrStr] + if !ok { + return nil, errors.New("no script for " + + "address") + } + return script, nil + } + address, err := w.Manager.Address(addr) + if err != nil { + return nil, err + } + sa, ok := address.(waddrmgr.ManagedScriptAddress) + if !ok { + return nil, errors.New("address is not a script" + + " address") + } + + return sa.Script() + }) + + // SigHashSingle inputs can only be signed if there's a + // corresponding output. However this could be already signed, + // so we always verify the output. + if (hashType&txscript.SigHashSingle) != + txscript.SigHashSingle || i < len(tx.TxOut) { + + script, err := txscript.SignTxOutput(w.ChainParams(), + tx, i, prevOutScript, hashType, getKey, + getScript, txIn.SignatureScript) + // Failure to sign isn't an error, it just means that + // the tx isn't complete. + if err != nil { + signErrors = append(signErrors, SignatureError{ + InputIndex: uint32(i), + Error: err, + }) + continue + } + txIn.SignatureScript = script + } + + // Either it was already signed or we just signed it. + // Find out if it is completely satisfied or still needs more. + vm, err := txscript.NewEngine(prevOutScript, tx, i, + txscript.StandardVerifyFlags, nil) + if err == nil { + err = vm.Execute() + } + if err != nil { + signErrors = append(signErrors, SignatureError{ + InputIndex: uint32(i), + Error: err, + }) + } + } + + return signErrors, nil +} + +// PublishTransaction sends the transaction to the consensus RPC server so it +// can be propigated to other nodes and eventually mined. +// +// This function is unstable and will be removed once syncing code is moved out +// of the wallet. +func (w *Wallet) PublishTransaction(tx *wire.MsgTx) error { + server, err := w.requireChainClient() + if err != nil { + return err + } + + _, err = server.SendRawTransaction(tx, false) + return err +} + +// ChainParams returns the network parameters for the blockchain the wallet +// belongs to. +func (w *Wallet) ChainParams() *chaincfg.Params { + return w.chainParams } // Open loads an already-created wallet from the passed database and namespaces. @@ -1670,22 +2204,24 @@ func Open(pubPass []byte, params *chaincfg.Params, db walletdb.DB, waddrmgrNS, w } txMgr, err := wtxmgr.Open(wtxmgrNS) if err != nil { - if !wtxmgr.IsNoExists(err) { - return nil, err - } - log.Info("No recorded transaction history -- needs full rescan") - err = addrMgr.SetSyncedTo(nil) - if err != nil { - return nil, err - } - txMgr, err = wtxmgr.Create(wtxmgrNS) - if err != nil { + if wtxmgr.IsNoExists(err) { + log.Info("No recorded transaction history -- needs full rescan") + err = addrMgr.SetSyncedTo(nil) + if err != nil { + return nil, err + } + txMgr, err = wtxmgr.Create(wtxmgrNS) + if err != nil { + return nil, err + } + } else { return nil, err } } log.Infof("Opened wallet") // TODO: log balance? last sync height? w := &Wallet{ + publicPassphrase: pubPass, db: db, Manager: addrMgr, TxStore: txMgr, @@ -1705,5 +2241,9 @@ func Open(pubPass []byte, params *chaincfg.Params, db walletdb.DB, waddrmgrNS, w chainParams: params, quit: make(chan struct{}), } + w.NtfnServer = newNotificationServer(w) + w.TxStore.NotifyUnspent = func(hash *wire.ShaHash, index uint32) { + w.NtfnServer.notifyUnspentOutput(0, hash, index) + } return w, nil } diff --git a/walletsetup.go b/walletsetup.go index 5a74599..f7f927b 100644 --- a/walletsetup.go +++ b/walletsetup.go @@ -18,12 +18,9 @@ package main import ( "bufio" - "bytes" - "encoding/hex" "fmt" "os" "path/filepath" - "strings" "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/chaincfg" @@ -31,11 +28,11 @@ import ( "github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil/hdkeychain" "github.com/btcsuite/btcwallet/internal/legacy/keystore" + "github.com/btcsuite/btcwallet/internal/prompt" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/walletdb" _ "github.com/btcsuite/btcwallet/walletdb/bdb" - "github.com/btcsuite/golangcrypto/ssh/terminal" ) // Namespace keys @@ -61,311 +58,6 @@ func networkDir(dataDir string, chainParams *chaincfg.Params) string { return filepath.Join(dataDir, netname) } -// promptSeed is used to prompt for the wallet seed which maybe required during -// upgrades. -func promptSeed() ([]byte, error) { - reader := bufio.NewReader(os.Stdin) - for { - fmt.Print("Enter existing wallet seed: ") - seedStr, err := reader.ReadString('\n') - if err != nil { - return nil, err - } - seedStr = strings.TrimSpace(strings.ToLower(seedStr)) - - seed, err := hex.DecodeString(seedStr) - if err != nil || len(seed) < hdkeychain.MinSeedBytes || - len(seed) > hdkeychain.MaxSeedBytes { - - fmt.Printf("Invalid seed specified. Must be a "+ - "hexadecimal value that is at least %d bits and "+ - "at most %d bits\n", hdkeychain.MinSeedBytes*8, - hdkeychain.MaxSeedBytes*8) - continue - } - - return seed, nil - } -} - -// promptPrivPassPhrase is used to prompt for the private passphrase which maybe -// required during upgrades. -func promptPrivPassPhrase() ([]byte, error) { - prompt := "Enter the private passphrase of your wallet: " - for { - fmt.Print(prompt) - pass, err := terminal.ReadPassword(int(os.Stdin.Fd())) - if err != nil { - return nil, err - } - fmt.Print("\n") - pass = bytes.TrimSpace(pass) - if len(pass) == 0 { - continue - } - - return pass, nil - } -} - -// promptConsoleList prompts the user with the given prefix, list of valid -// responses, and default list entry to use. The function will repeat the -// prompt to the user until they enter a valid response. -func promptConsoleList(reader *bufio.Reader, prefix string, validResponses []string, defaultEntry string) (string, error) { - // Setup the prompt according to the parameters. - validStrings := strings.Join(validResponses, "/") - var prompt string - if defaultEntry != "" { - prompt = fmt.Sprintf("%s (%s) [%s]: ", prefix, validStrings, - defaultEntry) - } else { - prompt = fmt.Sprintf("%s (%s): ", prefix, validStrings) - } - - // Prompt the user until one of the valid responses is given. - for { - fmt.Print(prompt) - reply, err := reader.ReadString('\n') - if err != nil { - return "", err - } - reply = strings.TrimSpace(strings.ToLower(reply)) - if reply == "" { - reply = defaultEntry - } - - for _, validResponse := range validResponses { - if reply == validResponse { - return reply, nil - } - } - } -} - -// promptConsoleListBool prompts the user for a boolean (yes/no) with the given -// prefix. The function will repeat the prompt to the user until they enter a -// valid reponse. -func promptConsoleListBool(reader *bufio.Reader, prefix string, defaultEntry string) (bool, error) { - // Setup the valid responses. - valid := []string{"n", "no", "y", "yes"} - response, err := promptConsoleList(reader, prefix, valid, defaultEntry) - if err != nil { - return false, err - } - return response == "yes" || response == "y", nil -} - -// promptConsolePass prompts the user for a passphrase with the given prefix. -// The function will ask the user to confirm the passphrase and will repeat -// the prompts until they enter a matching response. -func promptConsolePass(reader *bufio.Reader, prefix string, confirm bool) ([]byte, error) { - // Prompt the user until they enter a passphrase. - prompt := fmt.Sprintf("%s: ", prefix) - for { - fmt.Print(prompt) - pass, err := terminal.ReadPassword(int(os.Stdin.Fd())) - if err != nil { - return nil, err - } - fmt.Print("\n") - pass = bytes.TrimSpace(pass) - if len(pass) == 0 { - continue - } - - if !confirm { - return pass, nil - } - - fmt.Print("Confirm passphrase: ") - confirm, err := terminal.ReadPassword(int(os.Stdin.Fd())) - if err != nil { - return nil, err - } - fmt.Print("\n") - confirm = bytes.TrimSpace(confirm) - if !bytes.Equal(pass, confirm) { - fmt.Println("The entered passphrases do not match") - continue - } - - return pass, nil - } -} - -// promptConsolePrivatePass prompts the user for a private passphrase with -// varying behavior depending on whether the passed legacy keystore exists. -// When it does, the user is prompted for the existing passphrase which is then -// used to unlock it. On the other hand, when the legacy keystore is nil, the -// user is prompted for a new private passphrase. All prompts are repeated -// until the user enters a valid response. -func promptConsolePrivatePass(reader *bufio.Reader, legacyKeyStore *keystore.Store) ([]byte, error) { - // When there is not an existing legacy wallet, simply prompt the user - // for a new private passphase and return it. - if legacyKeyStore == nil { - return promptConsolePass(reader, "Enter the private "+ - "passphrase for your new wallet", true) - } - - // At this point, there is an existing legacy wallet, so prompt the user - // for the existing private passphrase and ensure it properly unlocks - // the legacy wallet so all of the addresses can later be imported. - fmt.Println("You have an existing legacy wallet. All addresses from " + - "your existing legacy wallet will be imported into the new " + - "wallet format.") - for { - privPass, err := promptConsolePass(reader, "Enter the private "+ - "passphrase for your existing wallet", false) - if err != nil { - return nil, err - } - - // Keep prompting the user until the passphrase is correct. - if err := legacyKeyStore.Unlock([]byte(privPass)); err != nil { - if err == keystore.ErrWrongPassphrase { - fmt.Println(err) - continue - } - - return nil, err - } - - return privPass, nil - } -} - -// promptConsolePublicPass prompts the user whether they want to add an -// additional layer of encryption to the wallet. When the user answers yes and -// there is already a public passphrase provided via the passed config, it -// prompts them whether or not to use that configured passphrase. It will also -// detect when the same passphrase is used for the private and public passphrase -// and prompt the user if they are sure they want to use the same passphrase for -// both. Finally, all prompts are repeated until the user enters a valid -// response. -func promptConsolePublicPass(reader *bufio.Reader, privPass []byte, cfg *config) ([]byte, error) { - pubPass := []byte(defaultPubPassphrase) - usePubPass, err := promptConsoleListBool(reader, "Do you want "+ - "to add an additional layer of encryption for public "+ - "data?", "no") - if err != nil { - return nil, err - } - - if !usePubPass { - return pubPass, nil - } - - walletPass := []byte(cfg.WalletPass) - if !bytes.Equal(walletPass, pubPass) { - useExisting, err := promptConsoleListBool(reader, "Use the "+ - "existing configured public passphrase for encryption "+ - "of public data?", "no") - if err != nil { - return nil, err - } - - if useExisting { - return walletPass, nil - } - } - - for { - pubPass, err = promptConsolePass(reader, "Enter the public "+ - "passphrase for your new wallet", true) - if err != nil { - return nil, err - } - - if bytes.Equal(pubPass, privPass) { - useSamePass, err := promptConsoleListBool(reader, - "Are you sure want to use the same passphrase "+ - "for public and private data?", "no") - if err != nil { - return nil, err - } - - if useSamePass { - break - } - - continue - } - - break - } - - fmt.Println("NOTE: Use the --walletpass option to configure your " + - "public passphrase.") - return pubPass, nil -} - -// promptConsoleSeed prompts the user whether they want to use an existing -// wallet generation seed. When the user answers no, a seed will be generated -// and displayed to the user along with prompting them for confirmation. When -// the user answers yes, a the user is prompted for it. All prompts are -// repeated until the user enters a valid response. -func promptConsoleSeed(reader *bufio.Reader) ([]byte, error) { - // Ascertain the wallet generation seed. - useUserSeed, err := promptConsoleListBool(reader, "Do you have an "+ - "existing wallet seed you want to use?", "no") - if err != nil { - return nil, err - } - if !useUserSeed { - seed, err := hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen) - if err != nil { - return nil, err - } - - fmt.Println("Your wallet generation seed is:") - fmt.Printf("%x\n", seed) - fmt.Println("IMPORTANT: Keep the seed in a safe place as you\n" + - "will NOT be able to restore your wallet without it.") - fmt.Println("Please keep in mind that anyone who has access\n" + - "to the seed can also restore your wallet thereby\n" + - "giving them access to all your funds, so it is\n" + - "imperative that you keep it in a secure location.") - - for { - fmt.Print(`Once you have stored the seed in a safe ` + - `and secure location, enter "OK" to continue: `) - confirmSeed, err := reader.ReadString('\n') - if err != nil { - return nil, err - } - confirmSeed = strings.TrimSpace(confirmSeed) - confirmSeed = strings.Trim(confirmSeed, `"`) - if confirmSeed == "OK" { - break - } - } - - return seed, nil - } - - for { - fmt.Print("Enter existing wallet seed: ") - seedStr, err := reader.ReadString('\n') - if err != nil { - return nil, err - } - seedStr = strings.TrimSpace(strings.ToLower(seedStr)) - - seed, err := hex.DecodeString(seedStr) - if err != nil || len(seed) < hdkeychain.MinSeedBytes || - len(seed) > hdkeychain.MaxSeedBytes { - - fmt.Printf("Invalid seed specified. Must be a "+ - "hexadecimal value that is at least %d bits and "+ - "at most %d bits\n", hdkeychain.MinSeedBytes*8, - hdkeychain.MaxSeedBytes*8) - continue - } - - return seed, nil - } -} - // convertLegacyKeystore converts all of the addresses in the passed legacy // key store to the new waddrmgr.Manager format. Both the legacy keystore and // the new manager must be unlocked. @@ -426,6 +118,9 @@ func convertLegacyKeystore(legacyKeyStore *keystore.Store, manager *waddrmgr.Man // and generates the wallet accordingly. The new wallet will reside at the // provided path. func createWallet(cfg *config) error { + dbDir := networkDir(cfg.DataDir, activeNet.Params) + loader := wallet.NewLoader(activeNet.Params, dbDir) + // When there is a legacy keystore, open it now to ensure any errors // don't end up exiting the process after the user has spent time // entering a bunch of information. @@ -449,15 +144,56 @@ func createWallet(cfg *config) error { // existing keystore, the user will be promped for that passphrase, // otherwise they will be prompted for a new one. reader := bufio.NewReader(os.Stdin) - privPass, err := promptConsolePrivatePass(reader, legacyKeyStore) + privPass, err := prompt.PrivatePass(reader, legacyKeyStore) if err != nil { return err } + // When there exists a legacy keystore, unlock it now and set up a + // callback to import all keystore keys into the new walletdb + // wallet + if legacyKeyStore != nil { + err = legacyKeyStore.Unlock(privPass) + if err != nil { + return err + } + + // Import the addresses in the legacy keystore to the new wallet if + // any exist, locking each wallet again when finished. + loader.RunAfterLoad(func(w *wallet.Wallet, db walletdb.DB) { + defer legacyKeyStore.Lock() + + fmt.Println("Importing addresses from existing wallet...") + + err := w.Manager.Unlock(privPass) + if err != nil { + fmt.Printf("ERR: Failed to unlock new wallet "+ + "during old wallet key import: %v", err) + return + } + defer w.Manager.Lock() + + err = convertLegacyKeystore(legacyKeyStore, w.Manager) + if err != nil { + fmt.Printf("ERR: Failed to import keys from old "+ + "wallet format: %v", err) + return + } + + // Remove the legacy key store. + err = os.Remove(keystorePath) + if err != nil { + fmt.Printf("WARN: Failed to remove legacy wallet "+ + "from'%s'\n", keystorePath) + } + }) + } + // Ascertain the public passphrase. This will either be a value // specified by the user or the default hard-coded public passphrase if // the user does not want the additional public data encryption. - pubPass, err := promptConsolePublicPass(reader, privPass, cfg) + pubPass, err := prompt.PublicPass(reader, privPass, + []byte(wallet.InsecurePubPassphrase), []byte(cfg.WalletPass)) if err != nil { return err } @@ -465,54 +201,18 @@ func createWallet(cfg *config) error { // Ascertain the wallet generation seed. This will either be an // automatically generated value the user has already confirmed or a // value the user has entered which has already been validated. - seed, err := promptConsoleSeed(reader) + seed, err := prompt.Seed(reader) if err != nil { return err } - // Create the wallet. - dbPath := filepath.Join(netDir, walletDbName) fmt.Println("Creating the wallet...") - - // Create the wallet database backed by bolt db. - db, err := walletdb.Create("bdb", dbPath) + w, err := loader.CreateNewWallet(pubPass, privPass, seed) if err != nil { return err } - // Create the address manager. - namespace, err := db.Namespace(waddrmgrNamespaceKey) - if err != nil { - return err - } - manager, err := waddrmgr.Create(namespace, seed, []byte(pubPass), - []byte(privPass), activeNet.Params, nil) - if err != nil { - return err - } - - // Import the addresses in the legacy keystore to the new wallet if - // any exist. - if legacyKeyStore != nil { - fmt.Println("Importing addresses from existing wallet...") - if err := manager.Unlock([]byte(privPass)); err != nil { - return err - } - if err := convertLegacyKeystore(legacyKeyStore, manager); err != nil { - return err - } - - legacyKeyStore.Lock() - legacyKeyStore = nil - - // Remove the legacy key store. - if err := os.Remove(keystorePath); err != nil { - fmt.Printf("WARN: Failed to remove legacy wallet "+ - "from'%s'\n", keystorePath) - } - } - - manager.Close() + w.Manager.Close() fmt.Println("The wallet has been created successfully.") return nil } @@ -524,7 +224,7 @@ func createSimulationWallet(cfg *config) error { privPass := []byte("password") // Public passphrase is the default. - pubPass := []byte(defaultPubPassphrase) + pubPass := []byte(wallet.InsecurePubPassphrase) // Generate a random seed. seed, err := hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen) @@ -583,46 +283,3 @@ func checkCreateDir(path string) error { return nil } - -// openDb opens and returns a walletdb.DB (boltdb here) given the directory and -// dbname -func openDb(directory string, dbname string) (walletdb.DB, error) { - dbPath := filepath.Join(directory, dbname) - - // Ensure that the network directory exists. - if err := checkCreateDir(directory); err != nil { - return nil, err - } - - // Open the database using the boltdb backend. - return walletdb.Open("bdb", dbPath) -} - -// openWallet returns a wallet. The function handles opening an existing wallet -// database, the address manager and the transaction store and uses the values -// to open a wallet.Wallet -func openWallet() (*wallet.Wallet, walletdb.DB, error) { - netdir := networkDir(cfg.DataDir, activeNet.Params) - - db, err := openDb(netdir, walletDbName) - if err != nil { - log.Errorf("Failed to open database: %v", err) - return nil, nil, err - } - - addrMgrNS, err := db.Namespace(waddrmgrNamespaceKey) - if err != nil { - return nil, nil, err - } - txMgrNS, err := db.Namespace(wtxmgrNamespaceKey) - if err != nil { - return nil, nil, err - } - cbs := &waddrmgr.OpenCallbacks{ - ObtainSeed: promptSeed, - ObtainPrivatePass: promptPrivPassPhrase, - } - w, err := wallet.Open([]byte(cfg.WalletPass), activeNet.Params, db, - addrMgrNS, txMgrNS, cbs) - return w, db, err -} diff --git a/wtxmgr/tx.go b/wtxmgr/tx.go index 4883826..a423511 100644 --- a/wtxmgr/tx.go +++ b/wtxmgr/tx.go @@ -142,6 +142,10 @@ type Credit struct { // transactions. type Store struct { namespace walletdb.Namespace + + // Event callbacks. These execute in the same goroutine as the wtxmgr + // caller. + NotifyUnspent func(hash *wire.ShaHash, index uint32) } // Open opens the wallet transaction store from a walletdb namespace. If the @@ -153,7 +157,7 @@ func Open(namespace walletdb.Namespace) (*Store, error) { if err != nil { return nil, err } - return &Store{namespace}, nil + return &Store{namespace, nil}, nil // TODO: set callbacks } // Create creates and opens a new persistent transaction store in the walletdb @@ -164,7 +168,7 @@ func Create(namespace walletdb.Namespace) (*Store, error) { if err != nil { return nil, err } - return &Store{namespace}, nil + return &Store{namespace, nil}, nil // TODO: set callbacks } // moveMinedTx moves a transaction record from the unmined buckets to block @@ -419,21 +423,34 @@ func (s *Store) AddCredit(rec *TxRecord, block *BlockMeta, index uint32, change return storeError(ErrInput, str, nil) } - return scopedUpdate(s.namespace, func(ns walletdb.Bucket) error { - return s.addCredit(ns, rec, block, index, change) + var isNew bool + err := scopedUpdate(s.namespace, func(ns walletdb.Bucket) error { + var err error + isNew, err = s.addCredit(ns, rec, block, index, change) + return err }) + if err == nil && isNew && s.NotifyUnspent != nil { + s.NotifyUnspent(&rec.Hash, index) + } + return err } -func (s *Store) addCredit(ns walletdb.Bucket, rec *TxRecord, block *BlockMeta, index uint32, change bool) error { +// addCredit is an AddCredit helper that runs in an update transaction. The +// bool return specifies whether the unspent output is newly added (true) or a +// duplicate (false). +func (s *Store) addCredit(ns walletdb.Bucket, rec *TxRecord, block *BlockMeta, index uint32, change bool) (bool, error) { if block == nil { k := canonicalOutPoint(&rec.Hash, index) + if existsRawUnminedCredit(ns, k) != nil { + return false, nil + } v := valueUnminedCredit(btcutil.Amount(rec.MsgTx.TxOut[index].Value), change) - return putRawUnminedCredit(ns, k, v) + return true, putRawUnminedCredit(ns, k, v) } k, v := existsCredit(ns, &rec.Hash, index, &block.Block) if v != nil { - return nil + return false, nil } txOutAmt := btcutil.Amount(rec.MsgTx.TxOut[index].Value) @@ -453,19 +470,19 @@ func (s *Store) addCredit(ns walletdb.Bucket, rec *TxRecord, block *BlockMeta, i v = valueUnspentCredit(&cred) err := putRawCredit(ns, k, v) if err != nil { - return err + return false, err } minedBalance, err := fetchMinedBalance(ns) if err != nil { - return err + return false, err } err = putMinedBalance(ns, minedBalance+txOutAmt) if err != nil { - return err + return false, err } - return putUnspent(ns, &cred.outPoint, &block.Block) + return true, putUnspent(ns, &cred.outPoint, &block.Block) } // Rollback removes all blocks at height onwards, moving any transactions within diff --git a/wtxmgr/unconfirmed.go b/wtxmgr/unconfirmed.go index 058b27d..acc028c 100644 --- a/wtxmgr/unconfirmed.go +++ b/wtxmgr/unconfirmed.go @@ -170,3 +170,28 @@ func (s *Store) unminedTxs(ns walletdb.Bucket) ([]*wire.MsgTx, error) { }) return unmined, err } + +// UnminedTxHashes returns the hashes of all transactions not known to have been +// mined in a block. +func (s *Store) UnminedTxHashes() ([]*wire.ShaHash, error) { + var hashes []*wire.ShaHash + err := scopedView(s.namespace, func(ns walletdb.Bucket) error { + var err error + hashes, err = s.unminedTxHashes(ns) + return err + }) + return hashes, err +} + +func (s *Store) unminedTxHashes(ns walletdb.Bucket) ([]*wire.ShaHash, error) { + var hashes []*wire.ShaHash + err := ns.Bucket(bucketUnmined).ForEach(func(k, v []byte) error { + hash := new(wire.ShaHash) + err := readRawUnminedHash(k, hash) + if err == nil { + hashes = append(hashes, hash) + } + return err + }) + return hashes, err +}