/* * 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/tx" "github.com/conformal/btcwallet/wallet" "github.com/conformal/btcwire" "os" "strings" "time" ) // 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", } // errNoTxs describes an error where the wallet and UTXO files were // successfully read, but the TX history file was not. It is up to // the caller whether this necessitates a rescan or not. errNoTxs = errors.New("tx file cannot be read") ) // 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(cfg.Net()) if err := checkCreateDir(netdir); err != nil { return nil, &walletOpenError{ Err: err.Error(), } } wlt := new(wallet.Wallet) txs := tx.NewStore() a := &Account{ name: name, Wallet: wlt, TxStore: txs, } wfilepath := accountFilename("wallet.bin", name, netdir) txfilepath := accountFilename("tx.bin", name, netdir) var wfile, txfile *os.File // Read wallet file. wfile, err := os.Open(wfilepath) 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} } defer wfile.Close() if _, err = wlt.ReadFrom(wfile); err != nil { msg := fmt.Sprintf("cannot read wallet: %s", err) return nil, &walletOpenError{msg} } // Read tx file. If this fails, return a errNoTxs error and let // the caller decide if a rescan is necessary. var finalErr error if txfile, err = os.Open(txfilepath); err != nil { log.Errorf("cannot open tx file: %s", err) // This is not a error we should immediately return with, // but other errors can be more important, so only return // this if none of the others are hit. finalErr = errNoTxs a.fullRescan = true } else { defer txfile.Close() if _, err = txs.ReadFrom(txfile); err != nil { log.Errorf("cannot read tx file: %s", err) a.fullRescan = true finalErr = errNoTxs } } return a, finalErr } // 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(cfg.Net()) tmpNetDir := tmpNetworkDir(cfg.Net()) 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 { switch err.(type) { case *walletOpenError: log.Errorf("Default account wallet file unreadable: %v", err) return ad default: log.Warnf("Non-critical problem opening an account file: %v", err) } } 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 accountDir.Close() 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 { am.ds.FlushAccount(a) } 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.Message) 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, } } // 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) { log.Infof("Rolling back tx history since block height %v", height) for _, a := range am.AllAccounts() { a.TxStore.Rollback(height) am.ds.ScheduleTxStoreWrite(a) } } // 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 *tx.BlockDetails) { now := time.Now() var created time.Time if block != nil && now.After(block.Time) { created = block.Time } else { created = now } 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. a.TxStore.InsertSignedTx(tx_, created, block) am.ds.ScheduleTxStoreWrite(a) } } // 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, cfg.Net(), &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: tx.NewStore(), } 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 locked := a.Wallet.IsLocked(); !locked { if err := a.Wallet.Lock(); err != nil { return err } } if err := a.Wallet.Unlock(old); err != nil { return err } defer a.Wallet.Lock() } // 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) error { accts := am.AllAccounts() unlockedAccts := make([]*Account, 0, len(accts)) for _, a := range accts { if err := a.Unlock([]byte(passphrase)); err != nil { for _, ua := range unlockedAccts { ua.Lock() } return fmt.Errorf("cannot unlock account %v: %v", a.name, err) } unlockedAccts = append(unlockedAccts, a) } return nil } // 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) { for _, a := range am.AllAccounts() { switch wif, err := a.DumpWIFPrivateKey(addr); err { case wallet.ErrAddressNotFound: // Move on to the next account. continue case nil: return wif, nil default: // all other non-nil errors return "", err } } return "", errors.New("address does not refer to a key") } // 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 maps of strings to interface containing // structures defining 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) ([]map[string]interface{}, error) { // Create and fill a map of account names and their balances. txInfoList := []map[string]interface{}{} for _, a := range am.AllAccounts() { txTmp, err := a.ListSinceBlock(since, curBlockHeight, minconf) if err != nil { return nil, err } txInfoList = append(txInfoList, txTmp...) } return txInfoList, nil } // accountTx represents an account/transaction pair to be used by // GetTransaction. type accountTx struct { Account string Tx tx.Record } // 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.SortedRecords() { if *record.TxSha() != *txsha { continue } atx := accountTx{ Account: a.name, Tx: record, } accumulatedTxen = append(accumulatedTxen, atx) } } return accumulatedTxen } // ListUnspent returns an array of objects representing the unspent // wallet transactions fitting the given criteria. The confirmations will be // more then minconf, less than maxconf and if addresses is populated only the // addresses contained within it will be considered. // a transaction on locally known wallets. 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() { for _, rtx := range a.TxStore.UnspentOutputs() { confs := confirms(rtx.Height(), bs.Height) switch { case int(confs) < minconf, int(confs) > maxconf: continue } _, addrs, _, _ := rtx.Addresses(cfg.Net()) if filter { for _, addr := range addrs { _, ok := addresses[addr.EncodeAddress()] if ok { goto include } } continue } include: outpoint := rtx.OutPoint() result := &btcjson.ListUnSpentResult{ TxId: outpoint.Hash.String(), Vout: float64(outpoint.Index), Account: a.Name(), ScriptPubKey: hex.EncodeToString(rtx.PkScript()), Amount: float64(rtx.Value()) / 1e8, Confirmations: float64(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() { var job *RescanJob for _, a := range am.AllAccounts() { acctJob := a.RescanActiveJob() if job == nil { job = acctJob } else { job.Merge(acctJob) } } if job == nil { return } // Submit merged job and block until rescan completes. jobFinished := am.rm.SubmitJob(job) <-jobFinished } 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() } }