216 lines
6.4 KiB
Go
216 lines
6.4 KiB
Go
// Copyright (c) 2013-2015 The btcsuite developers
|
|
// Use of this source code is governed by an ISC
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package wallet
|
|
|
|
import (
|
|
"github.com/roasbeef/btcd/txscript"
|
|
"github.com/roasbeef/btcwallet/chain"
|
|
"github.com/roasbeef/btcwallet/waddrmgr"
|
|
"github.com/roasbeef/btcwallet/walletdb"
|
|
"github.com/roasbeef/btcwallet/wtxmgr"
|
|
)
|
|
|
|
func (w *Wallet) handleChainNotifications() {
|
|
chainClient, err := w.requireChainClient()
|
|
if err != nil {
|
|
log.Errorf("handleChainNotifications called without RPC client")
|
|
w.wg.Done()
|
|
return
|
|
}
|
|
|
|
sync := func(w *Wallet) {
|
|
// At the moment there is no recourse if the rescan fails for
|
|
// some reason, however, the wallet will not be marked synced
|
|
// and many methods will error early since the wallet is known
|
|
// to be out of date.
|
|
err := w.syncWithChain()
|
|
if err != nil && !w.ShuttingDown() {
|
|
log.Warnf("Unable to synchronize wallet to chain: %v", err)
|
|
}
|
|
}
|
|
|
|
for n := range chainClient.Notifications() {
|
|
var notificationName string
|
|
var err error
|
|
switch n := n.(type) {
|
|
case chain.ClientConnected:
|
|
go sync(w)
|
|
case chain.BlockConnected:
|
|
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
|
return w.connectBlock(tx, wtxmgr.BlockMeta(n))
|
|
})
|
|
notificationName = "blockconnected"
|
|
case chain.BlockDisconnected:
|
|
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
|
return w.disconnectBlock(tx, wtxmgr.BlockMeta(n))
|
|
})
|
|
notificationName = "blockdisconnected"
|
|
case chain.RelevantTx:
|
|
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
|
return w.addRelevantTx(tx, n.TxRecord, n.Block)
|
|
})
|
|
notificationName = "recvtx/redeemingtx"
|
|
case chain.FilteredBlockConnected:
|
|
// Atomically update for the whole block.
|
|
err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error {
|
|
var err error
|
|
for _, rec := range n.RelevantTxs {
|
|
err = w.addRelevantTx(tx, rec, n.Block)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// The following are handled by the wallet's rescan
|
|
// goroutines, so just pass them there.
|
|
case *chain.RescanProgress, *chain.RescanFinished:
|
|
w.rescanNotifications <- n
|
|
}
|
|
if err != nil {
|
|
log.Errorf("Failed to process consensus server notification "+
|
|
"(name: `%s`, detail: `%v`)", notificationName, err)
|
|
}
|
|
}
|
|
w.wg.Done()
|
|
}
|
|
|
|
// connectBlock handles a chain server notification by marking a wallet
|
|
// that's currently in-sync with the chain server as being synced up to
|
|
// the passed block.
|
|
func (w *Wallet) connectBlock(dbtx walletdb.ReadWriteTx, b wtxmgr.BlockMeta) error {
|
|
addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey)
|
|
|
|
bs := waddrmgr.BlockStamp{
|
|
Height: b.Height,
|
|
Hash: b.Hash,
|
|
}
|
|
err := w.Manager.SetSyncedTo(addrmgrNs, &bs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Notify interested clients of the connected block.
|
|
//
|
|
// TODO: move all notifications outside of the database transaction.
|
|
w.NtfnServer.notifyAttachedBlock(dbtx, &b)
|
|
return nil
|
|
}
|
|
|
|
// disconnectBlock handles a chain server reorganize by rolling back all
|
|
// block history from the reorged block for a wallet in-sync with the chain
|
|
// server.
|
|
func (w *Wallet) disconnectBlock(dbtx walletdb.ReadWriteTx, b wtxmgr.BlockMeta) error {
|
|
addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey)
|
|
txmgrNs := dbtx.ReadWriteBucket(wtxmgrNamespaceKey)
|
|
|
|
if !w.ChainSynced() {
|
|
return nil
|
|
}
|
|
|
|
// Disconnect the last seen block from the manager if it matches the
|
|
// removed block.
|
|
iter := w.Manager.NewIterateRecentBlocks()
|
|
if iter != nil && iter.BlockStamp().Hash == b.Hash {
|
|
if iter.Prev() {
|
|
prev := iter.BlockStamp()
|
|
w.Manager.SetSyncedTo(addrmgrNs, &prev)
|
|
err := w.TxStore.Rollback(txmgrNs, prev.Height+1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// The reorg is farther back than the recently-seen list
|
|
// of blocks has recorded, so set it to unsynced which
|
|
// will in turn lead to a rescan from either the
|
|
// earliest blockstamp the addresses in the manager are
|
|
// known to have been created.
|
|
w.Manager.SetSyncedTo(addrmgrNs, nil)
|
|
// Rollback everything but the genesis block.
|
|
err := w.TxStore.Rollback(txmgrNs, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Notify interested clients of the disconnected block.
|
|
w.NtfnServer.notifyDetachedBlock(&b.Hash)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (w *Wallet) addRelevantTx(dbtx walletdb.ReadWriteTx, rec *wtxmgr.TxRecord, block *wtxmgr.BlockMeta) error {
|
|
addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey)
|
|
txmgrNs := dbtx.ReadWriteBucket(wtxmgrNamespaceKey)
|
|
|
|
// At the moment all notified transactions are assumed to actually be
|
|
// relevant. This assumption will not hold true when SPV support is
|
|
// added, but until then, simply insert the transaction because there
|
|
// should either be one or more relevant inputs or outputs.
|
|
err := w.TxStore.InsertTx(txmgrNs, rec, block)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check every output to determine whether it is controlled by a wallet
|
|
// key. If so, mark the output as a credit.
|
|
for i, output := range rec.MsgTx.TxOut {
|
|
_, addrs, _, err := txscript.ExtractPkScriptAddrs(output.PkScript,
|
|
w.chainParams)
|
|
if err != nil {
|
|
// Non-standard outputs are skipped.
|
|
continue
|
|
}
|
|
for _, addr := range addrs {
|
|
ma, err := w.Manager.Address(addrmgrNs, addr)
|
|
if err == nil {
|
|
// TODO: Credits should be added with the
|
|
// account they belong to, so wtxmgr is able to
|
|
// track per-account balances.
|
|
err = w.TxStore.AddCredit(txmgrNs, rec, block, uint32(i),
|
|
ma.Internal())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = w.Manager.MarkUsed(addrmgrNs, addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Debugf("Marked address %v used", addr)
|
|
continue
|
|
}
|
|
|
|
// Missing addresses are skipped. Other errors should
|
|
// be propagated.
|
|
if !waddrmgr.IsError(err, waddrmgr.ErrAddressNotFound) {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Send notification of mined or unmined transaction to any interested
|
|
// clients.
|
|
//
|
|
// TODO: Avoid the extra db hits.
|
|
if block == nil {
|
|
details, err := w.TxStore.UniqueTxDetails(txmgrNs, &rec.Hash, nil)
|
|
if err != nil {
|
|
log.Errorf("Cannot query transaction details for notifiation: %v", err)
|
|
} else {
|
|
w.NtfnServer.notifyUnminedTransaction(dbtx, details)
|
|
}
|
|
} else {
|
|
details, err := w.TxStore.UniqueTxDetails(txmgrNs, &rec.Hash, &block.Block)
|
|
if err != nil {
|
|
log.Errorf("Cannot query transaction details for notifiation: %v", err)
|
|
} else {
|
|
w.NtfnServer.notifyMinedTransaction(dbtx, details, block)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|