diff --git a/account.go b/account.go index 1c99721..72bb2db 100644 --- a/account.go +++ b/account.go @@ -26,12 +26,12 @@ import ( "github.com/conformal/btcwallet/wallet" "github.com/conformal/btcwire" "github.com/conformal/btcws" + "os" + "path/filepath" "sync" "time" ) -var accounts = NewAccountStore() - // Account is a structure containing all the components for a // complete wallet. It contains the Armory-style wallet (to store // addresses and keys), and tx and utxo data stores, along with locks @@ -56,30 +56,20 @@ type Account struct { } } -// AccountStore stores all wallets currently being handled by -// btcwallet. Wallet are stored in a map with the account name as the -// key. A RWMutex is used to protect against incorrect concurrent -// access. -type AccountStore struct { - sync.Mutex - m map[string]*Account +// Lock locks the underlying wallet for an account. +func (a *Account) Lock() error { + a.mtx.Lock() + defer a.mtx.Unlock() + + return a.Wallet.Lock() } -// NewAccountStore returns an initialized and empty AccountStore. -func NewAccountStore() *AccountStore { - return &AccountStore{ - m: make(map[string]*Account), - } -} +// Unlock unlocks the underlying wallet for an account. +func (a *Account) Unlock(passphrase []byte, timeout int64) error { + a.mtx.Lock() + defer a.mtx.Unlock() -// Rollback rolls back each Account saved in the store. -// -// TODO(jrick): This must also roll back the UTXO and TX stores, and notify -// all wallets of new account balances. -func (s *AccountStore) Rollback(height int32, hash *btcwire.ShaHash) { - for _, a := range s.m { - a.Rollback(height, hash) - } + return a.Wallet.Unlock(passphrase) } // Rollback reverts each stored Account to a state before the block @@ -129,6 +119,40 @@ func (a *Account) CalculateBalance(confirms int) float64 { return float64(bal) / float64(btcutil.SatoshiPerBitcoin) } +// ListTransactions returns a slice of maps with details about a recorded +// transaction. This is intended to be used for listtransactions RPC +// replies. +func (a *Account) ListTransactions(from, count int) ([]map[string]interface{}, error) { + // Get current block. The block height used for calculating + // the number of tx confirmations. + bs, err := GetCurBlock() + if err != nil { + return nil, err + } + + var txInfoList []map[string]interface{} + a.mtx.RLock() + a.TxStore.RLock() + + lastLookupIdx := len(a.TxStore.s) - count + // Search in reverse order: lookup most recently-added first. + for i := len(a.TxStore.s) - 1; i >= from && i >= lastLookupIdx; i-- { + switch e := a.TxStore.s[i].(type) { + case *tx.SendTx: + infos := e.TxInfo(a.name, bs.Height, a.Net()) + txInfoList = append(txInfoList, infos...) + + case *tx.RecvTx: + info := e.TxInfo(a.name, bs.Height, a.Net()) + txInfoList = append(txInfoList, info) + } + } + a.mtx.RUnlock() + a.TxStore.RUnlock() + + return txInfoList, nil +} + // DumpPrivKeys returns the WIF-encoded private keys for all addresses // non-watching addresses in a wallets. func (a *Account) DumpPrivKeys() ([]string, error) { @@ -138,8 +162,8 @@ func (a *Account) DumpPrivKeys() ([]string, error) { // Iterate over each active address, appending the private // key to privkeys. var privkeys []string - for _, addr := range a.GetActiveAddresses() { - key, err := a.GetAddressKey(addr.Address) + for _, addr := range a.ActiveAddresses() { + key, err := a.AddressKey(addr.Address) if err != nil { return nil, err } @@ -161,14 +185,14 @@ func (a *Account) DumpWIFPrivateKey(address string) (string, error) { defer a.mtx.RUnlock() // Get private key from wallet if it exists. - key, err := a.GetAddressKey(address) + key, err := a.AddressKey(address) if err != nil { return "", err } // Get address info. This is needed to determine whether // the pubkey is compressed or not. - info, err := a.GetAddressInfo(address) + info, err := a.AddressInfo(address) if err != nil { return "", err } @@ -177,12 +201,32 @@ func (a *Account) DumpWIFPrivateKey(address string) (string, error) { return btcutil.EncodePrivateKey(key.D.Bytes(), a.Net(), info.Compressed) } -// ImportWIFPrivateKey takes a WIF encoded private key and adds it to the +// ImportPrivKey imports a WIF-encoded private key into an account's wallet. +// This function is not recommended, as it gives no hints as to when the +// address first appeared (not just in the blockchain, but since the address +// was first generated, or made public), and will cause all future rescans to +// start from the genesis block. +func (a *Account) ImportPrivKey(wif string, rescan bool) error { + bs := &wallet.BlockStamp{} + addr, err := a.ImportWIFPrivateKey(wif, bs) + if err != nil { + return err + } + + if rescan { + addrs := map[string]struct{}{ + addr: struct{}{}, + } + + a.RescanAddresses(bs.Height, addrs) + } + return nil +} + +// ImportWIFPrivateKey takes a WIF-encoded private key and adds it to the // wallet. If the import is successful, the payment address string is // returned. -func (a *Account) ImportWIFPrivateKey(wif, label string, - bs *wallet.BlockStamp) (string, error) { - +func (a *Account) ImportWIFPrivateKey(wif string, bs *wallet.BlockStamp) (string, error) { // Decode WIF private key and perform sanity checking. privkey, net, compressed, err := btcutil.DecodePrivateKey(wif) if err != nil { @@ -229,7 +273,7 @@ func (a *Account) Track() { replyHandlers.Lock() replyHandlers.m[n] = a.newBlockTxOutHandler replyHandlers.Unlock() - for _, addr := range a.GetActiveAddresses() { + for _, addr := range a.ActiveAddresses() { a.ReqNewTxsForAddress(addr.Address) } @@ -254,6 +298,9 @@ func (a *Account) Track() { // it would have missed notifications as blocks are attached to the // main chain. func (a *Account) RescanActiveAddresses() { + a.mtx.RLock() + defer a.mtx.RUnlock() + // Determine the block to begin the rescan from. beginBlock := int32(0) if a.fullRescan { @@ -335,7 +382,7 @@ func (a *Account) SortedActivePaymentAddresses() []string { a.mtx.RLock() defer a.mtx.RUnlock() - infos := a.GetSortedActiveAddresses() + infos := a.SortedActiveAddresses() addrs := make([]string, len(infos)) for i, addr := range infos { @@ -351,7 +398,7 @@ func (a *Account) ActivePaymentAddresses() map[string]struct{} { a.mtx.RLock() defer a.mtx.RUnlock() - infos := a.GetActiveAddresses() + infos := a.ActiveAddresses() addrs := make(map[string]struct{}, len(infos)) for _, info := range infos { @@ -361,6 +408,35 @@ func (a *Account) ActivePaymentAddresses() map[string]struct{} { return addrs } +// NewAddress returns a new payment address for an account. +func (a *Account) NewAddress() (string, error) { + a.mtx.Lock() + defer a.mtx.Unlock() + + // Get current block's height and hash. + bs, err := GetCurBlock() + if err != nil { + return "", err + } + + // Get next address from wallet. + addr, err := a.NextChainedAddress(&bs) + if err != nil { + return "", err + } + + // Write updated wallet to disk. + a.dirty = true + if err = a.writeDirtyToDisk(); err != nil { + log.Errorf("cannot sync dirty wallet: %v", err) + } + + // Request updates from btcd for new transactions sent to this address. + a.ReqNewTxsForAddress(addr) + + return addr, nil +} + // ReqNewTxsForAddress sends a message to btcd to request tx updates // for addr for each new block that is added to the blockchain. func (a *Account) ReqNewTxsForAddress(addr string) { @@ -592,3 +668,37 @@ func (a *Account) newBlockTxOutHandler(result interface{}, e *btcjson.Error) boo // Never remove this handler. return false } + +// accountdir returns the directory path which holds an account's wallet, utxo, +// and tx files. +func (a *Account) accountdir(cfg *config) string { + var wname string + if a.name == "" { + wname = "btcwallet" + } else { + wname = fmt.Sprintf("btcwallet-%s", a.name) + } + + return filepath.Join(cfg.DataDir, wname) +} + +// checkCreateAccountDir checks that path exists and is a directory. +// If path does not exist, it is created. +func (a *Account) checkCreateAccountDir(path string) error { + fi, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + // Attempt data directory creation + if err = os.MkdirAll(path, 0700); err != nil { + return fmt.Errorf("cannot create account directory: %s", err) + } + } else { + return fmt.Errorf("error checking account directory: %s", err) + } + } else { + if !fi.IsDir() { + return fmt.Errorf("path '%s' is not a directory", cfg.DataDir) + } + } + return nil +} diff --git a/accountstore.go b/accountstore.go new file mode 100644 index 0000000..32fc3b5 --- /dev/null +++ b/accountstore.go @@ -0,0 +1,537 @@ +/* + * Copyright (c) 2013 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 ( + "bytes" + "errors" + "fmt" + "github.com/conformal/btcjson" + "github.com/conformal/btcwallet/tx" + "github.com/conformal/btcwallet/wallet" + "github.com/conformal/btcwire" + "os" + "path/filepath" + "sync" +) + +// Errors relating to accounts. +var ( + ErrAcctExists = errors.New("account already exists") + ErrAcctNotExist = errors.New("account does not exist") +) + +var accountstore = NewAccountStore() + +// AccountStore stores all wallets currently being handled by +// btcwallet. Wallet are stored in a map with the account name as the +// key. A RWMutex is used to protect against incorrect concurrent +// access. +type AccountStore struct { + sync.Mutex + accounts map[string]*Account +} + +// NewAccountStore returns an initialized and empty AccountStore. +func NewAccountStore() *AccountStore { + return &AccountStore{ + accounts: make(map[string]*Account), + } +} + +// Account returns the account specified by name, or ErrAcctNotExist +// as an error if the account is not found. +func (store *AccountStore) Account(name string) (*Account, error) { + store.Lock() + defer store.Unlock() + + account, ok := store.accounts[name] + if !ok { + return nil, ErrAcctNotExist + } + return account, nil +} + +// Rollback rolls back each Account saved in the store. +// +// TODO(jrick): This must also roll back the UTXO and TX stores, and notify +// all wallets of new account balances. +func (store *AccountStore) Rollback(height int32, hash *btcwire.ShaHash) { + store.Lock() + defer store.Unlock() + + for _, account := range store.accounts { + account.Rollback(height, hash) + } +} + +// BlockNotify runs after btcwallet is notified of a new block connected to +// the best chain. It 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 (store *AccountStore) BlockNotify(bs *wallet.BlockStamp) { + store.Lock() + defer store.Unlock() + + for _, a := range store.accounts { + // The UTXO store will be dirty if it was modified + // from a tx notification. + if a.UtxoStore.dirty { + // Notify all frontends of account's new unconfirmed + // and confirmed balance. + confirmed := a.CalculateBalance(1) + unconfirmed := a.CalculateBalance(0) - confirmed + NotifyWalletBalance(frontendNotificationMaster, + a.name, confirmed) + NotifyWalletBalanceUnconfirmed(frontendNotificationMaster, + a.name, unconfirmed) + } + + // The account is intentionaly not immediately synced to disk. + // If btcd is performing an IBD, writing the wallet file for + // each newly-connected block would result in too many + // unnecessary disk writes. The UTXO and transaction stores + // could be written, but in the case of btcwallet closing + // before writing the dirty wallet, both would have to be + // pruned anyways. + // + // Instead, the wallet is queued to be written to disk at the + // next scheduled disk sync. + a.mtx.Lock() + a.Wallet.SetSyncedWith(bs) + a.dirty = true + a.mtx.Unlock() + dirtyAccounts.Lock() + dirtyAccounts.m[a] = true + dirtyAccounts.Unlock() + } +} + +// 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 +// marked as dirty. +func (store *AccountStore) RecordMinedTx(txid *btcwire.ShaHash, + blkhash *btcwire.ShaHash, blkheight int32, blkindex int, + blktime int64) error { + + store.Lock() + defer store.Unlock() + + for _, account := range store.accounts { + account.TxStore.Lock() + + // Search in reverse order. Since more recently-created + // transactions are appended to the end of the store, it's + // more likely to find it when searching from the end. + for i := len(account.TxStore.s) - 1; i >= 0; i-- { + sendtx, ok := account.TxStore.s[i].(*tx.SendTx) + if ok { + if bytes.Equal(txid.Bytes(), sendtx.TxID[:]) { + copy(sendtx.BlockHash[:], blkhash.Bytes()) + sendtx.BlockHeight = blkheight + sendtx.BlockIndex = int32(blkindex) + sendtx.BlockTime = blktime + account.TxStore.dirty = true + account.TxStore.Unlock() + return nil + } + } + } + + account.TxStore.Unlock() + } + + return errors.New("txid does not match any recorded sent transaction") +} + +// CalculateBalance returns the balance, calculated using minconf +// block confirmations, of an account. +func (store *AccountStore) CalculateBalance(account string, + minconf int) (float64, error) { + + a, err := store.Account(account) + if err != nil { + return 0, err + } + + return a.CalculateBalance(minconf), nil +} + +// CreateEncryptedWallet creates a new account with a wallet file +// encrypted with passphrase. +// +// TODO(jrick): different passphrases on different accounts in the +// same wallet is a bad idea. Switch this to use one passphrase for all +// account wallet files. +func (store *AccountStore) CreateEncryptedWallet(name, desc string, passphrase []byte) error { + store.Lock() + defer store.Unlock() + + _, ok := store.accounts[name] + if ok { + return ErrAcctExists + } + + // Decide which Bitcoin network must be used. + var net btcwire.BitcoinNet + if cfg.MainNet { + net = btcwire.MainNet + } else { + net = btcwire.TestNet3 + } + + // Get current block's height and hash. + bs, err := GetCurBlock() + if err != nil { + return err + } + + // Create new wallet in memory. + wlt, err := wallet.NewWallet(name, desc, passphrase, net, &bs) + if err != nil { + return err + } + + // Create new account with the wallet. A new JSON ID is set for + // transaction notifications. + account := &Account{ + Wallet: wlt, + name: name, + dirty: true, + NewBlockTxJSONID: <-NewJSONID, + } + + // Save the account in the global account map. The mutex is + // already held at this point, and will be unlocked when this + // func returns. + store.accounts[name] = account + + // Begin tracking account against a connected btcd. + // + // TODO(jrick): this should *only* happen if btcd is connected. + account.Track() + + // Write new wallet to disk. + if err := account.writeDirtyToDisk(); err != nil { + log.Errorf("cannot sync dirty wallet: %v", err) + return nil + } + + return nil +} + +// DumpKeys returns all WIF-encoded private keys associated with all +// accounts. All wallets must be unlocked for this operation to succeed. +func (store *AccountStore) DumpKeys() ([]string, error) { + store.Lock() + defer store.Unlock() + + var keys []string + for _, a := range store.accounts { + 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 (store *AccountStore) DumpWIFPrivateKey(addr string) (string, error) { + store.Lock() + defer store.Unlock() + + for _, a := range store.accounts { + 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 (store *AccountStore) NotifyBalances(frontend chan []byte) { + store.Lock() + defer store.Unlock() + + for _, account := range store.accounts { + balance := account.CalculateBalance(1) + unconfirmed := account.CalculateBalance(0) - balance + NotifyWalletBalance(frontend, account.name, balance) + NotifyWalletBalanceUnconfirmed(frontend, account.name, unconfirmed) + } +} + +// ListAccounts returns a map of account names to their current account +// balances. The balances are calculated using minconf confirmations. +func (store *AccountStore) ListAccounts(minconf int) map[string]float64 { + store.Lock() + defer store.Unlock() + + // Create and fill a map of account names and their balances. + pairs := make(map[string]float64) + for name, a := range store.accounts { + pairs[name] = a.CalculateBalance(minconf) + } + return pairs +} + +// RescanActiveAddresses begins a rescan for all active addresses for +// each account. +// +// TODO(jrick): batch addresses for all accounts together so multiple +// rescan commands can be avoided. +func (store *AccountStore) RescanActiveAddresses() { + store.Lock() + defer store.Unlock() + + for _, account := range store.accounts { + account.RescanActiveAddresses() + } +} + +// Track begins tracking all addresses in all accounts for updates from +// btcd. +func (store *AccountStore) Track() { + store.Lock() + defer store.Unlock() + + for _, account := range store.accounts { + account.Track() + } +} + +// OpenAccount opens an account described by account in the data +// directory specified by cfg. If the wallet does not exist, ErrNoWallet +// is returned as an error. +// +// Wallets opened from this function are not set to track against a +// btcd connection. +func (store *AccountStore) OpenAccount(name string, cfg *config) error { + store.Lock() + defer store.Unlock() + + wlt := new(wallet.Wallet) + + account := &Account{ + Wallet: wlt, + name: name, + } + + var finalErr error + adir := account.accountdir(cfg) + if err := account.checkCreateAccountDir(adir); err != nil { + return err + } + + wfilepath := filepath.Join(adir, "wallet.bin") + utxofilepath := filepath.Join(adir, "utxo.bin") + txfilepath := filepath.Join(adir, "tx.bin") + var wfile, utxofile, 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 ErrNoWallet + } + return fmt.Errorf("cannot open wallet file: %s", err) + } + defer wfile.Close() + + if _, err = wlt.ReadFrom(wfile); err != nil { + return fmt.Errorf("cannot read wallet: %s", err) + } + + // Read tx file. If this fails, return a ErrNoTxs error and let + // the caller decide if a rescan is necessary. + 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 + } else { + defer txfile.Close() + var txs tx.TxStore + if _, err = txs.ReadFrom(txfile); err != nil { + log.Errorf("cannot read tx file: %s", err) + finalErr = ErrNoTxs + } else { + account.TxStore.s = txs + } + } + + // Read utxo file. If this fails, return a ErrNoUtxos error so a + // rescan can be done since the wallet creation block. + var utxos tx.UtxoStore + utxofile, err = os.Open(utxofilepath) + if err != nil { + log.Errorf("cannot open utxo file: %s", err) + finalErr = ErrNoUtxos + } else { + defer utxofile.Close() + if _, err = utxos.ReadFrom(utxofile); err != nil { + log.Errorf("cannot read utxo file: %s", err) + finalErr = ErrNoUtxos + } else { + account.UtxoStore.s = utxos + } + } + + switch finalErr { + case ErrNoTxs: + // Do nothing special for now. This will be implemented when + // the tx history file is properly written. + store.accounts[name] = account + + case ErrNoUtxos: + // Add wallet, but mark wallet as needing a full rescan since + // the wallet creation block. This will take place when btcd + // connects. + account.fullRescan = true + store.accounts[name] = account + case nil: + store.accounts[name] = account + + default: + log.Warnf("cannot open wallet: %v", err) + } + return nil +} + +func (store *AccountStore) handleSendRawTxReply(frontend chan []byte, icmd btcjson.Cmd, + result interface{}, e *btcjson.Error, a *Account, + txInfo *CreatedTx) bool { + + store.Lock() + defer store.Unlock() + + if e != nil { + ReplyError(frontend, icmd.Id(), e) + return true + } + + txIDStr, ok := result.(string) + if !ok { + e := &btcjson.Error{ + Code: btcjson.ErrInternal.Code, + Message: "Unexpected type from btcd reply", + } + ReplyError(frontend, icmd.Id(), e) + return true + } + txID, err := btcwire.NewShaHashFromStr(txIDStr) + if err != nil { + e := &btcjson.Error{ + Code: btcjson.ErrInternal.Code, + Message: "Invalid hash string from btcd reply", + } + ReplyError(frontend, icmd.Id(), e) + return true + } + + // Add to transaction store. + sendtx := &tx.SendTx{ + TxID: *txID, + Time: txInfo.time.Unix(), + BlockHeight: -1, + Fee: txInfo.fee, + Receivers: txInfo.outputs, + } + a.TxStore.Lock() + a.TxStore.s = append(a.TxStore.s, sendtx) + a.TxStore.dirty = true + a.TxStore.Unlock() + + // Remove previous unspent outputs now spent by the tx. + a.UtxoStore.Lock() + modified := a.UtxoStore.s.Remove(txInfo.inputs) + a.UtxoStore.dirty = a.UtxoStore.dirty || modified + + // Add unconfirmed change utxo (if any) to UtxoStore. + if txInfo.changeUtxo != nil { + a.UtxoStore.s = append(a.UtxoStore.s, txInfo.changeUtxo) + a.ReqSpentUtxoNtfn(txInfo.changeUtxo) + a.UtxoStore.dirty = true + } + a.UtxoStore.Unlock() + + // Disk sync tx and utxo stores. + if err := a.writeDirtyToDisk(); err != nil { + log.Errorf("cannot sync dirty wallet: %v", err) + } + + // Notify all frontends of account's new unconfirmed and + // confirmed balance. + confirmed := a.CalculateBalance(1) + unconfirmed := a.CalculateBalance(0) - confirmed + NotifyWalletBalance(frontendNotificationMaster, a.name, confirmed) + NotifyWalletBalanceUnconfirmed(frontendNotificationMaster, a.name, unconfirmed) + + // btcd cannot be trusted to successfully relay the tx to the + // Bitcoin network. Even if this succeeds, the rawtx must be + // saved and checked for an appearence in a later block. btcd + // will make a best try effort, but ultimately it's btcwallet's + // responsibility. + // + // Add hex string of raw tx to sent tx pool. If btcd disconnects + // and is reconnected, these txs are resent. + UnminedTxs.Lock() + UnminedTxs.m[TXID(*txID)] = txInfo + UnminedTxs.Unlock() + log.Infof("Successfully sent transaction %v", result) + ReplySuccess(frontend, icmd.Id(), result) + + // The comments to be saved differ based on the underlying type + // of the cmd, so switch on the type to check whether it is a + // SendFromCmd or SendManyCmd. + // + // TODO(jrick): If message succeeded in being sent, save the + // transaction details with comments. + switch cmd := icmd.(type) { + case *btcjson.SendFromCmd: + _ = cmd.Comment + _ = cmd.CommentTo + + case *btcjson.SendManyCmd: + _ = cmd.Comment + } + + return true +} diff --git a/cmd.go b/cmd.go index 3276b4c..e71b568 100644 --- a/cmd.go +++ b/cmd.go @@ -21,7 +21,6 @@ import ( "fmt" "github.com/conformal/btcjson" "github.com/conformal/btcutil" - "github.com/conformal/btcwallet/tx" "github.com/conformal/btcwallet/wallet" "github.com/conformal/btcwire" "github.com/conformal/btcws" @@ -30,7 +29,6 @@ import ( "net/http" _ "net/http/pprof" "os" - "path/filepath" "sync" "time" ) @@ -62,117 +60,6 @@ var ( } ) -// accountdir returns the directory path which holds an account's wallet, utxo, -// and tx files. -func accountdir(cfg *config, account string) string { - var wname string - if account == "" { - wname = "btcwallet" - } else { - wname = fmt.Sprintf("btcwallet-%s", account) - } - - return filepath.Join(cfg.DataDir, wname) -} - -// checkCreateAccountDir checks that path exists and is a directory. -// If path does not exist, it is created. -func checkCreateAccountDir(path string) error { - fi, err := os.Stat(path) - if err != nil { - if os.IsNotExist(err) { - // Attempt data directory creation - if err = os.MkdirAll(path, 0700); err != nil { - return fmt.Errorf("cannot create account directory: %s", err) - } - } else { - return fmt.Errorf("error checking account directory: %s", err) - } - } else { - if !fi.IsDir() { - return fmt.Errorf("path '%s' is not a directory", cfg.DataDir) - } - } - return nil -} - -// OpenAccount opens an account described by account in the data -// directory specified by cfg. If the wallet does not exist, ErrNoWallet -// is returned as an error. -// -// Wallets opened from this function are not set to track against a -// btcd connection. -func OpenAccount(cfg *config, account string) (*Account, error) { - var finalErr error - - adir := accountdir(cfg, account) - if err := checkCreateAccountDir(adir); err != nil { - return nil, err - } - - wfilepath := filepath.Join(adir, "wallet.bin") - utxofilepath := filepath.Join(adir, "utxo.bin") - txfilepath := filepath.Join(adir, "tx.bin") - var wfile, utxofile, 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 - } - return nil, fmt.Errorf("cannot open wallet file: %s", err) - } - defer wfile.Close() - - wlt := new(wallet.Wallet) - if _, err = wlt.ReadFrom(wfile); err != nil { - return nil, fmt.Errorf("cannot read wallet: %s", err) - } - - a := &Account{ - Wallet: wlt, - name: account, - } - - // Read tx file. If this fails, return a ErrNoTxs error and let - // the caller decide if a rescan is necessary. - 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 - } else { - defer txfile.Close() - var txs tx.TxStore - if _, err = txs.ReadFrom(txfile); err != nil { - log.Errorf("cannot read tx file: %s", err) - finalErr = ErrNoTxs - } else { - a.TxStore.s = txs - } - } - - // Read utxo file. If this fails, return a ErrNoUtxos error so a - // rescan can be done since the wallet creation block. - var utxos tx.UtxoStore - if utxofile, err = os.Open(utxofilepath); err != nil { - log.Errorf("cannot open utxo file: %s", err) - return a, ErrNoUtxos - } - defer utxofile.Close() - if _, err = utxos.ReadFrom(utxofile); err != nil { - log.Errorf("cannot read utxo file: %s", err) - finalErr = ErrNoUtxos - } else { - a.UtxoStore.s = utxos - } - - return a, finalErr -} - // GetCurBlock returns the blockchain height and SHA hash of the most // recently seen block. If no blocks have been seen since btcd has // connected, btcd is queried for the current block height and hash. @@ -318,31 +205,10 @@ func main() { } // Open default account - a, err := OpenAccount(cfg, "") - switch err { - case ErrNoTxs: - // Do nothing special for now. This will be implemented when - // the tx history file is properly written. - accounts.Lock() - accounts.m[""] = a - accounts.Unlock() - - case ErrNoUtxos: - // Add wallet, but mark wallet as needing a full rescan since - // the wallet creation block. This will take place when btcd - // connects. - accounts.Lock() - accounts.m[""] = a - accounts.Unlock() - a.fullRescan = true - - case nil: - accounts.Lock() - accounts.m[""] = a - accounts.Unlock() - - default: - log.Warnf("cannot open wallet: %v", err) + err = accountstore.OpenAccount("", cfg) + if err != nil { + log.Errorf("cannot open account: %v", err) + os.Exit(1) } // Read CA file to verify a btcd TLS connection. diff --git a/cmdmgr.go b/cmdmgr.go index 6520793..9eca514 100644 --- a/cmdmgr.go +++ b/cmdmgr.go @@ -177,42 +177,23 @@ func DumpPrivKey(frontend chan []byte, icmd btcjson.Cmd) { return } - // Iterate over all accounts, returning the key if it is found - // in any wallet. - for _, a := range accounts.m { - switch key, err := a.DumpWIFPrivateKey(cmd.Address); err { - case wallet.ErrAddressNotFound: - // Move on to the next account. - continue + switch key, err := accountstore.DumpWIFPrivateKey(cmd.Address); err { + case nil: + // Key was found. + ReplySuccess(frontend, cmd.Id(), key) - case wallet.ErrWalletLocked: - // Address was found, but the private key isn't - // accessible. - ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded) - return + case wallet.ErrWalletLocked: + // Address was found, but the private key isn't + // accessible. + ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded) - case nil: - // Key was found. - ReplySuccess(frontend, cmd.Id(), key) - return - - default: // all other non-nil errors - e := &btcjson.Error{ - Code: btcjson.ErrWallet.Code, - Message: err.Error(), - } - ReplyError(frontend, cmd.Id(), e) - return + default: // all other non-nil errors + e := &btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: err.Error(), } + ReplyError(frontend, cmd.Id(), e) } - - // If this is reached, all accounts have been checked, but none - // have the address. - e := &btcjson.Error{ - Code: btcjson.ErrWallet.Code, - Message: "Address does not refer to a key", - } - ReplyError(frontend, cmd.Id(), e) } // DumpWallet replies to a dumpwallet request with all private keys @@ -226,30 +207,22 @@ func DumpWallet(frontend chan []byte, icmd btcjson.Cmd) { return } - // Iterate over all accounts, appending the private keys - // for each. - var keys []string - for _, a := range accounts.m { - switch walletKeys, err := a.DumpPrivKeys(); err { - case wallet.ErrWalletLocked: - ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded) - return + switch keys, err := accountstore.DumpKeys(); err { + case nil: + // Reply with sorted WIF encoded private keys + ReplySuccess(frontend, cmd.Id(), keys) - case nil: - keys = append(keys, walletKeys...) + case wallet.ErrWalletLocked: + ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded) - default: // any other non-nil error - e := &btcjson.Error{ - Code: btcjson.ErrWallet.Code, - Message: err.Error(), - } - ReplyError(frontend, cmd.Id(), e) - return + default: // any other non-nil error + e := &btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: err.Error(), } + ReplyError(frontend, cmd.Id(), e) + return } - - // Reply with sorted WIF encoded private keys - ReplySuccess(frontend, cmd.Id(), keys) } // GetAddressesByAccount replies to a getaddressesbyaccount request with @@ -263,16 +236,22 @@ func GetAddressesByAccount(frontend chan []byte, icmd btcjson.Cmd) { return } - // Check that the account specified in the request exists. - a, ok := accounts.m[cmd.Account] - if !ok { + switch a, err := accountstore.Account(cmd.Account); err { + case nil: + // Reply with sorted active payment addresses. + ReplySuccess(frontend, cmd.Id(), a.SortedActivePaymentAddresses()) + + case ErrAcctNotExist: ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletInvalidAccountName) - return - } - // Reply with sorted active payment addresses. - ReplySuccess(frontend, cmd.Id(), a.SortedActivePaymentAddresses()) + default: // all other non-nil errors + e := &btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: err.Error(), + } + ReplyError(frontend, cmd.Id(), e) + } } // GetBalance replies to a getbalance request with the balance for an @@ -286,16 +265,15 @@ func GetBalance(frontend chan []byte, icmd btcjson.Cmd) { return } - // Check that the account specified in the request exists. - a, ok := accounts.m[cmd.Account] - if !ok { + balance, err := accountstore.CalculateBalance(cmd.Account, cmd.MinConf) + if err != nil { ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletInvalidAccountName) return } // Reply with calculated balance. - ReplySuccess(frontend, cmd.Id(), a.CalculateBalance(cmd.MinConf)) + ReplySuccess(frontend, cmd.Id(), balance) } // GetBalances replies to a getbalances extension request by notifying @@ -314,30 +292,19 @@ func ImportPrivKey(frontend chan []byte, icmd btcjson.Cmd) { return } - // Check that the account specified in the requests exists. - // Yes, Label is the account name. - a, ok := accounts.m[cmd.Label] - if !ok { + // Get the acount included in the request. Yes, Label is the + // account name... + a, err := accountstore.Account(cmd.Label) + switch err { + case nil: + break + + case ErrAcctNotExist: ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletInvalidAccountName) return - } - // Create a blockstamp for when this address first appeared. - // Because the importprivatekey RPC call does not allow - // specifying when the address first appeared, we must make - // a worst case guess. - bs := &wallet.BlockStamp{Height: 0} - - // Attempt importing the private key, replying with an appropiate - // error if the import was unsuccesful. - addr, err := a.ImportWIFPrivateKey(cmd.PrivKey, cmd.Label, bs) - switch { - case err == wallet.ErrWalletLocked: - ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded) - return - - case err != nil: + default: e := &btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), @@ -346,29 +313,32 @@ func ImportPrivKey(frontend chan []byte, icmd btcjson.Cmd) { return } - if cmd.Rescan { - addrs := map[string]struct{}{ - addr: struct{}{}, - } - a.RescanAddresses(bs.Height, addrs) - } + // Import the private key, handling any errors. + switch err := a.ImportPrivKey(cmd.PrivKey, cmd.Rescan); err { + case nil: + // If the import was successful, reply with nil. + ReplySuccess(frontend, cmd.Id(), nil) - // If the import was successful, reply with nil. - ReplySuccess(frontend, cmd.Id(), nil) + case wallet.ErrWalletLocked: + ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded) + + default: + e := &btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: err.Error(), + } + ReplyError(frontend, cmd.Id(), e) + } } // NotifyBalances notifies an attached frontend of the current confirmed // and unconfirmed account balances. // -// TODO(jrick): Switch this to return a JSON object (map) of all accounts -// and their balances, instead of separate notifications for each account. +// TODO(jrick): Switch this to return a single JSON object +// (map[string]interface{}) of all accounts and their balances, instead of +// separate notifications for each account. func NotifyBalances(frontend chan []byte) { - for _, a := range accounts.m { - balance := a.CalculateBalance(1) - unconfirmed := a.CalculateBalance(0) - balance - NotifyWalletBalance(frontend, a.name, balance) - NotifyWalletBalanceUnconfirmed(frontend, a.name, unconfirmed) - } + accountstore.NotifyBalances(frontend) } // GetNewAddress responds to a getnewaddress request by getting a new @@ -382,35 +352,25 @@ func GetNewAddress(frontend chan []byte, icmd btcjson.Cmd) { return } - // Check that the account specified in the request exists. - a, ok := accounts.m[cmd.Account] - if !ok { + a, err := accountstore.Account(cmd.Account) + switch err { + case nil: + break + + case ErrAcctNotExist: ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletInvalidAccountName) return - } - // Get current block's height and hash. - bs, err := GetCurBlock() - if err != nil { + case ErrBtcdDisconnected: e := &btcjson.Error{ Code: btcjson.ErrInternal.Code, Message: "btcd disconnected", } ReplyError(frontend, cmd.Id(), e) return - } - // Get next address from wallet. - addr, err := a.NextChainedAddress(&bs) - if err == wallet.ErrWalletLocked { - // The wallet is locked error may be sent if the keypool needs - // to be refilled, but the wallet is currently in a locked - // state. Notify the frontend that an unlock is needed to - // refill the keypool. - ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletKeypoolRanOut) - return - } else if err != nil { + default: // all other non-nil errors e := &btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: err.Error(), @@ -418,26 +378,27 @@ func GetNewAddress(frontend chan []byte, icmd btcjson.Cmd) { ReplyError(frontend, cmd.Id(), e) return } - if err != nil { - // TODO(jrick): generate new addresses if the address pool is - // empty. - e := btcjson.ErrInternal - e.Message = fmt.Sprintf("New address generation not implemented yet") - ReplyError(frontend, cmd.Id(), &e) - return + + addr, err := a.NewAddress() + switch err { + case nil: + // Reply with the new payment address string. + ReplySuccess(frontend, cmd.Id(), addr) + + case wallet.ErrWalletLocked: + // The wallet is locked error may be sent if the keypool needs + // to be refilled, but the wallet is currently in a locked + // state. Notify the frontend that an unlock is needed to + // refill the keypool. + ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletKeypoolRanOut) + + default: // all other non-nil errors + e := &btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: err.Error(), + } + ReplyError(frontend, cmd.Id(), e) } - - // Write updated wallet to disk. - a.dirty = true - if err = a.writeDirtyToDisk(); err != nil { - log.Errorf("cannot sync dirty wallet: %v", err) - } - - // Request updates from btcd for new transactions sent to this address. - a.ReqNewTxsForAddress(addr) - - // Reply with the new payment address string. - ReplySuccess(frontend, cmd.Id(), addr) } // ListAccounts replies to a listaccounts request by returning a JSON @@ -450,11 +411,7 @@ func ListAccounts(frontend chan []byte, icmd btcjson.Cmd) { return } - // Create and fill a map of account names and their balances. - pairs := make(map[string]float64) - for aname, a := range accounts.m { - pairs[aname] = a.CalculateBalance(cmd.MinConf) - } + pairs := accountstore.ListAccounts(cmd.MinConf) // Reply with the map. This will be marshaled into a JSON object. ReplySuccess(frontend, cmd.Id(), pairs) @@ -470,47 +427,44 @@ func ListTransactions(frontend chan []byte, icmd btcjson.Cmd) { return } - // Check that the account specified in the request exists. - a, ok := accounts.m[cmd.Account] - if !ok { + a, err := accountstore.Account(cmd.Account) + switch err { + case nil: + break + + case ErrAcctNotExist: ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletInvalidAccountName) return - } - // Get current block. The block height used for calculating - // the number of tx confirmations. - bs, err := GetCurBlock() - if err != nil { + default: // all other non-nil errors e := &btcjson.Error{ - Code: btcjson.ErrInternal.Code, + Code: btcjson.ErrWallet.Code, Message: err.Error(), } ReplyError(frontend, cmd.Id(), e) return } - a.mtx.RLock() - a.TxStore.RLock() - var txInfoList []map[string]interface{} - lastLookupIdx := len(a.TxStore.s) - cmd.Count - // Search in reverse order: lookup most recently-added first. - for i := len(a.TxStore.s) - 1; i >= cmd.From && i >= lastLookupIdx; i-- { - switch e := a.TxStore.s[i].(type) { - case *tx.SendTx: - infos := e.TxInfo(a.Name(), bs.Height, a.Net()) - txInfoList = append(txInfoList, infos...) + switch txList, err := a.ListTransactions(cmd.From, cmd.Count); err { + case nil: + // Reply with the list of tx information. + ReplySuccess(frontend, cmd.Id(), txList) - case *tx.RecvTx: - info := e.TxInfo(a.Name(), bs.Height, a.Net()) - txInfoList = append(txInfoList, info) + case ErrBtcdDisconnected: + e := &btcjson.Error{ + Code: btcjson.ErrInternal.Code, + Message: "btcd disconnected", } - } - a.mtx.RUnlock() - a.TxStore.RUnlock() + ReplyError(frontend, cmd.Id(), e) - // Reply with the list of tx information. - ReplySuccess(frontend, cmd.Id(), txInfoList) + default: + e := &btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: err.Error(), + } + ReplyError(frontend, cmd.Id(), e) + } } // SendFrom creates a new transaction spending unspent transaction @@ -545,8 +499,8 @@ func SendFrom(frontend chan []byte, icmd btcjson.Cmd) { } // Check that the account specified in the request exists. - a, ok := accounts.m[cmd.FromAccount] - if !ok { + a, err := accountstore.Account(cmd.FromAccount) + if err != nil { ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletInvalidAccountName) return @@ -648,8 +602,8 @@ func SendMany(frontend chan []byte, icmd btcjson.Cmd) { } // Check that the account specified in the request exists. - a, ok := accounts.m[cmd.FromAccount] - if !ok { + a, err := accountstore.Account(cmd.FromAccount) + if err != nil { ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletInvalidAccountName) return @@ -868,76 +822,27 @@ func CreateEncryptedWallet(frontend chan []byte, icmd btcjson.Cmd) { return } - // Grab the account map lock and defer the unlock. If an - // account is successfully created, it will be added to the - // map while the lock is held. - accounts.Lock() - defer accounts.Unlock() + err := accountstore.CreateEncryptedWallet(cmd.Account, cmd.Description, + []byte(cmd.Passphrase)) + switch err { + case nil: + // A nil reply is sent upon successful wallet creation. + ReplySuccess(frontend, cmd.Id(), nil) - // Does this wallet already exist? - if _, ok = accounts.m[cmd.Account]; ok { + case ErrAcctNotExist: ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletInvalidAccountName) - return - } - // Decide which Bitcoin network must be used. - var net btcwire.BitcoinNet - if cfg.MainNet { - net = btcwire.MainNet - } else { - net = btcwire.TestNet3 - } - - // Get current block's height and hash. - bs, err := GetCurBlock() - if err != nil { + case ErrBtcdDisconnected: e := &btcjson.Error{ Code: btcjson.ErrInternal.Code, Message: "btcd disconnected", } ReplyError(frontend, cmd.Id(), e) - return - } - // Create new wallet in memory. - wlt, err := wallet.NewWallet(cmd.Account, cmd.Description, - []byte(cmd.Passphrase), net, &bs) - if err != nil { - log.Error("Error creating wallet: " + err.Error()) + default: ReplyError(frontend, cmd.Id(), &btcjson.ErrInternal) - return } - - // Create new account with the wallet. A new JSON ID is set for - // transaction notifications. - a := &Account{ - Wallet: wlt, - name: cmd.Account, - dirty: true, - NewBlockTxJSONID: <-NewJSONID, - } - - // Begin tracking account against a connected btcd. - // - // TODO(jrick): this should *only* happen if btcd is connected. - a.Track() - - // Save the account in the global account map. The mutex is - // already held at this point, and will be unlocked when this - // func returns. - accounts.m[cmd.Account] = a - - // Write new wallet to disk. - if err := a.writeDirtyToDisk(); err != nil { - log.Errorf("cannot sync dirty wallet: %v", err) - } - - // Notify all frontends of this new account, and its balance. - NotifyBalances(frontendNotificationMaster) - - // A nil reply is sent upon successful wallet creation. - ReplySuccess(frontend, cmd.Id(), nil) } // WalletIsLocked responds to the walletislocked extension request by @@ -952,16 +857,31 @@ func WalletIsLocked(frontend chan []byte, icmd btcjson.Cmd) { return } - // Check that the account specified in the request exists. - a, ok := accounts.m[cmd.Account] - if !ok { + a, err := accountstore.Account(cmd.Account) + switch err { + case nil: + break + + case ErrAcctNotExist: ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletInvalidAccountName) return + + default: // all other non-nil errors + e := &btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: err.Error(), + } + ReplyError(frontend, cmd.Id(), e) + return } + a.mtx.RLock() + locked := a.Wallet.IsLocked() + a.mtx.RUnlock() + // Reply with true for a locked wallet, and false for unlocked. - ReplySuccess(frontend, cmd.Id(), a.IsLocked()) + ReplySuccess(frontend, cmd.Id(), locked) } // WalletLock responds to walletlock request by locking the wallet, @@ -971,17 +891,35 @@ func WalletIsLocked(frontend chan []byte, icmd btcjson.Cmd) { // with this. Lock all the wallets, like if all accounts are locked // for one bitcoind wallet? func WalletLock(frontend chan []byte, icmd btcjson.Cmd) { - if a, ok := accounts.m[""]; ok { - if err := a.Lock(); err != nil { - ReplyError(frontend, icmd.Id(), - &btcjson.ErrWalletWrongEncState) - return + a, err := accountstore.Account("") + switch err { + case nil: + break + + case ErrAcctNotExist: + e := &btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: "default account does not exist", } + ReplyError(frontend, icmd.Id(), e) + return + + default: // all other non-nil errors + e := &btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: err.Error(), + } + ReplyError(frontend, icmd.Id(), e) + return + } + + switch err := a.Lock(); err { + case nil: ReplySuccess(frontend, icmd.Id(), nil) - NotifyWalletLockStateChange("", true) - } else { + + default: ReplyError(frontend, icmd.Id(), - &btcjson.ErrWalletInvalidAccountName) + &btcjson.ErrWalletWrongEncState) } } @@ -998,23 +936,45 @@ func WalletPassphrase(frontend chan []byte, icmd btcjson.Cmd) { return } - if a, ok := accounts.m[""]; ok { - if err := a.Unlock([]byte(cmd.Passphrase)); err != nil { - ReplyError(frontend, cmd.Id(), - &btcjson.ErrWalletPassphraseIncorrect) - return + a, err := accountstore.Account("") + switch err { + case nil: + break + + case ErrAcctNotExist: + e := &btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: "default account does not exist", } - // XXX + ReplyError(frontend, cmd.Id(), e) + return + + default: // all other non-nil errors + e := &btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: err.Error(), + } + ReplyError(frontend, cmd.Id(), e) + return + } + + switch err := a.Unlock([]byte(cmd.Passphrase), cmd.Timeout); err { + case nil: ReplySuccess(frontend, cmd.Id(), nil) + NotifyWalletLockStateChange("", false) - go func() { - time.Sleep(time.Second * time.Duration(int64(cmd.Timeout))) - a.Lock() - NotifyWalletLockStateChange("", true) - }() - } else { + go func(timeout int64) { + time.Sleep(time.Second * time.Duration(timeout)) + _ = a.Lock() + }(cmd.Timeout) + + case ErrAcctNotExist: ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletInvalidAccountName) + + default: + ReplyError(frontend, cmd.Id(), + &btcjson.ErrWalletPassphraseIncorrect) } } diff --git a/createtx.go b/createtx.go index 97b800b..31d234e 100644 --- a/createtx.go +++ b/createtx.go @@ -270,13 +270,13 @@ func (w *Account) txToPairs(pairs map[string]int64, fee int64, minconf int) (*Cr if err != nil { return nil, err } - privkey, err := w.GetAddressKey(addrstr) + privkey, err := w.AddressKey(addrstr) if err == wallet.ErrWalletLocked { return nil, wallet.ErrWalletLocked } else if err != nil { return nil, fmt.Errorf("cannot get address key: %v", err) } - ai, err := w.GetAddressInfo(addrstr) + ai, err := w.AddressInfo(addrstr) if err != nil { return nil, fmt.Errorf("cannot get address info: %v", err) } diff --git a/disksync.go b/disksync.go index 91aef36..2a529b8 100644 --- a/disksync.go +++ b/disksync.go @@ -69,8 +69,8 @@ func (w *Account) writeDirtyToDisk() error { // for validity, and moved to replace the main file. timeStr := fmt.Sprintf("%v", time.Now().Unix()) - adir := accountdir(cfg, w.name) - if err := checkCreateAccountDir(adir); err != nil { + adir := w.accountdir(cfg) + if err := w.checkCreateAccountDir(adir); err != nil { return err } diff --git a/sockets.go b/sockets.go index 73ac96e..65d6e5f 100644 --- a/sockets.go +++ b/sockets.go @@ -17,7 +17,6 @@ package main import ( - "bytes" "code.google.com/p/go.net/websocket" "crypto/tls" "crypto/x509" @@ -26,7 +25,6 @@ import ( "errors" "fmt" "github.com/conformal/btcjson" - "github.com/conformal/btcwallet/tx" "github.com/conformal/btcwallet/wallet" "github.com/conformal/btcwire" "github.com/conformal/btcws" @@ -458,38 +456,8 @@ func NtfnBlockConnected(n btcws.Notification) { // // TODO(jrick): send frontend tx notifications once that's // implemented. - for _, a := range accounts.m { - // The UTXO store will be dirty if it was modified - // from a tx notification. - if a.UtxoStore.dirty { - // Notify all frontends of account's new unconfirmed - // and confirmed balance. - confirmed := a.CalculateBalance(1) - unconfirmed := a.CalculateBalance(0) - confirmed - NotifyWalletBalance(frontendNotificationMaster, - a.name, confirmed) - NotifyWalletBalanceUnconfirmed(frontendNotificationMaster, - a.name, unconfirmed) - } - // The account is intentionaly not immediately synced to disk. - // If btcd is performing an IBD, writing the wallet file for - // each newly-connected block would result in too many - // unnecessary disk writes. The UTXO and transaction stores - // could be written, but in the case of btcwallet closing - // before writing the dirty wallet, both would have to be - // pruned anyways. - // - // Instead, the wallet is queued to be written to disk at the - // next scheduled disk sync. - a.mtx.Lock() - a.Wallet.SetSyncedWith(bs) - a.dirty = true - a.mtx.Unlock() - dirtyAccounts.Lock() - dirtyAccounts.m[a] = true - dirtyAccounts.Unlock() - } + accountstore.BlockNotify(bs) // Notify frontends of new blockchain height. NotifyNewBlockChainHeight(frontendNotificationMaster, bcn.Height) @@ -514,7 +482,7 @@ func NtfnBlockDisconnected(n btcws.Notification) { // Rollback Utxo and Tx data stores. go func() { - accounts.Rollback(bdn.Height, hash) + accountstore.Rollback(bdn.Height, hash) }() // Notify frontends of new blockchain height. @@ -541,31 +509,12 @@ func NtfnTxMined(n btcws.Notification) { return } - // Lookup tx in store and add block information. - accounts.Lock() -out: - for _, a := range accounts.m { - a.TxStore.Lock() - - // Search in reverse order, more likely to find it - // sooner that way. - for i := len(a.TxStore.s) - 1; i >= 0; i-- { - sendtx, ok := a.TxStore.s[i].(*tx.SendTx) - if ok { - if bytes.Equal(txid.Bytes(), sendtx.TxID[:]) { - copy(sendtx.BlockHash[:], blockhash.Bytes()) - sendtx.BlockHeight = tmn.BlockHeight - sendtx.BlockIndex = int32(tmn.Index) - sendtx.BlockTime = tmn.BlockTime - a.TxStore.Unlock() - break out - } - } - } - - a.TxStore.Unlock() + err = accountstore.RecordMinedTx(txid, blockhash, + tmn.BlockHeight, tmn.Index, tmn.BlockTime) + if err != nil { + log.Errorf("%v handler: %v", n.Id(), err) + return } - accounts.Unlock() // Remove mined transaction from pool. UnminedTxs.Lock() @@ -748,14 +697,11 @@ func BtcdHandshake(ws *websocket.Conn) error { // since last connection. If so, rollback and rescan to // catch up. - for _, a := range accounts.m { - a.RescanActiveAddresses() - } + accountstore.RescanActiveAddresses() // Begin tracking wallets against this btcd instance. - for _, a := range accounts.m { - a.Track() - } + + accountstore.Track() // (Re)send any unmined transactions to btcd in case of a btcd restart. resendUnminedTxs() diff --git a/wallet/wallet.go b/wallet/wallet.go index c0b70d7..0479e57 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -791,12 +791,12 @@ func (w *Wallet) addrHashForAddress(addr string) ([]byte, error) { return addr160, nil } -// GetAddressKey returns the private key for a payment address stored +// AddressKey returns the private key for a payment address stored // in a wallet. This can fail if the payment address is for a different // Bitcoin network than what this wallet uses, the address is not // contained in the wallet, the address does not include a public and // private key, or if the wallet is locked. -func (w *Wallet) GetAddressKey(addr string) (key *ecdsa.PrivateKey, err error) { +func (w *Wallet) AddressKey(addr string) (key *ecdsa.PrivateKey, err error) { // Get address hash for payment address string. addr160, err := w.addrHashForAddress(addr) if err != nil { @@ -849,8 +849,8 @@ func (w *Wallet) GetAddressKey(addr string) (key *ecdsa.PrivateKey, err error) { }, nil } -// GetAddressInfo returns an AddressInfo for an address in a wallet. -func (w *Wallet) GetAddressInfo(addr string) (*AddressInfo, error) { +// AddressInfo returns an AddressInfo structure for an address in a wallet. +func (w *Wallet) AddressInfo(addr string) (*AddressInfo, error) { // Get address hash for addr. addr160, err := w.addrHashForAddress(addr) if err != nil { @@ -969,11 +969,11 @@ type AddressInfo struct { Pubkey string } -// GetSortedActiveAddresses returns all wallet addresses that have been +// SortedActiveAddresses returns all wallet addresses that have been // requested to be generated. These do not include unused addresses in // the key pool. Use this when ordered addresses are needed. Otherwise, -// GetActiveAddresses is preferred. -func (w *Wallet) GetSortedActiveAddresses() []*AddressInfo { +// ActiveAddresses is preferred. +func (w *Wallet) SortedActiveAddresses() []*AddressInfo { addrs := make([]*AddressInfo, 0, w.highestUsed+int64(len(w.importedAddrs))+1) for i := int64(rootKeyChainIdx); i <= w.highestUsed; i++ { @@ -996,10 +996,10 @@ func (w *Wallet) GetSortedActiveAddresses() []*AddressInfo { return addrs } -// GetActiveAddresses returns a map between active payment addresses +// ActiveAddresses returns a map between active payment addresses // and their full info. These do not include unused addresses in the -// key pool. If addresses must be sorted, use GetSortedActiveAddresses. -func (w *Wallet) GetActiveAddresses() map[string]*AddressInfo { +// key pool. If addresses must be sorted, use SortedActiveAddresses. +func (w *Wallet) ActiveAddresses() map[string]*AddressInfo { addrs := make(map[string]*AddressInfo) for i := int64(rootKeyChainIdx); i <= w.highestUsed; i++ { addr160, ok := w.chainIdxMap[i]