decred.org/dcrdex@v1.0.5/client/asset/dcr/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 dcr 5 6 import ( 7 "context" 8 "encoding/base64" 9 "encoding/hex" 10 "errors" 11 "fmt" 12 "math" 13 "net" 14 "os" 15 "path/filepath" 16 "sort" 17 "sync" 18 "sync/atomic" 19 "time" 20 21 "decred.org/dcrdex/client/asset" 22 "decred.org/dcrdex/dex" 23 "decred.org/dcrdex/dex/utils" 24 "decred.org/dcrwallet/v5/chain" 25 walleterrors "decred.org/dcrwallet/v5/errors" 26 "decred.org/dcrwallet/v5/p2p" 27 walletjson "decred.org/dcrwallet/v5/rpc/jsonrpc/types" 28 "decred.org/dcrwallet/v5/spv" 29 "decred.org/dcrwallet/v5/wallet" 30 "decred.org/dcrwallet/v5/wallet/udb" 31 "github.com/decred/dcrd/addrmgr/v3" 32 "github.com/decred/dcrd/blockchain/stake/v5" 33 "github.com/decred/dcrd/chaincfg/chainhash" 34 "github.com/decred/dcrd/chaincfg/v3" 35 "github.com/decred/dcrd/connmgr/v3" 36 "github.com/decred/dcrd/dcrec/secp256k1/v4" 37 "github.com/decred/dcrd/dcrutil/v4" 38 "github.com/decred/dcrd/gcs/v4" 39 chainjson "github.com/decred/dcrd/rpc/jsonrpc/types/v4" 40 "github.com/decred/dcrd/txscript/v4" 41 "github.com/decred/dcrd/txscript/v4/stdaddr" 42 "github.com/decred/dcrd/wire" 43 "github.com/decred/slog" 44 "github.com/jrick/logrotate/rotator" 45 ) 46 47 const ( 48 defaultAllowHighFees = false 49 defaultRelayFeePerKb = 1e4 50 defaultAccountGapLimit = 3 51 defaultManualTickets = false 52 defaultMixSplitLimit = 10 53 54 defaultAcct = 0 55 defaultAccountName = "default" 56 walletDbName = "wallet.db" 57 dbDriver = "bdb" 58 logDirName = "spvlogs" 59 logFileName = "neutrino.log" 60 ) 61 62 type dcrWallet interface { 63 KnownAddress(ctx context.Context, a stdaddr.Address) (wallet.KnownAddress, error) 64 AccountNumber(ctx context.Context, accountName string) (uint32, error) 65 AddressAtIdx(ctx context.Context, account, branch, childIdx uint32) (stdaddr.Address, error) 66 AccountBalance(ctx context.Context, account uint32, confirms int32) (wallet.Balances, error) 67 LockedOutpoints(ctx context.Context, accountName string) ([]chainjson.TransactionInput, error) 68 ListUnspent(ctx context.Context, minconf, maxconf int32, addresses map[string]struct{}, accountName string) ([]*walletjson.ListUnspentResult, error) 69 LockOutpoint(txHash *chainhash.Hash, index uint32) 70 ListTransactionDetails(ctx context.Context, txHash *chainhash.Hash) ([]walletjson.ListTransactionsResult, error) 71 MixAccount(context.Context, uint32, uint32, uint32) error 72 MainChainTip(ctx context.Context) (hash chainhash.Hash, height int32) 73 NewExternalAddress(ctx context.Context, account uint32, callOpts ...wallet.NextAddressCallOption) (stdaddr.Address, error) 74 NewInternalAddress(ctx context.Context, account uint32, callOpts ...wallet.NextAddressCallOption) (stdaddr.Address, error) 75 PublishTransaction(ctx context.Context, tx *wire.MsgTx, n wallet.NetworkBackend) (*chainhash.Hash, error) 76 BlockHeader(ctx context.Context, blockHash *chainhash.Hash) (*wire.BlockHeader, error) 77 BlockInMainChain(ctx context.Context, hash *chainhash.Hash) (haveBlock, invalidated bool, err error) 78 CFilterV2(ctx context.Context, blockHash *chainhash.Hash) ([gcs.KeySize]byte, *gcs.FilterV2, error) 79 BlockInfo(ctx context.Context, blockID *wallet.BlockIdentifier) (*wallet.BlockInfo, error) 80 AccountUnlocked(ctx context.Context, account uint32) (bool, error) 81 LockAccount(ctx context.Context, account uint32) error 82 UnlockAccount(ctx context.Context, account uint32, passphrase []byte) error 83 LoadPrivateKey(ctx context.Context, addr stdaddr.Address) (key *secp256k1.PrivateKey, zero func(), err error) 84 TxDetails(ctx context.Context, txHash *chainhash.Hash) (*udb.TxDetails, error) 85 StakeInfo(ctx context.Context) (*wallet.StakeInfoData, error) 86 PurchaseTickets(ctx context.Context, n wallet.NetworkBackend, req *wallet.PurchaseTicketsRequest) (*wallet.PurchaseTicketsResponse, error) 87 ForUnspentUnexpiredTickets(ctx context.Context, f func(hash *chainhash.Hash) error) error 88 GetTickets(ctx context.Context, f func([]*wallet.TicketSummary, *wire.BlockHeader) (bool, error), startBlock, endBlock *wallet.BlockIdentifier) error 89 TreasuryKeyPolicies() []wallet.TreasuryKeyPolicy 90 GetAllTSpends(ctx context.Context) []*wire.MsgTx 91 TSpendPolicy(tspendHash, ticketHash *chainhash.Hash) stake.TreasuryVoteT 92 VSPHostForTicket(ctx context.Context, ticketHash *chainhash.Hash) (string, error) 93 SetAgendaChoices(ctx context.Context, ticketHash *chainhash.Hash, choices map[string]string) (voteBits uint16, err error) 94 SetTSpendPolicy(ctx context.Context, tspendHash *chainhash.Hash, policy stake.TreasuryVoteT, ticketHash *chainhash.Hash) error 95 SetTreasuryKeyPolicy(ctx context.Context, pikey []byte, policy stake.TreasuryVoteT, ticketHash *chainhash.Hash) error 96 SetRelayFee(relayFee dcrutil.Amount) 97 GetTicketInfo(ctx context.Context, hash *chainhash.Hash) (*wallet.TicketSummary, *wire.BlockHeader, error) 98 GetTransactions(ctx context.Context, f func(*wallet.Block) (bool, error), startBlock, endBlock *wallet.BlockIdentifier) error 99 ListSinceBlock(ctx context.Context, start, end, syncHeight int32) ([]walletjson.ListTransactionsResult, error) 100 UnlockOutpoint(txHash *chainhash.Hash, index uint32) 101 SignTransaction(ctx context.Context, tx *wire.MsgTx, hashType txscript.SigHashType, additionalPrevScripts map[wire.OutPoint][]byte, 102 additionalKeysByAddress map[string]*dcrutil.WIF, p2shRedeemScriptsByAddress map[string][]byte) ([]wallet.SignatureError, error) 103 AgendaChoices(ctx context.Context, ticketHash *chainhash.Hash) (choices map[string]string, voteBits uint16, err error) 104 NewVSPTicket(ctx context.Context, hash *chainhash.Hash) (*wallet.VSPTicket, error) 105 RescanProgressFromHeight(ctx context.Context, n wallet.NetworkBackend, startHeight int32, p chan<- wallet.RescanProgress) 106 RescanPoint(ctx context.Context) (*chainhash.Hash, error) 107 TotalReceivedForAddr(ctx context.Context, addr stdaddr.Address, minConf int32) (dcrutil.Amount, error) 108 NewVSPClient(cfg wallet.VSPClientConfig, log slog.Logger, dialer wallet.DialFunc) (*wallet.VSPClient, error) 109 } 110 111 // Interface for *spv.Syncer so that we can test with a stub. 112 type spvSyncer interface { 113 wallet.NetworkBackend 114 Synced(context.Context) (bool, int32) 115 GetRemotePeers() map[string]*p2p.RemotePeer 116 } 117 118 // cachedBlock is a cached MsgBlock with a last-access time. The cleanBlockCache 119 // loop is started in Connect to periodically discard cachedBlocks that are too 120 // old. 121 type cachedBlock struct { 122 *wire.MsgBlock 123 lastAccess time.Time 124 } 125 126 type blockCache struct { 127 sync.Mutex 128 blocks map[chainhash.Hash]*cachedBlock // block hash -> block 129 } 130 131 // extendedWallet adds the TxDetails method to *wallet.Wallet. 132 type extendedWallet struct { 133 *wallet.Wallet 134 } 135 136 // TxDetails exposes the (UnstableApi).TxDetails method. 137 func (w *extendedWallet) TxDetails(ctx context.Context, txHash *chainhash.Hash) (*udb.TxDetails, error) { 138 return wallet.UnstableAPI(w.Wallet).TxDetails(ctx, txHash) 139 } 140 141 // MainTipChangedNotifications returns a channel for receiving main tip change 142 // notifications, along with a function to close the channel when it is no 143 // longer needed. 144 func (w *extendedWallet) MainTipChangedNotifications() (chan *wallet.MainTipChangedNotification, func()) { 145 ntfn := w.NtfnServer.MainTipChangedNotifications() 146 return ntfn.C, ntfn.Done 147 } 148 149 // spvWallet is a Wallet built on dcrwallet's *wallet.Wallet running in SPV 150 // mode. 151 type spvWallet struct { 152 dcrWallet // *extendedWallet 153 db wallet.DB 154 dir string 155 chainParams *chaincfg.Params 156 log dex.Logger 157 bestSpvPeerHeight int32 // atomic 158 tipChan chan *block 159 gapLimit uint32 160 161 blockCache blockCache 162 163 accts atomic.Value 164 165 cancel context.CancelFunc 166 wg sync.WaitGroup 167 168 spvMtx sync.RWMutex 169 spv spvSyncer // *spv.Syncer 170 } 171 172 var _ Wallet = (*spvWallet)(nil) 173 var _ tipNotifier = (*spvWallet)(nil) 174 175 func createSPVWallet(pw, seed []byte, dataDir string, extIdx, intIdx, gapLimit uint32, chainParams *chaincfg.Params) error { 176 netDir := filepath.Join(dataDir, chainParams.Name) 177 walletDir := filepath.Join(netDir, "spv") 178 179 if err := initLogging(netDir); err != nil { 180 return fmt.Errorf("error initializing dcrwallet logging: %w", err) 181 } 182 183 if exists, err := walletExists(walletDir); err != nil { 184 return err 185 } else if exists { 186 return fmt.Errorf("wallet at %q already exists", walletDir) 187 } 188 189 ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 190 defer cancel() 191 192 dbPath := filepath.Join(walletDir, walletDbName) 193 exists, err := fileExists(dbPath) 194 if err != nil { 195 return fmt.Errorf("error checking file existence for %q: %w", dbPath, err) 196 } 197 if exists { 198 return fmt.Errorf("database file already exists at %q", dbPath) 199 } 200 201 // Ensure the data directory for the network exists. 202 if err := checkCreateDir(walletDir); err != nil { 203 return fmt.Errorf("checkCreateDir error: %w", err) 204 } 205 206 // At this point it is asserted that there is no existing database file, and 207 // deleting anything won't destroy a wallet in use. Defer a function that 208 // attempts to remove any wallet remnants. 209 defer func() { 210 if err != nil { 211 _ = os.Remove(walletDir) 212 } 213 }() 214 215 // Create the wallet database backed by bolt db. 216 db, err := wallet.CreateDB(dbDriver, dbPath) 217 if err != nil { 218 return fmt.Errorf("CreateDB error: %w", err) 219 } 220 221 // Initialize the newly created database for the wallet before opening. 222 err = wallet.Create(ctx, db, nil, pw, seed, chainParams) 223 if err != nil { 224 return fmt.Errorf("wallet.Create error: %w", err) 225 } 226 227 // Open the newly-created wallet. 228 w, err := wallet.Open(ctx, newWalletConfig(db, chainParams, gapLimit)) 229 if err != nil { 230 return fmt.Errorf("wallet.Open error: %w", err) 231 } 232 233 defer func() { 234 if err := db.Close(); err != nil { 235 fmt.Println("Error closing database:", err) 236 } 237 }() 238 239 err = w.UpgradeToSLIP0044CoinType(ctx) 240 if err != nil { 241 return err 242 } 243 244 err = w.Unlock(ctx, pw, nil) 245 if err != nil { 246 return fmt.Errorf("error unlocking wallet: %w", err) 247 } 248 249 err = w.SetAccountPassphrase(ctx, defaultAcct, pw) 250 if err != nil { 251 return fmt.Errorf("error setting Decred account %d passphrase: %v", defaultAcct, err) 252 } 253 254 err = setupMixingAccounts(ctx, w, pw) 255 if err != nil { 256 return fmt.Errorf("error setting up mixing accounts: %v", err) 257 } 258 259 w.Lock() 260 261 if extIdx > 0 || intIdx > 0 { 262 err = extendAddresses(ctx, extIdx, intIdx, w) 263 if err != nil { 264 return fmt.Errorf("failed to set starting address indexes: %w", err) 265 } 266 } 267 268 return nil 269 } 270 271 // If we're running on simnet, add some tspends and treasury keys. 272 func (w *spvWallet) initializeSimnetTspends(ctx context.Context) { 273 if w.chainParams.Net != wire.SimNet { 274 return 275 } 276 tspendWallet, is := w.dcrWallet.(interface { 277 AddTSpend(tx wire.MsgTx) error 278 GetAllTSpends(ctx context.Context) []*wire.MsgTx 279 SetTreasuryKeyPolicy(ctx context.Context, pikey []byte, policy stake.TreasuryVoteT, ticketHash *chainhash.Hash) error 280 TreasuryKeyPolicies() []wallet.TreasuryKeyPolicy 281 }) 282 if !is { 283 return 284 } 285 const numFakeTspends = 3 286 if len(tspendWallet.GetAllTSpends(ctx)) >= numFakeTspends { 287 return 288 } 289 expiryBase := uint32(time.Now().Add(time.Hour * 24 * 365).Unix()) 290 for i := uint32(0); i < numFakeTspends; i++ { 291 var signatureScript [100]byte 292 tx := &wire.MsgTx{ 293 Expiry: expiryBase + i, 294 TxIn: []*wire.TxIn{wire.NewTxIn(&wire.OutPoint{}, 0, signatureScript[:])}, 295 TxOut: []*wire.TxOut{{Value: int64(i+1) * 1e8}}, 296 } 297 if err := tspendWallet.AddTSpend(*tx); err != nil { 298 w.log.Errorf("Error adding simnet tspend: %v", err) 299 } 300 } 301 if len(tspendWallet.TreasuryKeyPolicies()) == 0 { 302 priv, _ := secp256k1.GeneratePrivateKey() 303 tspendWallet.SetTreasuryKeyPolicy(ctx, priv.PubKey().SerializeCompressed(), 0x01 /* yes */, nil) 304 } 305 } 306 307 // setupMixingAccounts checks if the mixed, unmixed and trading accounts 308 // required to use this wallet for funds mixing exists and creates any of the 309 // accounts that does not yet exist. The wallet should be unlocked before 310 // calling this function. 311 func setupMixingAccounts(ctx context.Context, w *wallet.Wallet, pw []byte) error { 312 requiredAccts := []string{mixedAccountName, tradingAccountName} // unmixed (default) acct already exists 313 for _, acct := range requiredAccts { 314 _, err := w.AccountNumber(ctx, acct) 315 if err == nil { 316 continue // account exist, check next account 317 } 318 319 if !errors.Is(err, walleterrors.NotExist) { 320 return err 321 } 322 323 acctNum, err := w.NextAccount(ctx, acct) 324 if err != nil { 325 return err 326 } 327 if err = w.SetAccountPassphrase(ctx, acctNum, pw); err != nil { 328 return err 329 } 330 } 331 332 return nil 333 } 334 335 func (w *spvWallet) setAccounts(mixingEnabled bool) { 336 if mixingEnabled { 337 w.accts.Store(XCWalletAccounts{ 338 PrimaryAccount: mixedAccountName, 339 UnmixedAccount: defaultAccountName, 340 TradingAccount: tradingAccountName, 341 }) 342 return 343 } 344 w.accts.Store(XCWalletAccounts{ 345 PrimaryAccount: defaultAccountName, 346 }) 347 } 348 349 // Accounts returns the names of the accounts for use by the exchange wallet. 350 func (w *spvWallet) Accounts() XCWalletAccounts { 351 return w.accts.Load().(XCWalletAccounts) 352 } 353 354 func (w *spvWallet) Reconfigure(ctx context.Context, cfg *asset.WalletConfig, net dex.Network, currentAddress string) (restart bool, err error) { 355 return cfg.Type != walletTypeSPV, nil 356 } 357 358 // InitialAddress returns the branch 0, child 0 address of the default 359 // account. 360 func (w *spvWallet) InitialAddress(ctx context.Context) (string, error) { 361 acctNum, err := w.dcrWallet.AccountNumber(ctx, defaultAccountName) 362 if err != nil { 363 return "", err 364 } 365 366 addr, err := w.dcrWallet.AddressAtIdx(ctx, acctNum, 0, 0) 367 if err != nil { 368 return "", err 369 } 370 371 return addr.String(), nil 372 } 373 374 func (w *spvWallet) startWallet(ctx context.Context) error { 375 netDir := filepath.Dir(w.dir) 376 if err := initLogging(netDir); err != nil { 377 return fmt.Errorf("error initializing dcrwallet logging: %w", err) 378 } 379 380 db, err := wallet.OpenDB(dbDriver, filepath.Join(w.dir, walletDbName)) 381 if err != nil { 382 return fmt.Errorf("wallet.OpenDB error: %w", err) 383 } 384 385 dcrw, err := wallet.Open(ctx, newWalletConfig(db, w.chainParams, w.gapLimit)) 386 if err != nil { 387 // If this function does not return to completion the database must be 388 // closed. Otherwise, because the database is locked on open, any 389 // other attempts to open the wallet will hang, and there is no way to 390 // recover since this db handle would be leaked. 391 if err := db.Close(); err != nil { 392 w.log.Errorf("Uh oh. Failed to close the database: %v", err) 393 } 394 return fmt.Errorf("wallet.Open error: %w", err) 395 } 396 w.dcrWallet = &extendedWallet{dcrw} 397 w.db = db 398 399 w.wg.Add(2) 400 go func() { 401 defer w.wg.Done() 402 w.spvLoop(ctx) 403 }() 404 go func() { 405 defer w.wg.Done() 406 w.notesLoop(ctx, dcrw) 407 }() 408 409 w.initializeSimnetTspends(ctx) 410 411 return nil 412 } 413 414 // stop stops the wallet and database threads. 415 func (w *spvWallet) stop() { 416 w.log.Info("Unloading wallet") 417 if err := w.db.Close(); err != nil { 418 w.log.Info("Error closing database: %v", err) 419 } 420 421 w.log.Info("SPV wallet closed") 422 } 423 424 func (w *spvWallet) spvLoop(ctx context.Context) { 425 for { 426 // Create a new syncer after every failure or dcrwallet will 427 // panic because of closing of a closed channel. 428 syncer := w.newSpvSyncer() 429 err := syncer.Run(ctx) 430 if ctx.Err() != nil { 431 return 432 } 433 w.log.Errorf("SPV synchronization ended. trying again in 10 seconds: %v", err) 434 select { 435 case <-ctx.Done(): 436 return 437 case <-time.After(time.Second * 10): 438 } 439 } 440 } 441 442 func (w *spvWallet) notesLoop(ctx context.Context, dcrw *wallet.Wallet) { 443 txNotes := dcrw.NtfnServer.TransactionNotifications() 444 defer txNotes.Done() 445 // removeTxNotes := dcrw.NtfnServer.RemovedTransactionNotifications() 446 // defer removeTxNotes.Done() 447 // acctNotes := dcrw.NtfnServer.AccountNotifications() 448 // defer acctNotes.Done() 449 // tipNotes := dcrw.NtfnServer.MainTipChangedNotifications() 450 // defer tipNotes.Done() 451 // confirmNotes := w.NtfnServer.ConfirmationNotifications(ctx) 452 453 for { 454 select { 455 case n := <-txNotes.C: 456 if len(n.AttachedBlocks) == 0 { 457 if len(n.UnminedTransactions) > 0 { 458 select { 459 case w.tipChan <- nil: 460 default: 461 w.log.Warnf("tx report channel was blocking") 462 } 463 } 464 continue 465 } 466 lastBlock := n.AttachedBlocks[len(n.AttachedBlocks)-1] 467 h := lastBlock.Header.BlockHash() 468 select { 469 case w.tipChan <- &block{ 470 hash: &h, 471 height: int64(lastBlock.Header.Height), 472 }: 473 default: 474 w.log.Warnf("tip report channel was blocking") 475 } 476 case <-ctx.Done(): 477 return 478 } 479 } 480 } 481 482 func (w *spvWallet) tipFeed() <-chan *block { 483 return w.tipChan 484 } 485 486 // Connect starts the wallet and begins synchronization. 487 func (w *spvWallet) Connect(ctx context.Context) error { 488 ctx, w.cancel = context.WithCancel(ctx) 489 err := w.startWallet(ctx) 490 if err != nil { 491 return err 492 } 493 494 w.wg.Add(1) 495 go func() { 496 defer w.wg.Done() 497 defer w.stop() 498 499 ticker := time.NewTicker(time.Minute * 20) 500 501 for { 502 select { 503 case <-ticker.C: 504 w.cleanBlockCache() 505 case <-ctx.Done(): 506 return 507 } 508 } 509 }() 510 511 return nil 512 } 513 514 // Disconnect shuts down the wallet and waits for monitored threads to exit. 515 // Part of the Wallet interface. 516 func (w *spvWallet) Disconnect() { 517 w.cancel() 518 w.wg.Wait() 519 } 520 521 // SpvMode is always true for spvWallet. 522 // Part of the Wallet interface. 523 func (w *spvWallet) SpvMode() bool { 524 return true 525 } 526 527 // AddressInfo returns information for the provided address. It is an error if 528 // the address is not owned by the wallet. 529 func (w *spvWallet) AddressInfo(ctx context.Context, addrStr string) (*AddressInfo, error) { 530 addr, err := stdaddr.DecodeAddress(addrStr, w.chainParams) 531 if err != nil { 532 return nil, err 533 } 534 ka, err := w.KnownAddress(ctx, addr) 535 if err != nil { 536 return nil, err 537 } 538 539 if ka, ok := ka.(wallet.BIP0044Address); ok { 540 _, branch, _ := ka.Path() 541 return &AddressInfo{Account: ka.AccountName(), Branch: branch}, nil 542 } 543 return nil, fmt.Errorf("unsupported address type %T", ka) 544 } 545 546 // WalletOwnsAddress returns whether any of the account controlled by this 547 // wallet owns the specified address. 548 func (w *spvWallet) WalletOwnsAddress(ctx context.Context, addr stdaddr.Address) (bool, error) { 549 ka, err := w.KnownAddress(ctx, addr) 550 if err != nil { 551 if errors.Is(err, walleterrors.NotExist) { 552 return false, nil 553 } 554 return false, fmt.Errorf("KnownAddress error: %w", err) 555 } 556 if kind := ka.AccountKind(); kind != wallet.AccountKindBIP0044 && kind != wallet.AccountKindImported { 557 return false, nil 558 } 559 560 return true, nil 561 } 562 563 // AccountOwnsAddress checks if the provided address belongs to the specified 564 // account. 565 // Part of the Wallet interface. 566 func (w *spvWallet) AccountOwnsAddress(ctx context.Context, addr stdaddr.Address, account string) (bool, error) { 567 ka, err := w.KnownAddress(ctx, addr) 568 if err != nil { 569 if errors.Is(err, walleterrors.NotExist) { 570 return false, nil 571 } 572 return false, fmt.Errorf("KnownAddress error: %w", err) 573 } 574 if ka.AccountName() != account { 575 return false, nil 576 } 577 if kind := ka.AccountKind(); kind != wallet.AccountKindBIP0044 && kind != wallet.AccountKindImported { 578 return false, nil 579 } 580 return true, nil 581 } 582 583 // AccountBalance returns the balance breakdown for the specified account. 584 // Part of the Wallet interface. 585 func (w *spvWallet) AccountBalance(ctx context.Context, confirms int32, accountName string) (*walletjson.GetAccountBalanceResult, error) { 586 bal, err := w.accountBalance(ctx, confirms, accountName) 587 if err != nil { 588 return nil, err 589 } 590 591 return &walletjson.GetAccountBalanceResult{ 592 AccountName: accountName, 593 ImmatureCoinbaseRewards: bal.ImmatureCoinbaseRewards.ToCoin(), 594 ImmatureStakeGeneration: bal.ImmatureStakeGeneration.ToCoin(), 595 LockedByTickets: bal.LockedByTickets.ToCoin(), 596 Spendable: bal.Spendable.ToCoin(), 597 Total: bal.Total.ToCoin(), 598 Unconfirmed: bal.Unconfirmed.ToCoin(), 599 VotingAuthority: bal.VotingAuthority.ToCoin(), 600 }, nil 601 } 602 603 func (w *spvWallet) accountBalance(ctx context.Context, confirms int32, accountName string) (wallet.Balances, error) { 604 acctNum, err := w.dcrWallet.AccountNumber(ctx, accountName) 605 if err != nil { 606 return wallet.Balances{}, err 607 } 608 return w.dcrWallet.AccountBalance(ctx, acctNum, confirms) 609 } 610 611 // LockedOutputs fetches locked outputs for the specified account. 612 // Part of the Wallet interface. 613 func (w *spvWallet) LockedOutputs(ctx context.Context, accountName string) ([]chainjson.TransactionInput, error) { 614 return w.dcrWallet.LockedOutpoints(ctx, accountName) 615 } 616 617 // Unspents fetches unspent outputs for the specified account. 618 // Part of the Wallet interface. 619 func (w *spvWallet) Unspents(ctx context.Context, accountName string) ([]*walletjson.ListUnspentResult, error) { 620 return w.dcrWallet.ListUnspent(ctx, 0, math.MaxInt32, nil, accountName) 621 } 622 623 // LockUnspent locks or unlocks the specified outpoint. 624 // Part of the Wallet interface. 625 func (w *spvWallet) LockUnspent(ctx context.Context, unlock bool, ops []*wire.OutPoint) error { 626 fun := w.LockOutpoint 627 if unlock { 628 fun = w.UnlockOutpoint 629 } 630 for _, op := range ops { 631 fun(&op.Hash, op.Index) 632 } 633 return nil 634 } 635 636 // UnspentOutput returns information about an unspent tx output, if found 637 // and unspent. 638 // This method is only guaranteed to return results for outputs that pay to 639 // the wallet. Returns asset.CoinNotFoundError if the unspent output cannot 640 // be located. 641 // Part of the Wallet interface. 642 func (w *spvWallet) UnspentOutput(ctx context.Context, txHash *chainhash.Hash, index uint32, _ int8) (*TxOutput, error) { 643 txd, err := w.dcrWallet.TxDetails(ctx, txHash) 644 if errors.Is(err, walleterrors.NotExist) { 645 return nil, asset.CoinNotFoundError 646 } else if err != nil { 647 return nil, err 648 } 649 650 details, err := w.ListTransactionDetails(ctx, txHash) 651 if err != nil { 652 return nil, err 653 } 654 655 var addrStr string 656 for _, detail := range details { 657 if detail.Vout == index { 658 addrStr = detail.Address 659 } 660 } 661 if addrStr == "" { 662 return nil, fmt.Errorf("error locating address for output") 663 } 664 665 tree := wire.TxTreeRegular 666 if txd.TxType != stake.TxTypeRegular { 667 tree = wire.TxTreeStake 668 } 669 670 if len(txd.MsgTx.TxOut) <= int(index) { 671 return nil, fmt.Errorf("not enough outputs") 672 } 673 674 _, tipHeight := w.MainChainTip(ctx) 675 676 var ours bool 677 for _, credit := range txd.Credits { 678 if credit.Index == index { 679 if credit.Spent { 680 return nil, asset.CoinNotFoundError 681 } 682 ours = true 683 break 684 } 685 } 686 687 if !ours { 688 return nil, asset.CoinNotFoundError 689 } 690 691 return &TxOutput{ 692 TxOut: txd.MsgTx.TxOut[index], 693 Tree: tree, 694 Addresses: []string{addrStr}, 695 Confirmations: uint32(txd.Block.Height - tipHeight + 1), 696 }, nil 697 } 698 699 // ExternalAddress returns an external address using GapPolicyIgnore. 700 // Part of the Wallet interface. 701 // Using GapPolicyWrap here, introducing a relatively small risk of address 702 // reuse, but improving wallet recoverability. 703 func (w *spvWallet) ExternalAddress(ctx context.Context, accountName string) (stdaddr.Address, error) { 704 acctNum, err := w.dcrWallet.AccountNumber(ctx, accountName) 705 if err != nil { 706 return nil, err 707 } 708 return w.NewExternalAddress(ctx, acctNum, wallet.WithGapPolicyWrap()) 709 } 710 711 // InternalAddress returns an internal address using GapPolicyIgnore. 712 // Part of the Wallet interface. 713 func (w *spvWallet) InternalAddress(ctx context.Context, accountName string) (stdaddr.Address, error) { 714 acctNum, err := w.dcrWallet.AccountNumber(ctx, accountName) 715 if err != nil { 716 return nil, err 717 } 718 return w.NewInternalAddress(ctx, acctNum, wallet.WithGapPolicyWrap()) 719 } 720 721 // SignRawTransaction signs the provided transaction. 722 // Part of the Wallet interface. 723 func (w *spvWallet) SignRawTransaction(ctx context.Context, baseTx *wire.MsgTx) (*wire.MsgTx, error) { 724 tx := baseTx.Copy() 725 sigErrs, err := w.dcrWallet.SignTransaction(ctx, tx, txscript.SigHashAll, nil, nil, nil) 726 if err != nil { 727 return nil, err 728 } 729 if len(sigErrs) > 0 { 730 for _, sigErr := range sigErrs { 731 w.log.Errorf("signature error for index %d: %v", sigErr.InputIndex, sigErr.Error) 732 } 733 return nil, fmt.Errorf("%d signature errors", len(sigErrs)) 734 } 735 return tx, nil 736 } 737 738 // SendRawTransaction broadcasts the provided transaction to the Decred network. 739 // Part of the Wallet interface. 740 func (w *spvWallet) SendRawTransaction(ctx context.Context, tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error) { 741 w.spvMtx.RLock() 742 defer w.spvMtx.RUnlock() 743 // TODO: Conditional high fee check? 744 return w.PublishTransaction(ctx, tx, w.spv) 745 } 746 747 // BlockTimestamp gets the timestamp of the block. 748 func (w *spvWallet) BlockTimestamp(ctx context.Context, blockHash *chainhash.Hash) (time.Time, error) { 749 hdr, err := w.dcrWallet.BlockHeader(ctx, blockHash) 750 if err != nil { 751 return time.Time{}, err 752 } 753 return hdr.Timestamp, nil 754 } 755 756 // GetBlockHeader generates a *BlockHeader for the specified block hash. The 757 // returned block header is a wire.BlockHeader with the addition of the block's 758 // median time and other auxiliary information. 759 func (w *spvWallet) GetBlockHeader(ctx context.Context, blockHash *chainhash.Hash) (*BlockHeader, error) { 760 hdr, err := w.dcrWallet.BlockHeader(ctx, blockHash) 761 if err != nil { 762 return nil, err 763 } 764 765 medianTime, err := w.medianTime(ctx, hdr) 766 if err != nil { 767 return nil, err 768 } 769 770 // Get next block hash unless there are none. 771 var nextHash *chainhash.Hash 772 confirmations := int64(-1) 773 mainChainHasBlock, _, err := w.BlockInMainChain(ctx, blockHash) 774 if err != nil { 775 return nil, fmt.Errorf("error checking if block is in mainchain: %w", err) 776 } 777 if mainChainHasBlock { 778 _, tipHeight := w.MainChainTip(ctx) 779 if int32(hdr.Height) < tipHeight { 780 nextHash, err = w.GetBlockHash(ctx, int64(hdr.Height)+1) 781 if err != nil { 782 return nil, fmt.Errorf("error getting next hash for block %q: %w", blockHash, err) 783 } 784 } 785 if int32(hdr.Height) <= tipHeight { 786 confirmations = int64(tipHeight) - int64(hdr.Height) + 1 787 } else { // if tip is less, may be rolling back, so just mock dcrd/dcrwallet 788 confirmations = 0 789 } 790 } 791 792 return &BlockHeader{ 793 BlockHeader: hdr, 794 MedianTime: medianTime, 795 Confirmations: confirmations, 796 NextHash: nextHash, 797 }, nil 798 } 799 800 // medianTime calculates a blocks median time, which is the median of the 801 // timestamps of the previous 11 blocks. 802 func (w *spvWallet) medianTime(ctx context.Context, iBlkHeader *wire.BlockHeader) (int64, error) { 803 // Calculate past median time. Look at the last 11 blocks, starting 804 // with the requested block, which is consistent with dcrd. 805 const numStamp = 11 806 timestamps := make([]int64, 0, numStamp) 807 for { 808 timestamps = append(timestamps, iBlkHeader.Timestamp.Unix()) 809 if iBlkHeader.Height == 0 || len(timestamps) == numStamp { 810 break 811 } 812 var err error 813 iBlkHeader, err = w.dcrWallet.BlockHeader(ctx, &iBlkHeader.PrevBlock) 814 if err != nil { 815 return 0, fmt.Errorf("info not found for previous block: %v", err) 816 } 817 } 818 sort.Slice(timestamps, func(i, j int) bool { 819 return timestamps[i] < timestamps[j] 820 }) 821 return timestamps[len(timestamps)/2], nil 822 } 823 824 // GetBlock returns the MsgBlock. 825 // Part of the Wallet interface. 826 func (w *spvWallet) GetBlock(ctx context.Context, blockHash *chainhash.Hash) (*wire.MsgBlock, error) { 827 if block := w.cachedBlock(blockHash); block != nil { 828 return block, nil 829 } 830 831 w.spvMtx.RLock() 832 blocks, err := w.spv.Blocks(ctx, []*chainhash.Hash{blockHash}) 833 w.spvMtx.RUnlock() 834 if err != nil { 835 return nil, err 836 } 837 if len(blocks) == 0 { // Shouldn't actually be possible. 838 return nil, fmt.Errorf("network returned 0 blocks") 839 } 840 841 block := blocks[0] 842 w.cacheBlock(block) 843 return block, nil 844 } 845 846 // GetTransaction returns the details of a wallet tx, if the wallet contains a 847 // tx with the provided hash. Returns asset.CoinNotFoundError if the tx is not 848 // found in the wallet. 849 // Part of the Wallet interface. 850 func (w *spvWallet) GetTransaction(ctx context.Context, txHash *chainhash.Hash) (*WalletTransaction, error) { 851 // copy-pasted from dcrwallet/internal/rpc/jsonrpc/methods.go 852 txd, err := w.dcrWallet.TxDetails(ctx, txHash) 853 if errors.Is(err, walleterrors.NotExist) { 854 return nil, asset.CoinNotFoundError 855 } else if err != nil { 856 return nil, err 857 } 858 859 _, tipHeight := w.MainChainTip(ctx) 860 861 ret := WalletTransaction{ 862 MsgTx: &txd.MsgTx, 863 } 864 865 if txd.Block.Height != -1 { 866 ret.BlockHash = txd.Block.Hash.String() 867 if tipHeight >= txd.Block.Height { 868 ret.Confirmations = int64(tipHeight - txd.Block.Height + 1) 869 } else { 870 ret.Confirmations = 1 871 } 872 } 873 874 details, err := w.ListTransactionDetails(ctx, txHash) 875 if err != nil { 876 return nil, err 877 } 878 ret.Details = make([]walletjson.GetTransactionDetailsResult, len(details)) 879 for i, d := range details { 880 ret.Details[i] = walletjson.GetTransactionDetailsResult{ 881 Account: d.Account, 882 Address: d.Address, 883 Amount: d.Amount, 884 Category: d.Category, 885 InvolvesWatchOnly: d.InvolvesWatchOnly, 886 Fee: d.Fee, 887 Vout: d.Vout, 888 } 889 } 890 891 return &ret, nil 892 } 893 894 // MatchAnyScript looks for any of the provided scripts in the block specified. 895 // Part of the Wallet interface. 896 func (w *spvWallet) MatchAnyScript(ctx context.Context, blockHash *chainhash.Hash, scripts [][]byte) (bool, error) { 897 key, filter, err := w.dcrWallet.CFilterV2(ctx, blockHash) 898 if err != nil { 899 return false, err 900 } 901 return filter.MatchAny(key, scripts), nil 902 903 } 904 905 // GetBestBlock returns the hash and height of the wallet's best block. 906 // Part of the Wallet interface. 907 func (w *spvWallet) GetBestBlock(ctx context.Context) (*chainhash.Hash, int64, error) { 908 blockHash, blockHeight := w.dcrWallet.MainChainTip(ctx) 909 return &blockHash, int64(blockHeight), nil 910 } 911 912 // GetBlockHash returns the hash of the mainchain block at the specified height. 913 // Part of the Wallet interface. 914 func (w *spvWallet) GetBlockHash(ctx context.Context, blockHeight int64) (*chainhash.Hash, error) { 915 info, err := w.dcrWallet.BlockInfo(ctx, wallet.NewBlockIdentifierFromHeight(int32(blockHeight))) 916 if err != nil { 917 return nil, err 918 } 919 return &info.Hash, nil 920 } 921 922 // AccountUnlocked returns true if the account is unlocked. 923 // Part of the Wallet interface. 924 func (w *spvWallet) AccountUnlocked(ctx context.Context, accountName string) (bool, error) { 925 acctNum, err := w.dcrWallet.AccountNumber(ctx, accountName) 926 if err != nil { 927 return false, err 928 } 929 return w.dcrWallet.AccountUnlocked(ctx, acctNum) 930 } 931 932 // LockAccount locks the specified account. 933 // Part of the Wallet interface. 934 func (w *spvWallet) LockAccount(ctx context.Context, accountName string) error { 935 acctNum, err := w.dcrWallet.AccountNumber(ctx, accountName) 936 if err != nil { 937 return err 938 } 939 return w.dcrWallet.LockAccount(ctx, acctNum) 940 } 941 942 // UnlockAccount unlocks the specified account or the wallet if account is not 943 // encrypted. Part of the Wallet interface. 944 func (w *spvWallet) UnlockAccount(ctx context.Context, pw []byte, accountName string) error { 945 acctNum, err := w.dcrWallet.AccountNumber(ctx, accountName) 946 if err != nil { 947 return err 948 } 949 return w.dcrWallet.UnlockAccount(ctx, acctNum, pw) 950 } 951 952 func (w *spvWallet) upgradeAccounts(ctx context.Context, pw []byte) error { 953 ew, ok := w.dcrWallet.(*extendedWallet) 954 if !ok { 955 return nil // assume the accts exist, since we can't verify 956 } 957 958 if err := ew.Unlock(ctx, pw, nil); err != nil { 959 return fmt.Errorf("cannot unlock wallet to check mixing accts: %v", err) 960 } 961 defer ew.Lock() 962 963 if err := setupMixingAccounts(ctx, ew.Wallet, pw); err != nil { 964 return err 965 } 966 return nil 967 } 968 969 // SyncStatus returns the wallet's sync status. 970 // Part of the Wallet interface. 971 func (w *spvWallet) SyncStatus(ctx context.Context) (*asset.SyncStatus, error) { 972 ss := new(asset.SyncStatus) 973 974 targetHeight := w.bestPeerInitialHeight() 975 if targetHeight == 0 { 976 return ss, nil 977 } 978 ss.TargetHeight = uint64(targetHeight) 979 980 _, height := w.dcrWallet.MainChainTip(ctx) 981 if height == 0 { 982 return ss, nil 983 } 984 height = utils.Clamp(height, 0, targetHeight) 985 ss.Blocks = uint64(height) 986 987 w.spvMtx.RLock() 988 ss.Synced, _ = w.spv.Synced(ctx) 989 w.spvMtx.RUnlock() 990 991 if rescanHash, err := w.dcrWallet.RescanPoint(ctx); err != nil { 992 return nil, fmt.Errorf("error getting rescan point: %w", err) 993 } else if rescanHash != nil { 994 rescanHeader, err := w.dcrWallet.BlockHeader(ctx, rescanHash) 995 if err != nil { 996 return nil, fmt.Errorf("error getting rescan point header: %w", err) 997 } 998 h := uint64(utils.Clamp(rescanHeader.Height, 1, uint32(targetHeight)+1) - 1) 999 ss.Transactions = &h 1000 } 1001 1002 return ss, nil 1003 } 1004 1005 // bestPeerInitialHeight is the highest InitialHeight recorded from connected 1006 // spv peers. If no peers are connected, the last observed max peer height is 1007 // returned. 1008 func (w *spvWallet) bestPeerInitialHeight() int32 { 1009 w.spvMtx.RLock() 1010 peers := w.spv.GetRemotePeers() 1011 w.spvMtx.RUnlock() 1012 if len(peers) == 0 { 1013 return atomic.LoadInt32(&w.bestSpvPeerHeight) 1014 } 1015 1016 var bestHeight int32 1017 for _, p := range peers { 1018 if h := p.InitialHeight(); h > bestHeight { 1019 bestHeight = h 1020 } 1021 } 1022 atomic.StoreInt32(&w.bestSpvPeerHeight, bestHeight) 1023 return bestHeight 1024 } 1025 1026 // AddressPrivKey fetches the privkey for the specified address. 1027 // Part of the Wallet interface. 1028 func (w *spvWallet) AddressPrivKey(ctx context.Context, addr stdaddr.Address) (*secp256k1.PrivateKey, error) { 1029 privKey, _, err := w.dcrWallet.LoadPrivateKey(ctx, addr) 1030 return privKey, err 1031 } 1032 1033 // StakeInfo returns the current stake info. 1034 func (w *spvWallet) StakeInfo(ctx context.Context) (*wallet.StakeInfoData, error) { 1035 return w.dcrWallet.StakeInfo(ctx) 1036 } 1037 1038 func (w *spvWallet) newVSPClient(vspHost, vspPubKey string, log dex.Logger) (*wallet.VSPClient, error) { 1039 return w.NewVSPClient(wallet.VSPClientConfig{ 1040 URL: vspHost, 1041 PubKey: vspPubKey, 1042 Policy: &wallet.VSPPolicy{ 1043 MaxFee: 0.2e8, 1044 FeeAcct: 0, 1045 ChangeAcct: 0, 1046 }, 1047 }, log, new(net.Dialer).DialContext) 1048 } 1049 1050 // rescan performs a blocking rescan, sending updates on the channel. 1051 func (w *spvWallet) rescan(ctx context.Context, fromHeight int32, c chan wallet.RescanProgress) { 1052 w.spvMtx.RLock() 1053 defer w.spvMtx.RUnlock() 1054 w.dcrWallet.RescanProgressFromHeight(ctx, w.spv, fromHeight, c) 1055 } 1056 1057 // PurchaseTickets purchases n tickets, tells the provided vspd to monitor the 1058 // ticket, and pays the vsp fee. 1059 func (w *spvWallet) PurchaseTickets(ctx context.Context, n int, vspHost, vspPubKey string, mixing bool) ([]*asset.Ticket, error) { 1060 vspClient, err := w.newVSPClient(vspHost, vspPubKey, w.log.SubLogger("VSP")) 1061 if err != nil { 1062 return nil, err 1063 } 1064 1065 req := &wallet.PurchaseTicketsRequest{ 1066 Count: n, 1067 VSPClient: vspClient, 1068 Mixing: mixing, 1069 } 1070 1071 if mixing { 1072 accts := w.Accounts() 1073 mixedAccountNum, err := w.AccountNumber(ctx, accts.PrimaryAccount) 1074 if err != nil { 1075 return nil, fmt.Errorf("error getting mixed account number: %w", err) 1076 } 1077 req.SourceAccount = mixedAccountNum 1078 // For simnet, we just change the source account. Others we need to 1079 // mix tickets through the cspp server. 1080 if w.chainParams.Net != wire.SimNet { 1081 req.MixedAccount = mixedAccountNum 1082 req.MixedAccountBranch = mixedAccountBranch 1083 req.MixedSplitAccount = req.MixedAccount 1084 req.ChangeAccount, err = w.AccountNumber(ctx, accts.UnmixedAccount) 1085 if err != nil { 1086 return nil, fmt.Errorf("error getting mixed change account number: %w", err) 1087 } 1088 } 1089 } 1090 1091 w.spvMtx.RLock() 1092 res, err := w.dcrWallet.PurchaseTickets(ctx, w.spv, req) 1093 w.spvMtx.RUnlock() 1094 if err != nil { 1095 return nil, err 1096 } 1097 1098 tickets := make([]*asset.Ticket, len(res.TicketHashes)) 1099 for i, h := range res.TicketHashes { 1100 w.log.Debugf("Purchased ticket %s", h) 1101 ticketSummary, hdr, err := w.dcrWallet.GetTicketInfo(ctx, h) 1102 if err != nil { 1103 return nil, fmt.Errorf("error fetching info for new ticket") 1104 } 1105 ticket := ticketSummaryToAssetTicket(ticketSummary, hdr, w.log) 1106 if ticket == nil { 1107 return nil, fmt.Errorf("invalid ticket summary for %s", h) 1108 } 1109 tickets[i] = ticket 1110 } 1111 return tickets, err 1112 } 1113 1114 const ( 1115 upperHeightMempool = -1 1116 lowerHeightAutomatic = -1 1117 pageSizeUnlimited = 0 1118 ) 1119 1120 // Tickets returns current active tickets. 1121 func (w *spvWallet) Tickets(ctx context.Context) ([]*asset.Ticket, error) { 1122 return w.ticketsInRange(ctx, lowerHeightAutomatic, upperHeightMempool, pageSizeUnlimited, 0) 1123 } 1124 1125 var _ ticketPager = (*spvWallet)(nil) 1126 1127 func (w *spvWallet) TicketPage(ctx context.Context, scanStart int32, n, skipN int) ([]*asset.Ticket, error) { 1128 if scanStart == -1 { 1129 _, scanStart = w.MainChainTip(ctx) 1130 } 1131 return w.ticketsInRange(ctx, 0, scanStart, n, skipN) 1132 } 1133 1134 func (w *spvWallet) ticketsInRange(ctx context.Context, lowerHeight, upperHeight int32, maxN, skipN /* 0 = mempool */ int) ([]*asset.Ticket, error) { 1135 p := w.chainParams 1136 var startBlock, endBlock *wallet.BlockIdentifier // null endBlock goes through mempool 1137 // If mempool is included, there is no way to scan backwards. 1138 includeMempool := upperHeight == upperHeightMempool 1139 if includeMempool { 1140 _, upperHeight = w.MainChainTip(ctx) 1141 } else { 1142 endBlock = wallet.NewBlockIdentifierFromHeight(upperHeight) 1143 } 1144 if lowerHeight == lowerHeightAutomatic { 1145 bn := upperHeight - int32(p.TicketExpiry+uint32(p.TicketMaturity)) 1146 startBlock = wallet.NewBlockIdentifierFromHeight(bn) 1147 } else { 1148 startBlock = wallet.NewBlockIdentifierFromHeight(lowerHeight) 1149 } 1150 1151 // If not looking at mempool, we can reverse iteration order by swapping 1152 // start and end blocks. 1153 if endBlock != nil { 1154 startBlock, endBlock = endBlock, startBlock 1155 } 1156 1157 tickets := make([]*asset.Ticket, 0) 1158 var skipped int 1159 processTicket := func(ticketSummaries []*wallet.TicketSummary, hdr *wire.BlockHeader) (bool, error) { 1160 for _, ticketSummary := range ticketSummaries { 1161 if skipped < skipN { 1162 skipped++ 1163 continue 1164 } 1165 if ticket := ticketSummaryToAssetTicket(ticketSummary, hdr, w.log); ticket != nil { 1166 tickets = append(tickets, ticket) 1167 } 1168 1169 if maxN > 0 && len(tickets) >= maxN { 1170 return true, nil 1171 } 1172 } 1173 1174 return false, nil 1175 } 1176 1177 if err := w.dcrWallet.GetTickets(ctx, processTicket, startBlock, endBlock); err != nil { 1178 return nil, err 1179 } 1180 1181 // If this is a mempool scan, we cannot scan backwards, so reverse the 1182 // result order. 1183 if includeMempool { 1184 utils.ReverseSlice(tickets) 1185 } 1186 1187 return tickets, nil 1188 } 1189 1190 // VotingPreferences returns current voting preferences. 1191 func (w *spvWallet) VotingPreferences(ctx context.Context) ([]*walletjson.VoteChoice, []*asset.TBTreasurySpend, []*walletjson.TreasuryPolicyResult, error) { 1192 _, agendas := wallet.CurrentAgendas(w.chainParams) 1193 1194 choices, _, err := w.dcrWallet.AgendaChoices(ctx, nil) 1195 if err != nil { 1196 return nil, nil, nil, fmt.Errorf("unable to get agenda choices: %v", err) 1197 } 1198 1199 voteChoices := make([]*walletjson.VoteChoice, len(choices)) 1200 1201 i := 0 1202 for agendaID, choiceID := range choices { 1203 voteChoices[i] = &walletjson.VoteChoice{ 1204 AgendaID: agendaID, 1205 ChoiceID: choiceID, 1206 } 1207 for _, agenda := range agendas { 1208 if agenda.Vote.Id != agendaID { 1209 continue 1210 } 1211 voteChoices[i].AgendaDescription = agenda.Vote.Description 1212 for _, choice := range agenda.Vote.Choices { 1213 if choiceID == choice.Id { 1214 voteChoices[i].ChoiceDescription = choice.Description 1215 break 1216 } 1217 } 1218 } 1219 i++ 1220 } 1221 policyToStr := func(p stake.TreasuryVoteT) string { 1222 var policy string 1223 switch p { 1224 case stake.TreasuryVoteYes: 1225 policy = "yes" 1226 case stake.TreasuryVoteNo: 1227 policy = "no" 1228 } 1229 return policy 1230 } 1231 tspends := w.dcrWallet.GetAllTSpends(ctx) 1232 tSpendPolicy := make([]*asset.TBTreasurySpend, 0, len(tspends)) 1233 for i := range tspends { 1234 msgTx := tspends[i] 1235 tspendHash := msgTx.TxHash() 1236 var val uint64 1237 for _, txOut := range msgTx.TxOut { 1238 val += uint64(txOut.Value) 1239 } 1240 p := w.dcrWallet.TSpendPolicy(&tspendHash, nil) 1241 tSpendPolicy = append(tSpendPolicy, &asset.TBTreasurySpend{ 1242 Hash: tspendHash.String(), 1243 CurrentPolicy: policyToStr(p), 1244 Value: val, 1245 }) 1246 } 1247 1248 policies := w.dcrWallet.TreasuryKeyPolicies() 1249 treasuryPolicy := make([]*walletjson.TreasuryPolicyResult, 0, len(policies)) 1250 for i := range policies { 1251 r := walletjson.TreasuryPolicyResult{ 1252 Key: hex.EncodeToString(policies[i].PiKey), 1253 Policy: policyToStr(policies[i].Policy), 1254 } 1255 if policies[i].Ticket != nil { 1256 r.Ticket = policies[i].Ticket.String() 1257 } 1258 treasuryPolicy = append(treasuryPolicy, &r) 1259 } 1260 1261 return voteChoices, tSpendPolicy, treasuryPolicy, nil 1262 } 1263 1264 // SetVotingPreferences sets voting preferences for the wallet and for vsps with 1265 // active tickets. 1266 func (w *spvWallet) SetVotingPreferences(ctx context.Context, choices, tspendPolicy, 1267 treasuryPolicy map[string]string) error { 1268 // Set the consensus vote choices for the wallet. 1269 if len(choices) > 0 { 1270 _, err := w.SetAgendaChoices(ctx, nil, choices) 1271 if err != nil { 1272 return err 1273 } 1274 } 1275 strToPolicy := func(s, t string) (stake.TreasuryVoteT, error) { 1276 var policy stake.TreasuryVoteT 1277 switch s { 1278 case "abstain", "invalid", "": 1279 policy = stake.TreasuryVoteInvalid 1280 case "yes": 1281 policy = stake.TreasuryVoteYes 1282 case "no": 1283 policy = stake.TreasuryVoteNo 1284 default: 1285 return 0, fmt.Errorf("unknown %s policy %q", t, s) 1286 } 1287 return policy, nil 1288 } 1289 // Set the tspend policy for the wallet. 1290 for k, v := range tspendPolicy { 1291 if len(k) != chainhash.MaxHashStringSize { 1292 return fmt.Errorf("invalid tspend hash length, expected %d got %d", 1293 chainhash.MaxHashStringSize, len(k)) 1294 } 1295 hash, err := chainhash.NewHashFromStr(k) 1296 if err != nil { 1297 return fmt.Errorf("invalid hash %s: %v", k, err) 1298 } 1299 policy, err := strToPolicy(v, "tspend") 1300 if err != nil { 1301 return err 1302 } 1303 err = w.dcrWallet.SetTSpendPolicy(ctx, hash, policy, nil) 1304 if err != nil { 1305 return err 1306 } 1307 } 1308 // Set the treasury policy for the wallet. 1309 for k, v := range treasuryPolicy { 1310 pikey, err := hex.DecodeString(k) 1311 if err != nil { 1312 return fmt.Errorf("unable to decode pi key %s: %v", k, err) 1313 } 1314 if len(pikey) != secp256k1.PubKeyBytesLenCompressed { 1315 return fmt.Errorf("treasury key %s must be 33 bytes", k) 1316 } 1317 policy, err := strToPolicy(v, "treasury") 1318 if err != nil { 1319 return err 1320 } 1321 err = w.dcrWallet.SetTreasuryKeyPolicy(ctx, pikey, policy, nil) 1322 if err != nil { 1323 return err 1324 } 1325 } 1326 clientCache := make(map[string]*wallet.VSPClient) 1327 // Set voting preferences for VSPs. Continuing for all errors. 1328 // NOTE: Doing this in an unmetered loop like this is a privacy breaker. 1329 return w.dcrWallet.ForUnspentUnexpiredTickets(ctx, func(hash *chainhash.Hash) error { 1330 vspHost, err := w.dcrWallet.VSPHostForTicket(ctx, hash) 1331 if err != nil { 1332 if errors.Is(err, walleterrors.NotExist) { 1333 w.log.Warnf("ticket %s is not associated with a VSP", hash) 1334 return nil 1335 } 1336 w.log.Warnf("unable to get VSP associated with ticket %s: %v", hash, err) 1337 return nil 1338 } 1339 vspClient, have := clientCache[vspHost] 1340 if !have { 1341 info, err := vspInfo(ctx, vspHost) 1342 if err != nil { 1343 w.log.Warnf("unable to get info from vsp at %s for ticket %s: %v", vspHost, hash, err) 1344 return nil 1345 } 1346 vspPubKey := base64.StdEncoding.EncodeToString(info.PubKey) 1347 vspClient, err = w.newVSPClient(vspHost, vspPubKey, w.log.SubLogger("VSP")) 1348 if err != nil { 1349 w.log.Warnf("unable to load vsp at %s for ticket %s: %v", vspHost, hash, err) 1350 return nil 1351 } 1352 } 1353 // Never return errors here, so all tickets are tried. 1354 // The first error will be returned to the user. 1355 vspTicket, err := w.NewVSPTicket(ctx, hash) 1356 if err != nil { 1357 w.log.Warnf("unable to create vsp ticket for vsp at %s for ticket %s: %v", vspHost, hash, err) 1358 } 1359 err = vspClient.SetVoteChoice(ctx, vspTicket, choices, tspendPolicy, treasuryPolicy) 1360 if err != nil { 1361 w.log.Warnf("unable to set vote for vsp at %s for ticket %s: %v", vspHost, hash, err) 1362 } 1363 return nil 1364 }) 1365 } 1366 1367 func (w *spvWallet) ListSinceBlock(ctx context.Context, start int32) ([]ListTransactionsResult, error) { 1368 res := make([]ListTransactionsResult, 0) 1369 f := func(block *wallet.Block) (bool, error) { 1370 for _, tx := range block.Transactions { 1371 convertTxType := func(txType wallet.TransactionType) *walletjson.ListTransactionsTxType { 1372 switch txType { 1373 case wallet.TransactionTypeTicketPurchase: 1374 txType := walletjson.LTTTTicket 1375 return &txType 1376 case wallet.TransactionTypeVote: 1377 txType := walletjson.LTTTVote 1378 return &txType 1379 case wallet.TransactionTypeRevocation: 1380 txType := walletjson.LTTTRevocation 1381 return &txType 1382 case wallet.TransactionTypeCoinbase: 1383 case wallet.TransactionTypeRegular: 1384 txType := walletjson.LTTTRegular 1385 return &txType 1386 } 1387 w.log.Warnf("unknown transaction type %v", tx.Type) 1388 regularTxType := walletjson.LTTTRegular 1389 return ®ularTxType 1390 } 1391 fee := tx.Fee.ToUnit(dcrutil.AmountCoin) 1392 var blockIndex, blockTime int64 1393 if block.Header != nil { 1394 blockIndex = int64(block.Header.Height) 1395 blockTime = block.Header.Timestamp.Unix() 1396 } 1397 res = append(res, ListTransactionsResult{ 1398 TxID: tx.Hash.String(), 1399 BlockIndex: &blockIndex, 1400 BlockTime: blockTime, 1401 Send: len(tx.MyInputs) > 0, 1402 TxType: convertTxType(tx.Type), 1403 Fee: &fee, 1404 }) 1405 } 1406 return false, nil 1407 } 1408 1409 startID := wallet.NewBlockIdentifierFromHeight(start) 1410 return res, w.dcrWallet.GetTransactions(ctx, f, startID, nil) 1411 } 1412 1413 func (w *spvWallet) SetTxFee(_ context.Context, feePerKB dcrutil.Amount) error { 1414 w.dcrWallet.SetRelayFee(feePerKB) 1415 return nil 1416 } 1417 1418 func (w *spvWallet) AddressUsed(ctx context.Context, addrStr string) (bool, error) { 1419 addr, err := stdaddr.DecodeAddress(addrStr, w.chainParams) 1420 if err != nil { 1421 return false, err 1422 } 1423 const minConf = 0 1424 recv, err := w.TotalReceivedForAddr(ctx, addr, minConf) 1425 if err != nil { 1426 return false, err 1427 } 1428 return recv != 0, nil 1429 } 1430 1431 // cacheBlock caches a block for future use. The block has a lastAccess stamp 1432 // added, and will be discarded if not accessed again within 2 hours. 1433 func (w *spvWallet) cacheBlock(block *wire.MsgBlock) { 1434 blockHash := block.BlockHash() 1435 w.blockCache.Lock() 1436 defer w.blockCache.Unlock() 1437 cached := w.blockCache.blocks[blockHash] 1438 if cached == nil { 1439 cb := &cachedBlock{ 1440 MsgBlock: block, 1441 lastAccess: time.Now(), 1442 } 1443 w.blockCache.blocks[blockHash] = cb 1444 } else { 1445 cached.lastAccess = time.Now() 1446 } 1447 } 1448 1449 // cachedBlock retrieves the MsgBlock from the cache, if it's been cached, else 1450 // nil. 1451 func (w *spvWallet) cachedBlock(blockHash *chainhash.Hash) *wire.MsgBlock { 1452 w.blockCache.Lock() 1453 defer w.blockCache.Unlock() 1454 cached := w.blockCache.blocks[*blockHash] 1455 if cached == nil { 1456 return nil 1457 } 1458 cached.lastAccess = time.Now() 1459 return cached.MsgBlock 1460 } 1461 1462 // PeerCount returns the count of currently connected peers. 1463 func (w *spvWallet) PeerCount(ctx context.Context) (uint32, error) { 1464 w.spvMtx.RLock() 1465 defer w.spvMtx.RUnlock() 1466 return uint32(len(w.spv.GetRemotePeers())), nil 1467 } 1468 1469 // cleanBlockCache discards from the blockCache any blocks that have not been 1470 // accessed for > 2 hours. 1471 func (w *spvWallet) cleanBlockCache() { 1472 w.blockCache.Lock() 1473 defer w.blockCache.Unlock() 1474 for blockHash, cb := range w.blockCache.blocks { 1475 if time.Since(cb.lastAccess) > time.Hour*2 { 1476 delete(w.blockCache.blocks, blockHash) 1477 } 1478 } 1479 } 1480 1481 func (w *spvWallet) newSpvSyncer() *spv.Syncer { 1482 w.spvMtx.Lock() 1483 defer w.spvMtx.Unlock() 1484 ew := w.dcrWallet.(*extendedWallet) 1485 dcrw := ew.Wallet 1486 var connectPeers []string 1487 switch w.chainParams.Net { 1488 case wire.SimNet: 1489 connectPeers = []string{"localhost:19560"} 1490 } 1491 addr := &net.TCPAddr{IP: net.ParseIP("::1"), Port: 0} 1492 amgr := addrmgr.New(w.dir) 1493 lp := p2p.NewLocalPeer(dcrw.ChainParams(), addr, amgr) 1494 syncer := spv.NewSyncer(dcrw, lp) 1495 if len(connectPeers) > 0 { 1496 syncer.SetPersistentPeers(connectPeers) 1497 } 1498 dcrw.SetNetworkBackend(syncer) 1499 w.spv = syncer 1500 return syncer 1501 } 1502 1503 // extendAddresses ensures that the internal and external branches have been 1504 // extended to the specified indices. This can be used at wallet restoration to 1505 // ensure that no duplicates are encountered with existing but unused addresses. 1506 func extendAddresses(ctx context.Context, extIdx, intIdx uint32, dcrw *wallet.Wallet) error { 1507 if err := dcrw.SyncLastReturnedAddress(ctx, defaultAcct, udb.ExternalBranch, extIdx); err != nil { 1508 return fmt.Errorf("error syncing external branch index: %w", err) 1509 } 1510 1511 if err := dcrw.SyncLastReturnedAddress(ctx, defaultAcct, udb.InternalBranch, intIdx); err != nil { 1512 return fmt.Errorf("error syncing internal branch index: %w", err) 1513 } 1514 1515 return nil 1516 } 1517 1518 func newWalletConfig(db wallet.DB, chainParams *chaincfg.Params, gapLimit uint32) *wallet.Config { 1519 if gapLimit < wallet.DefaultGapLimit { 1520 gapLimit = wallet.DefaultGapLimit 1521 } 1522 return &wallet.Config{ 1523 DB: db, 1524 GapLimit: gapLimit, 1525 AccountGapLimit: defaultAccountGapLimit, 1526 ManualTickets: defaultManualTickets, 1527 AllowHighFees: defaultAllowHighFees, 1528 RelayFee: defaultRelayFeePerKb, 1529 Params: chainParams, 1530 MixSplitLimit: defaultMixSplitLimit, 1531 // TODO: Wallet should be closed and reopened when turning on 1532 // and off mixing, and this should be a variable. 1533 MixingEnabled: true, 1534 } 1535 } 1536 1537 func checkCreateDir(path string) error { 1538 if fi, err := os.Stat(path); err != nil { 1539 if os.IsNotExist(err) { 1540 // Attempt data directory creation 1541 if err = os.MkdirAll(path, 0700); err != nil { 1542 return fmt.Errorf("cannot create directory: %s", err) 1543 } 1544 } else { 1545 return fmt.Errorf("error checking directory: %s", err) 1546 } 1547 } else if !fi.IsDir() { 1548 return fmt.Errorf("path '%s' is not a directory", path) 1549 } 1550 1551 return nil 1552 } 1553 1554 // walletExists returns whether a file exists at the loader's database path. 1555 // This may return an error for unexpected I/O failures. 1556 func walletExists(dbDir string) (bool, error) { 1557 return fileExists(filepath.Join(dbDir, walletDbName)) 1558 } 1559 1560 func fileExists(filePath string) (bool, error) { 1561 _, err := os.Stat(filePath) 1562 if err != nil { 1563 if os.IsNotExist(err) { 1564 return false, nil 1565 } 1566 return false, err 1567 } 1568 return true, nil 1569 } 1570 1571 // logWriter implements an io.Writer that outputs to a rotating log file. 1572 type logWriter struct { 1573 *rotator.Rotator 1574 } 1575 1576 // Write writes the data in p to the log file. 1577 func (w logWriter) Write(p []byte) (n int, err error) { 1578 return w.Rotator.Write(p) 1579 } 1580 1581 var ( 1582 // loggingInited will be set when the log rotator has been initialized. 1583 loggingInited uint32 1584 ) 1585 1586 // logRotator initializes a rotating file logger. 1587 func logRotator(netDir string) (*rotator.Rotator, error) { 1588 const maxLogRolls = 8 1589 logDir := filepath.Join(netDir, logDirName) 1590 if err := os.MkdirAll(logDir, 0744); err != nil { 1591 return nil, fmt.Errorf("error creating log directory: %w", err) 1592 } 1593 1594 logFilename := filepath.Join(logDir, logFileName) 1595 return rotator.New(logFilename, 32*1024, false, maxLogRolls) 1596 } 1597 1598 // initLogging initializes logging in the dcrwallet packages. Logging only has 1599 // to be initialized once, so an atomic flag is used internally to return early 1600 // on subsequent invocations. 1601 // 1602 // TODO: See if the below precaution is even necessary for dcrwallet. In theory, 1603 // the the rotating file logger must be Close'd at some point, but there are 1604 // concurrency issues with that since btcd and btcwallet have unsupervised 1605 // goroutines still running after shutdown. So we leave the rotator running at 1606 // the risk of losing some logs. 1607 func initLogging(netDir string) error { 1608 if !atomic.CompareAndSwapUint32(&loggingInited, 0, 1) { 1609 return nil 1610 } 1611 1612 logSpinner, err := logRotator(netDir) 1613 if err != nil { 1614 return fmt.Errorf("error initializing log rotator: %w", err) 1615 } 1616 1617 backendLog := slog.NewBackend(logWriter{logSpinner}) 1618 1619 logger := func(name string, lvl slog.Level) slog.Logger { 1620 l := backendLog.Logger(name) 1621 l.SetLevel(lvl) 1622 return l 1623 } 1624 wallet.UseLogger(logger("WLLT", slog.LevelInfo)) 1625 udb.UseLogger(logger("UDB", slog.LevelInfo)) 1626 chain.UseLogger(logger("CHAIN", slog.LevelInfo)) 1627 spv.UseLogger(logger("SPV", slog.LevelDebug)) 1628 p2p.UseLogger(logger("P2P", slog.LevelInfo)) 1629 connmgr.UseLogger(logger("CONMGR", slog.LevelInfo)) 1630 1631 return nil 1632 } 1633 1634 func ticketSummaryToAssetTicket(ticketSummary *wallet.TicketSummary, hdr *wire.BlockHeader, log dex.Logger) *asset.Ticket { 1635 spender := "" 1636 if ticketSummary.Spender != nil { 1637 spender = ticketSummary.Spender.Hash.String() 1638 } 1639 1640 if ticketSummary.Ticket == nil || len(ticketSummary.Ticket.MyOutputs) < 1 { 1641 log.Errorf("No zeroth output") 1642 return nil 1643 } 1644 1645 var blockHeight int64 = -1 1646 if hdr != nil { 1647 blockHeight = int64(hdr.Height) 1648 } 1649 1650 return &asset.Ticket{ 1651 Tx: asset.TicketTransaction{ 1652 Hash: ticketSummary.Ticket.Hash.String(), 1653 TicketPrice: uint64(ticketSummary.Ticket.MyOutputs[0].Amount), 1654 Fees: uint64(ticketSummary.Ticket.Fee), 1655 Stamp: uint64(ticketSummary.Ticket.Timestamp), 1656 BlockHeight: blockHeight, 1657 }, 1658 Status: asset.TicketStatus(ticketSummary.Status), 1659 Spender: spender, 1660 } 1661 }