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
+}