Updated waddrmgr to manage account names

This commit is contained in:
Javed Khan 2014-12-12 14:24:26 +05:30
parent 85fe722e99
commit 68a9168d9e
10 changed files with 1549 additions and 277 deletions

View file

@ -128,7 +128,7 @@ func (u ByAmount) Swap(i, j int) { u[i], u[j] = u[j], u[i] }
// to addr or as a fee for the miner are sent to a newly generated
// address. InsufficientFundsError is returned if there are not enough
// eligible unspent outputs to create the transaction.
func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, minconf int) (*CreatedTx, error) {
func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, account uint32, minconf int) (*CreatedTx, error) {
// Address manager must be unlocked to compose transaction. Grab
// the unlock if possible (to prevent future unlocks), or return the
@ -145,12 +145,12 @@ func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, minconf int) (*Creat
return nil, err
}
eligible, err := w.findEligibleOutputs(minconf, bs)
eligible, err := w.findEligibleOutputs(account, minconf, bs)
if err != nil {
return nil, err
}
return createTx(eligible, pairs, bs, w.FeeIncrement, w.Manager, w.changeAddress)
return createTx(eligible, pairs, bs, w.FeeIncrement, w.Manager, account, w.NewChangeAddress)
}
// createTx selects inputs (from the given slice of eligible utxos)
@ -164,7 +164,8 @@ func createTx(
bs *waddrmgr.BlockStamp,
feeIncrement btcutil.Amount,
mgr *waddrmgr.Manager,
changeAddress func(*waddrmgr.BlockStamp) (btcutil.Address, error)) (
account uint32,
changeAddress func(account uint32) (btcutil.Address, error)) (
*CreatedTx, error) {
msgtx := wire.NewMsgTx()
@ -220,7 +221,7 @@ func createTx(
change := totalAdded - minAmount - feeEst
if change > 0 {
if changeAddr == nil {
changeAddr, err = changeAddress(bs)
changeAddr, err = changeAddress(account)
if err != nil {
return nil, err
}
@ -293,23 +294,6 @@ func addChange(msgtx *wire.MsgTx, change btcutil.Amount, changeAddr btcutil.Addr
return int(r), nil
}
// changeAddress obtains a new btcutil.Address to be used as a change
// transaction output. It will also mark the KeyStore as dirty and
// tells chainSvr to watch that address.
func (w *Wallet) changeAddress(bs *waddrmgr.BlockStamp) (btcutil.Address, error) {
changeAddrs, err := w.Manager.NextInternalAddresses(0, 1)
if err != nil {
return nil, fmt.Errorf("failed to get change address: %s", err)
}
changeAddr := changeAddrs[0].Address()
err = w.chainSvr.NotifyReceived([]btcutil.Address{changeAddr})
if err != nil {
return nil, fmt.Errorf("cannot request updates for "+
"change address: %v", err)
}
return changeAddr, nil
}
// addOutputs adds the given address/amount pairs as outputs to msgtx,
// returning their total amount.
func addOutputs(msgtx *wire.MsgTx, pairs map[string]btcutil.Amount) (btcutil.Amount, error) {
@ -335,7 +319,7 @@ func addOutputs(msgtx *wire.MsgTx, pairs map[string]btcutil.Amount) (btcutil.Amo
return minAmount, nil
}
func (w *Wallet) findEligibleOutputs(minconf int, bs *waddrmgr.BlockStamp) ([]txstore.Credit, error) {
func (w *Wallet) findEligibleOutputs(account uint32, minconf int, bs *waddrmgr.BlockStamp) ([]txstore.Credit, error) {
unspent, err := w.TxStore.UnspentOutputs()
if err != nil {
return nil, err
@ -365,7 +349,13 @@ func (w *Wallet) findEligibleOutputs(minconf int, bs *waddrmgr.BlockStamp) ([]tx
continue
}
eligible = append(eligible, unspent[i])
creditAccount, err := w.CreditAccount(unspent[i])
if err != nil {
continue
}
if creditAccount == account {
eligible = append(eligible, unspent[i])
}
}
}
return eligible, nil

View file

@ -73,8 +73,9 @@ func TestCreateTx(t *testing.T) {
cfg = &config{DisallowFree: false}
bs := &waddrmgr.BlockStamp{Height: 11111}
mgr := newManager(t, txInfo.privKeys, bs)
account := uint32(0)
changeAddr, _ := btcutil.DecodeAddress("muqW4gcixv58tVbSKRC5q6CRKy8RmyLgZ5", activeNet.Params)
var tstChangeAddress = func(bs *waddrmgr.BlockStamp) (btcutil.Address, error) {
var tstChangeAddress = func(account uint32) (btcutil.Address, error) {
return changeAddr, nil
}
@ -82,7 +83,7 @@ func TestCreateTx(t *testing.T) {
eligible := eligibleInputsFromTx(t, txInfo.hex, []uint32{1, 2, 3, 4, 5})
// Now create a new TX sending 25e6 satoshis to the following addresses:
outputs := map[string]btcutil.Amount{outAddr1: 15e6, outAddr2: 10e6}
tx, err := createTx(eligible, outputs, bs, defaultFeeIncrement, mgr, tstChangeAddress)
tx, err := createTx(eligible, outputs, bs, defaultFeeIncrement, mgr, account, tstChangeAddress)
if err != nil {
t.Fatal(err)
}
@ -124,12 +125,13 @@ func TestCreateTxInsufficientFundsError(t *testing.T) {
outputs := map[string]btcutil.Amount{outAddr1: 10, outAddr2: 1e9}
eligible := eligibleInputsFromTx(t, txInfo.hex, []uint32{1})
bs := &waddrmgr.BlockStamp{Height: 11111}
account := uint32(0)
changeAddr, _ := btcutil.DecodeAddress("muqW4gcixv58tVbSKRC5q6CRKy8RmyLgZ5", activeNet.Params)
var tstChangeAddress = func(bs *waddrmgr.BlockStamp) (btcutil.Address, error) {
var tstChangeAddress = func(account uint32) (btcutil.Address, error) {
return changeAddr, nil
}
_, err := createTx(eligible, outputs, bs, defaultFeeIncrement, nil, tstChangeAddress)
_, err := createTx(eligible, outputs, bs, defaultFeeIncrement, nil, account, tstChangeAddress)
if err == nil {
t.Error("Expected InsufficientFundsError, got no error")

View file

@ -89,8 +89,9 @@ var (
errors.New("minconf must be positive"),
}
ErrAddressNotInWallet = InvalidAddressOrKeyError{
errors.New("address not found in wallet"),
ErrAddressNotInWallet = btcjson.Error{
Code: btcjson.ErrWallet.Code,
Message: "address not found in wallet",
}
ErrNoAccountSupport = btcjson.Error{
@ -98,6 +99,11 @@ var (
Message: "btcwallet does not support non-default accounts",
}
ErrAccountNameNotFound = btcjson.Error{
Code: btcjson.ErrWalletInvalidAccountName.Code,
Message: "account name not found",
}
ErrUnloadedWallet = btcjson.Error{
Code: btcjson.ErrWallet.Code,
Message: "Request requires a wallet but wallet has not loaded yet",
@ -179,15 +185,25 @@ func isManagerWrongPassphraseError(err error) bool {
return ok && merr.ErrorCode == waddrmgr.ErrWrongPassphrase
}
// isManagerDuplicateError returns whether or not the passed error is due to a
// isManagerDuplicateAddressError returns whether or not the passed error is due to a
// duplicate item being provided to the address manager.
func isManagerDuplicateError(err error) bool {
func isManagerDuplicateAddressError(err error) bool {
merr, ok := err.(waddrmgr.ManagerError)
if !ok {
return false
}
return ok && merr.ErrorCode == waddrmgr.ErrDuplicateAddress
}
return merr.ErrorCode == waddrmgr.ErrDuplicate
// isManagerAddressNotFoundError returns whether or not the passed error is due to a
// the address not being found.
func isManagerAddressNotFoundError(err error) bool {
merr, ok := err.(waddrmgr.ManagerError)
return ok && merr.ErrorCode == waddrmgr.ErrAddressNotFound
}
// isManagerAccountNotFoundError returns whether or not the passed error is due
// to the account not being found.
func isManagerAccountNotFoundError(err error) bool {
merr, ok := err.(waddrmgr.ManagerError)
return ok && merr.ErrorCode == waddrmgr.ErrAccountNotFound
}
// parseListeners splits the list of listen addresses passed in addrs into
@ -984,7 +1000,7 @@ func (s *rpcServer) PostClientRPC(w http.ResponseWriter, r *http.Request) {
return
}
// Create the response and error from the request. Three special cases
// Create the response and error from the request. Two special cases
// are handled for the authenticate and stop request methods.
var resp btcjson.Reply
switch raw.Method {
@ -1044,13 +1060,18 @@ func (b blockDisconnected) notificationCmds(w *Wallet) []btcjson.Cmd {
func (c txCredit) notificationCmds(w *Wallet) []btcjson.Cmd {
blk := w.Manager.SyncedTo()
ltr, err := txstore.Credit(c).ToJSON("", blk.Height, activeNet.Params)
acctName := waddrmgr.DefaultAccountName
if creditAccount, err := w.CreditAccount(txstore.Credit(c)); err == nil {
// acctName is defaulted to DefaultAccountName in case of an error
acctName, _ = w.Manager.AccountName(creditAccount)
}
ltr, err := txstore.Credit(c).ToJSON(acctName, blk.Height, activeNet.Params)
if err != nil {
log.Errorf("Cannot create notification for transaction "+
"credit: %v", err)
return nil
}
n := btcws.NewTxNtfn("", &ltr)
n := btcws.NewTxNtfn(acctName, &ltr)
return []btcjson.Cmd{n}
}
@ -1076,13 +1097,13 @@ func (l managerLocked) notificationCmds(w *Wallet) []btcjson.Cmd {
func (b confirmedBalance) notificationCmds(w *Wallet) []btcjson.Cmd {
n := btcws.NewAccountBalanceNtfn("",
btcutil.Amount(b).ToUnit(btcutil.AmountBTC), true)
btcutil.Amount(b).ToBTC(), true)
return []btcjson.Cmd{n}
}
func (b unconfirmedBalance) notificationCmds(w *Wallet) []btcjson.Cmd {
n := btcws.NewAccountBalanceNtfn("",
btcutil.Amount(b).ToUnit(btcutil.AmountBTC), false)
btcutil.Amount(b).ToBTC(), false)
return []btcjson.Cmd{n}
}
@ -1359,6 +1380,7 @@ var rpcHandlers = map[string]requestHandler{
"setaccount": Unsupported,
// Extensions to the reference client JSON-RPC API
"createnewaccount": CreateNewAccount,
"exportwatchingwallet": ExportWatchingWallet,
"getbestblock": GetBestBlock,
// This was an extension but the reference implementation added it as
@ -1368,6 +1390,7 @@ var rpcHandlers = map[string]requestHandler{
"getunconfirmedbalance": GetUnconfirmedBalance,
"listaddresstransactions": ListAddressTransactions,
"listalltransactions": ListAllTransactions,
"renameaccount": RenameAccount,
"walletislocked": WalletIsLocked,
}
@ -1642,12 +1665,22 @@ func ExportWatchingWallet(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (
func GetAddressesByAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcjson.GetAddressesByAccountCmd)
err := checkAccountName(cmd.Account)
account, err := w.Manager.LookupAccount(cmd.Account)
if err != nil {
return nil, err
}
return w.SortedActivePaymentAddresses()
addrs, err := w.Manager.AllAccountAddresses(account)
if err != nil {
return nil, err
}
addrStrs := make([]string, len(addrs))
for i, addr := range addrs {
addrStrs[i] = addr.Address().EncodeAddress()
}
return addrStrs, nil
}
// GetBalance handles a getbalance request by returning the balance for an
@ -1656,22 +1689,22 @@ func GetAddressesByAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd)
func GetBalance(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcjson.GetBalanceCmd)
var account string
if cmd.Account != nil {
account = *cmd.Account
var balance btcutil.Amount
var account uint32
var err error
if cmd.Account == nil || *cmd.Account == "*" {
balance, err = w.CalculateBalance(cmd.MinConf)
} else {
account, err = w.Manager.LookupAccount(*cmd.Account)
if err != nil {
return nil, err
}
balance, err = w.CalculateAccountBalance(account, cmd.MinConf)
}
err := checkAccountName(account)
if err != nil {
return nil, err
}
balance, err := w.CalculateBalance(cmd.MinConf)
if err != nil {
return nil, err
}
return balance.ToUnit(btcutil.AmountBTC), nil
return balance.ToBTC(), nil
}
// GetBestBlock handles a getbestblock request by returning a JSON object
@ -1718,11 +1751,11 @@ func GetInfo(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{},
// TODO(davec): This should probably have a database version as opposed
// to using the manager version.
info.WalletVersion = int32(waddrmgr.LatestMgrVersion)
info.Balance = bal.ToUnit(btcutil.AmountBTC)
info.Balance = bal.ToBTC()
// Keypool times are not tracked. set to current time.
info.KeypoolOldest = time.Now().Unix()
info.KeypoolSize = int32(cfg.KeypoolSize)
info.PaytxFee = w.FeeIncrement.ToUnit(btcutil.AmountBTC)
info.PaytxFee = w.FeeIncrement.ToBTC()
// We don't set the following since they don't make much sense in the
// wallet architecture:
// - unlocked_until
@ -1742,13 +1775,17 @@ func GetAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{
return nil, btcjson.ErrInvalidAddressOrKey
}
// If it is in the wallet, we consider it part of the default account.
_, err = w.Manager.Address(addr)
// Fetch the associated account
account, err := w.Manager.AddrAccount(addr)
if err != nil {
return nil, btcjson.ErrInvalidAddressOrKey
return nil, ErrAddressNotInWallet
}
return "", nil
acctName, err := w.Manager.AccountName(account)
if err != nil {
return nil, ErrAccountNameNotFound
}
return acctName, nil
}
// GetAccountAddress handles a getaccountaddress by returning the most
@ -1760,12 +1797,11 @@ func GetAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{
func GetAccountAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcjson.GetAccountAddressCmd)
err := checkDefaultAccount(cmd.Account)
account, err := w.Manager.LookupAccount(cmd.Account)
if err != nil {
return nil, err
}
addr, err := w.CurrentAddress()
addr, err := w.CurrentAddress(account)
if err != nil {
return nil, err
}
@ -1778,21 +1814,21 @@ func GetAccountAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (int
func GetUnconfirmedBalance(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcws.GetUnconfirmedBalanceCmd)
err := checkAccountName(cmd.Account)
account, err := w.Manager.LookupAccount(cmd.Account)
if err != nil {
return nil, err
}
unconfirmed, err := w.CalculateBalance(0)
unconfirmed, err := w.CalculateAccountBalance(account, 0)
if err != nil {
return nil, err
}
confirmed, err := w.CalculateBalance(1)
confirmed, err := w.CalculateAccountBalance(account, 1)
if err != nil {
return nil, err
}
return (unconfirmed - confirmed).ToUnit(btcutil.AmountBTC), nil
return (unconfirmed - confirmed).ToBTC(), nil
}
// ImportPrivKey handles an importprivkey request by parsing
@ -1814,7 +1850,7 @@ func ImportPrivKey(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interfa
// Import the private key, handling any errors.
_, err = w.ImportPrivateKey(wif, nil, cmd.Rescan)
switch {
case isManagerDuplicateError(err):
case isManagerDuplicateAddressError(err):
// Do not return duplicate key errors to the client.
return nil, nil
case isManagerLockedError(err):
@ -1830,18 +1866,61 @@ func KeypoolRefill(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interfa
return nil, nil
}
// GetNewAddress handlesa getnewaddress request by returning a new
// address for an account. If the account does not exist or the keypool
// ran out with a locked wallet, an appropiate error is returned.
// CreateNewAccount handles a createnewaccount request by creating and
// returning a new account. If the last account has no transaction history
// as per BIP 0044 a new account cannot be created so an error will be returned.
func CreateNewAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcws.CreateNewAccountCmd)
// Check that we are within the maximum allowed non-empty accounts limit.
account, err := w.Manager.LastAccount()
if err != nil {
return nil, err
}
if account > maxEmptyAccounts {
used, err := w.AccountUsed(account)
if err != nil {
return nil, err
}
if !used {
return nil, errors.New("cannot create account: " +
"previous account has no transaction history")
}
}
_, err = w.Manager.NewAccount(cmd.Account)
if isManagerLockedError(err) {
return nil, btcjson.ErrWalletUnlockNeeded
}
return nil, err
}
// RenameAccount handles a renameaccount request by renaming an account.
// If the account does not exist an appropiate error will be returned.
func RenameAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcws.RenameAccountCmd)
// Check that given account exists
account, err := w.Manager.LookupAccount(cmd.OldAccount)
if err != nil {
return nil, err
}
return nil, w.Manager.RenameAccount(account, cmd.NewAccount)
}
// GetNewAddress handles a getnewaddress request by returning a new
// address for an account. If the account does not exist an appropiate
// error is returned.
// TODO: Follow BIP 0044 and warn if number of unused addresses exceeds
// the gap limit.
func GetNewAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcjson.GetNewAddressCmd)
err := checkDefaultAccount(cmd.Account)
account, err := w.Manager.LookupAccount(cmd.Account)
if err != nil {
return nil, err
}
addr, err := w.NewAddress()
addr, err := w.NewAddress(account)
if err != nil {
return nil, err
}
@ -1856,7 +1935,12 @@ func GetNewAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interfa
// Note: bitcoind allows specifying the account as an optional parameter,
// but ignores the parameter.
func GetRawChangeAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
addr, err := w.NewChangeAddress()
cmd := icmd.(*btcjson.GetRawChangeAddressCmd)
account, err := w.Manager.LookupAccount(cmd.Account)
if err != nil {
return nil, err
}
addr, err := w.NewChangeAddress(account)
if err != nil {
return nil, err
}
@ -1870,17 +1954,17 @@ func GetRawChangeAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (i
func GetReceivedByAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcjson.GetReceivedByAccountCmd)
err := checkAccountName(cmd.Account)
account, err := w.Manager.LookupAccount(cmd.Account)
if err != nil {
return nil, err
}
bal, err := w.TotalReceived(cmd.MinConf)
bal, _, err := w.TotalReceivedForAccount(account, cmd.MinConf)
if err != nil {
return nil, err
}
return bal.ToUnit(btcutil.AmountBTC), nil
return bal.ToBTC(), nil
}
// GetReceivedByAddress handles a getreceivedbyaddress request by returning
@ -1897,7 +1981,7 @@ func GetReceivedByAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (
return nil, err
}
return total.ToUnit(btcutil.AmountBTC), nil
return total.ToBTC(), nil
}
// GetTransaction handles a gettransaction request by returning details about
@ -1957,11 +2041,11 @@ func GetTransaction(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interf
ret.Details = make([]btcjson.GetTransactionDetailsResult, 1, len(credits)+1)
details := btcjson.GetTransactionDetailsResult{
Account: "",
Account: waddrmgr.DefaultAccountName,
Category: "send",
// negative since it is a send
Amount: (-debits.OutputAmount(true)).ToUnit(btcutil.AmountBTC),
Fee: debits.Fee().ToUnit(btcutil.AmountBTC),
Amount: (-debits.OutputAmount(true)).ToBTC(),
Fee: debits.Fee().ToBTC(),
}
targetAddr = &details.Address
ret.Details[0] = details
@ -1992,14 +2076,14 @@ func GetTransaction(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interf
}
ret.Details = append(ret.Details, btcjson.GetTransactionDetailsResult{
Account: "",
Account: waddrmgr.DefaultAccountName,
Category: cred.Category(blk.Height).String(),
Amount: cred.Amount().ToUnit(btcutil.AmountBTC),
Amount: cred.Amount().ToBTC(),
Address: addr,
})
}
ret.Amount = creditAmount.ToUnit(btcutil.AmountBTC)
ret.Amount = creditAmount.ToBTC()
return ret, nil
}
@ -2008,13 +2092,24 @@ func GetTransaction(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interf
func ListAccounts(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcjson.ListAccountsCmd)
bal, err := w.CalculateBalance(cmd.MinConf)
accountBalances := map[string]float64{}
accounts, err := w.Manager.AllAccounts()
if err != nil {
return nil, err
}
for _, account := range accounts {
acctName, err := w.Manager.AccountName(account)
if err != nil {
return nil, ErrAccountNameNotFound
}
bal, err := w.CalculateAccountBalance(account, cmd.MinConf)
if err != nil {
return nil, err
}
accountBalances[acctName] = bal.ToBTC()
}
// Return the map. This will be marshaled into a JSON object.
return map[string]float64{"": bal.ToUnit(btcutil.AmountBTC)}, nil
return accountBalances, nil
}
// ListLockUnspent handles a listlockunspent request by returning an slice of
@ -2033,36 +2128,29 @@ func ListLockUnspent(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (inter
// default: one;
// "includeempty": whether or not to include addresses that have no transactions -
// default: false.
// Since btcwallet doesn't implement account support yet, only the default account ""
// will be returned
func ListReceivedByAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcjson.ListReceivedByAccountCmd)
blk := w.Manager.SyncedTo()
// Total amount received.
var amount btcutil.Amount
// Number of confirmations of the last transaction.
var confirmations int32
for _, record := range w.TxStore.Records() {
for _, credit := range record.Credits() {
if !credit.Confirmed(cmd.MinConf, blk.Height) {
// Not enough confirmations, skip the current block.
continue
}
amount += credit.Amount()
confirmations = credit.Confirmations(blk.Height)
}
accounts, err := w.Manager.AllAccounts()
if err != nil {
return nil, err
}
ret := []btcjson.ListReceivedByAccountResult{
{
Account: "",
Amount: amount.ToUnit(btcutil.AmountBTC),
ret := make([]btcjson.ListReceivedByAccountResult, 0, len(accounts))
for _, account := range accounts {
acctName, err := w.Manager.AccountName(account)
if err != nil {
return nil, ErrAccountNameNotFound
}
bal, confirmations, err := w.TotalReceivedForAccount(account, cmd.MinConf)
if err != nil {
return nil, err
}
ret = append(ret, btcjson.ListReceivedByAccountResult{
Account: acctName,
Amount: bal.ToBTC(),
Confirmations: uint64(confirmations),
},
})
}
return ret, nil
}
@ -2089,6 +2177,8 @@ func ListReceivedByAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd)
confirmations int32
// Hashes of transactions which include an output paying to the address
tx []string
// Account which the address belongs to
account string
}
blk := w.Manager.SyncedTo()
@ -2144,9 +2234,9 @@ func ListReceivedByAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd)
idx := 0
for address, addrData := range allAddrData {
ret[idx] = btcjson.ListReceivedByAddressResult{
Account: "",
Account: waddrmgr.DefaultAccountName,
Address: address,
Amount: addrData.amount.ToUnit(btcutil.AmountBTC),
Amount: addrData.amount.ToBTC(),
Confirmations: uint64(addrData.confirmations),
TxIDs: addrData.tx,
}
@ -2204,14 +2294,11 @@ func ListSinceBlock(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interf
func ListTransactions(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcjson.ListTransactionsCmd)
var account string
if cmd.Account != nil {
account = *cmd.Account
}
err := checkAccountName(account)
if err != nil {
return nil, err
err := checkAccountName(*cmd.Account)
if err != nil {
return nil, err
}
}
return w.ListTransactions(cmd.From, cmd.Count)
@ -2254,14 +2341,11 @@ func ListAddressTransactions(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd
func ListAllTransactions(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcws.ListAllTransactionsCmd)
var account string
if cmd.Account != nil {
account = *cmd.Account
}
err := checkAccountName(account)
if err != nil {
return nil, err
err := checkAccountName(*cmd.Account)
if err != nil {
return nil, err
}
}
return w.ListAllTransactions()
@ -2318,11 +2402,11 @@ func LockUnspent(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface
// sendPairs is a helper routine to reduce duplicated code when creating and
// sending payment transactions.
func sendPairs(w *Wallet, chainSvr *chain.Client, cmd btcjson.Cmd,
amounts map[string]btcutil.Amount, minconf int) (interface{}, error) {
amounts map[string]btcutil.Amount, account uint32, minconf int) (interface{}, error) {
// Create transaction, replying with an error if the creation
// was not successful.
createdTx, err := w.CreateSimpleTx(amounts, minconf)
createdTx, err := w.CreateSimpleTx(account, amounts, minconf)
if err != nil {
switch {
case err == ErrNonPositiveAmount:
@ -2371,7 +2455,7 @@ func sendPairs(w *Wallet, chainSvr *chain.Client, cmd btcjson.Cmd,
func SendFrom(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcjson.SendFromCmd)
err := checkAccountName(cmd.FromAccount)
account, err := w.Manager.LookupAccount(cmd.FromAccount)
if err != nil {
return nil, err
}
@ -2388,7 +2472,7 @@ func SendFrom(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{},
cmd.ToAddress: btcutil.Amount(cmd.Amount),
}
return sendPairs(w, chainSvr, cmd, pairs, cmd.MinConf)
return sendPairs(w, chainSvr, cmd, pairs, account, cmd.MinConf)
}
// SendMany handles a sendmany RPC request by creating a new transaction
@ -2399,7 +2483,7 @@ func SendFrom(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{},
func SendMany(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcjson.SendManyCmd)
err := checkAccountName(cmd.FromAccount)
account, err := w.Manager.LookupAccount(cmd.FromAccount)
if err != nil {
return nil, err
}
@ -2415,7 +2499,7 @@ func SendMany(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{},
pairs[k] = btcutil.Amount(v)
}
return sendPairs(w, chainSvr, cmd, pairs, cmd.MinConf)
return sendPairs(w, chainSvr, cmd, pairs, account, cmd.MinConf)
}
// SendToAddress handles a sendtoaddress RPC request by creating a new
@ -2436,7 +2520,8 @@ func SendToAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interfa
cmd.Address: btcutil.Amount(cmd.Amount),
}
return sendPairs(w, chainSvr, cmd, pairs, 1)
// sendtoaddress always spends from the default account, this matches bitcoind
return sendPairs(w, chainSvr, cmd, pairs, waddrmgr.DefaultAccountNum, 1)
}
// SetTxFee sets the transaction fee per kilobyte added to transactions.
@ -2785,20 +2870,22 @@ func ValidateAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (inter
result.IsValid = true
ainfo, err := w.Manager.Address(addr)
if managerErr, ok := err.(waddrmgr.ManagerError); ok {
if managerErr.ErrorCode == waddrmgr.ErrAddressNotFound {
if err != nil {
if isManagerAddressNotFoundError(err) {
// No additional information available about the address.
return result, nil
}
}
if err != nil {
return nil, err
}
// The address lookup was successful which means there is further
// information about it available and it is "mine".
result.IsMine = true
result.Account = ""
acctName, err := w.Manager.AccountName(ainfo.Account())
if err != nil {
return nil, ErrAccountNameNotFound
}
result.Account = acctName
switch ma := ainfo.(type) {
case waddrmgr.ManagedPubKeyAddress:

View file

@ -78,8 +78,8 @@ func (d Debits) toJSON(account string, chainHeight int32,
Account: account,
Address: address,
Category: "send",
Amount: btcutil.Amount(-txOut.Value).ToUnit(btcutil.AmountBTC),
Fee: d.Fee().ToUnit(btcutil.AmountBTC),
Amount: btcutil.Amount(-txOut.Value).ToBTC(),
Fee: d.Fee().ToBTC(),
TxID: d.Tx().Sha().String(),
Time: d.txRecord.received.Unix(),
TimeReceived: d.txRecord.received.Unix(),
@ -176,7 +176,7 @@ func (c Credit) toJSON(account string, chainHeight int32,
Account: account,
Category: c.category(chainHeight).String(),
Address: address,
Amount: btcutil.Amount(txout.Value).ToUnit(btcutil.AmountBTC),
Amount: btcutil.Amount(txout.Value).ToBTC(),
TxID: c.Tx().Sha().String(),
Time: c.received.Unix(),
TimeReceived: c.received.Unix(),

View file

@ -17,19 +17,20 @@
package waddrmgr
import (
"bytes"
"encoding/binary"
"fmt"
"time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/btcsuite/fastsha256"
)
const (
// LatestMgrVersion is the most recent manager version.
LatestMgrVersion = 2
LatestMgrVersion = 3
)
var (
@ -38,6 +39,11 @@ var (
latestMgrVersion uint32 = LatestMgrVersion
)
// ObtainUserInputFunc is a function that reads a user input and returns it as
// a byte stream. It is used to accept data required during upgrades, for e.g.
// wallet seed and private passphrase.
type ObtainUserInputFunc func() ([]byte, error)
// maybeConvertDbError converts the passed error to a ManagerError with an
// error code of ErrDatabase if it is not already a ManagerError. This is
// useful for potential errors returned from managed transaction an other parts
@ -137,12 +143,48 @@ type dbScriptAddressRow struct {
// Key names for various database fields.
var (
// nullVall is null byte used as a flag value in a bucket entry
nullVal = []byte{0}
// Bucket names.
acctBucketName = []byte("acct")
addrBucketName = []byte("addr")
acctBucketName = []byte("acct")
addrBucketName = []byte("addr")
// addrAcctIdxBucketName is used to index account addresses
// Entries in this index may map:
// * addr hash => account id
// * account bucket -> addr hash => null
// To fetch the account of an address, lookup the value using
// the address hash.
// To fetch all addresses of an account, fetch the account bucket, iterate
// over the keys and fetch the address row from the addr bucket.
// The index needs to be updated whenever an address is created e.g.
// NewAddress
addrAcctIdxBucketName = []byte("addracctidx")
mainBucketName = []byte("main")
syncBucketName = []byte("sync")
// acctNameIdxBucketName is used to create an index
// mapping an account name string to the corresponding
// account id.
// The index needs to be updated whenever the account name
// and id changes e.g. RenameAccount
acctNameIdxBucketName = []byte("acctnameidx")
// acctIdIdxBucketName is used to create an index
// mapping an account id to the corresponding
// account name string.
// The index needs to be updated whenever the account name
// and id changes e.g. RenameAccount
acctIdIdxBucketName = []byte("acctididx")
// meta is used to store meta-data about the address manager
// e.g. last account number
metaBucketName = []byte("meta")
// lastAccountName is used to store the metadata - last account
// in the manager
lastAccountName = []byte("lastaccount")
mainBucketName = []byte("main")
syncBucketName = []byte("sync")
// Db related key names (main bucket).
mgrVersionName = []byte("mgrver")
@ -154,6 +196,8 @@ var (
cryptoPrivKeyName = []byte("cpriv")
cryptoPubKeyName = []byte("cpub")
cryptoScriptKeyName = []byte("cscript")
coinTypePrivKeyName = []byte("ctpriv")
coinTypePubKeyName = []byte("ctpub")
watchingOnlyName = []byte("watchonly")
// Sync related key names (sync bucket).
@ -176,6 +220,40 @@ func uint32ToBytes(number uint32) []byte {
return buf
}
// uint64ToBytes converts a 64 bit unsigned integer into a 8-byte slice in
// little-endian order: 1 -> [1 0 0 0 0 0 0 0].
func uint64ToBytes(number uint64) []byte {
buf := make([]byte, 8)
binary.LittleEndian.PutUint64(buf, number)
return buf
}
// stringToBytes converts a string into a variable length byte slice in
// little-endian order: "abc" -> [3 0 0 0 61 62 63]
func stringToBytes(s string) []byte {
// The serialized format is:
// <size><string>
//
// 4 bytes string size + string
size := len(s)
buf := make([]byte, 4+size)
copy(buf[0:4], uint32ToBytes(uint32(size)))
copy(buf[4:4+size], s)
return buf
}
// fetchManagerVersion fetches the current manager version from the database.
func fetchManagerVersion(tx walletdb.Tx) (uint32, error) {
mainBucket := tx.RootBucket().Bucket(mainBucketName)
verBytes := mainBucket.Get(mgrVersionName)
if verBytes == nil {
str := "required version number not stored in database"
return 0, managerError(ErrDatabase, str, nil)
}
version := binary.LittleEndian.Uint32(verBytes)
return version, nil
}
// putManagerVersion stores the provided version to the database.
func putManagerVersion(tx walletdb.Tx, version uint32) error {
bucket := tx.RootBucket().Bucket(mainBucketName)
@ -242,6 +320,50 @@ func putMasterKeyParams(tx walletdb.Tx, pubParams, privParams []byte) error {
return nil
}
// fetchCoinTypeKeys loads the encrypted cointype keys which are in turn used to
// derive the extended keys for all accounts.
func fetchCoinTypeKeys(tx walletdb.Tx) ([]byte, []byte, error) {
bucket := tx.RootBucket().Bucket(mainBucketName)
coinTypePubKeyEnc := bucket.Get(coinTypePubKeyName)
if coinTypePubKeyEnc == nil {
str := "required encrypted cointype public key not stored in database"
return nil, nil, managerError(ErrDatabase, str, nil)
}
coinTypePrivKeyEnc := bucket.Get(coinTypePrivKeyName)
if coinTypePrivKeyEnc == nil {
str := "required encrypted cointype private key not stored in database"
return nil, nil, managerError(ErrDatabase, str, nil)
}
return coinTypePubKeyEnc, coinTypePrivKeyEnc, nil
}
// putCoinTypeKeys stores the encrypted cointype keys which are in turn used to
// derive the extended keys for all accounts. Either parameter can be nil in which
// case no value is written for the parameter.
func putCoinTypeKeys(tx walletdb.Tx, coinTypePubKeyEnc []byte, coinTypePrivKeyEnc []byte) error {
bucket := tx.RootBucket().Bucket(mainBucketName)
if coinTypePubKeyEnc != nil {
err := bucket.Put(coinTypePubKeyName, coinTypePubKeyEnc)
if err != nil {
str := "failed to store encrypted cointype public key"
return managerError(ErrDatabase, str, err)
}
}
if coinTypePrivKeyEnc != nil {
err := bucket.Put(coinTypePrivKeyName, coinTypePrivKeyEnc)
if err != nil {
str := "failed to store encrypted cointype private key"
return managerError(ErrDatabase, str, err)
}
}
return nil
}
// fetchCryptoKeys loads the encrypted crypto keys which are in turn used to
// protect the extended keys, imported keys, and scripts. Any of the returned
// values can be nil, but in practice only the crypto private and script keys
@ -455,6 +577,70 @@ func serializeBIP0044AccountRow(encryptedPubKey,
return rawData
}
// fetchAllAccounts loads information about all accounts from the database.
// The returned value is a slice of account numbers which can be used to load
// the respective account rows.
// TODO(tuxcanfly): Switch over to an iterator to support the maximum of 2^31-2 accounts
func fetchAllAccounts(tx walletdb.Tx) ([]uint32, error) {
bucket := tx.RootBucket().Bucket(acctBucketName)
var accounts []uint32
err := bucket.ForEach(func(k, v []byte) error {
// Skip buckets.
if v == nil {
return nil
}
accounts = append(accounts, binary.LittleEndian.Uint32(k))
return nil
})
return accounts, err
}
// fetchLastAccount retreives the last account from the database.
func fetchLastAccount(tx walletdb.Tx) (uint32, error) {
bucket := tx.RootBucket().Bucket(metaBucketName)
val := bucket.Get(lastAccountName)
if len(val) != 4 {
str := fmt.Sprintf("malformed metadata '%s' stored in database",
lastAccountName)
return 0, managerError(ErrDatabase, str, nil)
}
account := binary.LittleEndian.Uint32(val[0:4])
return account, nil
}
// fetchAccountName retreives the account name given an account number from
// the database.
func fetchAccountName(tx walletdb.Tx, account uint32) (string, error) {
bucket := tx.RootBucket().Bucket(acctIdIdxBucketName)
val := bucket.Get(uint32ToBytes(account))
if val == nil {
str := fmt.Sprintf("account %d not found", account)
return "", managerError(ErrAccountNotFound, str, nil)
}
offset := uint32(0)
nameLen := binary.LittleEndian.Uint32(val[offset : offset+4])
offset += 4
acctName := string(val[offset : offset+nameLen])
return acctName, nil
}
// fetchAccountByName retreives the account number given an account name
// from the database.
func fetchAccountByName(tx walletdb.Tx, name string) (uint32, error) {
bucket := tx.RootBucket().Bucket(acctNameIdxBucketName)
val := bucket.Get(stringToBytes(name))
if val == nil {
str := fmt.Sprintf("account name '%s' not found", name)
return 0, managerError(ErrAccountNotFound, str, nil)
}
return binary.LittleEndian.Uint32(val), nil
}
// fetchAccountInfo loads information about the passed account from the
// database.
func fetchAccountInfo(tx walletdb.Tx, account uint32) (interface{}, error) {
@ -481,6 +667,81 @@ func fetchAccountInfo(tx walletdb.Tx, account uint32) (interface{}, error) {
return nil, managerError(ErrDatabase, str, nil)
}
// deleteAccountNameIndex deletes the given key from the account name index of the database.
func deleteAccountNameIndex(tx walletdb.Tx, name string) error {
bucket := tx.RootBucket().Bucket(acctNameIdxBucketName)
// Delete the account name key
err := bucket.Delete(stringToBytes(name))
if err != nil {
str := fmt.Sprintf("failed to delete account name index key %s", name)
return managerError(ErrDatabase, str, err)
}
return nil
}
// deleteAccounIdIndex deletes the given key from the account id index of the database.
func deleteAccountIdIndex(tx walletdb.Tx, account uint32) error {
bucket := tx.RootBucket().Bucket(acctIdIdxBucketName)
// Delete the account id key
err := bucket.Delete(uint32ToBytes(account))
if err != nil {
str := fmt.Sprintf("failed to delete account id index key %d", account)
return managerError(ErrDatabase, str, err)
}
return nil
}
// putAccountNameIndex stores the given key to the account name index of the database.
func putAccountNameIndex(tx walletdb.Tx, account uint32, name string) error {
bucket := tx.RootBucket().Bucket(acctNameIdxBucketName)
// Write the account number keyed by the account name.
err := bucket.Put(stringToBytes(name), uint32ToBytes(account))
if err != nil {
str := fmt.Sprintf("failed to store account name index key %s", name)
return managerError(ErrDatabase, str, err)
}
return nil
}
// putAccountIdIndex stores the given key to the account id index of the database.
func putAccountIdIndex(tx walletdb.Tx, account uint32, name string) error {
bucket := tx.RootBucket().Bucket(acctIdIdxBucketName)
// Write the account number keyed by the account id.
err := bucket.Put(uint32ToBytes(account), stringToBytes(name))
if err != nil {
str := fmt.Sprintf("failed to store account id index key %s", name)
return managerError(ErrDatabase, str, err)
}
return nil
}
// putAddrAccountIndex stores the given key to the address account index of the database.
func putAddrAccountIndex(tx walletdb.Tx, account uint32, addrHash []byte) error {
bucket := tx.RootBucket().Bucket(addrAcctIdxBucketName)
// Write account keyed by address hash
err := bucket.Put(addrHash, uint32ToBytes(account))
if err != nil {
return nil
}
bucket, err = bucket.CreateBucketIfNotExists(uint32ToBytes(account))
if err != nil {
return err
}
// In account bucket, write a null value keyed by the address hash
err = bucket.Put(addrHash, nullVal)
if err != nil {
str := fmt.Sprintf("failed to store address account index key %s", addrHash)
return managerError(ErrDatabase, str, err)
}
return nil
}
// putAccountRow stores the provided account information to the database. This
// is used a common base for storing the various account types.
func putAccountRow(tx walletdb.Tx, account uint32, row *dbAccountRow) error {
@ -507,36 +768,30 @@ func putAccountInfo(tx walletdb.Tx, account uint32, encryptedPubKey,
acctType: actBIP0044,
rawData: rawData,
}
return putAccountRow(tx, account, &acctRow)
}
// fetchNumAccounts loads the number of accounts that have been created from
// the database.
func fetchNumAccounts(tx walletdb.Tx) (uint32, error) {
bucket := tx.RootBucket().Bucket(acctBucketName)
val := bucket.Get(acctNumAcctsName)
if val == nil {
str := "required num accounts not stored in database"
return 0, managerError(ErrDatabase, str, nil)
if err := putAccountRow(tx, account, &acctRow); err != nil {
return err
}
// Update account id index
if err := putAccountIdIndex(tx, account, name); err != nil {
return err
}
// Update account name index
if err := putAccountNameIndex(tx, account, name); err != nil {
return err
}
return binary.LittleEndian.Uint32(val), nil
return nil
}
// putNumAccounts stores the number of accounts that have been created to the
// database.
func putNumAccounts(tx walletdb.Tx, numAccounts uint32) error {
bucket := tx.RootBucket().Bucket(acctBucketName)
// putLastAccount stores the provided metadata - last account - to the database.
func putLastAccount(tx walletdb.Tx, account uint32) error {
bucket := tx.RootBucket().Bucket(metaBucketName)
var val [4]byte
binary.LittleEndian.PutUint32(val[:], numAccounts)
err := bucket.Put(acctNumAcctsName, val[:])
err := bucket.Put(lastAccountName, uint32ToBytes(account))
if err != nil {
str := "failed to store num accounts"
str := fmt.Sprintf("failed to update metadata '%s'", lastAccountName)
return managerError(ErrDatabase, str, err)
}
return nil
}
@ -547,7 +802,7 @@ func putNumAccounts(tx walletdb.Tx, numAccounts uint32) error {
// deserializeAddressRow deserializes the passed serialized address information.
// This is used as a common base for the various address types to deserialize
// the common parts.
func deserializeAddressRow(addressID, serializedAddress []byte) (*dbAddressRow, error) {
func deserializeAddressRow(serializedAddress []byte) (*dbAddressRow, error) {
// The serialized address format is:
// <addrType><account><addedTime><syncStatus><rawdata>
//
@ -557,8 +812,7 @@ func deserializeAddressRow(addressID, serializedAddress []byte) (*dbAddressRow,
// Given the above, the length of the entry must be at a minimum
// the constant value sizes.
if len(serializedAddress) < 18 {
str := fmt.Sprintf("malformed serialized address for key %s",
addressID)
str := "malformed serialized address"
return nil, managerError(ErrDatabase, str, nil)
}
@ -595,14 +849,13 @@ func serializeAddressRow(row *dbAddressRow) []byte {
// deserializeChainedAddress deserializes the raw data from the passed address
// row as a chained address.
func deserializeChainedAddress(addressID []byte, row *dbAddressRow) (*dbChainAddressRow, error) {
func deserializeChainedAddress(row *dbAddressRow) (*dbChainAddressRow, error) {
// The serialized chain address raw data format is:
// <branch><index>
//
// 4 bytes branch + 4 bytes address index
if len(row.rawData) != 8 {
str := fmt.Sprintf("malformed serialized chained address for "+
"key %s", addressID)
str := "malformed serialized chained address"
return nil, managerError(ErrDatabase, str, nil)
}
@ -631,7 +884,7 @@ func serializeChainedAddress(branch, index uint32) []byte {
// deserializeImportedAddress deserializes the raw data from the passed address
// row as an imported address.
func deserializeImportedAddress(addressID []byte, row *dbAddressRow) (*dbImportedAddressRow, error) {
func deserializeImportedAddress(row *dbAddressRow) (*dbImportedAddressRow, error) {
// The serialized imported address raw data format is:
// <encpubkeylen><encpubkey><encprivkeylen><encprivkey>
//
@ -641,8 +894,7 @@ func deserializeImportedAddress(addressID []byte, row *dbAddressRow) (*dbImporte
// Given the above, the length of the entry must be at a minimum
// the constant value sizes.
if len(row.rawData) < 8 {
str := fmt.Sprintf("malformed serialized imported address for "+
"key %s", addressID)
str := "malformed serialized imported address"
return nil, managerError(ErrDatabase, str, nil)
}
@ -684,7 +936,7 @@ func serializeImportedAddress(encryptedPubKey, encryptedPrivKey []byte) []byte {
// deserializeScriptAddress deserializes the raw data from the passed address
// row as a script address.
func deserializeScriptAddress(addressID []byte, row *dbAddressRow) (*dbScriptAddressRow, error) {
func deserializeScriptAddress(row *dbAddressRow) (*dbScriptAddressRow, error) {
// The serialized script address raw data format is:
// <encscripthashlen><encscripthash><encscriptlen><encscript>
//
@ -694,8 +946,7 @@ func deserializeScriptAddress(addressID []byte, row *dbAddressRow) (*dbScriptAdd
// Given the above, the length of the entry must be at a minimum
// the constant value sizes.
if len(row.rawData) < 8 {
str := fmt.Sprintf("malformed serialized script address for "+
"key %s", addressID)
str := "malformed serialized script address"
return nil, managerError(ErrDatabase, str, nil)
}
@ -737,7 +988,7 @@ func serializeScriptAddress(encryptedHash, encryptedScript []byte) []byte {
}
// fetchAddressUsed returns true if the provided address hash was flagged as used.
func fetchAddressUsed(tx walletdb.Tx, addrHash [32]byte) bool {
func fetchAddressUsed(tx walletdb.Tx, addrHash []byte) bool {
bucket := tx.RootBucket().Bucket(usedAddrBucketName)
val := bucket.Get(addrHash[:])
@ -747,32 +998,33 @@ func fetchAddressUsed(tx walletdb.Tx, addrHash [32]byte) bool {
return false
}
// fetchAddress loads address information for the provided address id from
// the database. The returned value is one of the address rows for the specific
// address type. The caller should use type assertions to ascertain the type.
func fetchAddress(tx walletdb.Tx, addressID []byte) (interface{}, error) {
// fetchAddressByHash loads address information for the provided address hash
// from the database. The returned value is one of the address rows for the
// specific address type. The caller should use type assertions to ascertain
// the type. The caller should prefix the error message with the address hash
// which caused the failure.
func fetchAddressByHash(tx walletdb.Tx, addrHash []byte) (interface{}, error) {
bucket := tx.RootBucket().Bucket(addrBucketName)
addrHash := fastsha256.Sum256(addressID)
serializedRow := bucket.Get(addrHash[:])
if serializedRow == nil {
str := "address not found"
return nil, managerError(ErrAddressNotFound, str, nil)
}
row, err := deserializeAddressRow(addressID, serializedRow)
row, err := deserializeAddressRow(serializedRow)
if err != nil {
return nil, err
}
row.used = fetchAddressUsed(tx, addrHash)
row.used = fetchAddressUsed(tx, addrHash[:])
switch row.addrType {
case adtChain:
return deserializeChainedAddress(addressID, row)
return deserializeChainedAddress(row)
case adtImport:
return deserializeImportedAddress(addressID, row)
return deserializeImportedAddress(row)
case adtScript:
return deserializeScriptAddress(addressID, row)
return deserializeScriptAddress(row)
}
str := fmt.Sprintf("unsupported address type '%d'", row.addrType)
@ -796,6 +1048,16 @@ func markAddressUsed(tx walletdb.Tx, addressID []byte) error {
return nil
}
// fetchAddress loads address information for the provided address id from the
// database. The returned value is one of the address rows for the specific
// address type. The caller should use type assertions to ascertain the type.
// The caller should prefix the error message with the address which caused the
// failure.
func fetchAddress(tx walletdb.Tx, addressID []byte) (interface{}, error) {
addrHash := fastsha256.Sum256(addressID)
return fetchAddressByHash(tx, addrHash[:])
}
// putAddress stores the provided address information to the database. This
// is used a common base for storing the various address types.
func putAddress(tx walletdb.Tx, addressID []byte, row *dbAddressRow) error {
@ -810,8 +1072,8 @@ func putAddress(tx walletdb.Tx, addressID []byte, row *dbAddressRow) error {
str := fmt.Sprintf("failed to store address %x", addressID)
return managerError(ErrDatabase, str, err)
}
return nil
// Update address account index
return putAddrAccountIndex(tx, row.account, addrHash[:])
}
// putChainedAddress stores the provided chained address information to the
@ -914,9 +1176,64 @@ func existsAddress(tx walletdb.Tx, addressID []byte) bool {
return bucket.Get(addrHash[:]) != nil
}
// fetchAddrAccount returns the account to which the given address belongs to.
// It looks up the account using the addracctidx index which maps the address
// hash to its corresponding account id.
func fetchAddrAccount(tx walletdb.Tx, addressID []byte) (uint32, error) {
bucket := tx.RootBucket().Bucket(addrAcctIdxBucketName)
addrHash := fastsha256.Sum256(addressID)
val := bucket.Get(addrHash[:])
if val == nil {
str := "address not found"
return 0, managerError(ErrAddressNotFound, str, nil)
}
return binary.LittleEndian.Uint32(val), nil
}
// fetchAccountAddresses loads information about addresses of an account from the database.
// The returned value is a slice address rows for each specific address type.
// The caller should use type assertions to ascertain the types.
func fetchAccountAddresses(tx walletdb.Tx, account uint32) ([]interface{}, error) {
bucket := tx.RootBucket().Bucket(addrAcctIdxBucketName).
Bucket(uint32ToBytes(account))
// if index bucket is missing the account, there hasn't been any address
// entries yet
if bucket == nil {
return nil, nil
}
var addrs []interface{}
err := bucket.ForEach(func(k, v []byte) error {
// Skip buckets.
if v == nil {
return nil
}
addrRow, err := fetchAddressByHash(tx, k)
if err != nil {
if merr, ok := err.(*ManagerError); ok {
desc := fmt.Sprintf("failed to fetch address hash '%s': %v",
k, merr.Description)
merr.Description = desc
return merr
}
return err
}
addrs = append(addrs, addrRow)
return nil
})
if err != nil {
return nil, maybeConvertDbError(err)
}
return addrs, nil
}
// fetchAllAddresses loads information about all addresses from the database.
// The returned value is a slice of address rows for each specific address type.
// The caller should use type assertions to ascertain the types.
// TODO(tuxcanfly): Switch over to an iterator to support the maximum of 2^62 - 2^32 - 2^31 + 2 addrs
func fetchAllAddresses(tx walletdb.Tx) ([]interface{}, error) {
bucket := tx.RootBucket().Bucket(addrBucketName)
@ -929,23 +1246,12 @@ func fetchAllAddresses(tx walletdb.Tx) ([]interface{}, error) {
// Deserialize the address row first to determine the field
// values.
row, err := deserializeAddressRow(k, v)
if err != nil {
return err
}
var addrRow interface{}
switch row.addrType {
case adtChain:
addrRow, err = deserializeChainedAddress(k, row)
case adtImport:
addrRow, err = deserializeImportedAddress(k, row)
case adtScript:
addrRow, err = deserializeScriptAddress(k, row)
default:
str := fmt.Sprintf("unsupported address type '%d'",
row.addrType)
return managerError(ErrDatabase, str, nil)
addrRow, err := fetchAddressByHash(tx, k)
if merr, ok := err.(*ManagerError); ok {
desc := fmt.Sprintf("failed to fetch address hash '%s': %v",
k, merr.Description)
merr.Description = desc
return merr
}
if err != nil {
return err
@ -985,12 +1291,16 @@ func deletePrivateKeys(tx walletdb.Tx) error {
str := "failed to delete crypto script key"
return managerError(ErrDatabase, str, err)
}
if err := bucket.Delete(coinTypePrivKeyName); err != nil {
str := "failed to delete cointype private key"
return managerError(ErrDatabase, str, err)
}
// Delete the account extended private key for all accounts.
bucket = tx.RootBucket().Bucket(acctBucketName)
err := bucket.ForEach(func(k, v []byte) error {
// Skip buckets.
if v == nil || bytes.Equal(k, acctNumAcctsName) {
if v == nil {
return nil
}
@ -1036,14 +1346,14 @@ func deletePrivateKeys(tx walletdb.Tx) error {
// Deserialize the address row first to determine the field
// values.
row, err := deserializeAddressRow(k, v)
row, err := deserializeAddressRow(v)
if err != nil {
return err
}
switch row.addrType {
case adtImport:
irow, err := deserializeImportedAddress(k, row)
irow, err := deserializeImportedAddress(row)
if err != nil {
return err
}
@ -1059,7 +1369,7 @@ func deletePrivateKeys(tx walletdb.Tx) error {
}
case adtScript:
srow, err := deserializeScriptAddress(k, row)
srow, err := deserializeScriptAddress(row)
if err != nil {
return err
}
@ -1283,6 +1593,28 @@ func createManagerNS(namespace walletdb.Namespace) error {
return managerError(ErrDatabase, str, err)
}
_, err = rootBucket.CreateBucket(acctNameIdxBucketName)
if err != nil {
str := "failed to create an account name index bucket"
return managerError(ErrDatabase, str, err)
}
_, err = rootBucket.CreateBucket(acctIdIdxBucketName)
if err != nil {
str := "failed to create an account id index bucket"
return managerError(ErrDatabase, str, err)
}
_, err = rootBucket.CreateBucket(metaBucketName)
if err != nil {
str := "failed to create a meta bucket"
return managerError(ErrDatabase, str, err)
}
if err := putLastAccount(tx, DefaultAccountNum); err != nil {
return err
}
if err := putManagerVersion(tx, latestMgrVersion); err != nil {
return err
}
@ -1334,14 +1666,12 @@ func upgradeToVersion2(namespace walletdb.Namespace) error {
// upgradeManager upgrades the data in the provided manager namespace to newer
// versions as neeeded.
func upgradeManager(namespace walletdb.Namespace) error {
// Get the current version.
func upgradeManager(namespace walletdb.Namespace, pubPassPhrase []byte, config *Options) error {
var version uint32
err := namespace.View(func(tx walletdb.Tx) error {
mainBucket := tx.RootBucket().Bucket(mainBucketName)
verBytes := mainBucket.Get(mgrVersionName)
version = binary.LittleEndian.Uint32(verBytes)
return nil
var err error
version, err = fetchManagerVersion(tx)
return err
})
if err != nil {
str := "failed to fetch version for update"
@ -1388,6 +1718,29 @@ func upgradeManager(namespace walletdb.Namespace) error {
version = 2
}
if version < 3 {
if config.ObtainSeed == nil || config.ObtainPrivatePass == nil {
str := "failed to obtain seed and private passphrase required for upgrade"
return managerError(ErrDatabase, str, err)
}
seed, err := config.ObtainSeed()
if err != nil {
return err
}
privPassPhrase, err := config.ObtainPrivatePass()
if err != nil {
return err
}
// Upgrade from version 2 to 3.
if err := upgradeToVersion3(namespace, seed, privPassPhrase, pubPassPhrase); err != nil {
return err
}
// The manager is now at version 3.
version = 3
}
// Ensure the manager is upraded to the latest version. This check is
// to intentionally cause a failure if the manager version is updated
// without writing code to handle the upgrade.
@ -1400,3 +1753,115 @@ func upgradeManager(namespace walletdb.Namespace) error {
return nil
}
// upgradeToVersion3 upgrades the database from version 2 to version 3
// The following buckets were introduced in version 3 to support account names:
// * acctNameIdxBucketName
// * acctIdIdxBucketName
// * metaBucketName
func upgradeToVersion3(namespace walletdb.Namespace, seed, privPassPhrase, pubPassPhrase []byte) error {
err := namespace.Update(func(tx walletdb.Tx) error {
currentMgrVersion := uint32(3)
rootBucket := tx.RootBucket()
woMgr, err := loadManager(namespace, pubPassPhrase, &chaincfg.SimNetParams, nil)
if err != nil {
return err
}
defer woMgr.Close()
err = woMgr.Unlock(privPassPhrase)
if err != nil {
return err
}
// Derive the master extended key from the seed.
root, err := hdkeychain.NewMaster(seed)
if err != nil {
str := "failed to derive master extended key"
return managerError(ErrKeyChain, str, err)
}
// Derive the cointype key according to BIP0044.
coinTypeKeyPriv, err := deriveCoinTypeKey(root, chaincfg.SimNetParams.HDCoinType)
if err != nil {
str := "failed to derive cointype extended key"
return managerError(ErrKeyChain, str, err)
}
cryptoKeyPub := woMgr.cryptoKeyPub
cryptoKeyPriv := woMgr.cryptoKeyPriv
// Encrypt the cointype keys with the associated crypto keys.
coinTypeKeyPub, err := coinTypeKeyPriv.Neuter()
if err != nil {
str := "failed to convert cointype private key"
return managerError(ErrKeyChain, str, err)
}
coinTypePubEnc, err := cryptoKeyPub.Encrypt([]byte(coinTypeKeyPub.String()))
if err != nil {
str := "failed to encrypt cointype public key"
return managerError(ErrCrypto, str, err)
}
coinTypePrivEnc, err := cryptoKeyPriv.Encrypt([]byte(coinTypeKeyPriv.String()))
if err != nil {
str := "failed to encrypt cointype private key"
return managerError(ErrCrypto, str, err)
}
// Save the encrypted cointype keys to the database.
err = putCoinTypeKeys(tx, coinTypePubEnc, coinTypePrivEnc)
if err != nil {
return err
}
_, err = rootBucket.CreateBucket(acctNameIdxBucketName)
if err != nil {
str := "failed to create an account name index bucket"
return managerError(ErrDatabase, str, err)
}
_, err = rootBucket.CreateBucket(acctIdIdxBucketName)
if err != nil {
str := "failed to create an account id index bucket"
return managerError(ErrDatabase, str, err)
}
_, err = rootBucket.CreateBucket(metaBucketName)
if err != nil {
str := "failed to create a meta bucket"
return managerError(ErrDatabase, str, err)
}
// Initialize metadata for all keys
if err := putLastAccount(tx, DefaultAccountNum); err != nil {
return err
}
// Update default account indexes
if err := putAccountIdIndex(tx, DefaultAccountNum, DefaultAccountName); err != nil {
return err
}
if err := putAccountNameIndex(tx, DefaultAccountNum, DefaultAccountName); err != nil {
return err
}
// Update imported account indexes
if err := putAccountIdIndex(tx, ImportedAddrAccount, ImportedAddrAccountName); err != nil {
return err
}
if err := putAccountNameIndex(tx, ImportedAddrAccount, ImportedAddrAccountName); err != nil {
return err
}
// Write current manager version
if err := putManagerVersion(tx, currentMgrVersion); err != nil {
return err
}
// Save "" alias for default account name for backward compat
return putAccountNameIndex(tx, DefaultAccountNum, "")
})
if err != nil {
return maybeConvertDbError(err)
}
return nil
}

View file

@ -114,8 +114,11 @@ const (
// the account manager.
ErrAccountNotFound
// ErrDuplicate indicates that an address already exists.
ErrDuplicate
// ErrDuplicateAddress indicates an address already exists.
ErrDuplicateAddress
// ErrDuplicateAccount indicates an account already exists.
ErrDuplicateAccount
// ErrTooManyAddresses indicates that more than the maximum allowed number of
// addresses per account have been requested.
@ -146,7 +149,8 @@ var errorCodeStrings = map[ErrorCode]string{
ErrInvalidAccount: "ErrInvalidAccount",
ErrAddressNotFound: "ErrAddressNotFound",
ErrAccountNotFound: "ErrAccountNotFound",
ErrDuplicate: "ErrDuplicate",
ErrDuplicateAddress: "ErrDuplicateAddress",
ErrDuplicateAccount: "ErrDuplicateAccount",
ErrTooManyAddresses: "ErrTooManyAddresses",
ErrWrongPassphrase: "ErrWrongPassphrase",
ErrWrongNet: "ErrWrongNet",

View file

@ -43,7 +43,8 @@ func TestErrorCodeStringer(t *testing.T) {
{waddrmgr.ErrInvalidAccount, "ErrInvalidAccount"},
{waddrmgr.ErrAddressNotFound, "ErrAddressNotFound"},
{waddrmgr.ErrAccountNotFound, "ErrAccountNotFound"},
{waddrmgr.ErrDuplicate, "ErrDuplicate"},
{waddrmgr.ErrDuplicateAddress, "ErrDuplicateAddress"},
{waddrmgr.ErrDuplicateAccount, "ErrDuplicateAccount"},
{waddrmgr.ErrTooManyAddresses, "ErrTooManyAddresses"},
{waddrmgr.ErrWrongPassphrase, "ErrWrongPassphrase"},
{waddrmgr.ErrWrongNet, "ErrWrongNet"},

View file

@ -51,8 +51,14 @@ const (
// fit into that model.
ImportedAddrAccount = MaxAccountNum + 1 // 2^31 - 1
// defaultAccountNum is the number of the default account.
defaultAccountNum = 0
// ImportedAddrAccountName is the name of the imported account.
ImportedAddrAccountName = "imported"
// DefaultAccountNum is the number of the default account.
DefaultAccountNum = 0
// DefaultAccountName is the name of the default account.
DefaultAccountName = "default"
// The hierarchy described by BIP0043 is:
// m/<purpose>'/*
@ -82,11 +88,30 @@ const (
saltSize = 32
)
var (
// reservedAccountNames is a set of account names reserved for internal
// purposes
reservedAccountNames = map[string]struct{}{
"*": struct{}{},
DefaultAccountName: struct{}{},
ImportedAddrAccountName: struct{}{},
}
)
// Options is used to hold the optional parameters passed to Create or Load.
type Options struct {
ScryptN int
ScryptR int
ScryptP int
// ObtainSeed is a callback function that is potentially invoked during
// upgrades. It is intended to be used to request the wallet seed
// from the user (or any other mechanism the caller deems fit).
ObtainSeed ObtainUserInputFunc
// ObtainPrivatePass is a callback function that is potentially invoked
// during upgrades. It is intended to be used to request the wallet
// private passphrase from the user (or any other mechanism the caller
// deems fit).
ObtainPrivatePass ObtainUserInputFunc
}
// defaultConfig is an instance of the Options struct initialized with default
@ -615,6 +640,12 @@ func (m *Manager) loadAndCacheAddress(address btcutil.Address) (ManagedAddress,
return err
})
if err != nil {
if merr, ok := err.(*ManagerError); ok {
desc := fmt.Sprintf("failed to fetch address '%s': %v",
address.ScriptAddress(), merr.Description)
merr.Description = desc
return nil, merr
}
return nil, maybeConvertDbError(err)
}
@ -655,6 +686,20 @@ func (m *Manager) Address(address btcutil.Address) (ManagedAddress, error) {
return m.loadAndCacheAddress(address)
}
// AddrAccount returns the account to which the given address belongs.
func (m *Manager) AddrAccount(address btcutil.Address) (uint32, error) {
var account uint32
err := m.namespace.View(func(tx walletdb.Tx) error {
var err error
account, err = fetchAddrAccount(tx, address.ScriptAddress())
return err
})
if err != nil {
return 0, maybeConvertDbError(err)
}
return account, nil
}
// ChangePassphrase changes either the public or private passphrase to the
// provided value depending on the private flag. In order to change the private
// password, the address manager must not be watching-only.
@ -963,7 +1008,7 @@ func (m *Manager) ImportPrivateKey(wif *btcutil.WIF, bs *BlockStamp) (ManagedPub
if alreadyExists {
str := fmt.Sprintf("address for public key %x already exists",
serializedPubKey)
return nil, managerError(ErrDuplicate, str, nil)
return nil, managerError(ErrDuplicateAddress, str, nil)
}
// Encrypt public key.
@ -1067,7 +1112,7 @@ func (m *Manager) ImportScript(script []byte, bs *BlockStamp) (ManagedScriptAddr
if alreadyExists {
str := fmt.Sprintf("address for script hash %x already exists",
scriptHash)
return nil, managerError(ErrDuplicate, str, nil)
return nil, managerError(ErrDuplicateAddress, str, nil)
}
// Encrypt the script hash using the crypto public key so it is
@ -1176,6 +1221,29 @@ func (m *Manager) Lock() error {
return nil
}
// lookupAccount loads account number stored in the manager for the given
// account name
//
// This function MUST be called with the manager lock held for reads.
func (m *Manager) lookupAccount(name string) (uint32, error) {
var account uint32
err := m.namespace.View(func(tx walletdb.Tx) error {
var err error
account, err = fetchAccountByName(tx, name)
return err
})
return account, err
}
// LookupAccount loads account number stored in the manager for the given
// account name
func (m *Manager) LookupAccount(name string) (uint32, error) {
m.mtx.RLock()
defer m.mtx.RUnlock()
return m.lookupAccount(name)
}
// Unlock derives the master private key from the specified passphrase. An
// invalid passphrase will return an error. Otherwise, the derived secret key
// is stored in memory until the address manager is locked. Any failures that
@ -1543,6 +1611,254 @@ func (m *Manager) LastInternalAddress(account uint32) (ManagedAddress, error) {
return acctInfo.lastInternalAddr, nil
}
// ValidateAccountName validates the given account name and returns an error, if any.
func ValidateAccountName(name string) error {
if name == "" {
str := "invalid account name, cannot be blank"
return managerError(ErrInvalidAccount, str, nil)
}
if _, ok := reservedAccountNames[name]; ok {
str := "reserved account name"
return managerError(ErrInvalidAccount, str, nil)
}
return nil
}
// NewAccount creates and returns a new account stored in the manager based
// on the given account name. If an account with the same name already exists,
// ErrDuplicateAccount will be returned. Since creating a new account requires
// access to the cointype keys (from which extended account keys are derived),
// it requires the manager to be unlocked.
func (m *Manager) NewAccount(name string) (uint32, error) {
if m.watchingOnly {
return 0, managerError(ErrWatchingOnly, errWatchingOnly, nil)
}
m.mtx.Lock()
defer m.mtx.Unlock()
if m.locked {
return 0, managerError(ErrLocked, errLocked, nil)
}
// Validate account name
if err := ValidateAccountName(name); err != nil {
return 0, err
}
// Check that account with the same name does not exist
_, err := m.lookupAccount(name)
if err == nil {
str := fmt.Sprintf("account with the same name already exists")
return 0, managerError(ErrDuplicateAccount, str, err)
}
var account uint32
var coinTypePrivEnc []byte
// Fetch latest account, and create a new account in the same transaction
err = m.namespace.Update(func(tx walletdb.Tx) error {
var err error
// Fetch the latest account number to generate the next account number
account, err = fetchLastAccount(tx)
if err != nil {
return err
}
account++
// Fetch the cointype key which will be used to derive the next account
// extended keys
_, coinTypePrivEnc, err = fetchCoinTypeKeys(tx)
if err != nil {
return err
}
// Decrypt the cointype key
serializedKeyPriv, err := m.cryptoKeyPriv.Decrypt(coinTypePrivEnc)
if err != nil {
str := fmt.Sprintf("failed to decrypt cointype serialized private key")
return managerError(ErrLocked, str, err)
}
coinTypeKeyPriv, err := hdkeychain.NewKeyFromString(string(serializedKeyPriv))
zero.Bytes(serializedKeyPriv)
if err != nil {
str := fmt.Sprintf("failed to create cointype extended private key")
return managerError(ErrKeyChain, str, err)
}
// Derive the account key using the cointype key
acctKeyPriv, err := deriveAccountKey(coinTypeKeyPriv, account)
coinTypeKeyPriv.Zero()
if err != nil {
str := "failed to convert private key for account"
return managerError(ErrKeyChain, str, err)
}
acctKeyPub, err := acctKeyPriv.Neuter()
if err != nil {
str := "failed to convert public key for account"
return managerError(ErrKeyChain, str, err)
}
// Encrypt the default account keys with the associated crypto keys.
acctPubEnc, err := m.cryptoKeyPub.Encrypt([]byte(acctKeyPub.String()))
if err != nil {
str := "failed to encrypt public key for account"
return managerError(ErrCrypto, str, err)
}
acctPrivEnc, err := m.cryptoKeyPriv.Encrypt([]byte(acctKeyPriv.String()))
if err != nil {
str := "failed to encrypt private key for account"
return managerError(ErrCrypto, str, err)
}
// We have the encrypted account extended keys, so save them to the
// database
err = putAccountInfo(tx, account, acctPubEnc, acctPrivEnc, 0, 0, name)
if err != nil {
return err
}
// Save last account metadata
if err := putLastAccount(tx, account); err != nil {
return err
}
return nil
})
return account, err
}
// RenameAccount renames an account stored in the manager based on the
// given account number with the given name. If an account with the same name
// already exists, ErrDuplicateAccount will be returned.
func (m *Manager) RenameAccount(account uint32, name string) error {
m.mtx.Lock()
defer m.mtx.Unlock()
// Check that account with the new name does not exist
_, err := m.lookupAccount(name)
if err == nil {
str := fmt.Sprintf("account with the same name already exists")
return managerError(ErrDuplicateAccount, str, err)
}
// Validate account name
if err := ValidateAccountName(name); err != nil {
return err
}
var rowInterface interface{}
err = m.namespace.Update(func(tx walletdb.Tx) error {
var err error
rowInterface, err = fetchAccountInfo(tx, account)
if err != nil {
return err
}
// Ensure the account type is a BIP0044 account.
row, ok := rowInterface.(*dbBIP0044AccountRow)
if !ok {
str := fmt.Sprintf("unsupported account type %T", row)
err = managerError(ErrDatabase, str, nil)
}
// Remove the old name key from the accout id index
if err = deleteAccountIdIndex(tx, account); err != nil {
return err
}
// Remove the old name key from the accout name index
if err = deleteAccountNameIndex(tx, row.name); err != nil {
return err
}
err = putAccountInfo(tx, account, row.pubKeyEncrypted,
row.privKeyEncrypted, row.nextExternalIndex, row.nextInternalIndex, name)
return err
})
return err
}
// AccountName returns the account name for the given account number
// stored in the manager.
func (m *Manager) AccountName(account uint32) (string, error) {
m.mtx.Lock()
defer m.mtx.Unlock()
var acctName string
err := m.namespace.View(func(tx walletdb.Tx) error {
var err error
acctName, err = fetchAccountName(tx, account)
return err
})
if err != nil {
return "", err
}
return acctName, nil
}
// AllAccounts returns a slice of all the accounts stored in the manager.
func (m *Manager) AllAccounts() ([]uint32, error) {
m.mtx.Lock()
defer m.mtx.Unlock()
var accounts []uint32
err := m.namespace.View(func(tx walletdb.Tx) error {
var err error
accounts, err = fetchAllAccounts(tx)
return err
})
if err != nil {
return nil, err
}
return accounts, nil
}
// LastAccount returns the last account stored in the manager.
func (m *Manager) LastAccount() (uint32, error) {
m.mtx.Lock()
defer m.mtx.Unlock()
var account uint32
err := m.namespace.View(func(tx walletdb.Tx) error {
var err error
account, err = fetchLastAccount(tx)
return err
})
return account, err
}
// AllAccountAddresses returns a slice of addresses of an account stored in the manager.
func (m *Manager) AllAccountAddresses(account uint32) ([]ManagedAddress, error) {
m.mtx.Lock()
defer m.mtx.Unlock()
// Load the raw address information from the database.
var rowInterfaces []interface{}
err := m.namespace.View(func(tx walletdb.Tx) error {
var err error
rowInterfaces, err = fetchAccountAddresses(tx, account)
return err
})
if err != nil {
return nil, err
}
addrs := make([]ManagedAddress, 0, len(rowInterfaces))
for _, rowInterface := range rowInterfaces {
// Create a new managed address for the specific type of address
// based on type.
managedAddr, err := m.rowInterfaceToManaged(rowInterface)
if err != nil {
return nil, err
}
addrs = append(addrs, managedAddr)
}
return addrs, nil
}
// ActiveAccountAddresses returns a slice of active addresses of an account
// stored in the manager.
// TODO(tuxcanfly): actually return only active addresses
func (m *Manager) ActiveAccountAddresses(account uint32) ([]ManagedAddress, error) {
return m.AllAccountAddresses(account)
}
// AllActiveAddresses returns a slice of all addresses stored in the manager.
func (m *Manager) AllActiveAddresses() ([]btcutil.Address, error) {
m.mtx.Lock()
@ -1667,26 +1983,20 @@ func newManager(namespace walletdb.Namespace, chainParams *chaincfg.Params,
}
}
// deriveAccountKey derives the extended key for an account according to the
// hierarchy described by BIP0044 given the master node.
// deriveCoinTypeKey derives the cointype key which can be used to derive the
// extended key for an account according to the hierarchy described by BIP0044
// given the coin type key.
//
// In particular this is the hierarchical deterministic extended key path:
// m/44'/<coin type>'/<account>'
func deriveAccountKey(masterNode *hdkeychain.ExtendedKey, coinType uint32,
account uint32) (*hdkeychain.ExtendedKey, error) {
// m/44'/<coin type>'
func deriveCoinTypeKey(masterNode *hdkeychain.ExtendedKey,
coinType uint32) (*hdkeychain.ExtendedKey, error) {
// Enforce maximum coin type.
if coinType > maxCoinType {
err := managerError(ErrCoinTypeTooHigh, errCoinTypeTooHigh, nil)
return nil, err
}
// Enforce maximum account number.
if account > MaxAccountNum {
err := managerError(ErrAccountNumTooHigh, errAcctTooHigh, nil)
return nil, err
}
// The hierarchy described by BIP0043 is:
// m/<purpose>'/*
// This is further extended by BIP0044 to:
@ -1706,6 +2016,22 @@ func deriveAccountKey(masterNode *hdkeychain.ExtendedKey, coinType uint32,
return nil, err
}
return coinTypeKey, nil
}
// deriveAccountKey derives the extended key for an account according to the
// hierarchy described by BIP0044 given the master node.
//
// In particular this is the hierarchical deterministic extended key path:
// m/44'/<coin type>'/<account>'
func deriveAccountKey(coinTypeKey *hdkeychain.ExtendedKey,
account uint32) (*hdkeychain.ExtendedKey, error) {
// Enforce maximum account number.
if account > MaxAccountNum {
err := managerError(ErrAccountNumTooHigh, errAcctTooHigh, nil)
return nil, err
}
// Derive the account key as a child of the coin type key.
return coinTypeKey.Child(account + hdkeychain.HardenedKeyStart)
}
@ -1858,7 +2184,7 @@ func Open(namespace walletdb.Namespace, pubPassphrase []byte, chainParams *chain
}
// Upgrade the manager to the latest version as needed.
if err := upgradeManager(namespace); err != nil {
if err := upgradeManager(namespace, pubPassphrase, config); err != nil {
return nil, err
}
@ -1916,8 +2242,15 @@ func Create(namespace walletdb.Namespace, seed, pubPassphrase, privPassphrase []
return nil, managerError(ErrKeyChain, str, err)
}
// Derive the cointype key according to BIP0044.
coinTypeKeyPriv, err := deriveCoinTypeKey(root, chainParams.HDCoinType)
if err != nil {
str := "failed to derive cointype extended key"
return nil, managerError(ErrKeyChain, str, err)
}
// Derive the account key for the first account according to BIP0044.
acctKeyPriv, err := deriveAccountKey(root, chainParams.HDCoinType, 0)
acctKeyPriv, err := deriveAccountKey(coinTypeKeyPriv, 0)
if err != nil {
// The seed is unusable if the any of the children in the
// required hierarchy can't be derived due to invalid child.
@ -2010,6 +2343,23 @@ func Create(namespace walletdb.Namespace, seed, pubPassphrase, privPassphrase []
return nil, managerError(ErrCrypto, str, err)
}
// Encrypt the cointype keys with the associated crypto keys.
coinTypeKeyPub, err := coinTypeKeyPriv.Neuter()
if err != nil {
str := "failed to convert cointype private key"
return nil, managerError(ErrKeyChain, str, err)
}
coinTypePubEnc, err := cryptoKeyPub.Encrypt([]byte(coinTypeKeyPub.String()))
if err != nil {
str := "failed to encrypt cointype public key"
return nil, managerError(ErrCrypto, str, err)
}
coinTypePrivEnc, err := cryptoKeyPriv.Encrypt([]byte(coinTypeKeyPriv.String()))
if err != nil {
str := "failed to encrypt cointype private key"
return nil, managerError(ErrCrypto, str, err)
}
// Encrypt the default account keys with the associated crypto keys.
acctPubEnc, err := cryptoKeyPub.Encrypt([]byte(acctKeyPub.String()))
if err != nil {
@ -2048,6 +2398,12 @@ func Create(namespace walletdb.Namespace, seed, pubPassphrase, privPassphrase []
return err
}
// Save the encrypted cointype keys to the database.
err = putCoinTypeKeys(tx, coinTypePubEnc, coinTypePrivEnc)
if err != nil {
return err
}
// Save the fact this is not a watching-only address manager to
// the database.
err = putWatchingOnly(tx, false)
@ -2071,24 +2427,33 @@ func Create(namespace walletdb.Namespace, seed, pubPassphrase, privPassphrase []
return err
}
// Save the information for the default account to the database.
err = putAccountInfo(tx, defaultAccountNum, acctPubEnc,
acctPrivEnc, 0, 0, "")
// Save the information for the imported account to the database.
err = putAccountInfo(tx, ImportedAddrAccount, nil,
nil, 0, 0, ImportedAddrAccountName)
if err != nil {
return err
}
return putNumAccounts(tx, 1)
// Save the information for the default account to the database.
err = putAccountInfo(tx, DefaultAccountNum, acctPubEnc,
acctPrivEnc, 0, 0, DefaultAccountName)
if err != nil {
return err
}
// Save "" alias for default account name for backward compat
return putAccountNameIndex(tx, DefaultAccountNum, "")
})
if err != nil {
return nil, maybeConvertDbError(err)
}
// The new address manager is locked by default, so clear the master,
// crypto private, and crypto script keys from memory.
// crypto private, crypto script and cointype keys from memory.
masterKeyPriv.Zero()
cryptoKeyPriv.Zero()
cryptoKeyScript.Zero()
coinTypeKeyPriv.Zero()
return newManager(namespace, chainParams, masterKeyPub, masterKeyPriv,
cryptoKeyPub, cryptoKeyPrivEnc, cryptoKeyScriptEnc, syncInfo,
config, privPassphraseSalt), nil

View file

@ -1158,6 +1158,221 @@ func testChangePassphrase(tc *testContext) bool {
return true
}
// testNewAccount tests the new account creation func of the address manager works
// as expected.
func testNewAccount(tc *testContext) bool {
if tc.watchingOnly {
// Creating new accounts in watching-only mode should return ErrWatchingOnly
_, err := tc.manager.NewAccount("test")
if !checkManagerError(tc.t, "Create account in watching-only mode", err,
waddrmgr.ErrWatchingOnly) {
tc.manager.Close()
return false
}
return true
}
// Creating new accounts when wallet is locked should return ErrLocked
_, err := tc.manager.NewAccount("test")
if !checkManagerError(tc.t, "Create account when wallet is locked", err,
waddrmgr.ErrLocked) {
tc.manager.Close()
return false
}
// Unlock the wallet to decrypt cointype keys required
// to derive account keys
if err := tc.manager.Unlock(privPassphrase); err != nil {
tc.t.Errorf("Unlock: unexpected error: %v", err)
return false
}
tc.unlocked = true
// Get the next account number
expectedAccount := tc.account + 1
if !tc.create {
// Existing wallet manager, so it already has "account-1",
// so increment the expected account number
expectedAccount++
}
// Create accounts with names "account-1", "account-2", etc
testName := fmt.Sprintf("account-%d", expectedAccount)
account, err := tc.manager.NewAccount(testName)
if err != nil {
tc.t.Errorf("NewAccount: unexpected error: %v", err)
return false
}
if account != expectedAccount {
tc.t.Errorf("NewAccount "+
"account mismatch -- got %d, "+
"want %d", account, expectedAccount)
return false
}
// Test duplicate account name error
_, err = tc.manager.NewAccount(testName)
wantErrCode := waddrmgr.ErrDuplicateAccount
if !checkManagerError(tc.t, testName, err, wantErrCode) {
return false
}
// Test account name validation
testName = "*"
_, err = tc.manager.NewAccount(testName)
wantErrCode = waddrmgr.ErrInvalidAccount
if !checkManagerError(tc.t, testName, err, wantErrCode) {
return false
}
return true
}
// testLookupAccount tests the basic account lookup func of the address manager works
// as expected.
func testLookupAccount(tc *testContext) bool {
// Lookup accounts created earlier in testNewAccount
expectedAccounts := map[string]uint32{
waddrmgr.DefaultAccountName: waddrmgr.DefaultAccountNum,
"account-1": 1,
waddrmgr.ImportedAddrAccountName: waddrmgr.ImportedAddrAccount,
}
if !tc.create {
// Existing wallet manager will have 2 accounts
expectedAccounts["account-2"] = 2
}
for acctName, expectedAccount := range expectedAccounts {
account, err := tc.manager.LookupAccount(acctName)
if err != nil {
tc.t.Errorf("LookupAccount: unexpected error: %v", err)
return false
}
if account != expectedAccount {
tc.t.Errorf("LookupAccount "+
"account mismatch -- got %d, "+
"want %d", account, expectedAccount)
return false
}
}
// Test account not found error
testName := "non existent account"
_, err := tc.manager.LookupAccount(testName)
wantErrCode := waddrmgr.ErrAccountNotFound
if !checkManagerError(tc.t, testName, err, wantErrCode) {
return false
}
return true
}
// testRenameAccount tests the rename account func of the address manager works
// as expected.
func testRenameAccount(tc *testContext) bool {
acctName, err := tc.manager.AccountName(tc.account)
if err != nil {
tc.t.Errorf("AccountName: unexpected error: %v", err)
return false
}
testName := acctName + "-renamed"
err = tc.manager.RenameAccount(tc.account, testName)
if err != nil {
tc.t.Errorf("RenameAccount: unexpected error: %v", err)
return false
}
newName, err := tc.manager.AccountName(tc.account)
if err != nil {
tc.t.Errorf("AccountName: unexpected error: %v", err)
return false
}
if newName != testName {
tc.t.Errorf("RenameAccount "+
"account name mismatch -- got %s, "+
"want %s", newName, testName)
return false
}
// Test duplicate account name error
err = tc.manager.RenameAccount(tc.account, testName)
wantErrCode := waddrmgr.ErrDuplicateAccount
if !checkManagerError(tc.t, testName, err, wantErrCode) {
return false
}
// Test old account name is no longer valid
_, err = tc.manager.LookupAccount(acctName)
wantErrCode = waddrmgr.ErrAccountNotFound
if !checkManagerError(tc.t, testName, err, wantErrCode) {
return false
}
// Test account name validation
testName = "*"
err = tc.manager.RenameAccount(tc.account, testName)
wantErrCode = waddrmgr.ErrInvalidAccount
if !checkManagerError(tc.t, testName, err, wantErrCode) {
return false
}
return true
}
// testAllAccounts tests the retrieve all accounts func of the address manager works
// as expected.
func testAllAccounts(tc *testContext) bool {
expectedAccounts := []uint32{0, 1}
if !tc.create {
// Existing wallet manager will have 3 accounts
expectedAccounts = append(expectedAccounts, 2)
}
// Imported account
expectedAccounts = append(expectedAccounts, waddrmgr.ImportedAddrAccount)
accounts, err := tc.manager.AllAccounts()
if err != nil {
tc.t.Errorf("AllAccounts: unexpected error: %v", err)
return false
}
if len(accounts) != len(expectedAccounts) {
tc.t.Errorf("AllAccounts: unexpected number of accounts - got "+
"%d, want %d", len(accounts),
len(expectedAccounts))
return false
}
for i, account := range accounts {
if expectedAccounts[i] != account {
tc.t.Errorf("AllAccounts %s: "+
"account mismatch -- got %d, "+
"want %d", i, account, expectedAccounts[i])
}
}
return true
}
// testActiveAccountAddresses tests the retrieve all account addrs func of the address manager works
// as expected.
func testActiveAccountAddresses(tc *testContext) bool {
expectedAddrs := []string{
"1VTfwD4iHre2bMrR9qGiJMwoiZGQZ8e6s",
"1LJpGrAP1vWHuvfHqmUutQqFVYca2qwxhy",
"1Jc7An3JqjzRQULVr6Wh3iYR7miB6WPJCD",
"1AY6yAHvojvpFcevAichLMnJfxgE8eSe4N",
"1LTjSghkBecT59VjEKke331HxVdqcFwUDa",
"14wtcepMNiEazuN7YosWY8bwD9tcCtxXRB",
"1N3D8jy2aQuUsKBsDgZ6ZPTVR9VhHgJYpE",
"13TdEj4ehUuYFiSaB47eLVBwM2XhAhrK2J",
"15HNivzKhsLaMs1qRdQN1ifoJYUnJ2xW9z",
"13NhXy2nCLMwNug1TZ6uwaWnxp3uTqdDQq",
}
addrs, err := tc.manager.AllAccountAddresses(tc.account)
if err != nil {
tc.t.Errorf("ActiveAccountAddresses: unexpected error: %v", err)
return false
}
if len(addrs) != len(expectedAddrs) {
tc.t.Errorf("ActiveAccountAddresses: unexpected number of addrs - got "+
"%d, want %d", len(addrs),
len(expectedAddrs))
return false
}
for i, addr := range addrs {
if expectedAddrs[i] != addr.Address().EncodeAddress() {
tc.t.Errorf("ActiveAccountAddresses %s: "+
"addr mismatch -- got %s, "+
"want %s", i, addr.Address().EncodeAddress(),
expectedAddrs[i])
}
}
return true
}
// testManagerAPI tests the functions provided by the Manager API as well as
// the ManagedAddress, ManagedPubKeyAddress, and ManagedScriptAddress
// interfaces.
@ -1169,6 +1384,13 @@ func testManagerAPI(tc *testContext) {
testImportScript(tc)
testMarkUsed(tc)
testChangePassphrase(tc)
// Reset default account
tc.account = 0
testNewAccount(tc)
testLookupAccount(tc)
testAllAccounts(tc)
testActiveAccountAddresses(tc)
}
// testWatchingOnly tests various facets of a watching-only address

190
wallet.go
View file

@ -17,6 +17,7 @@
package main
import (
"bufio"
"bytes"
"encoding/base64"
"encoding/hex"
@ -26,6 +27,7 @@ import (
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
@ -34,10 +36,12 @@ import (
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/chain"
"github.com/btcsuite/btcwallet/txstore"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/btcsuite/golangcrypto/ssh/terminal"
)
// ErrNotSynced describes an error where an operation cannot complete
@ -56,8 +60,60 @@ const (
// provided by having all public data in the wallet encrypted by a
// passphrase only known to them.
defaultPubPassphrase = "public"
// maxEmptyAccounts is the number of accounts to scan even if they have no
// transaction history. This is a deviation from BIP044 to make account
// creation more easier by allowing a limited number of empty accounts.
maxEmptyAccounts = 100
)
// promptSeed is used to prompt for the wallet seed which maybe required during
// upgrades.
func promptSeed() ([]byte, error) {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print("Enter existing wallet seed: ")
seedStr, err := reader.ReadString('\n')
if err != nil {
return nil, err
}
seedStr = strings.TrimSpace(strings.ToLower(seedStr))
seed, err := hex.DecodeString(seedStr)
if err != nil || len(seed) < hdkeychain.MinSeedBytes ||
len(seed) > hdkeychain.MaxSeedBytes {
fmt.Printf("Invalid seed specified. Must be a "+
"hexadecimal value that is at least %d bits and "+
"at most %d bits\n", hdkeychain.MinSeedBytes*8,
hdkeychain.MaxSeedBytes*8)
continue
}
return seed, nil
}
}
// promptPrivPassPhrase is used to prompt for the private passphrase which maybe
// required during upgrades.
func promptPrivPassPhrase() ([]byte, error) {
prompt := "Enter the private passphrase of your wallet: "
for {
fmt.Print(prompt)
pass, err := terminal.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return nil, err
}
fmt.Print("\n")
pass = bytes.TrimSpace(pass)
if len(pass) == 0 {
continue
}
return pass, nil
}
}
// networkDir returns the directory name of a network directory to hold wallet
// files.
func networkDir(dataDir string, chainParams *chaincfg.Params) string {
@ -171,6 +227,15 @@ func (w *Wallet) updateNotificationLock() {
w.notificationLock = noopLocker{}
}
// CreditAccount returns the first account that can be associated
// with the given credit.
// If no account is found, ErrAccountNotFound is returned.
func (w *Wallet) CreditAccount(c txstore.Credit) (uint32, error) {
_, addrs, _, _ := c.Addresses(activeNet.Params)
addr := addrs[0]
return w.Manager.AddrAccount(addr)
}
// ListenConnectedBlocks returns a channel that passes all blocks that a wallet
// has been marked in sync with. The channel must be read, or other wallet
// methods will block.
@ -470,6 +535,7 @@ func (w *Wallet) syncWithChain() error {
type (
createTxRequest struct {
account uint32
pairs map[string]btcutil.Amount
minconf int
resp chan createTxResponse
@ -495,7 +561,7 @@ out:
for {
select {
case txr := <-w.createTxRequests:
tx, err := w.txToPairs(txr.pairs, txr.minconf)
tx, err := w.txToPairs(txr.pairs, txr.account, txr.minconf)
txr.resp <- createTxResponse{tx, err}
case <-w.quit:
@ -511,8 +577,11 @@ out:
// automatically included, if necessary. All transaction creation through
// this function is serialized to prevent the creation of many transactions
// which spend the same outputs.
func (w *Wallet) CreateSimpleTx(pairs map[string]btcutil.Amount, minconf int) (*CreatedTx, error) {
func (w *Wallet) CreateSimpleTx(account uint32, pairs map[string]btcutil.Amount,
minconf int) (*CreatedTx, error) {
req := createTxRequest{
account: account,
pairs: pairs,
minconf: minconf,
resp: make(chan createTxResponse),
@ -722,6 +791,22 @@ func (w *Wallet) AddressUsed(addr waddrmgr.ManagedAddress) bool {
return addr.Used()
}
// AccountUsed returns whether there are any recorded transactions spending to
// a given account. It returns true if atleast one address in the account was
// used and false if no address in the account was used.
func (w *Wallet) AccountUsed(account uint32) (bool, error) {
addrs, err := w.Manager.AllAccountAddresses(account)
if err != nil {
return false, err
}
for _, addr := range addrs {
if w.AddressUsed(addr) {
return true, nil
}
}
return false, nil
}
// CalculateBalance sums the amounts of all unspent transaction
// outputs to addresses of a wallet and returns the balance.
//
@ -735,19 +820,51 @@ func (w *Wallet) CalculateBalance(confirms int) (btcutil.Amount, error) {
return w.TxStore.Balance(confirms, blk.Height)
}
// CalculateAccountBalance sums the amounts of all unspent transaction
// outputs to the given account of a wallet and returns the balance.
func (w *Wallet) CalculateAccountBalance(account uint32, confirms int) (btcutil.Amount, error) {
var bal btcutil.Amount
// Get current block. The block height used for calculating
// the number of tx confirmations.
blk := w.Manager.SyncedTo()
unspent, err := w.TxStore.UnspentOutputs()
if err != nil {
return 0, err
}
for _, c := range unspent {
if c.IsCoinbase() {
if !c.Confirmed(blockchain.CoinbaseMaturity, blk.Height) {
continue
}
}
if c.Confirmed(confirms, blk.Height) {
creditAccount, err := w.CreditAccount(c)
if err != nil {
continue
}
if creditAccount == account {
bal += c.Amount()
}
}
}
return bal, nil
}
// CurrentAddress gets the most recently requested Bitcoin payment address
// from a wallet. If the address has already been used (there is at least
// one transaction spending to it in the blockchain or btcd mempool), the next
// chained address is returned.
func (w *Wallet) CurrentAddress() (btcutil.Address, error) {
addr, err := w.Manager.LastExternalAddress(0)
func (w *Wallet) CurrentAddress(account uint32) (btcutil.Address, error) {
addr, err := w.Manager.LastExternalAddress(account)
if err != nil {
return nil, err
}
// Get next chained address if the last one has already been used.
if w.AddressUsed(addr) {
return w.NewAddress()
return w.NewAddress(account)
}
return addr.Address(), nil
@ -773,7 +890,7 @@ func (w *Wallet) ListSinceBlock(since, curBlockHeight int32,
continue
}
jsonResults, err := txRecord.ToJSON("", curBlockHeight,
jsonResults, err := txRecord.ToJSON(waddrmgr.DefaultAccountName, curBlockHeight,
w.Manager.ChainParams())
if err != nil {
return nil, err
@ -798,7 +915,7 @@ func (w *Wallet) ListTransactions(from, count int) ([]btcjson.ListTransactionsRe
lastLookupIdx := len(records) - count
// Search in reverse order: lookup most recently-added first.
for i := len(records) - 1; i >= from && i >= lastLookupIdx; i-- {
jsonResults, err := records[i].ToJSON("", blk.Height,
jsonResults, err := records[i].ToJSON(waddrmgr.DefaultAccountName, blk.Height,
w.Manager.ChainParams())
if err != nil {
return nil, err
@ -837,7 +954,7 @@ func (w *Wallet) ListAddressTransactions(pkHashes map[string]struct{}) (
if _, ok := pkHashes[string(apkh.ScriptAddress())]; !ok {
continue
}
jsonResult, err := c.ToJSON("", blk.Height,
jsonResult, err := c.ToJSON(waddrmgr.DefaultAccountName, blk.Height,
w.Manager.ChainParams())
if err != nil {
return nil, err
@ -862,7 +979,7 @@ func (w *Wallet) ListAllTransactions() ([]btcjson.ListTransactionsResult, error)
// Search in reverse order: lookup most recently-added first.
records := w.TxStore.Records()
for i := len(records) - 1; i >= 0; i-- {
jsonResults, err := records[i].ToJSON("", blk.Height,
jsonResults, err := records[i].ToJSON(waddrmgr.DefaultAccountName, blk.Height,
w.Manager.ChainParams())
if err != nil {
return nil, err
@ -906,6 +1023,15 @@ func (w *Wallet) ListUnspent(minconf, maxconf int,
continue
}
creditAccount, err := w.CreditAccount(credit)
if err != nil {
continue
}
accountName, err := w.Manager.AccountName(creditAccount)
if err != nil {
return nil, err
}
_, addrs, _, _ := credit.Addresses(activeNet.Params)
if filter {
for _, addr := range addrs {
@ -920,9 +1046,9 @@ func (w *Wallet) ListUnspent(minconf, maxconf int,
result := &btcjson.ListUnspentResult{
TxId: credit.Tx().Sha().String(),
Vout: credit.OutputIndex,
Account: "",
Account: accountName,
ScriptPubKey: hex.EncodeToString(credit.TxOut().PkScript),
Amount: credit.Amount().ToUnit(btcutil.AmountBTC),
Amount: credit.Amount().ToBTC(),
Confirmations: int64(confs),
}
@ -1197,9 +1323,8 @@ func (w *Wallet) SortedActivePaymentAddresses() ([]string, error) {
}
// NewAddress returns the next external chained address for a wallet.
func (w *Wallet) NewAddress() (btcutil.Address, error) {
func (w *Wallet) NewAddress(account uint32) (btcutil.Address, error) {
// Get next address from wallet.
account := uint32(0)
addrs, err := w.Manager.NextExternalAddresses(account, 1)
if err != nil {
return nil, err
@ -1218,9 +1343,8 @@ func (w *Wallet) NewAddress() (btcutil.Address, error) {
}
// NewChangeAddress returns a new change address for a wallet.
func (w *Wallet) NewChangeAddress() (btcutil.Address, error) {
// Get next chained change address from wallet for account 0.
account := uint32(0)
func (w *Wallet) NewChangeAddress(account uint32) (btcutil.Address, error) {
// Get next chained change address from wallet for account.
addrs, err := w.Manager.NextInternalAddresses(account, 1)
if err != nil {
return nil, err
@ -1239,27 +1363,35 @@ func (w *Wallet) NewChangeAddress() (btcutil.Address, error) {
return utilAddrs[0], nil
}
// TotalReceived iterates through a wallet's transaction history, returning the
// total amount of bitcoins received for any wallet address. Amounts received
// through multisig transactions are ignored.
func (w *Wallet) TotalReceived(confirms int) (btcutil.Amount, error) {
// TotalReceivedForAccount iterates through a wallet's transaction history,
// returning the total amount of bitcoins received for a single wallet
// account.
func (w *Wallet) TotalReceivedForAccount(account uint32, confirms int) (btcutil.Amount, uint64, error) {
blk := w.Manager.SyncedTo()
// Number of confirmations of the last transaction.
var confirmations uint64
var amount btcutil.Amount
for _, r := range w.TxStore.Records() {
for _, c := range r.Credits() {
// Ignore change.
if c.Change() {
if !c.Confirmed(confirms, blk.Height) {
// Not enough confirmations, skip the current block.
continue
}
// Tally if the appropiate number of block confirmations have passed.
if c.Confirmed(confirms, blk.Height) {
creditAccount, err := w.CreditAccount(c)
if err != nil {
continue
}
if creditAccount == account {
amount += c.Amount()
confirmations = uint64(c.Confirmations(blk.Height))
break
}
}
}
return amount, nil
return amount, confirmations, nil
}
// TotalReceivedForAddr iterates through a wallet's transaction history,
@ -1329,8 +1461,12 @@ func openWallet() (*Wallet, error) {
// Open address manager and transaction store.
var txs *txstore.Store
config := &waddrmgr.Options{
ObtainSeed: promptSeed,
ObtainPrivatePass: promptPrivPassPhrase,
}
mgr, err := waddrmgr.Open(namespace, []byte(cfg.WalletPass),
activeNet.Params, nil)
activeNet.Params, config)
if err == nil {
txs, err = txstore.OpenDir(netdir)
}