diff --git a/rpc/rpcserver/server.go b/rpc/rpcserver/server.go index 724b97d10e..9761706367 100644 --- a/rpc/rpcserver/server.go +++ b/rpc/rpcserver/server.go @@ -188,7 +188,10 @@ func (s *walletServer) Accounts(ctx context.Context, req *pb.AccountsRequest) ( func (s *walletServer) RenameAccount(ctx context.Context, req *pb.RenameAccountRequest) ( *pb.RenameAccountResponse, error) { - err := s.wallet.RenameAccountDeprecated(waddrmgr.KeyScopeBIP0044, req.GetAccountNumber(), req.GetNewName()) + err := s.wallet.RenameAccountDeprecated( + waddrmgr.KeyScopeBIP0044, req.GetAccountNumber(), + req.GetNewName(), + ) if err != nil { return nil, translateError(err) } diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index 4a612f5562..2cb707f5ef 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -1566,7 +1566,9 @@ func (s *ScopedKeyManager) CanAddAccount() error { // differs from the NewAccount method in that this method takes the account // number *directly*, rather than taking a string name for the account, then // mapping that to the next highest account number. -func (s *ScopedKeyManager) NewRawAccount(ns walletdb.ReadWriteBucket, number uint32) error { +func (s *ScopedKeyManager) NewRawAccount( + ns walletdb.ReadWriteBucket, number uint32) error { + s.mtx.Lock() defer s.mtx.Unlock() diff --git a/wallet/account_manager.go b/wallet/account_manager.go index 9406bc14c3..56245949fd 100644 --- a/wallet/account_manager.go +++ b/wallet/account_manager.go @@ -2,6 +2,11 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. +// Package wallet implements the account management for the wallet. +// +// TODO(yy): bring wrapcheck back when implementing the `Store` interface. +// +//nolint:wrapcheck package wallet import ( @@ -14,6 +19,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/walletdb" + "github.com/btcsuite/btcwallet/wtxmgr" ) // AccountManager provides a high-level interface for managing wallet @@ -118,6 +124,7 @@ func (w *Wallet) NewAccount(_ context.Context, scope waddrmgr.KeyScope, } var props *waddrmgr.AccountProperties + err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) @@ -129,6 +136,7 @@ func (w *Wallet) NewAccount(_ context.Context, scope waddrmgr.KeyScope, // Get the account's properties. props, err = manager.AccountProperties(addrmgrNs, accNum) + return err }) @@ -176,6 +184,7 @@ func (w *Wallet) ListAccounts(_ context.Context) (*AccountsResult, error) { scopes := w.addrStore.ActiveScopedKeyManagers() var accounts []AccountResult + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) @@ -187,8 +196,8 @@ func (w *Wallet) ListAccounts(_ context.Context) (*AccountsResult, error) { return err } - // Now, iterate through each key scope to assemble the final list - // of accounts with their properties and balances. + // Now, iterate through all key scopes to assemble the final + // list of accounts with their properties and balances. for _, scopeMgr := range scopes { scope := scopeMgr.Scope() accountBalances := scopedBalances[scope] @@ -217,6 +226,7 @@ func (w *Wallet) ListAccounts(_ context.Context) (*AccountsResult, error) { // Include the wallet's current sync state in the result to provide a // point-in-time reference for the balances. syncBlock := w.addrStore.SyncedTo() + return &AccountsResult{ Accounts: accounts, CurrentBlockHash: syncBlock.Hash, @@ -246,6 +256,7 @@ func (w *Wallet) ListAccountsByScope(_ context.Context, } var accounts []AccountResult + err = walletdb.View(w.db, func(tx walletdb.ReadTx) error { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) @@ -263,6 +274,7 @@ func (w *Wallet) ListAccountsByScope(_ context.Context, accounts, err = listAccountsWithBalances( manager, addrmgrNs, scopedBalances[scope], ) + return err }) if err != nil { @@ -271,6 +283,7 @@ func (w *Wallet) ListAccountsByScope(_ context.Context, // Include the wallet's current sync state in the result. syncBlock := w.addrStore.SyncedTo() + return &AccountsResult{ Accounts: accounts, CurrentBlockHash: syncBlock.Hash, @@ -294,6 +307,7 @@ func (w *Wallet) ListAccountsByName(_ context.Context, scopes := w.addrStore.ActiveScopedKeyManagers() var accounts []AccountResult + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { // First, calculate the balances for any accounts that match the // given name. This is efficient as it iterates over the UTXO @@ -333,6 +347,7 @@ func (w *Wallet) ListAccountsByName(_ context.Context, // Get the pre-calculated balance for this account. If // the account has no balance, it will be zero. var balance btcutil.Amount + balances, ok := scopedBalances[scopeMgr.Scope()] if ok { balance = balances[accNum] @@ -351,6 +366,7 @@ func (w *Wallet) ListAccountsByName(_ context.Context, } syncBlock := w.addrStore.SyncedTo() + return &AccountsResult{ Accounts: accounts, CurrentBlockHash: syncBlock.Hash, @@ -374,6 +390,7 @@ func (w *Wallet) GetAccount(_ context.Context, scope waddrmgr.KeyScope, } var account *AccountResult + err = walletdb.View(w.db, func(tx walletdb.ReadTx) error { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) @@ -435,7 +452,8 @@ func (w *Wallet) RenameAccount(_ context.Context, scope waddrmgr.KeyScope, // Validate the new account name to ensure it meets the required // criteria. - if err := waddrmgr.ValidateAccountName(newName); err != nil { + err = waddrmgr.ValidateAccountName(newName) + if err != nil { return err } @@ -467,6 +485,7 @@ func (w *Wallet) Balance(_ context.Context, conf int32, scope waddrmgr.KeyScope, name string) (btcutil.Amount, error) { var balance btcutil.Amount + err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) @@ -476,6 +495,7 @@ func (w *Wallet) Balance(_ context.Context, conf int32, if err != nil { return err } + accNum, err := manager.LookupAccount(addrmgrNs, name) if err != nil { return err @@ -484,10 +504,12 @@ func (w *Wallet) Balance(_ context.Context, conf int32, // Iterate through all unspent outputs and sum the balances for // the addresses that belong to the target account. syncBlock := w.addrStore.SyncedTo() + utxos, err := w.txStore.UnspentOutputs(txmgrNs) if err != nil { return err } + for _, utxo := range utxos { // Skip any UTXOs that have not yet reached the required // number of confirmations. @@ -495,29 +517,9 @@ func (w *Wallet) Balance(_ context.Context, conf int32, continue } - // Extract the address from the UTXO's public key script. - addr := extractAddrFromPKScript( - utxo.PkScript, w.chainParams, - ) - if addr == nil { - continue - } - - // Look up the account that owns the address. - addrScope, addrAcc, err := w.addrStore.AddrAccount( - addrmgrNs, addr, + balance += w.balanceForUTXO( + addrmgrNs, scope, accNum, utxo, ) - if err != nil { - // Ignore addresses that are not found in the - // wallet. - continue - } - - // If the address belongs to the target account, add the - // UTXO's value to the total balance. - if addrScope.Scope() == scope && addrAcc == accNum { - balance += utxo.Amount - } } return nil @@ -529,6 +531,34 @@ func (w *Wallet) Balance(_ context.Context, conf int32, return balance, nil } +// balanceForUTXO is a helper function for Balance that calculates the balance +// of a single UTXO if it belongs to the target account. +func (w *Wallet) balanceForUTXO(addrmgrNs walletdb.ReadBucket, + scope waddrmgr.KeyScope, accNum uint32, + utxo wtxmgr.Credit) btcutil.Amount { + + // Extract the address from the UTXO's public key script. + addr := extractAddrFromPKScript(utxo.PkScript, w.chainParams) + if addr == nil { + return 0 + } + + // Look up the account that owns the address. + addrScope, addrAcc, err := w.addrStore.AddrAccount(addrmgrNs, addr) + if err != nil { + // Ignore addresses that are not found in the wallet. + return 0 + } + + // If the address belongs to the target account, add the UTXO's value + // to the total balance. + if addrScope.Scope() == scope && addrAcc == accNum { + return utxo.Amount + } + + return 0 +} + // ImportAccount imports an account from an extended public or private key. // // The time complexity of this method is dominated by the database lookup @@ -558,6 +588,11 @@ func (w *Wallet) ImportAccount(_ context.Context, // extractAddrFromPKScript extracts an address from a public key script. If the // script cannot be parsed or does not contain any addresses, it returns nil. +// +// The btcutil.Address is an interface that abstracts over different address +// types. Returning the interface is idiomatic in this context. +// +//nolint:ireturn func extractAddrFromPKScript(pkScript []byte, chainParams *chaincfg.Params) btcutil.Address { @@ -690,6 +725,7 @@ func (w *Wallet) fetchAccountBalances(tx walletdb.ReadTx, if err != nil { log.Errorf("Unable to query account using address %v: "+ "%v", addr, err) + continue } @@ -744,6 +780,7 @@ func listAccountsWithBalances(scopeMgr *waddrmgr.ScopedKeyManager, accountBalances map[uint32]btcutil.Amount) ([]AccountResult, error) { var accounts []AccountResult + lastAccount, err := scopeMgr.LastAccount(addrmgrNs) if err != nil { // If the scope has no accounts, we can just return an empty @@ -751,6 +788,7 @@ func listAccountsWithBalances(scopeMgr *waddrmgr.ScopedKeyManager, if waddrmgr.IsError(err, waddrmgr.ErrAccountNotFound) { return nil, nil } + return nil, err } diff --git a/wallet/account_manager_test.go b/wallet/account_manager_test.go index 5e3d8199b4..5144477885 100644 --- a/wallet/account_manager_test.go +++ b/wallet/account_manager_test.go @@ -41,8 +41,8 @@ func TestNewAccount(t *testing.T) { ) require.NoError(t, err, "unable to create new account") - // The new account should be the first account created, so it should have - // an index of 1. + // The new account should be the first account created, so it should + // have an index of 1. require.Equal(t, uint32(1), account.AccountNumber, "expected account 1") // We should be able to retrieve the account by its name. @@ -106,6 +106,7 @@ func TestListAccounts(t *testing.T) { for _, acc := range accounts.Accounts { if acc.AccountName == testAccountName { found = true + require.Equal( t, uint32(1), acc.AccountNumber, "expected new account number", @@ -116,6 +117,7 @@ func TestListAccounts(t *testing.T) { ) } } + require.True(t, found, "expected to find new account") } @@ -249,7 +251,9 @@ func TestGetAccount(t *testing.T) { require.NoError(t, err) // We should be able to get the new account. - account, err := w.GetAccount(context.Background(), scope, testAccountName) + account, err := w.GetAccount( + context.Background(), scope, testAccountName, + ) require.NoError(t, err) require.Equal(t, testAccountName, account.AccountName) require.Equal(t, uint32(1), account.AccountNumber) @@ -362,6 +366,7 @@ func TestBalance(t *testing.T) { err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { ns := tx.ReadWriteBucket(wtxmgrNamespaceKey) + err := w.txStore.InsertTx(ns, rec, &wtxmgr.BlockMeta{ Block: wtxmgr.Block{ Height: 1, @@ -382,6 +387,7 @@ func TestBalance(t *testing.T) { // Now, we'll update the wallet's sync state. err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return w.addrStore.SetSyncedTo(addrmgrNs, &waddrmgr.BlockStamp{ Height: 1, }) @@ -469,7 +475,7 @@ func TestExtractAddrFromPKScript(t *testing.T) { t.Parallel() w, cleanup := testWallet(t) - defer cleanup() + t.Cleanup(cleanup) w.chainParams = &chaincfg.MainNetParams @@ -550,7 +556,6 @@ func TestExtractAddrFromPKScript(t *testing.T) { } for _, testCase := range testCases { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() @@ -558,7 +563,7 @@ func TestExtractAddrFromPKScript(t *testing.T) { testCase.script(), w.chainParams, ) if addr == nil { - require.Equal(t, testCase.addr, "") + require.Empty(t, testCase.addr) } else { require.Equal(t, testCase.addr, addr.String()) } @@ -570,6 +575,8 @@ func TestExtractAddrFromPKScript(t *testing.T) { func addTestUTXOForBalance(t *testing.T, w *Wallet, scope waddrmgr.KeyScope, account uint32, amount btcutil.Amount) { + t.Helper() + addr, err := w.NewAddress(account, scope) require.NoError(t, err) @@ -594,10 +601,12 @@ func addTestUTXOForBalance(t *testing.T, w *Wallet, scope waddrmgr.KeyScope, block := &wtxmgr.BlockMeta{ Block: wtxmgr.Block{Height: 1}, } + err := w.txStore.InsertTx(ns, rec, block) if err != nil { return err } + return w.txStore.AddCredit(ns, rec, block, 0, false) }) require.NoError(t, err) @@ -639,6 +648,8 @@ func TestFetchAccountBalances(t *testing.T) { // function that should be deferred by the caller to ensure that the // wallet's resources are properly released after the test completes. setupTestCase := func(t *testing.T) (*Wallet, func()) { + t.Helper() + w, cleanup := testWallet(t) ctx := context.Background() @@ -655,14 +666,20 @@ func TestFetchAccountBalances(t *testing.T) { // Add UTXOs. addTestUTXOForBalance(t, w, waddrmgr.KeyScopeBIP0084, 0, 100) addTestUTXOForBalance(t, w, waddrmgr.KeyScopeBIP0084, 1, 200) - addTestUTXOForBalance(t, w, waddrmgr.KeyScopeBIP0049Plus, 1, 300) + addTestUTXOForBalance( + t, w, waddrmgr.KeyScopeBIP0049Plus, 1, 300, + ) // Update sync state. - err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { - addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) - bs := &waddrmgr.BlockStamp{Height: 1} - return w.addrStore.SetSyncedTo(addrmgrNs, bs) - }) + err = walletdb.Update( + w.db, func(tx walletdb.ReadWriteTx) error { + addrmgrNs := tx.ReadWriteBucket( + waddrmgrNamespaceKey, + ) + bs := &waddrmgr.BlockStamp{Height: 1} + + return w.addrStore.SetSyncedTo(addrmgrNs, bs) + }) require.NoError(t, err) return w, cleanup @@ -694,6 +711,8 @@ func TestFetchAccountBalances(t *testing.T) { { name: "account with no balance", setup: func(t *testing.T, w *Wallet) { + t.Helper() + _, err := w.NewAccount( context.Background(), waddrmgr.KeyScopeBIP0084, "no-balance", @@ -709,7 +728,6 @@ func TestFetchAccountBalances(t *testing.T) { } for _, tc := range testCases { - tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() @@ -721,13 +739,17 @@ func TestFetchAccountBalances(t *testing.T) { } var balances scopedBalances - err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { - var err error - balances, err = w.fetchAccountBalances( - tx, tc.filters..., - ) - return err - }) + + err := walletdb.View( + w.db, func(tx walletdb.ReadTx) error { + var err error + + balances, err = w.fetchAccountBalances( + tx, tc.filters..., + ) + + return err + }) require.NoError(t, err) require.Equal(t, tc.expectedBalances, balances) diff --git a/wallet/chainntfns.go b/wallet/chainntfns.go index 8a1e6b1b03..2e96dbb09e 100644 --- a/wallet/chainntfns.go +++ b/wallet/chainntfns.go @@ -67,6 +67,7 @@ func (w *Wallet) handleChainNotifications() { Hash: *hash, Timestamp: header.Timestamp, } + err = w.addrStore.SetSyncedTo(ns, &bs) if err != nil { return err @@ -248,6 +249,7 @@ func (w *Wallet) connectBlock(dbtx walletdb.ReadWriteTx, b wtxmgr.BlockMeta) err Hash: b.Hash, Timestamp: b.Time, } + err := w.addrStore.SetSyncedTo(addrmgrNs, &bs) if err != nil { return err @@ -283,6 +285,7 @@ func (w *Wallet) disconnectBlock(dbtx walletdb.ReadWriteTx, b wtxmgr.BlockMeta) bs := waddrmgr.BlockStamp{ Height: b.Height - 1, } + hash, err = w.addrStore.BlockHash(addrmgrNs, bs.Height) if err != nil { return err @@ -296,6 +299,7 @@ func (w *Wallet) disconnectBlock(dbtx walletdb.ReadWriteTx, b wtxmgr.BlockMeta) } bs.Timestamp = header.Timestamp + err = w.addrStore.SetSyncedTo(addrmgrNs, &bs) if err != nil { return err @@ -390,6 +394,7 @@ func (w *Wallet) addRelevantTx(dbtx walletdb.ReadWriteTx, rec *wtxmgr.TxRecord, if err != nil { return err } + err = w.addrStore.MarkUsed(addrmgrNs, addr) if err != nil { return err diff --git a/wallet/createtx.go b/wallet/createtx.go index 3bf0df1dba..4bedee1956 100644 --- a/wallet/createtx.go +++ b/wallet/createtx.go @@ -399,7 +399,10 @@ func (w *Wallet) findEligibleOutputs(dbtx walletdb.ReadTx, if err != nil || len(addrs) != 1 { continue } - scopedMgr, addrAcct, err := w.addrStore.AddrAccount(addrmgrNs, addrs[0]) + + scopedMgr, addrAcct, err := w.addrStore.AddrAccount( + addrmgrNs, addrs[0], + ) if err != nil { continue } @@ -447,6 +450,7 @@ func (w *Wallet) addrMgrWithChangeSource(dbtx walletdb.ReadWriteTx, // It's possible for the account to have an address schema override, so // prefer that if it exists. addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + scopeMgr, err := w.addrStore.FetchScopedKeyManager(*changeKeyScope) if err != nil { return nil, nil, err diff --git a/wallet/createtx_test.go b/wallet/createtx_test.go index 0ffdc37e13..4a82c2d444 100644 --- a/wallet/createtx_test.go +++ b/wallet/createtx_test.go @@ -238,6 +238,7 @@ func addTxAndCredit(t *testing.T, w *Wallet, tx *wire.MsgTx, err = walletdb.Update(w.db, func(dbTx walletdb.ReadWriteTx) error { ns := dbTx.ReadWriteBucket(wtxmgrNamespaceKey) + err = w.txStore.InsertTx(ns, rec, block) if err != nil { return err diff --git a/wallet/import.go b/wallet/import.go index 99e7108b30..3403737998 100644 --- a/wallet/import.go +++ b/wallet/import.go @@ -189,7 +189,8 @@ func (w *Wallet) validateExtendedPubKey(pubKey *hdkeychain.ExtendedKey, return nil } -// ImportAccountDeprecated imports an account backed by an account extended public key. +// ImportAccountDeprecated imports an account backed by an account extended +// public key. // The master key fingerprint denotes the fingerprint of the root key // corresponding to the account public key (also known as the key with // derivation path m/). This may be required by some hardware wallets for proper @@ -208,7 +209,8 @@ func (w *Wallet) validateExtendedPubKey(pubKey *hdkeychain.ExtendedKey, // distinction between the traditional BIP-0049 address schema (nested witness // pubkeys everywhere) and our own BIP-0049Plus address schema (nested // externally, witness internally). -func (w *Wallet) ImportAccountDeprecated(name string, accountPubKey *hdkeychain.ExtendedKey, +func (w *Wallet) ImportAccountDeprecated( + name string, accountPubKey *hdkeychain.ExtendedKey, masterKeyFingerprint uint32, addrType *waddrmgr.AddressType) ( *waddrmgr.AccountProperties, error) { diff --git a/wallet/interface.go b/wallet/interface.go index ef21d5896d..27f2761cfa 100644 --- a/wallet/interface.go +++ b/wallet/interface.go @@ -120,11 +120,14 @@ type Interface interface { RenameAccountDeprecated(scope waddrmgr.KeyScope, account uint32, newName string) error - // ImportAccountDeprecated imports an account backed by an extended public key. + // ImportAccountDeprecated imports an account backed by an extended + // public key. + // // This creates a watch-only account. // // Deprecated: Use AccountManager.ImportAccount instead. - ImportAccountDeprecated(name string, accountPubKey *hdkeychain.ExtendedKey, + ImportAccountDeprecated(name string, + accountPubKey *hdkeychain.ExtendedKey, masterKeyFingerprint uint32, addrType *waddrmgr.AddressType, ) (*waddrmgr.AccountProperties, error) diff --git a/wallet/multisig.go b/wallet/multisig.go index 7891f5ee9d..8ab388999a 100644 --- a/wallet/multisig.go +++ b/wallet/multisig.go @@ -52,6 +52,7 @@ func (w *Wallet) MakeMultiSigScript(addrs []btcutil.Address, nRequired int) ([]b } addrmgrNs = dbtx.ReadBucket(waddrmgrNamespaceKey) } + addrInfo, err := w.addrStore.Address(addrmgrNs, addr) if err != nil { return nil, err diff --git a/wallet/notifications.go b/wallet/notifications.go index 45803b192f..9441dd617e 100644 --- a/wallet/notifications.go +++ b/wallet/notifications.go @@ -54,6 +54,7 @@ func lookupInputAccount(dbtx walletdb.ReadTx, w *Wallet, details *wtxmgr.TxDetai // TODO: Debits should record which account(s?) they // debit from so this doesn't need to be looked up. prevOP := &details.MsgTx.TxIn[deb.Index].PreviousOutPoint + prev, err := w.txStore.TxDetails(txmgrNs, &prevOP.Hash) if err != nil { log.Errorf("Cannot query previous transaction details for %v: %v", prevOP.Hash, err) @@ -155,7 +156,10 @@ func makeTxSummary(dbtx walletdb.ReadTx, w *Wallet, details *wtxmgr.TxDetails) T func totalBalances(dbtx walletdb.ReadTx, w *Wallet, m map[uint32]btcutil.Amount) error { addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) - unspent, err := w.txStore.UnspentOutputs(dbtx.ReadBucket(wtxmgrNamespaceKey)) + + unspent, err := w.txStore.UnspentOutputs( + dbtx.ReadBucket(wtxmgrNamespaceKey), + ) if err != nil { return err } @@ -165,7 +169,9 @@ func totalBalances(dbtx walletdb.ReadTx, w *Wallet, m map[uint32]btcutil.Amount) _, addrs, _, err := txscript.ExtractPkScriptAddrs( output.PkScript, w.chainParams) if err == nil && len(addrs) > 0 { - _, outputAcct, err = w.addrStore.AddrAccount(addrmgrNs, addrs[0]) + _, outputAcct, err = w.addrStore.AddrAccount( + addrmgrNs, addrs[0], + ) } if err == nil { _, ok := m[outputAcct] @@ -231,7 +237,10 @@ func (s *NotificationServer) notifyUnminedTransaction(dbtx walletdb.ReadTx, } unminedTxs := []TransactionSummary{makeTxSummary(dbtx, s.wallet, details)} - unminedHashes, err := s.wallet.txStore.UnminedTxHashes(dbtx.ReadBucket(wtxmgrNamespaceKey)) + + unminedHashes, err := s.wallet.txStore.UnminedTxHashes( + dbtx.ReadBucket(wtxmgrNamespaceKey), + ) if err != nil { log.Errorf("Cannot fetch unmined transaction hashes: %v", err) return @@ -353,6 +362,7 @@ func (s *NotificationServer) notifyAttachedBlock(dbtx walletdb.ReadTx, block *wt // a new, previously unseen transaction appearing in unconfirmed. txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) + unminedHashes, err := s.wallet.txStore.UnminedTxHashes(txmgrNs) if err != nil { log.Errorf("Cannot fetch unmined transaction hashes: %v", err) diff --git a/wallet/unstable.go b/wallet/unstable.go index 2b79c90bda..8386ed7e3d 100644 --- a/wallet/unstable.go +++ b/wallet/unstable.go @@ -28,6 +28,7 @@ func (u unstableAPI) TxDetails(txHash *chainhash.Hash) (*wtxmgr.TxDetails, error err := walletdb.View(u.w.db, func(dbtx walletdb.ReadTx) error { txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) var err error + details, err = u.w.txStore.TxDetails(txmgrNs, txHash) return err }) diff --git a/wallet/utxos.go b/wallet/utxos.go index c50ba622df..b714e60799 100644 --- a/wallet/utxos.go +++ b/wallet/utxos.go @@ -70,7 +70,10 @@ func (w *Wallet) UnspentOutputs(policy OutputSelectionPolicy) ([]*TransactionOut // per output. continue } - _, outputAcct, err := w.addrStore.AddrAccount(addrmgrNs, addrs[0]) + + _, outputAcct, err := w.addrStore.AddrAccount( + addrmgrNs, addrs[0], + ) if err != nil { return err } diff --git a/wallet/wallet.go b/wallet/wallet.go index 6c3a3671cb..a626de4cf4 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -3,6 +3,13 @@ // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. +// Package wallet provides a bitcoin wallet that is capable of fulfilling all +// the duties of a typical bitcoin wallet such as creating and managing keys, +// creating and signing transactions, and customizing of transaction fees. +// +// TODO(yy): bring wrapcheck back when implementing the `Store` interface. +// +//nolint:wrapcheck package wallet import ( @@ -363,6 +370,7 @@ func (w *Wallet) activeData(dbtx walletdb.ReadWriteTx) ([]btcutil.Address, []wtx txmgrNs := dbtx.ReadWriteBucket(wtxmgrNamespaceKey) var addrs []btcutil.Address + err := w.addrStore.ForEachRelevantActiveAddress( addrmgrNs, func(addr btcutil.Address) error { addrs = append(addrs, addr) @@ -449,6 +457,7 @@ func (w *Wallet) syncWithChain(birthdayStamp *waddrmgr.BlockStamp) error { err = walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { ns := tx.ReadWriteBucket(waddrmgrNamespaceKey) + err := w.addrStore.SetSyncedTo(ns, &waddrmgr.BlockStamp{ Hash: *startHash, Height: startHeight, @@ -458,7 +467,9 @@ func (w *Wallet) syncWithChain(birthdayStamp *waddrmgr.BlockStamp) error { return err } - return w.addrStore.SetBirthdayBlock(ns, *birthdayStamp, true) + return w.addrStore.SetBirthdayBlock( + ns, *birthdayStamp, true, + ) }) if err != nil { return fmt.Errorf("unable to persist initial sync "+ @@ -726,6 +737,7 @@ func (w *Wallet) recovery(chainClient chain.Interface, } err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { txMgrNS := tx.ReadBucket(wtxmgrNamespaceKey) + credits, err := w.txStore.UnspentOutputs(txMgrNS) if err != nil { return err @@ -754,6 +766,7 @@ func (w *Wallet) recovery(chainClient chain.Interface, // that a wallet rescan will be performed from the wallet's tip, which // will be of bestHeight after completing the recovery process. var blocks []*waddrmgr.BlockStamp + startHeight := w.addrStore.SyncedTo().Height + 1 for height := startHeight; height <= bestHeight; height++ { if atomic.LoadUint32(&syncer.quit) == 1 { @@ -803,7 +816,9 @@ func (w *Wallet) recovery(chainClient chain.Interface, // point to become desyncronized. Refactor so // that this cannot happen. for _, block := range blocks { - err := w.addrStore.SetSyncedTo(ns, block) + err := w.addrStore.SetSyncedTo( + ns, block, + ) if err != nil { return err } @@ -1420,7 +1435,10 @@ out: case req := <-w.unlockRequests: err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) - return w.addrStore.Unlock(addrmgrNs, req.passphrase) + + return w.addrStore.Unlock( + addrmgrNs, req.passphrase, + ) }) if err != nil { req.err <- err @@ -1438,6 +1456,7 @@ out: case req := <-w.changePassphrase: err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + return w.addrStore.ChangePassphrase( addrmgrNs, req.old, req.new, req.private, &waddrmgr.DefaultScryptOptions, @@ -1449,6 +1468,7 @@ out: case req := <-w.changePassphrases: err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { addrmgrNs := tx.ReadWriteBucket(waddrmgrNamespaceKey) + err := w.addrStore.ChangePassphrase( addrmgrNs, req.publicOld, req.publicNew, false, &waddrmgr.DefaultScryptOptions, @@ -1504,6 +1524,7 @@ out: <-w.endRecovery() timeout = nil + err := w.addrStore.Lock() if err != nil && !waddrmgr.IsError(err, waddrmgr.ErrLocked) { log.Errorf("Could not lock wallet: %v", err) @@ -1615,10 +1636,13 @@ func (w *Wallet) ChangePassphrases(publicOld, publicNew, privateOld, func (w *Wallet) AccountAddresses(account uint32) (addrs []btcutil.Address, err error) { err = walletdb.View(w.db, func(tx walletdb.ReadTx) error { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) - return w.addrStore.ForEachAccountAddress(addrmgrNs, account, func(maddr waddrmgr.ManagedAddress) error { - addrs = append(addrs, maddr.Address()) - return nil - }) + + return w.addrStore.ForEachAccountAddress( + addrmgrNs, account, + func(maddr waddrmgr.ManagedAddress) error { + addrs = append(addrs, maddr.Address()) + return nil + }) }) return } @@ -1668,6 +1692,7 @@ func (w *Wallet) CalculateBalance(confirms int32) (btcutil.Amount, error) { err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) var err error + blk := w.addrStore.SyncedTo() balance, err = w.txStore.Balance(txmgrNs, confirms, blk.Height) return err @@ -1710,7 +1735,9 @@ func (w *Wallet) CalculateAccountBalances(account uint32, confirms int32) (Balan _, addrs, _, err := txscript.ExtractPkScriptAddrs( output.PkScript, w.chainParams) if err == nil && len(addrs) > 0 { - _, outputAcct, err = w.addrStore.AddrAccount(addrmgrNs, addrs[0]) + _, outputAcct, err = w.addrStore.AddrAccount( + addrmgrNs, addrs[0], + ) } if err != nil || outputAcct != account { continue @@ -1806,6 +1833,7 @@ func (w *Wallet) PubKeyForAddress(a btcutil.Address) (*btcec.PublicKey, error) { var pubKey *btcec.PublicKey err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + managedAddr, err := w.addrStore.Address(addrmgrNs, a) if err != nil { return err @@ -1878,6 +1906,7 @@ func (w *Wallet) PrivKeyForAddress(a btcutil.Address) (*btcec.PrivateKey, error) var privKey *btcec.PrivateKey err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) + managedAddr, err := w.addrStore.Address(addrmgrNs, a) if err != nil { return err @@ -1914,6 +1943,7 @@ func (w *Wallet) AccountOfAddress(a btcutil.Address) (uint32, error) { err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) var err error + _, account, err = w.addrStore.AddrAccount(addrmgrNs, a) return err }) @@ -1926,6 +1956,7 @@ func (w *Wallet) AddressInfo(a btcutil.Address) (waddrmgr.ManagedAddress, error) err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) var err error + managedAddress, err = w.addrStore.Address(addrmgrNs, a) return err }) @@ -2020,9 +2051,11 @@ func (w *Wallet) LookupAccount(name string) (waddrmgr.KeyScope, uint32, error) { err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { ns := tx.ReadBucket(waddrmgrNamespaceKey) var err error + keyScope, account, err = w.addrStore.LookupAccount(ns, name) return err }) + return keyScope, account, err } @@ -2262,8 +2295,10 @@ func (w *Wallet) ListTransactions(from, count int) ([]btcjson.ListTransactionsRe return true, nil } - jsonResults := listTransactions(tx, &details[i], - w.addrStore, syncBlock.Height, w.chainParams) + jsonResults := listTransactions( + tx, &details[i], w.addrStore, + syncBlock.Height, w.chainParams, + ) txList = append(txList, jsonResults...) if len(jsonResults) > 0 { @@ -2313,8 +2348,10 @@ func (w *Wallet) ListAddressTransactions(pkHashes map[string]struct{}) ([]btcjso continue } - jsonResults := listTransactions(tx, detail, - w.addrStore, syncBlock.Height, w.chainParams) + jsonResults := listTransactions( + tx, detail, w.addrStore, + syncBlock.Height, w.chainParams, + ) txList = append(txList, jsonResults...) continue loopDetails } @@ -2345,8 +2382,10 @@ func (w *Wallet) ListAllTransactions() ([]btcjson.ListTransactionsResult, error) // unsorted, but it will process mined transactions in the // reverse order they were marked mined. for i := len(details) - 1; i >= 0; i-- { - jsonResults := listTransactions(tx, &details[i], w.addrStore, - syncBlock.Height, w.chainParams) + jsonResults := listTransactions( + tx, &details[i], w.addrStore, + syncBlock.Height, w.chainParams, + ) txList = append(txList, jsonResults...) } return false, nil @@ -2693,6 +2732,7 @@ func (w *Wallet) ListUnspent(minconf, maxconf int32, syncBlock := w.addrStore.SyncedTo() filter := accountName != "" + unspent, err := w.txStore.UnspentOutputs(txmgrNs) if err != nil { return err @@ -2738,7 +2778,9 @@ func (w *Wallet) ListUnspent(minconf, maxconf int32, continue } if len(addrs) > 0 { - smgr, acct, err := w.addrStore.AddrAccount(addrmgrNs, addrs[0]) + smgr, acct, err := w.addrStore.AddrAccount( + addrmgrNs, addrs[0], + ) if err == nil { s, err := smgr.AccountName(addrmgrNs, acct) if err == nil { @@ -2775,7 +2817,9 @@ func (w *Wallet) ListUnspent(minconf, maxconf int32, spendable = true case txscript.MultiSigTy: for _, a := range addrs { - _, err := w.addrStore.Address(addrmgrNs, a) + _, err := w.addrStore.Address( + addrmgrNs, a, + ) if err == nil { continue } @@ -2825,13 +2869,16 @@ func (w *Wallet) ListLeasedOutputs() ([]*ListLeasedOutputResult, error) { var results []*ListLeasedOutputResult err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { ns := tx.ReadBucket(wtxmgrNamespaceKey) + outputs, err := w.txStore.ListLockedOutputs(ns) if err != nil { return err } for _, output := range outputs { - details, err := w.txStore.TxDetails(ns, &output.Outpoint.Hash) + details, err := w.txStore.TxDetails( + ns, &output.Outpoint.Hash, + ) if err != nil { return err } @@ -2866,29 +2913,33 @@ func (w *Wallet) DumpPrivKeys() ([]string, error) { err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) // Iterate over each active address, appending the private key to - // privkeys. - return w.addrStore.ForEachActiveAddress(addrmgrNs, func(addr btcutil.Address) error { - ma, err := w.addrStore.Address(addrmgrNs, addr) - if err != nil { - return err - } + return w.addrStore.ForEachActiveAddress( + addrmgrNs, func(addr btcutil.Address) error { + ma, err := w.addrStore.Address(addrmgrNs, addr) + if err != nil { + return err + } - // Only those addresses with keys needed. - pka, ok := ma.(waddrmgr.ManagedPubKeyAddress) - if !ok { - return nil - } + // Only those addresses with keys needed. + pka, ok := ma.(waddrmgr.ManagedPubKeyAddress) + if !ok { + return nil + } - wif, err := pka.ExportPrivKey() - if err != nil { - // It would be nice to zero out the array here. However, - // since strings in go are immutable, and we have no - // control over the caller I don't think we can. :( - return err - } - privkeys = append(privkeys, wif.String()) - return nil - }) + wif, err := pka.ExportPrivKey() + if err != nil { + // It would be nice to zero out the + // array here. However, since strings + // in go are immutable, and we have no + // control over the caller I don't + // think we can. :( + return err + } + + privkeys = append(privkeys, wif.String()) + + return nil + }) }) return privkeys, err } @@ -2901,6 +2952,7 @@ func (w *Wallet) DumpWIFPrivateKey(addr btcutil.Address) (string, error) { waddrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) // Get private key from wallet if it exists. var err error + maddr, err = w.addrStore.Address(waddrmgrNs, addr) return err }) @@ -2998,6 +3050,7 @@ func (w *Wallet) LeaseOutput(id wtxmgr.LockID, op wire.OutPoint, err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { ns := tx.ReadWriteBucket(wtxmgrNamespaceKey) var err error + expiry, err = w.txStore.LockOutput(ns, id, op, duration) return err }) @@ -3022,6 +3075,7 @@ func (w *Wallet) resendUnminedTxs() { err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) var err error + txs, err = w.txStore.UnminedTxs(txmgrNs) return err }) @@ -3050,10 +3104,15 @@ func (w *Wallet) SortedActivePaymentAddresses() ([]string, error) { var addrStrs []string err := walletdb.View(w.db, func(tx walletdb.ReadTx) error { addrmgrNs := tx.ReadBucket(waddrmgrNamespaceKey) - return w.addrStore.ForEachActiveAddress(addrmgrNs, func(addr btcutil.Address) error { - addrStrs = append(addrStrs, addr.EncodeAddress()) - return nil - }) + + return w.addrStore.ForEachActiveAddress( + addrmgrNs, func(addr btcutil.Address) error { + addrStrs = append( + addrStrs, addr.EncodeAddress(), + ) + + return nil + }) }) if err != nil { return nil, err @@ -3257,6 +3316,7 @@ func (w *Wallet) TotalReceivedForAccounts(scope waddrmgr.KeyScope, stopHeight = -1 } + //nolint:lll rangeFn := func(details []wtxmgr.TxDetails) (bool, error) { for i := range details { detail := &details[i] @@ -3282,7 +3342,9 @@ func (w *Wallet) TotalReceivedForAccounts(scope waddrmgr.KeyScope, return false, nil } - return w.txStore.RangeTransactions(txmgrNs, 0, stopHeight, rangeFn) + return w.txStore.RangeTransactions( + txmgrNs, 0, stopHeight, rangeFn, + ) }) return results, err } @@ -3330,7 +3392,9 @@ func (w *Wallet) TotalReceivedForAddr(addr btcutil.Address, minConf int32) (btcu return false, nil } - return w.txStore.RangeTransactions(txmgrNs, 0, stopHeight, rangeFn) + return w.txStore.RangeTransactions( + txmgrNs, 0, stopHeight, rangeFn, + ) }) return amount, err } @@ -3449,7 +3513,10 @@ func (w *Wallet) SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType, if !ok { prevHash := &txIn.PreviousOutPoint.Hash prevIndex := txIn.PreviousOutPoint.Index - txDetails, err := w.txStore.TxDetails(txmgrNs, prevHash) + + txDetails, err := w.txStore.TxDetails( + txmgrNs, prevHash, + ) if err != nil { return fmt.Errorf("cannot query previous transaction "+ "details for %v: %w", txIn.PreviousOutPoint, err) @@ -3466,6 +3533,8 @@ func (w *Wallet) SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType, // Set up our callbacks that we pass to txscript so it can // look up the appropriate keys and scripts by address. + // + //nolint:lll getKey := txscript.KeyClosure(func(addr btcutil.Address) (*btcec.PrivateKey, bool, error) { if len(additionalKeysByAddress) != 0 { addrStr := addr.EncodeAddress() @@ -3476,6 +3545,7 @@ func (w *Wallet) SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType, } return wif.PrivKey, wif.CompressPubKey, nil } + address, err := w.addrStore.Address(addrmgrNs, addr) if err != nil { return nil, false, err @@ -3494,6 +3564,7 @@ func (w *Wallet) SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType, return key, pka.Compressed(), nil }) + //nolint:lll getScript := txscript.ScriptClosure(func(addr btcutil.Address) ([]byte, error) { // If keys were provided then we can only use the // redeem scripts provided with our inputs, too. @@ -3505,6 +3576,7 @@ func (w *Wallet) SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType, } return script, nil } + address, err := w.addrStore.Address(addrmgrNs, addr) if err != nil { return nil, err @@ -3702,7 +3774,9 @@ func (w *Wallet) reliablyPublishTransaction(tx *wire.MsgTx, // and record it in the tx store. if len(label) != 0 { txmgrNs := dbTx.ReadWriteBucket(wtxmgrNamespaceKey) - if err = w.txStore.PutTxLabel(txmgrNs, tx.TxHash(), label); err != nil { + + err = w.txStore.PutTxLabel(txmgrNs, tx.TxHash(), label) + if err != nil { return err } }