package btcwallet

import (
	"bytes"
	"encoding/hex"
	"errors"
	"fmt"
	"math"
	"sync"
	"time"

	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/btcsuite/btcd/btcutil"
	"github.com/btcsuite/btcd/btcutil/hdkeychain"
	"github.com/btcsuite/btcd/chaincfg"
	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcd/rpcclient"
	"github.com/btcsuite/btcd/txscript"
	"github.com/btcsuite/btcd/wire"
	"github.com/btcsuite/btcwallet/chain"
	"github.com/btcsuite/btcwallet/waddrmgr"
	"github.com/btcsuite/btcwallet/wallet"
	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/davecgh/go-spew/spew"
	"github.com/lightningnetwork/lnd/blockcache"
	"github.com/lightningnetwork/lnd/fn"
	"github.com/lightningnetwork/lnd/input"
	"github.com/lightningnetwork/lnd/keychain"
	"github.com/lightningnetwork/lnd/kvdb"
	"github.com/lightningnetwork/lnd/lnwallet"
	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)

const (
	defaultAccount  = uint32(waddrmgr.DefaultAccountNum)
	importedAccount = uint32(waddrmgr.ImportedAddrAccount)

	// dryRunImportAccountNumAddrs represents the number of addresses we'll
	// derive for an imported account's external and internal branch when a
	// dry run is attempted.
	dryRunImportAccountNumAddrs = 5

	// UnconfirmedHeight is the special case end height that is used to
	// obtain unconfirmed transactions from ListTransactionDetails.
	UnconfirmedHeight int32 = -1

	// walletMetaBucket is used to store wallet metadata.
	walletMetaBucket = "lnwallet"

	// walletReadyKey is used to indicate that the wallet has been
	// initialized.
	walletReadyKey = "ready"
)

var (
	// waddrmgrNamespaceKey is the namespace key that the waddrmgr state is
	// stored within the top-level walletdb buckets of btcwallet.
	waddrmgrNamespaceKey = []byte("waddrmgr")

	// wtxmgrNamespaceKey is the namespace key that the wtxmgr state is
	// stored within the top-level waleltdb buckets of btcwallet.
	wtxmgrNamespaceKey = []byte("wtxmgr")

	// 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,
	}

	// LndDefaultKeyScopes is the list of default key scopes that lnd adds
	// to its wallet.
	LndDefaultKeyScopes = []waddrmgr.KeyScope{
		waddrmgr.KeyScopeBIP0049Plus,
		waddrmgr.KeyScopeBIP0084,
		waddrmgr.KeyScopeBIP0086,
	}

	// errNoImportedAddrGen is an error returned when a new address is
	// requested for the default imported account within the wallet.
	errNoImportedAddrGen = errors.New("addresses cannot be generated for " +
		"the default imported account")
)

// 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

	blockCache *blockcache.BlockCache

	*input.MusigSessionManager
}

// 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, blockCache *blockcache.BlockCache) (*BtcWallet, error) {
	// 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, err := NewWalletLoader(
			cfg.NetParams, cfg.RecoveryWindow, cfg.LoaderOptions...,
		)
		if err != nil {
			return nil, err
		}
		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
			}
		}
	}

	finalWallet := &BtcWallet{
		cfg:           &cfg,
		wallet:        wallet,
		db:            wallet.Database(),
		chain:         cfg.ChainSource,
		netParams:     cfg.NetParams,
		chainKeyScope: chainKeyScope,
		blockCache:    blockCache,
	}

	finalWallet.MusigSessionManager = input.NewMusigSessionManager(
		finalWallet.fetchPrivKey,
	)

	return finalWallet, nil
}

// loaderCfg holds optional wallet loader configuration.
type loaderCfg struct {
	dbDirPath      string
	noFreelistSync bool
	dbTimeout      time.Duration
	useLocalDB     bool
	externalDB     kvdb.Backend
}

// LoaderOption is a functional option to update the optional loader config.
type LoaderOption func(*loaderCfg)

// LoaderWithLocalWalletDB configures the wallet loader to use the local db.
func LoaderWithLocalWalletDB(dbDirPath string, noFreelistSync bool,
	dbTimeout time.Duration) LoaderOption {

	return func(cfg *loaderCfg) {
		cfg.dbDirPath = dbDirPath
		cfg.noFreelistSync = noFreelistSync
		cfg.dbTimeout = dbTimeout
		cfg.useLocalDB = true
	}
}

// LoaderWithExternalWalletDB configures the wallet loadr to use an external db.
func LoaderWithExternalWalletDB(db kvdb.Backend) LoaderOption {
	return func(cfg *loaderCfg) {
		cfg.externalDB = db
	}
}

// NewWalletLoader constructs a wallet loader.
func NewWalletLoader(chainParams *chaincfg.Params, recoveryWindow uint32,
	opts ...LoaderOption) (*base.Loader, error) {

	cfg := &loaderCfg{}

	// Apply all functional options.
	for _, o := range opts {
		o(cfg)
	}

	if cfg.externalDB != nil && cfg.useLocalDB {
		return nil, fmt.Errorf("wallet can either be in the local or " +
			"an external db")
	}

	if cfg.externalDB != nil {
		loader, err := base.NewLoaderWithDB(
			chainParams, recoveryWindow, cfg.externalDB,
			func() (bool, error) {
				return externalWalletExists(cfg.externalDB)
			},
		)
		if err != nil {
			return nil, err
		}

		// Decorate wallet db with out own key such that we
		// can always check whether the wallet exists or not.
		loader.OnWalletCreated(onWalletCreated)
		return loader, nil
	}

	return base.NewLoader(
		chainParams, cfg.dbDirPath, cfg.noFreelistSync,
		cfg.dbTimeout, recoveryWindow,
	), nil
}

