New Account and AccountStore API.

This change better organizes account handling by creating a new
AccountStore type and accountstore global variable, with receiver
funcs for all operations that require all accounts.  More Account
funcs are also added to clean up account handling in the RPC code.

Intial work on this done by dhill.
This commit is contained in:
Josh Rickmar 2013-12-02 14:56:06 -05:00
parent 2dd3fd0a21
commit 3c528f81ec
8 changed files with 923 additions and 504 deletions

View file

@ -26,12 +26,12 @@ import (
"github.com/conformal/btcwallet/wallet" "github.com/conformal/btcwallet/wallet"
"github.com/conformal/btcwire" "github.com/conformal/btcwire"
"github.com/conformal/btcws" "github.com/conformal/btcws"
"os"
"path/filepath"
"sync" "sync"
"time" "time"
) )
var accounts = NewAccountStore()
// Account is a structure containing all the components for a // Account is a structure containing all the components for a
// complete wallet. It contains the Armory-style wallet (to store // complete wallet. It contains the Armory-style wallet (to store
// addresses and keys), and tx and utxo data stores, along with locks // 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 // Lock locks the underlying wallet for an account.
// btcwallet. Wallet are stored in a map with the account name as the func (a *Account) Lock() error {
// key. A RWMutex is used to protect against incorrect concurrent a.mtx.Lock()
// access. defer a.mtx.Unlock()
type AccountStore struct {
sync.Mutex return a.Wallet.Lock()
m map[string]*Account
} }
// NewAccountStore returns an initialized and empty AccountStore. // Unlock unlocks the underlying wallet for an account.
func NewAccountStore() *AccountStore { func (a *Account) Unlock(passphrase []byte, timeout int64) error {
return &AccountStore{ a.mtx.Lock()
m: make(map[string]*Account), defer a.mtx.Unlock()
}
}
// Rollback rolls back each Account saved in the store. return a.Wallet.Unlock(passphrase)
//
// 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)
}
} }
// Rollback reverts each stored Account to a state before the block // 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) 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 // DumpPrivKeys returns the WIF-encoded private keys for all addresses
// non-watching addresses in a wallets. // non-watching addresses in a wallets.
func (a *Account) DumpPrivKeys() ([]string, error) { func (a *Account) DumpPrivKeys() ([]string, error) {
@ -138,8 +162,8 @@ func (a *Account) DumpPrivKeys() ([]string, error) {
// Iterate over each active address, appending the private // Iterate over each active address, appending the private
// key to privkeys. // key to privkeys.
var privkeys []string var privkeys []string
for _, addr := range a.GetActiveAddresses() { for _, addr := range a.ActiveAddresses() {
key, err := a.GetAddressKey(addr.Address) key, err := a.AddressKey(addr.Address)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -161,14 +185,14 @@ func (a *Account) DumpWIFPrivateKey(address string) (string, error) {
defer a.mtx.RUnlock() defer a.mtx.RUnlock()
// Get private key from wallet if it exists. // Get private key from wallet if it exists.
key, err := a.GetAddressKey(address) key, err := a.AddressKey(address)
if err != nil { if err != nil {
return "", err return "", err
} }
// Get address info. This is needed to determine whether // Get address info. This is needed to determine whether
// the pubkey is compressed or not. // the pubkey is compressed or not.
info, err := a.GetAddressInfo(address) info, err := a.AddressInfo(address)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -177,12 +201,32 @@ func (a *Account) DumpWIFPrivateKey(address string) (string, error) {
return btcutil.EncodePrivateKey(key.D.Bytes(), a.Net(), info.Compressed) 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 // wallet. If the import is successful, the payment address string is
// returned. // returned.
func (a *Account) ImportWIFPrivateKey(wif, label string, func (a *Account) ImportWIFPrivateKey(wif string, bs *wallet.BlockStamp) (string, error) {
bs *wallet.BlockStamp) (string, error) {
// Decode WIF private key and perform sanity checking. // Decode WIF private key and perform sanity checking.
privkey, net, compressed, err := btcutil.DecodePrivateKey(wif) privkey, net, compressed, err := btcutil.DecodePrivateKey(wif)
if err != nil { if err != nil {
@ -229,7 +273,7 @@ func (a *Account) Track() {
replyHandlers.Lock() replyHandlers.Lock()
replyHandlers.m[n] = a.newBlockTxOutHandler replyHandlers.m[n] = a.newBlockTxOutHandler
replyHandlers.Unlock() replyHandlers.Unlock()
for _, addr := range a.GetActiveAddresses() { for _, addr := range a.ActiveAddresses() {
a.ReqNewTxsForAddress(addr.Address) a.ReqNewTxsForAddress(addr.Address)
} }
@ -254,6 +298,9 @@ func (a *Account) Track() {
// it would have missed notifications as blocks are attached to the // it would have missed notifications as blocks are attached to the
// main chain. // main chain.
func (a *Account) RescanActiveAddresses() { func (a *Account) RescanActiveAddresses() {
a.mtx.RLock()
defer a.mtx.RUnlock()
// Determine the block to begin the rescan from. // Determine the block to begin the rescan from.
beginBlock := int32(0) beginBlock := int32(0)
if a.fullRescan { if a.fullRescan {
@ -335,7 +382,7 @@ func (a *Account) SortedActivePaymentAddresses() []string {
a.mtx.RLock() a.mtx.RLock()
defer a.mtx.RUnlock() defer a.mtx.RUnlock()
infos := a.GetSortedActiveAddresses() infos := a.SortedActiveAddresses()
addrs := make([]string, len(infos)) addrs := make([]string, len(infos))
for i, addr := range infos { for i, addr := range infos {
@ -351,7 +398,7 @@ func (a *Account) ActivePaymentAddresses() map[string]struct{} {
a.mtx.RLock() a.mtx.RLock()
defer a.mtx.RUnlock() defer a.mtx.RUnlock()
infos := a.GetActiveAddresses() infos := a.ActiveAddresses()
addrs := make(map[string]struct{}, len(infos)) addrs := make(map[string]struct{}, len(infos))
for _, info := range infos { for _, info := range infos {
@ -361,6 +408,35 @@ func (a *Account) ActivePaymentAddresses() map[string]struct{} {
return addrs 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 // ReqNewTxsForAddress sends a message to btcd to request tx updates
// for addr for each new block that is added to the blockchain. // for addr for each new block that is added to the blockchain.
func (a *Account) ReqNewTxsForAddress(addr string) { func (a *Account) ReqNewTxsForAddress(addr string) {
@ -592,3 +668,37 @@ func (a *Account) newBlockTxOutHandler(result interface{}, e *btcjson.Error) boo
// Never remove this handler. // Never remove this handler.
return false 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
}

537
accountstore.go Normal file
View file

@ -0,0 +1,537 @@
/*
* Copyright (c) 2013 Conformal Systems LLC <info@conformal.com>
*
* 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
}

142
cmd.go
View file

@ -21,7 +21,6 @@ import (
"fmt" "fmt"
"github.com/conformal/btcjson" "github.com/conformal/btcjson"
"github.com/conformal/btcutil" "github.com/conformal/btcutil"
"github.com/conformal/btcwallet/tx"
"github.com/conformal/btcwallet/wallet" "github.com/conformal/btcwallet/wallet"
"github.com/conformal/btcwire" "github.com/conformal/btcwire"
"github.com/conformal/btcws" "github.com/conformal/btcws"
@ -30,7 +29,6 @@ import (
"net/http" "net/http"
_ "net/http/pprof" _ "net/http/pprof"
"os" "os"
"path/filepath"
"sync" "sync"
"time" "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 // GetCurBlock returns the blockchain height and SHA hash of the most
// recently seen block. If no blocks have been seen since btcd has // recently seen block. If no blocks have been seen since btcd has
// connected, btcd is queried for the current block height and hash. // connected, btcd is queried for the current block height and hash.
@ -318,31 +205,10 @@ func main() {
} }
// Open default account // Open default account
a, err := OpenAccount(cfg, "") err = accountstore.OpenAccount("", cfg)
switch err { if err != nil {
case ErrNoTxs: log.Errorf("cannot open account: %v", err)
// Do nothing special for now. This will be implemented when os.Exit(1)
// 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)
} }
// Read CA file to verify a btcd TLS connection. // Read CA file to verify a btcd TLS connection.

468
cmdmgr.go
View file

@ -177,42 +177,23 @@ func DumpPrivKey(frontend chan []byte, icmd btcjson.Cmd) {
return return
} }
// Iterate over all accounts, returning the key if it is found switch key, err := accountstore.DumpWIFPrivateKey(cmd.Address); err {
// in any wallet. case nil:
for _, a := range accounts.m { // Key was found.
switch key, err := a.DumpWIFPrivateKey(cmd.Address); err { ReplySuccess(frontend, cmd.Id(), key)
case wallet.ErrAddressNotFound:
// Move on to the next account.
continue
case wallet.ErrWalletLocked: case wallet.ErrWalletLocked:
// Address was found, but the private key isn't // Address was found, but the private key isn't
// accessible. // accessible.
ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded) ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded)
return
case nil: default: // all other non-nil errors
// Key was found. e := &btcjson.Error{
ReplySuccess(frontend, cmd.Id(), key) Code: btcjson.ErrWallet.Code,
return Message: err.Error(),
default: // all other non-nil errors
e := &btcjson.Error{
Code: btcjson.ErrWallet.Code,
Message: err.Error(),
}
ReplyError(frontend, cmd.Id(), e)
return
} }
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 // DumpWallet replies to a dumpwallet request with all private keys
@ -226,30 +207,22 @@ func DumpWallet(frontend chan []byte, icmd btcjson.Cmd) {
return return
} }
// Iterate over all accounts, appending the private keys switch keys, err := accountstore.DumpKeys(); err {
// for each. case nil:
var keys []string // Reply with sorted WIF encoded private keys
for _, a := range accounts.m { ReplySuccess(frontend, cmd.Id(), keys)
switch walletKeys, err := a.DumpPrivKeys(); err {
case wallet.ErrWalletLocked:
ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded)
return
case nil: case wallet.ErrWalletLocked:
keys = append(keys, walletKeys...) ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletUnlockNeeded)
default: // any other non-nil error default: // any other non-nil error
e := &btcjson.Error{ e := &btcjson.Error{
Code: btcjson.ErrWallet.Code, Code: btcjson.ErrWallet.Code,
Message: err.Error(), Message: err.Error(),
}
ReplyError(frontend, cmd.Id(), e)
return
} }
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 // GetAddressesByAccount replies to a getaddressesbyaccount request with
@ -263,16 +236,22 @@ func GetAddressesByAccount(frontend chan []byte, icmd btcjson.Cmd) {
return return
} }
// Check that the account specified in the request exists. switch a, err := accountstore.Account(cmd.Account); err {
a, ok := accounts.m[cmd.Account] case nil:
if !ok { // Reply with sorted active payment addresses.
ReplySuccess(frontend, cmd.Id(), a.SortedActivePaymentAddresses())
case ErrAcctNotExist:
ReplyError(frontend, cmd.Id(), ReplyError(frontend, cmd.Id(),
&btcjson.ErrWalletInvalidAccountName) &btcjson.ErrWalletInvalidAccountName)
return
}
// Reply with sorted active payment addresses. default: // all other non-nil errors
ReplySuccess(frontend, cmd.Id(), a.SortedActivePaymentAddresses()) 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 // GetBalance replies to a getbalance request with the balance for an
@ -286,16 +265,15 @@ func GetBalance(frontend chan []byte, icmd btcjson.Cmd) {
return return
} }
// Check that the account specified in the request exists. balance, err := accountstore.CalculateBalance(cmd.Account, cmd.MinConf)
a, ok := accounts.m[cmd.Account] if err != nil {
if !ok {
ReplyError(frontend, cmd.Id(), ReplyError(frontend, cmd.Id(),
&btcjson.ErrWalletInvalidAccountName) &btcjson.ErrWalletInvalidAccountName)
return return
} }
// Reply with calculated balance. // 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 // GetBalances replies to a getbalances extension request by notifying
@ -314,30 +292,19 @@ func ImportPrivKey(frontend chan []byte, icmd btcjson.Cmd) {
return return
} }
// Check that the account specified in the requests exists. // Get the acount included in the request. Yes, Label is the
// Yes, Label is the account name. // account name...
a, ok := accounts.m[cmd.Label] a, err := accountstore.Account(cmd.Label)
if !ok { switch err {
case nil:
break
case ErrAcctNotExist:
ReplyError(frontend, cmd.Id(), ReplyError(frontend, cmd.Id(),
&btcjson.ErrWalletInvalidAccountName) &btcjson.ErrWalletInvalidAccountName)
return return
}
// Create a blockstamp for when this address first appeared. default:
// 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:
e := &btcjson.Error{ e := &btcjson.Error{
Code: btcjson.ErrWallet.Code, Code: btcjson.ErrWallet.Code,
Message: err.Error(), Message: err.Error(),
@ -346,29 +313,32 @@ func ImportPrivKey(frontend chan []byte, icmd btcjson.Cmd) {
return return
} }
if cmd.Rescan { // Import the private key, handling any errors.
addrs := map[string]struct{}{ switch err := a.ImportPrivKey(cmd.PrivKey, cmd.Rescan); err {
addr: struct{}{}, case nil:
} // If the import was successful, reply with nil.
a.RescanAddresses(bs.Height, addrs) ReplySuccess(frontend, cmd.Id(), nil)
}
// If the import was successful, reply with nil. case wallet.ErrWalletLocked:
ReplySuccess(frontend, cmd.Id(), nil) 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 // NotifyBalances notifies an attached frontend of the current confirmed
// and unconfirmed account balances. // and unconfirmed account balances.
// //
// TODO(jrick): Switch this to return a JSON object (map) of all accounts // TODO(jrick): Switch this to return a single JSON object
// and their balances, instead of separate notifications for each account. // (map[string]interface{}) of all accounts and their balances, instead of
// separate notifications for each account.
func NotifyBalances(frontend chan []byte) { func NotifyBalances(frontend chan []byte) {
for _, a := range accounts.m { accountstore.NotifyBalances(frontend)
balance := a.CalculateBalance(1)
unconfirmed := a.CalculateBalance(0) - balance
NotifyWalletBalance(frontend, a.name, balance)
NotifyWalletBalanceUnconfirmed(frontend, a.name, unconfirmed)
}
} }
// GetNewAddress responds to a getnewaddress request by getting a new // GetNewAddress responds to a getnewaddress request by getting a new
@ -382,35 +352,25 @@ func GetNewAddress(frontend chan []byte, icmd btcjson.Cmd) {
return return
} }
// Check that the account specified in the request exists. a, err := accountstore.Account(cmd.Account)
a, ok := accounts.m[cmd.Account] switch err {
if !ok { case nil:
break
case ErrAcctNotExist:
ReplyError(frontend, cmd.Id(), ReplyError(frontend, cmd.Id(),
&btcjson.ErrWalletInvalidAccountName) &btcjson.ErrWalletInvalidAccountName)
return return
}
// Get current block's height and hash. case ErrBtcdDisconnected:
bs, err := GetCurBlock()
if err != nil {
e := &btcjson.Error{ e := &btcjson.Error{
Code: btcjson.ErrInternal.Code, Code: btcjson.ErrInternal.Code,
Message: "btcd disconnected", Message: "btcd disconnected",
} }
ReplyError(frontend, cmd.Id(), e) ReplyError(frontend, cmd.Id(), e)
return return
}
// Get next address from wallet. default: // all other non-nil errors
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 {
e := &btcjson.Error{ e := &btcjson.Error{
Code: btcjson.ErrWallet.Code, Code: btcjson.ErrWallet.Code,
Message: err.Error(), Message: err.Error(),
@ -418,26 +378,27 @@ func GetNewAddress(frontend chan []byte, icmd btcjson.Cmd) {
ReplyError(frontend, cmd.Id(), e) ReplyError(frontend, cmd.Id(), e)
return return
} }
if err != nil {
// TODO(jrick): generate new addresses if the address pool is addr, err := a.NewAddress()
// empty. switch err {
e := btcjson.ErrInternal case nil:
e.Message = fmt.Sprintf("New address generation not implemented yet") // Reply with the new payment address string.
ReplyError(frontend, cmd.Id(), &e) ReplySuccess(frontend, cmd.Id(), addr)
return
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 // ListAccounts replies to a listaccounts request by returning a JSON
@ -450,11 +411,7 @@ func ListAccounts(frontend chan []byte, icmd btcjson.Cmd) {
return return
} }
// Create and fill a map of account names and their balances. pairs := accountstore.ListAccounts(cmd.MinConf)
pairs := make(map[string]float64)
for aname, a := range accounts.m {
pairs[aname] = a.CalculateBalance(cmd.MinConf)
}
// Reply with the map. This will be marshaled into a JSON object. // Reply with the map. This will be marshaled into a JSON object.
ReplySuccess(frontend, cmd.Id(), pairs) ReplySuccess(frontend, cmd.Id(), pairs)
@ -470,47 +427,44 @@ func ListTransactions(frontend chan []byte, icmd btcjson.Cmd) {
return return
} }
// Check that the account specified in the request exists. a, err := accountstore.Account(cmd.Account)
a, ok := accounts.m[cmd.Account] switch err {
if !ok { case nil:
break
case ErrAcctNotExist:
ReplyError(frontend, cmd.Id(), ReplyError(frontend, cmd.Id(),
&btcjson.ErrWalletInvalidAccountName) &btcjson.ErrWalletInvalidAccountName)
return return
}
// Get current block. The block height used for calculating default: // all other non-nil errors
// the number of tx confirmations.
bs, err := GetCurBlock()
if err != nil {
e := &btcjson.Error{ e := &btcjson.Error{
Code: btcjson.ErrInternal.Code, Code: btcjson.ErrWallet.Code,
Message: err.Error(), Message: err.Error(),
} }
ReplyError(frontend, cmd.Id(), e) ReplyError(frontend, cmd.Id(), e)
return return
} }
a.mtx.RLock() switch txList, err := a.ListTransactions(cmd.From, cmd.Count); err {
a.TxStore.RLock() case nil:
var txInfoList []map[string]interface{} // Reply with the list of tx information.
lastLookupIdx := len(a.TxStore.s) - cmd.Count ReplySuccess(frontend, cmd.Id(), txList)
// 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...)
case *tx.RecvTx: case ErrBtcdDisconnected:
info := e.TxInfo(a.Name(), bs.Height, a.Net()) e := &btcjson.Error{
txInfoList = append(txInfoList, info) Code: btcjson.ErrInternal.Code,
Message: "btcd disconnected",
} }
} ReplyError(frontend, cmd.Id(), e)
a.mtx.RUnlock()
a.TxStore.RUnlock()
// Reply with the list of tx information. default:
ReplySuccess(frontend, cmd.Id(), txInfoList) e := &btcjson.Error{
Code: btcjson.ErrWallet.Code,
Message: err.Error(),
}
ReplyError(frontend, cmd.Id(), e)
}
} }
// SendFrom creates a new transaction spending unspent transaction // 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. // Check that the account specified in the request exists.
a, ok := accounts.m[cmd.FromAccount] a, err := accountstore.Account(cmd.FromAccount)
if !ok { if err != nil {
ReplyError(frontend, cmd.Id(), ReplyError(frontend, cmd.Id(),
&btcjson.ErrWalletInvalidAccountName) &btcjson.ErrWalletInvalidAccountName)
return return
@ -648,8 +602,8 @@ func SendMany(frontend chan []byte, icmd btcjson.Cmd) {
} }
// Check that the account specified in the request exists. // Check that the account specified in the request exists.
a, ok := accounts.m[cmd.FromAccount] a, err := accountstore.Account(cmd.FromAccount)
if !ok { if err != nil {
ReplyError(frontend, cmd.Id(), ReplyError(frontend, cmd.Id(),
&btcjson.ErrWalletInvalidAccountName) &btcjson.ErrWalletInvalidAccountName)
return return
@ -868,76 +822,27 @@ func CreateEncryptedWallet(frontend chan []byte, icmd btcjson.Cmd) {
return return
} }
// Grab the account map lock and defer the unlock. If an err := accountstore.CreateEncryptedWallet(cmd.Account, cmd.Description,
// account is successfully created, it will be added to the []byte(cmd.Passphrase))
// map while the lock is held. switch err {
accounts.Lock() case nil:
defer accounts.Unlock() // A nil reply is sent upon successful wallet creation.
ReplySuccess(frontend, cmd.Id(), nil)
// Does this wallet already exist? case ErrAcctNotExist:
if _, ok = accounts.m[cmd.Account]; ok {
ReplyError(frontend, cmd.Id(), ReplyError(frontend, cmd.Id(),
&btcjson.ErrWalletInvalidAccountName) &btcjson.ErrWalletInvalidAccountName)
return
}
// Decide which Bitcoin network must be used. case ErrBtcdDisconnected:
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 {
e := &btcjson.Error{ e := &btcjson.Error{
Code: btcjson.ErrInternal.Code, Code: btcjson.ErrInternal.Code,
Message: "btcd disconnected", Message: "btcd disconnected",
} }
ReplyError(frontend, cmd.Id(), e) ReplyError(frontend, cmd.Id(), e)
return
}
// Create new wallet in memory. default:
wlt, err := wallet.NewWallet(cmd.Account, cmd.Description,
[]byte(cmd.Passphrase), net, &bs)
if err != nil {
log.Error("Error creating wallet: " + err.Error())
ReplyError(frontend, cmd.Id(), &btcjson.ErrInternal) 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 // WalletIsLocked responds to the walletislocked extension request by
@ -952,16 +857,31 @@ func WalletIsLocked(frontend chan []byte, icmd btcjson.Cmd) {
return return
} }
// Check that the account specified in the request exists. a, err := accountstore.Account(cmd.Account)
a, ok := accounts.m[cmd.Account] switch err {
if !ok { case nil:
break
case ErrAcctNotExist:
ReplyError(frontend, cmd.Id(), ReplyError(frontend, cmd.Id(),
&btcjson.ErrWalletInvalidAccountName) &btcjson.ErrWalletInvalidAccountName)
return 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. // 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, // 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 // with this. Lock all the wallets, like if all accounts are locked
// for one bitcoind wallet? // for one bitcoind wallet?
func WalletLock(frontend chan []byte, icmd btcjson.Cmd) { func WalletLock(frontend chan []byte, icmd btcjson.Cmd) {
if a, ok := accounts.m[""]; ok { a, err := accountstore.Account("")
if err := a.Lock(); err != nil { switch err {
ReplyError(frontend, icmd.Id(), case nil:
&btcjson.ErrWalletWrongEncState) break
return
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) ReplySuccess(frontend, icmd.Id(), nil)
NotifyWalletLockStateChange("", true)
} else { default:
ReplyError(frontend, icmd.Id(), ReplyError(frontend, icmd.Id(),
&btcjson.ErrWalletInvalidAccountName) &btcjson.ErrWalletWrongEncState)
} }
} }
@ -998,23 +936,45 @@ func WalletPassphrase(frontend chan []byte, icmd btcjson.Cmd) {
return return
} }
if a, ok := accounts.m[""]; ok { a, err := accountstore.Account("")
if err := a.Unlock([]byte(cmd.Passphrase)); err != nil { switch err {
ReplyError(frontend, cmd.Id(), case nil:
&btcjson.ErrWalletPassphraseIncorrect) break
return
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) ReplySuccess(frontend, cmd.Id(), nil)
NotifyWalletLockStateChange("", false) NotifyWalletLockStateChange("", false)
go func() { go func(timeout int64) {
time.Sleep(time.Second * time.Duration(int64(cmd.Timeout))) time.Sleep(time.Second * time.Duration(timeout))
a.Lock() _ = a.Lock()
NotifyWalletLockStateChange("", true) }(cmd.Timeout)
}()
} else { case ErrAcctNotExist:
ReplyError(frontend, cmd.Id(), ReplyError(frontend, cmd.Id(),
&btcjson.ErrWalletInvalidAccountName) &btcjson.ErrWalletInvalidAccountName)
default:
ReplyError(frontend, cmd.Id(),
&btcjson.ErrWalletPassphraseIncorrect)
} }
} }

View file

@ -270,13 +270,13 @@ func (w *Account) txToPairs(pairs map[string]int64, fee int64, minconf int) (*Cr
if err != nil { if err != nil {
return nil, err return nil, err
} }
privkey, err := w.GetAddressKey(addrstr) privkey, err := w.AddressKey(addrstr)
if err == wallet.ErrWalletLocked { if err == wallet.ErrWalletLocked {
return nil, wallet.ErrWalletLocked return nil, wallet.ErrWalletLocked
} else if err != nil { } else if err != nil {
return nil, fmt.Errorf("cannot get address key: %v", err) return nil, fmt.Errorf("cannot get address key: %v", err)
} }
ai, err := w.GetAddressInfo(addrstr) ai, err := w.AddressInfo(addrstr)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot get address info: %v", err) return nil, fmt.Errorf("cannot get address info: %v", err)
} }

View file

@ -69,8 +69,8 @@ func (w *Account) writeDirtyToDisk() error {
// for validity, and moved to replace the main file. // for validity, and moved to replace the main file.
timeStr := fmt.Sprintf("%v", time.Now().Unix()) timeStr := fmt.Sprintf("%v", time.Now().Unix())
adir := accountdir(cfg, w.name) adir := w.accountdir(cfg)
if err := checkCreateAccountDir(adir); err != nil { if err := w.checkCreateAccountDir(adir); err != nil {
return err return err
} }

View file

@ -17,7 +17,6 @@
package main package main
import ( import (
"bytes"
"code.google.com/p/go.net/websocket" "code.google.com/p/go.net/websocket"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
@ -26,7 +25,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/conformal/btcjson" "github.com/conformal/btcjson"
"github.com/conformal/btcwallet/tx"
"github.com/conformal/btcwallet/wallet" "github.com/conformal/btcwallet/wallet"
"github.com/conformal/btcwire" "github.com/conformal/btcwire"
"github.com/conformal/btcws" "github.com/conformal/btcws"
@ -458,38 +456,8 @@ func NtfnBlockConnected(n btcws.Notification) {
// //
// TODO(jrick): send frontend tx notifications once that's // TODO(jrick): send frontend tx notifications once that's
// implemented. // 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. accountstore.BlockNotify(bs)
// 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()
}
// Notify frontends of new blockchain height. // Notify frontends of new blockchain height.
NotifyNewBlockChainHeight(frontendNotificationMaster, bcn.Height) NotifyNewBlockChainHeight(frontendNotificationMaster, bcn.Height)
@ -514,7 +482,7 @@ func NtfnBlockDisconnected(n btcws.Notification) {
// Rollback Utxo and Tx data stores. // Rollback Utxo and Tx data stores.
go func() { go func() {
accounts.Rollback(bdn.Height, hash) accountstore.Rollback(bdn.Height, hash)
}() }()
// Notify frontends of new blockchain height. // Notify frontends of new blockchain height.
@ -541,31 +509,12 @@ func NtfnTxMined(n btcws.Notification) {
return return
} }
// Lookup tx in store and add block information. err = accountstore.RecordMinedTx(txid, blockhash,
accounts.Lock() tmn.BlockHeight, tmn.Index, tmn.BlockTime)
out: if err != nil {
for _, a := range accounts.m { log.Errorf("%v handler: %v", n.Id(), err)
a.TxStore.Lock() return
// 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()
} }
accounts.Unlock()
// Remove mined transaction from pool. // Remove mined transaction from pool.
UnminedTxs.Lock() UnminedTxs.Lock()
@ -748,14 +697,11 @@ func BtcdHandshake(ws *websocket.Conn) error {
// since last connection. If so, rollback and rescan to // since last connection. If so, rollback and rescan to
// catch up. // catch up.
for _, a := range accounts.m { accountstore.RescanActiveAddresses()
a.RescanActiveAddresses()
}
// Begin tracking wallets against this btcd instance. // 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. // (Re)send any unmined transactions to btcd in case of a btcd restart.
resendUnminedTxs() resendUnminedTxs()

View file

@ -791,12 +791,12 @@ func (w *Wallet) addrHashForAddress(addr string) ([]byte, error) {
return addr160, nil 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 // 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 // Bitcoin network than what this wallet uses, the address is not
// contained in the wallet, the address does not include a public and // contained in the wallet, the address does not include a public and
// private key, or if the wallet is locked. // 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. // Get address hash for payment address string.
addr160, err := w.addrHashForAddress(addr) addr160, err := w.addrHashForAddress(addr)
if err != nil { if err != nil {
@ -849,8 +849,8 @@ func (w *Wallet) GetAddressKey(addr string) (key *ecdsa.PrivateKey, err error) {
}, nil }, nil
} }
// GetAddressInfo returns an AddressInfo for an address in a wallet. // AddressInfo returns an AddressInfo structure for an address in a wallet.
func (w *Wallet) GetAddressInfo(addr string) (*AddressInfo, error) { func (w *Wallet) AddressInfo(addr string) (*AddressInfo, error) {
// Get address hash for addr. // Get address hash for addr.
addr160, err := w.addrHashForAddress(addr) addr160, err := w.addrHashForAddress(addr)
if err != nil { if err != nil {
@ -969,11 +969,11 @@ type AddressInfo struct {
Pubkey string 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 // requested to be generated. These do not include unused addresses in
// the key pool. Use this when ordered addresses are needed. Otherwise, // the key pool. Use this when ordered addresses are needed. Otherwise,
// GetActiveAddresses is preferred. // ActiveAddresses is preferred.
func (w *Wallet) GetSortedActiveAddresses() []*AddressInfo { func (w *Wallet) SortedActiveAddresses() []*AddressInfo {
addrs := make([]*AddressInfo, 0, addrs := make([]*AddressInfo, 0,
w.highestUsed+int64(len(w.importedAddrs))+1) w.highestUsed+int64(len(w.importedAddrs))+1)
for i := int64(rootKeyChainIdx); i <= w.highestUsed; i++ { for i := int64(rootKeyChainIdx); i <= w.highestUsed; i++ {
@ -996,10 +996,10 @@ func (w *Wallet) GetSortedActiveAddresses() []*AddressInfo {
return addrs 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 // and their full info. These do not include unused addresses in the
// key pool. If addresses must be sorted, use GetSortedActiveAddresses. // key pool. If addresses must be sorted, use SortedActiveAddresses.
func (w *Wallet) GetActiveAddresses() map[string]*AddressInfo { func (w *Wallet) ActiveAddresses() map[string]*AddressInfo {
addrs := make(map[string]*AddressInfo) addrs := make(map[string]*AddressInfo)
for i := int64(rootKeyChainIdx); i <= w.highestUsed; i++ { for i := int64(rootKeyChainIdx); i <= w.highestUsed; i++ {
addr160, ok := w.chainIdxMap[i] addr160, ok := w.chainIdxMap[i]