Redo account locking and RPC request processing.

This change removes the three separate mutexes which used to lock an
account's wallet, tx store, and utxo store.  Accounts no longer
contain any locking mechanism and rely on go's other synchronization
constructs (goroutines and channels) for correct access.

All accounts are now managed as a collection through the new
AccountManager, rather than the old AccountStore.  AccountManager runs
as its own goroutine to provide access to accounts.

RPC requests are now queued for handling, being denied if the queue
buffer is exhausted.  Notifications are also queued (instead of being
sent from their own goroutine after being received, in which order is
undefined), however, notifications are never dropped and will
potentially grow a queue of infinite size if unhandled.
This commit is contained in:
Josh Rickmar 2014-01-30 10:14:02 -05:00
parent 20e8201125
commit 6a08c7de07
12 changed files with 1508 additions and 1433 deletions

View file

@ -19,7 +19,6 @@ package main
import ( import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"github.com/conformal/btcutil" "github.com/conformal/btcutil"
"github.com/conformal/btcwallet/tx" "github.com/conformal/btcwallet/tx"
@ -29,12 +28,10 @@ import (
"sync" "sync"
) )
// ErrNotFound describes an error where a map lookup failed due to a
// key not being in the map.
var ErrNotFound = errors.New("not found")
// addressAccountMap holds a map of addresses to names of the // addressAccountMap holds a map of addresses to names of the
// accounts that hold each address. // accounts that hold each address.
//
// TODO: move this to AccountManager
var addressAccountMap = struct { var addressAccountMap = struct {
sync.RWMutex sync.RWMutex
m map[string]string m map[string]string
@ -64,28 +61,18 @@ func LookupAccountByAddress(address string) (string, error) {
// 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 stores, and a mutex to prevent
// to prevent against incorrect multiple access. // incorrect multiple access.
type Account struct { type Account struct {
*wallet.Wallet
mtx sync.RWMutex
name string name string
fullRescan bool fullRescan bool
UtxoStore struct { *wallet.Wallet
sync.RWMutex tx.UtxoStore
s tx.UtxoStore tx.TxStore
}
TxStore struct {
sync.RWMutex
s tx.TxStore
}
} }
// Lock locks the underlying wallet for an account. // Lock locks the underlying wallet for an account.
func (a *Account) Lock() error { func (a *Account) Lock() error {
a.mtx.Lock()
defer a.mtx.Unlock()
switch err := a.Wallet.Lock(); err { switch err := a.Wallet.Lock(); err {
case nil: case nil:
NotifyWalletLockStateChange(a.Name(), true) NotifyWalletLockStateChange(a.Name(), true)
@ -102,9 +89,6 @@ func (a *Account) Lock() error {
// Unlock unlocks the underlying wallet for an account. // Unlock unlocks the underlying wallet for an account.
func (a *Account) Unlock(passphrase []byte) error { func (a *Account) Unlock(passphrase []byte) error {
a.mtx.Lock()
defer a.mtx.Unlock()
if err := a.Wallet.Unlock(passphrase); err != nil { if err := a.Wallet.Unlock(passphrase); err != nil {
return err return err
} }
@ -113,26 +97,6 @@ func (a *Account) Unlock(passphrase []byte) error {
return nil return nil
} }
// Rollback reverts each stored Account to a state before the block
// with the passed chainheight and block hash was connected to the main
// chain. This is used to remove transactions and utxos for each wallet
// that occured on a chain no longer considered to be the main chain.
func (a *Account) Rollback(height int32, hash *btcwire.ShaHash) {
a.UtxoStore.Lock()
modified := a.UtxoStore.s.Rollback(height, hash)
a.UtxoStore.Unlock()
if modified {
a.ScheduleUtxoStoreWrite()
}
a.TxStore.Lock()
modified = a.TxStore.s.Rollback(height, hash)
a.TxStore.Unlock()
if modified {
a.ScheduleTxStoreWrite()
}
}
// AddressUsed returns whether there are any recorded transactions spending to // AddressUsed returns whether there are any recorded transactions spending to
// a given address. Assumming correct TxStore usage, this will return true iff // a given address. Assumming correct TxStore usage, this will return true iff
// there are any transactions with outputs to this address in the blockchain or // there are any transactions with outputs to this address in the blockchain or
@ -142,13 +106,10 @@ func (a *Account) AddressUsed(addr btcutil.Address) bool {
// opening an account, and keeping it up to date each time a new // opening an account, and keeping it up to date each time a new
// received tx arrives. // received tx arrives.
a.TxStore.RLock()
defer a.TxStore.RUnlock()
pkHash := addr.ScriptAddress() pkHash := addr.ScriptAddress()
for i := range a.TxStore.s { for i := range a.TxStore {
rtx, ok := a.TxStore.s[i].(*tx.RecvTx) rtx, ok := a.TxStore[i].(*tx.RecvTx)
if !ok { if !ok {
continue continue
} }
@ -170,22 +131,19 @@ func (a *Account) AddressUsed(addr btcutil.Address) bool {
// the balance will be calculated based on how many how many blocks // the balance will be calculated based on how many how many blocks
// include a UTXO. // include a UTXO.
func (a *Account) CalculateBalance(confirms int) float64 { func (a *Account) CalculateBalance(confirms int) float64 {
var bal uint64 // Measured in satoshi
bs, err := GetCurBlock() bs, err := GetCurBlock()
if bs.Height == int32(btcutil.BlockHeightUnknown) || err != nil { if bs.Height == int32(btcutil.BlockHeightUnknown) || err != nil {
return 0. return 0.
} }
a.UtxoStore.RLock() var bal uint64 // Measured in satoshi
for _, u := range a.UtxoStore.s { for _, u := range a.UtxoStore {
// Utxos not yet in blocks (height -1) should only be // Utxos not yet in blocks (height -1) should only be
// added if confirmations is 0. // added if confirmations is 0.
if confirms == 0 || (u.Height != -1 && int(bs.Height-u.Height+1) >= confirms) { if confirms == 0 || (u.Height != -1 && int(bs.Height-u.Height+1) >= confirms) {
bal += u.Amt bal += u.Amt
} }
} }
a.UtxoStore.RUnlock()
return float64(bal) / float64(btcutil.SatoshiPerBitcoin) return float64(bal) / float64(btcutil.SatoshiPerBitcoin)
} }
@ -199,15 +157,13 @@ func (a *Account) CalculateBalance(confirms int) float64 {
// the balance will be calculated based on how many how many blocks // the balance will be calculated based on how many how many blocks
// include a UTXO. // include a UTXO.
func (a *Account) CalculateAddressBalance(addr *btcutil.AddressPubKeyHash, confirms int) float64 { func (a *Account) CalculateAddressBalance(addr *btcutil.AddressPubKeyHash, confirms int) float64 {
var bal uint64 // Measured in satoshi
bs, err := GetCurBlock() bs, err := GetCurBlock()
if bs.Height == int32(btcutil.BlockHeightUnknown) || err != nil { if bs.Height == int32(btcutil.BlockHeightUnknown) || err != nil {
return 0. return 0.
} }
a.UtxoStore.RLock() var bal uint64 // Measured in satoshi
for _, u := range a.UtxoStore.s { for _, u := range a.UtxoStore {
// Utxos not yet in blocks (height -1) should only be // Utxos not yet in blocks (height -1) should only be
// added if confirmations is 0. // added if confirmations is 0.
if confirms == 0 || (u.Height != -1 && int(bs.Height-u.Height+1) >= confirms) { if confirms == 0 || (u.Height != -1 && int(bs.Height-u.Height+1) >= confirms) {
@ -216,7 +172,6 @@ func (a *Account) CalculateAddressBalance(addr *btcutil.AddressPubKeyHash, confi
} }
} }
} }
a.UtxoStore.RUnlock()
return float64(bal) / float64(btcutil.SatoshiPerBitcoin) return float64(bal) / float64(btcutil.SatoshiPerBitcoin)
} }
@ -225,9 +180,7 @@ func (a *Account) CalculateAddressBalance(addr *btcutil.AddressPubKeyHash, confi
// one transaction spending to it in the blockchain or btcd mempool), the next // one transaction spending to it in the blockchain or btcd mempool), the next
// chained address is returned. // chained address is returned.
func (a *Account) CurrentAddress() (btcutil.Address, error) { func (a *Account) CurrentAddress() (btcutil.Address, error) {
a.mtx.RLock()
addr := a.Wallet.LastChainedAddress() addr := a.Wallet.LastChainedAddress()
a.mtx.RUnlock()
// Get next chained address if the last one has already been used. // Get next chained address if the last one has already been used.
if a.AddressUsed(addr) { if a.AddressUsed(addr) {
@ -243,10 +196,7 @@ func (a *Account) CurrentAddress() (btcutil.Address, error) {
// replies. // replies.
func (a *Account) ListSinceBlock(since, curBlockHeight int32, minconf int) ([]map[string]interface{}, error) { func (a *Account) ListSinceBlock(since, curBlockHeight int32, minconf int) ([]map[string]interface{}, error) {
var txInfoList []map[string]interface{} var txInfoList []map[string]interface{}
a.TxStore.RLock() for _, tx := range a.TxStore {
defer a.TxStore.RUnlock()
for _, tx := range a.TxStore.s {
// check block number. // check block number.
if since != -1 && tx.Height() <= since { if since != -1 && tx.Height() <= since {
continue continue
@ -271,15 +221,13 @@ func (a *Account) ListTransactions(from, count int) ([]map[string]interface{}, e
} }
var txInfoList []map[string]interface{} var txInfoList []map[string]interface{}
a.TxStore.RLock()
lastLookupIdx := len(a.TxStore.s) - count lastLookupIdx := len(a.TxStore) - count
// Search in reverse order: lookup most recently-added first. // Search in reverse order: lookup most recently-added first.
for i := len(a.TxStore.s) - 1; i >= from && i >= lastLookupIdx; i-- { for i := len(a.TxStore) - 1; i >= from && i >= lastLookupIdx; i-- {
txInfoList = append(txInfoList, txInfoList = append(txInfoList,
a.TxStore.s[i].TxInfo(a.name, bs.Height, a.Net())...) a.TxStore[i].TxInfo(a.name, bs.Height, a.Net())...)
} }
a.TxStore.RUnlock()
return txInfoList, nil return txInfoList, nil
} }
@ -298,10 +246,8 @@ func (a *Account) ListAddressTransactions(pkHashes map[string]struct{}) (
} }
var txInfoList []map[string]interface{} var txInfoList []map[string]interface{}
a.TxStore.RLock() for i := range a.TxStore {
rtx, ok := a.TxStore[i].(*tx.RecvTx)
for i := range a.TxStore.s {
rtx, ok := a.TxStore.s[i].(*tx.RecvTx)
if !ok { if !ok {
continue continue
} }
@ -310,7 +256,6 @@ func (a *Account) ListAddressTransactions(pkHashes map[string]struct{}) (
txInfoList = append(txInfoList, info...) txInfoList = append(txInfoList, info...)
} }
} }
a.TxStore.RUnlock()
return txInfoList, nil return txInfoList, nil
} }
@ -326,15 +271,12 @@ func (a *Account) ListAllTransactions() ([]map[string]interface{}, error) {
return nil, err return nil, err
} }
var txInfoList []map[string]interface{}
a.TxStore.RLock()
// Search in reverse order: lookup most recently-added first. // Search in reverse order: lookup most recently-added first.
for i := len(a.TxStore.s) - 1; i >= 0; i-- { var txInfoList []map[string]interface{}
for i := len(a.TxStore) - 1; i >= 0; i-- {
txInfoList = append(txInfoList, txInfoList = append(txInfoList,
a.TxStore.s[i].TxInfo(a.name, bs.Height, a.Net())...) a.TxStore[i].TxInfo(a.name, bs.Height, a.Net())...)
} }
a.TxStore.RUnlock()
return txInfoList, nil return txInfoList, nil
} }
@ -342,9 +284,6 @@ func (a *Account) ListAllTransactions() ([]map[string]interface{}, error) {
// DumpPrivKeys returns the WIF-encoded private keys for all addresses with // DumpPrivKeys returns the WIF-encoded private keys for all addresses with
// private keys in a wallet. // private keys in a wallet.
func (a *Account) DumpPrivKeys() ([]string, error) { func (a *Account) DumpPrivKeys() ([]string, error) {
a.mtx.RLock()
defer a.mtx.RUnlock()
// 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
@ -367,9 +306,6 @@ func (a *Account) DumpPrivKeys() ([]string, error) {
// DumpWIFPrivateKey returns the WIF encoded private key for a // DumpWIFPrivateKey returns the WIF encoded private key for a
// single wallet address. // single wallet address.
func (a *Account) DumpWIFPrivateKey(addr btcutil.Address) (string, error) { func (a *Account) DumpWIFPrivateKey(addr btcutil.Address) (string, error) {
a.mtx.RLock()
defer a.mtx.RUnlock()
// Get private key from wallet if it exists. // Get private key from wallet if it exists.
key, err := a.Wallet.AddressKey(addr) key, err := a.Wallet.AddressKey(addr)
if err != nil { if err != nil {
@ -387,58 +323,19 @@ func (a *Account) DumpWIFPrivateKey(addr btcutil.Address) (string, error) {
return btcutil.EncodePrivateKey(key.D.Bytes(), a.Net(), info.Compressed) return btcutil.EncodePrivateKey(key.D.Bytes(), a.Net(), info.Compressed)
} }
// ImportPrivKey imports a WIF-encoded private key into an account's wallet. // ImportPrivateKey imports a private key to the account's wallet and
// This function is not recommended, as it gives no hints as to when the // writes the new wallet to disk.
// address first appeared (not just in the blockchain, but since the address func (a *Account) ImportPrivateKey(pk []byte, compressed bool, bs *wallet.BlockStamp) (string, error) {
// 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 {
// Do not wait for rescan to finish before returning to the
// caller.
go func() {
addrs := map[string]struct{}{
addr: struct{}{},
}
Rescan(CurrentRPCConn(), bs.Height, addrs)
a.WriteScheduledToDisk()
}()
}
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 string, bs *wallet.BlockStamp) (string, error) {
// Decode WIF private key and perform sanity checking.
privkey, net, compressed, err := btcutil.DecodePrivateKey(wif)
if err != nil {
return "", err
}
if net != a.Net() {
return "", errors.New("wrong network")
}
// Attempt to import private key into wallet. // Attempt to import private key into wallet.
a.mtx.Lock() addr, err := a.Wallet.ImportPrivateKey(pk, compressed, bs)
addr, err := a.Wallet.ImportPrivateKey(privkey, compressed, bs)
a.mtx.Unlock()
if err != nil { if err != nil {
return "", err return "", err
} }
addrStr := addr.String() addrStr := addr.String()
// Immediately write wallet to disk. // Immediately write wallet to disk.
a.ScheduleWalletWrite() AcctMgr.ds.ScheduleWalletWrite(a)
if err := a.WriteScheduledToDisk(); err != nil { if err := AcctMgr.ds.FlushAccount(a); err != nil {
return "", fmt.Errorf("cannot write account: %v", err) return "", fmt.Errorf("cannot write account: %v", err)
} }
@ -451,15 +348,24 @@ func (a *Account) ImportWIFPrivateKey(wif string, bs *wallet.BlockStamp) (string
return addrStr, nil return addrStr, nil
} }
// ExportToDirectory writes an account to a special export directory. Any
// previous files are overwritten.
func (a *Account) ExportToDirectory(dirBaseName string) error {
dir := filepath.Join(networkDir(cfg.Net()), dirBaseName)
if err := checkCreateDir(dir); err != nil {
return err
}
return AcctMgr.ds.ExportAccount(a, dir)
}
// ExportWatchingWallet returns a new account with a watching wallet // ExportWatchingWallet returns a new account with a watching wallet
// exported by this a's wallet. Both wallets share the same tx and utxo // exported by this a's wallet. Both wallets share the same tx and utxo
// stores, so locking one will lock the other as well. The returned account // stores, so locking one will lock the other as well. The returned account
// should be exported quickly, either to file or to an rpc caller, and then // should be exported quickly, either to file or to an rpc caller, and then
// dropped from scope. // dropped from scope.
func (a *Account) ExportWatchingWallet() (*Account, error) { func (a *Account) ExportWatchingWallet() (*Account, error) {
a.mtx.RLock()
ww, err := a.Wallet.ExportWatchingWallet() ww, err := a.Wallet.ExportWatchingWallet()
a.mtx.RUnlock()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -475,27 +381,20 @@ func (a *Account) exportBase64() (map[string]string, error) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
m := make(map[string]string) m := make(map[string]string)
a.mtx.RLock()
_, err := a.Wallet.WriteTo(buf) _, err := a.Wallet.WriteTo(buf)
a.mtx.RUnlock()
if err != nil { if err != nil {
return nil, err return nil, err
} }
m["wallet"] = base64.StdEncoding.EncodeToString(buf.Bytes()) m["wallet"] = base64.StdEncoding.EncodeToString(buf.Bytes())
buf.Reset() buf.Reset()
a.TxStore.RLock() if _, err = a.TxStore.WriteTo(buf); err != nil {
_, err = a.TxStore.s.WriteTo(buf)
a.TxStore.RUnlock()
if err != nil {
return nil, err return nil, err
} }
m["tx"] = base64.StdEncoding.EncodeToString(buf.Bytes()) m["tx"] = base64.StdEncoding.EncodeToString(buf.Bytes())
buf.Reset() buf.Reset()
a.UtxoStore.RLock() _, err = a.UtxoStore.WriteTo(buf)
_, err = a.UtxoStore.s.WriteTo(buf)
a.UtxoStore.RUnlock()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -518,16 +417,14 @@ func (a *Account) Track() {
i++ i++
} }
err := NotifyNewTXs(CurrentRPCConn(), addrstrs) err := NotifyNewTXs(CurrentServerConn(), addrstrs)
if err != nil { if err != nil {
log.Error("Unable to request transaction updates for address.") log.Error("Unable to request transaction updates for address.")
} }
a.UtxoStore.RLock() for _, utxo := range a.UtxoStore {
for _, utxo := range a.UtxoStore.s {
ReqSpentUtxoNtfn(utxo) ReqSpentUtxoNtfn(utxo)
} }
a.UtxoStore.RUnlock()
} }
// RescanActiveAddresses requests btcd to rescan the blockchain for new // RescanActiveAddresses requests btcd to rescan the blockchain for new
@ -538,7 +435,6 @@ func (a *Account) Track() {
func (a *Account) RescanActiveAddresses() { func (a *Account) RescanActiveAddresses() {
// Determine the block to begin the rescan from. // Determine the block to begin the rescan from.
beginBlock := int32(0) beginBlock := int32(0)
a.mtx.RLock()
if a.fullRescan { if a.fullRescan {
// Need to perform a complete rescan since the wallet creation // Need to perform a complete rescan since the wallet creation
// block. // block.
@ -556,19 +452,16 @@ func (a *Account) RescanActiveAddresses() {
// If we're synced with block x, must scan the blocks x+1 to best block. // If we're synced with block x, must scan the blocks x+1 to best block.
beginBlock = bs.Height + 1 beginBlock = bs.Height + 1
} }
a.mtx.RUnlock()
// Rescan active addresses starting at the determined block height. // Rescan active addresses starting at the determined block height.
Rescan(CurrentRPCConn(), beginBlock, a.ActivePaymentAddresses()) Rescan(CurrentServerConn(), beginBlock, a.ActivePaymentAddresses())
a.WriteScheduledToDisk() AcctMgr.ds.FlushAccount(a)
} }
// SortedActivePaymentAddresses returns a slice of all active payment // SortedActivePaymentAddresses returns a slice of all active payment
// addresses in an account. // addresses in an account.
func (a *Account) SortedActivePaymentAddresses() []string { func (a *Account) SortedActivePaymentAddresses() []string {
a.mtx.RLock()
infos := a.Wallet.SortedActiveAddresses() infos := a.Wallet.SortedActiveAddresses()
a.mtx.RUnlock()
addrs := make([]string, len(infos)) addrs := make([]string, len(infos))
for i, info := range infos { for i, info := range infos {
@ -581,9 +474,7 @@ func (a *Account) SortedActivePaymentAddresses() []string {
// ActivePaymentAddresses returns a set of all active pubkey hashes // ActivePaymentAddresses returns a set of all active pubkey hashes
// in an account. // in an account.
func (a *Account) ActivePaymentAddresses() map[string]struct{} { func (a *Account) ActivePaymentAddresses() map[string]struct{} {
a.mtx.RLock()
infos := a.ActiveAddresses() infos := a.ActiveAddresses()
a.mtx.RUnlock()
addrs := make(map[string]struct{}, len(infos)) addrs := make(map[string]struct{}, len(infos))
for _, info := range infos { for _, info := range infos {
@ -602,16 +493,14 @@ func (a *Account) NewAddress() (btcutil.Address, error) {
} }
// Get next address from wallet. // Get next address from wallet.
a.mtx.Lock()
addr, err := a.Wallet.NextChainedAddress(&bs, cfg.KeypoolSize) addr, err := a.Wallet.NextChainedAddress(&bs, cfg.KeypoolSize)
a.mtx.Unlock()
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Immediately write updated wallet to disk. // Immediately write updated wallet to disk.
a.ScheduleWalletWrite() AcctMgr.ds.ScheduleWalletWrite(a)
if err := a.WriteScheduledToDisk(); err != nil { if err := AcctMgr.ds.FlushAccount(a); err != nil {
return nil, fmt.Errorf("account write failed: %v", err) return nil, fmt.Errorf("account write failed: %v", err)
} }
@ -628,17 +517,13 @@ func (a *Account) NewAddress() (btcutil.Address, error) {
func (a *Account) RecoverAddresses(n int) error { func (a *Account) RecoverAddresses(n int) error {
// Get info on the last chained address. The rescan starts at the // Get info on the last chained address. The rescan starts at the
// earliest block height the last chained address might appear at. // earliest block height the last chained address might appear at.
a.mtx.RLock()
last := a.Wallet.LastChainedAddress() last := a.Wallet.LastChainedAddress()
lastInfo, err := a.Wallet.AddressInfo(last) lastInfo, err := a.Wallet.AddressInfo(last)
a.mtx.RUnlock()
if err != nil { if err != nil {
return err return err
} }
a.mtx.Lock()
addrs, err := a.Wallet.ExtendActiveAddresses(n, cfg.KeypoolSize) addrs, err := a.Wallet.ExtendActiveAddresses(n, cfg.KeypoolSize)
a.mtx.Unlock()
if err != nil { if err != nil {
return err return err
} }
@ -649,7 +534,7 @@ func (a *Account) RecoverAddresses(n int) error {
m[addrs[i].EncodeAddress()] = struct{}{} m[addrs[i].EncodeAddress()] = struct{}{}
} }
go func(addrs map[string]struct{}) { go func(addrs map[string]struct{}) {
jsonErr := Rescan(CurrentRPCConn(), lastInfo.FirstBlock, addrs) jsonErr := Rescan(CurrentServerConn(), lastInfo.FirstBlock, addrs)
if jsonErr != nil { if jsonErr != nil {
log.Errorf("Rescanning for recovered addresses failed: %v", log.Errorf("Rescanning for recovered addresses failed: %v",
jsonErr.Message) jsonErr.Message)
@ -670,7 +555,7 @@ func (a *Account) ReqNewTxsForAddress(addr btcutil.Address) {
log.Debugf("Requesting notifications of TXs sending to address %v", apkh) log.Debugf("Requesting notifications of TXs sending to address %v", apkh)
err := NotifyNewTXs(CurrentRPCConn(), []string{apkh.EncodeAddress()}) err := NotifyNewTXs(CurrentServerConn(), []string{apkh.EncodeAddress()})
if err != nil { if err != nil {
log.Error("Unable to request transaction updates for address.") log.Error("Unable to request transaction updates for address.")
} }
@ -682,7 +567,7 @@ func ReqSpentUtxoNtfn(u *tx.Utxo) {
log.Debugf("Requesting spent UTXO notifications for Outpoint hash %s index %d", log.Debugf("Requesting spent UTXO notifications for Outpoint hash %s index %d",
u.Out.Hash, u.Out.Index) u.Out.Hash, u.Out.Index)
NotifySpent(CurrentRPCConn(), (*btcwire.OutPoint)(&u.Out)) NotifySpent(CurrentServerConn(), (*btcwire.OutPoint)(&u.Out))
} }
// accountdir returns the directory containing an account's wallet, utxo, // accountdir returns the directory containing an account's wallet, utxo,

View file

@ -1,548 +0,0 @@
/*
* Copyright (c) 2013, 2014 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/btcutil"
"github.com/conformal/btcwallet/tx"
"github.com/conformal/btcwallet/wallet"
"github.com/conformal/btcwire"
"os"
"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.RWMutex
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.RLock()
defer store.RUnlock()
account, ok := store.accounts[name]
if !ok {
return nil, ErrAcctNotExist
}
return account, nil
}
// Rollback rolls back each Account saved in the store.
func (store *AccountStore) Rollback(height int32, hash *btcwire.ShaHash) {
log.Debugf("Rolling back tx history since block height %v hash %v",
height, hash)
store.RLock()
defer store.RUnlock()
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.RLock()
defer store.RUnlock()
for name, a := range store.accounts {
// TODO: need a flag or check that the utxo store was actually
// modified, or this will notify even if there are no balance
// changes, or sending these notifications as the utxos are added.
confirmed := a.CalculateBalance(1)
unconfirmed := a.CalculateBalance(0) - confirmed
NotifyWalletBalance(frontendNotificationMaster, a.name, confirmed)
NotifyWalletBalanceUnconfirmed(frontendNotificationMaster, a.name,
unconfirmed)
// If this is the default account, update the block all accounts
// are synced with, and schedule a wallet write.
if name == "" {
a.mtx.Lock()
a.Wallet.SetSyncedWith(bs)
a.mtx.Unlock()
a.ScheduleWalletWrite()
}
}
}
// RecordMinedTx searches through each account's TxStore, searching for a
// sent transaction with the same txid as from a txmined notification. If
// the transaction IDs match, the record in the TxStore is updated with
// the full information about the newly-mined tx, and the TxStore is
// scheduled to be written to disk..
func (store *AccountStore) RecordMinedTx(txid *btcwire.ShaHash,
blkhash *btcwire.ShaHash, blkheight int32, blkindex int,
blktime int64) error {
store.RLock()
defer store.RUnlock()
for _, account := range store.accounts {
// The tx stores will be searched through while holding the
// reader lock, and the writer will only be grabbed if necessary.
account.TxStore.RLock()
// 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[:]) {
account.TxStore.RUnlock()
account.TxStore.Lock()
copy(sendtx.BlockHash[:], blkhash.Bytes())
sendtx.BlockHeight = blkheight
sendtx.BlockIndex = int32(blkindex)
sendtx.BlockTime = blktime
account.TxStore.Unlock()
account.ScheduleTxStoreWrite()
return nil
}
}
}
account.TxStore.RUnlock()
}
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.
func (store *AccountStore) CreateEncryptedWallet(name, desc string, passphrase []byte) error {
store.RLock()
_, ok := store.accounts[name]
store.RUnlock()
if ok {
return ErrAcctExists
}
// 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, cfg.Net(), &bs, cfg.KeypoolSize)
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,
}
account.ScheduleWalletWrite()
account.ScheduleTxStoreWrite()
account.ScheduleUtxoStoreWrite()
// Mark all active payment addresses as belonging to this account.
for addr := range account.ActivePaymentAddresses() {
MarkAddressForAccount(addr, name)
}
// 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.Lock()
store.accounts[name] = account
store.Unlock()
// Begin tracking account against a connected btcd.
//
// TODO(jrick): this should *only* happen if btcd is connected.
account.Track()
// Ensure that the account is written out to disk.
if err := account.WriteScheduledToDisk(); err != nil {
return err
}
return nil
}
// ChangePassphrase unlocks all account wallets with the old
// passphrase, and re-encrypts each using the new passphrase.
//
// TODO(jrick): this is a perfect example of how awful the account
// locking is. It must be replaced.
func (store *AccountStore) ChangePassphrase(old, new []byte) error {
// Due to the undefined order of ranging over the accountstore
// map and how all account wallet writer locks are grabbed
// simultaneously and unlocked with a defer, this function is
// unsafe to call simulateously with other accountstore functions,
// even though the store itself is not modified.
store.Lock()
defer store.Unlock()
if err := store.changePassphrase(old, new); err != nil {
return err
}
// Immediately write out to disk.
return store.WriteAllToDisk()
}
// changePassphrase changes all passphrases for all accounts without grabbing
// any accountstore locks.
func (store *AccountStore) changePassphrase(old, new []byte) error {
// Check that each account can be unlocked with the old passphrase.
for _, a := range store.accounts {
a.mtx.Lock()
defer a.mtx.Unlock()
if locked := a.Wallet.IsLocked(); !locked {
if err := a.Wallet.Lock(); err != nil {
return err
}
}
if err := a.Wallet.Unlock(old); err != nil {
return err
}
defer a.Wallet.Lock()
}
// Change passphrase for each unlocked wallet.
for _, a := range store.accounts {
if err := a.Wallet.ChangePassphrase(new); err != nil {
return err
}
}
return nil
}
// LockWallets locks all account's wallets in the store.
func (store *AccountStore) LockWallets() error {
store.RLock()
defer store.RUnlock()
for _, a := range store.accounts {
if err := a.Lock(); err != nil {
return err
}
}
return nil
}
// UnlockWallets unlocks all account's wallets in the store with the provided
// passphrase. If any wallet unlocks fail, all successfully unlocked wallets
// are locked again.
func (store *AccountStore) UnlockWallets(passphrase string) error {
store.RLock()
defer store.RUnlock()
unlockedAccts := make([]*Account, 0, len(store.accounts))
for _, a := range store.accounts {
if err := a.Unlock([]byte(passphrase)); err != nil {
for _, ua := range unlockedAccts {
ua.Lock()
}
return fmt.Errorf("cannot unlock account %v: %v",
a.name, err)
}
unlockedAccts = append(unlockedAccts, a)
}
return nil
}
// DumpKeys returns all WIF-encoded private keys associated with all
// accounts. All wallets must be unlocked for this operation to succeed.
func (store *AccountStore) DumpKeys() ([]string, error) {
store.RLock()
defer store.RUnlock()
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 btcutil.Address) (string, error) {
store.RLock()
defer store.RUnlock()
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.RLock()
defer store.RUnlock()
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.RLock()
defer store.RUnlock()
// 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
}
// ListSinceBlock returns a slice of maps of strings to interface containing
// structures defining all transactions in the wallets since the given block.
// To be used for the listsinceblock command.
func (store *AccountStore) ListSinceBlock(since, curBlockHeight int32, minconf int) ([]map[string]interface{}, error) {
store.RLock()
defer store.RUnlock()
// Create and fill a map of account names and their balances.
txInfoList := []map[string]interface{}{}
for _, a := range store.accounts {
txTmp, err := a.ListSinceBlock(since, curBlockHeight, minconf)
if err != nil {
return nil, err
}
txInfoList = append(txInfoList, txTmp...)
}
return txInfoList, nil
}
// 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.RLock()
defer store.RUnlock()
for _, account := range store.accounts {
account.RescanActiveAddresses()
}
}
// Track begins tracking all addresses in all accounts for updates from
// btcd.
func (store *AccountStore) Track() {
store.RLock()
defer store.RUnlock()
for _, account := range store.accounts {
account.Track()
}
}
// WalletOpenError is a special error type so problems opening wallet
// files can be differentiated (by a type assertion) from other errors.
type WalletOpenError struct {
Err string
}
// Error satisifies the builtin error interface.
func (e *WalletOpenError) Error() string {
return e.Err
}
// 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 {
wlt := new(wallet.Wallet)
a := &Account{
Wallet: wlt,
name: name,
}
netdir := networkDir(cfg.Net())
if err := checkCreateDir(netdir); err != nil {
return err
}
wfilepath := accountFilename("wallet.bin", name, netdir)
utxofilepath := accountFilename("utxo.bin", name, netdir)
txfilepath := accountFilename("tx.bin", name, netdir)
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
}
msg := fmt.Sprintf("cannot open wallet file: %s", err)
return &WalletOpenError{msg}
}
defer wfile.Close()
if _, err = wlt.ReadFrom(wfile); err != nil {
msg := fmt.Sprintf("cannot read wallet: %s", err)
return &WalletOpenError{msg}
}
// Read tx file. If this fails, return a ErrNoTxs error and let
// the caller decide if a rescan is necessary.
var finalErr error
if txfile, err = os.Open(txfilepath); err != nil {
log.Errorf("cannot open tx file: %s", err)
// This is not a error we should immediately return with,
// but other errors can be more important, so only return
// this if none of the others are hit.
finalErr = ErrNoTxs
} 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
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 {
a.UtxoStore.s = utxos
}
}
store.Lock()
switch finalErr {
case ErrNoTxs:
// Do nothing special for now. This will be implemented when
// the tx history file is properly written.
store.accounts[name] = a
case ErrNoUtxos:
// Add wallet, but mark wallet as needing a full rescan since
// the wallet creation block. This will take place when btcd
// connects.
a.fullRescan = true
store.accounts[name] = a
case nil:
store.accounts[name] = a
default:
log.Warnf("cannot open wallet: %v", err)
}
store.Unlock()
// Mark all active payment addresses as belonging to this account.
for addr := range a.ActivePaymentAddresses() {
MarkAddressForAccount(addr, name)
}
return nil
}

494
acctmgr.go Normal file
View file

@ -0,0 +1,494 @@
/*
* Copyright (c) 2013, 2014 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"
"container/list"
"errors"
"fmt"
"github.com/conformal/btcutil"
"github.com/conformal/btcwallet/tx"
"github.com/conformal/btcwallet/wallet"
"github.com/conformal/btcwire"
"time"
)
// Errors relating to accounts.
var (
ErrAccountExists = errors.New("account already exists")
ErrWalletExists = errors.New("wallet already exists")
ErrNotFound = errors.New("not found")
)
// AcctMgr is the global account manager for all opened accounts.
var AcctMgr = NewAccountManager()
// AccountManager manages a collection of accounts.
type AccountManager struct {
// The accounts accessed through the account manager are not safe for
// concurrent access. The account manager therefore contains a
// binary semaphore channel to prevent incorrect access.
bsem chan struct{}
accessAccount chan *accessAccountRequest
accessAll chan *accessAllRequest
add chan *Account
remove chan *Account
ds *DiskSyncer // might move to inside Start
}
// NewAccountManager returns a new AccountManager.
func NewAccountManager() *AccountManager {
am := &AccountManager{
bsem: make(chan struct{}, 1),
accessAccount: make(chan *accessAccountRequest),
accessAll: make(chan *accessAllRequest),
add: make(chan *Account),
remove: make(chan *Account),
}
am.ds = NewDiskSyncer(am)
return am
}
// Start maintains accounts and structures for quick lookups for account
// information. Access to these structures must be done through with the
// channels in the AccountManger struct fields. This function never returns
// and should be called as a new goroutine.
func (am *AccountManager) Start() {
// Ready the semaphore - can't grab unless the manager has started.
am.bsem <- struct{}{}
// Start the account manager's disk syncer.
go am.ds.Start()
// List and map of all accounts.
l := list.New()
m := make(map[string]*Account)
wait := 10 * time.Second
timer := time.NewTimer(wait)
for {
select {
case access := <-am.accessAccount:
a, ok := m[access.name]
access.resp <- &accessAccountResponse{
a: a,
ok: ok,
}
case access := <-am.accessAll:
s := make([]*Account, 0, l.Len())
for e := l.Front(); e != nil; e = e.Next() {
s = append(s, e.Value.(*Account))
}
access.resp <- s
case a := <-am.add:
if _, ok := m[a.name]; ok {
break
}
m[a.name] = a
l.PushBack(a)
case a := <-am.remove:
if _, ok := m[a.name]; ok {
delete(m, a.name)
for e := l.Front(); e != nil; e = e.Next() {
v := e.Value.(*Account)
if v == a {
l.Remove(e)
break
}
}
}
case <-timer.C:
if err := am.ds.FlushScheduled(); err != nil {
log.Errorf("Cannot write account: %v", err)
}
timer = time.NewTimer(wait)
}
}
}
// Grab grabs the account manager's binary semaphore.
func (am *AccountManager) Grab() {
<-am.bsem
}
// Release releases the account manager's binary semaphore.
func (am *AccountManager) Release() {
am.bsem <- struct{}{}
}
type accessAccountRequest struct {
name string
resp chan *accessAccountResponse
}
type accessAccountResponse struct {
a *Account
ok bool
}
// Account returns the account specified by name, or ErrNotFound
// as an error if the account is not found.
func (am *AccountManager) Account(name string) (*Account, error) {
req := &accessAccountRequest{
name: name,
resp: make(chan *accessAccountResponse),
}
am.accessAccount <- req
resp := <-req.resp
if !resp.ok {
return nil, ErrNotFound
}
return resp.a, nil
}
type accessAllRequest struct {
resp chan []*Account
}
// AllAccounts returns a slice of all managed accounts.
func (am *AccountManager) AllAccounts() []*Account {
req := &accessAllRequest{
resp: make(chan []*Account),
}
am.accessAll <- req
return <-req.resp
}
// AddAccount adds an account to the collection managed by an AccountManager.
func (am *AccountManager) AddAccount(a *Account) {
am.add <- a
}
// RemoveAccount removes an account to the collection managed by an
// AccountManager.
func (am *AccountManager) RemoveAccount(a *Account) {
am.remove <- a
}
// RegisterNewAccount adds a new memory account to the account manager,
// and immediately writes the account to disk.
func (am *AccountManager) RegisterNewAccount(a *Account) error {
am.AddAccount(a)
// Ensure that the new account is written out to disk.
am.ds.ScheduleWalletWrite(a)
am.ds.ScheduleTxStoreWrite(a)
am.ds.ScheduleUtxoStoreWrite(a)
if err := am.ds.FlushAccount(a); err != nil {
am.RemoveAccount(a)
return err
}
return nil
}
// Rollback rolls back each managed Account to the state before the block
// specified by height and hash was connected to the main chain.
func (am *AccountManager) Rollback(height int32, hash *btcwire.ShaHash) {
log.Debugf("Rolling back tx history since block height %v hash %v",
height, hash)
for _, a := range am.AllAccounts() {
if a.UtxoStore.Rollback(height, hash) {
am.ds.ScheduleUtxoStoreWrite(a)
}
if a.TxStore.Rollback(height, hash) {
am.ds.ScheduleTxStoreWrite(a)
}
}
}
// Rollback reverts each stored Account to a state before the block
// with the passed chainheight and block hash was connected to the main
// chain. This is used to remove transactions and utxos for each wallet
// that occured on a chain no longer considered to be the main chain.
func (a *Account) Rollback(height int32, hash *btcwire.ShaHash) {
}
// BlockNotify notifies all frontends of any changes from the new block,
// including changed balances. Each account is then set to be synced
// with the latest block.
func (am *AccountManager) BlockNotify(bs *wallet.BlockStamp) {
for _, a := range am.AllAccounts() {
// TODO: need a flag or check that the utxo store was actually
// modified, or this will notify even if there are no balance
// changes, or sending these notifications as the utxos are added.
confirmed := a.CalculateBalance(1)
unconfirmed := a.CalculateBalance(0) - confirmed
NotifyWalletBalance(frontendNotificationMaster, a.name, confirmed)
NotifyWalletBalanceUnconfirmed(frontendNotificationMaster, a.name,
unconfirmed)
// If this is the default account, update the block all accounts
// are synced with, and schedule a wallet write.
if a.Name() == "" {
a.Wallet.SetSyncedWith(bs)
am.ds.ScheduleWalletWrite(a)
}
}
}
// RecordMinedTx searches through each account's TxStore, searching for a
// sent transaction with the same txid as from a txmined notification. If
// the transaction IDs match, the record in the TxStore is updated with
// the full information about the newly-mined tx, and the TxStore is
// scheduled to be written to disk..
func (am *AccountManager) RecordMinedTx(txid *btcwire.ShaHash,
blkhash *btcwire.ShaHash, blkheight int32, blkindex int,
blktime int64) error {
for _, a := range am.AllAccounts() {
// 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(a.TxStore) - 1; i >= 0; i-- {
sendtx, ok := a.TxStore[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
am.ds.ScheduleTxStoreWrite(a)
return nil
}
}
}
}
return errors.New("txid does not match any recorded sent transaction")
}
// CalculateBalance returns the balance, calculated using minconf block
// confirmations, of an account.
func (am *AccountManager) CalculateBalance(account string, minconf int) (float64, error) {
a, err := am.Account(account)
if err != nil {
return 0, err
}
return a.CalculateBalance(minconf), nil
}
// CreateEncryptedWallet creates a new default account with a wallet file
// encrypted with passphrase.
func (am *AccountManager) CreateEncryptedWallet(passphrase []byte) error {
if len(am.AllAccounts()) != 0 {
return ErrWalletExists
}
// Get current block's height and hash.
bs, err := GetCurBlock()
if err != nil {
return err
}
// Create new wallet in memory.
wlt, err := wallet.NewWallet("", "Default acccount", passphrase,
cfg.Net(), &bs, cfg.KeypoolSize)
if err != nil {
return err
}
// Create new account and begin managing with the global account
// manager. Registering will fail if the new account can not be
// written immediately to disk.
a := &Account{
Wallet: wlt,
}
if err := am.RegisterNewAccount(a); err != nil {
return err
}
// Mark all active payment addresses as belonging to this account.
//
// TODO(jrick) move this to the account manager
for addr := range a.ActivePaymentAddresses() {
MarkAddressForAccount(addr, "")
}
// Begin tracking account against a connected btcd.
a.Track()
return nil
}
// ChangePassphrase unlocks all account wallets with the old
// passphrase, and re-encrypts each using the new passphrase.
func (am *AccountManager) ChangePassphrase(old, new []byte) error {
accts := am.AllAccounts()
for _, a := range accts {
if locked := a.Wallet.IsLocked(); !locked {
if err := a.Wallet.Lock(); err != nil {
return err
}
}
if err := a.Wallet.Unlock(old); err != nil {
return err
}
defer a.Wallet.Lock()
}
// Change passphrase for each unlocked wallet.
for _, a := range accts {
if err := a.Wallet.ChangePassphrase(new); err != nil {
return err
}
}
// Immediately write out to disk.
return am.ds.WriteBatch(accts)
}
// LockWallets locks all managed account wallets.
func (am *AccountManager) LockWallets() error {
for _, a := range am.AllAccounts() {
if err := a.Lock(); err != nil {
return err
}
}
return nil
}
// UnlockWallets unlocks all managed account's wallets. If any wallet unlocks
// fail, all successfully unlocked wallets are locked again.
func (am *AccountManager) UnlockWallets(passphrase string) error {
accts := am.AllAccounts()
unlockedAccts := make([]*Account, 0, len(accts))
for _, a := range accts {
if err := a.Unlock([]byte(passphrase)); err != nil {
for _, ua := range unlockedAccts {
ua.Lock()
}
return fmt.Errorf("cannot unlock account %v: %v",
a.name, err)
}
unlockedAccts = append(unlockedAccts, a)
}
return nil
}
// DumpKeys returns all WIF-encoded private keys associated with all
// accounts. All wallets must be unlocked for this operation to succeed.
func (am *AccountManager) DumpKeys() ([]string, error) {
var keys []string
for _, a := range am.AllAccounts() {
switch walletKeys, err := a.DumpPrivKeys(); err {
case wallet.ErrWalletLocked:
return nil, err
case nil:
keys = append(keys, walletKeys...)
default: // any other non-nil error
return nil, err
}
}
return keys, nil
}
// DumpWIFPrivateKey searches through all accounts for the bitcoin
// payment address addr and returns the WIF-encdoded private key.
func (am *AccountManager) DumpWIFPrivateKey(addr btcutil.Address) (string, error) {
for _, a := range am.AllAccounts() {
switch wif, err := a.DumpWIFPrivateKey(addr); err {
case wallet.ErrAddressNotFound:
// Move on to the next account.
continue
case nil:
return wif, nil
default: // all other non-nil errors
return "", err
}
}
return "", errors.New("address does not refer to a key")
}
// NotifyBalances notifies a wallet frontend of all confirmed and unconfirmed
// account balances.
func (am *AccountManager) NotifyBalances(frontend chan []byte) {
for _, a := range am.AllAccounts() {
balance := a.CalculateBalance(1)
unconfirmed := a.CalculateBalance(0) - balance
NotifyWalletBalance(frontend, a.name, balance)
NotifyWalletBalanceUnconfirmed(frontend, a.name, unconfirmed)
}
}
// ListAccounts returns a map of account names to their current account
// balances. The balances are calculated using minconf confirmations.
func (am *AccountManager) ListAccounts(minconf int) map[string]float64 {
// Create and fill a map of account names and their balances.
pairs := make(map[string]float64)
for _, a := range am.AllAccounts() {
pairs[a.name] = a.CalculateBalance(minconf)
}
return pairs
}
// ListSinceBlock returns a slice of maps of strings to interface containing
// structures defining all transactions in the wallets since the given block.
// To be used for the listsinceblock command.
func (am *AccountManager) ListSinceBlock(since, curBlockHeight int32, minconf int) ([]map[string]interface{}, error) {
// Create and fill a map of account names and their balances.
txInfoList := []map[string]interface{}{}
for _, a := range am.AllAccounts() {
txTmp, err := a.ListSinceBlock(since, curBlockHeight, minconf)
if err != nil {
return nil, err
}
txInfoList = append(txInfoList, txTmp...)
}
return txInfoList, nil
}
// 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 (am *AccountManager) RescanActiveAddresses() {
for _, account := range am.AllAccounts() {
account.RescanActiveAddresses()
}
}
// Track begins tracking all addresses in all accounts for updates from
// btcd.
func (am *AccountManager) Track() {
for _, a := range am.AllAccounts() {
a.Track()
}
}

139
cmd.go
View file

@ -18,8 +18,10 @@ package main
import ( import (
"errors" "errors"
"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"
"io/ioutil" "io/ioutil"
@ -72,7 +74,7 @@ func GetCurBlock() (bs wallet.BlockStamp, err error) {
return bs, nil return bs, nil
} }
bb, _ := GetBestBlock(CurrentRPCConn()) bb, _ := GetBestBlock(CurrentServerConn())
if bb == nil { if bb == nil {
return wallet.BlockStamp{ return wallet.BlockStamp{
Height: int32(btcutil.BlockHeightUnknown), Height: int32(btcutil.BlockHeightUnknown),
@ -149,6 +151,8 @@ func main() {
// Check and update any old file locations. // Check and update any old file locations.
updateOldFileLocations() updateOldFileLocations()
go AcctMgr.Start()
// Open all account saved to disk. // Open all account saved to disk.
OpenAccounts() OpenAccounts()
@ -159,9 +163,6 @@ func main() {
os.Exit(1) os.Exit(1)
} }
// Start account disk syncer goroutine.
go AccountDiskSyncer()
go func() { go func() {
s, err := newServer(cfg.SvrListeners) s, err := newServer(cfg.SvrListeners)
if err != nil { if err != nil {
@ -177,6 +178,10 @@ func main() {
// Begin generating new IDs for JSON calls. // Begin generating new IDs for JSON calls.
go JSONIDGenerator(NewJSONID) go JSONIDGenerator(NewJSONID)
// Begin RPC server goroutines.
go RPCGateway()
go WalletRequestProcessor()
// Begin maintanence goroutines. // Begin maintanence goroutines.
go SendBeforeReceiveHistorySync(SendTxHistSyncChans.add, go SendBeforeReceiveHistorySync(SendTxHistSyncChans.add,
SendTxHistSyncChans.done, SendTxHistSyncChans.done,
@ -207,8 +212,8 @@ func main() {
case conn := <-updateBtcd: case conn := <-updateBtcd:
btcd = conn btcd = conn
case access := <-accessRPC: case access := <-accessServer:
access.rpc <- btcd access.server <- btcd
} }
} }
}() }()
@ -246,6 +251,99 @@ func main() {
} }
} }
// WalletOpenError is a special error type so problems opening wallet
// files can be differentiated (by a type assertion) from other errors.
type WalletOpenError struct {
Err string
}
// Error satisifies the builtin error interface.
func (e *WalletOpenError) Error() string {
return e.Err
}
// OpenSavedAccount opens a named account from disk. If the wallet does not
// exist, ErrNoWallet is returned as an error.
func OpenSavedAccount(name string, cfg *config) (*Account, error) {
netdir := networkDir(cfg.Net())
if err := checkCreateDir(netdir); err != nil {
return nil, err
}
wlt := new(wallet.Wallet)
a := &Account{
Wallet: wlt,
name: name,
}
wfilepath := accountFilename("wallet.bin", name, netdir)
utxofilepath := accountFilename("utxo.bin", name, netdir)
txfilepath := accountFilename("tx.bin", name, netdir)
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
}
msg := fmt.Sprintf("cannot open wallet file: %s", err)
return nil, &WalletOpenError{msg}
}
defer wfile.Close()
if _, err = wlt.ReadFrom(wfile); err != nil {
msg := fmt.Sprintf("cannot read wallet: %s", err)
return nil, &WalletOpenError{msg}
}
// Read tx file. If this fails, return a ErrNoTxs error and let
// the caller decide if a rescan is necessary.
var finalErr error
if txfile, err = os.Open(txfilepath); err != nil {
log.Errorf("cannot open tx file: %s", err)
// This is not a error we should immediately return with,
// but other errors can be more important, so only return
// this if none of the others are hit.
finalErr = ErrNoTxs
} 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 = 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
a.fullRescan = true
} else {
defer utxofile.Close()
if _, err = utxos.ReadFrom(utxofile); err != nil {
log.Errorf("cannot read utxo file: %s", err)
finalErr = ErrNoUtxos
} else {
a.UtxoStore = utxos
}
}
// Mark all active payment addresses as belonging to this account.
for addr := range a.ActivePaymentAddresses() {
MarkAddressForAccount(addr, name)
}
return a, finalErr
}
// OpenAccounts attempts to open all saved accounts. // OpenAccounts attempts to open all saved accounts.
func OpenAccounts() { func OpenAccounts() {
// If the network (account) directory is missing, but the temporary // If the network (account) directory is missing, but the temporary
@ -265,7 +363,8 @@ func OpenAccounts() {
// The default account must exist, or btcwallet acts as if no // The default account must exist, or btcwallet acts as if no
// wallets/accounts have been created yet. // wallets/accounts have been created yet.
if err := accountstore.OpenAccount("", cfg); err != nil { a, err := OpenSavedAccount("", cfg)
if err != nil {
switch err.(type) { switch err.(type) {
case *WalletOpenError: case *WalletOpenError:
log.Errorf("Default account wallet file unreadable: %v", err) log.Errorf("Default account wallet file unreadable: %v", err)
@ -275,6 +374,7 @@ func OpenAccounts() {
log.Warnf("Non-critical problem opening an account file: %v", err) log.Warnf("Non-critical problem opening an account file: %v", err)
} }
} }
AcctMgr.AddAccount(a)
// Read all filenames in the account directory, and look for any // Read all filenames in the account directory, and look for any
// filenames matching '*-wallet.bin'. These are wallets for // filenames matching '*-wallet.bin'. These are wallets for
@ -305,7 +405,8 @@ func OpenAccounts() {
// Log txstore/utxostore errors as these will be recovered // Log txstore/utxostore errors as these will be recovered
// from with a rescan, but wallet errors must be returned // from with a rescan, but wallet errors must be returned
// to the caller. // to the caller.
if err := accountstore.OpenAccount(a, cfg); err != nil { a, err := OpenSavedAccount(a, cfg)
if err != nil {
switch err.(type) { switch err.(type) {
case *WalletOpenError: case *WalletOpenError:
log.Errorf("Error opening account's wallet: %v", err) log.Errorf("Error opening account's wallet: %v", err)
@ -313,24 +414,26 @@ func OpenAccounts() {
default: default:
log.Warnf("Non-critical error opening an account file: %v", err) log.Warnf("Non-critical error opening an account file: %v", err)
} }
} else {
AcctMgr.AddAccount(a)
} }
} }
} }
var accessRPC = make(chan *AccessCurrentRPCConn) var accessServer = make(chan *AccessCurrentServerConn)
// AccessCurrentRPCConn is used to access the current RPC connection // AccessCurrentServerConn is used to access the current RPC connection
// from the goroutine managing btcd-side RPC connections. // from the goroutine managing btcd-side RPC connections.
type AccessCurrentRPCConn struct { type AccessCurrentServerConn struct {
rpc chan RPCConn server chan ServerConn
} }
// CurrentRPCConn returns the most recently-connected btcd-side // CurrentServerConn returns the most recently-connected btcd-side
// RPC connection. // RPC connection.
func CurrentRPCConn() RPCConn { func CurrentServerConn() ServerConn {
access := &AccessCurrentRPCConn{ access := &AccessCurrentServerConn{
rpc: make(chan RPCConn), server: make(chan ServerConn),
} }
accessRPC <- access accessServer <- access
return <-access.rpc return <-access.server
} }

View file

@ -158,11 +158,6 @@ func selectInputs(s tx.UtxoStore, amt uint64, minconf int) (inputs []*tx.Utxo, b
// block hash) Utxo. ErrInsufficientFunds is returned if there are not // block hash) Utxo. ErrInsufficientFunds is returned if there are not
// enough eligible unspent outputs to create the transaction. // enough eligible unspent outputs to create the transaction.
func (a *Account) txToPairs(pairs map[string]int64, minconf int) (*CreatedTx, error) { func (a *Account) txToPairs(pairs map[string]int64, minconf int) (*CreatedTx, error) {
// Recorded unspent transactions should not be modified until this
// finishes.
a.UtxoStore.RLock()
defer a.UtxoStore.RUnlock()
// Create a new transaction which will include all input scripts. // Create a new transaction which will include all input scripts.
msgtx := btcwire.NewMsgTx() msgtx := btcwire.NewMsgTx()
@ -224,13 +219,13 @@ func (a *Account) txToPairs(pairs map[string]int64, minconf int) (*CreatedTx, er
// Get the number of satoshis to increment fee by when searching for // Get the number of satoshis to increment fee by when searching for
// the minimum tx fee needed. // the minimum tx fee needed.
var fee int64 = 0 fee := int64(0)
for { for {
msgtx = txNoInputs.Copy() msgtx = txNoInputs.Copy()
// Select unspent outputs to be used in transaction based on the amount // Select unspent outputs to be used in transaction based on the amount
// neededing to sent, and the current fee estimation. // neededing to sent, and the current fee estimation.
inputs, btcin, err := selectInputs(a.UtxoStore.s, uint64(amt+fee), inputs, btcin, err := selectInputs(a.UtxoStore, uint64(amt+fee),
minconf) minconf)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -109,7 +109,7 @@ func TestFakeTxs(t *testing.T) {
utxo.Subscript = tx.PkScript(ss) utxo.Subscript = tx.PkScript(ss)
utxo.Amt = 1000000 utxo.Amt = 1000000
utxo.Height = 12345 utxo.Height = 12345
a.UtxoStore.s = append(a.UtxoStore.s, utxo) a.UtxoStore = append(a.UtxoStore, utxo)
// Fake our current block height so btcd doesn't need to be queried. // Fake our current block height so btcd doesn't need to be queried.
curBlock.BlockStamp.Height = 12346 curBlock.BlockStamp.Height = 12346

View file

@ -22,7 +22,6 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"time"
) )
// networkDir returns the directory name of a network directory to hold account // networkDir returns the directory name of a network directory to hold account
@ -122,9 +121,9 @@ func newSyncSchedule(dir string) *syncSchedule {
return s return s
} }
// FlushAccount writes all scheduled account files to disk for // flushAccount writes all scheduled account files to disk for
// a single account and removes them from the schedule. // a single account and removes them from the schedule.
func (s *syncSchedule) FlushAccount(a *Account) error { func (s *syncSchedule) flushAccount(a *Account) error {
if _, ok := s.utxos[a]; ok { if _, ok := s.utxos[a]; ok {
if err := a.writeUtxoStore(s.dir); err != nil { if err := a.writeUtxoStore(s.dir); err != nil {
return err return err
@ -147,9 +146,9 @@ func (s *syncSchedule) FlushAccount(a *Account) error {
return nil return nil
} }
// Flush writes all scheduled account files and removes each // flush writes all scheduled account files and removes each
// from the schedule. // from the schedule.
func (s *syncSchedule) Flush() error { func (s *syncSchedule) flush() error {
for a := range s.utxos { for a := range s.utxos {
if err := a.writeUtxoStore(s.dir); err != nil { if err := a.writeUtxoStore(s.dir); err != nil {
return err return err
@ -174,22 +173,16 @@ func (s *syncSchedule) Flush() error {
return nil return nil
} }
// Channels for AccountDiskSyncer. type flushScheduledRequest struct {
var ( err chan error
scheduleWalletWrite = make(chan *Account) }
scheduleTxStoreWrite = make(chan *Account)
scheduleUtxoStoreWrite = make(chan *Account)
syncBatch = make(chan *syncBatchRequest)
syncAccount = make(chan *syncRequest)
exportAccount = make(chan *exportRequest)
)
type syncRequest struct { type flushAccountRequest struct {
a *Account a *Account
err chan error err chan error
} }
type syncBatchRequest struct { type writeBatchRequest struct {
a []*Account a []*Account
err chan error err chan error
} }
@ -200,14 +193,48 @@ type exportRequest struct {
err chan error err chan error
} }
// AccountDiskSyncer manages a set of "dirty" account files which must // DiskSyncer manages all disk write operations for a collection of accounts.
// be written to disk, and synchronizes all writes in a single goroutine. type DiskSyncer struct {
// After 10 seconds since the latest sync, all unwritten files are written // Flush scheduled account writes.
// and removed. Writes for a single account may be scheduled immediately by flushScheduled chan *flushScheduledRequest
// calling WriteScheduledToDisk. flushAccount chan *flushAccountRequest
// Schedule file writes for an account.
scheduleWallet chan *Account
scheduleTxStore chan *Account
scheduleUtxoStore chan *Account
// Write a collection of accounts all at once.
writeBatch chan *writeBatchRequest
// Write an account export.
exportAccount chan *exportRequest
// Account manager for this DiskSyncer. This is only
// needed to grab the account manager semaphore.
am *AccountManager
}
// NewDiskSyncer creates a new DiskSyncer.
func NewDiskSyncer(am *AccountManager) *DiskSyncer {
return &DiskSyncer{
flushScheduled: make(chan *flushScheduledRequest),
flushAccount: make(chan *flushAccountRequest),
scheduleWallet: make(chan *Account),
scheduleTxStore: make(chan *Account),
scheduleUtxoStore: make(chan *Account),
writeBatch: make(chan *writeBatchRequest),
exportAccount: make(chan *exportRequest),
am: am,
}
}
// Start starts the disk syncer. It manages a set of "dirty" account files
// which must be written to disk, and synchronizes all writes in a single
// goroutine. Periodic flush operations may be signaled by an AccountManager.
// //
// This never returns and is meant to be called from a goroutine. // This never returns and is should be called from a new goroutine.
func AccountDiskSyncer() { func (ds *DiskSyncer) Start() {
netdir := networkDir(cfg.Net()) netdir := networkDir(cfg.Net())
if err := checkCreateDir(netdir); err != nil { if err := checkCreateDir(netdir); err != nil {
log.Errorf("Unable to create or write to account directory: %v", err) log.Errorf("Unable to create or write to account directory: %v", err)
@ -215,22 +242,24 @@ func AccountDiskSyncer() {
tmpnetdir := tmpNetworkDir(cfg.Net()) tmpnetdir := tmpNetworkDir(cfg.Net())
schedule := newSyncSchedule(netdir) schedule := newSyncSchedule(netdir)
ticker := time.Tick(10 * time.Second)
for { for {
select { select {
case a := <-scheduleWalletWrite: case fr := <-ds.flushScheduled:
fr.err <- schedule.flush()
case fr := <-ds.flushAccount:
fr.err <- schedule.flushAccount(fr.a)
case a := <-ds.scheduleWallet:
schedule.wallets[a] = struct{}{} schedule.wallets[a] = struct{}{}
case a := <-scheduleTxStoreWrite: case a := <-ds.scheduleTxStore:
schedule.txs[a] = struct{}{} schedule.txs[a] = struct{}{}
case a := <-scheduleUtxoStoreWrite: case a := <-ds.scheduleUtxoStore:
schedule.utxos[a] = struct{}{} schedule.utxos[a] = struct{}{}
case sr := <-syncAccount: case sr := <-ds.writeBatch:
sr.err <- schedule.FlushAccount(sr.a)
case sr := <-syncBatch:
err := batchWriteAccounts(sr.a, tmpnetdir, netdir) err := batchWriteAccounts(sr.a, tmpnetdir, netdir)
if err == nil { if err == nil {
// All accounts have been synced, old schedule // All accounts have been synced, old schedule
@ -239,40 +268,71 @@ func AccountDiskSyncer() {
} }
sr.err <- err sr.err <- err
case er := <-exportAccount: case er := <-ds.exportAccount:
a := er.a a := er.a
dir := er.dir dir := er.dir
er.err <- a.writeAll(dir) er.err <- a.writeAll(dir)
case <-ticker:
if err := schedule.Flush(); err != nil {
log.Errorf("Cannot write account: %v", err)
}
} }
} }
} }
// WriteAllToDisk writes all account files for all accounts at once. Unlike // FlushScheduled writes all scheduled account files to disk.
// writing individual account files, this causes each account file to be func (ds *DiskSyncer) FlushScheduled() error {
// written to a new network directory to replace the old one. Use this ds.am.Grab()
// function when it is needed to ensure an all or nothing write for all err := make(chan error)
// account files. ds.flushScheduled <- &flushScheduledRequest{err}
// ds.am.Release()
// It is a runtime error to call this without holding the store writer lock. return <-err
func (store *AccountStore) WriteAllToDisk() error { }
accts := make([]*Account, 0, len(store.accounts))
for _, a := range store.accounts {
accts = append(accts, a)
}
err := make(chan error, 1) // FlushAccount writes all scheduled account files to disk for a single
syncBatch <- &syncBatchRequest{ // account.
a: accts, func (ds *DiskSyncer) FlushAccount(a *Account) error {
err := make(chan error)
ds.flushAccount <- &flushAccountRequest{a: a, err: err}
return <-err
}
// ScheduleWalletWrite schedules an account's wallet to be written to disk.
func (ds *DiskSyncer) ScheduleWalletWrite(a *Account) {
ds.scheduleWallet <- a
}
// ScheduleTxStoreWrite schedules an account's transaction store to be
// written to disk.
func (ds *DiskSyncer) ScheduleTxStoreWrite(a *Account) {
ds.scheduleTxStore <- a
}
// ScheduleUtxoStoreWrite schedules an account's utxo store to be written
// to disk.
func (ds *DiskSyncer) ScheduleUtxoStoreWrite(a *Account) {
ds.scheduleUtxoStore <- a
}
// WriteBatch safely replaces all account files in the network directory
// with new files created from all accounts in a.
func (ds *DiskSyncer) WriteBatch(a []*Account) error {
err := make(chan error)
ds.writeBatch <- &writeBatchRequest{
a: a,
err: err, err: err,
} }
return <-err return <-err
} }
// ExportAccount writes all account files for a to a new directory.
func (ds *DiskSyncer) ExportAccount(a *Account, dir string) error {
err := make(chan error)
er := &exportRequest{
dir: dir,
a: a,
err: err,
}
ds.exportAccount <- er
return <-err
}
func batchWriteAccounts(accts []*Account, tmpdir, netdir string) error { func batchWriteAccounts(accts []*Account, tmpdir, netdir string) error {
if err := freshDir(tmpdir); err != nil { if err := freshDir(tmpdir); err != nil {
return err return err
@ -294,52 +354,6 @@ func batchWriteAccounts(accts []*Account, tmpdir, netdir string) error {
return nil return nil
} }
// WriteScheduledToDisk signals AccountDiskSyncer to write all scheduled
// account files for a to disk now instead of waiting for the next sync
// interval. This function blocks until all the file writes for a have
// finished, and returns a non-nil error if any of the file writes failed.
func (a *Account) WriteScheduledToDisk() error {
err := make(chan error, 1)
syncAccount <- &syncRequest{
a: a,
err: err,
}
return <-err
}
// ScheduleWalletWrite schedules a write of an account's wallet file.
func (a *Account) ScheduleWalletWrite() {
scheduleWalletWrite <- a
}
// ScheduleTxStoreWrite schedules a write of an account's tx store file.
func (a *Account) ScheduleTxStoreWrite() {
scheduleTxStoreWrite <- a
}
// ScheduleUtxoStoreWrite schedules a write of an account's utxo store file.
func (a *Account) ScheduleUtxoStoreWrite() {
scheduleUtxoStoreWrite <- a
}
// ExportToDirectory writes an account to a special export directory. Any
// previous files are overwritten.
func (a *Account) ExportToDirectory(dirBaseName string) error {
dir := filepath.Join(networkDir(cfg.Net()), dirBaseName)
if err := checkCreateDir(dir); err != nil {
return err
}
err := make(chan error)
er := &exportRequest{
dir: dir,
a: a,
err: err,
}
exportAccount <- er
return <-err
}
func (a *Account) writeAll(dir string) error { func (a *Account) writeAll(dir string) error {
if err := a.writeUtxoStore(dir); err != nil { if err := a.writeUtxoStore(dir); err != nil {
return err return err
@ -362,10 +376,7 @@ func (a *Account) writeWallet(dir string) error {
} }
defer tmpfile.Close() defer tmpfile.Close()
a.mtx.RLock() if _, err = a.Wallet.WriteTo(tmpfile); err != nil {
_, err = a.Wallet.WriteTo(tmpfile)
a.mtx.RUnlock()
if err != nil {
return err return err
} }
@ -385,10 +396,7 @@ func (a *Account) writeTxStore(dir string) error {
} }
defer tmpfile.Close() defer tmpfile.Close()
a.TxStore.RLock() if _, err = a.TxStore.WriteTo(tmpfile); err != nil {
_, err = a.TxStore.s.WriteTo(tmpfile)
a.TxStore.RUnlock()
if err != nil {
return err return err
} }
@ -408,10 +416,7 @@ func (a *Account) writeUtxoStore(dir string) error {
} }
defer tmpfile.Close() defer tmpfile.Close()
a.UtxoStore.RLock() if _, err = a.UtxoStore.WriteTo(tmpfile); err != nil {
_, err = a.UtxoStore.s.WriteTo(tmpfile)
a.UtxoStore.RUnlock()
if err != nil {
return err return err
} }

View file

@ -14,15 +14,12 @@
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/ */
// This file implements the websocket RPC connection to a btcd instance. // This file implements the notification handlers for btcd-side notifications.
package main package main
import ( import (
"code.google.com/p/go.net/websocket"
"encoding/hex" "encoding/hex"
"encoding/json"
"errors"
"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/tx"
@ -33,260 +30,7 @@ import (
"time" "time"
) )
// ErrBtcdDisconnected describes an error where an operation cannot type notificationHandler func(btcjson.Cmd)
// successfully complete due to btcwallet not being connected to
// btcd.
var ErrBtcdDisconnected = btcjson.Error{
Code: -1,
Message: "btcd disconnected",
}
// BtcdRPCConn is a type managing a client connection to a btcd RPC server
// over websockets.
type BtcdRPCConn struct {
ws *websocket.Conn
addRequest chan *AddRPCRequest
closed chan struct{}
}
// Ensure that BtcdRPCConn can be used as an RPCConn.
var _ RPCConn = &BtcdRPCConn{}
// NewBtcdRPCConn creates a new RPC connection from a btcd websocket
// connection to btcd.
func NewBtcdRPCConn(ws *websocket.Conn) *BtcdRPCConn {
conn := &BtcdRPCConn{
ws: ws,
addRequest: make(chan *AddRPCRequest),
closed: make(chan struct{}),
}
return conn
}
// SendRequest sends an RPC request and returns a channel to read the response's
// result and error. Part of the RPCConn interface.
func (btcd *BtcdRPCConn) SendRequest(request *RPCRequest) chan *RPCResponse {
select {
case <-btcd.closed:
// The connection has closed, so instead of adding and sending
// a request, return a channel that just replies with the
// error for a disconnected btcd.
responseChan := make(chan *RPCResponse)
go func() {
response := &RPCResponse{
Err: &ErrBtcdDisconnected,
}
responseChan <- response
}()
return responseChan
default:
addRequest := &AddRPCRequest{
Request: request,
ResponseChan: make(chan chan *RPCResponse, 1),
}
btcd.addRequest <- addRequest
return <-addRequest.ResponseChan
}
}
// Connected returns whether the connection remains established to the RPC
// server.
//
// This function probably should be removed, as any checks for confirming
// the connection are no longer valid after the check and may result in
// races.
func (btcd *BtcdRPCConn) Connected() bool {
select {
case <-btcd.closed:
return false
default:
return true
}
}
// AddRPCRequest is used to add an RPCRequest to the pool of requests
// being manaaged by a btcd RPC connection.
type AddRPCRequest struct {
Request *RPCRequest
ResponseChan chan chan *RPCResponse
}
// send performs the actual send of the marshaled request over the btcd
// websocket connection.
func (btcd *BtcdRPCConn) send(rpcrequest *RPCRequest) error {
// btcjson.Cmds define their own MarshalJSON which returns an error
// to satisify the json.Marshaler interface, but will never error.
mrequest, _ := rpcrequest.request.MarshalJSON()
return websocket.Message.Send(btcd.ws, mrequest)
}
type receivedResponse struct {
id uint64
raw string
reply *btcjson.Reply
}
// Start starts the goroutines required to send RPC requests and listen for
// replies.
func (btcd *BtcdRPCConn) Start() {
done := btcd.closed
responses := make(chan *receivedResponse)
// Maintain a map of JSON IDs to RPCRequests currently being waited on.
go func() {
m := make(map[uint64]*RPCRequest)
for {
select {
case addrequest := <-btcd.addRequest:
rpcrequest := addrequest.Request
m[rpcrequest.request.Id().(uint64)] = rpcrequest
if err := btcd.send(rpcrequest); err != nil {
// Connection lost.
btcd.ws.Close()
close(done)
}
addrequest.ResponseChan <- rpcrequest.response
case recvResponse := <-responses:
rpcrequest, ok := m[recvResponse.id]
if !ok {
log.Warnf("Received unexpected btcd response")
continue
}
delete(m, recvResponse.id)
// If no result var was set, create and send
// send the response unmarshaled by the json
// package.
if rpcrequest.result == nil {
response := &RPCResponse{
Result: recvResponse.reply.Result,
Err: recvResponse.reply.Error,
}
rpcrequest.response <- response
continue
}
// A return var was set, so unmarshal again
// into the var before sending the response.
r := &btcjson.Reply{
Result: rpcrequest.result,
}
json.Unmarshal([]byte(recvResponse.raw), &r)
response := &RPCResponse{
Result: r.Result,
Err: r.Error,
}
rpcrequest.response <- response
case <-done:
for _, request := range m {
response := &RPCResponse{
Err: &ErrBtcdDisconnected,
}
request.response <- response
}
return
}
}
}()
// Listen for replies/notifications from btcd, and decide how to handle them.
go func() {
// Idea: instead of reading btcd messages from just one websocket
// connection, maybe use two so the same connection isn't used
// for both notifications and responses? Should make handling
// must faster as unnecessary unmarshal attempts could be avoided.
for {
var m string
if err := websocket.Message.Receive(btcd.ws, &m); err != nil {
log.Debugf("Cannot receive btcd message: %v", err)
close(done)
return
}
// Try notifications (requests with nil ids) first.
n, err := unmarshalNotification(m)
if err == nil {
// Begin processing the notification.
go processNotification(n, m)
continue
}
// Must be a response.
r, err := unmarshalResponse(m)
if err == nil {
responses <- r
continue
}
// Not sure what was received but it isn't correct.
log.Warnf("Received invalid message from btcd")
}
}()
}
// unmarshalResponse attempts to unmarshal a marshaled JSON-RPC
// response.
func unmarshalResponse(s string) (*receivedResponse, error) {
var r btcjson.Reply
if err := json.Unmarshal([]byte(s), &r); err != nil {
return nil, err
}
// Check for a valid ID.
if r.Id == nil {
return nil, errors.New("id is nil")
}
fid, ok := (*r.Id).(float64)
if !ok {
return nil, errors.New("id is not a number")
}
response := &receivedResponse{
id: uint64(fid),
raw: s,
reply: &r,
}
return response, nil
}
// unmarshalNotification attempts to unmarshal a marshaled JSON-RPC
// notification (Request with a nil or no ID).
func unmarshalNotification(s string) (btcjson.Cmd, error) {
req, err := btcjson.ParseMarshaledCmd([]byte(s))
if err != nil {
return nil, err
}
if req.Id() != nil {
return nil, errors.New("id is non-nil")
}
return req, nil
}
// processNotification checks for a handler for a notification, and sends
func processNotification(n btcjson.Cmd, s string) {
// Message is a btcd notification. Check the method and dispatch
// correct handler, or if no handler, pass up to each wallet.
if ntfnHandler, ok := notificationHandlers[n.Method()]; ok {
log.Debugf("Running notification handler for method %v",
n.Method())
ntfnHandler(n, []byte(s))
} else {
// No handler; send to all wallets.
log.Debugf("Sending notification with method %v to all wallets",
n.Method())
frontendNotificationMaster <- []byte(s)
}
}
type notificationHandler func(btcjson.Cmd, []byte)
var notificationHandlers = map[string]notificationHandler{ var notificationHandlers = map[string]notificationHandler{
btcws.BlockConnectedNtfnMethod: NtfnBlockConnected, btcws.BlockConnectedNtfnMethod: NtfnBlockConnected,
@ -297,7 +41,7 @@ var notificationHandlers = map[string]notificationHandler{
} }
// NtfnProcessedTx handles the btcws.ProcessedTxNtfn notification. // NtfnProcessedTx handles the btcws.ProcessedTxNtfn notification.
func NtfnProcessedTx(n btcjson.Cmd, marshaled []byte) { func NtfnProcessedTx(n btcjson.Cmd) {
ptn, ok := n.(*btcws.ProcessedTxNtfn) ptn, ok := n.(*btcws.ProcessedTxNtfn)
if !ok { if !ok {
log.Errorf("%v handler: unexpected type", n.Method()) log.Errorf("%v handler: unexpected type", n.Method())
@ -332,8 +76,8 @@ func NtfnProcessedTx(n btcjson.Cmd, marshaled []byte) {
log.Warnf("Received rescan result for unknown address %v", ptn.Receiver) log.Warnf("Received rescan result for unknown address %v", ptn.Receiver)
return return
} }
a, err := accountstore.Account(aname) a, err := AcctMgr.Account(aname)
if err == ErrAcctNotExist { if err == ErrNotFound {
log.Errorf("Missing account for rescaned address %v", ptn.Receiver) log.Errorf("Missing account for rescaned address %v", ptn.Receiver)
} }
@ -366,11 +110,8 @@ func NtfnProcessedTx(n btcjson.Cmd, marshaled []byte) {
} }
// Record the tx history. // Record the tx history.
a.TxStore.Lock() a.TxStore.InsertRecvTx(t)
a.TxStore.s.InsertRecvTx(t) AcctMgr.ds.ScheduleTxStoreWrite(a)
a.TxStore.Unlock()
a.ScheduleTxStoreWrite()
// Notify frontends of tx. If the tx is unconfirmed, it is always // Notify frontends of tx. If the tx is unconfirmed, it is always
// notified and the outpoint is marked as notified. If the outpoint // notified and the outpoint is marked as notified. If the outpoint
// has already been notified and is now in a block, a txmined notifiction // has already been notified and is now in a block, a txmined notifiction
@ -402,10 +143,8 @@ func NtfnProcessedTx(n btcjson.Cmd, marshaled []byte) {
u.Out.Index = uint32(ptn.TxOutIndex) u.Out.Index = uint32(ptn.TxOutIndex)
copy(u.AddrHash[:], receiver.ScriptAddress()) copy(u.AddrHash[:], receiver.ScriptAddress())
copy(u.BlockHash[:], blockHash[:]) copy(u.BlockHash[:], blockHash[:])
a.UtxoStore.Lock() a.UtxoStore.Insert(u)
a.UtxoStore.s.Insert(u) AcctMgr.ds.ScheduleUtxoStoreWrite(a)
a.UtxoStore.Unlock()
a.ScheduleUtxoStoreWrite()
// If this notification came from mempool, notify frontends of // If this notification came from mempool, notify frontends of
// the new unconfirmed balance immediately. Otherwise, wait until // the new unconfirmed balance immediately. Otherwise, wait until
@ -431,7 +170,7 @@ func NtfnProcessedTx(n btcjson.Cmd, marshaled []byte) {
// to mark wallet files with a possibly-better earliest block height, // to mark wallet files with a possibly-better earliest block height,
// and will greatly reduce rescan times for wallets created with an // and will greatly reduce rescan times for wallets created with an
// out of sync btcd. // out of sync btcd.
func NtfnBlockConnected(n btcjson.Cmd, marshaled []byte) { func NtfnBlockConnected(n btcjson.Cmd) {
bcn, ok := n.(*btcws.BlockConnectedNtfn) bcn, ok := n.(*btcws.BlockConnectedNtfn)
if !ok { if !ok {
log.Errorf("%v handler: unexpected type", n.Method()) log.Errorf("%v handler: unexpected type", n.Method())
@ -465,16 +204,17 @@ func NtfnBlockConnected(n btcjson.Cmd, marshaled []byte) {
wg.Wait() wg.Wait()
NotifyBalanceSyncerChans.remove <- *hash NotifyBalanceSyncerChans.remove <- *hash
} }
accountstore.BlockNotify(bs) AcctMgr.BlockNotify(bs)
// Pass notification to frontends too. // Pass notification to frontends too.
marshaled, _ := n.MarshalJSON()
frontendNotificationMaster <- marshaled frontendNotificationMaster <- marshaled
} }
// NtfnBlockDisconnected handles btcd notifications resulting from // NtfnBlockDisconnected handles btcd notifications resulting from
// blocks disconnected from the main chain in the event of a chain // blocks disconnected from the main chain in the event of a chain
// switch and notifies frontends of the new blockchain height. // switch and notifies frontends of the new blockchain height.
func NtfnBlockDisconnected(n btcjson.Cmd, marshaled []byte) { func NtfnBlockDisconnected(n btcjson.Cmd) {
bdn, ok := n.(*btcws.BlockDisconnectedNtfn) bdn, ok := n.(*btcws.BlockDisconnectedNtfn)
if !ok { if !ok {
log.Errorf("%v handler: unexpected type", n.Method()) log.Errorf("%v handler: unexpected type", n.Method())
@ -487,15 +227,16 @@ func NtfnBlockDisconnected(n btcjson.Cmd, marshaled []byte) {
} }
// Rollback Utxo and Tx data stores. // Rollback Utxo and Tx data stores.
accountstore.Rollback(bdn.Height, hash) AcctMgr.Rollback(bdn.Height, hash)
// Pass notification to frontends too. // Pass notification to frontends too.
marshaled, _ := n.MarshalJSON()
frontendNotificationMaster <- marshaled frontendNotificationMaster <- marshaled
} }
// NtfnTxMined handles btcd notifications resulting from newly // NtfnTxMined handles btcd notifications resulting from newly
// mined transactions that originated from this wallet. // mined transactions that originated from this wallet.
func NtfnTxMined(n btcjson.Cmd, marshaled []byte) { func NtfnTxMined(n btcjson.Cmd) {
tmn, ok := n.(*btcws.TxMinedNtfn) tmn, ok := n.(*btcws.TxMinedNtfn)
if !ok { if !ok {
log.Errorf("%v handler: unexpected type", n.Method()) log.Errorf("%v handler: unexpected type", n.Method())
@ -513,7 +254,7 @@ func NtfnTxMined(n btcjson.Cmd, marshaled []byte) {
return return
} }
err = accountstore.RecordMinedTx(txid, blockhash, err = AcctMgr.RecordMinedTx(txid, blockhash,
tmn.BlockHeight, tmn.Index, tmn.BlockTime) tmn.BlockHeight, tmn.Index, tmn.BlockTime)
if err != nil { if err != nil {
log.Errorf("%v handler: %v", n.Method(), err) log.Errorf("%v handler: %v", n.Method(), err)
@ -528,7 +269,7 @@ func NtfnTxMined(n btcjson.Cmd, marshaled []byte) {
// NtfnTxSpent handles btcd txspent notifications resulting from a block // NtfnTxSpent handles btcd txspent notifications resulting from a block
// transaction being processed that spents a wallet UTXO. // transaction being processed that spents a wallet UTXO.
func NtfnTxSpent(n btcjson.Cmd, marshaled []byte) { func NtfnTxSpent(n btcjson.Cmd) {
// TODO(jrick): This might actually be useless and maybe it shouldn't // TODO(jrick): This might actually be useless and maybe it shouldn't
// be implemented. // be implemented.
} }

199
rpc.go
View file

@ -14,149 +14,104 @@
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/ */
// This file implements the RPC connection interface and functions to
// communicate with a bitcoin RPC server.
package main package main
import ( import (
"github.com/conformal/btcjson" "github.com/conformal/btcjson"
"github.com/conformal/btcwire"
"github.com/conformal/btcws"
) )
// RPCRequest is a type responsible for handling RPC requests and providing // RPCResponse is an interface type covering both server
// a method to access the response. // (frontend <-> btcwallet) and client (btcwallet <-> btcd) responses.
type RPCRequest struct { type RPCResponse interface {
request btcjson.Cmd Result() interface{}
result interface{} Error() *btcjson.Error
response chan *RPCResponse
} }
// NewRPCRequest creates a new RPCRequest from a btcjson.Cmd. request may be // ClientRequest is a type holding a bitcoin client's request and
// nil to create a new var for the result (with types determined by the // a channel to send the response.
// unmarshaling rules described in the json package), or set to a var with type ClientRequest struct {
// an expected type (i.e. *btcjson.BlockResult) to directly unmarshal the ws bool
// response's result into a convenient type. request btcjson.Cmd
func NewRPCRequest(request btcjson.Cmd, result interface{}) *RPCRequest { response chan RPCResponse
return &RPCRequest{ }
// NewClientRequest creates a new ClientRequest from a btcjson.Cmd.
func NewClientRequest(request btcjson.Cmd, ws bool) *ClientRequest {
return &ClientRequest{
ws: ws,
request: request, request: request,
result: result, response: make(chan RPCResponse),
response: make(chan *RPCResponse),
} }
} }
// RPCResponse holds a response's result and error returned from sending a // Handle sends a client request to the RPC gateway for processing,
// RPCRequest. // and returns the result when handling is finished.
type RPCResponse struct { func (r *ClientRequest) Handle() (interface{}, *btcjson.Error) {
clientRequests <- r
resp := <-r.response
return resp.Result(), resp.Error()
}
// ClientResponse holds a result and error returned from handling a
// client's request.
type ClientResponse struct {
result interface{}
err *btcjson.Error
}
// Result returns the result of a response to a client.
func (r *ClientResponse) Result() interface{} {
return r.result
}
// Error returns the error of a response to a client, or nil if
// there is no error.
func (r *ClientResponse) Error() *btcjson.Error {
return r.err
}
// ServerRequest is a type responsible for handling requests to a bitcoin
// server and providing a method to access the response.
type ServerRequest struct {
request btcjson.Cmd
result interface{}
response chan RPCResponse
}
// NewServerRequest creates a new ServerRequest from a btcjson.Cmd. request
// may be nil to create a new var for the result (with types determined by
// the unmarshaling rules described in the json package), or set to a var
// with an expected type (i.e. *btcjson.BlockResult) to directly unmarshal
// the response's result into a convenient type.
func NewServerRequest(request btcjson.Cmd, result interface{}) *ServerRequest {
return &ServerRequest{
request: request,
result: result,
response: make(chan RPCResponse, 1),
}
}
// ServerResponse holds a response's result and error returned from sending a
// ServerRequest.
type ServerResponse struct {
// Result will be set to a concrete type (i.e. *btcjson.BlockResult) // Result will be set to a concrete type (i.e. *btcjson.BlockResult)
// and may be type asserted to that type if a non-nil result was used // and may be type asserted to that type if a non-nil result was used
// to create the originating RPCRequest. Otherwise, Result will be // to create the originating ServerRequest. Otherwise, Result will be
// set to new memory allocated by json.Unmarshal, and the type rules // set to new memory allocated by json.Unmarshal, and the type rules
// for unmarshaling described in the json package should be followed // for unmarshaling described in the json package should be followed
// when type asserting Result. // when type asserting Result.
Result interface{} result interface{}
// Err points to an unmarshaled error, or nil if result is valid. // Err points to an unmarshaled error, or nil if result is valid.
Err *btcjson.Error err *btcjson.Error
} }
// RPCConn is an interface representing a client connection to a bitcoin RPC // Result returns the result of a server's RPC response.
// server. func (r *ServerResponse) Result() interface{} {
type RPCConn interface { return r.result
// SendRequest sends a bitcoin RPC request, returning a channel to
// read the reply. A channel is used so both synchronous and
// asynchronous RPC can be supported.
SendRequest(request *RPCRequest) chan *RPCResponse
} }
// GetBestBlockResult holds the result of a getbestblock response. // Result returns the error of a server's RPC response.
// func (r *ServerResponse) Error() *btcjson.Error {
// TODO(jrick): shove this in btcws. return r.err
type GetBestBlockResult struct {
Hash string `json:"hash"`
Height int32 `json:"height"`
}
// GetBestBlock gets both the block height and hash of the best block
// in the main chain.
func GetBestBlock(rpc RPCConn) (*GetBestBlockResult, *btcjson.Error) {
cmd := btcws.NewGetBestBlockCmd(<-NewJSONID)
request := NewRPCRequest(cmd, new(GetBestBlockResult))
response := <-rpc.SendRequest(request)
if response.Err != nil {
return nil, response.Err
}
return response.Result.(*GetBestBlockResult), nil
}
// GetBlock requests details about a block with the given hash.
func GetBlock(rpc RPCConn, blockHash string) (*btcjson.BlockResult, *btcjson.Error) {
// NewGetBlockCmd cannot fail with no optargs, so omit the check.
cmd, _ := btcjson.NewGetBlockCmd(<-NewJSONID, blockHash)
request := NewRPCRequest(cmd, new(btcjson.BlockResult))
response := <-rpc.SendRequest(request)
if response.Err != nil {
return nil, response.Err
}
return response.Result.(*btcjson.BlockResult), nil
}
// GetCurrentNet requests the network a bitcoin RPC server is running on.
func GetCurrentNet(rpc RPCConn) (btcwire.BitcoinNet, *btcjson.Error) {
cmd := btcws.NewGetCurrentNetCmd(<-NewJSONID)
request := NewRPCRequest(cmd, nil)
response := <-rpc.SendRequest(request)
if response.Err != nil {
return 0, response.Err
}
return btcwire.BitcoinNet(uint32(response.Result.(float64))), nil
}
// NotifyBlocks requests blockconnected and blockdisconnected notifications.
func NotifyBlocks(rpc RPCConn) *btcjson.Error {
cmd := btcws.NewNotifyBlocksCmd(<-NewJSONID)
request := NewRPCRequest(cmd, nil)
response := <-rpc.SendRequest(request)
return response.Err
}
// NotifyNewTXs requests notifications for new transactions that spend
// to any of the addresses in addrs.
func NotifyNewTXs(rpc RPCConn, addrs []string) *btcjson.Error {
cmd := btcws.NewNotifyNewTXsCmd(<-NewJSONID, addrs)
request := NewRPCRequest(cmd, nil)
response := <-rpc.SendRequest(request)
return response.Err
}
// NotifySpent requests notifications for when a transaction is processed which
// spends op.
func NotifySpent(rpc RPCConn, op *btcwire.OutPoint) *btcjson.Error {
cmd := btcws.NewNotifySpentCmd(<-NewJSONID, op)
request := NewRPCRequest(cmd, nil)
response := <-rpc.SendRequest(request)
return response.Err
}
// Rescan requests a blockchain rescan for transactions to any number of
// addresses and notifications to inform wallet about such transactions.
func Rescan(rpc RPCConn, beginBlock int32, addrs map[string]struct{}) *btcjson.Error {
// NewRescanCmd cannot fail with no optargs, so omit the check.
cmd, _ := btcws.NewRescanCmd(<-NewJSONID, beginBlock, addrs)
request := NewRPCRequest(cmd, nil)
response := <-rpc.SendRequest(request)
return response.Err
}
// SendRawTransaction sends a hex-encoded transaction for relay.
func SendRawTransaction(rpc RPCConn, hextx string) (txid string, error *btcjson.Error) {
// NewSendRawTransactionCmd cannot fail, so omit the check.
cmd, _ := btcjson.NewSendRawTransactionCmd(<-NewJSONID, hextx)
request := NewRPCRequest(cmd, new(string))
response := <-rpc.SendRequest(request)
if response.Err != nil {
return "", response.Err
}
return *response.Result.(*string), nil
} }

363
rpcclient.go Normal file
View file

@ -0,0 +1,363 @@
/*
* Copyright (c) 2013, 2014 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.
*/
// This file implements the websocket client connection to a bitcoin RPC
// server.
package main
import (
"code.google.com/p/go.net/websocket"
"encoding/json"
"errors"
"github.com/conformal/btcjson"
"github.com/conformal/btcwire"
"github.com/conformal/btcws"
)
// ServerConn is an interface representing a client connection to a bitcoin RPC
// server.
type ServerConn interface {
// SendRequest sends a bitcoin RPC request, returning a channel to
// read the reply. A channel is used so both synchronous and
// asynchronous RPC can be supported.
SendRequest(request *ServerRequest) chan RPCResponse
}
// ErrBtcdDisconnected describes an error where an operation cannot
// successfully complete due to btcwallet not being connected to
// btcd.
var ErrBtcdDisconnected = btcjson.Error{
Code: -1,
Message: "btcd disconnected",
}
// BtcdRPCConn is a type managing a client connection to a btcd RPC server
// over websockets.
type BtcdRPCConn struct {
ws *websocket.Conn
addRequest chan *AddRPCRequest
closed chan struct{}
}
// Ensure that BtcdRPCConn can be used as an RPCConn.
var _ ServerConn = &BtcdRPCConn{}
// NewBtcdRPCConn creates a new RPC connection from a btcd websocket
// connection to btcd.
func NewBtcdRPCConn(ws *websocket.Conn) *BtcdRPCConn {
conn := &BtcdRPCConn{
ws: ws,
addRequest: make(chan *AddRPCRequest),
closed: make(chan struct{}),
}
return conn
}
// SendRequest sends an RPC request and returns a channel to read the response's
// result and error. Part of the RPCConn interface.
func (btcd *BtcdRPCConn) SendRequest(request *ServerRequest) chan RPCResponse {
select {
case <-btcd.closed:
// The connection has closed, so instead of adding and sending
// a request, return a channel that just replies with the
// error for a disconnected btcd.
responseChan := make(chan RPCResponse, 1)
response := &ServerResponse{
err: &ErrBtcdDisconnected,
}
responseChan <- response
return responseChan
default:
addRequest := &AddRPCRequest{
Request: request,
ResponseChan: make(chan chan RPCResponse, 1),
}
btcd.addRequest <- addRequest
return <-addRequest.ResponseChan
}
}
// Connected returns whether the connection remains established to the RPC
// server.
//
// This function probably should be removed, as any checks for confirming
// the connection are no longer valid after the check and may result in
// races.
func (btcd *BtcdRPCConn) Connected() bool {
select {
case <-btcd.closed:
return false
default:
return true
}
}
// AddRPCRequest is used to add an RPCRequest to the pool of requests
// being manaaged by a btcd RPC connection.
type AddRPCRequest struct {
Request *ServerRequest
ResponseChan chan chan RPCResponse
}
// send performs the actual send of the marshaled request over the btcd
// websocket connection.
func (btcd *BtcdRPCConn) send(rpcrequest *ServerRequest) error {
// btcjson.Cmds define their own MarshalJSON which returns an error
// to satisify the json.Marshaler interface, but will never error.
mrequest, _ := rpcrequest.request.MarshalJSON()
return websocket.Message.Send(btcd.ws, mrequest)
}
type receivedResponse struct {
id uint64
raw string
reply *btcjson.Reply
}
// Start starts the goroutines required to send RPC requests and listen for
// replies.
func (btcd *BtcdRPCConn) Start() {
done := btcd.closed
responses := make(chan *receivedResponse)
// Maintain a map of JSON IDs to RPCRequests currently being waited on.
go func() {
m := make(map[uint64]*ServerRequest)
for {
select {
case addrequest := <-btcd.addRequest:
rpcrequest := addrequest.Request
m[rpcrequest.request.Id().(uint64)] = rpcrequest
if err := btcd.send(rpcrequest); err != nil {
// Connection lost.
btcd.ws.Close()
close(done)
}
addrequest.ResponseChan <- rpcrequest.response
case recvResponse := <-responses:
rpcrequest, ok := m[recvResponse.id]
if !ok {
log.Warnf("Received unexpected btcd response")
continue
}
delete(m, recvResponse.id)
// If no result var was set, create and send
// send the response unmarshaled by the json
// package.
if rpcrequest.result == nil {
response := &ServerResponse{
result: recvResponse.reply.Result,
err: recvResponse.reply.Error,
}
rpcrequest.response <- response
continue
}
// A return var was set, so unmarshal again
// into the var before sending the response.
r := &btcjson.Reply{
Result: rpcrequest.result,
}
json.Unmarshal([]byte(recvResponse.raw), &r)
response := &ServerResponse{
result: r.Result,
err: r.Error,
}
rpcrequest.response <- response
case <-done:
for _, request := range m {
response := &ServerResponse{
err: &ErrBtcdDisconnected,
}
request.response <- response
}
return
}
}
}()
// Listen for replies/notifications from btcd, and decide how to handle them.
go func() {
// Idea: instead of reading btcd messages from just one websocket
// connection, maybe use two so the same connection isn't used
// for both notifications and responses? Should make handling
// must faster as unnecessary unmarshal attempts could be avoided.
for {
var m string
if err := websocket.Message.Receive(btcd.ws, &m); err != nil {
log.Debugf("Cannot receive btcd message: %v", err)
close(done)
return
}
// Try notifications (requests with nil ids) first.
n, err := unmarshalNotification(m)
if err == nil {
svrNtfns <- n
continue
}
// Must be a response.
r, err := unmarshalResponse(m)
if err == nil {
responses <- r
continue
}
// Not sure what was received but it isn't correct.
log.Warnf("Received invalid message from btcd")
}
}()
}
// unmarshalResponse attempts to unmarshal a marshaled JSON-RPC
// response.
func unmarshalResponse(s string) (*receivedResponse, error) {
var r btcjson.Reply
if err := json.Unmarshal([]byte(s), &r); err != nil {
return nil, err
}
// Check for a valid ID.
if r.Id == nil {
return nil, errors.New("id is nil")
}
fid, ok := (*r.Id).(float64)
if !ok {
return nil, errors.New("id is not a number")
}
response := &receivedResponse{
id: uint64(fid),
raw: s,
reply: &r,
}
return response, nil
}
// unmarshalNotification attempts to unmarshal a marshaled JSON-RPC
// notification (Request with a nil or no ID).
func unmarshalNotification(s string) (btcjson.Cmd, error) {
req, err := btcjson.ParseMarshaledCmd([]byte(s))
if err != nil {
return nil, err
}
if req.Id() != nil {
return nil, errors.New("id is non-nil")
}
return req, nil
}
// GetBestBlockResult holds the result of a getbestblock response.
//
// TODO(jrick): shove this in btcws.
type GetBestBlockResult struct {
Hash string `json:"hash"`
Height int32 `json:"height"`
}
// GetBestBlock gets both the block height and hash of the best block
// in the main chain.
func GetBestBlock(rpc ServerConn) (*GetBestBlockResult, *btcjson.Error) {
cmd := btcws.NewGetBestBlockCmd(<-NewJSONID)
request := NewServerRequest(cmd, new(GetBestBlockResult))
response := <-rpc.SendRequest(request)
if response.Error() != nil {
return nil, response.Error()
}
return response.Result().(*GetBestBlockResult), nil
}
// GetBlock requests details about a block with the given hash.
func GetBlock(rpc ServerConn, blockHash string) (*btcjson.BlockResult, *btcjson.Error) {
// NewGetBlockCmd cannot fail with no optargs, so omit the check.
cmd, _ := btcjson.NewGetBlockCmd(<-NewJSONID, blockHash)
request := NewServerRequest(cmd, new(btcjson.BlockResult))
response := <-rpc.SendRequest(request)
if response.Error() != nil {
return nil, response.Error()
}
return response.Result().(*btcjson.BlockResult), nil
}
// GetCurrentNet requests the network a bitcoin RPC server is running on.
func GetCurrentNet(rpc ServerConn) (btcwire.BitcoinNet, *btcjson.Error) {
cmd := btcws.NewGetCurrentNetCmd(<-NewJSONID)
request := NewServerRequest(cmd, nil)
response := <-rpc.SendRequest(request)
if response.Error() != nil {
return 0, response.Error()
}
return btcwire.BitcoinNet(uint32(response.Result().(float64))), nil
}
// NotifyBlocks requests blockconnected and blockdisconnected notifications.
func NotifyBlocks(rpc ServerConn) *btcjson.Error {
cmd := btcws.NewNotifyBlocksCmd(<-NewJSONID)
request := NewServerRequest(cmd, nil)
response := <-rpc.SendRequest(request)
return response.Error()
}
// NotifyNewTXs requests notifications for new transactions that spend
// to any of the addresses in addrs.
func NotifyNewTXs(rpc ServerConn, addrs []string) *btcjson.Error {
cmd := btcws.NewNotifyNewTXsCmd(<-NewJSONID, addrs)
request := NewServerRequest(cmd, nil)
response := <-rpc.SendRequest(request)
return response.Error()
}
// NotifySpent requests notifications for when a transaction is processed which
// spends op.
func NotifySpent(rpc ServerConn, op *btcwire.OutPoint) *btcjson.Error {
cmd := btcws.NewNotifySpentCmd(<-NewJSONID, op)
request := NewServerRequest(cmd, nil)
response := <-rpc.SendRequest(request)
return response.Error()
}
// Rescan requests a blockchain rescan for transactions to any number of
// addresses and notifications to inform wallet about such transactions.
func Rescan(rpc ServerConn, beginBlock int32, addrs map[string]struct{}) *btcjson.Error {
// NewRescanCmd cannot fail with no optargs, so omit the check.
cmd, _ := btcws.NewRescanCmd(<-NewJSONID, beginBlock, addrs)
request := NewServerRequest(cmd, nil)
response := <-rpc.SendRequest(request)
return response.Error()
}
// SendRawTransaction sends a hex-encoded transaction for relay.
func SendRawTransaction(rpc ServerConn, hextx string) (txid string, error *btcjson.Error) {
// NewSendRawTransactionCmd cannot fail, so omit the check.
cmd, _ := btcjson.NewSendRawTransactionCmd(<-NewJSONID, hextx)
request := NewServerRequest(cmd, new(string))
response := <-rpc.SendRequest(request)
if response.Error() != nil {
return "", response.Error()
}
return *response.Result().(*string), nil
}

View file

@ -97,57 +97,116 @@ var wsHandlers = map[string]cmdHandler{
"walletislocked": WalletIsLocked, "walletislocked": WalletIsLocked,
} }
// ProcessFrontendRequest checks the requests sent from a frontend. If the // Channels to control RPCGateway
// request method is one that must be handled by btcwallet, the var (
// request is processed here. Otherwise, the request is sent to btcd // Incoming requests from frontends
// and btcd's reply is routed back to the frontend. clientRequests = make(chan *ClientRequest)
func ProcessFrontendRequest(msg []byte, ws bool) *btcjson.Reply {
// Parse marshaled command.
cmd, err := btcjson.ParseMarshaledCmd(msg)
if err != nil || cmd.Id() == nil {
// Invalid JSON-RPC request.
response := &btcjson.Reply{
Error: &btcjson.ErrInvalidRequest,
}
return response
}
id := cmd.Id() // Incoming notifications from a bitcoin server (btcd)
var result interface{} svrNtfns = make(chan btcjson.Cmd)
var jsonErr *btcjson.Error )
// Check for a handler to reply to cmd. If none exist, defer handlng // ErrServerBusy is a custom JSON-RPC error for when a client's request
// to btcd. // could not be added to the server request queue for handling.
if f, ok := rpcHandlers[cmd.Method()]; ok { var ErrServerBusy = btcjson.Error{
result, jsonErr = f(cmd) Code: -32000,
} else if f, ok := wsHandlers[cmd.Method()]; ws && ok { Message: "Server busy",
result, jsonErr = f(cmd)
} else {
// btcwallet does not have a handler for the command, so ask
// btcd.
result, jsonErr = DeferToBtcd(cmd)
}
// Create and return response.
response := &btcjson.Reply{
Id: &id,
Result: result,
Error: jsonErr,
}
return response
} }
// DeferToBtcd sends a marshaled JSON-RPC request to btcd and returns // RPCGateway is the common entry point for all client RPC requests and
// the reply. // server notifications. If a request needs to be handled by btcwallet,
func DeferToBtcd(cmd btcjson.Cmd) (interface{}, *btcjson.Error) { // it is sent to WalletRequestProcessor's request queue, or dropped if the
// Update cmd with a new ID so replies can be handled without frontend // queue is full. If a request is unhandled, it is recreated with a new
// IDs clashing with requests originating in btcwallet. The original // JSON-RPC id and sent to btcd for handling. Notifications are also queued
// request ID is always used in the frontend's response. // if they cannot be immediately handled, but are never dropped (queue may
cmd.SetId(<-NewJSONID) // grow infinitely large).
func RPCGateway() {
var ntfnQueue []btcjson.Cmd
unreadChan := make(chan btcjson.Cmd)
request := NewRPCRequest(cmd, nil) for {
response := <-CurrentRPCConn().SendRequest(request) var ntfnOut chan btcjson.Cmd
return response.Result, response.Err var oldestNtfn btcjson.Cmd
if len(ntfnQueue) > 0 {
ntfnOut = handleNtfn
oldestNtfn = ntfnQueue[0]
} else {
ntfnOut = unreadChan
}
select {
case r := <-clientRequests:
// Check whether to handle request or send to btcd.
_, std := rpcHandlers[r.request.Method()]
_, ext := wsHandlers[r.request.Method()]
if std || ext {
select {
case requestQueue <- r:
default:
// Server busy with too many requests.
resp := ClientResponse{
err: &ErrServerBusy,
}
r.response <- &resp
}
} else {
r.request.SetId(<-NewJSONID)
request := &ServerRequest{
request: r.request,
result: nil,
response: r.response,
}
CurrentServerConn().SendRequest(request)
}
case n := <-svrNtfns:
ntfnQueue = append(ntfnQueue, n)
case ntfnOut <- oldestNtfn:
ntfnQueue = ntfnQueue[1:]
}
}
}
// Channels to control WalletRequestProcessor
var (
requestQueue = make(chan *ClientRequest, 100)
handleNtfn = make(chan btcjson.Cmd)
)
// WalletRequestProcessor processes client requests and btcd notifications.
// Notifications are preferred over client requests.
func WalletRequestProcessor() {
for {
select {
case r := <-requestQueue:
var result interface{}
var jsonErr *btcjson.Error
if f, ok := rpcHandlers[r.request.Method()]; ok {
AcctMgr.Grab()
result, jsonErr = f(r.request)
AcctMgr.Release()
} else if f, ok := wsHandlers[r.request.Method()]; r.ws && ok {
AcctMgr.Grab()
result, jsonErr = f(r.request)
AcctMgr.Release()
} else {
result, jsonErr = Unimplemented(r.request)
}
resp := &ClientResponse{
result: result,
err: jsonErr,
}
r.response <- resp
case n := <-handleNtfn:
if f, ok := notificationHandlers[n.Method()]; ok {
AcctMgr.Grab()
f(n)
AcctMgr.Release()
}
}
}
} }
// Unimplemented handles an unimplemented RPC request with the // Unimplemented handles an unimplemented RPC request with the
@ -181,7 +240,7 @@ func DumpPrivKey(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
return nil, &btcjson.ErrInvalidAddressOrKey return nil, &btcjson.ErrInvalidAddressOrKey
} }
switch key, err := accountstore.DumpWIFPrivateKey(addr); err { switch key, err := AcctMgr.DumpWIFPrivateKey(addr); err {
case nil: case nil:
// Key was found. // Key was found.
return key, nil return key, nil
@ -210,7 +269,7 @@ func DumpWallet(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
return nil, &btcjson.ErrInternal return nil, &btcjson.ErrInternal
} }
switch keys, err := accountstore.DumpKeys(); err { switch keys, err := AcctMgr.DumpKeys(); err {
case nil: case nil:
// Reply with sorted WIF encoded private keys // Reply with sorted WIF encoded private keys
return keys, nil return keys, nil
@ -238,12 +297,12 @@ func ExportWatchingWallet(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
return nil, &btcjson.ErrInternal return nil, &btcjson.ErrInternal
} }
a, err := accountstore.Account(cmd.Account) a, err := AcctMgr.Account(cmd.Account)
switch err { switch err {
case nil: case nil:
break break
case ErrAcctNotExist: case ErrNotFound:
return nil, &btcjson.ErrWalletInvalidAccountName return nil, &btcjson.ErrWalletInvalidAccountName
default: // all other non-nil errors default: // all other non-nil errors
@ -299,12 +358,12 @@ func GetAddressesByAccount(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
return nil, &btcjson.ErrInternal return nil, &btcjson.ErrInternal
} }
switch a, err := accountstore.Account(cmd.Account); err { switch a, err := AcctMgr.Account(cmd.Account); err {
case nil: case nil:
// Return sorted active payment addresses. // Return sorted active payment addresses.
return a.SortedActivePaymentAddresses(), nil return a.SortedActivePaymentAddresses(), nil
case ErrAcctNotExist: case ErrNotFound:
return nil, &btcjson.ErrWalletInvalidAccountName return nil, &btcjson.ErrWalletInvalidAccountName
default: // all other non-nil errors default: // all other non-nil errors
@ -326,7 +385,7 @@ func GetBalance(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
return nil, &btcjson.ErrInternal return nil, &btcjson.ErrInternal
} }
balance, err := accountstore.CalculateBalance(cmd.Account, cmd.MinConf) balance, err := AcctMgr.CalculateBalance(cmd.Account, cmd.MinConf)
if err != nil { if err != nil {
return nil, &btcjson.ErrWalletInvalidAccountName return nil, &btcjson.ErrWalletInvalidAccountName
} }
@ -339,19 +398,18 @@ func GetBalance(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
// information about the current state of btcwallet. // information about the current state of btcwallet.
// exist. // exist.
func GetInfo(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { func GetInfo(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
// Call down to btcd for all of the information in this command known // Call down to btcd for all of the information in this command known
// by them. This call can not realistically ever fail. // by them. This call can not realistically ever fail.
gicmd, _ := btcjson.NewGetInfoCmd(<-NewJSONID) gicmd, _ := btcjson.NewGetInfoCmd(<-NewJSONID)
response := <-(CurrentRPCConn().SendRequest(NewRPCRequest(gicmd, req := NewServerRequest(gicmd, make(map[string]interface{}))
make(map[string]interface{})))) response := <-CurrentServerConn().SendRequest(req)
if response.Err != nil { if response.Error() != nil {
return nil, response.Err return nil, response.Error()
} }
ret := response.Result.(map[string]interface{}) ret := response.Result().(map[string]interface{})
balance := float64(0.0) balance := float64(0.0)
accounts := accountstore.ListAccounts(1) accounts := AcctMgr.ListAccounts(1)
for _, v := range accounts { for _, v := range accounts {
balance += v balance += v
} }
@ -429,12 +487,12 @@ func GetAccountAddress(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
} }
// Lookup account for this request. // Lookup account for this request.
a, err := accountstore.Account(cmd.Account) a, err := AcctMgr.Account(cmd.Account)
switch err { switch err {
case nil: case nil:
break break
case ErrAcctNotExist: case ErrNotFound:
return nil, &btcjson.ErrWalletInvalidAccountName return nil, &btcjson.ErrWalletInvalidAccountName
default: // all other non-nil errors default: // all other non-nil errors
@ -494,7 +552,7 @@ func GetAddressBalance(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
// Get the account which holds the address in the request. // Get the account which holds the address in the request.
// This should not fail, so if it does, return an internal // This should not fail, so if it does, return an internal
// error to the frontend. // error to the frontend.
a, err := accountstore.Account(aname) a, err := AcctMgr.Account(aname)
if err != nil { if err != nil {
return nil, &btcjson.ErrInternal return nil, &btcjson.ErrInternal
} }
@ -513,12 +571,12 @@ func GetUnconfirmedBalance(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
} }
// Get the account included in the request. // Get the account included in the request.
a, err := accountstore.Account(cmd.Account) a, err := AcctMgr.Account(cmd.Account)
switch err { switch err {
case nil: case nil:
break break
case ErrAcctNotExist: case ErrNotFound:
return nil, &btcjson.ErrWalletInvalidAccountName return nil, &btcjson.ErrWalletInvalidAccountName
default: default:
@ -545,12 +603,12 @@ func ImportPrivKey(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
// Get the acount included in the request. Yes, Label is the // Get the acount included in the request. Yes, Label is the
// account name... // account name...
a, err := accountstore.Account(cmd.Label) a, err := AcctMgr.Account(cmd.Label)
switch err { switch err {
case nil: case nil:
break break
case ErrAcctNotExist: case ErrNotFound:
return nil, &btcjson.ErrWalletInvalidAccountName return nil, &btcjson.ErrWalletInvalidAccountName
default: default:
@ -561,8 +619,14 @@ func ImportPrivKey(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
return nil, &e return nil, &e
} }
pk, net, compressed, err := btcutil.DecodePrivateKey(cmd.PrivKey)
if err != nil || net != a.Net() {
return nil, &btcjson.ErrInvalidAddressOrKey
}
// Import the private key, handling any errors. // Import the private key, handling any errors.
switch err := a.ImportPrivKey(cmd.PrivKey, cmd.Rescan); err { bs := &wallet.BlockStamp{}
switch _, err := a.ImportPrivateKey(pk, compressed, bs); err {
case nil: case nil:
// If the import was successful, reply with nil. // If the import was successful, reply with nil.
return nil, nil return nil, nil
@ -596,7 +660,7 @@ func KeypoolRefill(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
// (map[string]interface{}) of all accounts and their balances, instead of // (map[string]interface{}) of all accounts and their balances, instead of
// separate notifications for each account. // separate notifications for each account.
func NotifyBalances(frontend chan []byte) { func NotifyBalances(frontend chan []byte) {
accountstore.NotifyBalances(frontend) AcctMgr.NotifyBalances(frontend)
} }
// GetNewAddress handlesa getnewaddress request by returning a new // GetNewAddress handlesa getnewaddress request by returning a new
@ -609,20 +673,16 @@ func GetNewAddress(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
return nil, &btcjson.ErrInternal return nil, &btcjson.ErrInternal
} }
a, err := accountstore.Account(cmd.Account) a, err := AcctMgr.Account(cmd.Account)
switch err { switch err {
case nil: case nil:
break break
case ErrAcctNotExist: case ErrNotFound:
return nil, &btcjson.ErrWalletInvalidAccountName return nil, &btcjson.ErrWalletInvalidAccountName
case ErrBtcdDisconnected: case ErrBtcdDisconnected:
e := btcjson.Error{ return nil, &ErrBtcdDisconnected
Code: btcjson.ErrInternal.Code,
Message: "btcd disconnected",
}
return nil, &e
default: // all other non-nil errors default: // all other non-nil errors
e := btcjson.Error{ e := btcjson.Error{
@ -664,7 +724,7 @@ func ListAccounts(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
} }
// Return the map. This will be marshaled into a JSON object. // Return the map. This will be marshaled into a JSON object.
return accountstore.ListAccounts(cmd.MinConf), nil return AcctMgr.ListAccounts(cmd.MinConf), nil
} }
// ListSinceBlock handles a listsinceblock request by returning an array of maps // ListSinceBlock handles a listsinceblock request by returning an array of maps
@ -677,7 +737,7 @@ func ListSinceBlock(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
height := int32(-1) height := int32(-1)
if cmd.BlockHash != "" { if cmd.BlockHash != "" {
br, err := GetBlock(CurrentRPCConn(), cmd.BlockHash) br, err := GetBlock(CurrentServerConn(), cmd.BlockHash)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -704,9 +764,10 @@ func ListSinceBlock(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
} }
} }
bhChan := CurrentRPCConn().SendRequest(NewRPCRequest(gbh, new(string))) req := NewServerRequest(gbh, new(string))
bhChan := CurrentServerConn().SendRequest(req)
txInfoList, err := accountstore.ListSinceBlock(height, bs.Height, txInfoList, err := AcctMgr.ListSinceBlock(height, bs.Height,
cmd.TargetConfirmations) cmd.TargetConfirmations)
if err != nil { if err != nil {
return nil, &btcjson.Error{ return nil, &btcjson.Error{
@ -717,11 +778,11 @@ func ListSinceBlock(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
// Done with work, get the response. // Done with work, get the response.
response := <-bhChan response := <-bhChan
if response.Err != nil { if response.Error() != nil {
return nil, response.Err return nil, response.Error()
} }
hash := response.Result.(*string) hash := response.Result().(*string)
res := make(map[string]interface{}) res := make(map[string]interface{})
res["transactions"] = txInfoList res["transactions"] = txInfoList
@ -739,12 +800,12 @@ func ListTransactions(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
return nil, &btcjson.ErrInternal return nil, &btcjson.ErrInternal
} }
a, err := accountstore.Account(cmd.Account) a, err := AcctMgr.Account(cmd.Account)
switch err { switch err {
case nil: case nil:
break break
case ErrAcctNotExist: case ErrNotFound:
return nil, &btcjson.ErrWalletInvalidAccountName return nil, &btcjson.ErrWalletInvalidAccountName
default: // all other non-nil errors default: // all other non-nil errors
@ -788,12 +849,12 @@ func ListAddressTransactions(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
return nil, &btcjson.ErrInternal return nil, &btcjson.ErrInternal
} }
a, err := accountstore.Account(cmd.Account) a, err := AcctMgr.Account(cmd.Account)
switch err { switch err {
case nil: case nil:
break break
case ErrAcctNotExist: case ErrNotFound:
return nil, &btcjson.ErrWalletInvalidAccountName return nil, &btcjson.ErrWalletInvalidAccountName
default: // all other non-nil errors default: // all other non-nil errors
@ -840,12 +901,12 @@ func ListAllTransactions(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
return nil, &btcjson.ErrInternal return nil, &btcjson.ErrInternal
} }
a, err := accountstore.Account(cmd.Account) a, err := AcctMgr.Account(cmd.Account)
switch err { switch err {
case nil: case nil:
break break
case ErrAcctNotExist: case ErrNotFound:
return nil, &btcjson.ErrWalletInvalidAccountName return nil, &btcjson.ErrWalletInvalidAccountName
default: // all other non-nil errors default: // all other non-nil errors
@ -906,7 +967,7 @@ func SendFrom(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
} }
// Check that the account specified in the request exists. // Check that the account specified in the request exists.
a, err := accountstore.Account(cmd.FromAccount) a, err := AcctMgr.Account(cmd.FromAccount)
if err != nil { if err != nil {
return nil, &btcjson.ErrWalletInvalidAccountName return nil, &btcjson.ErrWalletInvalidAccountName
} }
@ -945,8 +1006,8 @@ func SendFrom(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
// If a change address was added, sync wallet to disk and request // If a change address was added, sync wallet to disk and request
// transaction notifications to the change address. // transaction notifications to the change address.
if createdTx.changeAddr != nil { if createdTx.changeAddr != nil {
a.ScheduleWalletWrite() AcctMgr.ds.ScheduleWalletWrite(a)
if err := a.WriteScheduledToDisk(); err != nil { if err := AcctMgr.ds.FlushAccount(a); err != nil {
e := btcjson.Error{ e := btcjson.Error{
Code: btcjson.ErrWallet.Code, Code: btcjson.ErrWallet.Code,
Message: "Cannot write account: " + err.Error(), Message: "Cannot write account: " + err.Error(),
@ -959,14 +1020,13 @@ func SendFrom(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
hextx := hex.EncodeToString(createdTx.rawTx) hextx := hex.EncodeToString(createdTx.rawTx)
// NewSendRawTransactionCmd will never fail so don't check error. // NewSendRawTransactionCmd will never fail so don't check error.
sendtx, _ := btcjson.NewSendRawTransactionCmd(<-NewJSONID, hextx) sendtx, _ := btcjson.NewSendRawTransactionCmd(<-NewJSONID, hextx)
var txid string request := NewServerRequest(sendtx, new(string))
request := NewRPCRequest(sendtx, txid) response := <-CurrentServerConn().SendRequest(request)
response := <-CurrentRPCConn().SendRequest(request) txid := *response.Result().(*string)
txid = response.Result.(string)
if response.Err != nil { if response.Error() != nil {
SendTxHistSyncChans.remove <- createdTx.txid SendTxHistSyncChans.remove <- createdTx.txid
return nil, response.Err return nil, response.Error()
} }
return handleSendRawTxReply(cmd, txid, a, createdTx) return handleSendRawTxReply(cmd, txid, a, createdTx)
@ -994,7 +1054,7 @@ func SendMany(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
} }
// Check that the account specified in the request exists. // Check that the account specified in the request exists.
a, err := accountstore.Account(cmd.FromAccount) a, err := AcctMgr.Account(cmd.FromAccount)
if err != nil { if err != nil {
return nil, &btcjson.ErrWalletInvalidAccountName return nil, &btcjson.ErrWalletInvalidAccountName
} }
@ -1028,8 +1088,8 @@ func SendMany(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
// If a change address was added, sync wallet to disk and request // If a change address was added, sync wallet to disk and request
// transaction notifications to the change address. // transaction notifications to the change address.
if createdTx.changeAddr != nil { if createdTx.changeAddr != nil {
a.ScheduleWalletWrite() AcctMgr.ds.ScheduleWalletWrite(a)
if err := a.WriteScheduledToDisk(); err != nil { if err := AcctMgr.ds.FlushAccount(a); err != nil {
e := btcjson.Error{ e := btcjson.Error{
Code: btcjson.ErrWallet.Code, Code: btcjson.ErrWallet.Code,
Message: "Cannot write account: " + err.Error(), Message: "Cannot write account: " + err.Error(),
@ -1042,14 +1102,13 @@ func SendMany(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
hextx := hex.EncodeToString(createdTx.rawTx) hextx := hex.EncodeToString(createdTx.rawTx)
// NewSendRawTransactionCmd will never fail so don't check error. // NewSendRawTransactionCmd will never fail so don't check error.
sendtx, _ := btcjson.NewSendRawTransactionCmd(<-NewJSONID, hextx) sendtx, _ := btcjson.NewSendRawTransactionCmd(<-NewJSONID, hextx)
var txid string request := NewServerRequest(sendtx, new(string))
request := NewRPCRequest(sendtx, txid) response := <-CurrentServerConn().SendRequest(request)
response := <-CurrentRPCConn().SendRequest(request) txid := *response.Result().(*string)
txid = response.Result.(string)
if response.Err != nil { if response.Error() != nil {
SendTxHistSyncChans.remove <- createdTx.txid SendTxHistSyncChans.remove <- createdTx.txid
return nil, response.Err return nil, response.Error()
} }
return handleSendRawTxReply(cmd, txid, a, createdTx) return handleSendRawTxReply(cmd, txid, a, createdTx)
@ -1128,10 +1187,8 @@ func handleSendRawTxReply(icmd btcjson.Cmd, txIDStr string, a *Account, txInfo *
Fee: txInfo.fee, Fee: txInfo.fee,
Receivers: txInfo.outputs, Receivers: txInfo.outputs,
} }
a.TxStore.Lock() a.TxStore = append(a.TxStore, sendtx)
a.TxStore.s = append(a.TxStore.s, sendtx) AcctMgr.ds.ScheduleTxStoreWrite(a)
a.TxStore.Unlock()
a.ScheduleTxStoreWrite()
// Notify frontends of new SendTx. // Notify frontends of new SendTx.
bs, err := GetCurBlock() bs, err := GetCurBlock()
@ -1146,15 +1203,12 @@ func handleSendRawTxReply(icmd btcjson.Cmd, txIDStr string, a *Account, txInfo *
SendTxHistSyncChans.done <- txInfo.txid SendTxHistSyncChans.done <- txInfo.txid
// Remove previous unspent outputs now spent by the tx. // Remove previous unspent outputs now spent by the tx.
a.UtxoStore.Lock() if a.UtxoStore.Remove(txInfo.inputs) {
modified := a.UtxoStore.s.Remove(txInfo.inputs) AcctMgr.ds.ScheduleUtxoStoreWrite(a)
a.UtxoStore.Unlock()
if modified {
a.ScheduleUtxoStoreWrite()
} }
// Disk sync tx and utxo stores. // Disk sync tx and utxo stores.
if err := a.WriteScheduledToDisk(); err != nil { if err := AcctMgr.ds.FlushAccount(a); err != nil {
log.Errorf("cannot write account: %v", err) log.Errorf("cannot write account: %v", err)
} }
@ -1236,39 +1290,36 @@ func CreateEncryptedWallet(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
return nil, &btcjson.ErrInternal return nil, &btcjson.ErrInternal
} }
err := accountstore.CreateEncryptedWallet("", "", []byte(cmd.Passphrase)) err := AcctMgr.CreateEncryptedWallet([]byte(cmd.Passphrase))
switch err { switch err {
case nil: case nil:
// A nil reply is sent upon successful wallet creation. // A nil reply is sent upon successful wallet creation.
return nil, nil return nil, nil
case ErrAcctNotExist: case ErrWalletExists:
return nil, &btcjson.ErrWalletInvalidAccountName return nil, &btcjson.ErrWalletInvalidAccountName
case ErrBtcdDisconnected: case ErrBtcdDisconnected:
e := btcjson.Error{ return nil, &ErrBtcdDisconnected
Code: btcjson.ErrInternal.Code,
Message: "btcd disconnected",
}
return nil, &e
default: // all other non-nil errors default: // all other non-nil errors
return nil, &btcjson.ErrInternal return nil, &btcjson.ErrInternal
} }
} }
// RecoverAddresses recovers the next n addresses from an account's wallet.
func RecoverAddresses(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { func RecoverAddresses(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
cmd, ok := icmd.(*btcws.RecoverAddressesCmd) cmd, ok := icmd.(*btcws.RecoverAddressesCmd)
if !ok { if !ok {
return nil, &btcjson.ErrInternal return nil, &btcjson.ErrInternal
} }
a, err := accountstore.Account(cmd.Account) a, err := AcctMgr.Account(cmd.Account)
switch err { switch err {
case nil: case nil:
break break
case ErrAcctNotExist: case ErrNotFound:
return nil, &btcjson.ErrWalletInvalidAccountName return nil, &btcjson.ErrWalletInvalidAccountName
default: // all other non-nil errors default: // all other non-nil errors
@ -1301,12 +1352,12 @@ func WalletIsLocked(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
return nil, &btcjson.ErrInternal return nil, &btcjson.ErrInternal
} }
a, err := accountstore.Account(cmd.Account) a, err := AcctMgr.Account(cmd.Account)
switch err { switch err {
case nil: case nil:
break break
case ErrAcctNotExist: case ErrNotFound:
return nil, &btcjson.ErrWalletInvalidAccountName return nil, &btcjson.ErrWalletInvalidAccountName
default: // all other non-nil errors default: // all other non-nil errors
@ -1317,19 +1368,14 @@ func WalletIsLocked(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
return nil, &e return nil, &e
} }
a.mtx.RLock() return a.Wallet.IsLocked(), nil
locked := a.Wallet.IsLocked()
a.mtx.RUnlock()
// Reply with true for a locked wallet, and false for unlocked.
return locked, nil
} }
// WalletLock handles a walletlock request by locking the all account // WalletLock handles a walletlock request by locking the all account
// wallets, returning an error if any wallet is not encrypted (for example, // wallets, returning an error if any wallet is not encrypted (for example,
// a watching-only wallet). // a watching-only wallet).
func WalletLock(icmd btcjson.Cmd) (interface{}, *btcjson.Error) { func WalletLock(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
if err := accountstore.LockWallets(); err != nil { if err := AcctMgr.LockWallets(); err != nil {
e := btcjson.Error{ e := btcjson.Error{
Code: btcjson.ErrWallet.Code, Code: btcjson.ErrWallet.Code,
Message: err.Error(), Message: err.Error(),
@ -1350,7 +1396,7 @@ func WalletPassphrase(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
return nil, &btcjson.ErrInternal return nil, &btcjson.ErrInternal
} }
if err := accountstore.UnlockWallets(cmd.Passphrase); err != nil { if err := AcctMgr.UnlockWallets(cmd.Passphrase); err != nil {
e := btcjson.Error{ e := btcjson.Error{
Code: btcjson.ErrWallet.Code, Code: btcjson.ErrWallet.Code,
Message: err.Error(), Message: err.Error(),
@ -1360,7 +1406,7 @@ func WalletPassphrase(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
go func(timeout int64) { go func(timeout int64) {
time.Sleep(time.Second * time.Duration(timeout)) time.Sleep(time.Second * time.Duration(timeout))
_ = accountstore.LockWallets() _ = AcctMgr.LockWallets()
}(cmd.Timeout) }(cmd.Timeout)
return nil, nil return nil, nil
@ -1379,7 +1425,7 @@ func WalletPassphraseChange(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
return nil, &btcjson.ErrInternal return nil, &btcjson.ErrInternal
} }
err := accountstore.ChangePassphrase([]byte(cmd.OldPassphrase), err := AcctMgr.ChangePassphrase([]byte(cmd.OldPassphrase),
[]byte(cmd.NewPassphrase)) []byte(cmd.NewPassphrase))
switch err { switch err {
case nil: case nil:

View file

@ -19,7 +19,6 @@ package main
import ( import (
"code.google.com/p/go.net/websocket" "code.google.com/p/go.net/websocket"
"crypto/sha256" "crypto/sha256"
_ "crypto/sha512" // for cert generation
"crypto/subtle" "crypto/subtle"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
@ -158,7 +157,7 @@ func newServer(listenAddrs []string) (*server, error) {
listeners = append(listeners, listener) listeners = append(listeners, listener)
} }
if len(listeners) == 0 { if len(listeners) == 0 {
return nil, errors.New("RPCS: No valid listen address") return nil, errors.New("no valid listen address")
} }
s.listeners = listeners s.listeners = listeners
@ -201,25 +200,64 @@ func genCertPair(certFile, keyFile string) error {
return nil return nil
} }
// handleRPCRequest processes a JSON-RPC request from a frontend. // ParseRequest parses a command or notification out of a JSON-RPC request,
func (s *server) handleRPCRequest(w http.ResponseWriter, r *http.Request) { // returning any errors as a JSON-RPC error.
func ParseRequest(msg []byte) (btcjson.Cmd, *btcjson.Error) {
cmd, err := btcjson.ParseMarshaledCmd(msg)
if err != nil || cmd.Id() == nil {
return cmd, &btcjson.ErrInvalidRequest
}
return cmd, nil
}
// ReplyToFrontend responds to a marshaled JSON-RPC request with a
// marshaled JSON-RPC response for both standard and extension
// (websocket) clients.
func ReplyToFrontend(msg []byte, ws bool) []byte {
cmd, jsonErr := ParseRequest(msg)
var id interface{}
if cmd != nil {
id = cmd.Id()
}
if jsonErr != nil {
response := btcjson.Reply{
Id: &id,
Error: jsonErr,
}
mresponse, _ := json.Marshal(response)
return mresponse
}
cReq := NewClientRequest(cmd, ws)
result, jsonErr := cReq.Handle()
response := btcjson.Reply{
Id: &id,
Result: result,
Error: jsonErr,
}
mresponse, err := json.Marshal(response)
if err != nil {
log.Errorf("Cannot marhal response: %v", err)
response = btcjson.Reply{
Id: &id,
Error: &btcjson.ErrInternal,
}
mresponse, _ = json.Marshal(&response)
}
return mresponse
}
// ServeRPCRequest processes and replies to a JSON-RPC client request.
func (s *server) ServeRPCRequest(w http.ResponseWriter, r *http.Request) {
body, err := btcjson.GetRaw(r.Body) body, err := btcjson.GetRaw(r.Body)
if err != nil { if err != nil {
log.Errorf("RPCS: Error getting JSON message: %v", err) log.Errorf("RPCS: Error getting JSON message: %v", err)
} }
response := ProcessFrontendRequest(body, false) resp := ReplyToFrontend(body, false)
mresponse, err := json.Marshal(response) if _, err := w.Write(resp); err != nil {
if err != nil {
id := response.Id
response = &btcjson.Reply{
Id: id,
Error: &btcjson.ErrInternal,
}
mresponse, _ = json.Marshal(response)
}
if _, err := w.Write(mresponse); err != nil {
log.Warnf("RPCS: could not respond to RPC request: %v", err) log.Warnf("RPCS: could not respond to RPC request: %v", err)
} }
} }
@ -234,18 +272,19 @@ func frontendListenerDuplicator() {
frontendListeners := make(map[chan []byte]bool) frontendListeners := make(map[chan []byte]bool)
// Don't want to add or delete a wallet listener while iterating // Don't want to add or delete a wallet listener while iterating
// through each to propigate to every attached wallet. Use a mutex to // through each to propigate to every attached wallet. Use a binary
// prevent this. // semaphore to prevent this.
var mtx sync.Mutex sem := make(chan struct{}, 1)
sem <- struct{}{}
// Check for listener channels to add or remove from set. // Check for listener channels to add or remove from set.
go func() { go func() {
for { for {
select { select {
case c := <-addFrontendListener: case c := <-addFrontendListener:
mtx.Lock() <-sem
frontendListeners[c] = true frontendListeners[c] = true
mtx.Unlock() sem <- struct{}{}
NotifyBtcdConnection(c) NotifyBtcdConnection(c)
bs, err := GetCurBlock() bs, err := GetCurBlock()
@ -255,9 +294,9 @@ func frontendListenerDuplicator() {
} }
case c := <-deleteFrontendListener: case c := <-deleteFrontendListener:
mtx.Lock() <-sem
delete(frontendListeners, c) delete(frontendListeners, c)
mtx.Unlock() sem <- struct{}{}
} }
} }
}() }()
@ -267,18 +306,18 @@ func frontendListenerDuplicator() {
for { for {
ntfn := <-frontendNotificationMaster ntfn := <-frontendNotificationMaster
mtx.Lock() <-sem
for c := range frontendListeners { for c := range frontendListeners {
c <- ntfn c <- ntfn
} }
mtx.Unlock() sem <- struct{}{}
} }
} }
// NotifyBtcdConnection notifies a frontend of the current connection // NotifyBtcdConnection notifies a frontend of the current connection
// status of btcwallet to btcd. // status of btcwallet to btcd.
func NotifyBtcdConnection(reply chan []byte) { func NotifyBtcdConnection(reply chan []byte) {
if btcd, ok := CurrentRPCConn().(*BtcdRPCConn); ok { if btcd, ok := CurrentServerConn().(*BtcdRPCConn); ok {
ntfn := btcws.NewBtcdConnectedNtfn(btcd.Connected()) ntfn := btcws.NewBtcdConnectedNtfn(btcd.Connected())
mntfn, _ := ntfn.MarshalJSON() mntfn, _ := ntfn.MarshalJSON()
reply <- mntfn reply <- mntfn
@ -286,12 +325,10 @@ func NotifyBtcdConnection(reply chan []byte) {
} }
// frontendSendRecv is the handler function for websocket connections from // WSSendRecv is the handler for websocket client connections. It loops
// a btcwallet instance. It reads requests and sends responses to a // forever (until disconnected), reading JSON-RPC requests and sending
// frontend, as well as notififying wallets of chain updates. There can // sending responses and notifications.
// possibly be many of these running, one for each currently connected func WSSendRecv(ws *websocket.Conn) {
// frontend.
func frontendSendRecv(ws *websocket.Conn) {
// Add frontend notification channel to set so this handler receives // Add frontend notification channel to set so this handler receives
// updates. // updates.
frontendNotification := make(chan []byte) frontendNotification := make(chan []byte)
@ -324,11 +361,10 @@ func frontendSendRecv(ws *websocket.Conn) {
return return
} }
// Handle request here. // Handle request here.
go func() { go func(m []byte) {
reply := ProcessFrontendRequest(m, true) resp := ReplyToFrontend(m, true)
mreply, _ := json.Marshal(reply) frontendNotification <- resp
frontendNotification <- mreply }(m)
}()
case ntfn, _ := <-frontendNotification: case ntfn, _ := <-frontendNotification:
if err := websocket.Message.Send(ws, ntfn); err != nil { if err := websocket.Message.Send(ws, ntfn); err != nil {
@ -370,14 +406,14 @@ func (s *server) Start() {
http.Error(w, "401 Unauthorized.", http.StatusUnauthorized) http.Error(w, "401 Unauthorized.", http.StatusUnauthorized)
return return
} }
s.handleRPCRequest(w, r) s.ServeRPCRequest(w, r)
}) })
serveMux.HandleFunc("/frontend", func(w http.ResponseWriter, r *http.Request) { serveMux.HandleFunc("/frontend", func(w http.ResponseWriter, r *http.Request) {
if err := s.checkAuth(r); err != nil { if err := s.checkAuth(r); err != nil {
http.Error(w, "401 Unauthorized.", http.StatusUnauthorized) http.Error(w, "401 Unauthorized.", http.StatusUnauthorized)
return return
} }
websocket.Handler(frontendSendRecv).ServeHTTP(w, r) websocket.Handler(WSSendRecv).ServeHTTP(w, r)
}) })
for _, listener := range s.listeners { for _, listener := range s.listeners {
s.wg.Add(1) s.wg.Add(1)
@ -480,7 +516,7 @@ func BtcdConnect(certificates []byte) (*BtcdRPCConn, error) {
func resendUnminedTxs() { func resendUnminedTxs() {
for _, createdTx := range UnminedTxs.m { for _, createdTx := range UnminedTxs.m {
hextx := hex.EncodeToString(createdTx.rawTx) hextx := hex.EncodeToString(createdTx.rawTx)
if txid, err := SendRawTransaction(CurrentRPCConn(), hextx); err != nil { if txid, err := SendRawTransaction(CurrentServerConn(), hextx); err != nil {
// TODO(jrick): Check error for if this tx is a double spend, // TODO(jrick): Check error for if this tx is a double spend,
// remove it if so. // remove it if so.
} else { } else {
@ -498,7 +534,7 @@ func resendUnminedTxs() {
// TODO(jrick): Track and Rescan commands should be replaced with a // TODO(jrick): Track and Rescan commands should be replaced with a
// single TrackSince function (or similar) which requests address // single TrackSince function (or similar) which requests address
// notifications and performs the rescan since some block height. // notifications and performs the rescan since some block height.
func Handshake(rpc RPCConn) error { func Handshake(rpc ServerConn) error {
net, jsonErr := GetCurrentNet(rpc) net, jsonErr := GetCurrentNet(rpc)
if jsonErr != nil { if jsonErr != nil {
return jsonErr return jsonErr
@ -523,7 +559,7 @@ func Handshake(rpc RPCConn) error {
// Get default account. Only the default account is used to // Get default account. Only the default account is used to
// track recently-seen blocks. // track recently-seen blocks.
a, err := accountstore.Account("") a, err := AcctMgr.Account("")
if err != nil { if err != nil {
// No account yet is not a handshake error, but means our // No account yet is not a handshake error, but means our
// handshake is done. // handshake is done.
@ -555,7 +591,7 @@ func Handshake(rpc RPCConn) error {
// try to write new tx and utxo files on each rollback. // try to write new tx and utxo files on each rollback.
if it.Next() { if it.Next() {
bs := it.BlockStamp() bs := it.BlockStamp()
accountstore.Rollback(bs.Height, &bs.Hash) AcctMgr.Rollback(bs.Height, &bs.Hash)
} }
// Set default account to be marked in sync with the current // Set default account to be marked in sync with the current
@ -563,8 +599,8 @@ func Handshake(rpc RPCConn) error {
a.Wallet.SetSyncedWith(bs) a.Wallet.SetSyncedWith(bs)
// Begin tracking wallets against this btcd instance. // Begin tracking wallets against this btcd instance.
accountstore.Track() AcctMgr.Track()
accountstore.RescanActiveAddresses() AcctMgr.RescanActiveAddresses()
// (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()
@ -581,8 +617,8 @@ func Handshake(rpc RPCConn) error {
// and start a new rescan since the earliest block wallet must know // and start a new rescan since the earliest block wallet must know
// about. // about.
a.fullRescan = true a.fullRescan = true
accountstore.Track() AcctMgr.Track()
accountstore.RescanActiveAddresses() AcctMgr.RescanActiveAddresses()
resendUnminedTxs() resendUnminedTxs()
return nil return nil
} }