/* * Copyright (c) 2013, 2014 Conformal Systems LLC * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ package main import ( "encoding/hex" "errors" "fmt" "github.com/conformal/btcjson" "github.com/conformal/btcutil" "github.com/conformal/btcwallet/txstore" "github.com/conformal/btcwallet/wallet" "github.com/conformal/btcwire" "io/ioutil" "os" "path/filepath" "strings" ) // Errors relating to accounts. var ( ErrAccountExists = errors.New("account already exists") ErrWalletExists = errors.New("wallet already exists") ErrNotFound = errors.New("not found") ) // AcctMgr is the global account manager for all opened accounts. var AcctMgr = NewAccountManager() type openAccountsCmd struct{} type accessAccountRequest struct { name string resp chan *Account } type accessAllRequest struct { resp chan []*Account } type accessAccountByAddressRequest struct { address string resp chan *Account } type markAddressForAccountCmd struct { address string account *Account } type addAccountCmd struct { a *Account } type removeAccountCmd struct { a *Account } // AccountManager manages a collection of accounts. type AccountManager struct { // The accounts accessed through the account manager are not safe for // concurrent access. The account manager therefore contains a // binary semaphore channel to prevent incorrect access. bsem chan struct{} cmdChan chan interface{} rescanMsgs chan RescanMsg ds *DiskSyncer rm *RescanManager } // NewAccountManager returns a new AccountManager. func NewAccountManager() *AccountManager { am := &AccountManager{ bsem: make(chan struct{}, 1), cmdChan: make(chan interface{}), rescanMsgs: make(chan RescanMsg, 1), } am.ds = NewDiskSyncer(am) am.rm = NewRescanManager(am.rescanMsgs) return am } // Start starts the goroutines required to run the AccountManager. func (am *AccountManager) Start() { // Ready the semaphore - can't grab unless the manager has started. am.bsem <- struct{}{} go am.accountHandler() go am.rescanListener() go am.ds.Start() go am.rm.Start() } // accountData is a helper structure to let us centralise logic for adding // and removing accounts. type accountData struct { // maps name to account struct. We could keep a list here for iteration // but iteration over the small amounts we have is likely not worth // the extra complexity. nameToAccount map[string]*Account addressToAccount map[string]*Account } func newAccountData() *accountData { return &accountData{ nameToAccount: make(map[string]*Account), addressToAccount: make(map[string]*Account), } } func (ad *accountData) addAccount(a *Account) { if _, ok := ad.nameToAccount[a.name]; ok { return } ad.nameToAccount[a.name] = a for addr := range a.ActivePaymentAddresses() { ad.addressToAccount[addr] = a } } func (ad *accountData) removeAccount(a *Account) { a, ok := ad.nameToAccount[a.name] if !ok { return } delete(ad.nameToAccount, a.name) for addr := range a.ActivePaymentAddresses() { delete(ad.addressToAccount, addr) } } // walletOpenError is a special error type so problems opening wallet // files can be differentiated (by a type assertion) from other errors. type walletOpenError struct { Err string } // Error satisifies the builtin error interface. func (e *walletOpenError) Error() string { return e.Err } var ( // errNoWallet describes an error where a wallet does not exist and // must be created first. errNoWallet = &walletOpenError{ Err: "wallet file does not exist", } ) // openSavedAccount opens a named account from disk. If the wallet does not // exist, errNoWallet is returned as an error. func openSavedAccount(name string, cfg *config) (*Account, error) { netdir := networkDir(activeNet.Params) if err := checkCreateDir(netdir); err != nil { return nil, &walletOpenError{ Err: err.Error(), } } wlt := new(wallet.Wallet) txs := txstore.New() a := &Account{ name: name, Wallet: wlt, TxStore: txs, } walletPath := accountFilename("wallet.bin", name, netdir) txstorePath := accountFilename("tx.bin", name, netdir) // Read wallet file. walletFi, err := os.Open(walletPath) if err != nil { if os.IsNotExist(err) { // Must create and save wallet first. return nil, errNoWallet } msg := fmt.Sprintf("cannot open wallet file: %s", err) return nil, &walletOpenError{msg} } if _, err = wlt.ReadFrom(walletFi); err != nil { if err := walletFi.Close(); err != nil { log.Warnf("Cannot close wallet file: %v", err) } msg := fmt.Sprintf("Cannot read wallet: %s", err) return nil, &walletOpenError{msg} } // Read txstore file. If this fails, write a new empty transaction // store to disk, mark the wallet as unsynced, and write the unsynced // wallet to disk. // // This file is opened read/write so it may be truncated if a new empty // transaction store must be written. txstoreFi, err := os.OpenFile(txstorePath, os.O_RDWR, 0) if err != nil { if err := walletFi.Close(); err != nil { log.Warnf("Cannot close wallet file: %v", err) } if err := writeUnsyncedWallet(a, walletPath); err != nil { return nil, err } // Create and write empty txstore, if it doesn't exist. if !fileExists(txstorePath) { log.Warn("Transaction store file missing") if txstoreFi, err = os.Create(txstorePath); err != nil { return nil, fmt.Errorf("cannot create new "+ "txstore file: %v", err) } defer func() { if err := txstoreFi.Close(); err != nil { log.Warnf("Cannot close transaction "+ "store file: %v", err) } }() } else { return nil, fmt.Errorf("transaction store file "+ "exists but cannot be opened: %v", err) } if _, err := txs.WriteTo(txstoreFi); err != nil { log.Warn(err) } return a, nil } if _, err = txs.ReadFrom(txstoreFi); err != nil { if err := walletFi.Close(); err != nil { log.Warnf("Cannot close wallet file: %v", err) } if err := writeUnsyncedWallet(a, walletPath); err != nil { return nil, err } defer func() { if err := txstoreFi.Close(); err != nil { log.Warnf("Cannot close transaction store "+ "file: %v", err) } }() log.Warnf("Cannot read transaction store: %s", err) if _, err := txstoreFi.Seek(0, os.SEEK_SET); err != nil { return nil, err } if err := txstoreFi.Truncate(0); err != nil { return nil, err } if _, err := txs.WriteTo(txstoreFi); err != nil { log.Warn("Cannot write new transaction store: %v", err) } log.Infof("Wrote empty transaction store file") return a, nil } if err := walletFi.Close(); err != nil { log.Warnf("Cannot close wallet file: %v", err) } if err := txstoreFi.Close(); err != nil { log.Warnf("Cannot close transaction store file: %v", err) } return a, nil } // writeUnsyncedWallet sets the wallet unsynced (to handle the case // where the transaction store was unreadable) and atomically writes // the new wallet file back to disk. The current wallet file on disk // should be already closed, or this will error on Windows for ovewriting // an open file. func writeUnsyncedWallet(a *Account, path string) error { // Mark wallet as unsynced and write back to disk. Later calls // to SyncHeight will use the wallet creation height, or possibly // an earlier height for imported keys. netdir, _ := filepath.Split(path) a.SetSyncedWith(nil) tmpwallet, err := ioutil.TempFile(netdir, "wallet.bin") if err != nil { return fmt.Errorf("cannot create temporary wallet: %v", err) } if _, err := a.Wallet.WriteTo(tmpwallet); err != nil { return fmt.Errorf("cannot write back unsynced wallet: %v", err) } tmpwalletpath := tmpwallet.Name() if err := tmpwallet.Close(); err != nil { return fmt.Errorf("cannot close temporary wallet file: %v", err) } if err := Rename(tmpwalletpath, path); err != nil { return fmt.Errorf("cannot move temporary wallet file: %v", err) } return nil } // openAccounts attempts to open all saved accounts. func openAccounts() *accountData { ad := newAccountData() // If the network (account) directory is missing, but the temporary // directory exists, move it. This is unlikely to happen, but possible, // if writing out every account file at once to a tmp directory (as is // done for changing a wallet passphrase) and btcwallet closes after // removing the network directory but before renaming the temporary // directory. netDir := networkDir(activeNet.Params) tmpNetDir := tmpNetworkDir(activeNet.Params) if !fileExists(netDir) && fileExists(tmpNetDir) { if err := Rename(tmpNetDir, netDir); err != nil { log.Errorf("Cannot move temporary network dir: %v", err) return ad } } // The default account must exist, or btcwallet acts as if no // wallets/accounts have been created yet. a, err := openSavedAccount("", cfg) if err != nil { log.Errorf("Cannot open default account: %v", err) return ad } ad.addAccount(a) // Read all filenames in the account directory, and look for any // filenames matching '*-wallet.bin'. These are wallets for // additional saved accounts. accountDir, err := os.Open(netDir) if err != nil { // Can't continue. log.Errorf("Unable to open account directory: %v", err) return ad } defer func() { if err := accountDir.Close(); err != nil { log.Warnf("Cannot close account directory") } }() fileNames, err := accountDir.Readdirnames(0) if err != nil { // fileNames might be partially set, so log an error and // at least try to open some accounts. log.Errorf("Unable to read all account files: %v", err) } var accountNames []string for _, file := range fileNames { if strings.HasSuffix(file, "-wallet.bin") { name := strings.TrimSuffix(file, "-wallet.bin") accountNames = append(accountNames, name) } } // Open all additional accounts. for _, acctName := range accountNames { // Log txstore/utxostore errors as these will be recovered // from with a rescan, but wallet errors must be returned // to the caller. a, err := openSavedAccount(acctName, cfg) if err != nil { switch err.(type) { case *walletOpenError: log.Errorf("Error opening account's wallet: %v", err) default: log.Warnf("Non-critical error opening an account file: %v", err) } } else { ad.addAccount(a) } } return ad } // accountHandler maintains accounts and structures for quick lookups for // account information. Access to these structures must be requested via // cmdChan. cmdChan is a single channel for multiple command types since there // is ordering inherent in the commands and ordering between multipl goroutine // reads via select{} is very much undefined. This function never returns and // should be called as a new goroutine. func (am *AccountManager) accountHandler() { ad := openAccounts() for c := range am.cmdChan { switch cmd := c.(type) { case *openAccountsCmd: // Write all old accounts before proceeding. for _, a := range ad.nameToAccount { if err := am.ds.FlushAccount(a); err != nil { log.Errorf("Cannot write previously "+ "scheduled account file: %v", err) } } ad = openAccounts() case *accessAccountRequest: a, ok := ad.nameToAccount[cmd.name] if !ok { a = nil } cmd.resp <- a case *accessAccountByAddressRequest: a, ok := ad.addressToAccount[cmd.address] if !ok { a = nil } cmd.resp <- a case *accessAllRequest: s := make([]*Account, 0, len(ad.nameToAccount)) for _, a := range ad.nameToAccount { s = append(s, a) } cmd.resp <- s case *addAccountCmd: ad.addAccount(cmd.a) case *removeAccountCmd: ad.removeAccount(cmd.a) case *markAddressForAccountCmd: // TODO(oga) make sure we own account ad.addressToAccount[cmd.address] = cmd.account } } } // rescanListener listens for messages from the rescan manager and marks // accounts and addresses as synced. func (am *AccountManager) rescanListener() { for msg := range am.rescanMsgs { AcctMgr.Grab() switch e := msg.(type) { case *RescanStartedMsg: // Log the newly-started rescan. n := 0 for _, addrs := range e.Addresses { n += len(addrs) } noun := pickNoun(n, "address", "addresses") log.Infof("Started rescan at height %d for %d %s", e.StartHeight, n, noun) case *RescanProgressMsg: for acct, addrs := range e.Addresses { for i := range addrs { err := acct.SetSyncStatus(addrs[i], wallet.PartialSync(e.Height)) if err != nil { log.Errorf("Error marking address partially synced: %v", err) continue } } am.ds.ScheduleWalletWrite(acct) err := am.ds.FlushAccount(acct) if err != nil { log.Errorf("Could not write rescan progress: %v", err) } } log.Infof("Rescanned through block height %d", e.Height) case *RescanFinishedMsg: if e.Error != nil { log.Errorf("Rescan failed: %v", e.Error) break } n := 0 for acct, addrs := range e.Addresses { n += len(addrs) for i := range addrs { err := acct.SetSyncStatus(addrs[i], wallet.FullSync{}) if err != nil { log.Errorf("Error marking address synced: %v", err) continue } } am.ds.ScheduleWalletWrite(acct) err := am.ds.FlushAccount(acct) if err != nil { log.Errorf("Could not write rescan progress: %v", err) } } noun := pickNoun(n, "address", "addresses") log.Infof("Finished rescan for %d %s", n, noun) } AcctMgr.Release() } } // Grab grabs the account manager's binary semaphore. A custom semaphore // is used instead of a sync.Mutex so the account manager's disk syncer // can grab the semaphore from a select statement. func (am *AccountManager) Grab() { <-am.bsem } // Release releases exclusive ownership of the AccountManager. func (am *AccountManager) Release() { am.bsem <- struct{}{} } // OpenAccounts triggers the manager to reopen all known accounts. func (am *AccountManager) OpenAccounts() { am.cmdChan <- &openAccountsCmd{} } // Account returns the account specified by name, or ErrNotFound // as an error if the account is not found. func (am *AccountManager) Account(name string) (*Account, error) { respChan := make(chan *Account) am.cmdChan <- &accessAccountRequest{ name: name, resp: respChan, } resp := <-respChan if resp == nil { return nil, ErrNotFound } return resp, nil } // AccountByAddress returns the account specified by address, or // ErrNotFound as an error if the account is not found. func (am *AccountManager) AccountByAddress(addr btcutil.Address) (*Account, error) { respChan := make(chan *Account) am.cmdChan <- &accessAccountByAddressRequest{ address: addr.EncodeAddress(), resp: respChan, } resp := <-respChan if resp == nil { return nil, ErrNotFound } return resp, nil } // MarkAddressForAccount labels the given account as containing the provided // address. func (am *AccountManager) MarkAddressForAccount(address btcutil.Address, account *Account) { // TODO(oga) really this entire dance should be carried out implicitly // instead of requiring explicit messaging from the account to the // manager. am.cmdChan <- &markAddressForAccountCmd{ address: address.EncodeAddress(), account: account, } } // Address looks up an address if it is known to wallet at all. func (am *AccountManager) Address(addr btcutil.Address) (wallet.WalletAddress, error) { a, err := am.AccountByAddress(addr) if err != nil { return nil, err } return a.Address(addr) } // AllAccounts returns a slice of all managed accounts. func (am *AccountManager) AllAccounts() []*Account { respChan := make(chan []*Account) am.cmdChan <- &accessAllRequest{ resp: respChan, } return <-respChan } // AddAccount adds an account to the collection managed by an AccountManager. func (am *AccountManager) AddAccount(a *Account) { am.cmdChan <- &addAccountCmd{ a: a, } } // RemoveAccount removes an account to the collection managed by an // AccountManager. func (am *AccountManager) RemoveAccount(a *Account) { am.cmdChan <- &removeAccountCmd{ a: a, } } // RegisterNewAccount adds a new memory account to the account manager, // and immediately writes the account to disk. func (am *AccountManager) RegisterNewAccount(a *Account) error { am.AddAccount(a) // Ensure that the new account is written out to disk. am.ds.ScheduleWalletWrite(a) am.ds.ScheduleTxStoreWrite(a) if err := am.ds.FlushAccount(a); err != nil { am.RemoveAccount(a) return err } return nil } // Rollback rolls back each managed Account to the state before the block // specified by height and hash was connected to the main chain. func (am *AccountManager) Rollback(height int32, hash *btcwire.ShaHash) error { log.Infof("Rolling back tx history since block height %v", height) for _, a := range am.AllAccounts() { if err := a.TxStore.Rollback(height); err != nil { return err } am.ds.ScheduleTxStoreWrite(a) } return nil } // BlockNotify notifies all frontends of any changes from the new block, // including changed balances. Each account is then set to be synced // with the latest block. func (am *AccountManager) BlockNotify(bs *wallet.BlockStamp) { for _, a := range am.AllAccounts() { // TODO: need a flag or check that the utxo store was actually // modified, or this will notify even if there are no balance // changes, or sending these notifications as the utxos are added. confirmed := a.CalculateBalance(1) unconfirmed := a.CalculateBalance(0) - confirmed NotifyWalletBalance(allClients, a.name, confirmed) NotifyWalletBalanceUnconfirmed(allClients, a.name, unconfirmed) // If this is the default account, update the block all accounts // are synced with, and schedule a wallet write. if a.Name() == "" { a.Wallet.SetSyncedWith(bs) am.ds.ScheduleWalletWrite(a) } } } // RecordMinedTx searches through each account's TxStore, searching for a // sent transaction with the same txid as from a txmined notification. If // the transaction IDs match, the record in the TxStore is updated with // the full information about the newly-mined tx, and the TxStore is // scheduled to be written to disk.. func (am *AccountManager) RecordSpendingTx(tx *btcutil.Tx, block *txstore.Block) error { for _, a := range am.AllAccounts() { // TODO(jrick): This needs to iterate through each txout's // addresses and find whether this account's keystore contains // any of the addresses this tx sends to. txr, err := a.TxStore.InsertTx(tx, block) if err != nil { return err } // When received as a notification, we don't know what the inputs are. if _, err := txr.AddDebits(nil); err != nil { return err } am.ds.ScheduleTxStoreWrite(a) } return nil } // CalculateBalance returns the balance, calculated using minconf block // confirmations, of an account. func (am *AccountManager) CalculateBalance(account string, minconf int) (float64, error) { a, err := am.Account(account) if err != nil { return 0, err } return a.CalculateBalance(minconf), nil } // CreateEncryptedWallet creates a new default account with a wallet file // encrypted with passphrase. func (am *AccountManager) CreateEncryptedWallet(passphrase []byte) error { if len(am.AllAccounts()) != 0 { return ErrWalletExists } // Get current block's height and hash. bs, err := GetCurBlock() if err != nil { return err } // Create new wallet in memory. wlt, err := wallet.NewWallet("", "Default acccount", passphrase, activeNet.Params, &bs, cfg.KeypoolSize) if err != nil { return err } // Create new account and begin managing with the global account // manager. Registering will fail if the new account can not be // written immediately to disk. a := &Account{ Wallet: wlt, TxStore: txstore.New(), } if err := am.RegisterNewAccount(a); err != nil { return err } // Begin tracking account against a connected btcd. a.Track() return nil } // ChangePassphrase unlocks all account wallets with the old // passphrase, and re-encrypts each using the new passphrase. func (am *AccountManager) ChangePassphrase(old, new []byte) error { accts := am.AllAccounts() for _, a := range accts { if !a.IsLocked() { if err := a.Wallet.Lock(); err != nil { return err } } if err := a.Wallet.Unlock(old); err != nil { return err } defer func(a *Account) { if err := a.Lock(); err != nil { log.Warnf("Cannot lock account: %v", err) } }(a) } // Change passphrase for each unlocked wallet. for _, a := range accts { if err := a.Wallet.ChangePassphrase(new); err != nil { return err } } // Immediately write out to disk. return am.ds.WriteBatch(accts) } // LockWallets locks all managed account wallets. func (am *AccountManager) LockWallets() error { for _, a := range am.AllAccounts() { if err := a.Lock(); err != nil { return err } } return nil } // UnlockWallets unlocks all managed account's wallets. If any wallet unlocks // fail, all successfully unlocked wallets are locked again. func (am *AccountManager) UnlockWallets(passphrase string) (err error) { accts := am.AllAccounts() unlockedAccts := make([]*Account, 0, len(accts)) defer func() { // Lock all account wallets unlocked during this call // if any of the unlocks failed. if err != nil { for _, ua := range unlockedAccts { if err := ua.Lock(); err != nil { log.Warnf("Cannot lock account '%s': %v", ua.name, err) } } } }() for _, a := range accts { if uErr := a.Unlock([]byte(passphrase)); uErr != nil { err = fmt.Errorf("cannot unlock account %v: %v", a.name, uErr) return } unlockedAccts = append(unlockedAccts, a) } return } // DumpKeys returns all WIF-encoded private keys associated with all // accounts. All wallets must be unlocked for this operation to succeed. func (am *AccountManager) DumpKeys() ([]string, error) { var keys []string for _, a := range am.AllAccounts() { switch walletKeys, err := a.DumpPrivKeys(); err { case wallet.ErrWalletLocked: return nil, err case nil: keys = append(keys, walletKeys...) default: // any other non-nil error return nil, err } } return keys, nil } // DumpWIFPrivateKey searches through all accounts for the bitcoin // payment address addr and returns the WIF-encdoded private key. func (am *AccountManager) DumpWIFPrivateKey(addr btcutil.Address) (string, error) { a, err := am.AccountByAddress(addr) if err != nil { return "", err } return a.DumpWIFPrivateKey(addr) } // NotifyBalances notifies a wallet frontend of all confirmed and unconfirmed // account balances. func (am *AccountManager) NotifyBalances(frontend chan []byte) { for _, a := range am.AllAccounts() { balance := a.CalculateBalance(1) unconfirmed := a.CalculateBalance(0) - balance NotifyWalletBalance(frontend, a.name, balance) NotifyWalletBalanceUnconfirmed(frontend, a.name, unconfirmed) } } // ListAccounts returns a map of account names to their current account // balances. The balances are calculated using minconf confirmations. func (am *AccountManager) ListAccounts(minconf int) map[string]float64 { // Create and fill a map of account names and their balances. pairs := make(map[string]float64) for _, a := range am.AllAccounts() { pairs[a.name] = a.CalculateBalance(minconf) } return pairs } // ListSinceBlock returns a slice of objects representing all transactions in // the wallets since the given block. // To be used for the listsinceblock command. func (am *AccountManager) ListSinceBlock(since, curBlockHeight int32, minconf int) ([]btcjson.ListTransactionsResult, error) { // Create and fill a map of account names and their balances. var txList []btcjson.ListTransactionsResult for _, a := range am.AllAccounts() { txTmp, err := a.ListSinceBlock(since, curBlockHeight, minconf) if err != nil { return nil, err } txList = append(txList, txTmp...) } return txList, nil } // accountTx represents an account/transaction pair to be used by // GetTransaction. type accountTx struct { Account string Tx *txstore.TxRecord } // GetTransaction returns an array of accountTx to fully represent the effect of // a transaction on locally known wallets. If we know nothing about a // transaction an empty array will be returned. func (am *AccountManager) GetTransaction(txSha *btcwire.ShaHash) []accountTx { accumulatedTxen := []accountTx{} for _, a := range am.AllAccounts() { for _, record := range a.TxStore.Records() { if *record.Tx().Sha() != *txSha { continue } atx := accountTx{ Account: a.name, Tx: record, } accumulatedTxen = append(accumulatedTxen, atx) } } return accumulatedTxen } // ListUnspent returns a slice of objects representing the unspent wallet // transactions fitting the given criteria. The confirmations will be more than // minconf, less than maxconf and if addresses is populated only the addresses // contained within it will be considered. If we know nothing about a // transaction an empty array will be returned. func (am *AccountManager) ListUnspent(minconf, maxconf int, addresses map[string]bool) ([]*btcjson.ListUnspentResult, error) { bs, err := GetCurBlock() if err != nil { return nil, err } filter := len(addresses) != 0 var results []*btcjson.ListUnspentResult for _, a := range am.AllAccounts() { unspent, err := a.TxStore.SortedUnspentOutputs() if err != nil { return nil, err } for _, credit := range unspent { confs := credit.Confirmations(bs.Height) if int(confs) < minconf || int(confs) > maxconf { continue } _, addrs, _, _ := credit.Addresses(activeNet.Params) if filter { for _, addr := range addrs { _, ok := addresses[addr.EncodeAddress()] if ok { goto include } } continue } include: result := &btcjson.ListUnspentResult{ TxId: credit.Tx().Sha().String(), Vout: credit.OutputIndex, Account: a.Name(), ScriptPubKey: hex.EncodeToString(credit.TxOut().PkScript), Amount: credit.Amount().ToUnit(btcutil.AmountBTC), Confirmations: int64(confs), } // BUG: this should be a JSON array so that all // addresses can be included, or removed (and the // caller extracts addresses from the pkScript). if len(addrs) > 0 { result.Address = addrs[0].EncodeAddress() } results = append(results, result) } } return results, nil } // RescanActiveAddresses begins a rescan for all active addresses for // each account. func (am *AccountManager) RescanActiveAddresses() error { var job *RescanJob for _, a := range am.AllAccounts() { acctJob, err := a.RescanActiveJob() if err != nil { return err } if job == nil { job = acctJob } else { job.Merge(acctJob) } } if job != nil { // Submit merged job and block until rescan completes. jobFinished := am.rm.SubmitJob(job) <-jobFinished } return nil } func (am *AccountManager) ResendUnminedTxs() { for _, a := range am.AllAccounts() { a.ResendUnminedTxs() } } // Track begins tracking all addresses in all accounts for updates from // btcd. func (am *AccountManager) Track() { for _, a := range am.AllAccounts() { a.Track() } }