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