// externalWalletExists is a helper function that we use to template btcwallet's
// Loader in order to be able check if the wallet database has been initialized
// in an external DB.
func externalWalletExists(db kvdb.Backend) (bool, error) {
	exists := false
	err := kvdb.View(db, func(tx kvdb.RTx) error {
		metaBucket := tx.ReadBucket([]byte(walletMetaBucket))
		if metaBucket != nil {
			walletReady := metaBucket.Get([]byte(walletReadyKey))
			exists = string(walletReady) == walletReadyKey
		}

		return nil
	}, func() {})

	return exists, err
}

// onWalletCreated is executed when btcwallet creates the wallet the first time.
func onWalletCreated(tx kvdb.RwTx) error {
	metaBucket, err := tx.CreateTopLevelBucket([]byte(walletMetaBucket))
	if err != nil {
		return err
	}

	return metaBucket.Put([]byte(walletReadyKey), []byte(walletReadyKey))
}

// 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 {
	// Is the wallet (according to its database) currently watch-only
	// already? If it is, we won't need to convert it later.
	walletIsWatchOnly := b.wallet.Manager.WatchOnly()

	// If the wallet is watch-only, but we don't expect it to be, then we
	// are in an unexpected state and cannot continue.
	if walletIsWatchOnly && !b.cfg.WatchOnly {
		return fmt.Errorf("wallet is watch-only but we expect it " +
			"not to be; check if remote signing was disabled by " +
			"accident")
	}

	// 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 this is a watch-only wallet, we don't have any private
	// keys and therefore unlocking is not necessary.
	if !walletIsWatchOnly {
		if err := b.wallet.Unlock(b.cfg.PrivatePass, nil); err != nil {
			return err
		}

		// If the wallet isn't about to be converted, we need to inform
		// the user that this wallet still contains all private key
		// material and that they need to migrate the existing wallet.
		if b.cfg.WatchOnly && !b.cfg.MigrateWatchOnly {
			log.Warnf("Wallet is expected to be in watch-only " +
				"mode but hasn't been migrated to watch-only " +
				"yet, it still contains private keys; " +
				"consider turning on the watch-only wallet " +
				"migration in remote signing mode")
		}
	}

	// Because we might add new "default" key scopes over time, they are
	// created correctly for new wallets. Existing wallets don't
	// automatically add them, we need to do that manually now.
	for _, scope := range LndDefaultKeyScopes {
		_, err := b.wallet.Manager.FetchScopedKeyManager(scope)
		if waddrmgr.IsError(err, waddrmgr.ErrScopeNotFound) {
			// The default scope wasn't found, that probably means
			// it was added recently and older wallets don't know it
			// yet. Let's add it now.
			addrSchema := waddrmgr.ScopeAddrMap[scope]
			err := walletdb.Update(
				b.db, func(tx walletdb.ReadWriteTx) error {
					addrmgrNs := tx.ReadWriteBucket(
						waddrmgrNamespaceKey,
					)

					_, err := b.wallet.Manager.NewScopedKeyManager(
						addrmgrNs, scope, addrSchema,
					)
					return err
				},
			)
			if err != nil {
				return err
			}
		}
	}

	scope, 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)

			scope, err = b.wallet.Manager.NewScopedKeyManager(
				addrmgrNs, b.chainKeyScope, lightningAddrSchema,
			)
			return err
		})
		if err != nil {
			return err
		}
	}

	// Now that the wallet is unlocked, we'll go ahead and make sure we
	// create accounts for all the key families we're going to use. This
	// will make it possible to list all the account/family xpubs in the
	// wallet list RPC.
	err = walletdb.Update(b.db, func(tx walletdb.ReadWriteTx) error {
		addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey)

		// Generate all accounts that we could ever need. This includes
		// all lnd key families as well as some key families used in
		// external liquidity tools.
		for keyFam := uint32(1); keyFam <= 255; keyFam++ {
			// Otherwise, we'll check if the account already exists,
			// if so, we can once again bail early.
			_, err := scope.AccountName(addrmgrNs, keyFam)
			if err == nil {
				continue
			}

			// If we reach this point, then the account hasn't yet
			// been created, so we'll need to create it before we
			// can proceed.
			err = scope.NewRawAccount(addrmgrNs, keyFam)
			if err != nil {
				return err
			}
		}

		// If this is the first startup with remote signing and wallet
		// migration turned on and the wallet wasn't previously
		// migrated, we can do that now that we made sure all accounts
		// that we need were derived correctly.
		if !walletIsWatchOnly && b.cfg.WatchOnly &&
			b.cfg.MigrateWatchOnly {

			log.Infof("Migrating wallet to watch-only mode, " +
				"purging all private key material")

			ns := tx.ReadWriteBucket(waddrmgrNamespaceKey)
			err = b.wallet.Manager.ConvertToWatchingOnly(ns)
			if err != nil {
				return err
			}
		}

		return nil
	})
	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. The account parameter serves as a filter to retrieve the balance
