Modernize the RPC server.
This is a rather monolithic commit that moves the old RPC server to its own package (rpc/legacyrpc), introduces a new RPC server using gRPC (rpc/rpcserver), and provides the ability to defer wallet loading until request at a later time by an RPC (--noinitialload). The legacy RPC server remains the default for now while the new gRPC server is not enabled by default. Enabling the new server requires setting a listen address (--experimenalrpclisten). This experimental flag is used to effectively feature gate the server until it is ready to use as a default. Both RPC servers can be run at the same time, but require binding to different listen addresses. In theory, with the legacy RPC server now living in its own package it should become much easier to unit test the handlers. This will be useful for any future changes to the package, as compatibility with Core's wallet is still desired. Type safety has also been improved in the legacy RPC server. Multiple handler types are now used for methods that do and do not require the RPC client as a dependency. This can statically help prevent nil pointer dereferences, and was very useful for catching bugs during refactoring. To synchronize the wallet loading process between the main package (the default) and through the gRPC WalletLoader service (with the --noinitialload option), as well as increasing the loose coupling of packages, a new wallet.Loader type has been added. All creating and loading of existing wallets is done through a single Loader instance, and callbacks can be attached to the instance to run after the wallet has been opened. This is how the legacy RPC server is associated with a loaded wallet, even after the wallet is loaded by a gRPC method in a completely unrelated package. Documentation for the new RPC server has been added to the rpc/documentation directory. The documentation includes a specification for the new RPC API, addresses how to make changes to the server implementation, and provides short example clients in several different languages. Some of the new RPC methods are not implementated exactly as described by the specification. These are considered bugs with the implementation, not the spec. Known bugs are commented as such.
This commit is contained in:
parent
6af96bfdb7
commit
497ffc11f0
39 changed files with 9891 additions and 3965 deletions
207
btcwallet.go
207
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
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
* purpose with or without fee is hereby granted, provided that the above
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
@ -23,8 +23,12 @@ import (
|
||||||
_ "net/http/pprof"
|
_ "net/http/pprof"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/btcsuite/btcwallet/chain"
|
"github.com/btcsuite/btcwallet/chain"
|
||||||
|
"github.com/btcsuite/btcwallet/rpc/legacyrpc"
|
||||||
|
"github.com/btcsuite/btcwallet/wallet"
|
||||||
|
"github.com/btcsuite/btcwallet/walletdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -67,88 +71,169 @@ func walletMain() error {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the wallet database. It must have been created with the
|
dbDir := networkDir(cfg.DataDir, activeNet.Params)
|
||||||
// --create option already or this will return an appropriate error.
|
loader := wallet.NewLoader(activeNet.Params, dbDir)
|
||||||
wallet, db, err := openWallet()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// Create and start HTTP server to serve wallet client connections.
|
// Create and start HTTP server to serve wallet client connections.
|
||||||
// This will be updated with the wallet and chain server RPC client
|
// This will be updated with the wallet and chain server RPC client
|
||||||
// created below after each is created.
|
// created below after each is created.
|
||||||
server, err := newRPCServer(cfg.SvrListeners, cfg.RPCMaxClients,
|
rpcs, legacyRPCServer, err := startRPCServers(loader)
|
||||||
cfg.RPCMaxWebsockets)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Unable to create HTTP server: %v", err)
|
log.Errorf("Unable to create RPC servers: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
server.Start()
|
|
||||||
server.SetWallet(wallet)
|
|
||||||
|
|
||||||
// Shutdown the server if an interrupt signal is received.
|
// Create and start chain RPC client so it's ready to connect to
|
||||||
addInterruptHandler(server.Stop)
|
// the wallet when loaded later.
|
||||||
|
if !cfg.NoInitialLoad {
|
||||||
|
go rpcClientConnectLoop(legacyRPCServer, loader)
|
||||||
|
}
|
||||||
|
|
||||||
|
var closeDB func() error
|
||||||
|
defer func() {
|
||||||
|
if closeDB != nil {
|
||||||
|
err := closeDB()
|
||||||
|
if err != nil {
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
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() {
|
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 {
|
for {
|
||||||
// Read CA certs and create the RPC client.
|
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
|
var certs []byte
|
||||||
if !cfg.DisableClientTLS {
|
if !cfg.DisableClientTLS {
|
||||||
|
var err error
|
||||||
certs, err = ioutil.ReadFile(cfg.CAFile)
|
certs, err = ioutil.ReadFile(cfg.CAFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Cannot open CA file: %v", err)
|
log.Warnf("Cannot open CA file: %v", err)
|
||||||
// If there's an error reading the CA file, continue
|
// If there's an error reading the CA file, continue
|
||||||
// with nil certs and without the client connection
|
// with nil certs and without the client connection.
|
||||||
certs = nil
|
certs = nil
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Info("Client TLS is disabled")
|
log.Info("Chain server RPC TLS is disabled")
|
||||||
}
|
}
|
||||||
rpcc, err := chain.NewClient(activeNet.Params, cfg.RPCConnect,
|
|
||||||
cfg.BtcdUsername, cfg.BtcdPassword, certs, cfg.DisableClientTLS)
|
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 {
|
if err != nil {
|
||||||
log.Errorf("Cannot create chain server RPC client: %v", err)
|
return nil, err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
err = rpcc.Start()
|
err = rpcc.Start()
|
||||||
if err != nil {
|
return rpcc, err
|
||||||
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:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Wait for the server to shutdown either due to a stop RPC request
|
|
||||||
// or an interrupt.
|
|
||||||
server.WaitForShutdown()
|
|
||||||
log.Info("Shutdown complete")
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
100
chain/chain.go
100
chain/chain.go
|
@ -30,11 +30,13 @@ import (
|
||||||
"github.com/btcsuite/btcwallet/wtxmgr"
|
"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.
|
// for information regarding the current best block chain.
|
||||||
type Client struct {
|
type RPCClient struct {
|
||||||
*btcrpcclient.Client
|
*btcrpcclient.Client
|
||||||
|
connConfig *btcrpcclient.ConnConfig // Work around unexported field
|
||||||
chainParams *chaincfg.Params
|
chainParams *chaincfg.Params
|
||||||
|
reconnectAttempts int
|
||||||
|
|
||||||
enqueueNotification chan interface{}
|
enqueueNotification chan interface{}
|
||||||
dequeueNotification chan interface{}
|
dequeueNotification chan interface{}
|
||||||
|
@ -46,30 +48,21 @@ type Client struct {
|
||||||
quitMtx sync.Mutex
|
quitMtx sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient creates a client connection to the server described by the connect
|
// NewRPCClient creates a client connection to the server described by the
|
||||||
// string. If disableTLS is false, the remote RPC certificate must be provided
|
// connect string. If disableTLS is false, the remote RPC certificate must be
|
||||||
// in the certs slice. The connection is not established immediately, but must
|
// provided in the certs slice. The connection is not established immediately,
|
||||||
// be done using the Start method. If the remote server does not operate on
|
// but must be done using the Start method. If the remote server does not
|
||||||
// the same bitcoin network as described by the passed chain parameters, the
|
// operate on the same bitcoin network as described by the passed chain
|
||||||
// connection will be disconnected.
|
// parameters, the connection will be disconnected.
|
||||||
func NewClient(chainParams *chaincfg.Params, connect, user, pass string, certs []byte, disableTLS bool) (*Client, error) {
|
func NewRPCClient(chainParams *chaincfg.Params, connect, user, pass string, certs []byte,
|
||||||
client := Client{
|
disableTLS bool, reconnectAttempts int) (*RPCClient, error) {
|
||||||
chainParams: chainParams,
|
|
||||||
enqueueNotification: make(chan interface{}),
|
if reconnectAttempts < 0 {
|
||||||
dequeueNotification: make(chan interface{}),
|
return nil, errors.New("reconnectAttempts must be positive")
|
||||||
currentBlock: make(chan *waddrmgr.BlockStamp),
|
|
||||||
quit: make(chan struct{}),
|
|
||||||
}
|
}
|
||||||
ntfnCallbacks := btcrpcclient.NotificationHandlers{
|
|
||||||
OnClientConnected: client.onClientConnect,
|
client := &RPCClient{
|
||||||
OnBlockConnected: client.onBlockConnected,
|
connConfig: &btcrpcclient.ConnConfig{
|
||||||
OnBlockDisconnected: client.onBlockDisconnected,
|
|
||||||
OnRecvTx: client.onRecvTx,
|
|
||||||
OnRedeemingTx: client.onRedeemingTx,
|
|
||||||
OnRescanFinished: client.onRescanFinished,
|
|
||||||
OnRescanProgress: client.onRescanProgress,
|
|
||||||
}
|
|
||||||
conf := btcrpcclient.ConnConfig{
|
|
||||||
Host: connect,
|
Host: connect,
|
||||||
Endpoint: "ws",
|
Endpoint: "ws",
|
||||||
User: user,
|
User: user,
|
||||||
|
@ -78,13 +71,29 @@ func NewClient(chainParams *chaincfg.Params, connect, user, pass string, certs [
|
||||||
DisableAutoReconnect: true,
|
DisableAutoReconnect: true,
|
||||||
DisableConnectOnNew: true,
|
DisableConnectOnNew: true,
|
||||||
DisableTLS: disableTLS,
|
DisableTLS: disableTLS,
|
||||||
|
},
|
||||||
|
chainParams: chainParams,
|
||||||
|
reconnectAttempts: reconnectAttempts,
|
||||||
|
enqueueNotification: make(chan interface{}),
|
||||||
|
dequeueNotification: make(chan interface{}),
|
||||||
|
currentBlock: make(chan *waddrmgr.BlockStamp),
|
||||||
|
quit: make(chan struct{}),
|
||||||
}
|
}
|
||||||
c, err := btcrpcclient.New(&conf, &ntfnCallbacks)
|
ntfnCallbacks := &btcrpcclient.NotificationHandlers{
|
||||||
|
OnClientConnected: client.onClientConnect,
|
||||||
|
OnBlockConnected: client.onBlockConnected,
|
||||||
|
OnBlockDisconnected: client.onBlockDisconnected,
|
||||||
|
OnRecvTx: client.onRecvTx,
|
||||||
|
OnRedeemingTx: client.onRedeemingTx,
|
||||||
|
OnRescanFinished: client.onRescanFinished,
|
||||||
|
OnRescanProgress: client.onRescanProgress,
|
||||||
|
}
|
||||||
|
rpcClient, err := btcrpcclient.New(client.connConfig, ntfnCallbacks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
client.Client = c
|
client.Client = rpcClient
|
||||||
return &client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start attempts to establish a client connection with the remote server.
|
// 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
|
// sent by the server. After a limited number of connection attempts, this
|
||||||
// function gives up, and therefore will not block forever waiting for the
|
// function gives up, and therefore will not block forever waiting for the
|
||||||
// connection to be established to a server that may not exist.
|
// connection to be established to a server that may not exist.
|
||||||
func (c *Client) Start() error {
|
func (c *RPCClient) Start() error {
|
||||||
err := c.Connect(5) // attempt connection 5 tries at most
|
err := c.Connect(c.reconnectAttempts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -120,7 +129,7 @@ func (c *Client) Start() error {
|
||||||
|
|
||||||
// Stop disconnects the client and signals the shutdown of all goroutines
|
// Stop disconnects the client and signals the shutdown of all goroutines
|
||||||
// started by Start.
|
// started by Start.
|
||||||
func (c *Client) Stop() {
|
func (c *RPCClient) Stop() {
|
||||||
c.quitMtx.Lock()
|
c.quitMtx.Lock()
|
||||||
select {
|
select {
|
||||||
case <-c.quit:
|
case <-c.quit:
|
||||||
|
@ -137,7 +146,7 @@ func (c *Client) Stop() {
|
||||||
|
|
||||||
// WaitForShutdown blocks until both the client has finished disconnecting
|
// WaitForShutdown blocks until both the client has finished disconnecting
|
||||||
// and all handlers have exited.
|
// and all handlers have exited.
|
||||||
func (c *Client) WaitForShutdown() {
|
func (c *RPCClient) WaitForShutdown() {
|
||||||
c.Client.WaitForShutdown()
|
c.Client.WaitForShutdown()
|
||||||
c.wg.Wait()
|
c.wg.Wait()
|
||||||
}
|
}
|
||||||
|
@ -187,13 +196,13 @@ type (
|
||||||
// bitcoin RPC server. This channel must be continually read or the process
|
// bitcoin RPC server. This channel must be continually read or the process
|
||||||
// may abort for running out memory, as unread notifications are queued for
|
// may abort for running out memory, as unread notifications are queued for
|
||||||
// later reads.
|
// later reads.
|
||||||
func (c *Client) Notifications() <-chan interface{} {
|
func (c *RPCClient) Notifications() <-chan interface{} {
|
||||||
return c.dequeueNotification
|
return c.dequeueNotification
|
||||||
}
|
}
|
||||||
|
|
||||||
// BlockStamp returns the latest block notified by the client, or an error
|
// BlockStamp returns the latest block notified by the client, or an error
|
||||||
// if the client has been shut down.
|
// if the client has been shut down.
|
||||||
func (c *Client) BlockStamp() (*waddrmgr.BlockStamp, error) {
|
func (c *RPCClient) BlockStamp() (*waddrmgr.BlockStamp, error) {
|
||||||
select {
|
select {
|
||||||
case bs := <-c.currentBlock:
|
case bs := <-c.currentBlock:
|
||||||
return bs, nil
|
return bs, nil
|
||||||
|
@ -223,14 +232,14 @@ func parseBlock(block *btcjson.BlockDetails) (*wtxmgr.BlockMeta, error) {
|
||||||
return blk, nil
|
return blk, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) onClientConnect() {
|
func (c *RPCClient) onClientConnect() {
|
||||||
select {
|
select {
|
||||||
case c.enqueueNotification <- ClientConnected{}:
|
case c.enqueueNotification <- ClientConnected{}:
|
||||||
case <-c.quit:
|
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 {
|
select {
|
||||||
case c.enqueueNotification <- BlockConnected{
|
case c.enqueueNotification <- BlockConnected{
|
||||||
Block: wtxmgr.Block{
|
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 {
|
select {
|
||||||
case c.enqueueNotification <- BlockDisconnected{
|
case c.enqueueNotification <- BlockDisconnected{
|
||||||
Block: wtxmgr.Block{
|
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)
|
blk, err := parseBlock(block)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Log and drop improper notification.
|
// 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.
|
// Handled exactly like recvtx notifications.
|
||||||
c.onRecvTx(tx, block)
|
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 {
|
select {
|
||||||
case c.enqueueNotification <- &RescanProgress{hash, height, blkTime}:
|
case c.enqueueNotification <- &RescanProgress{hash, height, blkTime}:
|
||||||
case <-c.quit:
|
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 {
|
select {
|
||||||
case c.enqueueNotification <- &RescanFinished{hash, height, blkTime}:
|
case c.enqueueNotification <- &RescanFinished{hash, height, blkTime}:
|
||||||
case <-c.quit:
|
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
|
// handler maintains a queue of notifications and the current state (best
|
||||||
// block) of the chain.
|
// block) of the chain.
|
||||||
func (c *Client) handler() {
|
func (c *RPCClient) handler() {
|
||||||
hash, height, err := c.GetBestBlock()
|
hash, height, err := c.GetBestBlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to receive best block from chain server: %v", err)
|
log.Errorf("Failed to receive best block from chain server: %v", err)
|
||||||
|
@ -410,3 +419,10 @@ out:
|
||||||
close(c.dequeueNotification)
|
close(c.dequeueNotification)
|
||||||
c.wg.Done()
|
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)
|
||||||
|
}
|
||||||
|
|
136
config.go
136
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
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
* purpose with or without fee is hereby granted, provided that the above
|
* 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/cfgutil"
|
||||||
"github.com/btcsuite/btcwallet/internal/legacy/keystore"
|
"github.com/btcsuite/btcwallet/internal/legacy/keystore"
|
||||||
"github.com/btcsuite/btcwallet/netparams"
|
"github.com/btcsuite/btcwallet/netparams"
|
||||||
|
"github.com/btcsuite/btcwallet/wallet"
|
||||||
flags "github.com/btcsuite/go-flags"
|
flags "github.com/btcsuite/go-flags"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -41,17 +42,6 @@ const (
|
||||||
defaultRPCMaxClients = 10
|
defaultRPCMaxClients = 10
|
||||||
defaultRPCMaxWebsockets = 25
|
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"
|
walletDbName = "wallet.db"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -67,35 +57,58 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
|
// 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"`
|
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"`
|
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"`
|
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"`
|
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)"`
|
MainNet bool `long:"mainnet" description:"Use the main Bitcoin network (default testnet3)"`
|
||||||
SimNet bool `long:"simnet" description:"Use the simulation test 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"`
|
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"`
|
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)"`
|
Proxy string `long:"proxy" description:"Connect via SOCKS5 proxy (eg. 127.0.0.1:9050)"`
|
||||||
ProxyUser string `long:"proxyuser" description:"Username for proxy server"`
|
ProxyUser string `long:"proxyuser" description:"Username for proxy server"`
|
||||||
ProxyPass string `long:"proxypass" default-mask:"-" description:"Password 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"`
|
|
||||||
|
// 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
|
// cleanAndExpandPath expands environement variables and leading ~ in the
|
||||||
|
@ -107,8 +120,9 @@ func cleanAndExpandPath(path string) string {
|
||||||
path = strings.Replace(path, "~", homeDir, 1)
|
path = strings.Replace(path, "~", homeDir, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: The os.ExpandEnv doesn't work with Windows-style %VARIABLE%,
|
// NOTE: The os.ExpandEnv doesn't work with Windows cmd.exe-style
|
||||||
// but they variables can still be expanded via POSIX-style $VARIABLE.
|
// %VARIABLE%, but they variables can still be expanded via POSIX-style
|
||||||
|
// $VARIABLE.
|
||||||
return filepath.Clean(os.ExpandEnv(path))
|
return filepath.Clean(os.ExpandEnv(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,12 +229,12 @@ func loadConfig() (*config, []string, error) {
|
||||||
ConfigFile: defaultConfigFile,
|
ConfigFile: defaultConfigFile,
|
||||||
DataDir: defaultDataDir,
|
DataDir: defaultDataDir,
|
||||||
LogDir: defaultLogDir,
|
LogDir: defaultLogDir,
|
||||||
WalletPass: defaultPubPassphrase,
|
WalletPass: wallet.InsecurePubPassphrase,
|
||||||
RPCKey: defaultRPCKeyFile,
|
RPCKey: defaultRPCKeyFile,
|
||||||
RPCCert: defaultRPCCertFile,
|
RPCCert: defaultRPCCertFile,
|
||||||
DisallowFree: defaultDisallowFree,
|
DisallowFree: defaultDisallowFree,
|
||||||
RPCMaxClients: defaultRPCMaxClients,
|
LegacyRPCMaxClients: defaultRPCMaxClients,
|
||||||
RPCMaxWebsockets: defaultRPCMaxWebsockets,
|
LegacyRPCMaxWebsockets: defaultRPCMaxWebsockets,
|
||||||
}
|
}
|
||||||
|
|
||||||
// A config file in the current directory takes precedence.
|
// 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.
|
// Created successfully, so exit now with success.
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
} else if !dbFileExists {
|
} else if !dbFileExists && !cfg.NoInitialLoad {
|
||||||
keystorePath := filepath.Join(netDir, keystore.Filename)
|
keystorePath := filepath.Join(netDir, keystore.Filename)
|
||||||
keystoreExists, err := cfgutil.FileExists(keystorePath)
|
keystoreExists, err := cfgutil.FileExists(keystorePath)
|
||||||
if err != nil {
|
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")
|
addrs, err := net.LookupHost("localhost")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
cfg.SvrListeners = make([]string, 0, len(addrs))
|
cfg.LegacyRPCListeners = make([]string, 0, len(addrs))
|
||||||
for _, addr := range addrs {
|
for _, addr := range addrs {
|
||||||
addr = net.JoinHostPort(addr, activeNet.RPCServerPort)
|
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
|
// Add default port to all rpc listener addresses if needed and remove
|
||||||
// duplicate addresses.
|
// duplicate addresses.
|
||||||
cfg.SvrListeners, err = cfgutil.NormalizeAddresses(
|
cfg.LegacyRPCListeners, err = cfgutil.NormalizeAddresses(
|
||||||
cfg.SvrListeners, activeNet.RPCServerPort)
|
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 {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr,
|
fmt.Fprintf(os.Stderr,
|
||||||
"Invalid network address in RPC listeners: %v\n", err)
|
"Invalid network address in RPC listeners: %v\n", err)
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only allow server TLS to be disabled if the RPC is bound to localhost
|
// Both RPC servers may not listen on the same interface/port.
|
||||||
// addresses.
|
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 {
|
if cfg.DisableServerTLS {
|
||||||
for _, addr := range cfg.SvrListeners {
|
allListeners := append(cfg.LegacyRPCListeners,
|
||||||
|
cfg.ExperimentalRPCListeners...)
|
||||||
|
for _, addr := range allListeners {
|
||||||
host, _, err := net.SplitHostPort(addr)
|
host, _, err := net.SplitHostPort(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
str := "%s: RPC listen interface '%s' is " +
|
str := "%s: RPC listen interface '%s' is " +
|
||||||
|
|
|
@ -18,7 +18,7 @@ package cfgutil
|
||||||
|
|
||||||
import "os"
|
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) {
|
func FileExists(filePath string) (bool, error) {
|
||||||
_, err := os.Stat(filePath)
|
_, err := os.Stat(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
320
internal/prompt/prompt.go
Normal file
320
internal/prompt/prompt.go
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -85,9 +85,14 @@ func writeUsage() {
|
||||||
func main() {
|
func main() {
|
||||||
defer outputFile.Close()
|
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("// AUTOGENERATED by internal/rpchelp/genrpcserverhelp.go; do not edit.")
|
||||||
writefln("")
|
writefln("")
|
||||||
writefln("package main")
|
writefln("package %s", packageName)
|
||||||
writefln("")
|
writefln("")
|
||||||
for _, h := range rpchelp.HelpDescs {
|
for _, h := range rpchelp.HelpDescs {
|
||||||
writeLocaleHelp(h.Locale, h.GoLocale, h.Descs)
|
writeLocaleHelp(h.Locale, h.GoLocale, h.Descs)
|
||||||
|
|
23
log.go
23
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
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
* purpose with or without fee is hereby granted, provided that the above
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
@ -23,20 +23,13 @@ import (
|
||||||
"github.com/btcsuite/btclog"
|
"github.com/btcsuite/btclog"
|
||||||
"github.com/btcsuite/btcrpcclient"
|
"github.com/btcsuite/btcrpcclient"
|
||||||
"github.com/btcsuite/btcwallet/chain"
|
"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/wallet"
|
||||||
"github.com/btcsuite/btcwallet/wtxmgr"
|
"github.com/btcsuite/btcwallet/wtxmgr"
|
||||||
"github.com/btcsuite/seelog"
|
"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
|
// Loggers per subsytem. Note that backendLog is a seelog logger that all of
|
||||||
// the subsystem loggers route their messages to. When adding new subsystems,
|
// the subsystem loggers route their messages to. When adding new subsystems,
|
||||||
// add a reference here, to the subsystemLoggers map, and the useLogger
|
// add a reference here, to the subsystemLoggers map, and the useLogger
|
||||||
|
@ -47,6 +40,8 @@ var (
|
||||||
walletLog = btclog.Disabled
|
walletLog = btclog.Disabled
|
||||||
txmgrLog = btclog.Disabled
|
txmgrLog = btclog.Disabled
|
||||||
chainLog = btclog.Disabled
|
chainLog = btclog.Disabled
|
||||||
|
grpcLog = btclog.Disabled
|
||||||
|
legacyRPCLog = btclog.Disabled
|
||||||
)
|
)
|
||||||
|
|
||||||
// subsystemLoggers maps each subsystem identifier to its associated logger.
|
// subsystemLoggers maps each subsystem identifier to its associated logger.
|
||||||
|
@ -55,6 +50,8 @@ var subsystemLoggers = map[string]btclog.Logger{
|
||||||
"WLLT": walletLog,
|
"WLLT": walletLog,
|
||||||
"TMGR": txmgrLog,
|
"TMGR": txmgrLog,
|
||||||
"CHNS": chainLog,
|
"CHNS": chainLog,
|
||||||
|
"GRPC": grpcLog,
|
||||||
|
"RPCS": legacyRPCLog,
|
||||||
}
|
}
|
||||||
|
|
||||||
// logClosure is used to provide a closure over expensive logging operations
|
// logClosure is used to provide a closure over expensive logging operations
|
||||||
|
@ -94,6 +91,12 @@ func useLogger(subsystemID string, logger btclog.Logger) {
|
||||||
chainLog = logger
|
chainLog = logger
|
||||||
chain.UseLogger(logger)
|
chain.UseLogger(logger)
|
||||||
btcrpcclient.UseLogger(logger)
|
btcrpcclient.UseLogger(logger)
|
||||||
|
case "GRPC":
|
||||||
|
grpcLog = logger
|
||||||
|
rpcserver.UseLogger(logger)
|
||||||
|
case "RPCS":
|
||||||
|
legacyRPCLog = logger
|
||||||
|
legacyrpc.UseLogger(logger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
303
rpc/api.proto
Normal file
303
rpc/api.proto
Normal file
|
@ -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 {}
|
16
rpc/documentation/README.md
Normal file
16
rpc/documentation/README.md
Normal file
|
@ -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.
|
941
rpc/documentation/api.md
Normal file
941
rpc/documentation/api.md
Normal file
|
@ -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.
|
438
rpc/documentation/clientusage.md
Normal file
438
rpc/documentation/clientusage.md
Normal file
|
@ -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))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="cpp"/>
|
||||||
|
## 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 <unistd.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <pwd.h>
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <grpc++/grpc++.h>
|
||||||
|
|
||||||
|
#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
|
||||||
|
```
|
||||||
|
|
||||||
|
<a name="csharp"/>
|
||||||
|
## 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<string> 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()
|
||||||
|
```
|
94
rpc/documentation/serverchanges.md
Normal file
94
rpc/documentation/serverchanges.md
Normal file
|
@ -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)
|
26
rpc/legacyrpc/config.go
Normal file
26
rpc/legacyrpc/config.go
Normal file
|
@ -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
|
||||||
|
}
|
96
rpc/legacyrpc/errors.go
Normal file
96
rpc/legacyrpc/errors.go
Normal file
|
@ -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",
|
||||||
|
}
|
||||||
|
)
|
27
rpc/legacyrpc/log.go
Normal file
27
rpc/legacyrpc/log.go
Normal file
|
@ -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
|
||||||
|
}
|
1987
rpc/legacyrpc/methods.go
Normal file
1987
rpc/legacyrpc/methods.go
Normal file
File diff suppressed because it is too large
Load diff
|
@ -14,7 +14,7 @@
|
||||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package main
|
package legacyrpc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
|
@ -1,6 +1,6 @@
|
||||||
// AUTOGENERATED by internal/rpchelp/genrpcserverhelp.go; do not edit.
|
// AUTOGENERATED by internal/rpchelp/genrpcserverhelp.go; do not edit.
|
||||||
|
|
||||||
package main
|
package legacyrpc
|
||||||
|
|
||||||
func helpDescsEnUS() map[string]string {
|
func helpDescsEnUS() map[string]string {
|
||||||
return map[string]string{
|
return map[string]string{
|
964
rpc/legacyrpc/server.go
Normal file
964
rpc/legacyrpc/server.go
Normal file
|
@ -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()
|
||||||
|
}
|
3
rpc/regen.sh
Normal file
3
rpc/regen.sh
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
protoc -I. api.proto --go_out=plugins=grpc:walletrpc
|
81
rpc/rpcserver/log.go
Normal file
81
rpc/rpcserver/log.go
Normal file
|
@ -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)...)
|
||||||
|
}
|
845
rpc/rpcserver/server.go
Normal file
845
rpc/rpcserver/server.go
Normal file
|
@ -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
|
||||||
|
}
|
1437
rpc/walletrpc/api.pb.go
Normal file
1437
rpc/walletrpc/api.pb.go
Normal file
File diff suppressed because it is too large
Load diff
109
rpchelp_test.go
109
rpchelp_test.go
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
3315
rpcserver.go
3315
rpcserver.go
File diff suppressed because it is too large
Load diff
|
@ -15,7 +15,7 @@
|
||||||
; directory for mainnet and testnet wallets, respectively.
|
; directory for mainnet and testnet wallets, respectively.
|
||||||
; datadir=~/.btcwallet
|
; datadir=~/.btcwallet
|
||||||
|
|
||||||
; Maximum number of addresses to generate for the keypool
|
; Maximum number of addresses to generate for the keypool (DEPRECATED)
|
||||||
; keypoolsize=100
|
; keypoolsize=100
|
||||||
|
|
||||||
; Whether transactions must be created with some minimum fee, even if the
|
; 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=0.0.0.0:18337 ; all ipv4 interfaces on non-standard port 18337
|
||||||
; rpclisten=[::]:18337 ; all ipv6 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=
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
; ------------------------------------------------------------------------------
|
; ------------------------------------------------------------------------------
|
||||||
|
|
34
signal.go
34
signal.go
|
@ -28,6 +28,21 @@ var interruptChannel chan os.Signal
|
||||||
// to be invoked on SIGINT (Ctrl+C) signals.
|
// to be invoked on SIGINT (Ctrl+C) signals.
|
||||||
var addHandlerChannel = make(chan func())
|
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
|
// mainInterruptHandler listens for SIGINT (Ctrl+C) signals on the
|
||||||
// interruptChannel and invokes the registered interruptCallbacks accordingly.
|
// interruptChannel and invokes the registered interruptCallbacks accordingly.
|
||||||
// It also listens for callback registration. It must be run as a goroutine.
|
// 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
|
// interruptCallbacks is a list of callbacks to invoke when a
|
||||||
// SIGINT (Ctrl+C) is received.
|
// SIGINT (Ctrl+C) is received.
|
||||||
var interruptCallbacks []func()
|
var interruptCallbacks []func()
|
||||||
|
invokeCallbacks := func() {
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-interruptChannel:
|
|
||||||
log.Info("Received SIGINT (Ctrl+C). Shutting down...")
|
|
||||||
// run handlers in LIFO order.
|
// run handlers in LIFO order.
|
||||||
for i := range interruptCallbacks {
|
for i := range interruptCallbacks {
|
||||||
idx := len(interruptCallbacks) - 1 - i
|
idx := len(interruptCallbacks) - 1 - i
|
||||||
interruptCallbacks[idx]()
|
interruptCallbacks[idx]()
|
||||||
}
|
}
|
||||||
|
close(interruptHandlersDone)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-interruptChannel:
|
||||||
|
log.Info("Received SIGINT (Ctrl+C). Shutting down...")
|
||||||
|
invokeCallbacks()
|
||||||
|
return
|
||||||
|
case <-simulateInterruptChannel:
|
||||||
|
log.Info("Received shutdown request. Shutting down...")
|
||||||
|
invokeCallbacks()
|
||||||
|
return
|
||||||
|
|
||||||
case handler := <-addHandlerChannel:
|
case handler := <-addHandlerChannel:
|
||||||
interruptCallbacks = append(interruptCallbacks, handler)
|
interruptCallbacks = append(interruptCallbacks, handler)
|
||||||
|
|
|
@ -135,6 +135,10 @@ const (
|
||||||
// ErrCallBackBreak is used to break from a callback function passed
|
// ErrCallBackBreak is used to break from a callback function passed
|
||||||
// down to the manager.
|
// down to the manager.
|
||||||
ErrCallBackBreak
|
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.
|
// Map of ErrorCode values back to their constant names for pretty printing.
|
||||||
|
@ -159,6 +163,7 @@ var errorCodeStrings = map[ErrorCode]string{
|
||||||
ErrWrongPassphrase: "ErrWrongPassphrase",
|
ErrWrongPassphrase: "ErrWrongPassphrase",
|
||||||
ErrWrongNet: "ErrWrongNet",
|
ErrWrongNet: "ErrWrongNet",
|
||||||
ErrCallBackBreak: "ErrCallBackBreak",
|
ErrCallBackBreak: "ErrCallBackBreak",
|
||||||
|
ErrEmptyPassphrase: "ErrEmptyPassphrase",
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns the ErrorCode as a human-readable name.
|
// String returns the ErrorCode as a human-readable name.
|
||||||
|
|
|
@ -49,6 +49,8 @@ func TestErrorCodeStringer(t *testing.T) {
|
||||||
{waddrmgr.ErrTooManyAddresses, "ErrTooManyAddresses"},
|
{waddrmgr.ErrTooManyAddresses, "ErrTooManyAddresses"},
|
||||||
{waddrmgr.ErrWrongPassphrase, "ErrWrongPassphrase"},
|
{waddrmgr.ErrWrongPassphrase, "ErrWrongPassphrase"},
|
||||||
{waddrmgr.ErrWrongNet, "ErrWrongNet"},
|
{waddrmgr.ErrWrongNet, "ErrWrongNet"},
|
||||||
|
{waddrmgr.ErrCallBackBreak, "ErrCallBackBreak"},
|
||||||
|
{waddrmgr.ErrEmptyPassphrase, "ErrEmptyPassphrase"},
|
||||||
{0xffff, "Unknown ErrorCode (65535)"},
|
{0xffff, "Unknown ErrorCode (65535)"},
|
||||||
}
|
}
|
||||||
t.Logf("Running %d tests", len(tests))
|
t.Logf("Running %d tests", len(tests))
|
||||||
|
|
|
@ -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
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
* purpose with or without fee is hereby granted, provided that the above
|
* 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
|
// private extended key so the unencrypted versions can be cleared from memory
|
||||||
// when the address manager is locked.
|
// when the address manager is locked.
|
||||||
type accountInfo struct {
|
type accountInfo struct {
|
||||||
|
acctName string
|
||||||
|
|
||||||
// The account key is used to derive the branches which in turn derive
|
// The account key is used to derive the branches which in turn derive
|
||||||
// the internal and external addresses.
|
// the internal and external addresses.
|
||||||
// The accountKeyPriv will be nil when the address manager is locked.
|
// The accountKeyPriv will be nil when the address manager is locked.
|
||||||
|
@ -165,6 +167,16 @@ type accountInfo struct {
|
||||||
lastInternalAddr ManagedAddress
|
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
|
// unlockDeriveInfo houses the information needed to derive a private key for a
|
||||||
// managed address when the address manager is unlocked. See the deriveOnUnlock
|
// managed address when the address manager is unlocked. See the deriveOnUnlock
|
||||||
// field in the Manager struct for more details on how this is used.
|
// 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
|
// Create the new account info with the known information. The rest
|
||||||
// of the fields are filled out below.
|
// of the fields are filled out below.
|
||||||
acctInfo := &accountInfo{
|
acctInfo := &accountInfo{
|
||||||
|
acctName: row.name,
|
||||||
acctKeyEncrypted: row.privKeyEncrypted,
|
acctKeyEncrypted: row.privKeyEncrypted,
|
||||||
acctKeyPub: acctKeyPub,
|
acctKeyPub: acctKeyPub,
|
||||||
nextExternalIndex: row.nextExternalIndex,
|
nextExternalIndex: row.nextExternalIndex,
|
||||||
|
@ -547,6 +560,60 @@ func (m *Manager) loadAccountInfo(account uint32) (*accountInfo, error) {
|
||||||
return acctInfo, nil
|
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
|
// deriveKeyFromPath returns either a public or private derived extended key
|
||||||
// based on the private flag for the given an account, branch, and index.
|
// 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 {
|
if err = deleteAccountIDIndex(tx, account); err != nil {
|
||||||
return err
|
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 {
|
if err = deleteAccountNameIndex(tx, row.name); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1812,6 +1879,15 @@ func (m *Manager) RenameAccount(account uint32, name string) error {
|
||||||
row.privKeyEncrypted, row.nextExternalIndex, row.nextInternalIndex, name)
|
row.privKeyEncrypted, row.nextExternalIndex, row.nextInternalIndex, name)
|
||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2232,6 +2308,12 @@ func Create(namespace walletdb.Namespace, seed, pubPassphrase, privPassphrase []
|
||||||
return nil, managerError(ErrAlreadyExists, errAlreadyExists, nil)
|
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.
|
// Perform the initial bucket creation and database namespace setup.
|
||||||
if err := createManagerNS(namespace); err != nil {
|
if err := createManagerNS(namespace); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -24,6 +24,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (w *Wallet) handleChainNotifications() {
|
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) {
|
sync := func(w *Wallet) {
|
||||||
// At the moment there is no recourse if the rescan fails for
|
// At the moment there is no recourse if the rescan fails for
|
||||||
// some reason, however, the wallet will not be marked synced
|
// 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
|
var err error
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case chain.ClientConnected:
|
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
|
// that's currently in-sync with the chain server as being synced up to
|
||||||
// the passed block.
|
// the passed block.
|
||||||
func (w *Wallet) connectBlock(b wtxmgr.BlockMeta) {
|
func (w *Wallet) connectBlock(b wtxmgr.BlockMeta) {
|
||||||
if !w.ChainSynced() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bs := waddrmgr.BlockStamp{
|
bs := waddrmgr.BlockStamp{
|
||||||
Height: b.Height,
|
Height: b.Height,
|
||||||
Hash: b.Hash,
|
Hash: b.Hash,
|
||||||
|
@ -77,9 +80,13 @@ func (w *Wallet) connectBlock(b wtxmgr.BlockMeta) {
|
||||||
"connect block for hash %v (height %d): %v", b.Hash,
|
"connect block for hash %v (height %d): %v", b.Hash,
|
||||||
b.Height, err)
|
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
|
// 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.notifyDisconnectedBlock(b)
|
||||||
w.notifyBalances(b.Height - 1)
|
w.notifyBalances(b.Height - 1)
|
||||||
|
|
||||||
|
@ -201,12 +212,37 @@ 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 {
|
||||||
|
bs, err := chainClient.BlockStamp()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
w.notifyBalances(bs.Height)
|
w.notifyBalances(bs.Height)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,6 +112,7 @@ type CreatedTx struct {
|
||||||
MsgTx *wire.MsgTx
|
MsgTx *wire.MsgTx
|
||||||
ChangeAddr btcutil.Address
|
ChangeAddr btcutil.Address
|
||||||
ChangeIndex int // negative if no change
|
ChangeIndex int // negative if no change
|
||||||
|
Fee btcutil.Amount
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByAmount defines the methods needed to satisify sort.Interface to
|
// 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()
|
defer heldUnlock.Release()
|
||||||
|
|
||||||
|
chainClient, err := w.requireChainClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Get current block's height and hash.
|
// Get current block's height and hash.
|
||||||
bs, err := w.chainSvr.BlockStamp()
|
bs, err := chainClient.BlockStamp()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -270,6 +276,7 @@ func createTx(eligible []wtxmgr.Credit,
|
||||||
MsgTx: msgtx,
|
MsgTx: msgtx,
|
||||||
ChangeAddr: changeAddr,
|
ChangeAddr: changeAddr,
|
||||||
ChangeIndex: changeIdx,
|
ChangeIndex: changeIdx,
|
||||||
|
Fee: feeEst, // Last estimate is the actual fee
|
||||||
}
|
}
|
||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
238
wallet/loader.go
Normal file
238
wallet/loader.go
Normal file
|
@ -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
|
||||||
|
}
|
652
wallet/notifications.go
Normal file
652
wallet/notifications.go
Normal file
|
@ -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()
|
||||||
|
}()
|
||||||
|
}
|
|
@ -238,6 +238,13 @@ out:
|
||||||
// RPC requests to perform a rescan. New jobs are not read until a rescan
|
// RPC requests to perform a rescan. New jobs are not read until a rescan
|
||||||
// finishes.
|
// finishes.
|
||||||
func (w *Wallet) rescanRPCHandler() {
|
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()
|
quit := w.quitChan()
|
||||||
|
|
||||||
out:
|
out:
|
||||||
|
@ -250,7 +257,7 @@ out:
|
||||||
log.Infof("Started rescan from block %v (height %d) for %d %s",
|
log.Infof("Started rescan from block %v (height %d) for %d %s",
|
||||||
batch.bs.Hash, batch.bs.Height, numAddrs, noun)
|
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)
|
batch.outpoints)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Rescan for %d %s failed: %v", numAddrs,
|
log.Errorf("Rescan for %d %s failed: %v", numAddrs,
|
||||||
|
|
670
wallet/wallet.go
670
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
|
* Permission to use, copy, modify, and distribute this software for any
|
||||||
* purpose with or without fee is hereby granted, provided that the above
|
* purpose with or without fee is hereby granted, provided that the above
|
||||||
|
@ -30,10 +30,12 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/blockchain"
|
"github.com/btcsuite/btcd/blockchain"
|
||||||
|
"github.com/btcsuite/btcd/btcec"
|
||||||
"github.com/btcsuite/btcd/btcjson"
|
"github.com/btcsuite/btcd/btcjson"
|
||||||
"github.com/btcsuite/btcd/chaincfg"
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
"github.com/btcsuite/btcd/txscript"
|
"github.com/btcsuite/btcd/txscript"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/btcsuite/btcrpcclient"
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
"github.com/btcsuite/btcwallet/chain"
|
"github.com/btcsuite/btcwallet/chain"
|
||||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||||
|
@ -42,6 +44,16 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
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"
|
walletDbWatchingOnlyName = "wowallet.db"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -60,15 +72,17 @@ var (
|
||||||
// complete wallet. It contains the Armory-style key store
|
// complete wallet. It contains the Armory-style key store
|
||||||
// addresses and keys),
|
// addresses and keys),
|
||||||
type Wallet struct {
|
type Wallet struct {
|
||||||
|
publicPassphrase []byte
|
||||||
|
|
||||||
// Data stores
|
// Data stores
|
||||||
db walletdb.DB
|
db walletdb.DB
|
||||||
Manager *waddrmgr.Manager
|
Manager *waddrmgr.Manager
|
||||||
TxStore *wtxmgr.Store
|
TxStore *wtxmgr.Store
|
||||||
|
|
||||||
chainSvr *chain.Client
|
chainClient *chain.RPCClient
|
||||||
chainSvrLock sync.Mutex
|
chainClientLock sync.Mutex
|
||||||
chainSvrSynced bool
|
chainClientSynced bool
|
||||||
chainSvrSyncMtx sync.Mutex
|
chainClientSyncMtx sync.Mutex
|
||||||
|
|
||||||
lockedOutpoints map[wire.OutPoint]struct{}
|
lockedOutpoints map[wire.OutPoint]struct{}
|
||||||
FeeIncrement btcutil.Amount
|
FeeIncrement btcutil.Amount
|
||||||
|
@ -93,9 +107,14 @@ type Wallet struct {
|
||||||
lockState chan bool
|
lockState chan bool
|
||||||
changePassphrase chan changePassphraseRequest
|
changePassphrase chan changePassphraseRequest
|
||||||
|
|
||||||
// Notification channels so other components can listen in on wallet
|
NtfnServer *NotificationServer
|
||||||
// activity. These are initialized as nil, and must be created by
|
|
||||||
// calling one of the Listen* methods.
|
// 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
|
connectedBlocks chan wtxmgr.BlockMeta
|
||||||
disconnectedBlocks chan wtxmgr.BlockMeta
|
disconnectedBlocks chan wtxmgr.BlockMeta
|
||||||
relevantTxs chan chain.RelevantTx
|
relevantTxs chan chain.RelevantTx
|
||||||
|
@ -263,7 +282,7 @@ func (w *Wallet) notifyRelevantTx(relevantTx chain.RelevantTx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start starts the goroutines necessary to manage a wallet.
|
// Start starts the goroutines necessary to manage a wallet.
|
||||||
func (w *Wallet) Start(chainServer *chain.Client) {
|
func (w *Wallet) Start() {
|
||||||
w.quitMu.Lock()
|
w.quitMu.Lock()
|
||||||
select {
|
select {
|
||||||
case <-w.quit:
|
case <-w.quit:
|
||||||
|
@ -280,19 +299,74 @@ func (w *Wallet) Start(chainServer *chain.Client) {
|
||||||
}
|
}
|
||||||
w.quitMu.Unlock()
|
w.quitMu.Unlock()
|
||||||
|
|
||||||
w.chainSvrLock.Lock()
|
w.wg.Add(2)
|
||||||
w.chainSvr = chainServer
|
|
||||||
w.chainSvrLock.Unlock()
|
|
||||||
|
|
||||||
w.wg.Add(6)
|
|
||||||
go w.handleChainNotifications()
|
|
||||||
go w.txCreator()
|
go w.txCreator()
|
||||||
go w.walletLocker()
|
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.rescanBatchHandler()
|
||||||
go w.rescanProgressHandler()
|
go w.rescanProgressHandler()
|
||||||
go w.rescanRPCHandler()
|
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.
|
// quitChan atomically reads the quit channel.
|
||||||
func (w *Wallet) quitChan() <-chan struct{} {
|
func (w *Wallet) quitChan() <-chan struct{} {
|
||||||
w.quitMu.Lock()
|
w.quitMu.Lock()
|
||||||
|
@ -311,11 +385,12 @@ func (w *Wallet) Stop() {
|
||||||
case <-quit:
|
case <-quit:
|
||||||
default:
|
default:
|
||||||
close(quit)
|
close(quit)
|
||||||
w.chainSvrLock.Lock()
|
w.chainClientLock.Lock()
|
||||||
if w.chainSvr != nil {
|
if w.chainClient != nil {
|
||||||
w.chainSvr.Stop()
|
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.
|
// WaitForShutdown blocks until all wallet goroutines have finished executing.
|
||||||
func (w *Wallet) WaitForShutdown() {
|
func (w *Wallet) WaitForShutdown() {
|
||||||
w.chainSvrLock.Lock()
|
w.chainClientLock.Lock()
|
||||||
if w.chainSvr != nil {
|
if w.chainClient != nil {
|
||||||
w.chainSvr.WaitForShutdown()
|
w.chainClient.WaitForShutdown()
|
||||||
}
|
}
|
||||||
w.chainSvrLock.Unlock()
|
w.chainClientLock.Unlock()
|
||||||
w.wg.Wait()
|
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
|
// ChainSynced returns whether the wallet has been attached to a chain server
|
||||||
// and synced up to the best block on the main chain.
|
// and synced up to the best block on the main chain.
|
||||||
func (w *Wallet) ChainSynced() bool {
|
func (w *Wallet) ChainSynced() bool {
|
||||||
w.chainSvrSyncMtx.Lock()
|
w.chainClientSyncMtx.Lock()
|
||||||
synced := w.chainSvrSynced
|
synced := w.chainClientSynced
|
||||||
w.chainSvrSyncMtx.Unlock()
|
w.chainClientSyncMtx.Unlock()
|
||||||
return synced
|
return synced
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -357,9 +445,9 @@ func (w *Wallet) ChainSynced() bool {
|
||||||
// until the reconnect notification is received, at which point the wallet can be
|
// until the reconnect notification is received, at which point the wallet can be
|
||||||
// marked out of sync again until after the next rescan completes.
|
// marked out of sync again until after the next rescan completes.
|
||||||
func (w *Wallet) SetChainSynced(synced bool) {
|
func (w *Wallet) SetChainSynced(synced bool) {
|
||||||
w.chainSvrSyncMtx.Lock()
|
w.chainClientSyncMtx.Lock()
|
||||||
w.chainSvrSynced = synced
|
w.chainClientSynced = synced
|
||||||
w.chainSvrSyncMtx.Unlock()
|
w.chainClientSyncMtx.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// activeData returns the currently-active receiving addresses and all unspent
|
// activeData returns the currently-active receiving addresses and all unspent
|
||||||
|
@ -383,6 +471,11 @@ func (w *Wallet) activeData() ([]btcutil.Address, []wtxmgr.Credit, error) {
|
||||||
// finished.
|
// finished.
|
||||||
//
|
//
|
||||||
func (w *Wallet) syncWithChain() error {
|
func (w *Wallet) syncWithChain() error {
|
||||||
|
chainClient, err := w.requireChainClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Request notifications for connected and disconnected blocks.
|
// Request notifications for connected and disconnected blocks.
|
||||||
//
|
//
|
||||||
// TODO(jrick): Either request this notification only once, or when
|
// 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
|
// as well. I am leaning towards allowing off all btcrpcclient
|
||||||
// notification re-registrations, in which case the code here should be
|
// notification re-registrations, in which case the code here should be
|
||||||
// left as is.
|
// left as is.
|
||||||
err := w.chainSvr.NotifyBlocks()
|
err = chainClient.NotifyBlocks()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -406,6 +499,28 @@ func (w *Wallet) syncWithChain() error {
|
||||||
// TODO(jrick): How should this handle a synced height earlier than
|
// TODO(jrick): How should this handle a synced height earlier than
|
||||||
// the chain server best block?
|
// 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
|
// Compare previously-seen blocks against the chain server. If any of
|
||||||
// these blocks no longer exist, rollback all of the missing blocks
|
// these blocks no longer exist, rollback all of the missing blocks
|
||||||
// before catching up with the rescan.
|
// before catching up with the rescan.
|
||||||
|
@ -419,7 +534,7 @@ func (w *Wallet) syncWithChain() error {
|
||||||
bs := iter.BlockStamp()
|
bs := iter.BlockStamp()
|
||||||
log.Debugf("Checking for previous saved block with height %v hash %v",
|
log.Debugf("Checking for previous saved block with height %v hash %v",
|
||||||
bs.Height, bs.Hash)
|
bs.Height, bs.Hash)
|
||||||
_, err = w.chainSvr.GetBlock(&bs.Hash)
|
_, err = chainClient.GetBlock(&bs.Hash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rollback = true
|
rollback = true
|
||||||
continue
|
continue
|
||||||
|
@ -508,7 +623,7 @@ func (w *Wallet) CreateSimpleTx(account uint32, pairs map[string]btcutil.Amount,
|
||||||
type (
|
type (
|
||||||
unlockRequest struct {
|
unlockRequest struct {
|
||||||
passphrase []byte
|
passphrase []byte
|
||||||
timeout time.Duration // Zero value prevents the timeout.
|
lockAfter <-chan time.Time // nil prevents the timeout.
|
||||||
err chan error
|
err chan error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -540,11 +655,7 @@ out:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
w.notifyLockStateChange(false)
|
w.notifyLockStateChange(false)
|
||||||
if req.timeout == 0 {
|
timeout = req.lockAfter
|
||||||
timeout = nil
|
|
||||||
} else {
|
|
||||||
timeout = time.After(req.timeout)
|
|
||||||
}
|
|
||||||
req.err <- nil
|
req.err <- nil
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -582,6 +693,7 @@ out:
|
||||||
break out
|
break out
|
||||||
|
|
||||||
case <-w.lockRequests:
|
case <-w.lockRequests:
|
||||||
|
timeout = nil
|
||||||
case <-timeout:
|
case <-timeout:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -605,11 +717,11 @@ out:
|
||||||
// correct, the current timeout is replaced with the new one. The wallet will
|
// 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
|
// be locked if the passphrase is incorrect or any other error occurs during the
|
||||||
// unlock.
|
// 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)
|
err := make(chan error, 1)
|
||||||
w.unlockRequests <- unlockRequest{
|
w.unlockRequests <- unlockRequest{
|
||||||
passphrase: passphrase,
|
passphrase: passphrase,
|
||||||
timeout: timeout,
|
lockAfter: lock,
|
||||||
err: err,
|
err: err,
|
||||||
}
|
}
|
||||||
return <-err
|
return <-err
|
||||||
|
@ -703,10 +815,22 @@ func (w *Wallet) CalculateBalance(confirms int32) (btcutil.Amount, error) {
|
||||||
return w.TxStore.Balance(confirms, blk.Height)
|
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.
|
// 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
|
// Get current block. The block height used for calculating
|
||||||
// the number of tx confirmations.
|
// the number of tx confirmations.
|
||||||
|
@ -714,32 +838,32 @@ func (w *Wallet) CalculateAccountBalance(account uint32, confirms int32) (btcuti
|
||||||
|
|
||||||
unspent, err := w.TxStore.UnspentOutputs()
|
unspent, err := w.TxStore.UnspentOutputs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return bals, err
|
||||||
}
|
}
|
||||||
for i := range unspent {
|
for i := range unspent {
|
||||||
output := &unspent[i]
|
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
|
var outputAcct uint32
|
||||||
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
|
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
|
||||||
output.PkScript, w.chainParams)
|
output.PkScript, w.chainParams)
|
||||||
if err == nil && len(addrs) > 0 {
|
if err == nil && len(addrs) > 0 {
|
||||||
outputAcct, err = w.Manager.AddrAccount(addrs[0])
|
outputAcct, err = w.Manager.AddrAccount(addrs[0])
|
||||||
}
|
}
|
||||||
if err == nil && outputAcct == account {
|
if err != nil || outputAcct != account {
|
||||||
bal += output.Amount
|
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
|
// 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
|
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
|
// CreditCategory describes the type of wallet transaction output. The category
|
||||||
// of "sent transactions" (debits) is always "send", and is not expressed by
|
// of "sent transactions" (debits) is always "send", and is not expressed by
|
||||||
// this type.
|
// this type.
|
||||||
|
@ -1066,6 +1227,189 @@ func (w *Wallet) ListAllTransactions() ([]btcjson.ListTransactionsResult, error)
|
||||||
return txList, err
|
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
|
// creditSlice satisifies the sort.Interface interface to provide sorting
|
||||||
// transaction credits from oldest to newest. Credits with the same receive
|
// 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
|
// 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()
|
addrStr := addr.Address().EncodeAddress()
|
||||||
log.Infof("Imported payment address %s", addrStr)
|
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 the payment address string of the imported private key.
|
||||||
return addrStr, nil
|
return addrStr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExportWatchingWallet returns a watching-only version of the wallet serialized
|
// ExportWatchingWallet returns a watching-only version of the wallet serialized
|
||||||
// database as a base64-encoded string.
|
// 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")
|
tmpDir, err := ioutil.TempDir("", "btcwallet")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -1370,7 +1722,7 @@ func (w *Wallet) ExportWatchingWallet(pubPass string) (string, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
woMgr, err := waddrmgr.Open(namespace, []byte(pubPass),
|
woMgr, err := waddrmgr.Open(namespace, w.publicPassphrase,
|
||||||
w.chainParams, nil)
|
w.chainParams, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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
|
// credits that are not known to have been mined into a block, and attempts
|
||||||
// to send each to the chain server for relay.
|
// to send each to the chain server for relay.
|
||||||
func (w *Wallet) ResendUnminedTxs() {
|
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()
|
txs, err := w.TxStore.UnminedTxs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Cannot load unmined transactions for resending: %v", err)
|
log.Errorf("Cannot load unmined transactions for resending: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, tx := range txs {
|
for _, tx := range txs {
|
||||||
resp, err := w.chainSvr.SendRawTransaction(tx, false)
|
resp, err := chainClient.SendRawTransaction(tx, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO(jrick): Check error for if this tx is a double spend,
|
// TODO(jrick): Check error for if this tx is a double spend,
|
||||||
// remove it if so.
|
// remove it if so.
|
||||||
|
@ -1496,9 +1854,23 @@ func (w *Wallet) NewAddress(account uint32) (btcutil.Address, error) {
|
||||||
for i, addr := range addrs {
|
for i, addr := range addrs {
|
||||||
utilAddrs[i] = addr.Address()
|
utilAddrs[i] = addr.Address()
|
||||||
}
|
}
|
||||||
if err := w.chainSvr.NotifyReceived(utilAddrs); err != nil {
|
w.chainClientLock.Lock()
|
||||||
|
chainClient := w.chainClient
|
||||||
|
w.chainClientLock.Unlock()
|
||||||
|
if chainClient != nil {
|
||||||
|
err := chainClient.NotifyReceived(utilAddrs)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
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
|
return utilAddrs[0], nil
|
||||||
}
|
}
|
||||||
|
@ -1517,9 +1889,13 @@ func (w *Wallet) NewChangeAddress(account uint32) (btcutil.Address, error) {
|
||||||
utilAddrs[i] = addr.Address()
|
utilAddrs[i] = addr.Address()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := w.chainSvr.NotifyReceived(utilAddrs); err != nil {
|
chainClient, err := w.requireChainClient()
|
||||||
|
if err == nil {
|
||||||
|
err = chainClient.NotifyReceived(utilAddrs)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return utilAddrs[0], nil
|
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,
|
func (w *Wallet) SendPairs(amounts map[string]btcutil.Amount, account uint32,
|
||||||
minconf int32) (*wire.ShaHash, error) {
|
minconf int32) (*wire.ShaHash, error) {
|
||||||
|
|
||||||
|
chainClient, err := w.requireChainClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Create transaction, replying with an error if the creation
|
// Create transaction, replying with an error if the creation
|
||||||
// was not successful.
|
// was not successful.
|
||||||
createdTx, err := w.CreateSimpleTx(account, amounts, minconf)
|
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
|
// TODO: The record already has the serialized tx, so no need to
|
||||||
// serialize it again.
|
// 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.
|
// Open loads an already-created wallet from the passed database and namespaces.
|
||||||
|
@ -1670,9 +2204,7 @@ func Open(pubPass []byte, params *chaincfg.Params, db walletdb.DB, waddrmgrNS, w
|
||||||
}
|
}
|
||||||
txMgr, err := wtxmgr.Open(wtxmgrNS)
|
txMgr, err := wtxmgr.Open(wtxmgrNS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !wtxmgr.IsNoExists(err) {
|
if wtxmgr.IsNoExists(err) {
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
log.Info("No recorded transaction history -- needs full rescan")
|
log.Info("No recorded transaction history -- needs full rescan")
|
||||||
err = addrMgr.SetSyncedTo(nil)
|
err = addrMgr.SetSyncedTo(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1682,10 +2214,14 @@ func Open(pubPass []byte, params *chaincfg.Params, db walletdb.DB, waddrmgrNS, w
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("Opened wallet") // TODO: log balance? last sync height?
|
log.Infof("Opened wallet") // TODO: log balance? last sync height?
|
||||||
w := &Wallet{
|
w := &Wallet{
|
||||||
|
publicPassphrase: pubPass,
|
||||||
db: db,
|
db: db,
|
||||||
Manager: addrMgr,
|
Manager: addrMgr,
|
||||||
TxStore: txMgr,
|
TxStore: txMgr,
|
||||||
|
@ -1705,5 +2241,9 @@ func Open(pubPass []byte, params *chaincfg.Params, db walletdb.DB, waddrmgrNS, w
|
||||||
chainParams: params,
|
chainParams: params,
|
||||||
quit: make(chan struct{}),
|
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
|
return w, nil
|
||||||
}
|
}
|
||||||
|
|
445
walletsetup.go
445
walletsetup.go
|
@ -18,12 +18,9 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcec"
|
"github.com/btcsuite/btcd/btcec"
|
||||||
"github.com/btcsuite/btcd/chaincfg"
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
|
@ -31,11 +28,11 @@ import (
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
"github.com/btcsuite/btcutil/hdkeychain"
|
"github.com/btcsuite/btcutil/hdkeychain"
|
||||||
"github.com/btcsuite/btcwallet/internal/legacy/keystore"
|
"github.com/btcsuite/btcwallet/internal/legacy/keystore"
|
||||||
|
"github.com/btcsuite/btcwallet/internal/prompt"
|
||||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||||
"github.com/btcsuite/btcwallet/wallet"
|
"github.com/btcsuite/btcwallet/wallet"
|
||||||
"github.com/btcsuite/btcwallet/walletdb"
|
"github.com/btcsuite/btcwallet/walletdb"
|
||||||
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
|
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
|
||||||
"github.com/btcsuite/golangcrypto/ssh/terminal"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Namespace keys
|
// Namespace keys
|
||||||
|
@ -61,311 +58,6 @@ func networkDir(dataDir string, chainParams *chaincfg.Params) string {
|
||||||
return filepath.Join(dataDir, netname)
|
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
|
// convertLegacyKeystore converts all of the addresses in the passed legacy
|
||||||
// key store to the new waddrmgr.Manager format. Both the legacy keystore and
|
// key store to the new waddrmgr.Manager format. Both the legacy keystore and
|
||||||
// the new manager must be unlocked.
|
// 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
|
// and generates the wallet accordingly. The new wallet will reside at the
|
||||||
// provided path.
|
// provided path.
|
||||||
func createWallet(cfg *config) error {
|
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
|
// 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
|
// don't end up exiting the process after the user has spent time
|
||||||
// entering a bunch of information.
|
// entering a bunch of information.
|
||||||
|
@ -449,15 +144,56 @@ func createWallet(cfg *config) error {
|
||||||
// existing keystore, the user will be promped for that passphrase,
|
// existing keystore, the user will be promped for that passphrase,
|
||||||
// otherwise they will be prompted for a new one.
|
// otherwise they will be prompted for a new one.
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
privPass, err := promptConsolePrivatePass(reader, legacyKeyStore)
|
privPass, err := prompt.PrivatePass(reader, legacyKeyStore)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
// Ascertain the public passphrase. This will either be a value
|
||||||
// specified by the user or the default hard-coded public passphrase if
|
// specified by the user or the default hard-coded public passphrase if
|
||||||
// the user does not want the additional public data encryption.
|
// 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -465,54 +201,18 @@ func createWallet(cfg *config) error {
|
||||||
// Ascertain the wallet generation seed. This will either be an
|
// Ascertain the wallet generation seed. This will either be an
|
||||||
// automatically generated value the user has already confirmed or a
|
// automatically generated value the user has already confirmed or a
|
||||||
// value the user has entered which has already been validated.
|
// value the user has entered which has already been validated.
|
||||||
seed, err := promptConsoleSeed(reader)
|
seed, err := prompt.Seed(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the wallet.
|
|
||||||
dbPath := filepath.Join(netDir, walletDbName)
|
|
||||||
fmt.Println("Creating the wallet...")
|
fmt.Println("Creating the wallet...")
|
||||||
|
w, err := loader.CreateNewWallet(pubPass, privPass, seed)
|
||||||
// Create the wallet database backed by bolt db.
|
|
||||||
db, err := walletdb.Create("bdb", dbPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the address manager.
|
w.Manager.Close()
|
||||||
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()
|
|
||||||
fmt.Println("The wallet has been created successfully.")
|
fmt.Println("The wallet has been created successfully.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -524,7 +224,7 @@ func createSimulationWallet(cfg *config) error {
|
||||||
privPass := []byte("password")
|
privPass := []byte("password")
|
||||||
|
|
||||||
// Public passphrase is the default.
|
// Public passphrase is the default.
|
||||||
pubPass := []byte(defaultPubPassphrase)
|
pubPass := []byte(wallet.InsecurePubPassphrase)
|
||||||
|
|
||||||
// Generate a random seed.
|
// Generate a random seed.
|
||||||
seed, err := hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen)
|
seed, err := hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen)
|
||||||
|
@ -583,46 +283,3 @@ func checkCreateDir(path string) error {
|
||||||
|
|
||||||
return nil
|
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
|
|
||||||
}
|
|
||||||
|
|
39
wtxmgr/tx.go
39
wtxmgr/tx.go
|
@ -142,6 +142,10 @@ type Credit struct {
|
||||||
// transactions.
|
// transactions.
|
||||||
type Store struct {
|
type Store struct {
|
||||||
namespace walletdb.Namespace
|
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
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// 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 storeError(ErrInput, str, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return scopedUpdate(s.namespace, func(ns walletdb.Bucket) error {
|
var isNew bool
|
||||||
return s.addCredit(ns, rec, block, index, change)
|
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 {
|
if block == nil {
|
||||||
k := canonicalOutPoint(&rec.Hash, index)
|
k := canonicalOutPoint(&rec.Hash, index)
|
||||||
|
if existsRawUnminedCredit(ns, k) != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
v := valueUnminedCredit(btcutil.Amount(rec.MsgTx.TxOut[index].Value), change)
|
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)
|
k, v := existsCredit(ns, &rec.Hash, index, &block.Block)
|
||||||
if v != nil {
|
if v != nil {
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
txOutAmt := btcutil.Amount(rec.MsgTx.TxOut[index].Value)
|
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)
|
v = valueUnspentCredit(&cred)
|
||||||
err := putRawCredit(ns, k, v)
|
err := putRawCredit(ns, k, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
minedBalance, err := fetchMinedBalance(ns)
|
minedBalance, err := fetchMinedBalance(ns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
err = putMinedBalance(ns, minedBalance+txOutAmt)
|
err = putMinedBalance(ns, minedBalance+txOutAmt)
|
||||||
if err != nil {
|
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
|
// Rollback removes all blocks at height onwards, moving any transactions within
|
||||||
|
|
|
@ -170,3 +170,28 @@ func (s *Store) unminedTxs(ns walletdb.Bucket) ([]*wire.MsgTx, error) {
|
||||||
})
|
})
|
||||||
return unmined, err
|
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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue