Implement exporting a watching-only wallet.
This change allows for the use of watching-only wallets. Unlike normal, "hot" wallets, watching-only wallets do not contain any private keys, and can be used in situations where you want to keep one wallet online to create new receiving addresses and watch for received transactions, while keeping the hot wallet offline (possibly on an air-gapped computer). Two (websocket) extension RPC calls have been added: First, exportwatchingwallet, which will export the current hot wallet to a watching-only wallet, saving either to disk or returning the base64-encoded wallet files to the caller. Second, recoveraddresses, which is used to recover the next n addresses from the address chain. This is used to "sync" a watching wallet with the hot wallet, or vice versa.
This commit is contained in:
parent
8b65e651cd
commit
bd89f076cd
3 changed files with 234 additions and 0 deletions
88
account.go
88
account.go
|
@ -18,6 +18,7 @@ package main
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/conformal/btcutil"
|
||||
|
@ -433,6 +434,58 @@ func (a *Account) ImportWIFPrivateKey(wif string, bs *wallet.BlockStamp) (string
|
|||
return addrStr, nil
|
||||
}
|
||||
|
||||
// ExportWatchingWallet returns a new account with a watching wallet
|
||||
// exported by this a's wallet. Both wallets share the same tx and utxo
|
||||
// stores, so locking one will lock the other as well. The returned account
|
||||
// should be exported quickly, either to file or to an rpc caller, and then
|
||||
// dropped from scope.
|
||||
func (a *Account) ExportWatchingWallet() (*Account, error) {
|
||||
a.mtx.RLock()
|
||||
defer a.mtx.RUnlock()
|
||||
|
||||
ww, err := a.Wallet.ExportWatchingWallet()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wa := *a
|
||||
wa.Wallet = ww
|
||||
return &wa, nil
|
||||
}
|
||||
|
||||
// exportBase64 exports an account's serialized wallet, tx, and utxo
|
||||
// stores as base64-encoded values in a map.
|
||||
func (a *Account) exportBase64() (map[string]string, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
m := make(map[string]string)
|
||||
|
||||
a.mtx.RLock()
|
||||
defer a.mtx.RUnlock()
|
||||
if _, err := a.Wallet.WriteTo(buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m["wallet"] = base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
buf.Reset()
|
||||
|
||||
a.TxStore.RLock()
|
||||
defer a.TxStore.RUnlock()
|
||||
if _, err := a.TxStore.s.WriteTo(buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m["tx"] = base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
buf.Reset()
|
||||
|
||||
a.UtxoStore.RLock()
|
||||
defer a.UtxoStore.RUnlock()
|
||||
if _, err := a.UtxoStore.s.WriteTo(buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m["utxo"] = base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
buf.Reset()
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Track requests btcd to send notifications of new transactions for
|
||||
// each address stored in a wallet.
|
||||
func (a *Account) Track() {
|
||||
|
@ -557,6 +610,41 @@ func (a *Account) NewAddress() (btcutil.Address, error) {
|
|||
return addr, nil
|
||||
}
|
||||
|
||||
// RecoverAddresses recovers the next n chained addresses of a wallet.
|
||||
func (a *Account) RecoverAddresses(n int) error {
|
||||
a.mtx.Lock()
|
||||
|
||||
// Get info on the last chained address. The rescan starts at the
|
||||
// earliest block height the last chained address might appear at.
|
||||
last := a.Wallet.LastChainedAddress()
|
||||
lastInfo, err := a.Wallet.AddressInfo(last)
|
||||
if err != nil {
|
||||
a.mtx.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
addrs, err := a.Wallet.ExtendActiveAddresses(n, cfg.KeypoolSize)
|
||||
a.mtx.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Run a goroutine to rescan blockchain for recovered addresses.
|
||||
m := make(map[string]struct{})
|
||||
for i := range addrs {
|
||||
m[addrs[i].EncodeAddress()] = struct{}{}
|
||||
}
|
||||
go func(addrs map[string]struct{}) {
|
||||
jsonErr := Rescan(CurrentRPCConn(), lastInfo.FirstBlock, addrs)
|
||||
if jsonErr != nil {
|
||||
log.Errorf("Rescanning for recovered addresses failed: %v",
|
||||
jsonErr.Message)
|
||||
}
|
||||
}(m)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReqNewTxsForAddress sends a message to btcd to request tx updates
|
||||
// for addr for each new block that is added to the blockchain.
|
||||
func (a *Account) ReqNewTxsForAddress(addr btcutil.Address) {
|
||||
|
|
97
cmdmgr.go
97
cmdmgr.go
|
@ -87,10 +87,12 @@ var rpcHandlers = map[string]cmdHandler{
|
|||
|
||||
// Extensions exclusive to websocket connections.
|
||||
var wsHandlers = map[string]cmdHandler{
|
||||
"exportwatchingwallet": ExportWatchingWallet,
|
||||
"getaddressbalance": GetAddressBalance,
|
||||
"getunconfirmedbalance": GetUnconfirmedBalance,
|
||||
"listaddresstransactions": ListAddressTransactions,
|
||||
"listalltransactions": ListAllTransactions,
|
||||
"recoveraddresses": RecoverAddresses,
|
||||
"walletislocked": WalletIsLocked,
|
||||
}
|
||||
|
||||
|
@ -224,6 +226,68 @@ func DumpWallet(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
|
|||
}
|
||||
}
|
||||
|
||||
// ExportWatchingWallet handles an exportwatchingwallet request by exporting
|
||||
// the current account wallet as a watching wallet (with no private keys), and
|
||||
// either writing the exported wallet to disk, or base64-encoding serialized
|
||||
// account files and sending them back in the response.
|
||||
func ExportWatchingWallet(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
|
||||
// Type assert icmd to access parameters.
|
||||
cmd, ok := icmd.(*btcws.ExportWatchingWalletCmd)
|
||||
if !ok {
|
||||
return nil, &btcjson.ErrInternal
|
||||
}
|
||||
|
||||
a, err := accountstore.Account(cmd.Account)
|
||||
switch err {
|
||||
case nil:
|
||||
break
|
||||
|
||||
case ErrAcctNotExist:
|
||||
return nil, &btcjson.ErrWalletInvalidAccountName
|
||||
|
||||
default: // all other non-nil errors
|
||||
e := btcjson.Error{
|
||||
Code: btcjson.ErrWallet.Code,
|
||||
Message: err.Error(),
|
||||
}
|
||||
return nil, &e
|
||||
}
|
||||
|
||||
wa, err := a.ExportWatchingWallet()
|
||||
if err != nil {
|
||||
e := btcjson.Error{
|
||||
Code: btcjson.ErrWallet.Code,
|
||||
Message: err.Error(),
|
||||
}
|
||||
return nil, &e
|
||||
}
|
||||
|
||||
if cmd.Download {
|
||||
switch m, err := wa.exportBase64(); err {
|
||||
case nil:
|
||||
return m, nil
|
||||
|
||||
default:
|
||||
e := btcjson.Error{
|
||||
Code: btcjson.ErrWallet.Code,
|
||||
Message: err.Error(),
|
||||
}
|
||||
return nil, &e
|
||||
}
|
||||
}
|
||||
|
||||
// Create export directory, write files there.
|
||||
if err = wa.WriteExport("watchingwallet"); err != nil {
|
||||
e := btcjson.Error{
|
||||
Code: btcjson.ErrWallet.Code,
|
||||
Message: err.Error(),
|
||||
}
|
||||
return nil, &e
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetAddressesByAccount handles a getaddressesbyaccount request by returning
|
||||
// all addresses for an account, or an error if the requested account does
|
||||
// not exist.
|
||||
|
@ -1081,6 +1145,39 @@ func CreateEncryptedWallet(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
|
|||
}
|
||||
}
|
||||
|
||||
func RecoverAddresses(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
|
||||
cmd, ok := icmd.(*btcws.RecoverAddressesCmd)
|
||||
if !ok {
|
||||
return nil, &btcjson.ErrInternal
|
||||
}
|
||||
|
||||
a, err := accountstore.Account(cmd.Account)
|
||||
switch err {
|
||||
case nil:
|
||||
break
|
||||
|
||||
case ErrAcctNotExist:
|
||||
return nil, &btcjson.ErrWalletInvalidAccountName
|
||||
|
||||
default: // all other non-nil errors
|
||||
e := btcjson.Error{
|
||||
Code: btcjson.ErrWallet.Code,
|
||||
Message: err.Error(),
|
||||
}
|
||||
return nil, &e
|
||||
}
|
||||
|
||||
if err := a.RecoverAddresses(cmd.N); err != nil {
|
||||
e := btcjson.Error{
|
||||
Code: btcjson.ErrWallet.Code,
|
||||
Message: err.Error(),
|
||||
}
|
||||
return nil, &e
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// WalletIsLocked handles the walletislocked extension request by
|
||||
// returning the current lock state (false for unlocked, true for locked)
|
||||
// of an account. An error is returned if the requested account does not
|
||||
|
|
49
disksync.go
49
disksync.go
|
@ -206,3 +206,52 @@ func (a *Account) writeDirtyToDisk() error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteExport writes an account to a special export directory named
|
||||
// by dirName. Any previous files are overwritten.
|
||||
func (a *Account) WriteExport(dirName string) error {
|
||||
exportPath := filepath.Join(networkDir(cfg.Net()), dirName)
|
||||
if err := checkCreateDir(exportPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aname := a.Name()
|
||||
wfilepath := accountFilename("wallet.bin", aname, exportPath)
|
||||
txfilepath := accountFilename("tx.bin", aname, exportPath)
|
||||
utxofilepath := accountFilename("utxo.bin", aname, exportPath)
|
||||
|
||||
a.UtxoStore.RLock()
|
||||
defer a.UtxoStore.RUnlock()
|
||||
utxofile, err := os.Create(utxofilepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer utxofile.Close()
|
||||
if _, err := a.UtxoStore.s.WriteTo(utxofile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.TxStore.RLock()
|
||||
defer a.TxStore.RUnlock()
|
||||
txfile, err := os.Create(txfilepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer txfile.Close()
|
||||
if _, err := a.TxStore.s.WriteTo(txfile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.mtx.RLock()
|
||||
defer a.mtx.RUnlock()
|
||||
wfile, err := os.Create(wfilepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer wfile.Close()
|
||||
if _, err := a.Wallet.WriteTo(wfile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue