1ad0dbade9
A while back, changes were made to the wallet such that it waits for the backend to be synced before beginning to store the latest 10,000 blocks of the chain. This inherently broke sync progress implementations based on the best_header_timestamp result from the GetInfo RPC for neutrino-based nodes as the wallet is no longer tracking all blocks in the chain. To work around this, we now make sure to return the backend's best header timestamp instead of the wallet's, allowing said sync progress implementations to work again.
992 lines
31 KiB
Go
992 lines
31 KiB
Go
package btcwallet
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"math"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/btcsuite/btcd/txscript"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/btcsuite/btcutil"
|
|
"github.com/btcsuite/btcutil/psbt"
|
|
"github.com/btcsuite/btcwallet/chain"
|
|
"github.com/btcsuite/btcwallet/waddrmgr"
|
|
base "github.com/btcsuite/btcwallet/wallet"
|
|
"github.com/btcsuite/btcwallet/wallet/txauthor"
|
|
"github.com/btcsuite/btcwallet/wallet/txrules"
|
|
"github.com/btcsuite/btcwallet/walletdb"
|
|
"github.com/btcsuite/btcwallet/wtxmgr"
|
|
"github.com/lightningnetwork/lnd/keychain"
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
|
)
|
|
|
|
const (
|
|
defaultAccount = uint32(waddrmgr.DefaultAccountNum)
|
|
|
|
// UnconfirmedHeight is the special case end height that is used to
|
|
// obtain unconfirmed transactions from ListTransactionDetails.
|
|
UnconfirmedHeight int32 = -1
|
|
)
|
|
|
|
var (
|
|
// waddrmgrNamespaceKey is the namespace key that the waddrmgr state is
|
|
// stored within the top-level waleltdb buckets of btcwallet.
|
|
waddrmgrNamespaceKey = []byte("waddrmgr")
|
|
|
|
// lightningAddrSchema is the scope addr schema for all keys that we
|
|
// derive. We'll treat them all as p2wkh addresses, as atm we must
|
|
// specify a particular type.
|
|
lightningAddrSchema = waddrmgr.ScopeAddrSchema{
|
|
ExternalAddrType: waddrmgr.WitnessPubKey,
|
|
InternalAddrType: waddrmgr.WitnessPubKey,
|
|
}
|
|
)
|
|
|
|
// BtcWallet is an implementation of the lnwallet.WalletController interface
|
|
// backed by an active instance of btcwallet. At the time of the writing of
|
|
// this documentation, this implementation requires a full btcd node to
|
|
// operate.
|
|
type BtcWallet struct {
|
|
// wallet is an active instance of btcwallet.
|
|
wallet *base.Wallet
|
|
|
|
chain chain.Interface
|
|
|
|
db walletdb.DB
|
|
|
|
cfg *Config
|
|
|
|
netParams *chaincfg.Params
|
|
|
|
chainKeyScope waddrmgr.KeyScope
|
|
}
|
|
|
|
// A compile time check to ensure that BtcWallet implements the
|
|
// WalletController and BlockChainIO interfaces.
|
|
var _ lnwallet.WalletController = (*BtcWallet)(nil)
|
|
var _ lnwallet.BlockChainIO = (*BtcWallet)(nil)
|
|
|
|
// New returns a new fully initialized instance of BtcWallet given a valid
|
|
// configuration struct.
|
|
func New(cfg Config) (*BtcWallet, error) {
|
|
// Ensure the wallet exists or create it when the create flag is set.
|
|
netDir := NetworkDir(cfg.DataDir, cfg.NetParams)
|
|
|
|
// Create the key scope for the coin type being managed by this wallet.
|
|
chainKeyScope := waddrmgr.KeyScope{
|
|
Purpose: keychain.BIP0043Purpose,
|
|
Coin: cfg.CoinType,
|
|
}
|
|
|
|
// Maybe the wallet has already been opened and unlocked by the
|
|
// WalletUnlocker. So if we get a non-nil value from the config,
|
|
// we assume everything is in order.
|
|
var wallet = cfg.Wallet
|
|
if wallet == nil {
|
|
// No ready wallet was passed, so try to open an existing one.
|
|
var pubPass []byte
|
|
if cfg.PublicPass == nil {
|
|
pubPass = defaultPubPassphrase
|
|
} else {
|
|
pubPass = cfg.PublicPass
|
|
}
|
|
loader := base.NewLoader(
|
|
cfg.NetParams, netDir, cfg.NoFreelistSync,
|
|
cfg.RecoveryWindow,
|
|
)
|
|
walletExists, err := loader.WalletExists()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !walletExists {
|
|
// Wallet has never been created, perform initial
|
|
// set up.
|
|
wallet, err = loader.CreateNewWallet(
|
|
pubPass, cfg.PrivatePass, cfg.HdSeed,
|
|
cfg.Birthday,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
// Wallet has been created and been initialized at
|
|
// this point, open it along with all the required DB
|
|
// namespaces, and the DB itself.
|
|
wallet, err = loader.OpenExistingWallet(pubPass, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return &BtcWallet{
|
|
cfg: &cfg,
|
|
wallet: wallet,
|
|
db: wallet.Database(),
|
|
chain: cfg.ChainSource,
|
|
netParams: cfg.NetParams,
|
|
chainKeyScope: chainKeyScope,
|
|
}, nil
|
|
}
|
|
|
|
// BackEnd returns the underlying ChainService's name as a string.
|
|
//
|
|
// This is a part of the WalletController interface.
|
|
func (b *BtcWallet) BackEnd() string {
|
|
if b.chain != nil {
|
|
return b.chain.BackEnd()
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// InternalWallet returns a pointer to the internal base wallet which is the
|
|
// core of btcwallet.
|
|
func (b *BtcWallet) InternalWallet() *base.Wallet {
|
|
return b.wallet
|
|
}
|
|
|
|
// Start initializes the underlying rpc connection, the wallet itself, and
|
|
// begins syncing to the current available blockchain state.
|
|
//
|
|
// This is a part of the WalletController interface.
|
|
func (b *BtcWallet) Start() error {
|
|
// We'll start by unlocking the wallet and ensuring that the KeyScope:
|
|
// (1017, 1) exists within the internal waddrmgr. We'll need this in
|
|
// order to properly generate the keys required for signing various
|
|
// contracts.
|
|
if err := b.wallet.Unlock(b.cfg.PrivatePass, nil); err != nil {
|
|
return err
|
|
}
|
|
_, err := b.wallet.Manager.FetchScopedKeyManager(b.chainKeyScope)
|
|
if err != nil {
|
|
// If the scope hasn't yet been created (it wouldn't been
|
|
// loaded by default if it was), then we'll manually create the
|
|
// scope for the first time ourselves.
|
|
err := walletdb.Update(b.db, func(tx walletdb.ReadWriteTx) error {
|
|
addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey)
|
|
|
|
_, err := b.wallet.Manager.NewScopedKeyManager(
|
|
addrmgrNs, b.chainKeyScope, lightningAddrSchema,
|
|
)
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Establish an RPC connection in addition to starting the goroutines
|
|
// in the underlying wallet.
|
|
if err := b.chain.Start(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Start the underlying btcwallet core.
|
|
b.wallet.Start()
|
|
|
|
// Pass the rpc client into the wallet so it can sync up to the
|
|
// current main chain.
|
|
b.wallet.SynchronizeRPC(b.chain)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop signals the wallet for shutdown. Shutdown may entail closing
|
|
// any active sockets, database handles, stopping goroutines, etc.
|
|
//
|
|
// This is a part of the WalletController interface.
|
|
func (b *BtcWallet) Stop() error {
|
|
b.wallet.Stop()
|
|
|
|
b.wallet.WaitForShutdown()
|
|
|
|
b.chain.Stop()
|
|
|
|
return nil
|
|
}
|
|
|
|
// ConfirmedBalance returns the sum of all the wallet's unspent outputs that
|
|
// have at least confs confirmations. If confs is set to zero, then all unspent
|
|
// outputs, including those currently in the mempool will be included in the
|
|
// final sum.
|
|
//
|
|
// This is a part of the WalletController interface.
|
|
func (b *BtcWallet) ConfirmedBalance(confs int32) (btcutil.Amount, error) {
|
|
var balance btcutil.Amount
|
|
|
|
witnessOutputs, err := b.ListUnspentWitness(confs, math.MaxInt32)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
for _, witnessOutput := range witnessOutputs {
|
|
balance += witnessOutput.Value
|
|
}
|
|
|
|
return balance, nil
|
|
}
|
|
|
|
// NewAddress returns the next external or internal address for the wallet
|
|
// dictated by the value of the `change` parameter. If change is true, then an
|
|
// internal address will be returned, otherwise an external address should be
|
|
// returned.
|
|
//
|
|
// This is a part of the WalletController interface.
|
|
func (b *BtcWallet) NewAddress(t lnwallet.AddressType, change bool) (btcutil.Address, error) {
|
|
var keyScope waddrmgr.KeyScope
|
|
|
|
switch t {
|
|
case lnwallet.WitnessPubKey:
|
|
keyScope = waddrmgr.KeyScopeBIP0084
|
|
case lnwallet.NestedWitnessPubKey:
|
|
keyScope = waddrmgr.KeyScopeBIP0049Plus
|
|
default:
|
|
return nil, fmt.Errorf("unknown address type")
|
|
}
|
|
|
|
if change {
|
|
return b.wallet.NewChangeAddress(defaultAccount, keyScope)
|
|
}
|
|
|
|
return b.wallet.NewAddress(defaultAccount, keyScope)
|
|
}
|
|
|
|
// LastUnusedAddress returns the last *unused* address known by the wallet. An
|
|
// address is unused if it hasn't received any payments. This can be useful in
|
|
// UIs in order to continually show the "freshest" address without having to
|
|
// worry about "address inflation" caused by continual refreshing. Similar to
|
|
// NewAddress it can derive a specified address type, and also optionally a
|
|
// change address.
|
|
func (b *BtcWallet) LastUnusedAddress(addrType lnwallet.AddressType) (
|
|
btcutil.Address, error) {
|
|
|
|
var keyScope waddrmgr.KeyScope
|
|
|
|
switch addrType {
|
|
case lnwallet.WitnessPubKey:
|
|
keyScope = waddrmgr.KeyScopeBIP0084
|
|
case lnwallet.NestedWitnessPubKey:
|
|
keyScope = waddrmgr.KeyScopeBIP0049Plus
|
|
default:
|
|
return nil, fmt.Errorf("unknown address type")
|
|
}
|
|
|
|
return b.wallet.CurrentAddress(defaultAccount, keyScope)
|
|
}
|
|
|
|
// IsOurAddress checks if the passed address belongs to this wallet
|
|
//
|
|
// This is a part of the WalletController interface.
|
|
func (b *BtcWallet) IsOurAddress(a btcutil.Address) bool {
|
|
result, err := b.wallet.HaveAddress(a)
|
|
return result && (err == nil)
|
|
}
|
|
|
|
// SendOutputs funds, signs, and broadcasts a Bitcoin transaction paying out to
|
|
// the specified outputs. In the case the wallet has insufficient funds, or the
|
|
// outputs are non-standard, a non-nil error will be returned.
|
|
//
|
|
// NOTE: This method requires the global coin selection lock to be held.
|
|
//
|
|
// This is a part of the WalletController interface.
|
|
func (b *BtcWallet) SendOutputs(outputs []*wire.TxOut,
|
|
feeRate chainfee.SatPerKWeight, minconf int32, label string) (*wire.MsgTx, error) {
|
|
|
|
// Convert our fee rate from sat/kw to sat/kb since it's required by
|
|
// SendOutputs.
|
|
feeSatPerKB := btcutil.Amount(feeRate.FeePerKVByte())
|
|
|
|
// Sanity check outputs.
|
|
if len(outputs) < 1 {
|
|
return nil, lnwallet.ErrNoOutputs
|
|
}
|
|
|
|
// Sanity check minconf.
|
|
if minconf < 0 {
|
|
return nil, lnwallet.ErrInvalidMinconf
|
|
}
|
|
|
|
return b.wallet.SendOutputs(
|
|
outputs, defaultAccount, minconf, feeSatPerKB, label,
|
|
)
|
|
}
|
|
|
|
// CreateSimpleTx creates a Bitcoin transaction paying to the specified
|
|
// outputs. The transaction is not broadcasted to the network, but a new change
|
|
// address might be created in the wallet database. In the case the wallet has
|
|
// insufficient funds, or the outputs are non-standard, an error should be
|
|
// returned. This method also takes the target fee expressed in sat/kw that
|
|
// should be used when crafting the transaction.
|
|
//
|
|
// 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.
|
|
//
|
|
// NOTE: This method requires the global coin selection lock to be held.
|
|
//
|
|
// This is a part of the WalletController interface.
|
|
func (b *BtcWallet) CreateSimpleTx(outputs []*wire.TxOut,
|
|
feeRate chainfee.SatPerKWeight, dryRun bool) (*txauthor.AuthoredTx, error) {
|
|
|
|
// The fee rate is passed in using units of sat/kw, so we'll convert
|
|
// this to sat/KB as the CreateSimpleTx method requires this unit.
|
|
feeSatPerKB := btcutil.Amount(feeRate.FeePerKVByte())
|
|
|
|
// Sanity check outputs.
|
|
if len(outputs) < 1 {
|
|
return nil, lnwallet.ErrNoOutputs
|
|
}
|
|
for _, output := range outputs {
|
|
// When checking an output for things like dusty-ness, we'll
|
|
// use the default mempool relay fee rather than the target
|
|
// effective fee rate to ensure accuracy. Otherwise, we may
|
|
// mistakenly mark small-ish, but not quite dust output as
|
|
// dust.
|
|
err := txrules.CheckOutput(
|
|
output, txrules.DefaultRelayFeePerKb,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return b.wallet.CreateSimpleTx(defaultAccount, outputs, 1, feeSatPerKB, dryRun)
|
|
}
|
|
|
|
// LockOutpoint marks an outpoint as locked meaning it will no longer be deemed
|
|
// as eligible for coin selection. Locking outputs are utilized in order to
|
|
// avoid race conditions when selecting inputs for usage when funding a
|
|
// channel.
|
|
//
|
|
// NOTE: This method requires the global coin selection lock to be held.
|
|
//
|
|
// This is a part of the WalletController interface.
|
|
func (b *BtcWallet) LockOutpoint(o wire.OutPoint) {
|
|
b.wallet.LockOutpoint(o)
|
|
}
|
|
|
|
// UnlockOutpoint unlocks a previously locked output, marking it eligible for
|
|
// coin selection.
|
|
//
|
|
// NOTE: This method requires the global coin selection lock to be held.
|
|
//
|
|
// This is a part of the WalletController interface.
|
|
func (b *BtcWallet) UnlockOutpoint(o wire.OutPoint) {
|
|
b.wallet.UnlockOutpoint(o)
|
|
}
|
|
|
|
// LeaseOutput locks an output to the given ID, preventing it from being
|
|
// available for any future coin selection attempts. 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 `ReleaseOutput`.
|
|
//
|
|
// If the output is not known, wtxmgr.ErrUnknownOutput is returned. If the
|
|
// output has already been locked to a different ID, then
|
|
// wtxmgr.ErrOutputAlreadyLocked is returned.
|
|
//
|
|
// NOTE: This method requires the global coin selection lock to be held.
|
|
func (b *BtcWallet) LeaseOutput(id wtxmgr.LockID, op wire.OutPoint) (time.Time,
|
|
error) {
|
|
|
|
// Make sure we don't attempt to double lock an output that's been
|
|
// locked by the in-memory implementation.
|
|
if b.wallet.LockedOutpoint(op) {
|
|
return time.Time{}, wtxmgr.ErrOutputAlreadyLocked
|
|
}
|
|
|
|
return b.wallet.LeaseOutput(id, op)
|
|
}
|
|
|
|
// 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.
|
|
//
|
|
// NOTE: This method requires the global coin selection lock to be held.
|
|
func (b *BtcWallet) ReleaseOutput(id wtxmgr.LockID, op wire.OutPoint) error {
|
|
return b.wallet.ReleaseOutput(id, op)
|
|
}
|
|
|
|
// ListUnspentWitness returns a slice of all the unspent outputs the wallet
|
|
// controls which pay to witness programs either directly or indirectly.
|
|
//
|
|
// NOTE: This method requires the global coin selection lock to be held.
|
|
//
|
|
// This is a part of the WalletController interface.
|
|
func (b *BtcWallet) ListUnspentWitness(minConfs, maxConfs int32) (
|
|
[]*lnwallet.Utxo, error) {
|
|
// First, grab all the unfiltered currently unspent outputs.
|
|
unspentOutputs, err := b.wallet.ListUnspent(minConfs, maxConfs, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Next, we'll run through all the regular outputs, only saving those
|
|
// which are p2wkh outputs or a p2wsh output nested within a p2sh output.
|
|
witnessOutputs := make([]*lnwallet.Utxo, 0, len(unspentOutputs))
|
|
for _, output := range unspentOutputs {
|
|
pkScript, err := hex.DecodeString(output.ScriptPubKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
addressType := lnwallet.UnknownAddressType
|
|
if txscript.IsPayToWitnessPubKeyHash(pkScript) {
|
|
addressType = lnwallet.WitnessPubKey
|
|
} else if txscript.IsPayToScriptHash(pkScript) {
|
|
// TODO(roasbeef): This assumes all p2sh outputs returned by the
|
|
// wallet are nested p2pkh. We can't check the redeem script because
|
|
// the btcwallet service does not include it.
|
|
addressType = lnwallet.NestedWitnessPubKey
|
|
}
|
|
|
|
if addressType == lnwallet.WitnessPubKey ||
|
|
addressType == lnwallet.NestedWitnessPubKey {
|
|
|
|
txid, err := chainhash.NewHashFromStr(output.TxID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// We'll ensure we properly convert the amount given in
|
|
// BTC to satoshis.
|
|
amt, err := btcutil.NewAmount(output.Amount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
utxo := &lnwallet.Utxo{
|
|
AddressType: addressType,
|
|
Value: amt,
|
|
PkScript: pkScript,
|
|
OutPoint: wire.OutPoint{
|
|
Hash: *txid,
|
|
Index: output.Vout,
|
|
},
|
|
Confirmations: output.Confirmations,
|
|
}
|
|
witnessOutputs = append(witnessOutputs, utxo)
|
|
}
|
|
|
|
}
|
|
|
|
return witnessOutputs, nil
|
|
}
|
|
|
|
// PublishTransaction performs cursory validation (dust checks, etc), then
|
|
// finally broadcasts the passed transaction to the Bitcoin network. If
|
|
// publishing the transaction fails, an error describing the reason is returned
|
|
// (currently ErrDoubleSpend). If the transaction is already published to the
|
|
// network (either in the mempool or chain) no error will be returned.
|
|
func (b *BtcWallet) PublishTransaction(tx *wire.MsgTx, label string) error {
|
|
if err := b.wallet.PublishTransaction(tx, label); err != nil {
|
|
|
|
// If we failed to publish the transaction, check whether we
|
|
// got an error of known type.
|
|
switch err.(type) {
|
|
|
|
// If the wallet reports a double spend, convert it to our
|
|
// internal ErrDoubleSpend and return.
|
|
case *base.ErrDoubleSpend:
|
|
return lnwallet.ErrDoubleSpend
|
|
|
|
// If the wallet reports a replacement error, return
|
|
// ErrDoubleSpend, as we currently are never attempting to
|
|
// replace transactions.
|
|
case *base.ErrReplacement:
|
|
return lnwallet.ErrDoubleSpend
|
|
|
|
default:
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LabelTransaction adds a label to a transaction. If the tx already
|
|
// has a label, this call will fail unless the overwrite parameter
|
|
// is set. Labels must not be empty, and they are limited to 500 chars.
|
|
//
|
|
// Note: it is part of the WalletController interface.
|
|
func (b *BtcWallet) LabelTransaction(hash chainhash.Hash, label string,
|
|
overwrite bool) error {
|
|
|
|
return b.wallet.LabelTransaction(hash, label, overwrite)
|
|
}
|
|
|
|
// extractBalanceDelta extracts the net balance delta from the PoV of the
|
|
// wallet given a TransactionSummary.
|
|
func extractBalanceDelta(
|
|
txSummary base.TransactionSummary,
|
|
tx *wire.MsgTx,
|
|
) (btcutil.Amount, error) {
|
|
// For each input we debit the wallet's outflow for this transaction,
|
|
// and for each output we credit the wallet's inflow for this
|
|
// transaction.
|
|
var balanceDelta btcutil.Amount
|
|
for _, input := range txSummary.MyInputs {
|
|
balanceDelta -= input.PreviousAmount
|
|
}
|
|
for _, output := range txSummary.MyOutputs {
|
|
balanceDelta += btcutil.Amount(tx.TxOut[output.Index].Value)
|
|
}
|
|
|
|
return balanceDelta, nil
|
|
}
|
|
|
|
// minedTransactionsToDetails is a helper function which converts a summary
|
|
// information about mined transactions to a TransactionDetail.
|
|
func minedTransactionsToDetails(
|
|
currentHeight int32,
|
|
block base.Block,
|
|
chainParams *chaincfg.Params,
|
|
) ([]*lnwallet.TransactionDetail, error) {
|
|
|
|
details := make([]*lnwallet.TransactionDetail, 0, len(block.Transactions))
|
|
for _, tx := range block.Transactions {
|
|
wireTx := &wire.MsgTx{}
|
|
txReader := bytes.NewReader(tx.Transaction)
|
|
|
|
if err := wireTx.Deserialize(txReader); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var destAddresses []btcutil.Address
|
|
for _, txOut := range wireTx.TxOut {
|
|
_, outAddresses, _, err := txscript.ExtractPkScriptAddrs(
|
|
txOut.PkScript, chainParams,
|
|
)
|
|
if err != nil {
|
|
// Skip any unsupported addresses to prevent
|
|
// other transactions from not being returned.
|
|
continue
|
|
}
|
|
|
|
destAddresses = append(destAddresses, outAddresses...)
|
|
}
|
|
|
|
txDetail := &lnwallet.TransactionDetail{
|
|
Hash: *tx.Hash,
|
|
NumConfirmations: currentHeight - block.Height + 1,
|
|
BlockHash: block.Hash,
|
|
BlockHeight: block.Height,
|
|
Timestamp: block.Timestamp,
|
|
TotalFees: int64(tx.Fee),
|
|
DestAddresses: destAddresses,
|
|
RawTx: tx.Transaction,
|
|
Label: tx.Label,
|
|
}
|
|
|
|
balanceDelta, err := extractBalanceDelta(tx, wireTx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
txDetail.Value = balanceDelta
|
|
|
|
details = append(details, txDetail)
|
|
}
|
|
|
|
return details, nil
|
|
}
|
|
|
|
// unminedTransactionsToDetail is a helper function which converts a summary
|
|
// for an unconfirmed transaction to a transaction detail.
|
|
func unminedTransactionsToDetail(
|
|
summary base.TransactionSummary,
|
|
chainParams *chaincfg.Params,
|
|
) (*lnwallet.TransactionDetail, error) {
|
|
|
|
wireTx := &wire.MsgTx{}
|
|
txReader := bytes.NewReader(summary.Transaction)
|
|
|
|
if err := wireTx.Deserialize(txReader); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var destAddresses []btcutil.Address
|
|
for _, txOut := range wireTx.TxOut {
|
|
_, outAddresses, _, err :=
|
|
txscript.ExtractPkScriptAddrs(txOut.PkScript, chainParams)
|
|
if err != nil {
|
|
// Skip any unsupported addresses to prevent other
|
|
// transactions from not being returned.
|
|
continue
|
|
}
|
|
|
|
destAddresses = append(destAddresses, outAddresses...)
|
|
}
|
|
|
|
txDetail := &lnwallet.TransactionDetail{
|
|
Hash: *summary.Hash,
|
|
TotalFees: int64(summary.Fee),
|
|
Timestamp: summary.Timestamp,
|
|
DestAddresses: destAddresses,
|
|
RawTx: summary.Transaction,
|
|
Label: summary.Label,
|
|
}
|
|
|
|
balanceDelta, err := extractBalanceDelta(summary, wireTx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
txDetail.Value = balanceDelta
|
|
|
|
return txDetail, nil
|
|
}
|
|
|
|
// ListTransactionDetails returns a list of all transactions which are
|
|
// relevant to the wallet. It takes inclusive start and end height to allow
|
|
// paginated queries. Unconfirmed transactions can be included in the query
|
|
// by providing endHeight = UnconfirmedHeight (= -1).
|
|
//
|
|
// This is a part of the WalletController interface.
|
|
func (b *BtcWallet) ListTransactionDetails(startHeight,
|
|
endHeight int32) ([]*lnwallet.TransactionDetail, error) {
|
|
|
|
// Grab the best block the wallet knows of, we'll use this to calculate
|
|
// # of confirmations shortly below.
|
|
bestBlock := b.wallet.Manager.SyncedTo()
|
|
currentHeight := bestBlock.Height
|
|
|
|
// We'll attempt to find all transactions from start to end height.
|
|
start := base.NewBlockIdentifierFromHeight(startHeight)
|
|
stop := base.NewBlockIdentifierFromHeight(endHeight)
|
|
txns, err := b.wallet.GetTransactions(start, stop, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
txDetails := make([]*lnwallet.TransactionDetail, 0,
|
|
len(txns.MinedTransactions)+len(txns.UnminedTransactions))
|
|
|
|
// For both confirmed and unconfirmed transactions, create a
|
|
// TransactionDetail which re-packages the data returned by the base
|
|
// wallet.
|
|
for _, blockPackage := range txns.MinedTransactions {
|
|
details, err := minedTransactionsToDetails(
|
|
currentHeight, blockPackage, b.netParams,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
txDetails = append(txDetails, details...)
|
|
}
|
|
for _, tx := range txns.UnminedTransactions {
|
|
detail, err := unminedTransactionsToDetail(tx, b.netParams)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
txDetails = append(txDetails, detail)
|
|
}
|
|
|
|
return txDetails, nil
|
|
}
|
|
|
|
// FundPsbt creates a fully populated PSBT packet that contains enough
|
|
// inputs to fund the outputs specified in the passed in packet with the
|
|
// specified fee rate. If there is change left, a change output from the
|
|
// internal wallet is added and the index of the change output is returned.
|
|
// Otherwise no additional output is created and the index -1 is returned.
|
|
//
|
|
// NOTE: If the packet doesn't contain any inputs, coin selection is
|
|
// performed automatically. If the packet does contain any inputs, it is
|
|
// assumed that full coin selection happened externally and no
|
|
// additional inputs are added. If the specified inputs aren't enough to
|
|
// fund the outputs with the given fee rate, an error is returned.
|
|
// No lock lease is acquired for any of the selected/validated inputs.
|
|
// It is in the caller's responsibility to lock the inputs before
|
|
// handing them out.
|
|
//
|
|
// This is a part of the WalletController interface.
|
|
func (b *BtcWallet) FundPsbt(packet *psbt.Packet,
|
|
feeRate chainfee.SatPerKWeight) (int32, error) {
|
|
|
|
// The fee rate is passed in using units of sat/kw, so we'll convert
|
|
// this to sat/KB as the CreateSimpleTx method requires this unit.
|
|
feeSatPerKB := btcutil.Amount(feeRate.FeePerKVByte())
|
|
|
|
// Let the wallet handle coin selection and/or fee estimation based on
|
|
// the partial TX information in the packet.
|
|
return b.wallet.FundPsbt(packet, defaultAccount, feeSatPerKB)
|
|
}
|
|
|
|
// FinalizePsbt expects a partial transaction with all inputs and
|
|
// outputs fully declared and tries to sign all inputs that belong to
|
|
// the wallet. Lnd must be the last signer of the transaction. That
|
|
// means, if there are any unsigned non-witness inputs or inputs without
|
|
// UTXO information attached or inputs without witness data that do not
|
|
// belong to lnd's wallet, this method will fail. If no error is
|
|
// returned, the PSBT is ready to be extracted and the final TX within
|
|
// to be broadcast.
|
|
//
|
|
// NOTE: This method does NOT publish the transaction after it's been
|
|
// finalized successfully.
|
|
//
|
|
// This is a part of the WalletController interface.
|
|
func (b *BtcWallet) FinalizePsbt(packet *psbt.Packet) error {
|
|
return b.wallet.FinalizePsbt(packet)
|
|
}
|
|
|
|
// txSubscriptionClient encapsulates the transaction notification client from
|
|
// the base wallet. Notifications received from the client will be proxied over
|
|
// two distinct channels.
|
|
type txSubscriptionClient struct {
|
|
txClient base.TransactionNotificationsClient
|
|
|
|
confirmed chan *lnwallet.TransactionDetail
|
|
unconfirmed chan *lnwallet.TransactionDetail
|
|
|
|
w *base.Wallet
|
|
|
|
wg sync.WaitGroup
|
|
quit chan struct{}
|
|
}
|
|
|
|
// ConfirmedTransactions returns a channel which will be sent on as new
|
|
// relevant transactions are confirmed.
|
|
//
|
|
// This is part of the TransactionSubscription interface.
|
|
func (t *txSubscriptionClient) ConfirmedTransactions() chan *lnwallet.TransactionDetail {
|
|
return t.confirmed
|
|
}
|
|
|
|
// UnconfirmedTransactions returns a channel which will be sent on as
|
|
// new relevant transactions are seen within the network.
|
|
//
|
|
// This is part of the TransactionSubscription interface.
|
|
func (t *txSubscriptionClient) UnconfirmedTransactions() chan *lnwallet.TransactionDetail {
|
|
return t.unconfirmed
|
|
}
|
|
|
|
// Cancel finalizes the subscription, cleaning up any resources allocated.
|
|
//
|
|
// This is part of the TransactionSubscription interface.
|
|
func (t *txSubscriptionClient) Cancel() {
|
|
close(t.quit)
|
|
t.wg.Wait()
|
|
|
|
t.txClient.Done()
|
|
}
|
|
|
|
// notificationProxier proxies the notifications received by the underlying
|
|
// wallet's notification client to a higher-level TransactionSubscription
|
|
// client.
|
|
func (t *txSubscriptionClient) notificationProxier() {
|
|
out:
|
|
for {
|
|
select {
|
|
case txNtfn := <-t.txClient.C:
|
|
// TODO(roasbeef): handle detached blocks
|
|
currentHeight := t.w.Manager.SyncedTo().Height
|
|
|
|
// Launch a goroutine to re-package and send
|
|
// notifications for any newly confirmed transactions.
|
|
go func() {
|
|
for _, block := range txNtfn.AttachedBlocks {
|
|
details, err := minedTransactionsToDetails(currentHeight, block, t.w.ChainParams())
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
for _, d := range details {
|
|
select {
|
|
case t.confirmed <- d:
|
|
case <-t.quit:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
}()
|
|
|
|
// Launch a goroutine to re-package and send
|
|
// notifications for any newly unconfirmed transactions.
|
|
go func() {
|
|
for _, tx := range txNtfn.UnminedTransactions {
|
|
detail, err := unminedTransactionsToDetail(
|
|
tx, t.w.ChainParams(),
|
|
)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
select {
|
|
case t.unconfirmed <- detail:
|
|
case <-t.quit:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
case <-t.quit:
|
|
break out
|
|
}
|
|
}
|
|
|
|
t.wg.Done()
|
|
}
|
|
|
|
// SubscribeTransactions returns a TransactionSubscription client which
|
|
// is capable of receiving async notifications as new transactions
|
|
// related to the wallet are seen within the network, or found in
|
|
// blocks.
|
|
//
|
|
// This is a part of the WalletController interface.
|
|
func (b *BtcWallet) SubscribeTransactions() (lnwallet.TransactionSubscription, error) {
|
|
walletClient := b.wallet.NtfnServer.TransactionNotifications()
|
|
|
|
txClient := &txSubscriptionClient{
|
|
txClient: walletClient,
|
|
confirmed: make(chan *lnwallet.TransactionDetail),
|
|
unconfirmed: make(chan *lnwallet.TransactionDetail),
|
|
w: b.wallet,
|
|
quit: make(chan struct{}),
|
|
}
|
|
txClient.wg.Add(1)
|
|
go txClient.notificationProxier()
|
|
|
|
return txClient, nil
|
|
}
|
|
|
|
// IsSynced returns a boolean indicating if from the PoV of the wallet, it has
|
|
// fully synced to the current best block in the main chain.
|
|
//
|
|
// This is a part of the WalletController interface.
|
|
func (b *BtcWallet) IsSynced() (bool, int64, error) {
|
|
// Grab the best chain state the wallet is currently aware of.
|
|
syncState := b.wallet.Manager.SyncedTo()
|
|
|
|
// We'll also extract the current best wallet timestamp so the caller
|
|
// can get an idea of where we are in the sync timeline.
|
|
bestTimestamp := syncState.Timestamp.Unix()
|
|
|
|
// Next, query the chain backend to grab the info about the tip of the
|
|
// main chain.
|
|
bestHash, bestHeight, err := b.cfg.ChainSource.GetBestBlock()
|
|
if err != nil {
|
|
return false, 0, err
|
|
}
|
|
|
|
// Make sure the backing chain has been considered synced first.
|
|
if !b.wallet.ChainSynced() {
|
|
bestHeader, err := b.cfg.ChainSource.GetBlockHeader(bestHash)
|
|
if err != nil {
|
|
return false, 0, err
|
|
}
|
|
bestTimestamp = bestHeader.Timestamp.Unix()
|
|
return false, bestTimestamp, nil
|
|
}
|
|
|
|
// If the wallet hasn't yet fully synced to the node's best chain tip,
|
|
// then we're not yet fully synced.
|
|
if syncState.Height < bestHeight {
|
|
return false, bestTimestamp, nil
|
|
}
|
|
|
|
// If the wallet is on par with the current best chain tip, then we
|
|
// still may not yet be synced as the chain backend may still be
|
|
// catching up to the main chain. So we'll grab the block header in
|
|
// order to make a guess based on the current time stamp.
|
|
blockHeader, err := b.cfg.ChainSource.GetBlockHeader(bestHash)
|
|
if err != nil {
|
|
return false, 0, err
|
|
}
|
|
|
|
// If the timestamp on the best header is more than 2 hours in the
|
|
// past, then we're not yet synced.
|
|
minus24Hours := time.Now().Add(-2 * time.Hour)
|
|
if blockHeader.Timestamp.Before(minus24Hours) {
|
|
return false, bestTimestamp, nil
|
|
}
|
|
|
|
return true, bestTimestamp, nil
|
|
}
|
|
|
|
// GetRecoveryInfo returns a boolean indicating whether the wallet is started
|
|
// in recovery mode. It also returns a float64, ranging from 0 to 1,
|
|
// representing the recovery progress made so far.
|
|
//
|
|
// This is a part of the WalletController interface.
|
|
func (b *BtcWallet) GetRecoveryInfo() (bool, float64, error) {
|
|
isRecoveryMode := true
|
|
progress := float64(0)
|
|
|
|
// A zero value in RecoveryWindow indicates there is no trigger of
|
|
// recovery mode.
|
|
if b.cfg.RecoveryWindow == 0 {
|
|
isRecoveryMode = false
|
|
return isRecoveryMode, progress, nil
|
|
}
|
|
|
|
// Query the wallet's birthday block height from db.
|
|
var birthdayBlock waddrmgr.BlockStamp
|
|
err := walletdb.View(b.db, func(tx walletdb.ReadTx) error {
|
|
var err error
|
|
addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
|
|
birthdayBlock, _, err = b.wallet.Manager.BirthdayBlock(addrmgrNs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
// The wallet won't start until the backend is synced, thus the birthday
|
|
// block won't be set and this particular error will be returned. We'll
|
|
// catch this error and return a progress of 0 instead.
|
|
if waddrmgr.IsError(err, waddrmgr.ErrBirthdayBlockNotSet) {
|
|
return isRecoveryMode, progress, nil
|
|
}
|
|
|
|
return isRecoveryMode, progress, err
|
|
}
|
|
|
|
// Grab the best chain state the wallet is currently aware of.
|
|
syncState := b.wallet.Manager.SyncedTo()
|
|
|
|
// Next, query the chain backend to grab the info about the tip of the
|
|
// main chain.
|
|
//
|
|
// NOTE: The actual recovery process is handled by the btcsuite/btcwallet.
|
|
// The process purposefully doesn't update the best height. It might create
|
|
// a small difference between the height queried here and the height used
|
|
// in the recovery process, ie, the bestHeight used here might be greater,
|
|
// showing the recovery being unfinished while it's actually done. However,
|
|
// during a wallet rescan after the recovery, the wallet's synced height
|
|
// will catch up and this won't be an issue.
|
|
_, bestHeight, err := b.cfg.ChainSource.GetBestBlock()
|
|
if err != nil {
|
|
return isRecoveryMode, progress, err
|
|
}
|
|
|
|
// The birthday block height might be greater than the current synced height
|
|
// in a newly restored wallet, and might be greater than the chain tip if a
|
|
// rollback happens. In that case, we will return zero progress here.
|
|
if syncState.Height < birthdayBlock.Height ||
|
|
bestHeight < birthdayBlock.Height {
|
|
return isRecoveryMode, progress, nil
|
|
}
|
|
|
|
// progress is the ratio of the [number of blocks processed] over the [total
|
|
// number of blocks] needed in a recovery mode, ranging from 0 to 1, in
|
|
// which,
|
|
// - total number of blocks is the current chain's best height minus the
|
|
// wallet's birthday height plus 1.
|
|
// - number of blocks processed is the wallet's synced height minus its
|
|
// birthday height plus 1.
|
|
// - If the wallet is born very recently, the bestHeight can be equal to
|
|
// the birthdayBlock.Height, and it will recovery instantly.
|
|
progress = float64(syncState.Height-birthdayBlock.Height+1) /
|
|
float64(bestHeight-birthdayBlock.Height+1)
|
|
|
|
return isRecoveryMode, progress, nil
|
|
}
|