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 = <cchaincfg.MainNetParams 104 case dexltc.TestNet4Params.Name: 105 ltcParams = <cchaincfg.TestNet4Params 106 case dexltc.RegressionNetParams.Name: 107 ltcParams = <cchaincfg.RegressionNetParams 108 } 109 w := <cSPVWallet{ 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] = <cwire.TxOut{ 547 Value: op.Value, 548 PkScript: op.PkScript, 549 } 550 } 551 552 ltcTx, err := w.Wallet.SendOutputs(ltcOuts, <cwaddrmgr.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 }