Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 211 additions & 0 deletions btcwbackend/boarding_backend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package btcwbackend

import (
"context"
"fmt"
"log/slog"
"math"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btclog/v2"
"github.com/lightninglabs/darepo-client/wallet"
"github.com/lightninglabs/darepo-client/walletcore"
"github.com/lightninglabs/neutrino"
"github.com/lightningnetwork/lnd/blockcache"
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
)

// BoardingBackendAdapter implements wallet.BoardingBackend by
// embedding walletcore.BoardingBackendBase for shared key derivation
// and script import, with neutrino providing block and transaction
// data. Unlike the lwwallet adapter which bypasses btcwallet for
// UTXO queries, this adapter uses btcwallet's native ListUnspent
// since neutrino's chain sync naturally detects matching outputs via
// compact block filters after address import.
type BoardingBackendAdapter struct {
// BoardingBackendBase provides shared DeriveNextKey,
// ImportTaprootScript, and address tracking.
walletcore.BoardingBackendBase

neutrinoCS *neutrino.ChainService
blockCache *blockcache.BlockCache

chainParams *chaincfg.Params
}

// NewBoardingBackendAdapter creates a new boarding backend adapter
// wrapping the given btcwallet instance with neutrino for chain data.
func NewBoardingBackendAdapter(btcw *btcwallet.BtcWallet,
neutrinoCS *neutrino.ChainService,
blockCache *blockcache.BlockCache,
chainParams *chaincfg.Params, coinType uint32,
logger btclog.Logger) *BoardingBackendAdapter {

return &BoardingBackendAdapter{
BoardingBackendBase: walletcore.NewBoardingBackendBase(
btcw, coinType, logger,
),
neutrinoCS: neutrinoCS,
blockCache: blockCache,
chainParams: chainParams,
}
}

// ListUnspent returns all UTXOs known to btcwallet with confirmation
// counts between minConfs and maxConfs. Unlike the lwwallet adapter
// which filters by imported boarding addresses, this returns all
// watched UTXOs directly from btcwallet. This avoids UTXO loss on
// restart since btcwallet persists its watch set independently of the
// in-memory importedAddrs map.
func (b *BoardingBackendAdapter) ListUnspent(ctx context.Context,
minConfs, maxConfs int32) ([]*wallet.Utxo, error) {

// Treat maxConfs of 0 as "no upper bound" so callers that
// omit the parameter don't accidentally filter everything.
if maxConfs == 0 {
maxConfs = math.MaxInt32
}

// Query btcwallet for all unspent outputs. btcwallet already
// scopes results to watched scripts (including those imported
// via ImportTaprootScript), so no additional address filtering
// is needed.
results, err := b.BtcWallet.InternalWallet().ListUnspent(
minConfs, maxConfs, "",
)
if err != nil {
return nil, fmt.Errorf("list unspent: %w", err)
}

var utxos []*wallet.Utxo

for _, r := range results {
addr, err := btcutil.DecodeAddress(
r.Address, b.chainParams,
)
if err != nil {
b.Log.WarnS(ctx,
"Failed to decode UTXO address", err,
slog.String("address", r.Address))

continue
}

pkScript, err := txscript.PayToAddrScript(addr)
if err != nil {
b.Log.WarnS(ctx,
"Failed to create pkScript for address",
err,
slog.String("address", r.Address))

continue
}

txid, err := chainhash.NewHashFromStr(r.TxID)
if err != nil {
b.Log.WarnS(ctx,
"Failed to parse UTXO txid", err,
slog.String("txid", r.TxID))

continue
}

// Convert BTC amount to satoshis.
amount, err := btcutil.NewAmount(r.Amount)
if err != nil {
b.Log.WarnS(ctx,
"Failed to parse UTXO amount", err)

continue
}

utxos = append(utxos, &wallet.Utxo{
Outpoint: wire.OutPoint{
Hash: *txid,
Index: r.Vout,
},
PkScript: pkScript,
Amount: amount,
Confirmations: int32(r.Confirmations),
})
}

b.Log.DebugS(ctx, "ListUnspent called",
slog.Int("min_confs", int(minConfs)),
slog.Int("max_confs", int(maxConfs)),
slog.Int("utxo_count", len(utxos)))

return utxos, nil
}

// GetTransaction returns the full transaction and its confirmation
// block hash for the given txid. It fetches from btcwallet's
// transaction store, which is populated by neutrino's chain sync.
func (b *BoardingBackendAdapter) GetTransaction(ctx context.Context,
txid chainhash.Hash) (*wire.MsgTx, *chainhash.Hash, error) {

// Fetch the raw transaction from btcwallet's store.
tx, err := b.BtcWallet.FetchTx(txid)
if err != nil || tx == nil {
b.Log.DebugS(ctx,
"Transaction not in wallet store",
slog.String("txid", txid.String()),
)

return nil, nil, fmt.Errorf(
"transaction %s not found in wallet", txid,
)
}
Comment thread
Roasbeef marked this conversation as resolved.

// Look up the transaction details from btcwallet to get the
// block hash.
txDetails, err := b.BtcWallet.InternalWallet().GetTransaction(
txid,
)
if err != nil {
// We have the tx but no details — return without block
// hash.
return tx, nil, nil
}

return tx, txDetails.BlockHash, nil
}

// GetBlock returns the full block for the given block hash via
// neutrino's P2P network. The block cache is used to avoid redundant
// fetches.
func (b *BoardingBackendAdapter) GetBlock(ctx context.Context,
blockHash chainhash.Hash) (*wire.MsgBlock, error) {

b.Log.DebugS(ctx, "Fetching block via neutrino",
slog.String("block_hash", blockHash.String()))

block, err := b.blockCache.GetBlock(
&blockHash,
func(hash *chainhash.Hash) (*wire.MsgBlock, error) {
blk, err := b.neutrinoCS.GetBlock(*hash)
if err != nil {
return nil, err
}

return blk.MsgBlock(), nil
},
)
if err != nil {
return nil, fmt.Errorf("get block: %w", err)
}

b.Log.DebugS(ctx, "Fetched block successfully",
slog.String("block_hash", blockHash.String()),
slog.Int("num_txs", len(block.Transactions)))

return block, nil
}

// Compile-time check that BoardingBackendAdapter implements
// wallet.BoardingBackend.
var _ wallet.BoardingBackend = (*BoardingBackendAdapter)(nil)
Loading
Loading