diff --git a/account.go b/account.go index 05028f6..23fea34 100644 --- a/account.go +++ b/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) { diff --git a/cmdmgr.go b/cmdmgr.go index cf8f9d8..3aba762 100644 --- a/cmdmgr.go +++ b/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 diff --git a/disksync.go b/disksync.go index 26e9d54..573e4fd 100644 --- a/disksync.go +++ b/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 +}