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 // to addr or as a fee for the miner are sent to a newly generated
// address. InsufficientFundsError is returned if there are not enough // address. InsufficientFundsError is returned if there are not enough
// eligible unspent outputs to create the transaction. // 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 // Address manager must be unlocked to compose transaction. Grab
// the unlock if possible (to prevent future unlocks), or return the // 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 return nil, err
} }
eligible, err := w.findEligibleOutputs(minconf, bs) eligible, err := w.findEligibleOutputs(account, minconf, bs)
if err != nil { if err != nil {
return nil, err 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) // createTx selects inputs (from the given slice of eligible utxos)
@ -164,7 +164,8 @@ func createTx(
bs *waddrmgr.BlockStamp, bs *waddrmgr.BlockStamp,
feeIncrement btcutil.Amount, feeIncrement btcutil.Amount,
mgr *waddrmgr.Manager, mgr *waddrmgr.Manager,
changeAddress func(*waddrmgr.BlockStamp) (btcutil.Address, error)) ( account uint32,
changeAddress func(account uint32) (btcutil.Address, error)) (
*CreatedTx, error) { *CreatedTx, error) {
msgtx := wire.NewMsgTx() msgtx := wire.NewMsgTx()
@ -220,7 +221,7 @@ func createTx(
change := totalAdded - minAmount - feeEst change := totalAdded - minAmount - feeEst
if change > 0 { if change > 0 {
if changeAddr == nil { if changeAddr == nil {
changeAddr, err = changeAddress(bs) changeAddr, err = changeAddress(account)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -293,23 +294,6 @@ func addChange(msgtx *wire.MsgTx, change btcutil.Amount, changeAddr btcutil.Addr
return int(r), nil 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, // addOutputs adds the given address/amount pairs as outputs to msgtx,
// returning their total amount. // returning their total amount.
func addOutputs(msgtx *wire.MsgTx, pairs map[string]btcutil.Amount) (btcutil.Amount, error) { 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 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() unspent, err := w.TxStore.UnspentOutputs()
if err != nil { if err != nil {
return nil, err return nil, err
@ -365,9 +349,15 @@ func (w *Wallet) findEligibleOutputs(minconf int, bs *waddrmgr.BlockStamp) ([]tx
continue continue
} }
creditAccount, err := w.CreditAccount(unspent[i])
if err != nil {
continue
}
if creditAccount == account {
eligible = append(eligible, unspent[i]) eligible = append(eligible, unspent[i])
} }
} }
}
return eligible, nil return eligible, nil
} }

View file

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

View file

@ -89,8 +89,9 @@ var (
errors.New("minconf must be positive"), errors.New("minconf must be positive"),
} }
ErrAddressNotInWallet = InvalidAddressOrKeyError{ ErrAddressNotInWallet = btcjson.Error{
errors.New("address not found in wallet"), Code: btcjson.ErrWallet.Code,
Message: "address not found in wallet",
} }
ErrNoAccountSupport = btcjson.Error{ ErrNoAccountSupport = btcjson.Error{
@ -98,6 +99,11 @@ var (
Message: "btcwallet does not support non-default accounts", Message: "btcwallet does not support non-default accounts",
} }
ErrAccountNameNotFound = btcjson.Error{
Code: btcjson.ErrWalletInvalidAccountName.Code,
Message: "account name not found",
}
ErrUnloadedWallet = btcjson.Error{ ErrUnloadedWallet = btcjson.Error{
Code: btcjson.ErrWallet.Code, Code: btcjson.ErrWallet.Code,
Message: "Request requires a wallet but wallet has not loaded yet", 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 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. // duplicate item being provided to the address manager.
func isManagerDuplicateError(err error) bool { func isManagerDuplicateAddressError(err error) bool {
merr, ok := err.(waddrmgr.ManagerError) merr, ok := err.(waddrmgr.ManagerError)
if !ok { return ok && merr.ErrorCode == waddrmgr.ErrDuplicateAddress
return false }
}
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 // 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 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. // are handled for the authenticate and stop request methods.
var resp btcjson.Reply var resp btcjson.Reply
switch raw.Method { switch raw.Method {
@ -1044,13 +1060,18 @@ func (b blockDisconnected) notificationCmds(w *Wallet) []btcjson.Cmd {
func (c txCredit) notificationCmds(w *Wallet) []btcjson.Cmd { func (c txCredit) notificationCmds(w *Wallet) []btcjson.Cmd {
blk := w.Manager.SyncedTo() 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 { if err != nil {
log.Errorf("Cannot create notification for transaction "+ log.Errorf("Cannot create notification for transaction "+
"credit: %v", err) "credit: %v", err)
return nil return nil
} }
n := btcws.NewTxNtfn("", &ltr) n := btcws.NewTxNtfn(acctName, &ltr)
return []btcjson.Cmd{n} return []btcjson.Cmd{n}
} }
@ -1076,13 +1097,13 @@ func (l managerLocked) notificationCmds(w *Wallet) []btcjson.Cmd {
func (b confirmedBalance) notificationCmds(w *Wallet) []btcjson.Cmd { func (b confirmedBalance) notificationCmds(w *Wallet) []btcjson.Cmd {
n := btcws.NewAccountBalanceNtfn("", n := btcws.NewAccountBalanceNtfn("",
btcutil.Amount(b).ToUnit(btcutil.AmountBTC), true) btcutil.Amount(b).ToBTC(), true)
return []btcjson.Cmd{n} return []btcjson.Cmd{n}
} }
func (b unconfirmedBalance) notificationCmds(w *Wallet) []btcjson.Cmd { func (b unconfirmedBalance) notificationCmds(w *Wallet) []btcjson.Cmd {
n := btcws.NewAccountBalanceNtfn("", n := btcws.NewAccountBalanceNtfn("",
btcutil.Amount(b).ToUnit(btcutil.AmountBTC), false) btcutil.Amount(b).ToBTC(), false)
return []btcjson.Cmd{n} return []btcjson.Cmd{n}
} }
@ -1359,6 +1380,7 @@ var rpcHandlers = map[string]requestHandler{
"setaccount": Unsupported, "setaccount": Unsupported,
// Extensions to the reference client JSON-RPC API // Extensions to the reference client JSON-RPC API
"createnewaccount": CreateNewAccount,
"exportwatchingwallet": ExportWatchingWallet, "exportwatchingwallet": ExportWatchingWallet,
"getbestblock": GetBestBlock, "getbestblock": GetBestBlock,
// This was an extension but the reference implementation added it as // This was an extension but the reference implementation added it as
@ -1368,6 +1390,7 @@ var rpcHandlers = map[string]requestHandler{
"getunconfirmedbalance": GetUnconfirmedBalance, "getunconfirmedbalance": GetUnconfirmedBalance,
"listaddresstransactions": ListAddressTransactions, "listaddresstransactions": ListAddressTransactions,
"listalltransactions": ListAllTransactions, "listalltransactions": ListAllTransactions,
"renameaccount": RenameAccount,
"walletislocked": WalletIsLocked, "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) { func GetAddressesByAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcjson.GetAddressesByAccountCmd) cmd := icmd.(*btcjson.GetAddressesByAccountCmd)
err := checkAccountName(cmd.Account) account, err := w.Manager.LookupAccount(cmd.Account)
if err != nil { if err != nil {
return nil, err 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 // 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) { func GetBalance(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcjson.GetBalanceCmd) cmd := icmd.(*btcjson.GetBalanceCmd)
var account string var balance btcutil.Amount
if cmd.Account != nil { var account uint32
account = *cmd.Account var err error
} if cmd.Account == nil || *cmd.Account == "*" {
balance, err = w.CalculateBalance(cmd.MinConf)
err := checkAccountName(account) } else {
account, err = w.Manager.LookupAccount(*cmd.Account)
if err != nil { if err != nil {
return nil, err return nil, err
} }
balance, err = w.CalculateAccountBalance(account, cmd.MinConf)
balance, err := w.CalculateBalance(cmd.MinConf) }
if err != nil { if err != nil {
return nil, err return nil, err
} }
return balance.ToBTC(), nil
return balance.ToUnit(btcutil.AmountBTC), nil
} }
// GetBestBlock handles a getbestblock request by returning a JSON object // 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 // TODO(davec): This should probably have a database version as opposed
// to using the manager version. // to using the manager version.
info.WalletVersion = int32(waddrmgr.LatestMgrVersion) info.WalletVersion = int32(waddrmgr.LatestMgrVersion)
info.Balance = bal.ToUnit(btcutil.AmountBTC) info.Balance = bal.ToBTC()
// Keypool times are not tracked. set to current time. // Keypool times are not tracked. set to current time.
info.KeypoolOldest = time.Now().Unix() info.KeypoolOldest = time.Now().Unix()
info.KeypoolSize = int32(cfg.KeypoolSize) 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 // We don't set the following since they don't make much sense in the
// wallet architecture: // wallet architecture:
// - unlocked_until // - unlocked_until
@ -1742,13 +1775,17 @@ func GetAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{
return nil, btcjson.ErrInvalidAddressOrKey return nil, btcjson.ErrInvalidAddressOrKey
} }
// If it is in the wallet, we consider it part of the default account. // Fetch the associated account
_, err = w.Manager.Address(addr) account, err := w.Manager.AddrAccount(addr)
if err != nil { 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 // 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) { func GetAccountAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcjson.GetAccountAddressCmd) cmd := icmd.(*btcjson.GetAccountAddressCmd)
err := checkDefaultAccount(cmd.Account) account, err := w.Manager.LookupAccount(cmd.Account)
if err != nil { if err != nil {
return nil, err return nil, err
} }
addr, err := w.CurrentAddress(account)
addr, err := w.CurrentAddress()
if err != nil { if err != nil {
return nil, err 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) { func GetUnconfirmedBalance(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcws.GetUnconfirmedBalanceCmd) cmd := icmd.(*btcws.GetUnconfirmedBalanceCmd)
err := checkAccountName(cmd.Account) account, err := w.Manager.LookupAccount(cmd.Account)
if err != nil { if err != nil {
return nil, err return nil, err
} }
unconfirmed, err := w.CalculateBalance(0) unconfirmed, err := w.CalculateAccountBalance(account, 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }
confirmed, err := w.CalculateBalance(1) confirmed, err := w.CalculateAccountBalance(account, 1)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return (unconfirmed - confirmed).ToUnit(btcutil.AmountBTC), nil return (unconfirmed - confirmed).ToBTC(), nil
} }
// ImportPrivKey handles an importprivkey request by parsing // 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. // Import the private key, handling any errors.
_, err = w.ImportPrivateKey(wif, nil, cmd.Rescan) _, err = w.ImportPrivateKey(wif, nil, cmd.Rescan)
switch { switch {
case isManagerDuplicateError(err): case isManagerDuplicateAddressError(err):
// Do not return duplicate key errors to the client. // Do not return duplicate key errors to the client.
return nil, nil return nil, nil
case isManagerLockedError(err): case isManagerLockedError(err):
@ -1830,18 +1866,61 @@ func KeypoolRefill(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interfa
return nil, nil return nil, nil
} }
// GetNewAddress handlesa getnewaddress request by returning a new // CreateNewAccount handles a createnewaccount request by creating and
// address for an account. If the account does not exist or the keypool // returning a new account. If the last account has no transaction history
// ran out with a locked wallet, an appropiate error is returned. // 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) { func GetNewAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcjson.GetNewAddressCmd) cmd := icmd.(*btcjson.GetNewAddressCmd)
err := checkDefaultAccount(cmd.Account) account, err := w.Manager.LookupAccount(cmd.Account)
if err != nil { if err != nil {
return nil, err return nil, err
} }
addr, err := w.NewAddress() addr, err := w.NewAddress(account)
if err != nil { if err != nil {
return nil, err 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, // Note: bitcoind allows specifying the account as an optional parameter,
// but ignores the parameter. // but ignores the parameter.
func GetRawChangeAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { 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 { if err != nil {
return nil, err 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) { func GetReceivedByAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcjson.GetReceivedByAccountCmd) cmd := icmd.(*btcjson.GetReceivedByAccountCmd)
err := checkAccountName(cmd.Account) account, err := w.Manager.LookupAccount(cmd.Account)
if err != nil { if err != nil {
return nil, err return nil, err
} }
bal, err := w.TotalReceived(cmd.MinConf) bal, _, err := w.TotalReceivedForAccount(account, cmd.MinConf)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return bal.ToUnit(btcutil.AmountBTC), nil return bal.ToBTC(), nil
} }
// GetReceivedByAddress handles a getreceivedbyaddress request by returning // 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 nil, err
} }
return total.ToUnit(btcutil.AmountBTC), nil return total.ToBTC(), nil
} }
// GetTransaction handles a gettransaction request by returning details about // 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) ret.Details = make([]btcjson.GetTransactionDetailsResult, 1, len(credits)+1)
details := btcjson.GetTransactionDetailsResult{ details := btcjson.GetTransactionDetailsResult{
Account: "", Account: waddrmgr.DefaultAccountName,
Category: "send", Category: "send",
// negative since it is a send // negative since it is a send
Amount: (-debits.OutputAmount(true)).ToUnit(btcutil.AmountBTC), Amount: (-debits.OutputAmount(true)).ToBTC(),
Fee: debits.Fee().ToUnit(btcutil.AmountBTC), Fee: debits.Fee().ToBTC(),
} }
targetAddr = &details.Address targetAddr = &details.Address
ret.Details[0] = details 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{ ret.Details = append(ret.Details, btcjson.GetTransactionDetailsResult{
Account: "", Account: waddrmgr.DefaultAccountName,
Category: cred.Category(blk.Height).String(), Category: cred.Category(blk.Height).String(),
Amount: cred.Amount().ToUnit(btcutil.AmountBTC), Amount: cred.Amount().ToBTC(),
Address: addr, Address: addr,
}) })
} }
ret.Amount = creditAmount.ToUnit(btcutil.AmountBTC) ret.Amount = creditAmount.ToBTC()
return ret, nil 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) { func ListAccounts(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcjson.ListAccountsCmd) cmd := icmd.(*btcjson.ListAccountsCmd)
bal, err := w.CalculateBalance(cmd.MinConf) accountBalances := map[string]float64{}
accounts, err := w.Manager.AllAccounts()
if err != nil { if err != nil {
return nil, err 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 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 // 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; // default: one;
// "includeempty": whether or not to include addresses that have no transactions - // "includeempty": whether or not to include addresses that have no transactions -
// default: false. // 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) { func ListReceivedByAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcjson.ListReceivedByAccountCmd) cmd := icmd.(*btcjson.ListReceivedByAccountCmd)
blk := w.Manager.SyncedTo() accounts, err := w.Manager.AllAccounts()
if err != nil {
// Total amount received. return nil, err
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)
}
} }
ret := []btcjson.ListReceivedByAccountResult{ ret := make([]btcjson.ListReceivedByAccountResult, 0, len(accounts))
{ for _, account := range accounts {
Account: "", acctName, err := w.Manager.AccountName(account)
Amount: amount.ToUnit(btcutil.AmountBTC), 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), Confirmations: uint64(confirmations),
}, })
} }
return ret, nil return ret, nil
} }
@ -2089,6 +2177,8 @@ func ListReceivedByAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd)
confirmations int32 confirmations int32
// Hashes of transactions which include an output paying to the address // Hashes of transactions which include an output paying to the address
tx []string tx []string
// Account which the address belongs to
account string
} }
blk := w.Manager.SyncedTo() blk := w.Manager.SyncedTo()
@ -2144,9 +2234,9 @@ func ListReceivedByAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd)
idx := 0 idx := 0
for address, addrData := range allAddrData { for address, addrData := range allAddrData {
ret[idx] = btcjson.ListReceivedByAddressResult{ ret[idx] = btcjson.ListReceivedByAddressResult{
Account: "", Account: waddrmgr.DefaultAccountName,
Address: address, Address: address,
Amount: addrData.amount.ToUnit(btcutil.AmountBTC), Amount: addrData.amount.ToBTC(),
Confirmations: uint64(addrData.confirmations), Confirmations: uint64(addrData.confirmations),
TxIDs: addrData.tx, TxIDs: addrData.tx,
} }
@ -2204,15 +2294,12 @@ func ListSinceBlock(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interf
func ListTransactions(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { func ListTransactions(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcjson.ListTransactionsCmd) cmd := icmd.(*btcjson.ListTransactionsCmd)
var account string
if cmd.Account != nil { if cmd.Account != nil {
account = *cmd.Account err := checkAccountName(*cmd.Account)
}
err := checkAccountName(account)
if err != nil { if err != nil {
return nil, err return nil, err
} }
}
return w.ListTransactions(cmd.From, cmd.Count) return w.ListTransactions(cmd.From, cmd.Count)
} }
@ -2254,15 +2341,12 @@ func ListAddressTransactions(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd
func ListAllTransactions(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { func ListAllTransactions(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcws.ListAllTransactionsCmd) cmd := icmd.(*btcws.ListAllTransactionsCmd)
var account string
if cmd.Account != nil { if cmd.Account != nil {
account = *cmd.Account err := checkAccountName(*cmd.Account)
}
err := checkAccountName(account)
if err != nil { if err != nil {
return nil, err return nil, err
} }
}
return w.ListAllTransactions() 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 // sendPairs is a helper routine to reduce duplicated code when creating and
// sending payment transactions. // sending payment transactions.
func sendPairs(w *Wallet, chainSvr *chain.Client, cmd btcjson.Cmd, 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 // Create transaction, replying with an error if the creation
// was not successful. // was not successful.
createdTx, err := w.CreateSimpleTx(amounts, minconf) createdTx, err := w.CreateSimpleTx(account, amounts, minconf)
if err != nil { if err != nil {
switch { switch {
case err == ErrNonPositiveAmount: 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) { func SendFrom(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcjson.SendFromCmd) cmd := icmd.(*btcjson.SendFromCmd)
err := checkAccountName(cmd.FromAccount) account, err := w.Manager.LookupAccount(cmd.FromAccount)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -2388,7 +2472,7 @@ func SendFrom(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{},
cmd.ToAddress: btcutil.Amount(cmd.Amount), 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 // 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) { func SendMany(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) {
cmd := icmd.(*btcjson.SendManyCmd) cmd := icmd.(*btcjson.SendManyCmd)
err := checkAccountName(cmd.FromAccount) account, err := w.Manager.LookupAccount(cmd.FromAccount)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -2415,7 +2499,7 @@ func SendMany(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{},
pairs[k] = btcutil.Amount(v) 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 // 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), 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. // 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 result.IsValid = true
ainfo, err := w.Manager.Address(addr) ainfo, err := w.Manager.Address(addr)
if managerErr, ok := err.(waddrmgr.ManagerError); ok { if err != nil {
if managerErr.ErrorCode == waddrmgr.ErrAddressNotFound { if isManagerAddressNotFoundError(err) {
// No additional information available about the address. // No additional information available about the address.
return result, nil return result, nil
} }
}
if err != nil {
return nil, err return nil, err
} }
// The address lookup was successful which means there is further // The address lookup was successful which means there is further
// information about it available and it is "mine". // information about it available and it is "mine".
result.IsMine = true 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) { switch ma := ainfo.(type) {
case waddrmgr.ManagedPubKeyAddress: case waddrmgr.ManagedPubKeyAddress:

View file

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

View file

@ -17,19 +17,20 @@
package waddrmgr package waddrmgr
import ( import (
"bytes"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"time" "time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/walletdb" "github.com/btcsuite/btcwallet/walletdb"
"github.com/btcsuite/fastsha256" "github.com/btcsuite/fastsha256"
) )
const ( const (
// LatestMgrVersion is the most recent manager version. // LatestMgrVersion is the most recent manager version.
LatestMgrVersion = 2 LatestMgrVersion = 3
) )
var ( var (
@ -38,6 +39,11 @@ var (
latestMgrVersion uint32 = LatestMgrVersion 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 // maybeConvertDbError converts the passed error to a ManagerError with an
// error code of ErrDatabase if it is not already a ManagerError. This is // error code of ErrDatabase if it is not already a ManagerError. This is
// useful for potential errors returned from managed transaction an other parts // useful for potential errors returned from managed transaction an other parts
@ -137,10 +143,46 @@ type dbScriptAddressRow struct {
// Key names for various database fields. // Key names for various database fields.
var ( var (
// nullVall is null byte used as a flag value in a bucket entry
nullVal = []byte{0}
// Bucket names. // Bucket names.
acctBucketName = []byte("acct") acctBucketName = []byte("acct")
addrBucketName = []byte("addr") 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") addrAcctIdxBucketName = []byte("addracctidx")
// 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") mainBucketName = []byte("main")
syncBucketName = []byte("sync") syncBucketName = []byte("sync")
@ -154,6 +196,8 @@ var (
cryptoPrivKeyName = []byte("cpriv") cryptoPrivKeyName = []byte("cpriv")
cryptoPubKeyName = []byte("cpub") cryptoPubKeyName = []byte("cpub")
cryptoScriptKeyName = []byte("cscript") cryptoScriptKeyName = []byte("cscript")
coinTypePrivKeyName = []byte("ctpriv")
coinTypePubKeyName = []byte("ctpub")
watchingOnlyName = []byte("watchonly") watchingOnlyName = []byte("watchonly")
// Sync related key names (sync bucket). // Sync related key names (sync bucket).
@ -176,6 +220,40 @@ func uint32ToBytes(number uint32) []byte {
return buf 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. // putManagerVersion stores the provided version to the database.
func putManagerVersion(tx walletdb.Tx, version uint32) error { func putManagerVersion(tx walletdb.Tx, version uint32) error {
bucket := tx.RootBucket().Bucket(mainBucketName) bucket := tx.RootBucket().Bucket(mainBucketName)
@ -242,6 +320,50 @@ func putMasterKeyParams(tx walletdb.Tx, pubParams, privParams []byte) error {
return nil 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 // fetchCryptoKeys loads the encrypted crypto keys which are in turn used to
// protect the extended keys, imported keys, and scripts. Any of the returned // 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 // values can be nil, but in practice only the crypto private and script keys
@ -455,6 +577,70 @@ func serializeBIP0044AccountRow(encryptedPubKey,
return rawData 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 // fetchAccountInfo loads information about the passed account from the
// database. // database.
func fetchAccountInfo(tx walletdb.Tx, account uint32) (interface{}, error) { 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) 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 // putAccountRow stores the provided account information to the database. This
// is used a common base for storing the various account types. // is used a common base for storing the various account types.
func putAccountRow(tx walletdb.Tx, account uint32, row *dbAccountRow) error { func putAccountRow(tx walletdb.Tx, account uint32, row *dbAccountRow) error {
@ -507,36 +768,30 @@ func putAccountInfo(tx walletdb.Tx, account uint32, encryptedPubKey,
acctType: actBIP0044, acctType: actBIP0044,
rawData: rawData, rawData: rawData,
} }
return putAccountRow(tx, account, &acctRow) if err := putAccountRow(tx, account, &acctRow); err != nil {
} return err
}
// fetchNumAccounts loads the number of accounts that have been created from // Update account id index
// the database. if err := putAccountIdIndex(tx, account, name); err != nil {
func fetchNumAccounts(tx walletdb.Tx) (uint32, error) { return err
bucket := tx.RootBucket().Bucket(acctBucketName) }
// Update account name index
val := bucket.Get(acctNumAcctsName) if err := putAccountNameIndex(tx, account, name); err != nil {
if val == nil { return err
str := "required num accounts not stored in database"
return 0, managerError(ErrDatabase, str, nil)
} }
return binary.LittleEndian.Uint32(val), nil return nil
} }
// putNumAccounts stores the number of accounts that have been created to the // putLastAccount stores the provided metadata - last account - to the database.
// database. func putLastAccount(tx walletdb.Tx, account uint32) error {
func putNumAccounts(tx walletdb.Tx, numAccounts uint32) error { bucket := tx.RootBucket().Bucket(metaBucketName)
bucket := tx.RootBucket().Bucket(acctBucketName)
var val [4]byte err := bucket.Put(lastAccountName, uint32ToBytes(account))
binary.LittleEndian.PutUint32(val[:], numAccounts)
err := bucket.Put(acctNumAcctsName, val[:])
if err != nil { if err != nil {
str := "failed to store num accounts" str := fmt.Sprintf("failed to update metadata '%s'", lastAccountName)
return managerError(ErrDatabase, str, err) return managerError(ErrDatabase, str, err)
} }
return nil return nil
} }
@ -547,7 +802,7 @@ func putNumAccounts(tx walletdb.Tx, numAccounts uint32) error {
// deserializeAddressRow deserializes the passed serialized address information. // deserializeAddressRow deserializes the passed serialized address information.
// This is used as a common base for the various address types to deserialize // This is used as a common base for the various address types to deserialize
// the common parts. // the common parts.
func deserializeAddressRow(addressID, serializedAddress []byte) (*dbAddressRow, error) { func deserializeAddressRow(serializedAddress []byte) (*dbAddressRow, error) {
// The serialized address format is: // The serialized address format is:
// <addrType><account><addedTime><syncStatus><rawdata> // <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 // Given the above, the length of the entry must be at a minimum
// the constant value sizes. // the constant value sizes.
if len(serializedAddress) < 18 { if len(serializedAddress) < 18 {
str := fmt.Sprintf("malformed serialized address for key %s", str := "malformed serialized address"
addressID)
return nil, managerError(ErrDatabase, str, nil) return nil, managerError(ErrDatabase, str, nil)
} }
@ -595,14 +849,13 @@ func serializeAddressRow(row *dbAddressRow) []byte {
// deserializeChainedAddress deserializes the raw data from the passed address // deserializeChainedAddress deserializes the raw data from the passed address
// row as a chained 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: // The serialized chain address raw data format is:
// <branch><index> // <branch><index>
// //
// 4 bytes branch + 4 bytes address index // 4 bytes branch + 4 bytes address index
if len(row.rawData) != 8 { if len(row.rawData) != 8 {
str := fmt.Sprintf("malformed serialized chained address for "+ str := "malformed serialized chained address"
"key %s", addressID)
return nil, managerError(ErrDatabase, str, nil) 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 // deserializeImportedAddress deserializes the raw data from the passed address
// row as an imported 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: // The serialized imported address raw data format is:
// <encpubkeylen><encpubkey><encprivkeylen><encprivkey> // <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 // Given the above, the length of the entry must be at a minimum
// the constant value sizes. // the constant value sizes.
if len(row.rawData) < 8 { if len(row.rawData) < 8 {
str := fmt.Sprintf("malformed serialized imported address for "+ str := "malformed serialized imported address"
"key %s", addressID)
return nil, managerError(ErrDatabase, str, nil) 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 // deserializeScriptAddress deserializes the raw data from the passed address
// row as a script 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: // The serialized script address raw data format is:
// <encscripthashlen><encscripthash><encscriptlen><encscript> // <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 // Given the above, the length of the entry must be at a minimum
// the constant value sizes. // the constant value sizes.
if len(row.rawData) < 8 { if len(row.rawData) < 8 {
str := fmt.Sprintf("malformed serialized script address for "+ str := "malformed serialized script address"
"key %s", addressID)
return nil, managerError(ErrDatabase, str, nil) 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. // 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) bucket := tx.RootBucket().Bucket(usedAddrBucketName)
val := bucket.Get(addrHash[:]) val := bucket.Get(addrHash[:])
@ -747,32 +998,33 @@ func fetchAddressUsed(tx walletdb.Tx, addrHash [32]byte) bool {
return false return false
} }
// fetchAddress loads address information for the provided address id from // fetchAddressByHash loads address information for the provided address hash
// the database. The returned value is one of the address rows for the specific // from the database. The returned value is one of the address rows for the
// address type. The caller should use type assertions to ascertain the type. // specific address type. The caller should use type assertions to ascertain
func fetchAddress(tx walletdb.Tx, addressID []byte) (interface{}, error) { // 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) bucket := tx.RootBucket().Bucket(addrBucketName)
addrHash := fastsha256.Sum256(addressID)
serializedRow := bucket.Get(addrHash[:]) serializedRow := bucket.Get(addrHash[:])
if serializedRow == nil { if serializedRow == nil {
str := "address not found" str := "address not found"
return nil, managerError(ErrAddressNotFound, str, nil) return nil, managerError(ErrAddressNotFound, str, nil)
} }
row, err := deserializeAddressRow(addressID, serializedRow) row, err := deserializeAddressRow(serializedRow)
if err != nil { if err != nil {
return nil, err return nil, err
} }
row.used = fetchAddressUsed(tx, addrHash) row.used = fetchAddressUsed(tx, addrHash[:])
switch row.addrType { switch row.addrType {
case adtChain: case adtChain:
return deserializeChainedAddress(addressID, row) return deserializeChainedAddress(row)
case adtImport: case adtImport:
return deserializeImportedAddress(addressID, row) return deserializeImportedAddress(row)
case adtScript: case adtScript:
return deserializeScriptAddress(addressID, row) return deserializeScriptAddress(row)
} }
str := fmt.Sprintf("unsupported address type '%d'", row.addrType) str := fmt.Sprintf("unsupported address type '%d'", row.addrType)
@ -796,6 +1048,16 @@ func markAddressUsed(tx walletdb.Tx, addressID []byte) error {
return nil 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 // putAddress stores the provided address information to the database. This
// is used a common base for storing the various address types. // is used a common base for storing the various address types.
func putAddress(tx walletdb.Tx, addressID []byte, row *dbAddressRow) error { 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) str := fmt.Sprintf("failed to store address %x", addressID)
return managerError(ErrDatabase, str, err) return managerError(ErrDatabase, str, err)
} }
// Update address account index
return nil return putAddrAccountIndex(tx, row.account, addrHash[:])
} }
// putChainedAddress stores the provided chained address information to the // 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 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. // fetchAllAddresses loads information about all addresses from the database.
// The returned value is a slice of address rows for each specific address type. // The returned value is a slice of address rows for each specific address type.
// The caller should use type assertions to ascertain the types. // 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) { func fetchAllAddresses(tx walletdb.Tx) ([]interface{}, error) {
bucket := tx.RootBucket().Bucket(addrBucketName) 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 // Deserialize the address row first to determine the field
// values. // values.
row, err := deserializeAddressRow(k, v) addrRow, err := fetchAddressByHash(tx, k)
if err != nil { if merr, ok := err.(*ManagerError); ok {
return err desc := fmt.Sprintf("failed to fetch address hash '%s': %v",
} k, merr.Description)
merr.Description = desc
var addrRow interface{} return merr
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)
} }
if err != nil { if err != nil {
return err return err
@ -985,12 +1291,16 @@ func deletePrivateKeys(tx walletdb.Tx) error {
str := "failed to delete crypto script key" str := "failed to delete crypto script key"
return managerError(ErrDatabase, str, err) 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. // Delete the account extended private key for all accounts.
bucket = tx.RootBucket().Bucket(acctBucketName) bucket = tx.RootBucket().Bucket(acctBucketName)
err := bucket.ForEach(func(k, v []byte) error { err := bucket.ForEach(func(k, v []byte) error {
// Skip buckets. // Skip buckets.
if v == nil || bytes.Equal(k, acctNumAcctsName) { if v == nil {
return nil return nil
} }
@ -1036,14 +1346,14 @@ func deletePrivateKeys(tx walletdb.Tx) error {
// Deserialize the address row first to determine the field // Deserialize the address row first to determine the field
// values. // values.
row, err := deserializeAddressRow(k, v) row, err := deserializeAddressRow(v)
if err != nil { if err != nil {
return err return err
} }
switch row.addrType { switch row.addrType {
case adtImport: case adtImport:
irow, err := deserializeImportedAddress(k, row) irow, err := deserializeImportedAddress(row)
if err != nil { if err != nil {
return err return err
} }
@ -1059,7 +1369,7 @@ func deletePrivateKeys(tx walletdb.Tx) error {
} }
case adtScript: case adtScript:
srow, err := deserializeScriptAddress(k, row) srow, err := deserializeScriptAddress(row)
if err != nil { if err != nil {
return err return err
} }
@ -1283,6 +1593,28 @@ func createManagerNS(namespace walletdb.Namespace) error {
return managerError(ErrDatabase, str, err) 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 { if err := putManagerVersion(tx, latestMgrVersion); err != nil {
return err return err
} }
@ -1334,14 +1666,12 @@ func upgradeToVersion2(namespace walletdb.Namespace) error {
// upgradeManager upgrades the data in the provided manager namespace to newer // upgradeManager upgrades the data in the provided manager namespace to newer
// versions as neeeded. // versions as neeeded.
func upgradeManager(namespace walletdb.Namespace) error { func upgradeManager(namespace walletdb.Namespace, pubPassPhrase []byte, config *Options) error {
// Get the current version.
var version uint32 var version uint32
err := namespace.View(func(tx walletdb.Tx) error { err := namespace.View(func(tx walletdb.Tx) error {
mainBucket := tx.RootBucket().Bucket(mainBucketName) var err error
verBytes := mainBucket.Get(mgrVersionName) version, err = fetchManagerVersion(tx)
version = binary.LittleEndian.Uint32(verBytes) return err
return nil
}) })
if err != nil { if err != nil {
str := "failed to fetch version for update" str := "failed to fetch version for update"
@ -1388,6 +1718,29 @@ func upgradeManager(namespace walletdb.Namespace) error {
version = 2 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 // Ensure the manager is upraded to the latest version. This check is
// to intentionally cause a failure if the manager version is updated // to intentionally cause a failure if the manager version is updated
// without writing code to handle the upgrade. // without writing code to handle the upgrade.
@ -1400,3 +1753,115 @@ func upgradeManager(namespace walletdb.Namespace) error {
return nil 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. // the account manager.
ErrAccountNotFound ErrAccountNotFound
// ErrDuplicate indicates that an address already exists. // ErrDuplicateAddress indicates an address already exists.
ErrDuplicate ErrDuplicateAddress
// ErrDuplicateAccount indicates an account already exists.
ErrDuplicateAccount
// ErrTooManyAddresses indicates that more than the maximum allowed number of // ErrTooManyAddresses indicates that more than the maximum allowed number of
// addresses per account have been requested. // addresses per account have been requested.
@ -146,7 +149,8 @@ var errorCodeStrings = map[ErrorCode]string{
ErrInvalidAccount: "ErrInvalidAccount", ErrInvalidAccount: "ErrInvalidAccount",
ErrAddressNotFound: "ErrAddressNotFound", ErrAddressNotFound: "ErrAddressNotFound",
ErrAccountNotFound: "ErrAccountNotFound", ErrAccountNotFound: "ErrAccountNotFound",
ErrDuplicate: "ErrDuplicate", ErrDuplicateAddress: "ErrDuplicateAddress",
ErrDuplicateAccount: "ErrDuplicateAccount",
ErrTooManyAddresses: "ErrTooManyAddresses", ErrTooManyAddresses: "ErrTooManyAddresses",
ErrWrongPassphrase: "ErrWrongPassphrase", ErrWrongPassphrase: "ErrWrongPassphrase",
ErrWrongNet: "ErrWrongNet", ErrWrongNet: "ErrWrongNet",

View file

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

View file

@ -51,8 +51,14 @@ const (
// fit into that model. // fit into that model.
ImportedAddrAccount = MaxAccountNum + 1 // 2^31 - 1 ImportedAddrAccount = MaxAccountNum + 1 // 2^31 - 1
// defaultAccountNum is the number of the default account. // ImportedAddrAccountName is the name of the imported account.
defaultAccountNum = 0 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: // The hierarchy described by BIP0043 is:
// m/<purpose>'/* // m/<purpose>'/*
@ -82,11 +88,30 @@ const (
saltSize = 32 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. // Options is used to hold the optional parameters passed to Create or Load.
type Options struct { type Options struct {
ScryptN int ScryptN int
ScryptR int ScryptR int
ScryptP 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 // 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 return err
}) })
if err != nil { 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) return nil, maybeConvertDbError(err)
} }
@ -655,6 +686,20 @@ func (m *Manager) Address(address btcutil.Address) (ManagedAddress, error) {
return m.loadAndCacheAddress(address) 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 // ChangePassphrase changes either the public or private passphrase to the
// provided value depending on the private flag. In order to change the private // provided value depending on the private flag. In order to change the private
// password, the address manager must not be watching-only. // 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 { if alreadyExists {
str := fmt.Sprintf("address for public key %x already exists", str := fmt.Sprintf("address for public key %x already exists",
serializedPubKey) serializedPubKey)
return nil, managerError(ErrDuplicate, str, nil) return nil, managerError(ErrDuplicateAddress, str, nil)
} }
// Encrypt public key. // Encrypt public key.
@ -1067,7 +1112,7 @@ func (m *Manager) ImportScript(script []byte, bs *BlockStamp) (ManagedScriptAddr
if alreadyExists { if alreadyExists {
str := fmt.Sprintf("address for script hash %x already exists", str := fmt.Sprintf("address for script hash %x already exists",
scriptHash) 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 // Encrypt the script hash using the crypto public key so it is
@ -1176,6 +1221,29 @@ func (m *Manager) Lock() error {
return nil 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 // Unlock derives the master private key from the specified passphrase. An
// invalid passphrase will return an error. Otherwise, the derived secret key // invalid passphrase will return an error. Otherwise, the derived secret key
// is stored in memory until the address manager is locked. Any failures that // 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 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. // AllActiveAddresses returns a slice of all addresses stored in the manager.
func (m *Manager) AllActiveAddresses() ([]btcutil.Address, error) { func (m *Manager) AllActiveAddresses() ([]btcutil.Address, error) {
m.mtx.Lock() 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 // deriveCoinTypeKey derives the cointype key which can be used to derive the
// hierarchy described by BIP0044 given the master node. // 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: // In particular this is the hierarchical deterministic extended key path:
// m/44'/<coin type>'/<account>' // m/44'/<coin type>'
func deriveAccountKey(masterNode *hdkeychain.ExtendedKey, coinType uint32, func deriveCoinTypeKey(masterNode *hdkeychain.ExtendedKey,
account uint32) (*hdkeychain.ExtendedKey, error) { coinType uint32) (*hdkeychain.ExtendedKey, error) {
// Enforce maximum coin type. // Enforce maximum coin type.
if coinType > maxCoinType { if coinType > maxCoinType {
err := managerError(ErrCoinTypeTooHigh, errCoinTypeTooHigh, nil) err := managerError(ErrCoinTypeTooHigh, errCoinTypeTooHigh, nil)
return nil, err return nil, err
} }
// Enforce maximum account number.
if account > MaxAccountNum {
err := managerError(ErrAccountNumTooHigh, errAcctTooHigh, nil)
return nil, err
}
// The hierarchy described by BIP0043 is: // The hierarchy described by BIP0043 is:
// m/<purpose>'/* // m/<purpose>'/*
// This is further extended by BIP0044 to: // This is further extended by BIP0044 to:
@ -1706,6 +2016,22 @@ func deriveAccountKey(masterNode *hdkeychain.ExtendedKey, coinType uint32,
return nil, err 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. // Derive the account key as a child of the coin type key.
return coinTypeKey.Child(account + hdkeychain.HardenedKeyStart) 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. // 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 return nil, err
} }
@ -1916,8 +2242,15 @@ func Create(namespace walletdb.Namespace, seed, pubPassphrase, privPassphrase []
return nil, managerError(ErrKeyChain, str, err) 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. // 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 { if err != nil {
// The seed is unusable if the any of the children in the // The seed is unusable if the any of the children in the
// required hierarchy can't be derived due to invalid child. // 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) 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. // Encrypt the default account keys with the associated crypto keys.
acctPubEnc, err := cryptoKeyPub.Encrypt([]byte(acctKeyPub.String())) acctPubEnc, err := cryptoKeyPub.Encrypt([]byte(acctKeyPub.String()))
if err != nil { if err != nil {
@ -2048,6 +2398,12 @@ func Create(namespace walletdb.Namespace, seed, pubPassphrase, privPassphrase []
return err 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 // Save the fact this is not a watching-only address manager to
// the database. // the database.
err = putWatchingOnly(tx, false) err = putWatchingOnly(tx, false)
@ -2071,24 +2427,33 @@ func Create(namespace walletdb.Namespace, seed, pubPassphrase, privPassphrase []
return err return err
} }
// Save the information for the default account to the database. // Save the information for the imported account to the database.
err = putAccountInfo(tx, defaultAccountNum, acctPubEnc, err = putAccountInfo(tx, ImportedAddrAccount, nil,
acctPrivEnc, 0, 0, "") nil, 0, 0, ImportedAddrAccountName)
if err != nil { if err != nil {
return err 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 { if err != nil {
return nil, maybeConvertDbError(err) return nil, maybeConvertDbError(err)
} }
// The new address manager is locked by default, so clear the master, // 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() masterKeyPriv.Zero()
cryptoKeyPriv.Zero() cryptoKeyPriv.Zero()
cryptoKeyScript.Zero() cryptoKeyScript.Zero()
coinTypeKeyPriv.Zero()
return newManager(namespace, chainParams, masterKeyPub, masterKeyPriv, return newManager(namespace, chainParams, masterKeyPub, masterKeyPriv,
cryptoKeyPub, cryptoKeyPrivEnc, cryptoKeyScriptEnc, syncInfo, cryptoKeyPub, cryptoKeyPrivEnc, cryptoKeyScriptEnc, syncInfo,
config, privPassphraseSalt), nil config, privPassphraseSalt), nil

View file

@ -1158,6 +1158,221 @@ func testChangePassphrase(tc *testContext) bool {
return true 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 // testManagerAPI tests the functions provided by the Manager API as well as
// the ManagedAddress, ManagedPubKeyAddress, and ManagedScriptAddress // the ManagedAddress, ManagedPubKeyAddress, and ManagedScriptAddress
// interfaces. // interfaces.
@ -1169,6 +1384,13 @@ func testManagerAPI(tc *testContext) {
testImportScript(tc) testImportScript(tc)
testMarkUsed(tc) testMarkUsed(tc)
testChangePassphrase(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 // testWatchingOnly tests various facets of a watching-only address

190
wallet.go
View file

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