decred.org/dcrdex@v1.0.3/client/asset/ltc/spv.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package ltc
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"path/filepath"
    12  	"sync/atomic"
    13  	"time"
    14  
    15  	"decred.org/dcrdex/client/asset"
    16  	"decred.org/dcrdex/client/asset/btc"
    17  	"decred.org/dcrdex/dex"
    18  	dexltc "decred.org/dcrdex/dex/networks/ltc"
    19  	"github.com/btcsuite/btcd/btcec/v2"
    20  	"github.com/btcsuite/btcd/btcjson"
    21  	"github.com/btcsuite/btcd/btcutil"
    22  	"github.com/btcsuite/btcd/btcutil/gcs"
    23  	"github.com/btcsuite/btcd/btcutil/psbt"
    24  	"github.com/btcsuite/btcd/chaincfg"
    25  	"github.com/btcsuite/btcd/chaincfg/chainhash"
    26  	"github.com/btcsuite/btcd/wire"
    27  	"github.com/btcsuite/btclog"
    28  	"github.com/btcsuite/btcwallet/waddrmgr"
    29  	btcwallet "github.com/btcsuite/btcwallet/wallet"
    30  	"github.com/btcsuite/btcwallet/wtxmgr"
    31  	"github.com/dcrlabs/ltcwallet/chain"
    32  	neutrino "github.com/dcrlabs/ltcwallet/spv"
    33  	ltcwaddrmgr "github.com/dcrlabs/ltcwallet/waddrmgr"
    34  	"github.com/dcrlabs/ltcwallet/wallet"
    35  	"github.com/dcrlabs/ltcwallet/wallet/txauthor"
    36  	"github.com/dcrlabs/ltcwallet/walletdb"
    37  	_ "github.com/dcrlabs/ltcwallet/walletdb/bdb"
    38  	ltcwtxmgr "github.com/dcrlabs/ltcwallet/wtxmgr"
    39  	"github.com/decred/slog"
    40  	btcneutrino "github.com/lightninglabs/neutrino"
    41  	"github.com/lightninglabs/neutrino/headerfs"
    42  	ltcchaincfg "github.com/ltcsuite/ltcd/chaincfg"
    43  	ltcchainhash "github.com/ltcsuite/ltcd/chaincfg/chainhash"
    44  	"github.com/ltcsuite/ltcd/ltcutil"
    45  	ltctxscript "github.com/ltcsuite/ltcd/txscript"
    46  	ltcwire "github.com/ltcsuite/ltcd/wire"
    47  )
    48  
    49  const (
    50  	DefaultM        uint64 = 784931 // From ltcutil. Used for gcs filters.
    51  	logDirName             = "logs"
    52  	neutrinoDBName         = "neutrino.db"
    53  	defaultAcctNum         = 0
    54  	defaultAcctName        = "default"
    55  	dbTimeout              = 20 * time.Second
    56  )
    57  
    58  var (
    59  	waddrmgrNamespace = []byte("waddrmgr")
    60  	wtxmgrNamespace   = []byte("wtxmgr")
    61  
    62  	// Snapshot of valid peers. 10 May 2024
    63  	testnet4Seeds = []string{
    64  		"13.200.66.216:19335",
    65  		"208.91.111.150:18333",
    66  		"92.244.111.167:19335",
    67  		"164.92.171.95:8333",
    68  		"204.16.244.114:18333",
    69  		"34.227.13.195:19335",
    70  		"18.192.56.149:18333",
    71  	}
    72  )
    73  
    74  // ltcSPVWallet is an implementation of btc.BTCWallet that runs a native
    75  // Litecoin SPV Wallet. ltcSPVWallet mostly just translates types from the
    76  // btcsuite types to ltcsuite and vice-versa. Startup and shutdown are notable
    77  // exceptions, and have some critical code that needed to be duplicated (in
    78  // order to avoid interface hell).
    79  type ltcSPVWallet struct {
    80  	// This section is populated in openSPVWallet.
    81  	dir         string
    82  	chainParams *ltcchaincfg.Params
    83  	btcParams   *chaincfg.Params
    84  	log         dex.Logger
    85  
    86  	// This section is populated in Start.
    87  	*wallet.Wallet
    88  	chainClient *chain.NeutrinoClient
    89  	cl          *neutrino.ChainService
    90  	loader      *wallet.Loader
    91  	neutrinoDB  walletdb.DB
    92  
    93  	peerManager *btc.SPVPeerManager
    94  }
    95  
    96  var _ btc.BTCWallet = (*ltcSPVWallet)(nil)
    97  
    98  // openSPVWallet creates a ltcSPVWallet, but does not Start.
    99  func openSPVWallet(dir string, cfg *btc.WalletConfig, btcParams *chaincfg.Params, log dex.Logger) btc.BTCWallet {
   100  	var ltcParams *ltcchaincfg.Params
   101  	switch btcParams.Name {
   102  	case dexltc.MainNetParams.Name:
   103  		ltcParams = &ltcchaincfg.MainNetParams
   104  	case dexltc.TestNet4Params.Name:
   105  		ltcParams = &ltcchaincfg.TestNet4Params
   106  	case dexltc.RegressionNetParams.Name:
   107  		ltcParams = &ltcchaincfg.RegressionNetParams
   108  	}
   109  	w := &ltcSPVWallet{
   110  		dir:         dir,
   111  		chainParams: ltcParams,
   112  		btcParams:   btcParams,
   113  		log:         log,
   114  	}
   115  	return w
   116  }
   117  
   118  // createSPVWallet creates a new SPV wallet.
   119  func createSPVWallet(privPass []byte, seed []byte, bday time.Time, walletDir string, log dex.Logger, extIdx, intIdx uint32, net *ltcchaincfg.Params) error {
   120  	if err := logNeutrino(walletDir, log); err != nil {
   121  		return fmt.Errorf("error initializing dcrwallet+neutrino logging: %w", err)
   122  	}
   123  
   124  	// timeout and recoverWindow arguments borrowed from btcwallet directly.
   125  	loader := wallet.NewLoader(net, walletDir, true, dbTimeout, 250)
   126  
   127  	pubPass := []byte(wallet.InsecurePubPassphrase)
   128  
   129  	btcw, err := loader.CreateNewWallet(pubPass, privPass, seed, bday)
   130  	if err != nil {
   131  		return fmt.Errorf("CreateNewWallet error: %w", err)
   132  	}
   133  
   134  	errCloser := dex.NewErrorCloser()
   135  	defer errCloser.Done(log)
   136  	errCloser.Add(loader.UnloadWallet)
   137  
   138  	if extIdx > 0 || intIdx > 0 {
   139  		err = extendAddresses(extIdx, intIdx, btcw)
   140  		if err != nil {
   141  			return fmt.Errorf("failed to set starting address indexes: %w", err)
   142  		}
   143  	}
   144  
   145  	// The chain service DB
   146  	neutrinoDBPath := filepath.Join(walletDir, neutrinoDBName)
   147  	db, err := walletdb.Create("bdb", neutrinoDBPath, true, dbTimeout)
   148  	if err != nil {
   149  		return fmt.Errorf("unable to create neutrino db at %q: %w", neutrinoDBPath, err)
   150  	}
   151  	if err = db.Close(); err != nil {
   152  		return fmt.Errorf("error closing newly created wallet database: %w", err)
   153  	}
   154  
   155  	if err := loader.UnloadWallet(); err != nil {
   156  		return fmt.Errorf("error unloading wallet: %w", err)
   157  	}
   158  
   159  	errCloser.Success()
   160  	return nil
   161  }
   162  
   163  // AccountInfo returns the account information of the wallet for use by the
   164  // exchange wallet.
   165  func (w *ltcSPVWallet) AccountInfo() btc.XCWalletAccount {
   166  	return btc.XCWalletAccount{
   167  		AccountName:   defaultAcctName,
   168  		AccountNumber: defaultAcctNum,
   169  	}
   170  }
   171  
   172  // walletParams works around a bug in ltcwallet that doesn't recognize
   173  // wire.TestNet4 in (*ScopedKeyManager).cloneKeyWithVersion which is called from
   174  // AccountProperties. Only do this for the *wallet.Wallet, not the
   175  // *neutrino.ChainService.
   176  func (w *ltcSPVWallet) walletParams() *ltcchaincfg.Params {
   177  	if w.chainParams.Name != ltcchaincfg.TestNet4Params.Name {
   178  		return w.chainParams
   179  	}
   180  	spoofParams := *w.chainParams
   181  	spoofParams.Net = ltcwire.TestNet4
   182  	return &spoofParams
   183  }
   184  
   185  // Start initializes the *ltcwallet.Wallet and its supporting players and starts
   186  // syncing.
   187  func (w *ltcSPVWallet) Start() (btc.SPVService, error) {
   188  	if err := logNeutrino(w.dir, w.log); err != nil {
   189  		return nil, fmt.Errorf("error initializing dcrwallet+neutrino logging: %v", err)
   190  	}
   191  	// recoverWindow arguments borrowed from ltcwallet directly.
   192  
   193  	w.loader = wallet.NewLoader(w.walletParams(), w.dir, true, dbTimeout, 250)
   194  
   195  	exists, err := w.loader.WalletExists()
   196  	if err != nil {
   197  		return nil, fmt.Errorf("error verifying wallet existence: %v", err)
   198  	}
   199  	if !exists {
   200  		return nil, errors.New("wallet not found")
   201  	}
   202  
   203  	w.log.Debug("Starting native LTC wallet...")
   204  	w.Wallet, err = w.loader.OpenExistingWallet([]byte(wallet.InsecurePubPassphrase), false)
   205  	if err != nil {
   206  		return nil, fmt.Errorf("couldn't load wallet: %w", err)
   207  	}
   208  
   209  	errCloser := dex.NewErrorCloser()
   210  	defer errCloser.Done(w.log)
   211  	errCloser.Add(w.loader.UnloadWallet)
   212  
   213  	neutrinoDBPath := filepath.Join(w.dir, neutrinoDBName)
   214  	w.neutrinoDB, err = walletdb.Create("bdb", neutrinoDBPath, true, dbTimeout)
   215  	if err != nil {
   216  		return nil, fmt.Errorf("unable to create wallet db at %q: %v", neutrinoDBPath, err)
   217  	}
   218  	errCloser.Add(w.neutrinoDB.Close)
   219  
   220  	w.log.Debug("Starting neutrino chain service...")
   221  	w.cl, err = neutrino.NewChainService(neutrino.Config{
   222  		DataDir:       w.dir,
   223  		Database:      w.neutrinoDB,
   224  		ChainParams:   *w.chainParams,
   225  		PersistToDisk: true, // keep cfilter headers on disk for efficient rescanning
   226  		// WARNING: PublishTransaction currently uses the entire duration
   227  		// because if an external bug, but even if the resolved, a typical
   228  		// inv/getdata round trip is ~4 seconds, so we set this so neutrino does
   229  		// not cancel queries too readily.
   230  		BroadcastTimeout: 6 * time.Second,
   231  	})
   232  	if err != nil {
   233  		return nil, fmt.Errorf("couldn't create Neutrino ChainService: %v", err)
   234  	}
   235  	errCloser.Add(w.cl.Stop)
   236  
   237  	w.chainClient = chain.NewNeutrinoClient(w.chainParams, w.cl)
   238  
   239  	var defaultPeers []string
   240  	switch w.chainParams.Net {
   241  	case ltcwire.TestNet4:
   242  		defaultPeers = append([]string{"127.0.0.1:19335"}, testnet4Seeds...)
   243  	case ltcwire.TestNet, ltcwire.SimNet: // plain "wire.TestNet" is regnet!
   244  		defaultPeers = []string{"127.0.0.1:20585"}
   245  	}
   246  	peerManager := btc.NewSPVPeerManager(&spvService{w.cl}, defaultPeers, w.dir, w.log, w.chainParams.DefaultPort)
   247  	w.peerManager = peerManager
   248  
   249  	if err = w.chainClient.Start(); err != nil { // lazily starts connmgr
   250  		return nil, fmt.Errorf("couldn't start Neutrino client: %v", err)
   251  	}
   252  
   253  	w.log.Info("Synchronizing wallet with network...")
   254  	w.SynchronizeRPC(w.chainClient)
   255  
   256  	errCloser.Success()
   257  
   258  	w.peerManager.ConnectToInitialWalletPeers()
   259  
   260  	return &spvService{w.cl}, nil
   261  }
   262  
   263  func (w *ltcSPVWallet) Birthday() time.Time {
   264  	return w.Manager.Birthday()
   265  }
   266  
   267  func (w *ltcSPVWallet) updateDBBirthday(bday time.Time) error {
   268  	btcw, isLoaded := w.loader.LoadedWallet()
   269  	if !isLoaded {
   270  		return fmt.Errorf("wallet not loaded")
   271  	}
   272  	return walletdb.Update(btcw.Database(), func(dbtx walletdb.ReadWriteTx) error {
   273  		ns := dbtx.ReadWriteBucket(waddrmgrNamespace)
   274  		return btcw.Manager.SetBirthday(ns, bday)
   275  	})
   276  }
   277  
   278  func (w *ltcSPVWallet) txDetails(txHash *ltcchainhash.Hash) (*ltcwtxmgr.TxDetails, error) {
   279  	details, err := wallet.UnstableAPI(w.Wallet).TxDetails(txHash)
   280  	if err != nil {
   281  		return nil, err
   282  	}
   283  	if details == nil {
   284  		return nil, btc.WalletTransactionNotFound
   285  	}
   286  
   287  	return details, nil
   288  }
   289  
   290  func (w *ltcSPVWallet) addrLTC2BTC(addr ltcutil.Address) (btcutil.Address, error) {
   291  	return btcutil.DecodeAddress(addr.String(), w.btcParams)
   292  }
   293  
   294  func (w *ltcSPVWallet) addrBTC2LTC(addr btcutil.Address) (ltcutil.Address, error) {
   295  	return ltcutil.DecodeAddress(addr.String(), w.chainParams)
   296  }
   297  
   298  func (w *ltcSPVWallet) PublishTransaction(btcTx *wire.MsgTx, label string) error {
   299  	ltcTx, err := convertMsgTxToLTC(btcTx)
   300  	if err != nil {
   301  		return err
   302  	}
   303  
   304  	return w.Wallet.PublishTransaction(ltcTx, label)
   305  }
   306  
   307  func (w *ltcSPVWallet) CalculateAccountBalances(account uint32, confirms int32) (btcwallet.Balances, error) {
   308  	bals, err := w.Wallet.CalculateAccountBalances(account, confirms)
   309  	if err != nil {
   310  		return btcwallet.Balances{}, err
   311  	}
   312  	return btcwallet.Balances{
   313  		Total:          btcutil.Amount(bals.Total),
   314  		Spendable:      btcutil.Amount(bals.Spendable),
   315  		ImmatureReward: btcutil.Amount(bals.ImmatureReward),
   316  	}, nil
   317  }
   318  
   319  func (w *ltcSPVWallet) ListSinceBlock(start, end, syncHeight int32) ([]btcjson.ListTransactionsResult, error) {
   320  	res, err := w.Wallet.ListSinceBlock(start, end, syncHeight)
   321  	if err != nil {
   322  		return nil, err
   323  	}
   324  
   325  	btcRes := make([]btcjson.ListTransactionsResult, len(res))
   326  	for i, r := range res {
   327  		btcRes[i] = btcjson.ListTransactionsResult{
   328  			Abandoned:         r.Abandoned,
   329  			Account:           r.Account,
   330  			Address:           r.Address,
   331  			Amount:            r.Amount,
   332  			BIP125Replaceable: r.BIP125Replaceable,
   333  			BlockHash:         r.BlockHash,
   334  			BlockHeight:       r.BlockHeight,
   335  			BlockIndex:        r.BlockIndex,
   336  			BlockTime:         r.BlockTime,
   337  			Category:          r.Category,
   338  			Confirmations:     r.Confirmations,
   339  			Fee:               r.Fee,
   340  			Generated:         r.Generated,
   341  			InvolvesWatchOnly: r.InvolvesWatchOnly,
   342  			Label:             r.Label,
   343  			Time:              r.Time,
   344  			TimeReceived:      r.TimeReceived,
   345  			Trusted:           r.Trusted,
   346  			TxID:              r.TxID,
   347  			Vout:              r.Vout,
   348  			WalletConflicts:   r.WalletConflicts,
   349  			Comment:           r.Comment,
   350  			OtherAccount:      r.OtherAccount,
   351  		}
   352  	}
   353  
   354  	return btcRes, nil
   355  }
   356  
   357  func (w *ltcSPVWallet) GetTransactions(startBlock, endBlock int32, accountName string, cancel <-chan struct{}) (*btcwallet.GetTransactionsResult, error) {
   358  	startID := wallet.NewBlockIdentifierFromHeight(startBlock)
   359  	endID := wallet.NewBlockIdentifierFromHeight(endBlock)
   360  	ltcGTR, err := w.Wallet.GetTransactions(startID, endID, accountName, cancel)
   361  	if err != nil {
   362  		return nil, err
   363  	}
   364  
   365  	convertTxs := func(txs []wallet.TransactionSummary) []btcwallet.TransactionSummary {
   366  		transactions := make([]btcwallet.TransactionSummary, len(txs))
   367  		for i, tx := range txs {
   368  			txHash := chainhash.Hash(*tx.Hash)
   369  			inputs := make([]btcwallet.TransactionSummaryInput, len(tx.MyInputs))
   370  			for k, in := range tx.MyInputs {
   371  				inputs[k] = btcwallet.TransactionSummaryInput{
   372  					Index:           in.Index,
   373  					PreviousAccount: in.PreviousAccount,
   374  					PreviousAmount:  btcutil.Amount(in.PreviousAmount),
   375  				}
   376  			}
   377  			outputs := make([]btcwallet.TransactionSummaryOutput, len(tx.MyOutputs))
   378  			for k, out := range tx.MyOutputs {
   379  				outputs[k] = btcwallet.TransactionSummaryOutput{
   380  					Index:    out.Index,
   381  					Account:  out.Account,
   382  					Internal: out.Internal,
   383  				}
   384  			}
   385  			transactions[i] = btcwallet.TransactionSummary{
   386  				Hash:        &txHash,
   387  				Transaction: tx.Transaction,
   388  				MyInputs:    inputs,
   389  				MyOutputs:   outputs,
   390  				Fee:         btcutil.Amount(tx.Fee),
   391  				Timestamp:   tx.Timestamp,
   392  				Label:       tx.Label,
   393  			}
   394  		}
   395  		return transactions
   396  	}
   397  
   398  	btcGTR := &btcwallet.GetTransactionsResult{
   399  		MinedTransactions:   make([]btcwallet.Block, len(ltcGTR.MinedTransactions)),
   400  		UnminedTransactions: convertTxs(ltcGTR.UnminedTransactions),
   401  	}
   402  
   403  	for i, block := range ltcGTR.MinedTransactions {
   404  		blockHash := chainhash.Hash(*block.Hash)
   405  		btcGTR.MinedTransactions[i] = btcwallet.Block{
   406  			Hash:         &blockHash,
   407  			Height:       block.Height,
   408  			Timestamp:    block.Timestamp,
   409  			Transactions: convertTxs(block.Transactions),
   410  		}
   411  	}
   412  
   413  	return btcGTR, nil
   414  }
   415  
   416  func (w *ltcSPVWallet) ListUnspent(minconf, maxconf int32, acctName string) ([]*btcjson.ListUnspentResult, error) {
   417  	// ltcwallet's ListUnspent takes either a list of addresses, or else returns
   418  	// all non-locked unspent outputs for all accounts. We need to iterate the
   419  	// results anyway to convert type.
   420  	uns, err := w.Wallet.ListUnspent(minconf, maxconf, acctName)
   421  	if err != nil {
   422  		return nil, err
   423  	}
   424  
   425  	outs := make([]*btcjson.ListUnspentResult, len(uns))
   426  	for i, u := range uns {
   427  		if u.Account != acctName {
   428  			continue
   429  		}
   430  		outs[i] = &btcjson.ListUnspentResult{
   431  			TxID:          u.TxID,
   432  			Vout:          u.Vout,
   433  			Address:       u.Address,
   434  			Account:       u.Account,
   435  			ScriptPubKey:  u.ScriptPubKey,
   436  			RedeemScript:  u.RedeemScript,
   437  			Amount:        u.Amount,
   438  			Confirmations: u.Confirmations,
   439  			Spendable:     u.Spendable,
   440  		}
   441  	}
   442  
   443  	return outs, nil
   444  }
   445  
   446  // FetchInputInfo is not actually implemented in ltcwallet. This is based on the
   447  // btcwallet implementation. As this is used by btc.spvWallet, we really only
   448  // need the TxOut, and to show ownership.
   449  func (w *ltcSPVWallet) FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx, *wire.TxOut, *psbt.Bip32Derivation, int64, error) {
   450  
   451  	td, err := w.txDetails((*ltcchainhash.Hash)(&prevOut.Hash))
   452  	if err != nil {
   453  		return nil, nil, nil, 0, err
   454  	}
   455  
   456  	if prevOut.Index >= uint32(len(td.TxRecord.MsgTx.TxOut)) {
   457  		return nil, nil, nil, 0, fmt.Errorf("not enough outputs")
   458  	}
   459  
   460  	ltcTxOut := td.TxRecord.MsgTx.TxOut[prevOut.Index]
   461  
   462  	// Verify we own at least one parsed address.
   463  	_, addrs, _, err := ltctxscript.ExtractPkScriptAddrs(ltcTxOut.PkScript, w.chainParams)
   464  	if err != nil {
   465  		return nil, nil, nil, 0, err
   466  	}
   467  	notOurs := true
   468  	for i := 0; notOurs && i < len(addrs); i++ {
   469  		_, err := w.Wallet.AddressInfo(addrs[i])
   470  		notOurs = err != nil
   471  	}
   472  	if notOurs {
   473  		return nil, nil, nil, 0, btcwallet.ErrNotMine
   474  	}
   475  
   476  	btcTxOut := &wire.TxOut{
   477  		Value:    ltcTxOut.Value,
   478  		PkScript: ltcTxOut.PkScript,
   479  	}
   480  
   481  	return nil, btcTxOut, nil, 0, nil
   482  }
   483  
   484  func (w *ltcSPVWallet) LockOutpoint(op wire.OutPoint) {
   485  	w.Wallet.LockOutpoint(ltcwire.OutPoint{
   486  		Hash:  ltcchainhash.Hash(op.Hash),
   487  		Index: op.Index,
   488  	})
   489  }
   490  
   491  func (w *ltcSPVWallet) UnlockOutpoint(op wire.OutPoint) {
   492  	w.Wallet.UnlockOutpoint(ltcwire.OutPoint{
   493  		Hash:  ltcchainhash.Hash(op.Hash),
   494  		Index: op.Index,
   495  	})
   496  }
   497  
   498  func (w *ltcSPVWallet) LockedOutpoints() []btcjson.TransactionInput {
   499  	locks := w.Wallet.LockedOutpoints()
   500  	locked := make([]btcjson.TransactionInput, len(locks))
   501  	for i, lock := range locks {
   502  		locked[i] = btcjson.TransactionInput{
   503  			Txid: lock.Txid,
   504  			Vout: lock.Vout,
   505  		}
   506  	}
   507  	return locked
   508  }
   509  
   510  func (w *ltcSPVWallet) NewChangeAddress(account uint32, _ waddrmgr.KeyScope) (btcutil.Address, error) {
   511  	ltcAddr, err := w.Wallet.NewChangeAddress(account, ltcwaddrmgr.KeyScopeBIP0084WithBitcoinCoinID)
   512  	if err != nil {
   513  		return nil, err
   514  	}
   515  	return w.addrLTC2BTC(ltcAddr)
   516  }
   517  
   518  func (w *ltcSPVWallet) NewAddress(account uint32, _ waddrmgr.KeyScope) (btcutil.Address, error) {
   519  	ltcAddr, err := w.Wallet.NewAddress(account, ltcwaddrmgr.KeyScopeBIP0084WithBitcoinCoinID)
   520  	if err != nil {
   521  		return nil, err
   522  	}
   523  	return w.addrLTC2BTC(ltcAddr)
   524  }
   525  
   526  func (w *ltcSPVWallet) PrivKeyForAddress(a btcutil.Address) (*btcec.PrivateKey, error) {
   527  	ltcAddr, err := w.addrBTC2LTC(a)
   528  	if err != nil {
   529  		return nil, err
   530  	}
   531  
   532  	ltcKey, err := w.Wallet.PrivKeyForAddress(ltcAddr)
   533  	if err != nil {
   534  		return nil, err
   535  	}
   536  
   537  	priv, _ /* pub */ := btcec.PrivKeyFromBytes(ltcKey.Serialize())
   538  	return priv, nil
   539  }
   540  
   541  func (w *ltcSPVWallet) SendOutputs(outputs []*wire.TxOut, _ *waddrmgr.KeyScope, account uint32, minconf int32,
   542  	satPerKb btcutil.Amount, _ btcwallet.CoinSelectionStrategy, label string) (*wire.MsgTx, error) {
   543  
   544  	ltcOuts := make([]*ltcwire.TxOut, len(outputs))
   545  	for i, op := range outputs {
   546  		ltcOuts[i] = &ltcwire.TxOut{
   547  			Value:    op.Value,
   548  			PkScript: op.PkScript,
   549  		}
   550  	}
   551  
   552  	ltcTx, err := w.Wallet.SendOutputs(ltcOuts, &ltcwaddrmgr.KeyScopeBIP0084WithBitcoinCoinID, account,
   553  		minconf, ltcutil.Amount(satPerKb), &wallet.RandomCoinSelector{}, label)
   554  	if err != nil {
   555  		return nil, err
   556  	}
   557  
   558  	btcTx, err := convertMsgTxToBTC(ltcTx)
   559  	if err != nil {
   560  		return nil, err
   561  	}
   562  
   563  	return btcTx, nil
   564  }
   565  
   566  func (w *ltcSPVWallet) HaveAddress(a btcutil.Address) (bool, error) {
   567  	ltcAddr, err := w.addrBTC2LTC(a)
   568  	if err != nil {
   569  		return false, err
   570  	}
   571  
   572  	return w.Wallet.HaveAddress(ltcAddr)
   573  }
   574  
   575  func (w *ltcSPVWallet) Stop() {
   576  	w.log.Info("Unloading wallet")
   577  	if err := w.loader.UnloadWallet(); err != nil {
   578  		w.log.Errorf("UnloadWallet error: %v", err)
   579  	}
   580  	if w.chainClient != nil {
   581  		w.log.Trace("Stopping neutrino client chain interface")
   582  		w.chainClient.Stop()
   583  		w.chainClient.WaitForShutdown()
   584  	}
   585  	w.log.Trace("Stopping neutrino chain sync service")
   586  	if err := w.cl.Stop(); err != nil {
   587  		w.log.Errorf("error stopping neutrino chain service: %v", err)
   588  	}
   589  	w.log.Trace("Stopping neutrino DB.")
   590  	if err := w.neutrinoDB.Close(); err != nil {
   591  		w.log.Errorf("wallet db close error: %v", err)
   592  	}
   593  
   594  	w.log.Info("SPV wallet closed")
   595  }
   596  
   597  func (w *ltcSPVWallet) AccountProperties(_ waddrmgr.KeyScope, acct uint32) (*waddrmgr.AccountProperties, error) {
   598  	scope := ltcwaddrmgr.KeyScopeBIP0084WithBitcoinCoinID
   599  	props, err := w.Wallet.AccountProperties(scope, acct)
   600  	if err != nil {
   601  		return nil, err
   602  	}
   603  	return &waddrmgr.AccountProperties{
   604  		AccountNumber:        props.AccountNumber,
   605  		AccountName:          props.AccountName,
   606  		ExternalKeyCount:     props.ExternalKeyCount,
   607  		InternalKeyCount:     props.InternalKeyCount,
   608  		ImportedKeyCount:     props.ImportedKeyCount,
   609  		MasterKeyFingerprint: props.MasterKeyFingerprint,
   610  		KeyScope: waddrmgr.KeyScope{
   611  			Purpose: scope.Purpose,
   612  			Coin:    scope.Coin,
   613  		},
   614  		IsWatchOnly: props.IsWatchOnly,
   615  		// The last two would need conversion but aren't currently used.
   616  		// AccountPubKey:        props.AccountPubKey,
   617  		// AddrSchema:           props.AddrSchema,
   618  	}, nil
   619  }
   620  
   621  func (w *ltcSPVWallet) RescanAsync() error {
   622  	w.log.Info("Stopping wallet and chain client...")
   623  	w.Wallet.Stop() // stops Wallet and chainClient (not chainService)
   624  	w.Wallet.WaitForShutdown()
   625  	w.chainClient.WaitForShutdown()
   626  
   627  	w.ForceRescan()
   628  
   629  	w.log.Info("Starting wallet...")
   630  	w.Wallet.Start()
   631  
   632  	if err := w.chainClient.Start(); err != nil {
   633  		return fmt.Errorf("couldn't start Neutrino client: %v", err)
   634  	}
   635  
   636  	w.log.Info("Synchronizing wallet with network...")
   637  	w.Wallet.SynchronizeRPC(w.chainClient)
   638  	return nil
   639  }
   640  
   641  // ForceRescan forces a full rescan with active address discovery on wallet
   642  // restart by dropping the complete transaction history and setting the
   643  // "synced to" field to nil. See the btcwallet/cmd/dropwtxmgr app for more
   644  // information.
   645  func (w *ltcSPVWallet) ForceRescan() {
   646  	w.log.Info("Dropping transaction history to perform full rescan...")
   647  	err := w.dropTransactionHistory()
   648  	if err != nil {
   649  		w.log.Errorf("Failed to drop wallet transaction history: %v", err)
   650  		// Continue to attempt restarting the wallet anyway.
   651  	}
   652  
   653  	err = walletdb.Update(w.Database(), func(dbtx walletdb.ReadWriteTx) error {
   654  		ns := dbtx.ReadWriteBucket(waddrmgrNamespace) // it'll be fine
   655  		return w.Manager.SetSyncedTo(ns, nil)         // never synced, forcing recover from birthday
   656  	})
   657  	if err != nil {
   658  		w.log.Errorf("Failed to reset wallet manager sync height: %v", err)
   659  	}
   660  }
   661  
   662  // dropTransactionHistory drops the transaction history. It is based off of the
   663  // dropwtxmgr utility in the ltcwallet repo.
   664  func (w *ltcSPVWallet) dropTransactionHistory() error {
   665  	w.log.Info("Dropping wallet transaction history")
   666  
   667  	return walletdb.Update(w.Database(), func(tx walletdb.ReadWriteTx) error {
   668  		err := tx.DeleteTopLevelBucket(wtxmgrNamespace)
   669  		if err != nil && err != walletdb.ErrBucketNotFound {
   670  			return err
   671  		}
   672  		ns, err := tx.CreateTopLevelBucket(wtxmgrNamespace)
   673  		if err != nil {
   674  			return err
   675  		}
   676  		err = ltcwtxmgr.Create(ns)
   677  		if err != nil {
   678  			return err
   679  		}
   680  
   681  		ns = tx.ReadWriteBucket(waddrmgrNamespace)
   682  		birthdayBlock, err := ltcwaddrmgr.FetchBirthdayBlock(ns)
   683  		if err != nil {
   684  			fmt.Println("Wallet does not have a birthday block " +
   685  				"set, falling back to rescan from genesis")
   686  
   687  			startBlock, err := ltcwaddrmgr.FetchStartBlock(ns)
   688  			if err != nil {
   689  				return err
   690  			}
   691  			return ltcwaddrmgr.PutSyncedTo(ns, startBlock)
   692  		}
   693  
   694  		// We'll need to remove our birthday block first because it
   695  		// serves as a barrier when updating our state to detect reorgs
   696  		// due to the wallet not storing all block hashes of the chain.
   697  		if err := ltcwaddrmgr.DeleteBirthdayBlock(ns); err != nil {
   698  			return err
   699  		}
   700  
   701  		if err := ltcwaddrmgr.PutSyncedTo(ns, &birthdayBlock); err != nil {
   702  			return err
   703  		}
   704  		return ltcwaddrmgr.PutBirthdayBlock(ns, birthdayBlock)
   705  	})
   706  }
   707  
   708  func (w *ltcSPVWallet) WalletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDetails, error) {
   709  	txDetails, err := w.txDetails((*ltcchainhash.Hash)(txHash))
   710  	if err != nil {
   711  		return nil, err
   712  	}
   713  
   714  	btcTx, err := convertMsgTxToBTC(&txDetails.MsgTx)
   715  	if err != nil {
   716  		return nil, err
   717  	}
   718  
   719  	credits := make([]wtxmgr.CreditRecord, len(txDetails.Credits))
   720  	for i, c := range txDetails.Credits {
   721  		credits[i] = wtxmgr.CreditRecord{
   722  			Amount: btcutil.Amount(c.Amount),
   723  			Index:  c.Index,
   724  			Spent:  c.Spent,
   725  			Change: c.Change,
   726  		}
   727  	}
   728  
   729  	debits := make([]wtxmgr.DebitRecord, len(txDetails.Debits))
   730  	for i, d := range txDetails.Debits {
   731  		debits[i] = wtxmgr.DebitRecord{
   732  			Amount: btcutil.Amount(d.Amount),
   733  			Index:  d.Index,
   734  		}
   735  	}
   736  
   737  	return &wtxmgr.TxDetails{
   738  		TxRecord: wtxmgr.TxRecord{
   739  			MsgTx:        *btcTx,
   740  			Hash:         chainhash.Hash(txDetails.TxRecord.Hash),
   741  			Received:     txDetails.TxRecord.Received,
   742  			SerializedTx: txDetails.TxRecord.SerializedTx,
   743  		},
   744  		Block: wtxmgr.BlockMeta{
   745  			Block: wtxmgr.Block{
   746  				Hash:   chainhash.Hash(txDetails.Block.Hash),
   747  				Height: txDetails.Block.Height,
   748  			},
   749  			Time: txDetails.Block.Time,
   750  		},
   751  		Credits: credits,
   752  		Debits:  debits,
   753  	}, nil
   754  }
   755  
   756  func (w *ltcSPVWallet) SyncedTo() waddrmgr.BlockStamp {
   757  	bs := w.Manager.SyncedTo()
   758  	return waddrmgr.BlockStamp{
   759  		Height:    bs.Height,
   760  		Hash:      chainhash.Hash(bs.Hash),
   761  		Timestamp: bs.Timestamp,
   762  	}
   763  }
   764  
   765  func (w *ltcSPVWallet) SignTx(btcTx *wire.MsgTx) error {
   766  	ltcTx, err := convertMsgTxToLTC(btcTx)
   767  	if err != nil {
   768  		return err
   769  	}
   770  
   771  	var prevPkScripts [][]byte
   772  	var inputValues []ltcutil.Amount
   773  	for _, txIn := range btcTx.TxIn {
   774  		_, txOut, _, _, err := w.FetchInputInfo(&txIn.PreviousOutPoint)
   775  		if err != nil {
   776  			return err
   777  		}
   778  		inputValues = append(inputValues, ltcutil.Amount(txOut.Value))
   779  		prevPkScripts = append(prevPkScripts, txOut.PkScript)
   780  		// Zero the previous witness and signature script or else
   781  		// AddAllInputScripts does some weird stuff.
   782  		txIn.SignatureScript = nil
   783  		txIn.Witness = nil
   784  	}
   785  
   786  	err = txauthor.AddAllInputScripts(ltcTx, prevPkScripts, inputValues, &secretSource{w.Wallet, w.chainParams})
   787  	if err != nil {
   788  		return err
   789  	}
   790  	if len(ltcTx.TxIn) != len(btcTx.TxIn) {
   791  		return fmt.Errorf("txin count mismatch")
   792  	}
   793  	for i, txIn := range btcTx.TxIn {
   794  		ltcIn := ltcTx.TxIn[i]
   795  		txIn.SignatureScript = ltcIn.SignatureScript
   796  		txIn.Witness = make(wire.TxWitness, len(ltcIn.Witness))
   797  		copy(txIn.Witness, ltcIn.Witness)
   798  	}
   799  	return nil
   800  }
   801  
   802  // BlockNotifications returns a channel on which to receive notifications of
   803  // newly processed blocks. The caller should only call BlockNotificaitons once.
   804  func (w *ltcSPVWallet) BlockNotifications(ctx context.Context) <-chan *btc.BlockNotification {
   805  	cl := w.NtfnServer.TransactionNotifications()
   806  	ch := make(chan *btc.BlockNotification, 1)
   807  	go func() {
   808  		defer cl.Done()
   809  		for {
   810  			select {
   811  			case note := <-cl.C:
   812  				if len(note.AttachedBlocks) > 0 {
   813  					lastBlock := note.AttachedBlocks[len(note.AttachedBlocks)-1]
   814  					select {
   815  					case ch <- &btc.BlockNotification{
   816  						Hash:   chainhash.Hash(*lastBlock.Hash),
   817  						Height: lastBlock.Height,
   818  					}:
   819  					default:
   820  					}
   821  				}
   822  			case <-ctx.Done():
   823  				return
   824  			}
   825  		}
   826  	}()
   827  	return ch
   828  }
   829  
   830  func (w *ltcSPVWallet) Peers() ([]*asset.WalletPeer, error) {
   831  	return w.peerManager.Peers()
   832  }
   833  
   834  func (w *ltcSPVWallet) AddPeer(addr string) error {
   835  	return w.peerManager.AddPeer(addr)
   836  }
   837  
   838  func (w *ltcSPVWallet) RemovePeer(addr string) error {
   839  	return w.peerManager.RemovePeer(addr)
   840  }
   841  
   842  // secretSource is used to locate keys and redemption scripts while signing a
   843  // transaction. secretSource satisfies the txauthor.SecretsSource interface.
   844  type secretSource struct {
   845  	w           *wallet.Wallet
   846  	chainParams *ltcchaincfg.Params
   847  }
   848  
   849  // ChainParams returns the chain parameters.
   850  func (s *secretSource) ChainParams() *ltcchaincfg.Params {
   851  	return s.chainParams
   852  }
   853  
   854  // GetKey fetches a private key for the specified address.
   855  func (s *secretSource) GetKey(addr ltcutil.Address) (*btcec.PrivateKey, bool, error) {
   856  	ma, err := s.w.AddressInfo(addr)
   857  	if err != nil {
   858  		return nil, false, err
   859  	}
   860  
   861  	mpka, ok := ma.(ltcwaddrmgr.ManagedPubKeyAddress)
   862  	if !ok {
   863  		e := fmt.Errorf("managed address type for %v is `%T` but "+
   864  			"want waddrmgr.ManagedPubKeyAddress", addr, ma)
   865  		return nil, false, e
   866  	}
   867  
   868  	privKey, err := mpka.PrivKey()
   869  	if err != nil {
   870  		return nil, false, err
   871  	}
   872  
   873  	k, _ /* pub */ := btcec.PrivKeyFromBytes(privKey.Serialize())
   874  
   875  	return k, ma.Compressed(), nil
   876  }
   877  
   878  // GetScript fetches the redemption script for the specified p2sh/p2wsh address.
   879  func (s *secretSource) GetScript(addr ltcutil.Address) ([]byte, error) {
   880  	ma, err := s.w.AddressInfo(addr)
   881  	if err != nil {
   882  		return nil, err
   883  	}
   884  
   885  	msa, ok := ma.(ltcwaddrmgr.ManagedScriptAddress)
   886  	if !ok {
   887  		e := fmt.Errorf("managed address type for %v is `%T` but "+
   888  			"want waddrmgr.ManagedScriptAddress", addr, ma)
   889  		return nil, e
   890  	}
   891  	return msa.Script()
   892  }
   893  
   894  // spvService embeds ltcsuite neutrino.ChainService and translates types.
   895  type spvService struct {
   896  	*neutrino.ChainService
   897  }
   898  
   899  var _ btc.SPVService = (*spvService)(nil)
   900  
   901  func (s *spvService) GetBlockHash(height int64) (*chainhash.Hash, error) {
   902  	ltcHash, err := s.ChainService.GetBlockHash(height)
   903  	if err != nil {
   904  		return nil, err
   905  	}
   906  	return (*chainhash.Hash)(ltcHash), nil
   907  }
   908  
   909  func (s *spvService) BestBlock() (*headerfs.BlockStamp, error) {
   910  	bs, err := s.ChainService.BestBlock()
   911  	if err != nil {
   912  		return nil, err
   913  	}
   914  	return &headerfs.BlockStamp{
   915  		Height:    bs.Height,
   916  		Hash:      chainhash.Hash(bs.Hash),
   917  		Timestamp: bs.Timestamp,
   918  	}, nil
   919  }
   920  
   921  func (s *spvService) Peers() []btc.SPVPeer {
   922  	rawPeers := s.ChainService.Peers()
   923  	peers := make([]btc.SPVPeer, len(rawPeers))
   924  	for i, p := range rawPeers {
   925  		peers[i] = p
   926  	}
   927  	return peers
   928  }
   929  
   930  func (s *spvService) AddPeer(addr string) error {
   931  	return s.ChainService.ConnectNode(addr, true)
   932  }
   933  
   934  func (s *spvService) RemovePeer(addr string) error {
   935  	return s.ChainService.RemoveNodeByAddr(addr)
   936  }
   937  
   938  func (s *spvService) GetBlockHeight(h *chainhash.Hash) (int32, error) {
   939  	return s.ChainService.GetBlockHeight((*ltcchainhash.Hash)(h))
   940  }
   941  
   942  func (s *spvService) GetBlockHeader(h *chainhash.Hash) (*wire.BlockHeader, error) {
   943  	hdr, err := s.ChainService.GetBlockHeader((*ltcchainhash.Hash)(h))
   944  	if err != nil {
   945  		return nil, err
   946  	}
   947  	return &wire.BlockHeader{
   948  		Version:    hdr.Version,
   949  		PrevBlock:  chainhash.Hash(hdr.PrevBlock),
   950  		MerkleRoot: chainhash.Hash(hdr.MerkleRoot),
   951  		Timestamp:  hdr.Timestamp,
   952  		Bits:       hdr.Bits,
   953  		Nonce:      hdr.Nonce,
   954  	}, nil
   955  }
   956  
   957  func (s *spvService) GetCFilter(blockHash chainhash.Hash, filterType wire.FilterType, _ ...btcneutrino.QueryOption) (*gcs.Filter, error) {
   958  	f, err := s.ChainService.GetCFilter(ltcchainhash.Hash(blockHash), ltcwire.GCSFilterRegular)
   959  	if err != nil {
   960  		return nil, err
   961  	}
   962  
   963  	b, err := f.Bytes()
   964  	if err != nil {
   965  		return nil, err
   966  	}
   967  
   968  	return gcs.FromBytes(f.N(), f.P(), DefaultM, b)
   969  }
   970  
   971  func (s *spvService) GetBlock(blockHash chainhash.Hash, _ ...btcneutrino.QueryOption) (*btcutil.Block, error) {
   972  	blk, err := s.ChainService.GetBlock(ltcchainhash.Hash(blockHash))
   973  	if err != nil {
   974  		return nil, err
   975  	}
   976  
   977  	b, err := blk.Bytes()
   978  	if err != nil {
   979  		return nil, err
   980  	}
   981  
   982  	return btcutil.NewBlockFromBytes(b)
   983  }
   984  
   985  func convertMsgTxToBTC(tx *ltcwire.MsgTx) (*wire.MsgTx, error) {
   986  	buf := new(bytes.Buffer)
   987  	if err := tx.Serialize(buf); err != nil {
   988  		return nil, err
   989  	}
   990  
   991  	btcTx := new(wire.MsgTx)
   992  	if err := btcTx.Deserialize(buf); err != nil {
   993  		return nil, err
   994  	}
   995  	return btcTx, nil
   996  }
   997  
   998  func convertMsgTxToLTC(tx *wire.MsgTx) (*ltcwire.MsgTx, error) {
   999  	buf := new(bytes.Buffer)
  1000  	if err := tx.Serialize(buf); err != nil {
  1001  		return nil, err
  1002  	}
  1003  	ltcTx := new(ltcwire.MsgTx)
  1004  	if err := ltcTx.Deserialize(buf); err != nil {
  1005  		return nil, err
  1006  	}
  1007  
  1008  	return ltcTx, nil
  1009  }
  1010  
  1011  func extendAddresses(extIdx, intIdx uint32, ltcw *wallet.Wallet) error {
  1012  	scopedKeyManager, err := ltcw.Manager.FetchScopedKeyManager(ltcwaddrmgr.KeyScopeBIP0084WithBitcoinCoinID)
  1013  	if err != nil {
  1014  		return err
  1015  	}
  1016  
  1017  	return walletdb.Update(ltcw.Database(), func(dbtx walletdb.ReadWriteTx) error {
  1018  		ns := dbtx.ReadWriteBucket(waddrmgrNamespace)
  1019  		if extIdx > 0 {
  1020  			scopedKeyManager.ExtendExternalAddresses(ns, defaultAcctNum, extIdx)
  1021  		}
  1022  		if intIdx > 0 {
  1023  			scopedKeyManager.ExtendInternalAddresses(ns, defaultAcctNum, intIdx)
  1024  		}
  1025  		return nil
  1026  	})
  1027  }
  1028  
  1029  var (
  1030  	loggingInited uint32
  1031  	logFileName   = "neutrino.log"
  1032  )
  1033  
  1034  // logNeutrino initializes logging in the neutrino + wallet packages. Logging
  1035  // only has to be initialized once, so an atomic flag is used internally to
  1036  // return early on subsequent invocations.
  1037  //
  1038  // In theory, the rotating file logger must be Closed at some point, but
  1039  // there are concurrency issues with that since btcd and btcwallet have
  1040  // unsupervised goroutines still running after shutdown. So we leave the rotator
  1041  // running at the risk of losing some logs.
  1042  func logNeutrino(walletDir string, baseLogger dex.Logger) error {
  1043  	if !atomic.CompareAndSwapUint32(&loggingInited, 0, 1) {
  1044  		return nil
  1045  	}
  1046  
  1047  	logDir := filepath.Join(walletDir, logDirName)
  1048  	logSpinner, err := dex.LogRotator(logDir, logFileName)
  1049  	if err != nil {
  1050  		return fmt.Errorf("error initializing log rotator: %w", err)
  1051  	}
  1052  
  1053  	fileLogger := baseLogger.FileLogger(logSpinner)
  1054  	log := newFileLoggerPlus(baseLogger, fileLogger)
  1055  
  1056  	neutrino.UseLogGenerator(log)
  1057  	wallet.UseLogger(log)
  1058  
  1059  	return nil
  1060  }
  1061  
  1062  // logAdapter adapts dex.Logger to the btclog.Logger interface.
  1063  type logAdapter struct {
  1064  	dex.Logger
  1065  }
  1066  
  1067  var _ btclog.Logger = (*logAdapter)(nil)
  1068  
  1069  func (a *logAdapter) Level() btclog.Level {
  1070  	return btclog.Level(a.Logger.Level())
  1071  }
  1072  
  1073  func (a *logAdapter) SetLevel(lvl btclog.Level) {
  1074  	a.Logger.SetLevel(slog.Level(lvl))
  1075  }
  1076  
  1077  // fileLoggerPlus logs everything to a file, and everything with level >= warn
  1078  // to both file and a specified dex.Logger.
  1079  type fileLoggerPlus struct {
  1080  	btclog.Logger
  1081  	fileLogger dex.Logger
  1082  	baseLogger dex.Logger
  1083  }
  1084  
  1085  func newFileLoggerPlus(baseLogger, fileLogger dex.Logger) *fileLoggerPlus {
  1086  	return &fileLoggerPlus{
  1087  		Logger:     &logAdapter{fileLogger},
  1088  		fileLogger: fileLogger,
  1089  		baseLogger: baseLogger,
  1090  	}
  1091  }
  1092  
  1093  // NewLogger satisfies LogGenerator interface.
  1094  func (f *fileLoggerPlus) NewLogger(name string) btclog.Logger {
  1095  	fileLogger := f.fileLogger.SubLogger(name)
  1096  	return newFileLoggerPlus(f.baseLogger.SubLogger(name), fileLogger)
  1097  }
  1098  
  1099  func (f *fileLoggerPlus) Warnf(format string, params ...any) {
  1100  	f.baseLogger.Warnf(format, params...)
  1101  	f.fileLogger.Warnf(format, params...)
  1102  }
  1103  
  1104  func (f *fileLoggerPlus) Errorf(format string, params ...any) {
  1105  	f.baseLogger.Errorf(format, params...)
  1106  	f.fileLogger.Errorf(format, params...)
  1107  }
  1108  
  1109  func (f *fileLoggerPlus) Criticalf(format string, params ...any) {
  1110  	f.baseLogger.Criticalf(format, params...)
  1111  	f.fileLogger.Criticalf(format, params...)
  1112  }
  1113  
  1114  func (f *fileLoggerPlus) Warn(v ...any) {
  1115  	f.baseLogger.Warn(v...)
  1116  	f.fileLogger.Warn(v...)
  1117  }
  1118  
  1119  func (f *fileLoggerPlus) Error(v ...any) {
  1120  	f.baseLogger.Error(v...)
  1121  	f.fileLogger.Error(v...)
  1122  }
  1123  
  1124  func (f *fileLoggerPlus) Critical(v ...any) {
  1125  	f.baseLogger.Critical(v...)
  1126  	f.fileLogger.Critical(v...)
  1127  }