// for a specific account. When empty, the confirmed balance of all wallet
// accounts is returned.
//
// This is a part of the WalletController interface.
func (b *BtcWallet) ConfirmedBalance(confs int32,
	accountFilter string) (btcutil.Amount, error) {

	var balance btcutil.Amount

	witnessOutputs, err := b.ListUnspentWitness(
		confs, math.MaxInt32, accountFilter,
	)
	if err != nil {
		return 0, err
	}

	for _, witnessOutput := range witnessOutputs {
		balance += witnessOutput.Value
	}

	return balance, nil
}

// keyScopeForAccountAddr determines the appropriate key scope of an account
// based on its name/address type.
func (b *BtcWallet) keyScopeForAccountAddr(accountName string,
	addrType lnwallet.AddressType) (waddrmgr.KeyScope, uint32, error) {

	// Map the requested address type to its key scope.
	var addrKeyScope waddrmgr.KeyScope
	switch addrType {
	case lnwallet.WitnessPubKey:
		addrKeyScope = waddrmgr.KeyScopeBIP0084
	case lnwallet.NestedWitnessPubKey:
		addrKeyScope = waddrmgr.KeyScopeBIP0049Plus
	case lnwallet.TaprootPubkey:
		addrKeyScope = waddrmgr.KeyScopeBIP0086
	default:
		return waddrmgr.KeyScope{}, 0,
			fmt.Errorf("unknown address type")
	}

	// The default account spans across multiple key scopes, so the
	// requested address type should already be valid for this account.
	if accountName == lnwallet.DefaultAccountName {
		return addrKeyScope, defaultAccount, nil
	}

	// Otherwise, look up the custom account and if it supports the given
	// key scope.
	accountNumber, err := b.wallet.AccountNumber(addrKeyScope, accountName)
	if err != nil {
		return waddrmgr.KeyScope{}, 0, err
	}

	return addrKeyScope, accountNumber, 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. The account parameter must be non-empty as it determines which
// account the address should be generated from.
//
// This is a part of the WalletController interface.
func (b *BtcWallet) NewAddress(t lnwallet.AddressType, change bool,
	accountName string) (btcutil.Address, error) {

	// Addresses cannot be derived from the catch-all imported accounts.
	if accountName == waddrmgr.ImportedAddrAccountName {
		return nil, errNoImportedAddrGen
	}

	keyScope, account, err := b.keyScopeForAccountAddr(accountName, t)
	if err != nil {
		return nil, err
	}

	if change {
		return b.wallet.NewChangeAddress(account, keyScope)
	}
	return b.wallet.NewAddress(account, 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. The account parameter must be non-empty as it determines
// which account the address should be generated from.
func (b *BtcWallet) LastUnusedAddress(addrType lnwallet.AddressType,
	accountName string) (btcutil.Address, error) {

	// Addresses cannot be derived from the catch-all imported accounts.
	if accountName == waddrmgr.ImportedAddrAccountName {
		return nil, errNoImportedAddrGen
	}

	keyScope, account, err := b.keyScopeForAccountAddr(accountName, addrType)
	if err != nil {
		return nil, err
	}

	return b.wallet.CurrentAddress(account, 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)
}

// AddressInfo returns the information about an address, if it's known to this
// wallet.
//
// NOTE: This is a part of the WalletController interface.
func (b *BtcWallet) AddressInfo(a btcutil.Address) (waddrmgr.ManagedAddress,
	error) {

	return b.wallet.AddressInfo(a)
}

// ListAccounts retrieves all accounts belonging to the wallet by default. A
// name and key scope filter can be provided to filter through all of the wallet
// accounts and return only those matching.
//
// This is a part of the WalletController interface.
func (b *BtcWallet) ListAccounts(name string,
	keyScope *waddrmgr.KeyScope) ([]*waddrmgr.AccountProperties, error) {

	var res []*waddrmgr.AccountProperties
	switch {
	// If both the name and key scope filters were provided, we'll return
	// the existing account matching those.
	case name != "" && keyScope != nil:
		account, err := b.wallet.AccountPropertiesByName(*keyScope, name)
		if err != nil {
			return nil, err
		}
		res = append(res, account)

	// Only the name filter was provided.
	case name != "" && keyScope == nil:
		// If the name corresponds to the default or imported accounts,
		// we'll return them for all our supported key scopes.
		if name == lnwallet.DefaultAccountName ||
			name == waddrmgr.ImportedAddrAccountName {

			for _, defaultScope := range LndDefaultKeyScopes {
				a, err := b.wallet.AccountPropertiesByName(
					defaultScope, name,
				)
				if err != nil {
					return nil, err
				}
				res = append(res, a)
			}

			break
		}

		// In theory, there should be only one custom account for the
		// given name. However, due to a lack of check, users could
		// create custom accounts with various key scopes. This
		// behaviour has been fixed but, we return all potential custom
		// accounts with the given name.
		for _, scope := range waddrmgr.DefaultKeyScopes {
			a, err := b.wallet.AccountPropertiesByName(
				scope, name,
			)
			switch {
			case waddrmgr.IsError(err, waddrmgr.ErrAccountNotFound):
				continue

			// In the specific case of a wallet initialized only by
			// importing account xpubs (watch only wallets), it is
			// possible that some keyscopes will be 'unknown' by the
			// wallet (depending on the xpubs given to initialize
			// it). If the keyscope is not found, just skip it.
			case waddrmgr.IsError(err, waddrmgr.ErrScopeNotFound):
				continue

			case err != nil:
				return nil, err
			}

			res = append(res, a)
		}
		if len(res) == 0 {
			return nil, newAccountNotFoundError(name)
		}

	// Only the key scope filter was provided, so we'll return all accounts
	// matching it.
	case name == "" && keyScope != nil:
		accounts, err := b.wallet.Accounts(*keyScope)
		if err != nil {
			return nil, err
		}
		for _, account := range accounts.Accounts {
			account := account
			res = append(res, &account.AccountProperties)
		}

	// Neither of the filters were provided, so return all accounts for our
	// supported key scopes.
	case name == "" && keyScope == nil:
		for _, defaultScope := range LndDefaultKeyScopes {
			accounts, err := b.wallet.Accounts(defaultScope)
			if err != nil {
				return nil, err
			}
			for _, account := range accounts.Accounts {
				account := account
				res = append(res, &account.AccountProperties)
			}
		}

		accounts, err := b.wallet.Accounts(waddrmgr.KeyScope{
			Purpose: keychain.BIP0043Purpose,
			Coin:    b.cfg.CoinType,
		})
		if err != nil {
			return nil, err
		}
		for _, account := range accounts.Accounts {
			account := account
			res = append(res, &account.AccountProperties)
		}
	}

	return res, nil
}

// newAccountNotFoundError returns an error indicating that the manager didn't
// find the specific account. This error is used to be compatible with the old
// 'LookupAccount' behaviour previously used.
func newAccountNotFoundError(name string) error {
	str := fmt.Sprintf("account name '%s' not found", name)

	return waddrmgr.ManagerError{
		ErrorCode:   waddrmgr.ErrAccountNotFound,
		Description: str,
	}
}

// RequiredReserve returns the minimum amount of satoshis that should be
// kept in the wallet in order to fee bump anchor channels if necessary.
// The value scales with the number of public anchor channels but is
// capped at a maximum.
func (b *BtcWallet) RequiredReserve(
	numAnchorChans uint32) btcutil.Amount {

	anchorChanReservedValue := lnwallet.AnchorChanReservedValue
	reserved := btcutil.Amount(numAnchorChans) * anchorChanReservedValue
	if reserved > lnwallet.MaxAnchorChanReservedValue {
		reserved = lnwallet.MaxAnchorChanReservedValue
	}

	return reserved
}

// ListAddresses retrieves all the addresses along with their balance. An
// account name filter can be provided to filter through all of the
// wallet accounts and return the addresses of only those matching.
//
// This is a part of the WalletController interface.
func (b *BtcWallet) ListAddresses(name string,
	showCustomAccounts bool) (lnwallet.AccountAddressMap, error) {

	accounts, err := b.ListAccounts(name, nil)
	if err != nil {
		return nil, err
	}

	addresses := make(lnwallet.AccountAddressMap)
	addressBalance := make(map[string]btcutil.Amount)

	// Retrieve all the unspent ouputs.
	outputs, err := b.wallet.ListUnspent(0, math.MaxInt32, "")
	if err != nil {
		return nil, err
	}

	// Calculate the total balance of each address.
	for _, output := range outputs {
		amount, err := btcutil.NewAmount(output.Amount)
		if err != nil {
			return nil, err
		}

		addressBalance[output.Address] += amount
	}

	for _, accntDetails := range accounts {
		accntScope := accntDetails.KeyScope
		scopedMgr, err := b.wallet.Manager.FetchScopedKeyManager(
			accntScope,
		)
		if err != nil {
			return nil, err
		}

		var managedAddrs []waddrmgr.ManagedAddress
		err = walletdb.View(
			b.wallet.Database(), func(tx walletdb.ReadTx) error {
				managedAddrs = nil
				addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey)
				return scopedMgr.ForEachAccountAddress(
					addrmgrNs, accntDetails.AccountNumber,
					func(a waddrmgr.ManagedAddress) error {
						managedAddrs = append(
							managedAddrs, a,
						)

						return nil
					},
				)
			},
		)
		if err != nil {
			return nil, err
		}

		// Only consider those accounts which have addresses.
		if len(managedAddrs) == 0 {
			continue
		}

		// All the lnd internal/custom keys for channels and other
		// functionality are derived from the same scope. Since they
		// aren't really used as addresses and will never have an
		// on-chain balance, we'll want to show the public key instead.
		isLndCustom := accntScope.Purpose == keychain.BIP0043Purpose
		addressProperties := make(
			[]lnwallet.AddressProperty, len(managedAddrs),
		)

		for idx, managedAddr := range managedAddrs {
			addr := managedAddr.Address()
			addressString := addr.String()

			// Hex-encode the compressed public key for custom lnd
			// keys, addresses don't make a lot of sense.
			var (
				pubKey         *btcec.PublicKey
				derivationPath string
			)
			pka, ok := managedAddr.(waddrmgr.ManagedPubKeyAddress)
			if ok {
				pubKey = pka.PubKey()

				// There can be an error in two cases: Either
				// the address isn't a managed pubkey address,
				// which we already checked above, or the
				// address is imported in which case we don't
				// know the derivation path, and it will just be
				// empty anyway.
				_, _, derivationPath, _ =
					Bip32DerivationFromAddress(pka)
			}
			if pubKey != nil && isLndCustom {
				addressString = hex.EncodeToString(
					pubKey.SerializeCompressed(),
				)
			}

			addressProperties[idx] = lnwallet.AddressProperty{
				Address:        addressString,
				Internal:       managedAddr.Internal(),
				Balance:        addressBalance[addressString],
				PublicKey:      pubKey,
				DerivationPath: derivationPath,
			}
		}

		if accntScope.Purpose != keychain.BIP0043Purpose ||
			showCustomAccounts {

			addresses[accntDetails] = addressProperties
		}
	}

	return addresses, nil
}

// ImportAccount imports an account backed by an account extended public key.
// The master key fingerprint denotes the fingerprint of the root key
// corresponding to the account public key (also known as the key with
// derivation path m/). This may be required by some hardware wallets for proper
// identification and signing.
//
// The address type can usually be inferred from the key's version, but may be
// required for certain keys to map them into the proper scope.
//
// For custom accounts, we will first check if there is no account with the same
// name (even with a different key scope). No custom account should have various
// key scopes as it will result in non-deterministic behaviour.
//
// For BIP-0044 keys, an address type must be specified as we intend to not
// support importing BIP-0044 keys into the wallet using the legacy
// pay-to-pubkey-hash (P2PKH) scheme. A nested witness address type will force
// the standard BIP-0049 derivation scheme, while a witness address type will
// force the standard BIP-0084 derivation scheme.
//
// For BIP-0049 keys, an address type must also be specified to make a
// distinction between the standard BIP-0049 address schema (nested witness
// pubkeys everywhere) and our own BIP-0049Plus address schema (nested pubkeys
// externally, witness pubkeys internally).
//
// This is a part of the WalletController interface.
func (b *BtcWallet) ImportAccount(name string, accountPubKey *hdkeychain.ExtendedKey,
	masterKeyFingerprint uint32, addrType *waddrmgr.AddressType,
	dryRun bool) (*waddrmgr.AccountProperties, []btcutil.Address,
	[]btcutil.Address, error) {

	// For custom accounts, we first check if there is no existing account
	// with the same name.
	if name != lnwallet.DefaultAccountName &&
		name != waddrmgr.ImportedAddrAccountName {

		_, err := b.ListAccounts(name, nil)
		if err == nil {
			return nil, nil, nil,
				fmt.Errorf("account '%s' already exists",
					name)
		}
		if !waddrmgr.IsError(err, waddrmgr.ErrAccountNotFound) {
			return nil, nil, nil, err
		}
	}

	if !dryRun {
		accountProps, err := b.wallet.ImportAccount(
			name, accountPubKey, masterKeyFingerprint, addrType,
		)
		if err != nil {
			return nil, nil, nil, err
		}
		return accountProps, nil, nil, nil
	}

	// Derive addresses from both the external and internal branches of the
	// account. There's no risk of address inflation as this is only done
	// for dry runs.
	accountProps, extAddrs, intAddrs, err := b.wallet.ImportAccountDryRun(
		name, accountPubKey, masterKeyFingerprint, addrType,
		dryRunImportAccountNumAddrs,
	)
	if err != nil {
		return nil, nil, nil, err
	}

	externalAddrs := make([]btcutil.Address, len(extAddrs))
	for i := 0; i < len(extAddrs); i++ {
		externalAddrs[i] = extAddrs[i].Address()
	}

	internalAddrs := make([]btcutil.Address, len(intAddrs))
	for i := 0; i < len(intAddrs); i++ {
		internalAddrs[i] = intAddrs[i].Address()
	}

	return accountProps, externalAddrs, internalAddrs, nil
}

// ImportPublicKey imports a single derived public key into the wallet. The
// address type can usually be inferred from the key's version, but in the case
// of legacy versions (xpub, tpub), an address type must be specified as we
// intend to not support importing BIP-44 keys into the wallet using the legacy
// pay-to-pubkey-hash (P2PKH) scheme.
//
// This is a part of the WalletController interface.
func (b *BtcWallet) ImportPublicKey(pubKey *btcec.PublicKey,
	addrType waddrmgr.AddressType) error {

	return b.wallet.ImportPublicKey(pubKey, addrType)
}

// ImportTaprootScript imports a user-provided taproot script into the address
// manager. The imported script will act as a pay-to-taproot address.
func (b *BtcWallet) ImportTaprootScript(scope waddrmgr.KeyScope,
	tapscript *waddrmgr.Tapscript) (waddrmgr.ManagedAddress, error) {

	// We want to be able to import script addresses into a watch-only
	// wallet, which is only possible if we don't encrypt the script with
	// the private key encryption key. By specifying the script as being
	// "not secret", we can also decrypt the script in a watch-only wallet.
	const isSecretScript = false

	// Currently, only v1 (Taproot) scripts are supported. We don't even
	// know what a v2 witness version would look like at this point.
	const witnessVersionTaproot byte = 1

	return b.wallet.ImportTaprootScript(
		scope, tapscript, nil, witnessVersionTaproot, isSecretScript,
	)
}

// 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(inputs fn.Set[wire.OutPoint],
	outputs []*wire.TxOut, feeRate chainfee.SatPerKWeight,
	minConfs int32, label string,
	strategy base.CoinSelectionStrategy) (*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 minConfs.
	if minConfs < 0 {
		return nil, lnwallet.ErrInvalidMinconf
	}

	// Use selected UTXOs if specified, otherwise default selection.
	if len(inputs) != 0 {
		return b.wallet.SendOutputsWithInput(
			outputs, nil, defaultAccount, minConfs, feeSatPerKB,
			strategy, label, inputs.ToSlice(),
		)
	}

	return b.wallet.SendOutputs(
		outputs, nil, defaultAccount, minConfs, feeSatPerKB,
		strategy, 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(inputs fn.Set[wire.OutPoint],
	outputs []*wire.TxOut, feeRate chainfee.SatPerKWeight, minConfs int32,
	strategy base.CoinSelectionStrategy, 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
	}

	// Sanity check minConfs.
	if minConfs < 0 {
		return nil, lnwallet.ErrInvalidMinconf
	}

	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
		}
	}

	// Add the optional inputs to the transaction.
	optFunc := wallet.WithCustomSelectUtxos(inputs.ToSlice())

	return b.wallet.CreateSimpleTx(
		nil, defaultAccount, outputs, minConfs, feeSatPerKB,
		strategy, dryRun, []wallet.TxCreateOption{optFunc}...,
	)
}

