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/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
}

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"
"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.

468
cmdmgr.go
View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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()

View file

@ -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]