From 497ffc11f04518755456604e3b9622c4f75e278a Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Mon, 1 Jun 2015 15:57:50 -0400 Subject: [PATCH] Modernize the RPC server. This is a rather monolithic commit that moves the old RPC server to its own package (rpc/legacyrpc), introduces a new RPC server using gRPC (rpc/rpcserver), and provides the ability to defer wallet loading until request at a later time by an RPC (--noinitialload). The legacy RPC server remains the default for now while the new gRPC server is not enabled by default. Enabling the new server requires setting a listen address (--experimenalrpclisten). This experimental flag is used to effectively feature gate the server until it is ready to use as a default. Both RPC servers can be run at the same time, but require binding to different listen addresses. In theory, with the legacy RPC server now living in its own package it should become much easier to unit test the handlers. This will be useful for any future changes to the package, as compatibility with Core's wallet is still desired. Type safety has also been improved in the legacy RPC server. Multiple handler types are now used for methods that do and do not require the RPC client as a dependency. This can statically help prevent nil pointer dereferences, and was very useful for catching bugs during refactoring. To synchronize the wallet loading process between the main package (the default) and through the gRPC WalletLoader service (with the --noinitialload option), as well as increasing the loose coupling of packages, a new wallet.Loader type has been added. All creating and loading of existing wallets is done through a single Loader instance, and callbacks can be attached to the instance to run after the wallet has been opened. This is how the legacy RPC server is associated with a loaded wallet, even after the wallet is loaded by a gRPC method in a completely unrelated package. Documentation for the new RPC server has been added to the rpc/documentation directory. The documentation includes a specification for the new RPC API, addresses how to make changes to the server implementation, and provides short example clients in several different languages. Some of the new RPC methods are not implementated exactly as described by the specification. These are considered bugs with the implementation, not the spec. Known bugs are commented as such. --- btcwallet.go | 223 +- chain/chain.go | 94 +- config.go | 170 +- internal/cfgutil/file.go | 2 +- internal/prompt/prompt.go | 320 ++ internal/rpchelp/genrpcserverhelp.go | 7 +- log.go | 33 +- rpc/api.proto | 303 ++ rpc/documentation/README.md | 16 + rpc/documentation/api.md | 941 +++++ rpc/documentation/clientusage.md | 438 +++ rpc/documentation/serverchanges.md | 94 + rpc/legacyrpc/config.go | 26 + rpc/legacyrpc/errors.go | 96 + rpc/legacyrpc/log.go | 27 + rpc/legacyrpc/methods.go | 1987 ++++++++++ .../legacyrpc/rpcserver_test.go | 2 +- .../legacyrpc/rpcserverhelp.go | 2 +- rpc/legacyrpc/server.go | 964 +++++ rpc/regen.sh | 3 + rpc/rpcserver/log.go | 81 + rpc/rpcserver/server.go | 845 +++++ rpc/walletrpc/api.pb.go | 1437 +++++++ rpchelp_test.go | 109 - rpcserver.go | 3337 +---------------- sample-btcwallet.conf | 11 +- signal.go | 34 +- waddrmgr/error.go | 5 + waddrmgr/error_test.go | 2 + waddrmgr/manager.go | 86 +- wallet/chainntfns.go | 56 +- wallet/createtx.go | 9 +- wallet/loader.go | 238 ++ wallet/notifications.go | 652 ++++ wallet/rescan.go | 9 +- wallet/wallet.go | 688 +++- walletsetup.go | 445 +-- wtxmgr/tx.go | 39 +- wtxmgr/unconfirmed.go | 25 + 39 files changed, 9891 insertions(+), 3965 deletions(-) create mode 100644 internal/prompt/prompt.go create mode 100644 rpc/api.proto create mode 100644 rpc/documentation/README.md create mode 100644 rpc/documentation/api.md create mode 100644 rpc/documentation/clientusage.md create mode 100644 rpc/documentation/serverchanges.md create mode 100644 rpc/legacyrpc/config.go create mode 100644 rpc/legacyrpc/errors.go create mode 100644 rpc/legacyrpc/log.go create mode 100644 rpc/legacyrpc/methods.go rename rpcserver_test.go => rpc/legacyrpc/rpcserver_test.go (98%) rename rpcserverhelp.go => rpc/legacyrpc/rpcserverhelp.go (99%) create mode 100644 rpc/legacyrpc/server.go create mode 100644 rpc/regen.sh create mode 100644 rpc/rpcserver/log.go create mode 100644 rpc/rpcserver/server.go create mode 100644 rpc/walletrpc/api.pb.go delete mode 100644 rpchelp_test.go create mode 100644 wallet/loader.go create mode 100644 wallet/notifications.go 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 +}