3815 lines
116 KiB
Go
3815 lines
116 KiB
Go
// Copyright (c) 2013-2017 The btcsuite developers
|
|
// Copyright (c) 2015-2016 The Decred developers
|
|
// Use of this source code is governed by an ISC
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package wallet
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/lbryio/lbcd/blockchain"
|
|
"github.com/lbryio/lbcd/btcec"
|
|
"github.com/lbryio/lbcd/btcjson"
|
|
"github.com/lbryio/lbcd/chaincfg"
|
|
"github.com/lbryio/lbcd/chaincfg/chainhash"
|
|
"github.com/lbryio/lbcd/txscript"
|
|
"github.com/lbryio/lbcd/wire"
|
|
btcutil "github.com/lbryio/lbcutil"
|
|
"github.com/lbryio/lbcutil/hdkeychain"
|
|
"github.com/lbryio/lbcwallet/chain"
|
|
"github.com/lbryio/lbcwallet/internal/prompt"
|
|
"github.com/lbryio/lbcwallet/waddrmgr"
|
|
"github.com/lbryio/lbcwallet/wallet/txauthor"
|
|
"github.com/lbryio/lbcwallet/wallet/txrules"
|
|
"github.com/lbryio/lbcwallet/walletdb"
|
|
"github.com/lbryio/lbcwallet/walletdb/migration"
|
|
"github.com/lbryio/lbcwallet/wtxmgr"
|
|
)
|
|
|
|
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"
|
|
|
|
// recoveryBatchSize is the default number of blocks that will be
|
|
// scanned successively by the recovery manager, in the event that the
|
|
// wallet is started in recovery mode.
|
|
recoveryBatchSize = 2000
|
|
)
|
|
|
|
var (
|
|
// ErrNotSynced describes an error where an operation cannot complete
|
|
// due wallet being out of sync (and perhaps currently syncing with)
|
|
// the remote chain server.
|
|
ErrNotSynced = errors.New("wallet is not synchronized with the chain server")
|
|
|
|
// ErrWalletShuttingDown is an error returned when we attempt to make a
|
|
// request to the wallet but it is in the process of or has already shut
|
|
// down.
|
|
ErrWalletShuttingDown = errors.New("wallet shutting down")
|
|
|
|
// ErrUnknownTransaction is returned when an attempt is made to label
|
|
// a transaction that is not known to the wallet.
|
|
ErrUnknownTransaction = errors.New("cannot label transaction not " +
|
|
"known to wallet")
|
|
|
|
// ErrTxLabelExists is returned when a transaction already has a label
|
|
// and an attempt has been made to label it without setting overwrite
|
|
// to true.
|
|
ErrTxLabelExists = errors.New("transaction already labelled")
|
|
|
|
// ErrTxUnsigned is returned when a transaction is created in the
|
|
// watch-only mode where we can select coins but not sign any inputs.
|
|
ErrTxUnsigned = errors.New("watch-only wallet, transaction not signed")
|
|
|
|
// Namespace bucket keys.
|
|
waddrmgrNamespaceKey = []byte("waddrmgr")
|
|
wtxmgrNamespaceKey = []byte("wtxmgr")
|
|
)
|
|
|
|
type CoinSelectionStrategy int
|
|
|
|
const (
|
|
// CoinSelectionLargest always picks the largest available utxo to add
|
|
// to the transaction next.
|
|
CoinSelectionLargest CoinSelectionStrategy = iota
|
|
|
|
// CoinSelectionRandom randomly selects the next utxo to add to the
|
|
// transaction. This strategy prevents the creation of ever smaller
|
|
// utxos over time.
|
|
CoinSelectionRandom
|
|
)
|
|
|
|
// 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 {
|
|
publicPassphrase []byte
|
|
|
|
// Data stores
|
|
db walletdb.DB
|
|
Manager *waddrmgr.Manager
|
|
TxStore *wtxmgr.Store
|
|
|
|
chainClient chain.Interface
|
|
chainClientLock sync.Mutex
|
|
chainClientSynced bool
|
|
chainClientSyncMtx sync.Mutex
|
|
|
|
lockedOutpoints map[wire.OutPoint]struct{}
|
|
lockedOutpointsMtx sync.Mutex
|
|
|
|
recoveryWindow uint32
|
|
|
|
// 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
|
|
changePassphrases chan changePassphrasesRequest
|
|
|
|
NtfnServer *NotificationServer
|
|
|
|
chainParams *chaincfg.Params
|
|
wg sync.WaitGroup
|
|
|
|
started bool
|
|
quit chan struct{}
|
|
quitMu sync.Mutex
|
|
}
|
|
|
|
// Start starts the goroutines necessary to manage a wallet.
|
|
func (w *Wallet) Start() {
|
|
w.quitMu.Lock()
|
|
select {
|
|
case <-w.quit:
|
|
// Restart the wallet goroutines after shutdown finishes.
|
|
w.WaitForShutdown()
|
|
w.quit = make(chan struct{})
|
|
default:
|
|
// Ignore when the wallet is still running.
|
|
if w.started {
|
|
w.quitMu.Unlock()
|
|
return
|
|
}
|
|
w.started = true
|
|
}
|
|
w.quitMu.Unlock()
|
|
|
|
w.wg.Add(2)
|
|
go w.txCreator()
|
|
go w.walletLocker()
|
|
}
|
|
|
|
// SynchronizeRPC associates the wallet with the consensus RPC client,
|
|
// synchronizes the wallet with the latest changes to the blockchain, and
|
|
// continuously updates the wallet through RPC notifications.
|
|
//
|
|
// This method is unstable and will be removed when all syncing logic is moved
|
|
// outside of the wallet package.
|
|
func (w *Wallet) SynchronizeRPC(chainClient chain.Interface) {
|
|
w.quitMu.Lock()
|
|
select {
|
|
case <-w.quit:
|
|
w.quitMu.Unlock()
|
|
return
|
|
default:
|
|
}
|
|
w.quitMu.Unlock()
|
|
|
|
// TODO: Ignoring the new client when one is already set breaks callers
|
|
// who are replacing the client, perhaps after a disconnect.
|
|
w.chainClientLock.Lock()
|
|
if w.chainClient != nil {
|
|
w.chainClientLock.Unlock()
|
|
return
|
|
}
|
|
w.chainClient = chainClient
|
|
|
|
w.chainClientLock.Unlock()
|
|
|
|
// TODO: It would be preferable to either run these goroutines
|
|
// separately from the wallet (use wallet mutator functions to
|
|
// make changes from the RPC client) and not have to stop and
|
|
// restart them each time the client disconnects and reconnets.
|
|
w.wg.Add(4)
|
|
go w.handleChainNotifications()
|
|
go w.rescanBatchHandler()
|
|
go w.rescanProgressHandler()
|
|
go w.rescanRPCHandler()
|
|
}
|
|
|
|
// requireChainClient marks that a wallet method can only be completed when the
|
|
// consensus RPC server is set. This function and all functions that call it
|
|
// are unstable and will need to be moved when the syncing code is moved out of
|
|
// the wallet.
|
|
func (w *Wallet) requireChainClient() (chain.Interface, 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.Interface {
|
|
w.chainClientLock.Lock()
|
|
chainClient := w.chainClient
|
|
w.chainClientLock.Unlock()
|
|
return chainClient
|
|
}
|
|
|
|
// quitChan atomically reads the quit channel.
|
|
func (w *Wallet) quitChan() <-chan struct{} {
|
|
w.quitMu.Lock()
|
|
c := w.quit
|
|
w.quitMu.Unlock()
|
|
return c
|
|
}
|
|
|
|
// Stop signals all wallet goroutines to shutdown.
|
|
func (w *Wallet) Stop() {
|
|
w.quitMu.Lock()
|
|
quit := w.quit
|
|
w.quitMu.Unlock()
|
|
|
|
select {
|
|
case <-quit:
|
|
default:
|
|
close(quit)
|
|
w.chainClientLock.Lock()
|
|
if w.chainClient != nil {
|
|
w.chainClient.Stop()
|
|
w.chainClient = nil
|
|
}
|
|
w.chainClientLock.Unlock()
|
|
}
|
|
}
|
|
|
|
// ShuttingDown returns whether the wallet is currently in the process of
|
|
// shutting down or not.
|
|
func (w *Wallet) ShuttingDown() bool {
|
|
select {
|
|
case <-w.quitChan():
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// WaitForShutdown blocks until all wallet goroutines have finished executing.
|
|
func (w *Wallet) WaitForShutdown() {
|
|
w.chainClientLock.Lock()
|
|
if w.chainClient != nil {
|
|
w.chainClient.WaitForShutdown()
|
|
}
|
|
w.chainClientLock.Unlock()
|
|
w.wg.Wait()
|
|
}
|
|
|
|
// SynchronizingToNetwork returns whether the wallet is currently synchronizing
|
|
// with the Bitcoin network.
|
|
func (w *Wallet) SynchronizingToNetwork() bool {
|
|
// At the moment, RPC is the only synchronization method. In the
|
|
// future, when SPV is added, a separate check will also be needed, or
|
|
// SPV could always be enabled if RPC was not explicitly specified when
|
|
// creating the wallet.
|
|
w.chainClientSyncMtx.Lock()
|
|
syncing := w.chainClient != nil
|
|
w.chainClientSyncMtx.Unlock()
|
|
return syncing
|
|
}
|
|
|
|
// ChainSynced returns whether the wallet has been attached to a chain server
|
|
// and synced up to the best block on the main chain.
|
|
func (w *Wallet) ChainSynced() bool {
|
|
w.chainClientSyncMtx.Lock()
|
|
synced := w.chainClientSynced
|
|
w.chainClientSyncMtx.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 rpcclient, 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.chainClientSyncMtx.Lock()
|
|
w.chainClientSynced = synced
|
|
w.chainClientSyncMtx.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(dbtx walletdb.ReadWriteTx) ([]btcutil.Address, []wtxmgr.Credit, error) {
|
|
addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey)
|
|
txmgrNs := dbtx.ReadWriteBucket(wtxmgrNamespaceKey)
|
|
|
|
var addrs []btcutil.Address
|
|
err := w.Manager.ForEachRelevantActiveAddress(
|
|
addrmgrNs, func(addr btcutil.Address) error {
|
|
addrs = append(addrs, addr)
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Before requesting the list of spendable UTXOs, we'll delete any
|
|
// expired output locks.
|
|
err = w.TxStore.DeleteExpiredLockedOutputs(
|
|
dbtx.ReadWriteBucket(wtxmgrNamespaceKey),
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
unspent, err := w.TxStore.UnspentOutputs(txmgrNs)
|
|
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. The birthday block can be passed in, if set, to ensure we can
|
|
// properly detect if it gets rolled back.
|
|
func (w *Wallet) syncWithChain(birthdayStamp *waddrmgr.BlockStamp) error {
|
|
chainClient, err := w.requireChainClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// We'll wait until the backend is synced to ensure we get the latest
|
|
// MaxReorgDepth blocks to store. We don't do this for development
|
|
// environments as we can't guarantee a lively chain, except for
|
|
// Neutrino, where the cfheader server tells us what it believes the
|
|
// chain tip is.
|
|
if !w.isDevEnv() {
|
|
log.Debug("Waiting for chain backend to sync to tip")
|
|
if err := w.waitUntilBackendSynced(chainClient); err != nil {
|
|
return err
|
|
}
|
|
log.Debug("Chain backend synced to tip!")
|
|
}
|
|
|
|
// If we've yet to find our birthday block, we'll do so now.
|
|
if birthdayStamp == nil {
|
|
var err error
|
|
birthdayStamp, err = locateBirthdayBlock(
|
|
chainClient, w.Manager.Birthday(),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to locate birthday block: %v",
|
|
err)
|
|
}
|
|
|
|
// We'll also determine our initial sync starting height. This
|
|
// is needed as the wallet can now begin storing blocks from an
|
|
// arbitrary height, rather than all the blocks from genesis, so
|
|
// we persist this height to ensure we don't store any blocks
|
|
// before it.
|
|
startHeight := birthdayStamp.Height
|
|
|
|
// With the starting height obtained, get the remaining block
|
|
// details required by the wallet.
|
|
startHash, err := chainClient.GetBlockHash(int64(startHeight))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
startHeader, err := chainClient.GetBlockHeader(startHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
|
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
|
err := w.Manager.SetSyncedTo(ns, &waddrmgr.BlockStamp{
|
|
Hash: *startHash,
|
|
Height: startHeight,
|
|
Timestamp: startHeader.Timestamp,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return w.Manager.SetBirthdayBlock(ns, *birthdayStamp, true)
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("unable to persist initial sync "+
|
|
"data: %v", err)
|
|
}
|
|
}
|
|
|
|
// If the wallet requested an on-chain recovery of its funds, we'll do
|
|
// so now.
|
|
if w.recoveryWindow > 0 {
|
|
if err := w.recovery(chainClient, birthdayStamp); err != nil {
|
|
return fmt.Errorf("unable to perform wallet recovery: "+
|
|
"%v", err)
|
|
}
|
|
}
|
|
|
|
// Compare previously-seen blocks against the current chain. If any of
|
|
// these blocks no longer exist, rollback all of the missing blocks
|
|
// before catching up with the rescan.
|
|
rollback := false
|
|
rollbackStamp := w.Manager.SyncedTo()
|
|
|
|
// Handle corner cases where the chain has reorged to a lower height
|
|
// than the wallet had synced to. This could happen if the chain has
|
|
// reorged to a shorter chain with difficulty greater than the current.
|
|
// Most of the time, it's only temporary since the chain will grow
|
|
// past the previous height anyway. In regtest, however, it burdens
|
|
// the testing setup without this check.
|
|
bestBlockHash, bestBlockHeight, err := chainClient.GetBestBlock()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rollbackStamp.Height > bestBlockHeight {
|
|
rollbackStamp.Hash = *bestBlockHash
|
|
rollbackStamp.Height = bestBlockHeight
|
|
rollback = true
|
|
}
|
|
|
|
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
|
addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
|
txmgrNs := tx.ReadWriteBucket(wtxmgrNamespaceKey)
|
|
|
|
for height := rollbackStamp.Height; true; height-- {
|
|
hash, err := w.Manager.BlockHash(addrmgrNs, height)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
chainHash, err := chainClient.GetBlockHash(int64(height))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
header, err := chainClient.GetBlockHeader(chainHash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rollbackStamp.Hash = *chainHash
|
|
rollbackStamp.Height = height
|
|
rollbackStamp.Timestamp = header.Timestamp
|
|
|
|
if bytes.Equal(hash[:], chainHash[:]) {
|
|
break
|
|
}
|
|
rollback = true
|
|
}
|
|
|
|
// If a rollback did not happen, we can proceed safely.
|
|
if !rollback {
|
|
return nil
|
|
}
|
|
|
|
// Otherwise, we'll mark this as our new synced height.
|
|
err := w.Manager.SetSyncedTo(addrmgrNs, &rollbackStamp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If the rollback happened to go beyond our birthday stamp,
|
|
// we'll need to find a new one by syncing with the chain again
|
|
// until finding one.
|
|
if rollbackStamp.Height <= birthdayStamp.Height &&
|
|
rollbackStamp.Hash != birthdayStamp.Hash {
|
|
|
|
err := w.Manager.SetBirthdayBlock(
|
|
addrmgrNs, rollbackStamp, true,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Finally, we'll roll back our transaction store to reflect the
|
|
// stale state. `Rollback` unconfirms transactions at and beyond
|
|
// the passed height, so add one to the new synced-to height to
|
|
// prevent unconfirming transactions in the synced-to block.
|
|
return w.TxStore.Rollback(txmgrNs, rollbackStamp.Height+1)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Request notifications for connected and disconnected blocks.
|
|
//
|
|
// TODO(jrick): Either request this notification only once, or when
|
|
// rpcclient 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 rpcclient
|
|
// notification re-registrations, in which case the code here should be
|
|
// left as is.
|
|
if err := chainClient.NotifyBlocks(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Finally, we'll trigger a wallet rescan and request notifications for
|
|
// transactions sending to all wallet addresses and spending all wallet
|
|
// UTXOs.
|
|
var (
|
|
addrs []btcutil.Address
|
|
unspent []wtxmgr.Credit
|
|
)
|
|
err = walletdb.Update(w.db, func(dbtx walletdb.ReadWriteTx) error {
|
|
addrs, unspent, err = w.activeData(dbtx)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return w.rescanWithTarget(addrs, unspent, nil)
|
|
}
|
|
|
|
// isDevEnv determines whether the wallet is currently under a local developer
|
|
// environment, e.g. simnet or regtest.
|
|
func (w *Wallet) isDevEnv() bool {
|
|
switch uint32(w.ChainParams().Net) {
|
|
case uint32(chaincfg.RegressionNetParams.Net):
|
|
case uint32(chaincfg.SimNetParams.Net):
|
|
default:
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// waitUntilBackendSynced blocks until the chain backend considers itself
|
|
// "current".
|
|
func (w *Wallet) waitUntilBackendSynced(chainClient chain.Interface) error {
|
|
// We'll poll every second to determine if our chain considers itself
|
|
// "current".
|
|
t := time.NewTicker(time.Second)
|
|
defer t.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-t.C:
|
|
if chainClient.IsCurrent() {
|
|
return nil
|
|
}
|
|
case <-w.quitChan():
|
|
return ErrWalletShuttingDown
|
|
}
|
|
}
|
|
}
|
|
|
|
// locateBirthdayBlock returns a block that meets the given birthday timestamp
|
|
// by a margin of +/-2 hours. This is safe to do as the timestamp is already 2
|
|
// days in the past of the actual timestamp.
|
|
func locateBirthdayBlock(chainClient chainConn,
|
|
birthday time.Time) (*waddrmgr.BlockStamp, error) {
|
|
|
|
// Retrieve the lookup range for our block.
|
|
startHeight := int32(0)
|
|
_, bestHeight, err := chainClient.GetBestBlock()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Debugf("Locating suitable block for birthday %v between blocks "+
|
|
"%v-%v", birthday, startHeight, bestHeight)
|
|
|
|
var (
|
|
birthdayBlock *waddrmgr.BlockStamp
|
|
left, right = startHeight, bestHeight
|
|
)
|
|
|
|
// Binary search for a block that meets the birthday timestamp by a
|
|
// margin of +/-2 hours.
|
|
for {
|
|
// Retrieve the timestamp for the block halfway through our
|
|
// range.
|
|
mid := left + (right-left)/2
|
|
hash, err := chainClient.GetBlockHash(int64(mid))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
header, err := chainClient.GetBlockHeader(hash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Debugf("Checking candidate block: height=%v, hash=%v, "+
|
|
"timestamp=%v", mid, hash, header.Timestamp)
|
|
|
|
// If the search happened to reach either of our range extremes,
|
|
// then we'll just use that as there's nothing left to search.
|
|
if mid == startHeight || mid == bestHeight || mid == left {
|
|
birthdayBlock = &waddrmgr.BlockStamp{
|
|
Hash: *hash,
|
|
Height: mid,
|
|
Timestamp: header.Timestamp,
|
|
}
|
|
break
|
|
}
|
|
|
|
// The block's timestamp is more than 2 hours after the
|
|
// birthday, so look for a lower block.
|
|
if header.Timestamp.Sub(birthday) > birthdayBlockDelta {
|
|
right = mid
|
|
continue
|
|
}
|
|
|
|
// The birthday is more than 2 hours before the block's
|
|
// timestamp, so look for a higher block.
|
|
if header.Timestamp.Sub(birthday) < -birthdayBlockDelta {
|
|
left = mid
|
|
continue
|
|
}
|
|
|
|
birthdayBlock = &waddrmgr.BlockStamp{
|
|
Hash: *hash,
|
|
Height: mid,
|
|
Timestamp: header.Timestamp,
|
|
}
|
|
break
|
|
}
|
|
|
|
log.Debugf("Found birthday block: height=%d, hash=%v, timestamp=%v",
|
|
birthdayBlock.Height, birthdayBlock.Hash,
|
|
birthdayBlock.Timestamp)
|
|
|
|
return birthdayBlock, nil
|
|
}
|
|
|
|
// recovery attempts to recover any unspent outputs that pay to any of our
|
|
// addresses starting from our birthday, or the wallet's tip (if higher), which
|
|
// would indicate resuming a recovery after a restart.
|
|
func (w *Wallet) recovery(chainClient chain.Interface,
|
|
birthdayBlock *waddrmgr.BlockStamp) error {
|
|
|
|
log.Infof("RECOVERY MODE ENABLED -- rescanning for used addresses "+
|
|
"with recovery_window=%d", w.recoveryWindow)
|
|
|
|
// We'll initialize the recovery manager with a default batch size of
|
|
// 2000.
|
|
recoveryMgr := NewRecoveryManager(
|
|
w.recoveryWindow, recoveryBatchSize, w.chainParams,
|
|
)
|
|
|
|
scopedMgrs := make(map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager)
|
|
for _, scopedMgr := range w.Manager.ActiveScopedKeyManagers() {
|
|
scopedMgrs[scopedMgr.Scope()] = scopedMgr
|
|
}
|
|
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
var credits []wtxmgr.Credit
|
|
txMgrNS := tx.ReadBucket(wtxmgrNamespaceKey)
|
|
credits, err := w.TxStore.UnspentOutputs(txMgrNS)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
addrMgrNS := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
return recoveryMgr.Resurrect(addrMgrNS, scopedMgrs, credits)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Fetch the best height from the backend to determine when we should
|
|
// stop.
|
|
_, bestHeight, err := chainClient.GetBestBlock()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Now we can begin scanning the chain from the wallet's current tip to
|
|
// ensure we properly handle restarts. Since the recovery process itself
|
|
// acts as rescan, we'll also update our wallet's synced state along the
|
|
// way to reflect the blocks we process and prevent rescanning them
|
|
// later on.
|
|
//
|
|
// NOTE: We purposefully don't update our best height since we assume
|
|
// that a wallet rescan will be performed from the wallet's tip, which
|
|
// will be of bestHeight after completing the recovery process.
|
|
|
|
pass, err := prompt.ProvidePrivPassphrase()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = w.Unlock(pass, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer w.Lock()
|
|
|
|
var blocks []*waddrmgr.BlockStamp
|
|
startHeight := w.Manager.SyncedTo().Height + 1
|
|
for height := startHeight; height <= bestHeight; height++ {
|
|
hash, err := chainClient.GetBlockHash(int64(height))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
header, err := chainClient.GetBlockHeader(hash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
blocks = append(blocks, &waddrmgr.BlockStamp{
|
|
Hash: *hash,
|
|
Height: height,
|
|
Timestamp: header.Timestamp,
|
|
})
|
|
|
|
// It's possible for us to run into blocks before our birthday
|
|
// if our birthday is after our reorg safe height, so we'll make
|
|
// sure to not add those to the batch.
|
|
if height >= birthdayBlock.Height {
|
|
recoveryMgr.AddToBlockBatch(
|
|
hash, height, header.Timestamp,
|
|
)
|
|
}
|
|
|
|
// We'll perform our recovery in batches of 2000 blocks. It's
|
|
// possible for us to reach our best height without exceeding
|
|
// the recovery batch size, so we can proceed to commit our
|
|
// state to disk.
|
|
recoveryBatch := recoveryMgr.BlockBatch()
|
|
if len(recoveryBatch) != recoveryBatchSize && height != bestHeight {
|
|
continue
|
|
}
|
|
|
|
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
|
ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
|
for _, block := range blocks {
|
|
err = w.Manager.SetSyncedTo(ns, block)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for scope, scopedMgr := range scopedMgrs {
|
|
scopeState := recoveryMgr.State().StateForScope(scope)
|
|
err = expandScopeHorizons(ns, scopedMgr, scopeState)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return w.recoverScopedAddresses(chainClient, tx, ns,
|
|
recoveryBatch, recoveryMgr.State(), scopedMgrs,
|
|
)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(recoveryBatch) > 0 {
|
|
log.Infof("Recovered addresses from blocks "+
|
|
"%d-%d", recoveryBatch[0].Height,
|
|
recoveryBatch[len(recoveryBatch)-1].Height)
|
|
}
|
|
|
|
// Clear the batch of all processed blocks to reuse the
|
|
// same memory for future batches.
|
|
blocks = blocks[:0]
|
|
recoveryMgr.ResetBlockBatch()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// recoverScopedAddresses scans a range of blocks in attempts to recover any
|
|
// previously used addresses for a particular account derivation path. At a high
|
|
// level, the algorithm works as follows:
|
|
//
|
|
// 1. Ensure internal and external branch horizons are fully expanded.
|
|
// 2. Filter the entire range of blocks, stopping if a non-zero number of
|
|
// address are contained in a particular block.
|
|
// 3. Record all internal and external addresses found in the block.
|
|
// 4. Record any outpoints found in the block that should be watched for spends
|
|
// 5. Trim the range of blocks up to and including the one reporting the addrs.
|
|
// 6. Repeat from (1) if there are still more blocks in the range.
|
|
//
|
|
// TODO(conner): parallelize/pipeline/cache intermediate network requests
|
|
func (w *Wallet) recoverScopedAddresses(
|
|
chainClient chain.Interface,
|
|
tx walletdb.ReadWriteTx,
|
|
ns walletdb.ReadWriteBucket,
|
|
batch []wtxmgr.BlockMeta,
|
|
recoveryState *RecoveryState,
|
|
scopedMgrs map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager) error {
|
|
|
|
// If there are no blocks in the batch, we are done.
|
|
if len(batch) == 0 {
|
|
return nil
|
|
}
|
|
|
|
expandHorizons:
|
|
log.Infof("Scanning %d blocks for recoverable addresses", len(batch))
|
|
|
|
// With the internal and external horizons properly expanded, we now
|
|
// construct the filter blocks request. The request includes the range
|
|
// of blocks we intend to scan, in addition to the scope-index -> addr
|
|
// map for all internal and external branches.
|
|
filterReq := newFilterBlocksRequest(batch, scopedMgrs, recoveryState)
|
|
|
|
// Initiate the filter blocks request using our chain backend. If an
|
|
// error occurs, we are unable to proceed with the recovery.
|
|
filterResp, err := chainClient.FilterBlocks(filterReq)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If the filter response is empty, this signals that the rest of the
|
|
// batch was completed, and no other addresses were discovered. As a
|
|
// result, no further modifications to our recovery state are required
|
|
// and we can proceed to the next batch.
|
|
if filterResp == nil {
|
|
return nil
|
|
}
|
|
|
|
// Otherwise, retrieve the block info for the block that detected a
|
|
// non-zero number of address matches.
|
|
block := batch[filterResp.BatchIndex]
|
|
|
|
// Log any non-trivial findings of addresses or outpoints.
|
|
logFilterBlocksResp(block, filterResp)
|
|
|
|
// Report any external or internal addresses found as a result of the
|
|
// appropriate branch recovery state. Adding indexes above the
|
|
// last-found index of either will result in the horizons being expanded
|
|
// upon the next iteration. Any found addresses are also marked used
|
|
// using the scoped key manager.
|
|
err = extendFoundAddresses(ns, filterResp, scopedMgrs, recoveryState)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update the global set of watched outpoints with any that were found
|
|
// in the block.
|
|
for outPoint, addr := range filterResp.FoundOutPoints {
|
|
outPoint := outPoint
|
|
recoveryState.AddWatchedOutPoint(&outPoint, addr)
|
|
}
|
|
|
|
// Finally, record all of the relevant transactions that were returned
|
|
// in the filter blocks response. This ensures that these transactions
|
|
// and their outputs are tracked when the final rescan is performed.
|
|
for _, txn := range filterResp.RelevantTxns {
|
|
txRecord, err := wtxmgr.NewTxRecordFromMsgTx(
|
|
txn, filterResp.BlockMeta.Time,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = w.addRelevantTx(tx, txRecord, &filterResp.BlockMeta)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Update the batch to indicate that we've processed all block through
|
|
// the one that returned found addresses.
|
|
batch = batch[filterResp.BatchIndex+1:]
|
|
|
|
// If this was not the last block in the batch, we will repeat the
|
|
// filtering process again after expanding our horizons.
|
|
if len(batch) > 0 {
|
|
goto expandHorizons
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// expandScopeHorizons ensures that the ScopeRecoveryState has an adequately
|
|
// sized look ahead for both its internal and external branches. The keys
|
|
// derived here are added to the scope's recovery state, but do not affect the
|
|
// persistent state of the wallet. If any invalid child keys are detected, the
|
|
// horizon will be properly extended such that our lookahead always includes the
|
|
// proper number of valid child keys.
|
|
func expandScopeHorizons(
|
|
ns walletdb.ReadWriteBucket,
|
|
scopedMgr *waddrmgr.ScopedKeyManager,
|
|
scopeState ScopeRecoveryState) error {
|
|
|
|
for accountIndex, accountState := range scopeState {
|
|
acctKey, err := scopedMgr.DeriveAccountKey(ns, uint32(accountIndex))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for branchIndex, branchState := range accountState {
|
|
exHorizon, exWindow := branchState.ExtendHorizon()
|
|
count, addrIndex := uint32(0), exHorizon
|
|
|
|
branchKey, err := acctKey.Derive(uint32(branchIndex))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for count < exWindow {
|
|
kp := keyPath(uint32(accountIndex), uint32(branchIndex), addrIndex)
|
|
indexKey, err := branchKey.Derive(addrIndex)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
addrType := waddrmgr.ScopeAddrMap[scopedMgr.Scope()].ExternalAddrType
|
|
addr, err := scopedMgr.DeriveFromExtKeys(kp, indexKey, addrType)
|
|
switch {
|
|
case err == hdkeychain.ErrInvalidChild:
|
|
branchState.MarkInvalidChild(addrIndex)
|
|
addrIndex++
|
|
continue
|
|
|
|
case err != nil:
|
|
return err
|
|
}
|
|
|
|
branchState.AddAddr(addrIndex, addr.Address())
|
|
|
|
addrIndex++
|
|
count++
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// keyPath returns the relative derivation path /account/branch/index.
|
|
func keyPath(account, branch, index uint32) waddrmgr.DerivationPath {
|
|
return waddrmgr.DerivationPath{
|
|
InternalAccount: account,
|
|
Account: account,
|
|
Branch: branch,
|
|
Index: index,
|
|
}
|
|
}
|
|
|
|
// newFilterBlocksRequest constructs FilterBlocksRequests using our current
|
|
// block range, scoped managers, and recovery state.
|
|
func newFilterBlocksRequest(batch []wtxmgr.BlockMeta,
|
|
scopedMgrs map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager,
|
|
recoveryState *RecoveryState) *chain.FilterBlocksRequest {
|
|
|
|
filterReq := &chain.FilterBlocksRequest{
|
|
Blocks: batch,
|
|
Addresses: make(map[waddrmgr.ScopedIndex]btcutil.Address),
|
|
WatchedOutPoints: recoveryState.WatchedOutPoints(),
|
|
}
|
|
|
|
// Populate the addresses by merging the addresses sets belong to all
|
|
// currently tracked scopes.
|
|
for scope := range scopedMgrs {
|
|
scopeState := recoveryState.StateForScope(scope)
|
|
for accountIndex, accountState := range scopeState {
|
|
for branchIndex, branchState := range accountState {
|
|
for addrIndex, addr := range branchState.Addrs() {
|
|
scopedIndex := waddrmgr.ScopedIndex{
|
|
Scope: scope,
|
|
Account: uint32(accountIndex),
|
|
Branch: uint32(branchIndex),
|
|
Index: addrIndex,
|
|
}
|
|
filterReq.Addresses[scopedIndex] = addr
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return filterReq
|
|
}
|
|
|
|
// extendFoundAddresses accepts a filter blocks response that contains addresses
|
|
// found on chain, and advances the state of all relevant derivation paths to
|
|
// match the highest found child index for each branch.
|
|
func extendFoundAddresses(ns walletdb.ReadWriteBucket,
|
|
filterResp *chain.FilterBlocksResponse,
|
|
scopedMgrs map[waddrmgr.KeyScope]*waddrmgr.ScopedKeyManager,
|
|
recoveryState *RecoveryState) error {
|
|
|
|
// Mark all recovered addresses as used. This will be done only for
|
|
// scopes that reported a non-zero number of addresses in this block.
|
|
for index := range filterResp.FoundAddresses {
|
|
scopedMgr := scopedMgrs[index.Scope]
|
|
// First, report all child indexes found for this scope. This
|
|
// ensures that the last-found index will be updated to include
|
|
// the maximum child index seen thus far.
|
|
scopeState := recoveryState.StateForScope(index.Scope)
|
|
branchState := scopeState[index.Account][index.Branch]
|
|
branchState.ReportFound(index.Index)
|
|
// Now, with all found addresses reported, derive and extend all
|
|
// external addresses up to and including the current last found
|
|
// index for this scope.
|
|
nextFound := branchState.NextUnfound()
|
|
|
|
lastFound := nextFound
|
|
if lastFound > 0 {
|
|
lastFound--
|
|
}
|
|
|
|
err := scopedMgr.ExtendAddresses(
|
|
ns, index.Account, index.Branch, lastFound,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Finally, with the scope's addresses extended, we mark used
|
|
// the addresses that were found in the block and belong to
|
|
// this scope.
|
|
addr := branchState.GetAddr(index.Index)
|
|
err = scopedMgr.MarkUsed(ns, addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var lastAccount uint32
|
|
for _, scopedMgr := range scopedMgrs {
|
|
account, err := scopedMgr.LastAccount(ns)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if lastAccount < account {
|
|
lastAccount = account
|
|
}
|
|
}
|
|
|
|
// Make sure all scopes are extended to the same account.
|
|
for _, s := range scopedMgrs {
|
|
for gapAccount := lastAccount; gapAccount >= 0; gapAccount-- {
|
|
_, err := s.AccountProperties(ns, gapAccount)
|
|
// If the account exists, we can stop extending.
|
|
if err == nil {
|
|
break
|
|
}
|
|
err = s.NewRawAccount(ns, gapAccount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// logFilterBlocksResp provides useful logging information when filtering
|
|
// succeeded in finding relevant transactions.
|
|
func logFilterBlocksResp(block wtxmgr.BlockMeta,
|
|
resp *chain.FilterBlocksResponse) {
|
|
|
|
// Log the number of external addresses found in this block.
|
|
var nFoundAddresses int
|
|
nFoundAddresses += len(resp.FoundAddresses)
|
|
|
|
if nFoundAddresses > 0 {
|
|
log.Infof("Recovered %d addrs at height=%d hash=%v",
|
|
nFoundAddresses, block.Height, block.Hash)
|
|
}
|
|
|
|
// Log the number of outpoints found in this block.
|
|
nFoundOutPoints := len(resp.FoundOutPoints)
|
|
if nFoundOutPoints > 0 {
|
|
log.Infof("Found %d spends from watched outpoints at "+
|
|
"height=%d hash=%v",
|
|
nFoundOutPoints, block.Height, block.Hash)
|
|
}
|
|
}
|
|
|
|
type (
|
|
createTxRequest struct {
|
|
keyScope *waddrmgr.KeyScope
|
|
account uint32
|
|
outputs []*wire.TxOut
|
|
minconf int32
|
|
feeSatPerKB btcutil.Amount
|
|
coinSelectionStrategy CoinSelectionStrategy
|
|
dryRun bool
|
|
resp chan createTxResponse
|
|
}
|
|
createTxResponse struct {
|
|
tx *txauthor.AuthoredTx
|
|
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() {
|
|
quit := w.quitChan()
|
|
out:
|
|
for {
|
|
select {
|
|
case txr := <-w.createTxRequests:
|
|
// If the wallet can be locked because it contains
|
|
// private key material, we need to prevent it from
|
|
// doing so while we are assembling the transaction.
|
|
release := func() {}
|
|
heldUnlock, err := w.holdUnlock()
|
|
if err != nil {
|
|
txr.resp <- createTxResponse{nil, err}
|
|
continue
|
|
}
|
|
|
|
release = heldUnlock.release
|
|
|
|
tx, err := w.txToOutputs(
|
|
txr.outputs, txr.keyScope, txr.account,
|
|
txr.minconf, txr.feeSatPerKB,
|
|
txr.coinSelectionStrategy, txr.dryRun,
|
|
)
|
|
|
|
release()
|
|
txr.resp <- createTxResponse{tx, err}
|
|
case <-quit:
|
|
break out
|
|
}
|
|
}
|
|
w.wg.Done()
|
|
}
|
|
|
|
// CreateSimpleTx creates a new signed transaction spending unspent outputs with
|
|
// at least minconf confirmations spending to any number of address/amount
|
|
// pairs. Only unspent outputs belonging to the given key scope and account will
|
|
// be selected, unless a key scope is not specified. In that case, inputs from all
|
|
// accounts may be selected, no matter what key scope they belong to. This is
|
|
// done to handle the default account case, where a user wants to fund a PSBT
|
|
// with inputs regardless of their type (NP2WKH, P2WKH, etc.). Change and an
|
|
// appropriate 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.
|
|
//
|
|
// NOTE: The dryRun argument can be set true to create a tx that doesn't alter
|
|
// the database. A tx created with this set to true SHOULD NOT be broadcasted.
|
|
func (w *Wallet) CreateSimpleTx(keyScope *waddrmgr.KeyScope, account uint32,
|
|
outputs []*wire.TxOut, minconf int32, satPerKb btcutil.Amount,
|
|
coinSelectionStrategy CoinSelectionStrategy, dryRun bool) (
|
|
*txauthor.AuthoredTx, error) {
|
|
|
|
req := createTxRequest{
|
|
keyScope: keyScope,
|
|
account: account,
|
|
outputs: outputs,
|
|
minconf: minconf,
|
|
feeSatPerKB: satPerKb,
|
|
coinSelectionStrategy: coinSelectionStrategy,
|
|
dryRun: dryRun,
|
|
resp: make(chan createTxResponse),
|
|
}
|
|
w.createTxRequests <- req
|
|
resp := <-req.resp
|
|
return resp.tx, resp.err
|
|
}
|
|
|
|
type (
|
|
unlockRequest struct {
|
|
passphrase []byte
|
|
lockAfter <-chan time.Time // nil prevents the timeout.
|
|
err chan error
|
|
}
|
|
|
|
changePassphraseRequest struct {
|
|
old, new []byte
|
|
private bool
|
|
err chan error
|
|
}
|
|
|
|
changePassphrasesRequest struct {
|
|
publicOld, publicNew []byte
|
|
privateOld, privateNew []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 acquired 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)
|
|
quit := w.quitChan()
|
|
out:
|
|
for {
|
|
select {
|
|
case req := <-w.unlockRequests:
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
return w.Manager.Unlock(addrmgrNs, req.passphrase)
|
|
})
|
|
if err != nil {
|
|
req.err <- err
|
|
continue
|
|
}
|
|
timeout = req.lockAfter
|
|
if timeout == nil {
|
|
log.Info("The wallet has been unlocked without a time limit")
|
|
} else {
|
|
log.Info("The wallet has been temporarily unlocked")
|
|
}
|
|
req.err <- nil
|
|
continue
|
|
|
|
case req := <-w.changePassphrase:
|
|
err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
|
addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
|
return w.Manager.ChangePassphrase(
|
|
addrmgrNs, req.old, req.new, req.private,
|
|
&waddrmgr.DefaultScryptOptions,
|
|
)
|
|
})
|
|
req.err <- err
|
|
continue
|
|
|
|
case req := <-w.changePassphrases:
|
|
err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
|
addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
|
err := w.Manager.ChangePassphrase(
|
|
addrmgrNs, req.publicOld, req.publicNew,
|
|
false, &waddrmgr.DefaultScryptOptions,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return w.Manager.ChangePassphrase(
|
|
addrmgrNs, req.privateOld, req.privateNew,
|
|
true, &waddrmgr.DefaultScryptOptions,
|
|
)
|
|
})
|
|
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 <-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 && !waddrmgr.IsError(err, waddrmgr.ErrLocked) {
|
|
log.Errorf("Could not lock wallet: %v", err)
|
|
} else {
|
|
log.Info("The wallet has been locked")
|
|
}
|
|
}
|
|
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, lock <-chan time.Time) error {
|
|
err := make(chan error, 1)
|
|
w.unlockRequests <- unlockRequest{
|
|
passphrase: passphrase,
|
|
lockAfter: lock,
|
|
err: err,
|
|
}
|
|
return <-err
|
|
}
|
|
|
|
// Lock locks the wallet's address manager.
|
|
func (w *Wallet) Lock() {
|
|
select {
|
|
case <-w.quitChan():
|
|
case 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{}{}
|
|
}
|
|
|
|
// ChangePrivatePassphrase 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) ChangePrivatePassphrase(old, new []byte) error {
|
|
err := make(chan error, 1)
|
|
w.changePassphrase <- changePassphraseRequest{
|
|
old: old,
|
|
new: new,
|
|
private: true,
|
|
err: err,
|
|
}
|
|
return <-err
|
|
}
|
|
|
|
// ChangePublicPassphrase modifies the public passphrase of the wallet.
|
|
func (w *Wallet) ChangePublicPassphrase(old, new []byte) error {
|
|
err := make(chan error, 1)
|
|
w.changePassphrase <- changePassphraseRequest{
|
|
old: old,
|
|
new: new,
|
|
private: false,
|
|
err: err,
|
|
}
|
|
return <-err
|
|
}
|
|
|
|
// ChangePassphrases modifies the public and private passphrase of the wallet
|
|
// atomically.
|
|
func (w *Wallet) ChangePassphrases(publicOld, publicNew, privateOld,
|
|
privateNew []byte) error {
|
|
|
|
err := make(chan error, 1)
|
|
w.changePassphrases <- changePassphrasesRequest{
|
|
publicOld: publicOld,
|
|
publicNew: publicNew,
|
|
privateOld: privateOld,
|
|
privateNew: privateNew,
|
|
err: err,
|
|
}
|
|
return <-err
|
|
}
|
|
|
|
// AccountAddresses returns the addresses for every created address for an
|
|
// account.
|
|
func (w *Wallet) AccountAddresses(account uint32, scope *waddrmgr.KeyScope) (
|
|
addrs []btcutil.Address, err error) {
|
|
|
|
// By default, append all addresses under this account.
|
|
fn := func(maddr waddrmgr.ManagedAddress) error {
|
|
addrs = append(addrs, maddr.Address())
|
|
return nil
|
|
}
|
|
|
|
// If scope is set, append only those have the address type under
|
|
// this scope.
|
|
if scope != nil {
|
|
addrSchema := waddrmgr.ScopeAddrMap[*scope]
|
|
|
|
fn = func(maddr waddrmgr.ManagedAddress) error {
|
|
if maddr.AddrType() == addrSchema.InternalAddrType ||
|
|
maddr.AddrType() == addrSchema.ExternalAddrType {
|
|
addrs = append(addrs, maddr.Address())
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
err = walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
return w.Manager.ForEachAccountAddress(addrmgrNs, account, fn)
|
|
})
|
|
return
|
|
}
|
|
|
|
// 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 int32) (btcutil.Amount, btcutil.Amount, error) {
|
|
var balance btcutil.Amount
|
|
var staked btcutil.Amount
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey)
|
|
var err error
|
|
blk := w.Manager.SyncedTo()
|
|
balance, staked, err = w.TxStore.Balance(txmgrNs, confirms, blk.Height)
|
|
return err
|
|
})
|
|
return balance, staked, err
|
|
}
|
|
|
|
// 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.
|
|
//
|
|
// 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
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey)
|
|
|
|
// Get current block. The block height used for calculating
|
|
// the number of tx confirmations.
|
|
syncBlock := w.Manager.SyncedTo()
|
|
|
|
unspent, err := w.TxStore.UnspentOutputs(txmgrNs)
|
|
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(addrmgrNs, addrs[0])
|
|
}
|
|
if err != nil || outputAcct != account || isStake(output.PkScript) {
|
|
continue
|
|
}
|
|
|
|
bals.Total += output.Amount
|
|
if output.FromCoinBase && !confirmed(int32(w.chainParams.CoinbaseMaturity),
|
|
output.Height, syncBlock.Height) {
|
|
bals.ImmatureReward += output.Amount
|
|
} else if confirmed(confirms, output.Height, syncBlock.Height) {
|
|
bals.Spendable += output.Amount
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
return bals, err
|
|
}
|
|
|
|
// CurrentAddress gets the most recently requested Bitcoin payment address
|
|
// from a wallet for a particular key-chain scope. If the address has already
|
|
// been used (there is at least one transaction spending to it in the
|
|
// blockchain or lbcd mempool), the next chained address is returned.
|
|
func (w *Wallet) CurrentAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) {
|
|
chainClient, err := w.requireChainClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
manager, err := w.Manager.FetchScopedKeyManager(scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var (
|
|
addr btcutil.Address
|
|
props *waddrmgr.AccountProperties
|
|
)
|
|
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
|
addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
|
maddr, err := manager.LastAddress(addrmgrNs, account, waddrmgr.ExternalBranch)
|
|
if err != nil {
|
|
// If no address exists yet, create the first external
|
|
// address.
|
|
if waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) {
|
|
addr, props, err = w.newAddress(
|
|
addrmgrNs, account, scope,
|
|
)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Get next chained address if the last one has already been
|
|
// used.
|
|
if maddr.Used(addrmgrNs) {
|
|
addr, props, err = w.newAddress(
|
|
addrmgrNs, account, scope,
|
|
)
|
|
return err
|
|
}
|
|
|
|
addr = maddr.Address()
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If the props have been initially, then we had to create a new address
|
|
// to satisfy the query. Notify the rpc server about the new address.
|
|
if props != nil {
|
|
err = chainClient.NotifyReceived([]btcutil.Address{addr})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
w.NtfnServer.notifyAccountProperties(props)
|
|
}
|
|
|
|
return addr, nil
|
|
}
|
|
|
|
// PubKeyForAddress looks up the associated public key for a P2PKH address.
|
|
func (w *Wallet) PubKeyForAddress(a btcutil.Address) (*btcec.PublicKey, error) {
|
|
var pubKey *btcec.PublicKey
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
managedAddr, err := w.Manager.Address(addrmgrNs, a)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
managedPubKeyAddr, ok := managedAddr.(waddrmgr.ManagedPubKeyAddress)
|
|
if !ok {
|
|
return errors.New("address does not have an associated public key")
|
|
}
|
|
pubKey = managedPubKeyAddr.PubKey()
|
|
return nil
|
|
})
|
|
return pubKey, err
|
|
}
|
|
|
|
// LabelTransaction adds a label to the transaction with the hash provided. The
|
|
// call will fail if the label is too long, or if the transaction already has
|
|
// a label and the overwrite boolean is not set.
|
|
func (w *Wallet) LabelTransaction(hash chainhash.Hash, label string,
|
|
overwrite bool) error {
|
|
|
|
// Check that the transaction is known to the wallet, and fail if it is
|
|
// unknown. If the transaction is known, check whether it already has
|
|
// a label.
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey)
|
|
|
|
dbTx, err := w.TxStore.TxDetails(txmgrNs, &hash)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If the transaction looked up is nil, it was not found. We
|
|
// do not allow labelling of unknown transactions so we fail.
|
|
if dbTx == nil {
|
|
return ErrUnknownTransaction
|
|
}
|
|
|
|
_, err = wtxmgr.FetchTxLabel(txmgrNs, hash)
|
|
return err
|
|
})
|
|
|
|
switch err {
|
|
// If no labels have been written yet, we can silence the error.
|
|
// Likewise if there is no label, we do not need to do any overwrite
|
|
// checks.
|
|
case wtxmgr.ErrNoLabelBucket:
|
|
case wtxmgr.ErrTxLabelNotFound:
|
|
|
|
// If we successfully looked up a label, fail if the overwrite param
|
|
// is not set.
|
|
case nil:
|
|
if !overwrite {
|
|
return ErrTxLabelExists
|
|
}
|
|
|
|
// In another unrelated error occurred, return it.
|
|
default:
|
|
return err
|
|
}
|
|
|
|
return walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
|
txmgrNs := tx.ReadWriteBucket(wtxmgrNamespaceKey)
|
|
return w.TxStore.PutTxLabel(txmgrNs, hash, label)
|
|
})
|
|
}
|
|
|
|
// PrivKeyForAddress looks up the associated private key for a P2PKH or P2PK
|
|
// address.
|
|
func (w *Wallet) PrivKeyForAddress(a btcutil.Address) (*btcec.PrivateKey, error) {
|
|
var privKey *btcec.PrivateKey
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
managedAddr, err := w.Manager.Address(addrmgrNs, a)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
managedPubKeyAddr, ok := managedAddr.(waddrmgr.ManagedPubKeyAddress)
|
|
if !ok {
|
|
return errors.New("address does not have an associated private key")
|
|
}
|
|
privKey, err = managedPubKeyAddr.PrivKey()
|
|
return err
|
|
})
|
|
return privKey, err
|
|
}
|
|
|
|
// HaveAddress returns whether the wallet is the owner of the address a.
|
|
func (w *Wallet) HaveAddress(a btcutil.Address) (bool, error) {
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
_, err := w.Manager.Address(addrmgrNs, a)
|
|
return err
|
|
})
|
|
if err == nil {
|
|
return true, nil
|
|
}
|
|
if waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
// AccountOfAddress finds the account that an address is associated with.
|
|
func (w *Wallet) AccountOfAddress(a btcutil.Address) (uint32, error) {
|
|
var account uint32
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
var err error
|
|
_, account, err = w.Manager.AddrAccount(addrmgrNs, a)
|
|
return err
|
|
})
|
|
return account, err
|
|
}
|
|
|
|
// AddressInfo returns detailed information regarding a wallet address.
|
|
func (w *Wallet) AddressInfo(a btcutil.Address) (waddrmgr.ManagedAddress, error) {
|
|
var managedAddress waddrmgr.ManagedAddress
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
var err error
|
|
managedAddress, err = w.Manager.Address(addrmgrNs, a)
|
|
return err
|
|
})
|
|
return managedAddress, err
|
|
}
|
|
|
|
// AccountNumber returns the account number for an account name under a
|
|
// particular key scope.
|
|
func (w *Wallet) AccountNumber(accountName string) (uint32, error) {
|
|
// By design, the same account number is shared across all scopes.
|
|
return w.accountNumber(waddrmgr.DefaultKeyScope, accountName)
|
|
}
|
|
|
|
func (w *Wallet) accountNumber(scope waddrmgr.KeyScope, accountName string) (uint32, error) {
|
|
manager, err := w.Manager.FetchScopedKeyManager(scope)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
var account uint32
|
|
err = walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
account, err = manager.LookupAccount(addrmgrNs, accountName)
|
|
return err
|
|
})
|
|
return account, err
|
|
}
|
|
|
|
// AccountName returns the name of an account.
|
|
func (w *Wallet) AccountName(accountNumber uint32) (string, error) {
|
|
// By design, the same account name is shared across all scopes.
|
|
return w.accountName(waddrmgr.DefaultKeyScope, accountNumber)
|
|
}
|
|
|
|
func (w *Wallet) accountName(scope waddrmgr.KeyScope, accountNumber uint32) (string, error) {
|
|
manager, err := w.Manager.FetchScopedKeyManager(scope)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var accountName string
|
|
err = walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
accountName, err = manager.AccountName(addrmgrNs, accountNumber)
|
|
return err
|
|
})
|
|
return accountName, err
|
|
}
|
|
|
|
// AccountProperties returns the properties of an account, including address
|
|
// indexes and name. It first fetches the desynced information from the address
|
|
// manager, then updates the indexes based on the address pools.
|
|
func (w *Wallet) AccountProperties(scope waddrmgr.KeyScope, acct uint32) (*waddrmgr.AccountProperties, error) {
|
|
manager, err := w.Manager.FetchScopedKeyManager(scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var props *waddrmgr.AccountProperties
|
|
err = walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
waddrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
var err error
|
|
props, err = manager.AccountProperties(waddrmgrNs, acct)
|
|
return err
|
|
})
|
|
return props, err
|
|
}
|
|
|
|
// AccountPropertiesByName returns the properties of an account by its name. It
|
|
// first fetches the desynced information from the address manager, then updates
|
|
// the indexes based on the address pools.
|
|
func (w *Wallet) AccountPropertiesByName(scope waddrmgr.KeyScope,
|
|
name string) (*waddrmgr.AccountProperties, error) {
|
|
|
|
manager, err := w.Manager.FetchScopedKeyManager(scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var props *waddrmgr.AccountProperties
|
|
err = walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
waddrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
acct, err := manager.LookupAccount(waddrmgrNs, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
props, err = manager.AccountProperties(waddrmgrNs, acct)
|
|
return err
|
|
})
|
|
return props, err
|
|
}
|
|
|
|
// LookupAccount returns the corresponding key scope and account number for the
|
|
// account with the given name.
|
|
func (w *Wallet) LookupAccount(name string) (waddrmgr.KeyScope, uint32, error) {
|
|
var (
|
|
keyScope waddrmgr.KeyScope
|
|
account uint32
|
|
)
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
ns := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
var err error
|
|
keyScope, account, err = w.Manager.LookupAccount(ns, name)
|
|
return err
|
|
})
|
|
return keyScope, account, err
|
|
}
|
|
|
|
// RenameAccount sets the name for an account number to newName.
|
|
func (w *Wallet) RenameAccount(scope waddrmgr.KeyScope, account uint32, newName string) error {
|
|
manager, err := w.Manager.FetchScopedKeyManager(scope)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var props *waddrmgr.AccountProperties
|
|
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
|
addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
|
err := manager.RenameAccount(addrmgrNs, account, newName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
props, err = manager.AccountProperties(addrmgrNs, account)
|
|
return err
|
|
})
|
|
if err == nil {
|
|
w.NtfnServer.notifyAccountProperties(props)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// NextAccount creates the next account and returns its account number. The
|
|
// name must be unique to the account. In order to support automatic seed
|
|
// restoring, new accounts may not be created when all of the previous 100
|
|
// accounts have no transaction history (this is a deviation from the BIP0044
|
|
// spec, which allows no unused account gaps).
|
|
func (w *Wallet) NextAccount(scope waddrmgr.KeyScope, name string) (uint32, error) {
|
|
manager, err := w.Manager.FetchScopedKeyManager(scope)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
var (
|
|
account uint32
|
|
props *waddrmgr.AccountProperties
|
|
)
|
|
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
|
addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
|
var err error
|
|
account, err = manager.NewAccount(addrmgrNs, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
props, err = manager.AccountProperties(addrmgrNs, account)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
log.Errorf("Cannot fetch new account properties for notification "+
|
|
"after account creation: %v", err)
|
|
} else {
|
|
w.NtfnServer.notifyAccountProperties(props)
|
|
}
|
|
return account, err
|
|
}
|
|
|
|
// CreditCategory describes the type of wallet transaction output. The category
|
|
// of "sent transactions" (debits) is always "send", and is not expressed by
|
|
// this type.
|
|
//
|
|
// TODO: This is a requirement of the RPC server and should be moved.
|
|
type CreditCategory byte
|
|
|
|
// These constants define the possible credit categories.
|
|
const (
|
|
CreditReceive CreditCategory = iota
|
|
CreditGenerate
|
|
CreditImmature
|
|
)
|
|
|
|
// String returns the category as a string. This string may be used as the
|
|
// JSON string for categories as part of listtransactions and gettransaction
|
|
// RPC responses.
|
|
func (c CreditCategory) String() string {
|
|
switch c {
|
|
case CreditReceive:
|
|
return "receive"
|
|
case CreditGenerate:
|
|
return "generate"
|
|
case CreditImmature:
|
|
return "immature"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// RecvCategory returns the category of received credit outputs from a
|
|
// transaction record. The passed block chain height is used to distinguish
|
|
// immature from mature coinbase outputs.
|
|
//
|
|
// TODO: This is intended for use by the RPC server and should be moved out of
|
|
// this package at a later time.
|
|
func RecvCategory(details *wtxmgr.TxDetails, syncHeight int32, net *chaincfg.Params) CreditCategory {
|
|
if blockchain.IsCoinBaseTx(&details.MsgTx) {
|
|
if confirmed(int32(net.CoinbaseMaturity), details.Block.Height,
|
|
syncHeight) {
|
|
return CreditGenerate
|
|
}
|
|
return CreditImmature
|
|
}
|
|
return CreditReceive
|
|
}
|
|
|
|
// listTransactions creates a object that may be marshalled to a response result
|
|
// for a listtransactions RPC.
|
|
//
|
|
// TODO: This should be moved to the legacyrpc package.
|
|
func listTransactions(accountName string, tx walletdb.ReadTx,
|
|
details *wtxmgr.TxDetails, addrMgr *waddrmgr.Manager,
|
|
syncHeight int32, net *chaincfg.Params) []btcjson.ListTransactionsResult {
|
|
|
|
addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
|
|
var (
|
|
blockHashStr string
|
|
blockTime int64
|
|
confirmations int64
|
|
)
|
|
if details.Block.Height != -1 {
|
|
blockHashStr = details.Block.Hash.String()
|
|
blockTime = details.Block.Time.Unix()
|
|
confirmations = int64(confirms(details.Block.Height, syncHeight))
|
|
}
|
|
|
|
results := []btcjson.ListTransactionsResult{}
|
|
txHashStr := details.Hash.String()
|
|
received := details.Received.Unix()
|
|
generated := blockchain.IsCoinBaseTx(&details.MsgTx)
|
|
recvCat := RecvCategory(details, syncHeight, net).String()
|
|
|
|
send := len(details.Debits) != 0
|
|
|
|
// Fee can only be determined if every input is a debit.
|
|
var feeF64 float64
|
|
if len(details.Debits) == len(details.MsgTx.TxIn) {
|
|
var debitTotal btcutil.Amount
|
|
for _, deb := range details.Debits {
|
|
debitTotal += deb.Amount
|
|
}
|
|
var outputTotal btcutil.Amount
|
|
for _, output := range details.MsgTx.TxOut {
|
|
outputTotal += btcutil.Amount(output.Value)
|
|
}
|
|
// Note: The actual fee is debitTotal - outputTotal. However,
|
|
// this RPC reports negative numbers for fees, so the inverse
|
|
// is calculated.
|
|
feeF64 = (outputTotal - debitTotal).ToBTC()
|
|
}
|
|
|
|
outputs:
|
|
for i, output := range details.MsgTx.TxOut {
|
|
// Determine if this output is a credit, and if so, determine
|
|
// its spentness.
|
|
var isCredit bool
|
|
var spentCredit bool
|
|
for _, cred := range details.Credits {
|
|
if cred.Index == uint32(i) {
|
|
// Change outputs are ignored.
|
|
if cred.Change {
|
|
continue outputs
|
|
}
|
|
|
|
isCredit = true
|
|
spentCredit = cred.Spent
|
|
break
|
|
}
|
|
}
|
|
|
|
var address string
|
|
var name string
|
|
_, addrs, _, _ := txscript.ExtractPkScriptAddrs(output.PkScript, net)
|
|
if len(addrs) == 1 {
|
|
addr := addrs[0]
|
|
address = addr.EncodeAddress()
|
|
mgr, account, err := addrMgr.AddrAccount(addrmgrNs, addrs[0])
|
|
if err == nil {
|
|
name, err = mgr.AccountName(addrmgrNs, account)
|
|
if err != nil {
|
|
name = ""
|
|
}
|
|
}
|
|
}
|
|
if accountName != "*" && accountName != name {
|
|
continue
|
|
}
|
|
|
|
amountF64 := btcutil.Amount(output.Value).ToBTC()
|
|
result := btcjson.ListTransactionsResult{
|
|
// Fields left zeroed:
|
|
// InvolvesWatchOnly
|
|
// BlockIndex
|
|
//
|
|
// Fields set below:
|
|
// Account
|
|
// Category
|
|
// Amount
|
|
// Fee
|
|
Address: address,
|
|
Vout: uint32(i),
|
|
Confirmations: confirmations,
|
|
Generated: generated,
|
|
BlockHash: blockHashStr,
|
|
BlockTime: blockTime,
|
|
TxID: txHashStr,
|
|
WalletConflicts: []string{},
|
|
Time: received,
|
|
TimeReceived: received,
|
|
}
|
|
|
|
// Add a received/generated/immature result if this is a credit.
|
|
// If the output was spent, create a second result under the
|
|
// send category with the inverse of the output amount. It is
|
|
// therefore possible that a single output may be included in
|
|
// the results set zero, one, or two times.
|
|
//
|
|
// Since credits are not saved for outputs that are not
|
|
// controlled by this wallet, all non-credits from transactions
|
|
// with debits are grouped under the send category.
|
|
|
|
if send || spentCredit {
|
|
result.Account = name
|
|
result.Category = "send"
|
|
result.Amount = -amountF64
|
|
result.Fee = &feeF64
|
|
results = append(results, result)
|
|
}
|
|
if isCredit {
|
|
result.Account = name
|
|
result.Category = recvCat
|
|
result.Amount = amountF64
|
|
result.Fee = nil
|
|
results = append(results, result)
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
// 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(start, end, syncHeight int32) ([]btcjson.ListTransactionsResult, error) {
|
|
txList := []btcjson.ListTransactionsResult{}
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey)
|
|
|
|
rangeFn := func(details []wtxmgr.TxDetails) (bool, error) {
|
|
for _, detail := range details {
|
|
detail := detail
|
|
|
|
jsonResults := listTransactions(
|
|
tx, &detail, w.Manager, syncHeight,
|
|
w.chainParams,
|
|
)
|
|
txList = append(txList, jsonResults...)
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
return w.TxStore.RangeTransactions(txmgrNs, start, end, rangeFn)
|
|
})
|
|
return txList, err
|
|
}
|
|
|
|
// 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(accountName string, from, count int) ([]btcjson.ListTransactionsResult, error) {
|
|
txList := []btcjson.ListTransactionsResult{}
|
|
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey)
|
|
|
|
// Get current block. The block height used for calculating
|
|
// the number of tx confirmations.
|
|
syncBlock := w.Manager.SyncedTo()
|
|
|
|
// Need to skip the first from transactions, and after those, only
|
|
// include the next count transactions.
|
|
skipped := 0
|
|
rangeFn := func(details []wtxmgr.TxDetails) (bool, error) {
|
|
|
|
for _, detail := range details {
|
|
if from > skipped {
|
|
skipped++
|
|
continue
|
|
}
|
|
|
|
if len(txList) >= count {
|
|
txList = txList[:count]
|
|
return true, nil
|
|
}
|
|
|
|
jsonResults := listTransactions(accountName,
|
|
tx, &detail, w.Manager,
|
|
syncBlock.Height, w.chainParams)
|
|
|
|
txList = append(txList, jsonResults...)
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// Return newer results first by starting at mempool height and working
|
|
// down to the genesis block.
|
|
return w.TxStore.RangeTransactions(txmgrNs, -1, 0, rangeFn)
|
|
})
|
|
|
|
for i, j := 0, len(txList)-1; i < j; i, j = i+1, j-1 {
|
|
txList[i], txList[j] = txList[j], txList[i]
|
|
}
|
|
|
|
return txList, err
|
|
}
|
|
|
|
// 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{}
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey)
|
|
|
|
// Get current block. The block height used for calculating
|
|
// the number of tx confirmations.
|
|
syncBlock := w.Manager.SyncedTo()
|
|
rangeFn := func(details []wtxmgr.TxDetails) (bool, error) {
|
|
loopDetails:
|
|
for i := range details {
|
|
detail := &details[i]
|
|
|
|
for _, cred := range detail.Credits {
|
|
pkScript := detail.MsgTx.TxOut[cred.Index].PkScript
|
|
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
|
|
pkScript, w.chainParams)
|
|
if err != nil || len(addrs) != 1 {
|
|
continue
|
|
}
|
|
apkh, ok := addrs[0].(*btcutil.AddressPubKeyHash)
|
|
if !ok {
|
|
continue
|
|
}
|
|
_, ok = pkHashes[string(apkh.ScriptAddress())]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
jsonResults := listTransactions(tx, detail,
|
|
w.Manager, syncBlock.Height, w.chainParams)
|
|
txList = append(txList, jsonResults...)
|
|
continue loopDetails
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
return w.TxStore.RangeTransactions(txmgrNs, 0, -1, rangeFn)
|
|
})
|
|
return txList, err
|
|
}
|
|
|
|
// 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{}
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey)
|
|
|
|
// Get current block. The block height used for calculating
|
|
// the number of tx confirmations.
|
|
syncBlock := w.Manager.SyncedTo()
|
|
|
|
rangeFn := func(details []wtxmgr.TxDetails) (bool, error) {
|
|
// Iterate over transactions at this height in reverse order.
|
|
// This does nothing for unmined transactions, which are
|
|
// unsorted, but it will process mined transactions in the
|
|
// reverse order they were marked mined.
|
|
for i := len(details) - 1; i >= 0; i-- {
|
|
jsonResults := listTransactions(tx, &details[i], w.Manager,
|
|
syncBlock.Height, w.chainParams)
|
|
txList = append(txList, jsonResults...)
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// Return newer results first by starting at mempool height and
|
|
// working down to the genesis block.
|
|
return w.TxStore.RangeTransactions(txmgrNs, -1, 0, rangeFn)
|
|
})
|
|
return txList, err
|
|
}
|
|
|
|
// BlockIdentifier identifies a block by either a height or a hash.
|
|
type BlockIdentifier struct {
|
|
height int32
|
|
hash *chainhash.Hash
|
|
}
|
|
|
|
// 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 *chainhash.Hash) *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,
|
|
accountName string, 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.
|
|
if startBlock != nil {
|
|
if startBlock.hash == nil {
|
|
start = startBlock.height
|
|
} else {
|
|
if chainClient == nil {
|
|
return nil, errors.New("no chain server client")
|
|
}
|
|
switch client := chainClient.(type) {
|
|
case *chain.RPCClient:
|
|
startHeader, err := client.GetBlockHeaderVerbose(
|
|
startBlock.hash,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
start = startHeader.Height
|
|
}
|
|
}
|
|
}
|
|
if endBlock != nil {
|
|
if endBlock.hash == nil {
|
|
end = endBlock.height
|
|
} else {
|
|
if chainClient == nil {
|
|
return nil, errors.New("no chain server client")
|
|
}
|
|
switch client := chainClient.(type) {
|
|
case *chain.RPCClient:
|
|
endHeader, err := client.GetBlockHeaderVerbose(
|
|
endBlock.hash,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
end = endHeader.Height
|
|
}
|
|
}
|
|
}
|
|
|
|
var res GetTransactionsResult
|
|
err := walletdb.View(w.db, func(dbtx walletdb.ReadTx) error {
|
|
txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey)
|
|
|
|
rangeFn := 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(dbtx, 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 w.TxStore.RangeTransactions(txmgrNs, start, end, rangeFn)
|
|
})
|
|
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 *chainhash.Hash
|
|
CurrentBlockHeight int32
|
|
}
|
|
|
|
// Accounts returns the current names, numbers, and total balances of all
|
|
// accounts in the wallet restricted to a particular key scope. 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(scope waddrmgr.KeyScope) (*AccountsResult, error) {
|
|
manager, err := w.Manager.FetchScopedKeyManager(scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var (
|
|
accounts []AccountResult
|
|
syncBlockHash *chainhash.Hash
|
|
syncBlockHeight int32
|
|
)
|
|
err = walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey)
|
|
|
|
syncBlock := w.Manager.SyncedTo()
|
|
syncBlockHash = &syncBlock.Hash
|
|
syncBlockHeight = syncBlock.Height
|
|
unspent, err := w.TxStore.UnspentOutputs(txmgrNs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = manager.ForEachAccount(addrmgrNs, func(acct uint32) error {
|
|
props, err := manager.AccountProperties(addrmgrNs, acct)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
accounts = append(accounts, AccountResult{
|
|
AccountProperties: *props,
|
|
// TotalBalance set below
|
|
})
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return 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(addrmgrNs, addrs[0])
|
|
}
|
|
if err == nil {
|
|
amt, ok := m[outputAcct]
|
|
if ok {
|
|
*amt += output.Amount
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
return &AccountsResult{
|
|
Accounts: accounts,
|
|
CurrentBlockHash: syncBlockHash,
|
|
CurrentBlockHeight: syncBlockHeight,
|
|
}, err
|
|
}
|
|
|
|
// AccountBalanceResult is a single result for the Wallet.AccountBalances method.
|
|
type AccountBalanceResult struct {
|
|
AccountNumber uint32
|
|
AccountName string
|
|
AccountBalance btcutil.Amount
|
|
}
|
|
|
|
// AccountBalances returns all accounts in the wallet and their balances.
|
|
// Balances are determined by excluding transactions that have not met
|
|
// requiredConfs confirmations.
|
|
func (w *Wallet) AccountBalances(scope waddrmgr.KeyScope,
|
|
requiredConfs int32) ([]AccountBalanceResult, error) {
|
|
|
|
manager, err := w.Manager.FetchScopedKeyManager(scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var results []AccountBalanceResult
|
|
err = walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey)
|
|
|
|
syncBlock := w.Manager.SyncedTo()
|
|
|
|
// Fill out all account info except for the balances.
|
|
lastAcct, err := manager.LastAccount(addrmgrNs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
results = make([]AccountBalanceResult, lastAcct+2)
|
|
for i := range results[:len(results)-1] {
|
|
accountName, err := manager.AccountName(addrmgrNs, uint32(i))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
results[i].AccountNumber = uint32(i)
|
|
results[i].AccountName = accountName
|
|
}
|
|
results[len(results)-1].AccountNumber = waddrmgr.ImportedAddrAccount
|
|
results[len(results)-1].AccountName = waddrmgr.ImportedAddrAccountName
|
|
|
|
// Fetch all unspent outputs, and iterate over them tallying each
|
|
// account's balance where the output script pays to an account address
|
|
// and the required number of confirmations is met.
|
|
unspentOutputs, err := w.TxStore.UnspentOutputs(txmgrNs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := range unspentOutputs {
|
|
output := &unspentOutputs[i]
|
|
if !confirmed(requiredConfs, output.Height, syncBlock.Height) {
|
|
continue
|
|
}
|
|
if output.FromCoinBase && !confirmed(int32(w.ChainParams().CoinbaseMaturity),
|
|
output.Height, syncBlock.Height) {
|
|
continue
|
|
}
|
|
_, addrs, _, err := txscript.ExtractPkScriptAddrs(output.PkScript, w.chainParams)
|
|
if err != nil || len(addrs) == 0 {
|
|
continue
|
|
}
|
|
if isStake(output.PkScript) {
|
|
continue
|
|
}
|
|
outputAcct, err := manager.AddrAccount(addrmgrNs, addrs[0])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
switch {
|
|
case outputAcct == waddrmgr.ImportedAddrAccount:
|
|
results[len(results)-1].AccountBalance += output.Amount
|
|
case outputAcct > lastAcct:
|
|
return errors.New("waddrmgr.Manager.AddrAccount returned account " +
|
|
"beyond recorded last account")
|
|
default:
|
|
results[outputAcct].AccountBalance += output.Amount
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
return results, err
|
|
}
|
|
|
|
// creditSlice satisifies the sort.Interface interface to provide sorting
|
|
// transaction credits from oldest to newest. Credits with the same receive
|
|
// time and mined in the same block are not guaranteed to be sorted by the order
|
|
// they appear in the block. Credits from the same transaction are sorted by
|
|
// output index.
|
|
type creditSlice []wtxmgr.Credit
|
|
|
|
func (s creditSlice) Len() int {
|
|
return len(s)
|
|
}
|
|
|
|
func (s creditSlice) Less(i, j int) bool {
|
|
switch {
|
|
// If both credits are from the same tx, sort by output index.
|
|
case s[i].OutPoint.Hash == s[j].OutPoint.Hash:
|
|
return s[i].OutPoint.Index < s[j].OutPoint.Index
|
|
|
|
// If both transactions are unmined, sort by their received date.
|
|
case s[i].Height == -1 && s[j].Height == -1:
|
|
return s[i].Received.Before(s[j].Received)
|
|
|
|
// Unmined (newer) txs always come last.
|
|
case s[i].Height == -1:
|
|
return false
|
|
case s[j].Height == -1:
|
|
return true
|
|
|
|
// If both txs are mined in different blocks, sort by block height.
|
|
default:
|
|
return s[i].Height < s[j].Height
|
|
}
|
|
}
|
|
|
|
func (s creditSlice) Swap(i, j int) {
|
|
s[i], s[j] = s[j], s[i]
|
|
}
|
|
|
|
// 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 int32,
|
|
accountName string) ([]*btcjson.ListUnspentResult, error) {
|
|
|
|
var results []*btcjson.ListUnspentResult
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey)
|
|
|
|
syncBlock := w.Manager.SyncedTo()
|
|
|
|
filter := accountName != ""
|
|
unspent, err := w.TxStore.UnspentOutputs(txmgrNs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sort.Sort(sort.Reverse(creditSlice(unspent)))
|
|
|
|
defaultAccountName := "default"
|
|
|
|
results = make([]*btcjson.ListUnspentResult, 0, len(unspent))
|
|
for i := range unspent {
|
|
output := unspent[i]
|
|
|
|
// Outputs with fewer confirmations than the minimum or more
|
|
// confs than the maximum are excluded.
|
|
confs := confirms(output.Height, syncBlock.Height)
|
|
if confs < minconf || confs > maxconf {
|
|
continue
|
|
}
|
|
|
|
// Only mature coinbase outputs are included.
|
|
if output.FromCoinBase {
|
|
target := int32(w.ChainParams().CoinbaseMaturity)
|
|
if !confirmed(target, output.Height, syncBlock.Height) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Exclude locked outputs from the result set.
|
|
if w.LockedOutpoint(output.OutPoint) {
|
|
continue
|
|
}
|
|
|
|
// Lookup the associated account for the output. Use the
|
|
// default account name in case there is no associated account
|
|
// for some reason, although this should never happen.
|
|
//
|
|
// This will be unnecessary once transactions and outputs are
|
|
// grouped under the associated account in the db.
|
|
outputAcctName := defaultAccountName
|
|
sc, addrs, _, err := txscript.ExtractPkScriptAddrs(
|
|
output.PkScript, w.chainParams)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if len(addrs) > 0 {
|
|
smgr, acct, err := w.Manager.AddrAccount(addrmgrNs, addrs[0])
|
|
if err == nil {
|
|
s, err := smgr.AccountName(addrmgrNs, acct)
|
|
if err == nil {
|
|
outputAcctName = s
|
|
}
|
|
}
|
|
}
|
|
|
|
if filter && outputAcctName != accountName {
|
|
continue
|
|
}
|
|
|
|
// At the moment watch-only addresses are not supported, so all
|
|
// recorded outputs that are not multisig are "spendable".
|
|
// Multisig outputs are only "spendable" if all keys are
|
|
// controlled by this wallet.
|
|
//
|
|
// TODO: Each case will need updates when watch-only addrs
|
|
// is added. For P2PK, P2PKH, and P2SH, the address must be
|
|
// looked up.
|
|
// For multisig, all pubkeys must belong to the manager with
|
|
// the associated private key (currently it only checks whether
|
|
// the pubkey exists, since the private key is required at the
|
|
// moment).
|
|
var spendable bool
|
|
scSwitch:
|
|
switch sc {
|
|
case txscript.PubKeyHashTy:
|
|
spendable = true
|
|
case txscript.PubKeyTy:
|
|
spendable = true
|
|
case txscript.WitnessV0ScriptHashTy:
|
|
spendable = true
|
|
case txscript.WitnessV0PubKeyHashTy:
|
|
spendable = true
|
|
case txscript.MultiSigTy:
|
|
for _, a := range addrs {
|
|
_, err := w.Manager.Address(addrmgrNs, a)
|
|
if err == nil {
|
|
continue
|
|
}
|
|
if waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) {
|
|
break scSwitch
|
|
}
|
|
return err
|
|
}
|
|
spendable = true
|
|
}
|
|
|
|
result := &btcjson.ListUnspentResult{
|
|
TxID: output.OutPoint.Hash.String(),
|
|
Vout: output.OutPoint.Index,
|
|
Account: outputAcctName,
|
|
ScriptPubKey: hex.EncodeToString(output.PkScript),
|
|
Amount: output.Amount.ToBTC(),
|
|
Confirmations: int64(confs),
|
|
Spendable: spendable,
|
|
// Solvable: , TODO: make this work
|
|
IsStake: isStake(output.PkScript),
|
|
}
|
|
|
|
// 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 nil
|
|
})
|
|
return results, err
|
|
}
|
|
|
|
// ListLeasedOutputs returns a list of objects representing the currently locked
|
|
// utxos.
|
|
func (w *Wallet) ListLeasedOutputs() ([]*wtxmgr.LockedOutput, error) {
|
|
var outputs []*wtxmgr.LockedOutput
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
ns := tx.ReadBucket(wtxmgrNamespaceKey)
|
|
var err error
|
|
outputs, err = w.TxStore.ListLockedOutputs(ns)
|
|
return err
|
|
})
|
|
return outputs, err
|
|
}
|
|
|
|
// DumpPrivKeys returns the WIF-encoded private keys for all addresses with
|
|
// private keys in a wallet.
|
|
func (w *Wallet) DumpPrivKeys() ([]string, error) {
|
|
var privkeys []string
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
// Iterate over each active address, appending the private key to
|
|
// privkeys.
|
|
return w.Manager.ForEachActiveAddress(addrmgrNs, func(addr btcutil.Address) error {
|
|
ma, err := w.Manager.Address(addrmgrNs, addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Only those addresses with keys needed.
|
|
pka, ok := ma.(waddrmgr.ManagedPubKeyAddress)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
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 err
|
|
}
|
|
privkeys = append(privkeys, wif.String())
|
|
return nil
|
|
})
|
|
})
|
|
return privkeys, err
|
|
}
|
|
|
|
// DumpWIFPrivateKey returns the WIF encoded private key for a
|
|
// single wallet address.
|
|
func (w *Wallet) DumpWIFPrivateKey(addr btcutil.Address) (string, error) {
|
|
var maddr waddrmgr.ManagedAddress
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
waddrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
// Get private key from wallet if it exists.
|
|
var err error
|
|
maddr, err = w.Manager.Address(waddrmgrNs, addr)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
pka, ok := maddr.(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
|
|
}
|
|
|
|
// 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 {
|
|
w.lockedOutpointsMtx.Lock()
|
|
defer w.lockedOutpointsMtx.Unlock()
|
|
|
|
_, 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.lockedOutpointsMtx.Lock()
|
|
defer w.lockedOutpointsMtx.Unlock()
|
|
|
|
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) {
|
|
w.lockedOutpointsMtx.Lock()
|
|
defer w.lockedOutpointsMtx.Unlock()
|
|
|
|
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.lockedOutpointsMtx.Lock()
|
|
defer w.lockedOutpointsMtx.Unlock()
|
|
|
|
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 {
|
|
w.lockedOutpointsMtx.Lock()
|
|
defer w.lockedOutpointsMtx.Unlock()
|
|
|
|
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
|
|
}
|
|
|
|
// LeaseOutput locks an output to the given ID, preventing it from being
|
|
// available for coin selection. The absolute time of the lock's expiration is
|
|
// returned. The expiration of the lock can be extended by successive
|
|
// invocations of this call.
|
|
//
|
|
// Outputs can be unlocked before their expiration through `UnlockOutput`.
|
|
// Otherwise, they are unlocked lazily through calls which iterate through all
|
|
// known outputs, e.g., `CalculateBalance`, `ListUnspent`.
|
|
//
|
|
// If the output is not known, ErrUnknownOutput is returned. If the output has
|
|
// already been locked to a different ID, then ErrOutputAlreadyLocked is
|
|
// returned.
|
|
//
|
|
// NOTE: This differs from LockOutpoint in that outputs are locked for a limited
|
|
// amount of time and their locks are persisted to disk.
|
|
func (w *Wallet) LeaseOutput(id wtxmgr.LockID, op wire.OutPoint,
|
|
duration time.Duration) (time.Time, error) {
|
|
|
|
var expiry time.Time
|
|
err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
|
ns := tx.ReadWriteBucket(wtxmgrNamespaceKey)
|
|
var err error
|
|
expiry, err = w.TxStore.LockOutput(ns, id, op, duration)
|
|
return err
|
|
})
|
|
return expiry, err
|
|
}
|
|
|
|
// ReleaseOutput unlocks an output, allowing it to be available for coin
|
|
// selection if it remains unspent. The ID should match the one used to
|
|
// originally lock the output.
|
|
func (w *Wallet) ReleaseOutput(id wtxmgr.LockID, op wire.OutPoint) error {
|
|
return walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
|
ns := tx.ReadWriteBucket(wtxmgrNamespaceKey)
|
|
return w.TxStore.UnlockOutput(ns, id, op)
|
|
})
|
|
}
|
|
|
|
// 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() {
|
|
var txs []*wire.MsgTx
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey)
|
|
var err error
|
|
txs, err = w.TxStore.UnminedTxs(txmgrNs)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
log.Errorf("Unable to retrieve unconfirmed transactions to "+
|
|
"resend: %v", err)
|
|
return
|
|
}
|
|
|
|
for _, tx := range txs {
|
|
txHash, err := w.publishTransaction(tx)
|
|
if err != nil {
|
|
log.Debugf("Unable to rebroadcast transaction %v: %v",
|
|
tx.TxHash(), err)
|
|
continue
|
|
}
|
|
|
|
log.Debugf("Successfully rebroadcast unconfirmed transaction %v",
|
|
txHash)
|
|
}
|
|
}
|
|
|
|
// SortedActivePaymentAddresses returns a slice of all active payment
|
|
// addresses in a wallet.
|
|
func (w *Wallet) SortedActivePaymentAddresses() ([]string, error) {
|
|
var addrStrs []string
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
return w.Manager.ForEachActiveAddress(addrmgrNs, func(addr btcutil.Address) error {
|
|
addrStrs = append(addrStrs, addr.EncodeAddress())
|
|
return nil
|
|
})
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sort.Strings(addrStrs)
|
|
return addrStrs, nil
|
|
}
|
|
|
|
// NewAddress returns the next external chained address for a wallet.
|
|
func (w *Wallet) NewAddress(account uint32,
|
|
scope waddrmgr.KeyScope) (btcutil.Address, error) {
|
|
|
|
chainClient, err := w.requireChainClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var (
|
|
addr btcutil.Address
|
|
props *waddrmgr.AccountProperties
|
|
)
|
|
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
|
addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
|
var err error
|
|
addr, props, err = w.newAddress(addrmgrNs, account, scope)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Notify the rpc server about the newly created address.
|
|
err = chainClient.NotifyReceived([]btcutil.Address{addr})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
w.NtfnServer.notifyAccountProperties(props)
|
|
|
|
return addr, nil
|
|
}
|
|
|
|
func (w *Wallet) newAddress(addrmgrNs walletdb.ReadWriteBucket, account uint32,
|
|
scope waddrmgr.KeyScope) (btcutil.Address, *waddrmgr.AccountProperties, error) {
|
|
|
|
manager, err := w.Manager.FetchScopedKeyManager(scope)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Get next address from wallet.
|
|
addrs, err := manager.NextAddresses(addrmgrNs, account, waddrmgr.ExternalBranch, 1)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
props, err := manager.AccountProperties(addrmgrNs, account)
|
|
if err != nil {
|
|
log.Errorf("Cannot fetch account properties for notification "+
|
|
"after deriving next external address: %v", err)
|
|
return nil, nil, err
|
|
}
|
|
|
|
return addrs[0].Address(), props, nil
|
|
}
|
|
|
|
// NewChangeAddress returns a new change address for a wallet.
|
|
func (w *Wallet) NewChangeAddress(account uint32,
|
|
scope waddrmgr.KeyScope) (btcutil.Address, error) {
|
|
|
|
chainClient, err := w.requireChainClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var addr btcutil.Address
|
|
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
|
addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
|
var err error
|
|
addr, err = w.newChangeAddress(addrmgrNs, account, scope)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Notify the rpc server about the newly created address.
|
|
err = chainClient.NotifyReceived([]btcutil.Address{addr})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return addr, nil
|
|
}
|
|
|
|
// newChangeAddress returns a new change address for the wallet.
|
|
//
|
|
// NOTE: This method requires the caller to use the backend's NotifyReceived
|
|
// method in order to detect when an on-chain transaction pays to the address
|
|
// being created.
|
|
func (w *Wallet) newChangeAddress(addrmgrNs walletdb.ReadWriteBucket,
|
|
account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) {
|
|
|
|
manager, err := w.Manager.FetchScopedKeyManager(scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get next chained change address from wallet for account.
|
|
addrs, err := manager.NextAddresses(addrmgrNs, account, waddrmgr.InternalBranch, 1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return addrs[0].Address(), 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
|
|
}
|
|
}
|
|
|
|
// AccountTotalReceivedResult is a single result for the
|
|
// Wallet.TotalReceivedForAccounts method.
|
|
type AccountTotalReceivedResult struct {
|
|
AccountNumber uint32
|
|
AccountName string
|
|
TotalReceived btcutil.Amount
|
|
LastConfirmation int32
|
|
}
|
|
|
|
// TotalReceivedForAccounts iterates through a wallet's transaction history,
|
|
// returning the total amount of Bitcoin received for all accounts.
|
|
func (w *Wallet) TotalReceivedForAccounts(scope waddrmgr.KeyScope,
|
|
minConf int32) ([]AccountTotalReceivedResult, error) {
|
|
|
|
manager, err := w.Manager.FetchScopedKeyManager(scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var results []AccountTotalReceivedResult
|
|
err = walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey)
|
|
|
|
syncBlock := w.Manager.SyncedTo()
|
|
|
|
err := manager.ForEachAccount(addrmgrNs, func(account uint32) error {
|
|
accountName, err := manager.AccountName(addrmgrNs, account)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
results = append(results, AccountTotalReceivedResult{
|
|
AccountNumber: account,
|
|
AccountName: accountName,
|
|
})
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var stopHeight int32
|
|
|
|
if minConf > 0 {
|
|
stopHeight = syncBlock.Height - minConf + 1
|
|
} else {
|
|
stopHeight = -1
|
|
}
|
|
|
|
rangeFn := func(details []wtxmgr.TxDetails) (bool, error) {
|
|
for i := range details {
|
|
detail := &details[i]
|
|
for _, cred := range detail.Credits {
|
|
pkScript := detail.MsgTx.TxOut[cred.Index].PkScript
|
|
var outputAcct uint32
|
|
_, addrs, _, err := txscript.ExtractPkScriptAddrs(pkScript, w.chainParams)
|
|
if err == nil && len(addrs) > 0 {
|
|
_, outputAcct, err = w.Manager.AddrAccount(addrmgrNs, addrs[0])
|
|
}
|
|
if err == nil {
|
|
acctIndex := int(outputAcct)
|
|
if outputAcct == waddrmgr.ImportedAddrAccount {
|
|
acctIndex = len(results) - 1
|
|
}
|
|
res := &results[acctIndex]
|
|
res.TotalReceived += cred.Amount
|
|
res.LastConfirmation = confirms(
|
|
detail.Block.Height, syncBlock.Height)
|
|
}
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
return w.TxStore.RangeTransactions(txmgrNs, 0, stopHeight, rangeFn)
|
|
})
|
|
return results, err
|
|
}
|
|
|
|
// 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, minConf int32) (btcutil.Amount, error) {
|
|
var amount btcutil.Amount
|
|
err := walletdb.View(w.db, func(tx walletdb.ReadTx) error {
|
|
txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey)
|
|
|
|
syncBlock := w.Manager.SyncedTo()
|
|
|
|
var (
|
|
addrStr = addr.EncodeAddress()
|
|
stopHeight int32
|
|
)
|
|
|
|
if minConf > 0 {
|
|
stopHeight = syncBlock.Height - minConf + 1
|
|
} else {
|
|
stopHeight = -1
|
|
}
|
|
rangeFn := func(details []wtxmgr.TxDetails) (bool, error) {
|
|
for i := range details {
|
|
detail := &details[i]
|
|
for _, cred := range detail.Credits {
|
|
pkScript := detail.MsgTx.TxOut[cred.Index].PkScript
|
|
_, addrs, _, err := txscript.ExtractPkScriptAddrs(pkScript,
|
|
w.chainParams)
|
|
// 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 += cred.Amount
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
return w.TxStore.RangeTransactions(txmgrNs, 0, stopHeight, rangeFn)
|
|
})
|
|
return amount, err
|
|
}
|
|
|
|
// SendOutputs creates and sends payment transactions. Coin selection is
|
|
// performed by the wallet, choosing inputs that belong to the given key scope
|
|
// and account, unless a key scope is not specified. In that case, inputs from
|
|
// accounts matching the account number provided across all key scopes may be
|
|
// selected. This is done to handle the default account case, where a user wants
|
|
// to fund a PSBT with inputs regardless of their type (NP2WKH, P2WKH, etc.). It
|
|
// returns the transaction upon success.
|
|
func (w *Wallet) SendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope,
|
|
account uint32, minconf int32, satPerKb btcutil.Amount,
|
|
coinSelectionStrategy CoinSelectionStrategy, label string) (
|
|
*wire.MsgTx, error) {
|
|
|
|
// Ensure the outputs to be created adhere to the network's consensus
|
|
// rules.
|
|
for _, output := range outputs {
|
|
err := txrules.CheckOutput(
|
|
output, txrules.DefaultRelayFeePerKb,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Create the transaction and broadcast it to the network. The
|
|
// transaction will be added to the database in order to ensure that we
|
|
// continue to re-broadcast the transaction upon restarts until it has
|
|
// been confirmed.
|
|
createdTx, err := w.CreateSimpleTx(
|
|
keyScope, account, outputs, minconf, satPerKb,
|
|
coinSelectionStrategy, false,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
txHash, err := w.reliablyPublishTransaction(createdTx.Tx, label)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Sanity check on the returned tx hash.
|
|
if *txHash != createdTx.Tx.TxHash() {
|
|
return nil, errors.New("tx hash mismatch")
|
|
}
|
|
|
|
return createdTx.Tx, nil
|
|
}
|
|
|
|
// 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
|
|
err := walletdb.View(w.db, func(dbtx walletdb.ReadTx) error {
|
|
addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey)
|
|
txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey)
|
|
|
|
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(txmgrNs, prevHash)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot query previous transaction "+
|
|
"details for %v: %v", txIn.PreviousOutPoint, err)
|
|
}
|
|
if txDetails == nil {
|
|
return 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(addrmgrNs, addr)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
pka, ok := address.(waddrmgr.ManagedPubKeyAddress)
|
|
if !ok {
|
|
return nil, false, fmt.Errorf("address %v is not "+
|
|
"a pubkey address", address.Address().EncodeAddress())
|
|
}
|
|
|
|
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(addrmgrNs, 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, nil, 0)
|
|
if err == nil {
|
|
err = vm.Execute()
|
|
}
|
|
if err != nil {
|
|
signErrors = append(signErrors, SignatureError{
|
|
InputIndex: uint32(i),
|
|
Error: err,
|
|
})
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
return signErrors, err
|
|
}
|
|
|
|
// ErrDoubleSpend is an error returned from PublishTransaction in case the
|
|
// published transaction failed to propagate since it was double spending a
|
|
// confirmed transaction or a transaction in the mempool.
|
|
type ErrDoubleSpend struct {
|
|
backendError error
|
|
}
|
|
|
|
// Error returns the string representation of ErrDoubleSpend.
|
|
//
|
|
// NOTE: Satisfies the error interface.
|
|
func (e *ErrDoubleSpend) Error() string {
|
|
return fmt.Sprintf("double spend: %v", e.backendError)
|
|
}
|
|
|
|
// Unwrap returns the underlying error returned from the backend.
|
|
func (e *ErrDoubleSpend) Unwrap() error {
|
|
return e.backendError
|
|
}
|
|
|
|
// ErrReplacement is an error returned from PublishTransaction in case the
|
|
// published transaction failed to propagate since it was double spending a
|
|
// replacable transaction but did not satisfy the requirements to replace it.
|
|
type ErrReplacement struct {
|
|
backendError error
|
|
}
|
|
|
|
// Error returns the string representation of ErrReplacement.
|
|
//
|
|
// NOTE: Satisfies the error interface.
|
|
func (e *ErrReplacement) Error() string {
|
|
return fmt.Sprintf("unable to replace transaction: %v", e.backendError)
|
|
}
|
|
|
|
// Unwrap returns the underlying error returned from the backend.
|
|
func (e *ErrReplacement) Unwrap() error {
|
|
return e.backendError
|
|
}
|
|
|
|
// PublishTransaction sends the transaction to the consensus RPC server so it
|
|
// can be propagated 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, label string) error {
|
|
_, err := w.reliablyPublishTransaction(tx, label)
|
|
return err
|
|
}
|
|
|
|
// reliablyPublishTransaction is a superset of publishTransaction which contains
|
|
// the primary logic required for publishing a transaction, updating the
|
|
// relevant database state, and finally possible removing the transaction from
|
|
// the database (along with cleaning up all inputs used, and outputs created) if
|
|
// the transaction is rejected by the backend.
|
|
func (w *Wallet) reliablyPublishTransaction(tx *wire.MsgTx,
|
|
label string) (*chainhash.Hash, error) {
|
|
|
|
chainClient, err := w.requireChainClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// As we aim for this to be general reliable transaction broadcast API,
|
|
// we'll write this tx to disk as an unconfirmed transaction. This way,
|
|
// upon restarts, we'll always rebroadcast it, and also add it to our
|
|
// set of records.
|
|
txRec, err := wtxmgr.NewTxRecordFromMsgTx(tx, time.Now())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Along the way, we'll extract our relevant destination addresses from
|
|
// the transaction.
|
|
var ourAddrs []btcutil.Address
|
|
err = walletdb.Update(w.db, func(dbTx walletdb.ReadWriteTx) error {
|
|
addrmgrNs := dbTx.ReadWriteBucket(waddrmgrNamespaceKey)
|
|
for _, txOut := range tx.TxOut {
|
|
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
|
|
txOut.PkScript, w.chainParams,
|
|
)
|
|
if err != nil {
|
|
// Non-standard outputs can safely be skipped because
|
|
// they're not supported by the wallet.
|
|
continue
|
|
}
|
|
for _, addr := range addrs {
|
|
// Skip any addresses which are not relevant to
|
|
// us.
|
|
_, err := w.Manager.Address(addrmgrNs, addr)
|
|
if waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ourAddrs = append(ourAddrs, addr)
|
|
}
|
|
}
|
|
|
|
// If there is a label we should write, get the namespace key
|
|
// and record it in the tx store.
|
|
if len(label) != 0 {
|
|
txmgrNs := dbTx.ReadWriteBucket(wtxmgrNamespaceKey)
|
|
if err = w.TxStore.PutTxLabel(txmgrNs, tx.TxHash(), label); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return w.addRelevantTx(dbTx, txRec, nil)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// We'll also ask to be notified of the transaction once it confirms
|
|
// on-chain. This is done outside of the database transaction to prevent
|
|
// backend interaction within it.
|
|
if err := chainClient.NotifyReceived(ourAddrs); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return w.publishTransaction(tx)
|
|
}
|
|
|
|
// publishTransaction attempts to send an unconfirmed transaction to the
|
|
// wallet's current backend. In the event that sending the transaction fails for
|
|
// whatever reason, it will be removed from the wallet's unconfirmed transaction
|
|
// store.
|
|
func (w *Wallet) publishTransaction(tx *wire.MsgTx) (*chainhash.Hash, error) {
|
|
chainClient, err := w.requireChainClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// match is a helper method to easily string match on the error
|
|
// message.
|
|
match := func(err error, s string) bool {
|
|
return strings.Contains(strings.ToLower(err.Error()), s)
|
|
}
|
|
|
|
_, err = chainClient.SendRawTransaction(tx, false)
|
|
|
|
// Determine if this was an RPC error thrown due to the transaction
|
|
// already confirming.
|
|
var rpcTxConfirmed bool
|
|
if rpcErr, ok := err.(*btcjson.RPCError); ok {
|
|
rpcTxConfirmed = rpcErr.Code == btcjson.ErrRPCTxAlreadyInChain
|
|
}
|
|
|
|
var (
|
|
txid = tx.TxHash()
|
|
returnErr error
|
|
)
|
|
|
|
switch {
|
|
case err == nil:
|
|
return &txid, nil
|
|
|
|
// Since we have different backends that can be used with the wallet,
|
|
// we'll need to check specific errors for each one.
|
|
//
|
|
// If the transaction is already in the mempool, we can just return now.
|
|
//
|
|
// This error is returned when broadcasting/sending a transaction to a
|
|
// lbcd node that already has it in their mempool.
|
|
// https://github.com/lbryio/lbcd/blob/130ea5bddde33df32b06a1cdb42a6316eb73cff5/mempool/mempool.go#L953
|
|
case match(err, "already have transaction"):
|
|
fallthrough
|
|
|
|
// This error is returned when broadcasting a transaction to a bitcoind
|
|
// node that already has it in their mempool.
|
|
// https://github.com/bitcoin/bitcoin/blob/9bf5768dd628b3a7c30dd42b5ed477a92c4d3540/src/validation.cpp#L590
|
|
case match(err, "txn-already-in-mempool"):
|
|
return &txid, nil
|
|
|
|
// If the transaction has already confirmed, we can safely remove it
|
|
// from the unconfirmed store as it should already exist within the
|
|
// confirmed store. We'll avoid returning an error as the broadcast was
|
|
// in a sense successful.
|
|
//
|
|
// This error is returned when sending a transaction that has already
|
|
// confirmed to a lbcd/bitcoind node over RPC.
|
|
// https://github.com/lbryio/lbcd/blob/130ea5bddde33df32b06a1cdb42a6316eb73cff5/rpcserver.go#L3355
|
|
// https://github.com/bitcoin/bitcoin/blob/9bf5768dd628b3a7c30dd42b5ed477a92c4d3540/src/node/transaction.cpp#L36
|
|
case rpcTxConfirmed:
|
|
fallthrough
|
|
|
|
// This error is returned when broadcasting a transaction that has
|
|
// already confirmed to a lbcd node over the P2P network.
|
|
// https://github.com/lbryio/lbcd/blob/130ea5bddde33df32b06a1cdb42a6316eb73cff5/mempool/mempool.go#L1036
|
|
case match(err, "transaction already exists"):
|
|
fallthrough
|
|
|
|
// This error is returned when broadcasting a transaction that has
|
|
// already confirmed to a bitcoind node over the P2P network.
|
|
// https://github.com/bitcoin/bitcoin/blob/9bf5768dd628b3a7c30dd42b5ed477a92c4d3540/src/validation.cpp#L648
|
|
case match(err, "txn-already-known"):
|
|
dbErr := walletdb.Update(w.db, func(dbTx walletdb.ReadWriteTx) error {
|
|
txmgrNs := dbTx.ReadWriteBucket(wtxmgrNamespaceKey)
|
|
txRec, err := wtxmgr.NewTxRecordFromMsgTx(tx, time.Now())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return w.TxStore.RemoveUnminedTx(txmgrNs, txRec)
|
|
})
|
|
if dbErr != nil {
|
|
log.Warnf("Unable to remove confirmed transaction %v "+
|
|
"from unconfirmed store: %v", tx.TxHash(), dbErr)
|
|
}
|
|
|
|
return &txid, nil
|
|
|
|
// If the transactions is invalid since it attempts to double spend a
|
|
// transaction already in the mempool or in the chain, we'll remove it
|
|
// from the store and return an error.
|
|
//
|
|
// This error is returned from lbcd when there is already a transaction
|
|
// not signaling replacement in the mempool that spends one of the
|
|
// referenced outputs.
|
|
// https://github.com/lbryio/lbcd/blob/130ea5bddde33df32b06a1cdb42a6316eb73cff5/mempool/mempool.go#L591
|
|
case match(err, "already spent"):
|
|
fallthrough
|
|
|
|
// This error is returned from lbcd when a referenced output cannot be
|
|
// found, meaning it etiher has been spent or doesn't exist.
|
|
// https://github.com/lbryio/lbcd/blob/130ea5bddde33df32b06a1cdb42a6316eb73cff5/blockchain/chain.go#L405
|
|
case match(err, "already been spent"):
|
|
fallthrough
|
|
|
|
// This error is returned from lbcd when a transaction is spending
|
|
// either output that is missing or already spent, and orphans aren't
|
|
// allowed.
|
|
// https://github.com/lbryio/lbcd/blob/130ea5bddde33df32b06a1cdb42a6316eb73cff5/mempool/mempool.go#L1409
|
|
case match(err, "orphan transaction"):
|
|
fallthrough
|
|
|
|
// Error returned from bitcoind when output was spent by other
|
|
// non-replacable transaction already in the mempool.
|
|
// https://github.com/bitcoin/bitcoin/blob/9bf5768dd628b3a7c30dd42b5ed477a92c4d3540/src/validation.cpp#L622
|
|
case match(err, "txn-mempool-conflict"):
|
|
fallthrough
|
|
|
|
// Returned by bitcoind on the RPC when broadcasting a transaction that
|
|
// is spending either output that is missing or already spent.
|
|
//
|
|
// https://github.com/bitcoin/bitcoin/blob/9bf5768dd628b3a7c30dd42b5ed477a92c4d3540/src/node/transaction.cpp#L49
|
|
// https://github.com/bitcoin/bitcoin/blob/0.20/src/validation.cpp#L642
|
|
case match(err, "missing inputs") ||
|
|
match(err, "bad-txns-inputs-missingorspent"):
|
|
|
|
returnErr = &ErrDoubleSpend{
|
|
backendError: err,
|
|
}
|
|
|
|
// Returned by bitcoind if the transaction spends outputs that would be
|
|
// replaced by it.
|
|
// https://github.com/bitcoin/bitcoin/blob/9bf5768dd628b3a7c30dd42b5ed477a92c4d3540/src/validation.cpp#L790
|
|
case match(err, "bad-txns-spends-conflicting-tx"):
|
|
fallthrough
|
|
|
|
// Returned by bitcoind when a replacement transaction did not have
|
|
// enough fee.
|
|
// https://github.com/bitcoin/bitcoin/blob/9bf5768dd628b3a7c30dd42b5ed477a92c4d3540/src/validation.cpp#L830
|
|
// https://github.com/bitcoin/bitcoin/blob/9bf5768dd628b3a7c30dd42b5ed477a92c4d3540/src/validation.cpp#L894
|
|
// https://github.com/bitcoin/bitcoin/blob/9bf5768dd628b3a7c30dd42b5ed477a92c4d3540/src/validation.cpp#L904
|
|
case match(err, "insufficient fee"):
|
|
fallthrough
|
|
|
|
// Returned by bitcoind in case the transaction would replace too many
|
|
// transaction in the mempool.
|
|
// https://github.com/bitcoin/bitcoin/blob/9bf5768dd628b3a7c30dd42b5ed477a92c4d3540/src/validation.cpp#L858
|
|
case match(err, "too many potential replacements"):
|
|
fallthrough
|
|
|
|
// Returned by bitcoind if the transaction spends an output that is
|
|
// unconfimed and not spent by the transaction it replaces.
|
|
// https://github.com/bitcoin/bitcoin/blob/9bf5768dd628b3a7c30dd42b5ed477a92c4d3540/src/validation.cpp#L882
|
|
case match(err, "replacement-adds-unconfirmed"):
|
|
fallthrough
|
|
|
|
// Returned by lbcd when replacement transaction was rejected for
|
|
// whatever reason.
|
|
// https://github.com/lbryio/lbcd/blob/130ea5bddde33df32b06a1cdb42a6316eb73cff5/mempool/mempool.go#L841
|
|
// https://github.com/lbryio/lbcd/blob/130ea5bddde33df32b06a1cdb42a6316eb73cff5/mempool/mempool.go#L854
|
|
// https://github.com/lbryio/lbcd/blob/130ea5bddde33df32b06a1cdb42a6316eb73cff5/mempool/mempool.go#L875
|
|
// https://github.com/lbryio/lbcd/blob/130ea5bddde33df32b06a1cdb42a6316eb73cff5/mempool/mempool.go#L896
|
|
// https://github.com/lbryio/lbcd/blob/130ea5bddde33df32b06a1cdb42a6316eb73cff5/mempool/mempool.go#L913
|
|
case match(err, "replacement transaction"):
|
|
returnErr = &ErrReplacement{
|
|
backendError: err,
|
|
}
|
|
|
|
// We received an error not matching any of the above cases.
|
|
default:
|
|
returnErr = fmt.Errorf("unmatched backend error: %v", err)
|
|
}
|
|
|
|
// If the transaction was rejected for whatever other reason, then
|
|
// we'll remove it from the transaction store, as otherwise, we'll
|
|
// attempt to continually re-broadcast it, and the UTXO state of the
|
|
// wallet won't be accurate.
|
|
dbErr := walletdb.Update(w.db, func(dbTx walletdb.ReadWriteTx) error {
|
|
txmgrNs := dbTx.ReadWriteBucket(wtxmgrNamespaceKey)
|
|
txRec, err := wtxmgr.NewTxRecordFromMsgTx(tx, time.Now())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return w.TxStore.RemoveUnminedTx(txmgrNs, txRec)
|
|
})
|
|
if dbErr != nil {
|
|
log.Warnf("Unable to remove invalid transaction %v: %v",
|
|
tx.TxHash(), dbErr)
|
|
} else {
|
|
log.Infof("Removed invalid transaction: %v",
|
|
spew.Sdump(tx))
|
|
}
|
|
|
|
return nil, returnErr
|
|
}
|
|
|
|
// ChainParams returns the network parameters for the blockchain the wallet
|
|
// belongs to.
|
|
func (w *Wallet) ChainParams() *chaincfg.Params {
|
|
return w.chainParams
|
|
}
|
|
|
|
// Database returns the underlying walletdb database. This method is provided
|
|
// in order to allow applications wrapping lbcwallet to store app-specific data
|
|
// with the wallet's database.
|
|
func (w *Wallet) Database() walletdb.DB {
|
|
return w.db
|
|
}
|
|
|
|
// CreateWithCallback is the same as Create with an added callback that will be
|
|
// called in the same transaction the wallet structure is initialized.
|
|
func CreateWithCallback(db walletdb.DB, pubPass, privPass []byte,
|
|
rootKey *hdkeychain.ExtendedKey, params *chaincfg.Params,
|
|
birthday time.Time, cb func(walletdb.ReadWriteTx) error) error {
|
|
|
|
return create(
|
|
db, pubPass, privPass, rootKey, params, birthday, cb,
|
|
)
|
|
}
|
|
|
|
// Create creates an new wallet, writing it to an empty database. If the passed
|
|
// root key is non-nil, it is used. Otherwise, a secure random seed of the
|
|
// recommended length is generated.
|
|
func Create(db walletdb.DB, pubPass, privPass []byte,
|
|
rootKey *hdkeychain.ExtendedKey, params *chaincfg.Params,
|
|
birthday time.Time) error {
|
|
|
|
return create(
|
|
db, pubPass, privPass, rootKey, params, birthday, nil,
|
|
)
|
|
}
|
|
func create(db walletdb.DB, pubPass, privPass []byte,
|
|
rootKey *hdkeychain.ExtendedKey, params *chaincfg.Params,
|
|
birthday time.Time,
|
|
cb func(walletdb.ReadWriteTx) error) error {
|
|
|
|
// If no root key was provided, we create one now from a random seed.
|
|
if rootKey == nil {
|
|
hdSeed, err := hdkeychain.GenerateSeed(
|
|
hdkeychain.RecommendedSeedLen,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Derive the master extended key from the seed.
|
|
rootKey, err = hdkeychain.NewMaster(hdSeed, params)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to derive master extended " +
|
|
"key")
|
|
}
|
|
}
|
|
|
|
// We need a private key if this isn't a watching only wallet.
|
|
if rootKey != nil && !rootKey.IsPrivate() {
|
|
return fmt.Errorf("need extended private key for wallet that " +
|
|
"is not watching only")
|
|
}
|
|
|
|
return walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
|
|
addrmgrNs, err := tx.CreateTopLevelBucket(waddrmgrNamespaceKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
txmgrNs, err := tx.CreateTopLevelBucket(wtxmgrNamespaceKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = waddrmgr.Create(
|
|
addrmgrNs, rootKey, pubPass, privPass, params, nil,
|
|
birthday,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = wtxmgr.Create(txmgrNs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if cb != nil {
|
|
return cb(tx)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Open loads an already-created wallet from the passed database and namespaces.
|
|
func Open(db walletdb.DB, pubPass []byte, cbs *waddrmgr.OpenCallbacks,
|
|
params *chaincfg.Params, recoveryWindow uint32) (*Wallet, error) {
|
|
|
|
var (
|
|
addrMgr *waddrmgr.Manager
|
|
txMgr *wtxmgr.Store
|
|
)
|
|
|
|
// Before attempting to open the wallet, we'll check if there are any
|
|
// database upgrades for us to proceed. We'll also create our references
|
|
// to the address and transaction managers, as they are backed by the
|
|
// database.
|
|
err := walletdb.Update(db, func(tx walletdb.ReadWriteTx) error {
|
|
addrMgrBucket := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
|
if addrMgrBucket == nil {
|
|
return errors.New("missing address manager namespace")
|
|
}
|
|
txMgrBucket := tx.ReadWriteBucket(wtxmgrNamespaceKey)
|
|
if txMgrBucket == nil {
|
|
return errors.New("missing transaction manager namespace")
|
|
}
|
|
|
|
addrMgrUpgrader := waddrmgr.NewMigrationManager(addrMgrBucket)
|
|
txMgrUpgrader := wtxmgr.NewMigrationManager(txMgrBucket)
|
|
err := migration.Upgrade(txMgrUpgrader, addrMgrUpgrader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
addrMgr, err = waddrmgr.Open(addrMgrBucket, pubPass, params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
txMgr, err = wtxmgr.Open(txMgrBucket, params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Infof("Opened wallet") // TODO: log balance? last sync height?
|
|
|
|
w := &Wallet{
|
|
publicPassphrase: pubPass,
|
|
db: db,
|
|
Manager: addrMgr,
|
|
TxStore: txMgr,
|
|
lockedOutpoints: map[wire.OutPoint]struct{}{},
|
|
recoveryWindow: recoveryWindow,
|
|
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),
|
|
changePassphrases: make(chan changePassphrasesRequest),
|
|
chainParams: params,
|
|
quit: make(chan struct{}),
|
|
}
|
|
|
|
w.NtfnServer = newNotificationServer(w)
|
|
w.TxStore.NotifyUnspent = func(hash *chainhash.Hash, index uint32) {
|
|
w.NtfnServer.notifyUnspentOutput(0, hash, index)
|
|
}
|
|
|
|
return w, nil
|
|
}
|