// 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,
	duration time.Duration) (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
	}

	lockedUntil, err := b.wallet.LeaseOutput(id, op, duration)
	if err != nil {
		return time.Time{}, err
	}

	return lockedUntil, nil
}

// ListLeasedOutputs returns a list of all currently locked outputs.
func (b *BtcWallet) ListLeasedOutputs() ([]*base.ListLeasedOutputResult,
	error) {

	return b.wallet.ListLeasedOutputs()
}

// 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 all unspent outputs which are version 0 witness
// programs. The 'minConfs' and 'maxConfs' parameters indicate the minimum
// and maximum number of confirmations an output needs in order to be returned
// by this method. Passing -1 as 'minConfs' indicates that even unconfirmed
// outputs should be returned. Using MaxInt32 as 'maxConfs' implies returning
// all outputs with at least 'minConfs'. The account parameter serves as a
// filter to retrieve the unspent outputs for a specific account.  When empty,
// the unspent outputs of all wallet accounts are returned.
//
// 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,
	accountFilter string) ([]*lnwallet.Utxo, error) {

	// First, grab all the unfiltered currently unspent outputs.
	unspentOutputs, err := b.wallet.ListUnspent(
		minConfs, maxConfs, accountFilter,
	)
	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
		} else if txscript.IsPayToTaproot(pkScript) {
			addressType = lnwallet.TaprootPubkey
		}

		if addressType == lnwallet.WitnessPubKey ||
			addressType == lnwallet.NestedWitnessPubKey ||
			addressType == lnwallet.TaprootPubkey {

			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
}

