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