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 = <cchaincfg.MainNetParams 103 case dexltc.TestNet4Params.Name: 104 ltcParams = <cchaincfg.TestNet4Params 105 case dexltc.RegressionNetParams.Name: 106 ltcParams = <cchaincfg.RegressionNetParams 107 } 108 w := <cSPVWallet{ 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] = <cwire.TxOut{ 537 Value: op.Value, 538 PkScript: op.PkScript, 539 } 540 } 541 542 ltcTx, err := w.Wallet.SendOutputs(ltcOuts, <cwaddrmgr.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 }