// mapRpcclientError maps an error from the `btcwallet/chain` package to
// defined error in this package.
//
// NOTE: we are mapping the errors returned from `sendrawtransaction` RPC or
// the reject reason from `testmempoolaccept` RPC.
func mapRpcclientError(err error) error {
	// If we failed to publish the transaction, check whether we got an
	// error of known type.
	switch {
	// If the wallet reports a double spend, convert it to our internal
	// ErrDoubleSpend and return.
	case errors.Is(err, chain.ErrMempoolConflict),
		errors.Is(err, chain.ErrMissingInputs),
		errors.Is(err, chain.ErrTxAlreadyKnown),
		errors.Is(err, chain.ErrTxAlreadyConfirmed):

		return lnwallet.ErrDoubleSpend

	// If the wallet reports that fee requirements for accepting the tx
	// into mempool are not met, convert it to our internal ErrMempoolFee
	// and return.
	case errors.Is(err, chain.ErrMempoolMinFeeNotMet):
		return fmt.Errorf("%w: %v", lnwallet.ErrMempoolFee, err.Error())
	}

	return err
}

// 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
// and mapped to the wallet's internal error types. 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 {
	// For neutrino backend there's no mempool, so we return early by
	// publishing the transaction.
	if b.chain.BackEnd() == "neutrino" {
		err := b.wallet.PublishTransaction(tx, label)

		return mapRpcclientError(err)
	}

	// For non-neutrino nodes, we will first check whether the transaction
	// can be accepted by the mempool.
	// Use a max feerate of 0 means the default value will be used when
	// testing mempool acceptance. The default max feerate is 0.10 BTC/kvb,
	// or 10,000 sat/vb.
	results, err := b.chain.TestMempoolAccept([]*wire.MsgTx{tx}, 0)
	if err != nil {
		// If the chain backend doesn't support the mempool acceptance
		// test RPC, we'll just attempt to publish the transaction.
		if errors.Is(err, rpcclient.ErrBackendVersion) {
			log.Warnf("TestMempoolAccept not supported by "+
				"backend, consider upgrading %s to a newer "+
				"version", b.chain.BackEnd())

			err := b.wallet.PublishTransaction(tx, label)

			return mapRpcclientError(err)
		}

		return err
	}

	// Sanity check that the expected single result is returned.
	if len(results) != 1 {
		return fmt.Errorf("expected 1 result from TestMempoolAccept, "+
			"instead got %v", len(results))
	}

	result := results[0]
	log.Debugf("TestMempoolAccept result: %s", spew.Sdump(result))

	// Once mempool check passed, we can publish the transaction.
	if result.Allowed {
		err = b.wallet.PublishTransaction(tx, label)

		return mapRpcclientError(err)
	}

	// If the check failed, there's no need to publish it. We'll handle the
	// error and return.
	log.Warnf("Transaction %v not accepted by mempool: %v",
		tx.TxHash(), result.RejectReason)

	// We need to use the string to create an error type and map it to a
	// btcwallet error.
	err = b.chain.MapRPCErr(errors.New(result.RejectReason))

	//nolint:lll
	// These two errors are ignored inside `PublishTransaction`:
	// https://github.com/btcsuite/btcwallet/blob/master/wallet/wallet.go#L3763
	// To keep our current behavior, we need to ignore the same errors
	// returned from TestMempoolAccept.
	//
	// TODO(yy): since `LightningWallet.PublishTransaction` always publish
	// the same tx twice, we'd always get ErrTxAlreadyInMempool. We should
	// instead create a new rebroadcaster that monitors the mempool, and
	// only rebroadcast when the tx is evicted. This way we don't need to
	// broadcast twice, and can instead return these errors here.
	switch {
	// NOTE: In addition to ignoring these errors, we need to call
	// `PublishTransaction` again because we need to mark the label in the
	// wallet. We can remove this exception once we have the above TODO
	// fixed.
	case errors.Is(err, chain.ErrTxAlreadyInMempool),
		errors.Is(err, chain.ErrTxAlreadyKnown),
		errors.Is(err, chain.ErrTxAlreadyConfirmed):

		err := b.wallet.PublishTransaction(tx, label)
		return mapRpcclientError(err)
	}

	return mapRpcclientError(err)
}

