lbcwallet/wallet.go
2015-03-19 06:28:23 +05:30

1496 lines
42 KiB
Go

/*
* Copyright (c) 2013, 2014 Conformal Systems LLC <info@conformal.com>
*
* 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 (
"bufio"
"bytes"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/btcjson"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/chain"
"github.com/btcsuite/btcwallet/txstore"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/btcsuite/golangcrypto/ssh/terminal"
)
// ErrNotSynced describes an error where an operation cannot complete
// due wallet being out of sync (and perhaps currently syncing with)
// the remote chain server.
var ErrNotSynced = errors.New("wallet is not synchronized with the chain server")
var (
// waddrmgrNamespaceKey is the namespace key for the waddrmgr package.
waddrmgrNamespaceKey = []byte("waddrmgr")
)
const (
// 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 more easier by allowing a limited number of empty accounts.
maxEmptyAccounts = 100
)
// 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
}
}
// networkDir returns the directory name of a network directory to hold wallet
// files.
func networkDir(dataDir string, chainParams *chaincfg.Params) string {
netname := chainParams.Name
// For now, we must always name the testnet data directory as "testnet"
// and not "testnet3" or any other version, as the chaincfg testnet3
// paramaters will likely be switched to being named "testnet3" in the
// future. This is done to future proof that change, and an upgrade
// plan to move the testnet3 data directory can be worked out later.
if chainParams.Net == wire.TestNet3 {
netname = "testnet"
}
return filepath.Join(dataDir, netname)
}
// Wallet is a structure containing all the components for a
// complete wallet. It contains the Armory-style key store
// addresses and keys),
type Wallet struct {
// Data stores
db walletdb.DB
Manager *waddrmgr.Manager
TxStore *txstore.Store
chainSvr *chain.Client
chainSvrLock sync.Locker
chainSvrSynced bool
chainSvrSyncMtx sync.Mutex
lockedOutpoints map[wire.OutPoint]struct{}
FeeIncrement btcutil.Amount
// Channels for rescan processing. Requests are added and merged with
// any waiting requests, before being sent to another goroutine to
// call the rescan RPC.
rescanAddJob chan *RescanJob
rescanBatch chan *rescanBatch
rescanNotifications chan interface{} // From chain server
rescanProgress chan *RescanProgressMsg
rescanFinished chan *RescanFinishedMsg
// Channel for transaction creation requests.
createTxRequests chan createTxRequest
// Channels for the manager locker.
unlockRequests chan unlockRequest
lockRequests chan struct{}
holdUnlockRequests chan chan HeldUnlock
lockState chan bool
changePassphrase chan changePassphraseRequest
// Notification channels so other components can listen in on wallet
// activity. These are initialized as nil, and must be created by
// calling one of the Listen* methods.
connectedBlocks chan waddrmgr.BlockStamp
disconnectedBlocks chan waddrmgr.BlockStamp
lockStateChanges chan bool // true when locked
confirmedBalance chan btcutil.Amount
unconfirmedBalance chan btcutil.Amount
notificationLock sync.Locker
wg sync.WaitGroup
quit chan struct{}
}
// newWallet creates a new Wallet structure with the provided address manager
// and transaction store.
func newWallet(mgr *waddrmgr.Manager, txs *txstore.Store) *Wallet {
return &Wallet{
Manager: mgr,
TxStore: txs,
chainSvrLock: new(sync.Mutex),
lockedOutpoints: map[wire.OutPoint]struct{}{},
FeeIncrement: defaultFeeIncrement,
rescanAddJob: make(chan *RescanJob),
rescanBatch: make(chan *rescanBatch),
rescanNotifications: make(chan interface{}),
rescanProgress: make(chan *RescanProgressMsg),
rescanFinished: make(chan *RescanFinishedMsg),
createTxRequests: make(chan createTxRequest),
unlockRequests: make(chan unlockRequest),
lockRequests: make(chan struct{}),
holdUnlockRequests: make(chan chan HeldUnlock),
lockState: make(chan bool),
changePassphrase: make(chan changePassphraseRequest),
notificationLock: new(sync.Mutex),
quit: make(chan struct{}),
}
}
// ErrDuplicateListen is returned for any attempts to listen for the same
// notification more than once. If callers must pass along a notifiation to
// multiple places, they must broadcast it themself.
var ErrDuplicateListen = errors.New("duplicate listen")
func (w *Wallet) updateNotificationLock() {
switch {
case w.connectedBlocks == nil:
fallthrough
case w.disconnectedBlocks == nil:
fallthrough
case w.lockStateChanges == nil:
fallthrough
case w.confirmedBalance == nil:
fallthrough
case w.unconfirmedBalance == nil:
return
}
w.notificationLock = noopLocker{}
}
// CreditAccount returns the first account that can be associated
// with the given credit.
// If no account is found, ErrAccountNotFound is returned.
func (w *Wallet) CreditAccount(c txstore.Credit) (uint32, error) {
_, addrs, _, _ := c.Addresses(activeNet.Params)
addr := addrs[0]
return w.Manager.AddrAccount(addr)
}
// ListenConnectedBlocks returns a channel that passes all blocks that a wallet
// has been marked in sync with. The channel must be read, or other wallet
// methods will block.
//
// If this is called twice, ErrDuplicateListen is returned.
func (w *Wallet) ListenConnectedBlocks() (<-chan waddrmgr.BlockStamp, error) {
w.notificationLock.Lock()
defer w.notificationLock.Unlock()
if w.connectedBlocks != nil {
return nil, ErrDuplicateListen
}
w.connectedBlocks = make(chan waddrmgr.BlockStamp)
w.updateNotificationLock()
return w.connectedBlocks, nil
}
// ListenDisconnectedBlocks returns a channel that passes all blocks that a
// wallet has detached. The channel must be read, or other wallet methods will
// block.
//
// If this is called twice, ErrDuplicateListen is returned.
func (w *Wallet) ListenDisconnectedBlocks() (<-chan waddrmgr.BlockStamp, error) {
w.notificationLock.Lock()
defer w.notificationLock.Unlock()
if w.disconnectedBlocks != nil {
return nil, ErrDuplicateListen
}
w.disconnectedBlocks = make(chan waddrmgr.BlockStamp)
w.updateNotificationLock()
return w.disconnectedBlocks, nil
}
// ListenLockStatus returns a channel that passes the current lock state
// of the wallet whenever the lock state is changed. The value is true for
// locked, and false for unlocked. The channel must be read, or other wallet
// methods will block.
//
// If this is called twice, ErrDuplicateListen is returned.
func (w *Wallet) ListenLockStatus() (<-chan bool, error) {
w.notificationLock.Lock()
defer w.notificationLock.Unlock()
if w.lockStateChanges != nil {
return nil, ErrDuplicateListen
}
w.lockStateChanges = make(chan bool)
w.updateNotificationLock()
return w.lockStateChanges, nil
}
// ListenConfirmedBalance returns a channel that passes the confirmed balance
// when any changes to the balance are made. This channel must be read, or
// other wallet methods will block.
//
// If this is called twice, ErrDuplicateListen is returned.
func (w *Wallet) ListenConfirmedBalance() (<-chan btcutil.Amount, error) {
w.notificationLock.Lock()
defer w.notificationLock.Unlock()
if w.confirmedBalance != nil {
return nil, ErrDuplicateListen
}
w.confirmedBalance = make(chan btcutil.Amount)
w.updateNotificationLock()
return w.confirmedBalance, nil
}
// ListenUnconfirmedBalance returns a channel that passes the unconfirmed
// balance when any changes to the balance are made. This channel must be
// read, or other wallet methods will block.
//
// If this is called twice, ErrDuplicateListen is returned.
func (w *Wallet) ListenUnconfirmedBalance() (<-chan btcutil.Amount, error) {
w.notificationLock.Lock()
defer w.notificationLock.Unlock()
if w.unconfirmedBalance != nil {
return nil, ErrDuplicateListen
}
w.unconfirmedBalance = make(chan btcutil.Amount)
w.updateNotificationLock()
return w.unconfirmedBalance, nil
}
// markAddrsUsed marks the addresses credited by the given transaction
// record as used.
func (w *Wallet) markAddrsUsed(t *txstore.TxRecord) error {
for _, c := range t.Credits() {
// Errors don't matter here. If addrs is nil, the
// range below does nothing.
_, addrs, _, _ := c.Addresses(activeNet.Params)
for _, addr := range addrs {
addressID := addr.ScriptAddress()
if err := w.Manager.MarkUsed(addressID); err != nil {
return err
}
log.Infof("Marked address used %s", addr.EncodeAddress())
}
}
return nil
}
func (w *Wallet) notifyConnectedBlock(block waddrmgr.BlockStamp) {
w.notificationLock.Lock()
if w.connectedBlocks != nil {
w.connectedBlocks <- block
}
w.notificationLock.Unlock()
}
func (w *Wallet) notifyDisconnectedBlock(block waddrmgr.BlockStamp) {
w.notificationLock.Lock()
if w.disconnectedBlocks != nil {
w.disconnectedBlocks <- block
}
w.notificationLock.Unlock()
}
func (w *Wallet) notifyLockStateChange(locked bool) {
w.notificationLock.Lock()
if w.lockStateChanges != nil {
w.lockStateChanges <- locked
}
w.notificationLock.Unlock()
}
func (w *Wallet) notifyConfirmedBalance(bal btcutil.Amount) {
w.notificationLock.Lock()
if w.confirmedBalance != nil {
w.confirmedBalance <- bal
}
w.notificationLock.Unlock()
}
func (w *Wallet) notifyUnconfirmedBalance(bal btcutil.Amount) {
w.notificationLock.Lock()
if w.unconfirmedBalance != nil {
w.unconfirmedBalance <- bal
}
w.notificationLock.Unlock()
}
// Start starts the goroutines necessary to manage a wallet.
func (w *Wallet) Start(chainServer *chain.Client) {
select {
case <-w.quit:
return
default:
}
w.chainSvrLock.Lock()
defer w.chainSvrLock.Unlock()
w.chainSvr = chainServer
w.chainSvrLock = noopLocker{}
w.wg.Add(7)
go w.diskWriter()
go w.handleChainNotifications()
go w.txCreator()
go w.walletLocker()
go w.rescanBatchHandler()
go w.rescanProgressHandler()
go w.rescanRPCHandler()
}
// Stop signals all wallet goroutines to shutdown.
func (w *Wallet) Stop() {
select {
case <-w.quit:
default:
close(w.quit)
w.chainSvrLock.Lock()
if w.chainSvr != nil {
w.chainSvr.Stop()
}
w.chainSvrLock.Unlock()
}
}
// ShuttingDown returns whether the wallet is currently in the process of
// shutting down or not.
func (w *Wallet) ShuttingDown() bool {
select {
case <-w.quit:
return true
default:
return false
}
}
// WaitForShutdown blocks until all wallet goroutines have finished executing.
func (w *Wallet) WaitForShutdown() {
w.chainSvrLock.Lock()
if w.chainSvr != nil {
w.chainSvr.WaitForShutdown()
}
w.chainSvrLock.Unlock()
w.wg.Wait()
}
// ChainSynced returns whether the wallet has been attached to a chain server
// and synced up to the best block on the main chain.
func (w *Wallet) ChainSynced() bool {
w.chainSvrSyncMtx.Lock()
synced := w.chainSvrSynced
w.chainSvrSyncMtx.Unlock()
return synced
}
// SetChainSynced marks whether the wallet is connected to and currently in sync
// with the latest block notified by the chain server.
//
// NOTE: Due to an API limitation with btcrpcclient, this may return true after
// the client disconnected (and is attempting a reconnect). This will be unknown
// until the reconnect notification is received, at which point the wallet can be
// marked out of sync again until after the next rescan completes.
func (w *Wallet) SetChainSynced(synced bool) {
w.chainSvrSyncMtx.Lock()
w.chainSvrSynced = synced
w.chainSvrSyncMtx.Unlock()
}
// activeData returns the currently-active receiving addresses and all unspent
// outputs. This is primarely intended to provide the parameters for a
// rescan request.
func (w *Wallet) activeData() ([]btcutil.Address, []txstore.Credit, error) {
addrs, err := w.Manager.AllActiveAddresses()
if err != nil {
return nil, nil, err
}
unspent, err := w.TxStore.UnspentOutputs()
return addrs, unspent, err
}
// syncWithChain brings the wallet up to date with the current chain server
// connection. It creates a rescan request and blocks until the rescan has
// finished.
//
func (w *Wallet) syncWithChain() error {
// Request notifications for connected and disconnected blocks.
//
// TODO(jrick): Either request this notification only once, or when
// btcrpcclient is modified to allow some notification request to not
// automatically resent on reconnect, include the notifyblocks request
// as well. I am leaning towards allowing off all btcrpcclient
// notification re-registrations, in which case the code here should be
// left as is.
err := w.chainSvr.NotifyBlocks()
if err != nil {
return err
}
// Request notifications for transactions sending to all wallet
// addresses.
addrs, unspent, err := w.activeData()
if err != nil {
return err
}
// TODO(jrick): How should this handle a synced height earlier than
// the chain server best block?
// Check that there was not any reorgs done since last connection.
// If so, rollback and rescan to catch up.
iter := w.Manager.NewIterateRecentBlocks()
for cont := iter != nil; cont; cont = iter.Prev() {
bs := iter.BlockStamp()
log.Debugf("Checking for previous saved block with height %v hash %v",
bs.Height, bs.Hash)
_, err = w.chainSvr.GetBlock(&bs.Hash)
if err != nil {
continue
}
log.Debug("Found matching block.")
// If we had to go back to any previous blocks (iter.Next
// returns true), then rollback the next and all child blocks.
if iter.Next() {
bs := iter.BlockStamp()
w.Manager.SetSyncedTo(&bs)
err = w.TxStore.Rollback(bs.Height)
if err != nil {
return err
}
w.TxStore.MarkDirty()
}
break
}
return w.Rescan(addrs, unspent)
}
type (
createTxRequest struct {
account uint32
pairs map[string]btcutil.Amount
minconf int
resp chan createTxResponse
}
createTxResponse struct {
tx *CreatedTx
err error
}
)
// txCreator is responsible for the input selection and creation of
// transactions. These functions are the responsibility of this method
// (designed to be run as its own goroutine) since input selection must be
// serialized, or else it is possible to create double spends by choosing the
// same inputs for multiple transactions. Along with input selection, this
// method is also responsible for the signing of transactions, since we don't
// want to end up in a situation where we run out of inputs as multiple
// transactions are being created. In this situation, it would then be possible
// for both requests, rather than just one, to fail due to not enough available
// inputs.
func (w *Wallet) txCreator() {
out:
for {
select {
case txr := <-w.createTxRequests:
tx, err := w.txToPairs(txr.pairs, txr.account, txr.minconf)
txr.resp <- createTxResponse{tx, err}
case <-w.quit:
break out
}
}
w.wg.Done()
}
// CreateSimpleTx creates a new signed transaction spending unspent P2PKH
// outputs with at laest minconf confirmations spending to any number of
// address/amount pairs. Change and an appropiate transaction fee are
// automatically included, if necessary. All transaction creation through
// this function is serialized to prevent the creation of many transactions
// which spend the same outputs.
func (w *Wallet) CreateSimpleTx(account uint32, pairs map[string]btcutil.Amount,
minconf int) (*CreatedTx, error) {
req := createTxRequest{
account: account,
pairs: pairs,
minconf: minconf,
resp: make(chan createTxResponse),
}
w.createTxRequests <- req
resp := <-req.resp
return resp.tx, resp.err
}
type (
unlockRequest struct {
passphrase []byte
timeout time.Duration // Zero value prevents the timeout.
err chan error
}
changePassphraseRequest struct {
old, new []byte
err chan error
}
// HeldUnlock is a tool to prevent the wallet from automatically
// locking after some timeout before an operation which needed
// the unlocked wallet has finished. Any aquired HeldUnlock
// *must* be released (preferably with a defer) or the wallet
// will forever remain unlocked.
HeldUnlock chan struct{}
)
// walletLocker manages the locked/unlocked state of a wallet.
func (w *Wallet) walletLocker() {
var timeout <-chan time.Time
holdChan := make(HeldUnlock)
out:
for {
select {
case req := <-w.unlockRequests:
err := w.Manager.Unlock(req.passphrase)
if err != nil {
req.err <- err
continue
}
w.notifyLockStateChange(false)
if req.timeout == 0 {
timeout = nil
} else {
timeout = time.After(req.timeout)
}
req.err <- nil
continue
case req := <-w.changePassphrase:
err := w.Manager.ChangePassphrase(req.old, req.new, true)
req.err <- err
continue
case req := <-w.holdUnlockRequests:
if w.Manager.IsLocked() {
close(req)
continue
}
req <- holdChan
<-holdChan // Block until the lock is released.
// If, after holding onto the unlocked wallet for some
// time, the timeout has expired, lock it now instead
// of hoping it gets unlocked next time the top level
// select runs.
select {
case <-timeout:
// Let the top level select fallthrough so the
// wallet is locked.
default:
continue
}
case w.lockState <- w.Manager.IsLocked():
continue
case <-w.quit:
break out
case <-w.lockRequests:
case <-timeout:
}
// Select statement fell through by an explicit lock or the
// timer expiring. Lock the manager here.
timeout = nil
err := w.Manager.Lock()
if err != nil {
log.Errorf("Could not lock wallet: %v", err)
} else {
w.notifyLockStateChange(true)
}
}
w.wg.Done()
}
// Unlock unlocks the wallet's address manager and relocks it after timeout has
// expired. If the wallet is already unlocked and the new passphrase is
// correct, the current timeout is replaced with the new one. The wallet will
// be locked if the passphrase is incorrect or any other error occurs during the
// unlock.
func (w *Wallet) Unlock(passphrase []byte, timeout time.Duration) error {
err := make(chan error, 1)
w.unlockRequests <- unlockRequest{
passphrase: passphrase,
timeout: timeout,
err: err,
}
return <-err
}
// Lock locks the wallet's address manager.
func (w *Wallet) Lock() {
w.lockRequests <- struct{}{}
}
// Locked returns whether the account manager for a wallet is locked.
func (w *Wallet) Locked() bool {
return <-w.lockState
}
// HoldUnlock prevents the wallet from being locked. The HeldUnlock object
// *must* be released, or the wallet will forever remain unlocked.
//
// TODO: To prevent the above scenario, perhaps closures should be passed
// to the walletLocker goroutine and disallow callers from explicitly
// handling the locking mechanism.
func (w *Wallet) HoldUnlock() (HeldUnlock, error) {
req := make(chan HeldUnlock)
w.holdUnlockRequests <- req
hl, ok := <-req
if !ok {
// TODO(davec): This should be defined and exported from
// waddrmgr.
return nil, waddrmgr.ManagerError{
ErrorCode: waddrmgr.ErrLocked,
Description: "address manager is locked",
}
}
return hl, nil
}
// Release releases the hold on the unlocked-state of the wallet and allows the
// wallet to be locked again. If a lock timeout has already expired, the
// wallet is locked again as soon as Release is called.
func (c HeldUnlock) Release() {
c <- struct{}{}
}
// ChangePassphrase attempts to change the passphrase for a wallet from old
// to new. Changing the passphrase is synchronized with all other address
// manager locking and unlocking. The lock state will be the same as it was
// before the password change.
func (w *Wallet) ChangePassphrase(old, new []byte) error {
err := make(chan error, 1)
w.changePassphrase <- changePassphraseRequest{
old: old,
new: new,
err: err,
}
return <-err
}
// diskWriter periodically (every 10 seconds) writes out the transaction store
// to disk if it is marked dirty.
func (w *Wallet) diskWriter() {
ticker := time.NewTicker(10 * time.Second)
var wg sync.WaitGroup
var done bool
for {
select {
case <-ticker.C:
case <-w.quit:
done = true
}
log.Trace("Writing txstore")
wg.Add(1)
go func() {
err := w.TxStore.WriteIfDirty()
if err != nil {
log.Errorf("Cannot write txstore: %v",
err)
}
wg.Done()
}()
wg.Wait()
if done {
break
}
}
w.wg.Done()
}
// AddressUsed returns whether there are any recorded transactions spending to
// a given address. Assumming correct TxStore usage, this will return true iff
// there are any transactions with outputs to this address in the blockchain or
// the btcd mempool.
func (w *Wallet) AddressUsed(addr waddrmgr.ManagedAddress) bool {
return addr.Used()
}
// AccountUsed returns whether there are any recorded transactions spending to
// a given account. It returns true if atleast one address in the account was
// used and false if no address in the account was used.
func (w *Wallet) AccountUsed(account uint32) (bool, error) {
addrs, err := w.Manager.AllAccountAddresses(account)
if err != nil {
return false, err
}
for _, addr := range addrs {
if w.AddressUsed(addr) {
return true, nil
}
}
return false, nil
}
// CalculateBalance sums the amounts of all unspent transaction
// outputs to addresses of a wallet and returns the balance.
//
// If confirmations is 0, all UTXOs, even those not present in a
// block (height -1), will be used to get the balance. Otherwise,
// a UTXO must be in a block. If confirmations is 1 or greater,
// the balance will be calculated based on how many how many blocks
// include a UTXO.
func (w *Wallet) CalculateBalance(confirms int) (btcutil.Amount, error) {
blk := w.Manager.SyncedTo()
return w.TxStore.Balance(confirms, blk.Height)
}
// CalculateAccountBalance sums the amounts of all unspent transaction
// outputs to the given account of a wallet and returns the balance.
func (w *Wallet) CalculateAccountBalance(account uint32, confirms int) (btcutil.Amount, error) {
var bal btcutil.Amount
// Get current block. The block height used for calculating
// the number of tx confirmations.
blk := w.Manager.SyncedTo()
unspent, err := w.TxStore.UnspentOutputs()
if err != nil {
return 0, err
}
for _, c := range unspent {
if c.IsCoinbase() {
if !c.Confirmed(blockchain.CoinbaseMaturity, blk.Height) {
continue
}
}
if c.Confirmed(confirms, blk.Height) {
creditAccount, err := w.CreditAccount(c)
if err != nil {
continue
}
if creditAccount == account {
bal += c.Amount()
}
}
}
return bal, nil
}
// CurrentAddress gets the most recently requested Bitcoin payment address
// from a wallet. If the address has already been used (there is at least
// one transaction spending to it in the blockchain or btcd mempool), the next
// chained address is returned.
func (w *Wallet) CurrentAddress(account uint32) (btcutil.Address, error) {
addr, err := w.Manager.LastExternalAddress(account)
if err != nil {
return nil, err
}
// Get next chained address if the last one has already been used.
if w.AddressUsed(addr) {
return w.NewAddress(account)
}
return addr.Address(), nil
}
// ListSinceBlock returns a slice of objects with details about transactions
// since the given block. If the block is -1 then all transactions are included.
// This is intended to be used for listsinceblock RPC replies.
func (w *Wallet) ListSinceBlock(since, curBlockHeight int32,
minconf int) ([]btcjson.ListTransactionsResult, error) {
txList := []btcjson.ListTransactionsResult{}
for _, txRecord := range w.TxStore.Records() {
// Transaction records must only be considered if they occur
// after the block height since.
if since != -1 && txRecord.BlockHeight <= since {
continue
}
// Transactions that have not met minconf confirmations are to
// be ignored.
if !txRecord.Confirmed(minconf, curBlockHeight) {
continue
}
jsonResults, err := txRecord.ToJSON(waddrmgr.DefaultAccountName, curBlockHeight,
w.Manager.ChainParams())
if err != nil {
return nil, err
}
txList = append(txList, jsonResults...)
}
return txList, nil
}
// ListTransactions returns a slice of objects with details about a recorded
// transaction. This is intended to be used for listtransactions RPC
// replies.
func (w *Wallet) ListTransactions(from, count int) ([]btcjson.ListTransactionsResult, error) {
txList := []btcjson.ListTransactionsResult{}
// Get current block. The block height used for calculating
// the number of tx confirmations.
blk := w.Manager.SyncedTo()
records := w.TxStore.Records()
lastLookupIdx := len(records) - count
// Search in reverse order: lookup most recently-added first.
for i := len(records) - 1; i >= from && i >= lastLookupIdx; i-- {
jsonResults, err := records[i].ToJSON(waddrmgr.DefaultAccountName, blk.Height,
w.Manager.ChainParams())
if err != nil {
return nil, err
}
txList = append(txList, jsonResults...)
}
return txList, nil
}
// ListAddressTransactions returns a slice of objects with details about
// recorded transactions to or from any address belonging to a set. This is
// intended to be used for listaddresstransactions RPC replies.
func (w *Wallet) ListAddressTransactions(pkHashes map[string]struct{}) (
[]btcjson.ListTransactionsResult, error) {
txList := []btcjson.ListTransactionsResult{}
// Get current block. The block height used for calculating
// the number of tx confirmations.
blk := w.Manager.SyncedTo()
for _, r := range w.TxStore.Records() {
for _, c := range r.Credits() {
// We only care about the case where len(addrs) == 1,
// and err will never be non-nil in that case.
_, addrs, _, _ := c.Addresses(activeNet.Params)
if len(addrs) != 1 {
continue
}
apkh, ok := addrs[0].(*btcutil.AddressPubKeyHash)
if !ok {
continue
}
if _, ok := pkHashes[string(apkh.ScriptAddress())]; !ok {
continue
}
jsonResult, err := c.ToJSON(waddrmgr.DefaultAccountName, blk.Height,
w.Manager.ChainParams())
if err != nil {
return nil, err
}
txList = append(txList, jsonResult)
}
}
return txList, nil
}
// ListAllTransactions returns a slice of objects with details about a recorded
// transaction. This is intended to be used for listalltransactions RPC
// replies.
func (w *Wallet) ListAllTransactions() ([]btcjson.ListTransactionsResult, error) {
txList := []btcjson.ListTransactionsResult{}
// Get current block. The block height used for calculating
// the number of tx confirmations.
blk := w.Manager.SyncedTo()
// Search in reverse order: lookup most recently-added first.
records := w.TxStore.Records()
for i := len(records) - 1; i >= 0; i-- {
jsonResults, err := records[i].ToJSON(waddrmgr.DefaultAccountName, blk.Height,
w.Manager.ChainParams())
if err != nil {
return nil, err
}
txList = append(txList, jsonResults...)
}
return txList, nil
}
// ListUnspent returns a slice of objects representing the unspent wallet
// transactions fitting the given criteria. The confirmations will be more than
// minconf, less than maxconf and if addresses is populated only the addresses
// contained within it will be considered. If we know nothing about a
// transaction an empty array will be returned.
func (w *Wallet) ListUnspent(minconf, maxconf int,
addresses map[string]bool) ([]*btcjson.ListUnspentResult, error) {
results := []*btcjson.ListUnspentResult{}
blk := w.Manager.SyncedTo()
filter := len(addresses) != 0
unspent, err := w.TxStore.SortedUnspentOutputs()
if err != nil {
return nil, err
}
for _, credit := range unspent {
confs := credit.Confirmations(blk.Height)
if int(confs) < minconf || int(confs) > maxconf {
continue
}
if credit.IsCoinbase() {
if !credit.Confirmed(blockchain.CoinbaseMaturity, blk.Height) {
continue
}
}
if w.LockedOutpoint(*credit.OutPoint()) {
continue
}
creditAccount, err := w.CreditAccount(credit)
if err != nil {
continue
}
accountName, err := w.Manager.AccountName(creditAccount)
if err != nil {
return nil, err
}
_, addrs, _, _ := credit.Addresses(activeNet.Params)
if filter {
for _, addr := range addrs {
_, ok := addresses[addr.EncodeAddress()]
if ok {
goto include
}
}
continue
}
include:
result := &btcjson.ListUnspentResult{
TxId: credit.Tx().Sha().String(),
Vout: credit.OutputIndex,
Account: accountName,
ScriptPubKey: hex.EncodeToString(credit.TxOut().PkScript),
Amount: credit.Amount().ToBTC(),
Confirmations: int64(confs),
}
// BUG: this should be a JSON array so that all
// addresses can be included, or removed (and the
// caller extracts addresses from the pkScript).
if len(addrs) > 0 {
result.Address = addrs[0].EncodeAddress()
}
results = append(results, result)
}
return results, nil
}
// DumpPrivKeys returns the WIF-encoded private keys for all addresses with
// private keys in a wallet.
func (w *Wallet) DumpPrivKeys() ([]string, error) {
addrs, err := w.Manager.AllActiveAddresses()
if err != nil {
return nil, err
}
// Iterate over each active address, appending the private key to
// privkeys.
privkeys := make([]string, 0, len(addrs))
for _, addr := range addrs {
ma, err := w.Manager.Address(addr)
if err != nil {
return nil, err
}
// Only those addresses with keys needed.
pka, ok := ma.(waddrmgr.ManagedPubKeyAddress)
if !ok {
continue
}
wif, err := pka.ExportPrivKey()
if err != nil {
// It would be nice to zero out the array here. However,
// since strings in go are immutable, and we have no
// control over the caller I don't think we can. :(
return nil, err
}
privkeys = append(privkeys, wif.String())
}
return privkeys, nil
}
// DumpWIFPrivateKey returns the WIF encoded private key for a
// single wallet address.
func (w *Wallet) DumpWIFPrivateKey(addr btcutil.Address) (string, error) {
// Get private key from wallet if it exists.
address, err := w.Manager.Address(addr)
if err != nil {
return "", err
}
pka, ok := address.(waddrmgr.ManagedPubKeyAddress)
if !ok {
return "", fmt.Errorf("address %s is not a key type", addr)
}
wif, err := pka.ExportPrivKey()
if err != nil {
return "", err
}
return wif.String(), nil
}
// ImportPrivateKey imports a private key to the wallet and writes the new
// wallet to disk.
func (w *Wallet) ImportPrivateKey(wif *btcutil.WIF, bs *waddrmgr.BlockStamp,
rescan bool) (string, error) {
// The starting block for the key is the genesis block unless otherwise
// specified.
if bs == nil {
bs = &waddrmgr.BlockStamp{
Hash: *activeNet.Params.GenesisHash,
Height: 0,
}
}
// Attempt to import private key into wallet.
addr, err := w.Manager.ImportPrivateKey(wif, bs)
if err != nil {
return "", err
}
// Rescan blockchain for transactions with txout scripts paying to the
// imported address.
if rescan {
job := &RescanJob{
Addrs: []btcutil.Address{addr.Address()},
OutPoints: nil,
BlockStamp: *bs,
}
// Submit rescan job and log when the import has completed.
// Do not block on finishing the rescan. The rescan success
// or failure is logged elsewhere, and the channel is not
// required to be read, so discard the return value.
_ = w.SubmitRescan(job)
}
addrStr := addr.Address().EncodeAddress()
log.Infof("Imported payment address %s", addrStr)
// Return the payment address string of the imported private key.
return addrStr, nil
}
// ExportWatchingWallet returns a watching-only version of the wallet serialized
// in a map.
func (w *Wallet) ExportWatchingWallet() (map[string]string, error) {
tmpDir, err := ioutil.TempDir("", "btcwallet")
if err != nil {
return nil, err
}
defer os.RemoveAll(tmpDir)
// Create a new file and write a copy of the current database into it.
woDbPath := filepath.Join(tmpDir, walletDbWatchingOnlyName)
fi, err := os.OpenFile(woDbPath, os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return nil, err
}
if err := w.db.Copy(fi); err != nil {
fi.Close()
return nil, err
}
fi.Close()
defer os.Remove(woDbPath)
// Open the new database, get the address manager namespace, and open
// it.
woDb, err := walletdb.Open("bdb", woDbPath)
if err != nil {
_ = os.Remove(woDbPath)
return nil, err
}
defer woDb.Close()
namespace, err := woDb.Namespace(waddrmgrNamespaceKey)
if err != nil {
return nil, err
}
woMgr, err := waddrmgr.Open(namespace, []byte(cfg.WalletPass),
activeNet.Params, nil)
if err != nil {
return nil, err
}
defer woMgr.Close()
// Convert the namespace to watching only if needed.
if err := woMgr.ConvertToWatchingOnly(); err != nil {
// Only return the error is it's not because it's already
// watching-only. When it is already watching-only, the code
// just falls through to the export below.
if merr, ok := err.(waddrmgr.ManagerError); ok &&
merr.ErrorCode != waddrmgr.ErrWatchingOnly {
return nil, err
}
}
// Export the watching only wallet's serialized data.
woWallet := *w
woWallet.db = woDb
woWallet.Manager = woMgr
return woWallet.exportBase64()
}
// exportBase64 exports a wallet's serialized database and tx store as
// base64-encoded values in a map.
func (w *Wallet) exportBase64() (map[string]string, error) {
var buf bytes.Buffer
m := make(map[string]string)
if err := w.db.Copy(&buf); err != nil {
return nil, err
}
m["wallet"] = base64.StdEncoding.EncodeToString(buf.Bytes())
buf.Reset()
if _, err := w.TxStore.WriteTo(&buf); err != nil {
return nil, err
}
m["tx"] = base64.StdEncoding.EncodeToString(buf.Bytes())
buf.Reset()
return m, nil
}
// LockedOutpoint returns whether an outpoint has been marked as locked and
// should not be used as an input for created transactions.
func (w *Wallet) LockedOutpoint(op wire.OutPoint) bool {
_, locked := w.lockedOutpoints[op]
return locked
}
// LockOutpoint marks an outpoint as locked, that is, it should not be used as
// an input for newly created transactions.
func (w *Wallet) LockOutpoint(op wire.OutPoint) {
w.lockedOutpoints[op] = struct{}{}
}
// UnlockOutpoint marks an outpoint as unlocked, that is, it may be used as an
// input for newly created transactions.
func (w *Wallet) UnlockOutpoint(op wire.OutPoint) {
delete(w.lockedOutpoints, op)
}
// ResetLockedOutpoints resets the set of locked outpoints so all may be used
// as inputs for new transactions.
func (w *Wallet) ResetLockedOutpoints() {
w.lockedOutpoints = map[wire.OutPoint]struct{}{}
}
// LockedOutpoints returns a slice of currently locked outpoints. This is
// intended to be used by marshaling the result as a JSON array for
// listlockunspent RPC results.
func (w *Wallet) LockedOutpoints() []btcjson.TransactionInput {
locked := make([]btcjson.TransactionInput, len(w.lockedOutpoints))
i := 0
for op := range w.lockedOutpoints {
locked[i] = btcjson.TransactionInput{
Txid: op.Hash.String(),
Vout: op.Index,
}
i++
}
return locked
}
// ResendUnminedTxs iterates through all transactions that spend from wallet
// credits that are not known to have been mined into a block, and attempts
// to send each to the chain server for relay.
func (w *Wallet) ResendUnminedTxs() {
txs := w.TxStore.UnminedDebitTxs()
for _, tx := range txs {
_, err := w.chainSvr.SendRawTransaction(tx.MsgTx(), false)
if err != nil {
// TODO(jrick): Check error for if this tx is a double spend,
// remove it if so.
log.Debugf("Could not resend transaction %v: %v",
tx.Sha(), err)
continue
}
log.Debugf("Resent unmined transaction %v", tx.Sha())
}
}
// SortedActivePaymentAddresses returns a slice of all active payment
// addresses in a wallet.
func (w *Wallet) SortedActivePaymentAddresses() ([]string, error) {
addrs, err := w.Manager.AllActiveAddresses()
if err != nil {
return nil, err
}
addrStrs := make([]string, len(addrs))
for i, addr := range addrs {
addrStrs[i] = addr.EncodeAddress()
}
sort.Sort(sort.StringSlice(addrStrs))
return addrStrs, nil
}
// NewAddress returns the next external chained address for a wallet.
func (w *Wallet) NewAddress(account uint32) (btcutil.Address, error) {
// Get next address from wallet.
addrs, err := w.Manager.NextExternalAddresses(account, 1)
if err != nil {
return nil, err
}
// Request updates from btcd for new transactions sent to this address.
utilAddrs := make([]btcutil.Address, len(addrs))
for i, addr := range addrs {
utilAddrs[i] = addr.Address()
}
if err := w.chainSvr.NotifyReceived(utilAddrs); err != nil {
return nil, err
}
return utilAddrs[0], nil
}
// NewChangeAddress returns a new change address for a wallet.
func (w *Wallet) NewChangeAddress(account uint32) (btcutil.Address, error) {
// Get next chained change address from wallet for account.
addrs, err := w.Manager.NextInternalAddresses(account, 1)
if err != nil {
return nil, err
}
// Request updates from btcd for new transactions sent to this address.
utilAddrs := make([]btcutil.Address, len(addrs))
for i, addr := range addrs {
utilAddrs[i] = addr.Address()
}
if err := w.chainSvr.NotifyReceived(utilAddrs); err != nil {
return nil, err
}
return utilAddrs[0], nil
}
// TotalReceivedForAccount iterates through a wallet's transaction history,
// returning the total amount of bitcoins received for a single wallet
// account.
func (w *Wallet) TotalReceivedForAccount(account uint32, confirms int) (btcutil.Amount, uint64, error) {
blk := w.Manager.SyncedTo()
// Number of confirmations of the last transaction.
var confirmations uint64
var amount btcutil.Amount
for _, r := range w.TxStore.Records() {
for _, c := range r.Credits() {
if !c.Confirmed(confirms, blk.Height) {
// Not enough confirmations, skip the current block.
continue
}
creditAccount, err := w.CreditAccount(c)
if err != nil {
continue
}
if creditAccount == account {
amount += c.Amount()
confirmations = uint64(c.Confirmations(blk.Height))
break
}
}
}
return amount, confirmations, nil
}
// TotalReceivedForAddr iterates through a wallet's transaction history,
// returning the total amount of bitcoins received for a single wallet
// address.
func (w *Wallet) TotalReceivedForAddr(addr btcutil.Address, confirms int) (btcutil.Amount, error) {
blk := w.Manager.SyncedTo()
addrStr := addr.EncodeAddress()
var amount btcutil.Amount
for _, r := range w.TxStore.Records() {
for _, c := range r.Credits() {
if !c.Confirmed(confirms, blk.Height) {
continue
}
_, addrs, _, err := c.Addresses(activeNet.Params)
// An error creating addresses from the output script only
// indicates a non-standard script, so ignore this credit.
if err != nil {
continue
}
for _, a := range addrs {
if addrStr == a.EncodeAddress() {
amount += c.Amount()
break
}
}
}
}
return amount, nil
}
// TxRecord iterates through all transaction records saved in the store,
// returning the first with an equivalent transaction hash.
func (w *Wallet) TxRecord(txSha *wire.ShaHash) (r *txstore.TxRecord, ok bool) {
for _, r = range w.TxStore.Records() {
if *r.Tx().Sha() == *txSha {
return r, true
}
}
return nil, false
}
// openWallet opens a wallet from disk.
func openWallet() (*Wallet, error) {
netdir := networkDir(cfg.DataDir, activeNet.Params)
dbPath := filepath.Join(netdir, walletDbName)
// Ensure that the network directory exists.
if err := checkCreateDir(netdir); err != nil {
return nil, err
}
// Open the database using the boltdb backend.
db, err := walletdb.Open("bdb", dbPath)
if err != nil {
return nil, err
}
// Get the namespace for the address manager.
namespace, err := db.Namespace(waddrmgrNamespaceKey)
if err != nil {
return nil, err
}
// Open address manager and transaction store.
var txs *txstore.Store
config := &waddrmgr.Options{
ObtainSeed: promptSeed,
ObtainPrivatePass: promptPrivPassPhrase,
}
mgr, err := waddrmgr.Open(namespace, []byte(cfg.WalletPass),
activeNet.Params, config)
if err == nil {
txs, err = txstore.OpenDir(netdir)
}
if err != nil {
// Special case: if the address manager was successfully read
// (mgr != nil) but the transaction store was not, create a
// new txstore and write it out to disk. Write an unsynced
// manager back to disk so on future opens, the empty txstore
// is not considered fully synced.
if mgr == nil {
return nil, err
}
txs = txstore.New(netdir)
txs.MarkDirty()
err = txs.WriteIfDirty()
if err != nil {
return nil, err
}
mgr.SetSyncedTo(nil)
}
log.Infof("Opened wallet files") // TODO: log balance? last sync height?
wallet := newWallet(mgr, txs)
wallet.db = db
return wallet, nil
}