// 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
}

// getPreviousOutpoints is a helper function which gets the previous
// outpoints of a transaction.
func getPreviousOutpoints(wireTx *wire.MsgTx,
	myInputs []base.TransactionSummaryInput) []lnwallet.PreviousOutPoint {

	// isOurOutput is a map containing the output indices
	// controlled by the wallet.
	// Note: We make use of the information in `myInputs` provided
	// by the `wallet.TransactionSummary` structure that holds
	// information only if the input/previous_output is controlled by the wallet.
	isOurOutput := make(map[uint32]bool, len(myInputs))
	for _, myInput := range myInputs {
		isOurOutput[myInput.Index] = true
	}

	previousOutpoints := make([]lnwallet.PreviousOutPoint, len(wireTx.TxIn))
	for idx, txIn := range wireTx.TxIn {
		previousOutpoints[idx] = lnwallet.PreviousOutPoint{
			OutPoint:    txIn.PreviousOutPoint.String(),
			IsOurOutput: isOurOutput[uint32(idx)],
		}
	}

	return previousOutpoints
}

// GetTransactionDetails returns details of a transaction given its
// transaction hash.
func (b *BtcWallet) GetTransactionDetails(
	txHash *chainhash.Hash) (*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
	tx, err := b.wallet.GetTransaction(*txHash)
	if err != nil {
		return nil, err
	}

	// For both confirmed and unconfirmed transactions, create a
	// TransactionDetail which re-packages the data returned by the base
	// wallet.
	if tx.Confirmations > 0 {
		txDetails, err := minedTransactionsToDetails(
			currentHeight,
			base.Block{
				Transactions: []base.TransactionSummary{
					tx.Summary,
				},
				Hash:      tx.BlockHash,
				Height:    tx.Height,
				Timestamp: tx.Summary.Timestamp},
			b.netParams,
		)
		if err != nil {
			return nil, err
		}

		return txDetails[0], nil
	}

	return unminedTransactionsToDetail(tx.Summary, b.netParams)
}

// 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
		}

		// isOurAddress is a map containing the output indices
		// controlled by the wallet.
		// Note: We make use of the information in `MyOutputs` provided
		// by the `wallet.TransactionSummary` structure that holds
		// information only if the output is controlled by the wallet.
		isOurAddress := make(map[int]bool, len(tx.MyOutputs))
		for _, o := range tx.MyOutputs {
			isOurAddress[int(o.Index)] = true
		}

		var outputDetails []lnwallet.OutputDetail
		for i, txOut := range wireTx.TxOut {
			var addresses []btcutil.Address
			sc, outAddresses, _, err := txscript.ExtractPkScriptAddrs(
				txOut.PkScript, chainParams,
			)
			if err == nil {
				// Add supported addresses.
				addresses = outAddresses
			}

			outputDetails = append(outputDetails, lnwallet.OutputDetail{
				OutputType:   sc,
				Addresses:    addresses,
				PkScript:     txOut.PkScript,
				OutputIndex:  i,
				Value:        btcutil.Amount(txOut.Value),
				IsOurAddress: isOurAddress[i],
			})
		}

		previousOutpoints := getPreviousOutpoints(wireTx, tx.MyInputs)

		txDetail := &lnwallet.TransactionDetail{
			Hash:              *tx.Hash,
			NumConfirmations:  currentHeight - block.Height + 1,
			BlockHash:         block.Hash,
			BlockHeight:       block.Height,
			Timestamp:         block.Timestamp,
			TotalFees:         int64(tx.Fee),
			OutputDetails:     outputDetails,
			RawTx:             tx.Transaction,
			Label:             tx.Label,
			PreviousOutpoints: previousOutpoints,
		}

		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
	}

	// isOurAddress is a map containing the output indices controlled by
	// the wallet.
	// Note: We make use of the information in `MyOutputs` provided
	// by the `wallet.TransactionSummary` structure that holds information
	// only if the output is controlled by the wallet.
	isOurAddress := make(map[int]bool, len(summary.MyOutputs))
	for _, o := range summary.MyOutputs {
		isOurAddress[int(o.Index)] = true
	}

	var outputDetails []lnwallet.OutputDetail
	for i, txOut := range wireTx.TxOut {
		var addresses []btcutil.Address
		sc, outAddresses, _, err := txscript.ExtractPkScriptAddrs(
			txOut.PkScript, chainParams,
		)
		if err == nil {
			// Add supported addresses.
			addresses = outAddresses
		}

		outputDetails = append(outputDetails, lnwallet.OutputDetail{
			OutputType:   sc,
			Addresses:    addresses,
			PkScript:     txOut.PkScript,
			OutputIndex:  i,
			Value:        btcutil.Amount(txOut.Value),
			IsOurAddress: isOurAddress[i],
		})
	}

	previousOutpoints := getPreviousOutpoints(wireTx, summary.MyInputs)

	txDetail := &lnwallet.TransactionDetail{
		Hash:              *summary.Hash,
		TotalFees:         int64(summary.Fee),
		Timestamp:         summary.Timestamp,
		OutputDetails:     outputDetails,
		RawTx:             summary.Transaction,
		Label:             summary.Label,
		PreviousOutpoints: previousOutpoints,
	}

	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 over [startHeight;endHeight]. If start height is greater than
// end height, the transactions will be retrieved in reverse order. To include
// unconfirmed transactions, endHeight should be set to the special value -1.
// This will return transactions from the tip of the chain until the start
// height (inclusive) and unconfirmed transactions. The account parameter serves
// as a filter to retrieve the transactions relevant to a specific account. When
// empty, transactions of all wallet accounts are returned.
//
// This is a part of the WalletController interface.
func (b *BtcWallet) ListTransactionDetails(startHeight, endHeight int32,
	accountFilter string) ([]*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, accountFilter, 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
}

// 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() {
	defer t.wg.Done()

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.
			//nolint:lll
			go func(txNtfn *base.TransactionNotifications) {
				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
						}
					}
				}
			}(txNtfn)

			// Launch a goroutine to re-package and send
			// notifications for any newly unconfirmed transactions.
			go func(txNtfn *base.TransactionNotifications) {
				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
					}
				}
			}(txNtfn)
		case <-t.quit:
			break out
		}
	}
}

// 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
}

// FetchTx attempts to fetch a transaction in the wallet's database identified
// by the passed transaction hash. If the transaction can't be found, then a
// nil pointer is returned.
func (b *BtcWallet) FetchTx(txHash chainhash.Hash) (*wire.MsgTx, error) {
	var targetTx *wtxmgr.TxDetails
	err := walletdb.View(b.db, func(tx walletdb.ReadTx) error {
		wtxmgrNs := tx.ReadBucket(wtxmgrNamespaceKey)
		txDetails, err := b.wallet.TxStore.TxDetails(wtxmgrNs, &txHash)
		if err != nil {
			return err
		}

		targetTx = txDetails

		return nil
	})
	if err != nil {
		return nil, err
	}

	if targetTx == nil {
		return nil, nil
	}

	return &targetTx.TxRecord.MsgTx, nil
}

// RemoveDescendants attempts to remove any transaction from the wallet's tx
// store (that may be unconfirmed) that spends outputs created by the passed
// transaction. This remove propagates recursively down the chain of descendent
// transactions.
func (b *BtcWallet) RemoveDescendants(tx *wire.MsgTx) error {
	txRecord, err := wtxmgr.NewTxRecordFromMsgTx(tx, time.Now())
	if err != nil {
		return err
	}

	return walletdb.Update(b.db, func(tx walletdb.ReadWriteTx) error {
		wtxmgrNs := tx.ReadWriteBucket(wtxmgrNamespaceKey)
		return b.wallet.TxStore.RemoveUnminedTx(wtxmgrNs, txRecord)
	})
}

// CheckMempoolAcceptance is a wrapper around `TestMempoolAccept` which checks
// the mempool acceptance of a transaction.
func (b *BtcWallet) CheckMempoolAcceptance(tx *wire.MsgTx) error {
	// Use a max feerate of 0 means the default value will be used when
	// testing mempool acceptance. The default max feerate is 0.10 BTC/kvb,
	// or 10,000 sat/vb.
	results, err := b.chain.TestMempoolAccept([]*wire.MsgTx{tx}, 0)
	if err != nil {
		return err
	}

	// Sanity check that the expected single result is returned.
	if len(results) != 1 {
		return fmt.Errorf("expected 1 result from TestMempoolAccept, "+
			"instead got %v", len(results))
	}

	result := results[0]
	log.Debugf("TestMempoolAccept result: %s", spew.Sdump(result))

	// Mempool check failed, we now map the reject reason to a proper RPC
	// error and return it.
	if !result.Allowed {
		err := b.chain.MapRPCErr(errors.New(result.RejectReason))

		return fmt.Errorf("mempool rejection: %w", err)
	}

	return nil
}
