diff --git a/createtx.go b/createtx.go index b62cc1f..61daad0 100644 --- a/createtx.go +++ b/createtx.go @@ -128,7 +128,7 @@ func (u ByAmount) Swap(i, j int) { u[i], u[j] = u[j], u[i] } // to addr or as a fee for the miner are sent to a newly generated // address. InsufficientFundsError is returned if there are not enough // eligible unspent outputs to create the transaction. -func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, minconf int) (*CreatedTx, error) { +func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, account uint32, minconf int) (*CreatedTx, error) { // Address manager must be unlocked to compose transaction. Grab // the unlock if possible (to prevent future unlocks), or return the @@ -145,12 +145,12 @@ func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, minconf int) (*Creat return nil, err } - eligible, err := w.findEligibleOutputs(minconf, bs) + eligible, err := w.findEligibleOutputs(account, minconf, bs) if err != nil { return nil, err } - return createTx(eligible, pairs, bs, w.FeeIncrement, w.Manager, w.changeAddress) + return createTx(eligible, pairs, bs, w.FeeIncrement, w.Manager, account, w.NewChangeAddress) } // createTx selects inputs (from the given slice of eligible utxos) @@ -164,7 +164,8 @@ func createTx( bs *waddrmgr.BlockStamp, feeIncrement btcutil.Amount, mgr *waddrmgr.Manager, - changeAddress func(*waddrmgr.BlockStamp) (btcutil.Address, error)) ( + account uint32, + changeAddress func(account uint32) (btcutil.Address, error)) ( *CreatedTx, error) { msgtx := wire.NewMsgTx() @@ -220,7 +221,7 @@ func createTx( change := totalAdded - minAmount - feeEst if change > 0 { if changeAddr == nil { - changeAddr, err = changeAddress(bs) + changeAddr, err = changeAddress(account) if err != nil { return nil, err } @@ -293,23 +294,6 @@ func addChange(msgtx *wire.MsgTx, change btcutil.Amount, changeAddr btcutil.Addr return int(r), nil } -// changeAddress obtains a new btcutil.Address to be used as a change -// transaction output. It will also mark the KeyStore as dirty and -// tells chainSvr to watch that address. -func (w *Wallet) changeAddress(bs *waddrmgr.BlockStamp) (btcutil.Address, error) { - changeAddrs, err := w.Manager.NextInternalAddresses(0, 1) - if err != nil { - return nil, fmt.Errorf("failed to get change address: %s", err) - } - changeAddr := changeAddrs[0].Address() - err = w.chainSvr.NotifyReceived([]btcutil.Address{changeAddr}) - if err != nil { - return nil, fmt.Errorf("cannot request updates for "+ - "change address: %v", err) - } - return changeAddr, nil -} - // addOutputs adds the given address/amount pairs as outputs to msgtx, // returning their total amount. func addOutputs(msgtx *wire.MsgTx, pairs map[string]btcutil.Amount) (btcutil.Amount, error) { @@ -335,7 +319,7 @@ func addOutputs(msgtx *wire.MsgTx, pairs map[string]btcutil.Amount) (btcutil.Amo return minAmount, nil } -func (w *Wallet) findEligibleOutputs(minconf int, bs *waddrmgr.BlockStamp) ([]txstore.Credit, error) { +func (w *Wallet) findEligibleOutputs(account uint32, minconf int, bs *waddrmgr.BlockStamp) ([]txstore.Credit, error) { unspent, err := w.TxStore.UnspentOutputs() if err != nil { return nil, err @@ -365,7 +349,13 @@ func (w *Wallet) findEligibleOutputs(minconf int, bs *waddrmgr.BlockStamp) ([]tx continue } - eligible = append(eligible, unspent[i]) + creditAccount, err := w.CreditAccount(unspent[i]) + if err != nil { + continue + } + if creditAccount == account { + eligible = append(eligible, unspent[i]) + } } } return eligible, nil diff --git a/createtx_test.go b/createtx_test.go index c659c42..b1fb46d 100644 --- a/createtx_test.go +++ b/createtx_test.go @@ -73,8 +73,9 @@ func TestCreateTx(t *testing.T) { cfg = &config{DisallowFree: false} bs := &waddrmgr.BlockStamp{Height: 11111} mgr := newManager(t, txInfo.privKeys, bs) + account := uint32(0) changeAddr, _ := btcutil.DecodeAddress("muqW4gcixv58tVbSKRC5q6CRKy8RmyLgZ5", activeNet.Params) - var tstChangeAddress = func(bs *waddrmgr.BlockStamp) (btcutil.Address, error) { + var tstChangeAddress = func(account uint32) (btcutil.Address, error) { return changeAddr, nil } @@ -82,7 +83,7 @@ func TestCreateTx(t *testing.T) { eligible := eligibleInputsFromTx(t, txInfo.hex, []uint32{1, 2, 3, 4, 5}) // Now create a new TX sending 25e6 satoshis to the following addresses: outputs := map[string]btcutil.Amount{outAddr1: 15e6, outAddr2: 10e6} - tx, err := createTx(eligible, outputs, bs, defaultFeeIncrement, mgr, tstChangeAddress) + tx, err := createTx(eligible, outputs, bs, defaultFeeIncrement, mgr, account, tstChangeAddress) if err != nil { t.Fatal(err) } @@ -124,12 +125,13 @@ func TestCreateTxInsufficientFundsError(t *testing.T) { outputs := map[string]btcutil.Amount{outAddr1: 10, outAddr2: 1e9} eligible := eligibleInputsFromTx(t, txInfo.hex, []uint32{1}) bs := &waddrmgr.BlockStamp{Height: 11111} + account := uint32(0) changeAddr, _ := btcutil.DecodeAddress("muqW4gcixv58tVbSKRC5q6CRKy8RmyLgZ5", activeNet.Params) - var tstChangeAddress = func(bs *waddrmgr.BlockStamp) (btcutil.Address, error) { + var tstChangeAddress = func(account uint32) (btcutil.Address, error) { return changeAddr, nil } - _, err := createTx(eligible, outputs, bs, defaultFeeIncrement, nil, tstChangeAddress) + _, err := createTx(eligible, outputs, bs, defaultFeeIncrement, nil, account, tstChangeAddress) if err == nil { t.Error("Expected InsufficientFundsError, got no error") diff --git a/rpcserver.go b/rpcserver.go index a21d2ae..473296d 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -89,8 +89,9 @@ var ( errors.New("minconf must be positive"), } - ErrAddressNotInWallet = InvalidAddressOrKeyError{ - errors.New("address not found in wallet"), + ErrAddressNotInWallet = btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: "address not found in wallet", } ErrNoAccountSupport = btcjson.Error{ @@ -98,6 +99,11 @@ var ( Message: "btcwallet does not support non-default accounts", } + ErrAccountNameNotFound = btcjson.Error{ + Code: btcjson.ErrWalletInvalidAccountName.Code, + Message: "account name not found", + } + ErrUnloadedWallet = btcjson.Error{ Code: btcjson.ErrWallet.Code, Message: "Request requires a wallet but wallet has not loaded yet", @@ -179,15 +185,25 @@ func isManagerWrongPassphraseError(err error) bool { return ok && merr.ErrorCode == waddrmgr.ErrWrongPassphrase } -// isManagerDuplicateError returns whether or not the passed error is due to a +// isManagerDuplicateAddressError returns whether or not the passed error is due to a // duplicate item being provided to the address manager. -func isManagerDuplicateError(err error) bool { +func isManagerDuplicateAddressError(err error) bool { merr, ok := err.(waddrmgr.ManagerError) - if !ok { - return false - } + return ok && merr.ErrorCode == waddrmgr.ErrDuplicateAddress +} - return merr.ErrorCode == waddrmgr.ErrDuplicate +// isManagerAddressNotFoundError returns whether or not the passed error is due to a +// the address not being found. +func isManagerAddressNotFoundError(err error) bool { + merr, ok := err.(waddrmgr.ManagerError) + return ok && merr.ErrorCode == waddrmgr.ErrAddressNotFound +} + +// isManagerAccountNotFoundError returns whether or not the passed error is due +// to the account not being found. +func isManagerAccountNotFoundError(err error) bool { + merr, ok := err.(waddrmgr.ManagerError) + return ok && merr.ErrorCode == waddrmgr.ErrAccountNotFound } // parseListeners splits the list of listen addresses passed in addrs into @@ -984,7 +1000,7 @@ func (s *rpcServer) PostClientRPC(w http.ResponseWriter, r *http.Request) { return } - // Create the response and error from the request. Three special cases + // Create the response and error from the request. Two special cases // are handled for the authenticate and stop request methods. var resp btcjson.Reply switch raw.Method { @@ -1044,13 +1060,18 @@ func (b blockDisconnected) notificationCmds(w *Wallet) []btcjson.Cmd { func (c txCredit) notificationCmds(w *Wallet) []btcjson.Cmd { blk := w.Manager.SyncedTo() - ltr, err := txstore.Credit(c).ToJSON("", blk.Height, activeNet.Params) + acctName := waddrmgr.DefaultAccountName + if creditAccount, err := w.CreditAccount(txstore.Credit(c)); err == nil { + // acctName is defaulted to DefaultAccountName in case of an error + acctName, _ = w.Manager.AccountName(creditAccount) + } + ltr, err := txstore.Credit(c).ToJSON(acctName, blk.Height, activeNet.Params) if err != nil { log.Errorf("Cannot create notification for transaction "+ "credit: %v", err) return nil } - n := btcws.NewTxNtfn("", <r) + n := btcws.NewTxNtfn(acctName, <r) return []btcjson.Cmd{n} } @@ -1076,13 +1097,13 @@ func (l managerLocked) notificationCmds(w *Wallet) []btcjson.Cmd { func (b confirmedBalance) notificationCmds(w *Wallet) []btcjson.Cmd { n := btcws.NewAccountBalanceNtfn("", - btcutil.Amount(b).ToUnit(btcutil.AmountBTC), true) + btcutil.Amount(b).ToBTC(), true) return []btcjson.Cmd{n} } func (b unconfirmedBalance) notificationCmds(w *Wallet) []btcjson.Cmd { n := btcws.NewAccountBalanceNtfn("", - btcutil.Amount(b).ToUnit(btcutil.AmountBTC), false) + btcutil.Amount(b).ToBTC(), false) return []btcjson.Cmd{n} } @@ -1359,6 +1380,7 @@ var rpcHandlers = map[string]requestHandler{ "setaccount": Unsupported, // Extensions to the reference client JSON-RPC API + "createnewaccount": CreateNewAccount, "exportwatchingwallet": ExportWatchingWallet, "getbestblock": GetBestBlock, // This was an extension but the reference implementation added it as @@ -1368,6 +1390,7 @@ var rpcHandlers = map[string]requestHandler{ "getunconfirmedbalance": GetUnconfirmedBalance, "listaddresstransactions": ListAddressTransactions, "listalltransactions": ListAllTransactions, + "renameaccount": RenameAccount, "walletislocked": WalletIsLocked, } @@ -1642,12 +1665,22 @@ func ExportWatchingWallet(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) ( func GetAddressesByAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { cmd := icmd.(*btcjson.GetAddressesByAccountCmd) - err := checkAccountName(cmd.Account) + account, err := w.Manager.LookupAccount(cmd.Account) if err != nil { return nil, err } - return w.SortedActivePaymentAddresses() + addrs, err := w.Manager.AllAccountAddresses(account) + if err != nil { + return nil, err + } + + addrStrs := make([]string, len(addrs)) + for i, addr := range addrs { + addrStrs[i] = addr.Address().EncodeAddress() + } + + return addrStrs, nil } // GetBalance handles a getbalance request by returning the balance for an @@ -1656,22 +1689,22 @@ func GetAddressesByAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) func GetBalance(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { cmd := icmd.(*btcjson.GetBalanceCmd) - var account string - if cmd.Account != nil { - account = *cmd.Account + var balance btcutil.Amount + var account uint32 + var err error + if cmd.Account == nil || *cmd.Account == "*" { + balance, err = w.CalculateBalance(cmd.MinConf) + } else { + account, err = w.Manager.LookupAccount(*cmd.Account) + if err != nil { + return nil, err + } + balance, err = w.CalculateAccountBalance(account, cmd.MinConf) } - - err := checkAccountName(account) if err != nil { return nil, err } - - balance, err := w.CalculateBalance(cmd.MinConf) - if err != nil { - return nil, err - } - - return balance.ToUnit(btcutil.AmountBTC), nil + return balance.ToBTC(), nil } // GetBestBlock handles a getbestblock request by returning a JSON object @@ -1718,11 +1751,11 @@ func GetInfo(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, // TODO(davec): This should probably have a database version as opposed // to using the manager version. info.WalletVersion = int32(waddrmgr.LatestMgrVersion) - info.Balance = bal.ToUnit(btcutil.AmountBTC) + info.Balance = bal.ToBTC() // Keypool times are not tracked. set to current time. info.KeypoolOldest = time.Now().Unix() info.KeypoolSize = int32(cfg.KeypoolSize) - info.PaytxFee = w.FeeIncrement.ToUnit(btcutil.AmountBTC) + info.PaytxFee = w.FeeIncrement.ToBTC() // We don't set the following since they don't make much sense in the // wallet architecture: // - unlocked_until @@ -1742,13 +1775,17 @@ func GetAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{ return nil, btcjson.ErrInvalidAddressOrKey } - // If it is in the wallet, we consider it part of the default account. - _, err = w.Manager.Address(addr) + // Fetch the associated account + account, err := w.Manager.AddrAccount(addr) if err != nil { - return nil, btcjson.ErrInvalidAddressOrKey + return nil, ErrAddressNotInWallet } - return "", nil + acctName, err := w.Manager.AccountName(account) + if err != nil { + return nil, ErrAccountNameNotFound + } + return acctName, nil } // GetAccountAddress handles a getaccountaddress by returning the most @@ -1760,12 +1797,11 @@ func GetAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{ func GetAccountAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { cmd := icmd.(*btcjson.GetAccountAddressCmd) - err := checkDefaultAccount(cmd.Account) + account, err := w.Manager.LookupAccount(cmd.Account) if err != nil { return nil, err } - - addr, err := w.CurrentAddress() + addr, err := w.CurrentAddress(account) if err != nil { return nil, err } @@ -1778,21 +1814,21 @@ func GetAccountAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (int func GetUnconfirmedBalance(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { cmd := icmd.(*btcws.GetUnconfirmedBalanceCmd) - err := checkAccountName(cmd.Account) + account, err := w.Manager.LookupAccount(cmd.Account) if err != nil { return nil, err } - unconfirmed, err := w.CalculateBalance(0) + unconfirmed, err := w.CalculateAccountBalance(account, 0) if err != nil { return nil, err } - confirmed, err := w.CalculateBalance(1) + confirmed, err := w.CalculateAccountBalance(account, 1) if err != nil { return nil, err } - return (unconfirmed - confirmed).ToUnit(btcutil.AmountBTC), nil + return (unconfirmed - confirmed).ToBTC(), nil } // ImportPrivKey handles an importprivkey request by parsing @@ -1814,7 +1850,7 @@ func ImportPrivKey(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interfa // Import the private key, handling any errors. _, err = w.ImportPrivateKey(wif, nil, cmd.Rescan) switch { - case isManagerDuplicateError(err): + case isManagerDuplicateAddressError(err): // Do not return duplicate key errors to the client. return nil, nil case isManagerLockedError(err): @@ -1830,18 +1866,61 @@ func KeypoolRefill(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interfa return nil, nil } -// GetNewAddress handlesa getnewaddress request by returning a new -// address for an account. If the account does not exist or the keypool -// ran out with a locked wallet, an appropiate error is returned. +// CreateNewAccount handles a createnewaccount request by creating and +// returning a new account. If the last account has no transaction history +// as per BIP 0044 a new account cannot be created so an error will be returned. +func CreateNewAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcws.CreateNewAccountCmd) + + // Check that we are within the maximum allowed non-empty accounts limit. + account, err := w.Manager.LastAccount() + if err != nil { + return nil, err + } + if account > maxEmptyAccounts { + used, err := w.AccountUsed(account) + if err != nil { + return nil, err + } + if !used { + return nil, errors.New("cannot create account: " + + "previous account has no transaction history") + } + } + + _, err = w.Manager.NewAccount(cmd.Account) + if isManagerLockedError(err) { + return nil, btcjson.ErrWalletUnlockNeeded + } + return nil, err +} + +// RenameAccount handles a renameaccount request by renaming an account. +// If the account does not exist an appropiate error will be returned. +func RenameAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { + cmd := icmd.(*btcws.RenameAccountCmd) + // Check that given account exists + account, err := w.Manager.LookupAccount(cmd.OldAccount) + if err != nil { + return nil, err + } + return nil, w.Manager.RenameAccount(account, cmd.NewAccount) +} + +// GetNewAddress handles a getnewaddress request by returning a new +// address for an account. If the account does not exist an appropiate +// error is returned. +// TODO: Follow BIP 0044 and warn if number of unused addresses exceeds +// the gap limit. func GetNewAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { cmd := icmd.(*btcjson.GetNewAddressCmd) - err := checkDefaultAccount(cmd.Account) + account, err := w.Manager.LookupAccount(cmd.Account) if err != nil { return nil, err } - addr, err := w.NewAddress() + addr, err := w.NewAddress(account) if err != nil { return nil, err } @@ -1856,7 +1935,12 @@ func GetNewAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interfa // Note: bitcoind allows specifying the account as an optional parameter, // but ignores the parameter. func GetRawChangeAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { - addr, err := w.NewChangeAddress() + cmd := icmd.(*btcjson.GetRawChangeAddressCmd) + account, err := w.Manager.LookupAccount(cmd.Account) + if err != nil { + return nil, err + } + addr, err := w.NewChangeAddress(account) if err != nil { return nil, err } @@ -1870,17 +1954,17 @@ func GetRawChangeAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (i func GetReceivedByAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { cmd := icmd.(*btcjson.GetReceivedByAccountCmd) - err := checkAccountName(cmd.Account) + account, err := w.Manager.LookupAccount(cmd.Account) if err != nil { return nil, err } - bal, err := w.TotalReceived(cmd.MinConf) + bal, _, err := w.TotalReceivedForAccount(account, cmd.MinConf) if err != nil { return nil, err } - return bal.ToUnit(btcutil.AmountBTC), nil + return bal.ToBTC(), nil } // GetReceivedByAddress handles a getreceivedbyaddress request by returning @@ -1897,7 +1981,7 @@ func GetReceivedByAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) ( return nil, err } - return total.ToUnit(btcutil.AmountBTC), nil + return total.ToBTC(), nil } // GetTransaction handles a gettransaction request by returning details about @@ -1957,11 +2041,11 @@ func GetTransaction(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interf ret.Details = make([]btcjson.GetTransactionDetailsResult, 1, len(credits)+1) details := btcjson.GetTransactionDetailsResult{ - Account: "", + Account: waddrmgr.DefaultAccountName, Category: "send", // negative since it is a send - Amount: (-debits.OutputAmount(true)).ToUnit(btcutil.AmountBTC), - Fee: debits.Fee().ToUnit(btcutil.AmountBTC), + Amount: (-debits.OutputAmount(true)).ToBTC(), + Fee: debits.Fee().ToBTC(), } targetAddr = &details.Address ret.Details[0] = details @@ -1992,14 +2076,14 @@ func GetTransaction(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interf } ret.Details = append(ret.Details, btcjson.GetTransactionDetailsResult{ - Account: "", + Account: waddrmgr.DefaultAccountName, Category: cred.Category(blk.Height).String(), - Amount: cred.Amount().ToUnit(btcutil.AmountBTC), + Amount: cred.Amount().ToBTC(), Address: addr, }) } - ret.Amount = creditAmount.ToUnit(btcutil.AmountBTC) + ret.Amount = creditAmount.ToBTC() return ret, nil } @@ -2008,13 +2092,24 @@ func GetTransaction(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interf func ListAccounts(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { cmd := icmd.(*btcjson.ListAccountsCmd) - bal, err := w.CalculateBalance(cmd.MinConf) + accountBalances := map[string]float64{} + accounts, err := w.Manager.AllAccounts() if err != nil { return nil, err } - + for _, account := range accounts { + acctName, err := w.Manager.AccountName(account) + if err != nil { + return nil, ErrAccountNameNotFound + } + bal, err := w.CalculateAccountBalance(account, cmd.MinConf) + if err != nil { + return nil, err + } + accountBalances[acctName] = bal.ToBTC() + } // Return the map. This will be marshaled into a JSON object. - return map[string]float64{"": bal.ToUnit(btcutil.AmountBTC)}, nil + return accountBalances, nil } // ListLockUnspent handles a listlockunspent request by returning an slice of @@ -2033,36 +2128,29 @@ func ListLockUnspent(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (inter // default: one; // "includeempty": whether or not to include addresses that have no transactions - // default: false. -// Since btcwallet doesn't implement account support yet, only the default account "" -// will be returned func ListReceivedByAccount(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { cmd := icmd.(*btcjson.ListReceivedByAccountCmd) - blk := w.Manager.SyncedTo() - - // Total amount received. - var amount btcutil.Amount - - // Number of confirmations of the last transaction. - var confirmations int32 - - for _, record := range w.TxStore.Records() { - for _, credit := range record.Credits() { - if !credit.Confirmed(cmd.MinConf, blk.Height) { - // Not enough confirmations, skip the current block. - continue - } - amount += credit.Amount() - confirmations = credit.Confirmations(blk.Height) - } + accounts, err := w.Manager.AllAccounts() + if err != nil { + return nil, err } - ret := []btcjson.ListReceivedByAccountResult{ - { - Account: "", - Amount: amount.ToUnit(btcutil.AmountBTC), + ret := make([]btcjson.ListReceivedByAccountResult, 0, len(accounts)) + for _, account := range accounts { + acctName, err := w.Manager.AccountName(account) + if err != nil { + return nil, ErrAccountNameNotFound + } + bal, confirmations, err := w.TotalReceivedForAccount(account, cmd.MinConf) + if err != nil { + return nil, err + } + ret = append(ret, btcjson.ListReceivedByAccountResult{ + Account: acctName, + Amount: bal.ToBTC(), Confirmations: uint64(confirmations), - }, + }) } return ret, nil } @@ -2089,6 +2177,8 @@ func ListReceivedByAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) confirmations int32 // Hashes of transactions which include an output paying to the address tx []string + // Account which the address belongs to + account string } blk := w.Manager.SyncedTo() @@ -2144,9 +2234,9 @@ func ListReceivedByAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) idx := 0 for address, addrData := range allAddrData { ret[idx] = btcjson.ListReceivedByAddressResult{ - Account: "", + Account: waddrmgr.DefaultAccountName, Address: address, - Amount: addrData.amount.ToUnit(btcutil.AmountBTC), + Amount: addrData.amount.ToBTC(), Confirmations: uint64(addrData.confirmations), TxIDs: addrData.tx, } @@ -2204,14 +2294,11 @@ func ListSinceBlock(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interf func ListTransactions(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { cmd := icmd.(*btcjson.ListTransactionsCmd) - var account string if cmd.Account != nil { - account = *cmd.Account - } - - err := checkAccountName(account) - if err != nil { - return nil, err + err := checkAccountName(*cmd.Account) + if err != nil { + return nil, err + } } return w.ListTransactions(cmd.From, cmd.Count) @@ -2254,14 +2341,11 @@ func ListAddressTransactions(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd func ListAllTransactions(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { cmd := icmd.(*btcws.ListAllTransactionsCmd) - var account string if cmd.Account != nil { - account = *cmd.Account - } - - err := checkAccountName(account) - if err != nil { - return nil, err + err := checkAccountName(*cmd.Account) + if err != nil { + return nil, err + } } return w.ListAllTransactions() @@ -2318,11 +2402,11 @@ func LockUnspent(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface // sendPairs is a helper routine to reduce duplicated code when creating and // sending payment transactions. func sendPairs(w *Wallet, chainSvr *chain.Client, cmd btcjson.Cmd, - amounts map[string]btcutil.Amount, minconf int) (interface{}, error) { + amounts map[string]btcutil.Amount, account uint32, minconf int) (interface{}, error) { // Create transaction, replying with an error if the creation // was not successful. - createdTx, err := w.CreateSimpleTx(amounts, minconf) + createdTx, err := w.CreateSimpleTx(account, amounts, minconf) if err != nil { switch { case err == ErrNonPositiveAmount: @@ -2371,7 +2455,7 @@ func sendPairs(w *Wallet, chainSvr *chain.Client, cmd btcjson.Cmd, func SendFrom(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { cmd := icmd.(*btcjson.SendFromCmd) - err := checkAccountName(cmd.FromAccount) + account, err := w.Manager.LookupAccount(cmd.FromAccount) if err != nil { return nil, err } @@ -2388,7 +2472,7 @@ func SendFrom(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, cmd.ToAddress: btcutil.Amount(cmd.Amount), } - return sendPairs(w, chainSvr, cmd, pairs, cmd.MinConf) + return sendPairs(w, chainSvr, cmd, pairs, account, cmd.MinConf) } // SendMany handles a sendmany RPC request by creating a new transaction @@ -2399,7 +2483,7 @@ func SendFrom(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, func SendMany(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, error) { cmd := icmd.(*btcjson.SendManyCmd) - err := checkAccountName(cmd.FromAccount) + account, err := w.Manager.LookupAccount(cmd.FromAccount) if err != nil { return nil, err } @@ -2415,7 +2499,7 @@ func SendMany(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interface{}, pairs[k] = btcutil.Amount(v) } - return sendPairs(w, chainSvr, cmd, pairs, cmd.MinConf) + return sendPairs(w, chainSvr, cmd, pairs, account, cmd.MinConf) } // SendToAddress handles a sendtoaddress RPC request by creating a new @@ -2436,7 +2520,8 @@ func SendToAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (interfa cmd.Address: btcutil.Amount(cmd.Amount), } - return sendPairs(w, chainSvr, cmd, pairs, 1) + // sendtoaddress always spends from the default account, this matches bitcoind + return sendPairs(w, chainSvr, cmd, pairs, waddrmgr.DefaultAccountNum, 1) } // SetTxFee sets the transaction fee per kilobyte added to transactions. @@ -2785,20 +2870,22 @@ func ValidateAddress(w *Wallet, chainSvr *chain.Client, icmd btcjson.Cmd) (inter result.IsValid = true ainfo, err := w.Manager.Address(addr) - if managerErr, ok := err.(waddrmgr.ManagerError); ok { - if managerErr.ErrorCode == waddrmgr.ErrAddressNotFound { + if err != nil { + if isManagerAddressNotFoundError(err) { // No additional information available about the address. return result, nil } - } - if err != nil { return nil, err } // The address lookup was successful which means there is further // information about it available and it is "mine". result.IsMine = true - result.Account = "" + acctName, err := w.Manager.AccountName(ainfo.Account()) + if err != nil { + return nil, ErrAccountNameNotFound + } + result.Account = acctName switch ma := ainfo.(type) { case waddrmgr.ManagedPubKeyAddress: diff --git a/txstore/json.go b/txstore/json.go index ced3a00..549ef8c 100644 --- a/txstore/json.go +++ b/txstore/json.go @@ -78,8 +78,8 @@ func (d Debits) toJSON(account string, chainHeight int32, Account: account, Address: address, Category: "send", - Amount: btcutil.Amount(-txOut.Value).ToUnit(btcutil.AmountBTC), - Fee: d.Fee().ToUnit(btcutil.AmountBTC), + Amount: btcutil.Amount(-txOut.Value).ToBTC(), + Fee: d.Fee().ToBTC(), TxID: d.Tx().Sha().String(), Time: d.txRecord.received.Unix(), TimeReceived: d.txRecord.received.Unix(), @@ -176,7 +176,7 @@ func (c Credit) toJSON(account string, chainHeight int32, Account: account, Category: c.category(chainHeight).String(), Address: address, - Amount: btcutil.Amount(txout.Value).ToUnit(btcutil.AmountBTC), + Amount: btcutil.Amount(txout.Value).ToBTC(), TxID: c.Tx().Sha().String(), Time: c.received.Unix(), TimeReceived: c.received.Unix(), diff --git a/waddrmgr/db.go b/waddrmgr/db.go index 5edc440..9f346a0 100644 --- a/waddrmgr/db.go +++ b/waddrmgr/db.go @@ -17,19 +17,20 @@ package waddrmgr import ( - "bytes" "encoding/binary" "fmt" "time" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil/hdkeychain" "github.com/btcsuite/btcwallet/walletdb" "github.com/btcsuite/fastsha256" ) const ( // LatestMgrVersion is the most recent manager version. - LatestMgrVersion = 2 + LatestMgrVersion = 3 ) var ( @@ -38,6 +39,11 @@ var ( latestMgrVersion uint32 = LatestMgrVersion ) +// ObtainUserInputFunc is a function that reads a user input and returns it as +// a byte stream. It is used to accept data required during upgrades, for e.g. +// wallet seed and private passphrase. +type ObtainUserInputFunc func() ([]byte, error) + // maybeConvertDbError converts the passed error to a ManagerError with an // error code of ErrDatabase if it is not already a ManagerError. This is // useful for potential errors returned from managed transaction an other parts @@ -137,12 +143,48 @@ type dbScriptAddressRow struct { // Key names for various database fields. var ( + // nullVall is null byte used as a flag value in a bucket entry + nullVal = []byte{0} + // Bucket names. - acctBucketName = []byte("acct") - addrBucketName = []byte("addr") + acctBucketName = []byte("acct") + addrBucketName = []byte("addr") + + // addrAcctIdxBucketName is used to index account addresses + // Entries in this index may map: + // * addr hash => account id + // * account bucket -> addr hash => null + // To fetch the account of an address, lookup the value using + // the address hash. + // To fetch all addresses of an account, fetch the account bucket, iterate + // over the keys and fetch the address row from the addr bucket. + // The index needs to be updated whenever an address is created e.g. + // NewAddress addrAcctIdxBucketName = []byte("addracctidx") - mainBucketName = []byte("main") - syncBucketName = []byte("sync") + + // acctNameIdxBucketName is used to create an index + // mapping an account name string to the corresponding + // account id. + // The index needs to be updated whenever the account name + // and id changes e.g. RenameAccount + acctNameIdxBucketName = []byte("acctnameidx") + + // acctIdIdxBucketName is used to create an index + // mapping an account id to the corresponding + // account name string. + // The index needs to be updated whenever the account name + // and id changes e.g. RenameAccount + acctIdIdxBucketName = []byte("acctididx") + + // meta is used to store meta-data about the address manager + // e.g. last account number + metaBucketName = []byte("meta") + // lastAccountName is used to store the metadata - last account + // in the manager + lastAccountName = []byte("lastaccount") + + mainBucketName = []byte("main") + syncBucketName = []byte("sync") // Db related key names (main bucket). mgrVersionName = []byte("mgrver") @@ -154,6 +196,8 @@ var ( cryptoPrivKeyName = []byte("cpriv") cryptoPubKeyName = []byte("cpub") cryptoScriptKeyName = []byte("cscript") + coinTypePrivKeyName = []byte("ctpriv") + coinTypePubKeyName = []byte("ctpub") watchingOnlyName = []byte("watchonly") // Sync related key names (sync bucket). @@ -176,6 +220,40 @@ func uint32ToBytes(number uint32) []byte { return buf } +// uint64ToBytes converts a 64 bit unsigned integer into a 8-byte slice in +// little-endian order: 1 -> [1 0 0 0 0 0 0 0]. +func uint64ToBytes(number uint64) []byte { + buf := make([]byte, 8) + binary.LittleEndian.PutUint64(buf, number) + return buf +} + +// stringToBytes converts a string into a variable length byte slice in +// little-endian order: "abc" -> [3 0 0 0 61 62 63] +func stringToBytes(s string) []byte { + // The serialized format is: + // + // + // 4 bytes string size + string + size := len(s) + buf := make([]byte, 4+size) + copy(buf[0:4], uint32ToBytes(uint32(size))) + copy(buf[4:4+size], s) + return buf +} + +// fetchManagerVersion fetches the current manager version from the database. +func fetchManagerVersion(tx walletdb.Tx) (uint32, error) { + mainBucket := tx.RootBucket().Bucket(mainBucketName) + verBytes := mainBucket.Get(mgrVersionName) + if verBytes == nil { + str := "required version number not stored in database" + return 0, managerError(ErrDatabase, str, nil) + } + version := binary.LittleEndian.Uint32(verBytes) + return version, nil +} + // putManagerVersion stores the provided version to the database. func putManagerVersion(tx walletdb.Tx, version uint32) error { bucket := tx.RootBucket().Bucket(mainBucketName) @@ -242,6 +320,50 @@ func putMasterKeyParams(tx walletdb.Tx, pubParams, privParams []byte) error { return nil } +// fetchCoinTypeKeys loads the encrypted cointype keys which are in turn used to +// derive the extended keys for all accounts. +func fetchCoinTypeKeys(tx walletdb.Tx) ([]byte, []byte, error) { + bucket := tx.RootBucket().Bucket(mainBucketName) + + coinTypePubKeyEnc := bucket.Get(coinTypePubKeyName) + if coinTypePubKeyEnc == nil { + str := "required encrypted cointype public key not stored in database" + return nil, nil, managerError(ErrDatabase, str, nil) + } + + coinTypePrivKeyEnc := bucket.Get(coinTypePrivKeyName) + if coinTypePrivKeyEnc == nil { + str := "required encrypted cointype private key not stored in database" + return nil, nil, managerError(ErrDatabase, str, nil) + } + return coinTypePubKeyEnc, coinTypePrivKeyEnc, nil +} + +// putCoinTypeKeys stores the encrypted cointype keys which are in turn used to +// derive the extended keys for all accounts. Either parameter can be nil in which +// case no value is written for the parameter. +func putCoinTypeKeys(tx walletdb.Tx, coinTypePubKeyEnc []byte, coinTypePrivKeyEnc []byte) error { + bucket := tx.RootBucket().Bucket(mainBucketName) + + if coinTypePubKeyEnc != nil { + err := bucket.Put(coinTypePubKeyName, coinTypePubKeyEnc) + if err != nil { + str := "failed to store encrypted cointype public key" + return managerError(ErrDatabase, str, err) + } + } + + if coinTypePrivKeyEnc != nil { + err := bucket.Put(coinTypePrivKeyName, coinTypePrivKeyEnc) + if err != nil { + str := "failed to store encrypted cointype private key" + return managerError(ErrDatabase, str, err) + } + } + + return nil +} + // fetchCryptoKeys loads the encrypted crypto keys which are in turn used to // protect the extended keys, imported keys, and scripts. Any of the returned // values can be nil, but in practice only the crypto private and script keys @@ -455,6 +577,70 @@ func serializeBIP0044AccountRow(encryptedPubKey, return rawData } +// fetchAllAccounts loads information about all accounts from the database. +// The returned value is a slice of account numbers which can be used to load +// the respective account rows. +// TODO(tuxcanfly): Switch over to an iterator to support the maximum of 2^31-2 accounts +func fetchAllAccounts(tx walletdb.Tx) ([]uint32, error) { + bucket := tx.RootBucket().Bucket(acctBucketName) + + var accounts []uint32 + err := bucket.ForEach(func(k, v []byte) error { + // Skip buckets. + if v == nil { + return nil + } + accounts = append(accounts, binary.LittleEndian.Uint32(k)) + return nil + }) + return accounts, err +} + +// fetchLastAccount retreives the last account from the database. +func fetchLastAccount(tx walletdb.Tx) (uint32, error) { + bucket := tx.RootBucket().Bucket(metaBucketName) + + val := bucket.Get(lastAccountName) + if len(val) != 4 { + str := fmt.Sprintf("malformed metadata '%s' stored in database", + lastAccountName) + return 0, managerError(ErrDatabase, str, nil) + } + account := binary.LittleEndian.Uint32(val[0:4]) + return account, nil +} + +// fetchAccountName retreives the account name given an account number from +// the database. +func fetchAccountName(tx walletdb.Tx, account uint32) (string, error) { + bucket := tx.RootBucket().Bucket(acctIdIdxBucketName) + + val := bucket.Get(uint32ToBytes(account)) + if val == nil { + str := fmt.Sprintf("account %d not found", account) + return "", managerError(ErrAccountNotFound, str, nil) + } + offset := uint32(0) + nameLen := binary.LittleEndian.Uint32(val[offset : offset+4]) + offset += 4 + acctName := string(val[offset : offset+nameLen]) + return acctName, nil +} + +// fetchAccountByName retreives the account number given an account name +// from the database. +func fetchAccountByName(tx walletdb.Tx, name string) (uint32, error) { + bucket := tx.RootBucket().Bucket(acctNameIdxBucketName) + + val := bucket.Get(stringToBytes(name)) + if val == nil { + str := fmt.Sprintf("account name '%s' not found", name) + return 0, managerError(ErrAccountNotFound, str, nil) + } + + return binary.LittleEndian.Uint32(val), nil +} + // fetchAccountInfo loads information about the passed account from the // database. func fetchAccountInfo(tx walletdb.Tx, account uint32) (interface{}, error) { @@ -481,6 +667,81 @@ func fetchAccountInfo(tx walletdb.Tx, account uint32) (interface{}, error) { return nil, managerError(ErrDatabase, str, nil) } +// deleteAccountNameIndex deletes the given key from the account name index of the database. +func deleteAccountNameIndex(tx walletdb.Tx, name string) error { + bucket := tx.RootBucket().Bucket(acctNameIdxBucketName) + + // Delete the account name key + err := bucket.Delete(stringToBytes(name)) + if err != nil { + str := fmt.Sprintf("failed to delete account name index key %s", name) + return managerError(ErrDatabase, str, err) + } + return nil +} + +// deleteAccounIdIndex deletes the given key from the account id index of the database. +func deleteAccountIdIndex(tx walletdb.Tx, account uint32) error { + bucket := tx.RootBucket().Bucket(acctIdIdxBucketName) + + // Delete the account id key + err := bucket.Delete(uint32ToBytes(account)) + if err != nil { + str := fmt.Sprintf("failed to delete account id index key %d", account) + return managerError(ErrDatabase, str, err) + } + return nil +} + +// putAccountNameIndex stores the given key to the account name index of the database. +func putAccountNameIndex(tx walletdb.Tx, account uint32, name string) error { + bucket := tx.RootBucket().Bucket(acctNameIdxBucketName) + + // Write the account number keyed by the account name. + err := bucket.Put(stringToBytes(name), uint32ToBytes(account)) + if err != nil { + str := fmt.Sprintf("failed to store account name index key %s", name) + return managerError(ErrDatabase, str, err) + } + return nil +} + +// putAccountIdIndex stores the given key to the account id index of the database. +func putAccountIdIndex(tx walletdb.Tx, account uint32, name string) error { + bucket := tx.RootBucket().Bucket(acctIdIdxBucketName) + + // Write the account number keyed by the account id. + err := bucket.Put(uint32ToBytes(account), stringToBytes(name)) + if err != nil { + str := fmt.Sprintf("failed to store account id index key %s", name) + return managerError(ErrDatabase, str, err) + } + return nil +} + +// putAddrAccountIndex stores the given key to the address account index of the database. +func putAddrAccountIndex(tx walletdb.Tx, account uint32, addrHash []byte) error { + bucket := tx.RootBucket().Bucket(addrAcctIdxBucketName) + + // Write account keyed by address hash + err := bucket.Put(addrHash, uint32ToBytes(account)) + if err != nil { + return nil + } + + bucket, err = bucket.CreateBucketIfNotExists(uint32ToBytes(account)) + if err != nil { + return err + } + // In account bucket, write a null value keyed by the address hash + err = bucket.Put(addrHash, nullVal) + if err != nil { + str := fmt.Sprintf("failed to store address account index key %s", addrHash) + return managerError(ErrDatabase, str, err) + } + return nil +} + // putAccountRow stores the provided account information to the database. This // is used a common base for storing the various account types. func putAccountRow(tx walletdb.Tx, account uint32, row *dbAccountRow) error { @@ -507,36 +768,30 @@ func putAccountInfo(tx walletdb.Tx, account uint32, encryptedPubKey, acctType: actBIP0044, rawData: rawData, } - return putAccountRow(tx, account, &acctRow) -} - -// fetchNumAccounts loads the number of accounts that have been created from -// the database. -func fetchNumAccounts(tx walletdb.Tx) (uint32, error) { - bucket := tx.RootBucket().Bucket(acctBucketName) - - val := bucket.Get(acctNumAcctsName) - if val == nil { - str := "required num accounts not stored in database" - return 0, managerError(ErrDatabase, str, nil) + if err := putAccountRow(tx, account, &acctRow); err != nil { + return err + } + // Update account id index + if err := putAccountIdIndex(tx, account, name); err != nil { + return err + } + // Update account name index + if err := putAccountNameIndex(tx, account, name); err != nil { + return err } - return binary.LittleEndian.Uint32(val), nil + return nil } -// putNumAccounts stores the number of accounts that have been created to the -// database. -func putNumAccounts(tx walletdb.Tx, numAccounts uint32) error { - bucket := tx.RootBucket().Bucket(acctBucketName) +// putLastAccount stores the provided metadata - last account - to the database. +func putLastAccount(tx walletdb.Tx, account uint32) error { + bucket := tx.RootBucket().Bucket(metaBucketName) - var val [4]byte - binary.LittleEndian.PutUint32(val[:], numAccounts) - err := bucket.Put(acctNumAcctsName, val[:]) + err := bucket.Put(lastAccountName, uint32ToBytes(account)) if err != nil { - str := "failed to store num accounts" + str := fmt.Sprintf("failed to update metadata '%s'", lastAccountName) return managerError(ErrDatabase, str, err) } - return nil } @@ -547,7 +802,7 @@ func putNumAccounts(tx walletdb.Tx, numAccounts uint32) error { // deserializeAddressRow deserializes the passed serialized address information. // This is used as a common base for the various address types to deserialize // the common parts. -func deserializeAddressRow(addressID, serializedAddress []byte) (*dbAddressRow, error) { +func deserializeAddressRow(serializedAddress []byte) (*dbAddressRow, error) { // The serialized address format is: // // @@ -557,8 +812,7 @@ func deserializeAddressRow(addressID, serializedAddress []byte) (*dbAddressRow, // Given the above, the length of the entry must be at a minimum // the constant value sizes. if len(serializedAddress) < 18 { - str := fmt.Sprintf("malformed serialized address for key %s", - addressID) + str := "malformed serialized address" return nil, managerError(ErrDatabase, str, nil) } @@ -595,14 +849,13 @@ func serializeAddressRow(row *dbAddressRow) []byte { // deserializeChainedAddress deserializes the raw data from the passed address // row as a chained address. -func deserializeChainedAddress(addressID []byte, row *dbAddressRow) (*dbChainAddressRow, error) { +func deserializeChainedAddress(row *dbAddressRow) (*dbChainAddressRow, error) { // The serialized chain address raw data format is: // // // 4 bytes branch + 4 bytes address index if len(row.rawData) != 8 { - str := fmt.Sprintf("malformed serialized chained address for "+ - "key %s", addressID) + str := "malformed serialized chained address" return nil, managerError(ErrDatabase, str, nil) } @@ -631,7 +884,7 @@ func serializeChainedAddress(branch, index uint32) []byte { // deserializeImportedAddress deserializes the raw data from the passed address // row as an imported address. -func deserializeImportedAddress(addressID []byte, row *dbAddressRow) (*dbImportedAddressRow, error) { +func deserializeImportedAddress(row *dbAddressRow) (*dbImportedAddressRow, error) { // The serialized imported address raw data format is: // // @@ -641,8 +894,7 @@ func deserializeImportedAddress(addressID []byte, row *dbAddressRow) (*dbImporte // Given the above, the length of the entry must be at a minimum // the constant value sizes. if len(row.rawData) < 8 { - str := fmt.Sprintf("malformed serialized imported address for "+ - "key %s", addressID) + str := "malformed serialized imported address" return nil, managerError(ErrDatabase, str, nil) } @@ -684,7 +936,7 @@ func serializeImportedAddress(encryptedPubKey, encryptedPrivKey []byte) []byte { // deserializeScriptAddress deserializes the raw data from the passed address // row as a script address. -func deserializeScriptAddress(addressID []byte, row *dbAddressRow) (*dbScriptAddressRow, error) { +func deserializeScriptAddress(row *dbAddressRow) (*dbScriptAddressRow, error) { // The serialized script address raw data format is: // // @@ -694,8 +946,7 @@ func deserializeScriptAddress(addressID []byte, row *dbAddressRow) (*dbScriptAdd // Given the above, the length of the entry must be at a minimum // the constant value sizes. if len(row.rawData) < 8 { - str := fmt.Sprintf("malformed serialized script address for "+ - "key %s", addressID) + str := "malformed serialized script address" return nil, managerError(ErrDatabase, str, nil) } @@ -737,7 +988,7 @@ func serializeScriptAddress(encryptedHash, encryptedScript []byte) []byte { } // fetchAddressUsed returns true if the provided address hash was flagged as used. -func fetchAddressUsed(tx walletdb.Tx, addrHash [32]byte) bool { +func fetchAddressUsed(tx walletdb.Tx, addrHash []byte) bool { bucket := tx.RootBucket().Bucket(usedAddrBucketName) val := bucket.Get(addrHash[:]) @@ -747,32 +998,33 @@ func fetchAddressUsed(tx walletdb.Tx, addrHash [32]byte) bool { return false } -// fetchAddress loads address information for the provided address id from -// the database. The returned value is one of the address rows for the specific -// address type. The caller should use type assertions to ascertain the type. -func fetchAddress(tx walletdb.Tx, addressID []byte) (interface{}, error) { +// fetchAddressByHash loads address information for the provided address hash +// from the database. The returned value is one of the address rows for the +// specific address type. The caller should use type assertions to ascertain +// the type. The caller should prefix the error message with the address hash +// which caused the failure. +func fetchAddressByHash(tx walletdb.Tx, addrHash []byte) (interface{}, error) { bucket := tx.RootBucket().Bucket(addrBucketName) - addrHash := fastsha256.Sum256(addressID) serializedRow := bucket.Get(addrHash[:]) if serializedRow == nil { str := "address not found" return nil, managerError(ErrAddressNotFound, str, nil) } - row, err := deserializeAddressRow(addressID, serializedRow) + row, err := deserializeAddressRow(serializedRow) if err != nil { return nil, err } - row.used = fetchAddressUsed(tx, addrHash) + row.used = fetchAddressUsed(tx, addrHash[:]) switch row.addrType { case adtChain: - return deserializeChainedAddress(addressID, row) + return deserializeChainedAddress(row) case adtImport: - return deserializeImportedAddress(addressID, row) + return deserializeImportedAddress(row) case adtScript: - return deserializeScriptAddress(addressID, row) + return deserializeScriptAddress(row) } str := fmt.Sprintf("unsupported address type '%d'", row.addrType) @@ -796,6 +1048,16 @@ func markAddressUsed(tx walletdb.Tx, addressID []byte) error { return nil } +// fetchAddress loads address information for the provided address id from the +// database. The returned value is one of the address rows for the specific +// address type. The caller should use type assertions to ascertain the type. +// The caller should prefix the error message with the address which caused the +// failure. +func fetchAddress(tx walletdb.Tx, addressID []byte) (interface{}, error) { + addrHash := fastsha256.Sum256(addressID) + return fetchAddressByHash(tx, addrHash[:]) +} + // putAddress stores the provided address information to the database. This // is used a common base for storing the various address types. func putAddress(tx walletdb.Tx, addressID []byte, row *dbAddressRow) error { @@ -810,8 +1072,8 @@ func putAddress(tx walletdb.Tx, addressID []byte, row *dbAddressRow) error { str := fmt.Sprintf("failed to store address %x", addressID) return managerError(ErrDatabase, str, err) } - - return nil + // Update address account index + return putAddrAccountIndex(tx, row.account, addrHash[:]) } // putChainedAddress stores the provided chained address information to the @@ -914,9 +1176,64 @@ func existsAddress(tx walletdb.Tx, addressID []byte) bool { return bucket.Get(addrHash[:]) != nil } +// fetchAddrAccount returns the account to which the given address belongs to. +// It looks up the account using the addracctidx index which maps the address +// hash to its corresponding account id. +func fetchAddrAccount(tx walletdb.Tx, addressID []byte) (uint32, error) { + bucket := tx.RootBucket().Bucket(addrAcctIdxBucketName) + + addrHash := fastsha256.Sum256(addressID) + val := bucket.Get(addrHash[:]) + if val == nil { + str := "address not found" + return 0, managerError(ErrAddressNotFound, str, nil) + } + return binary.LittleEndian.Uint32(val), nil +} + +// fetchAccountAddresses loads information about addresses of an account from the database. +// The returned value is a slice address rows for each specific address type. +// The caller should use type assertions to ascertain the types. +func fetchAccountAddresses(tx walletdb.Tx, account uint32) ([]interface{}, error) { + bucket := tx.RootBucket().Bucket(addrAcctIdxBucketName). + Bucket(uint32ToBytes(account)) + // if index bucket is missing the account, there hasn't been any address + // entries yet + if bucket == nil { + return nil, nil + } + + var addrs []interface{} + err := bucket.ForEach(func(k, v []byte) error { + // Skip buckets. + if v == nil { + return nil + } + addrRow, err := fetchAddressByHash(tx, k) + if err != nil { + if merr, ok := err.(*ManagerError); ok { + desc := fmt.Sprintf("failed to fetch address hash '%s': %v", + k, merr.Description) + merr.Description = desc + return merr + } + return err + } + + addrs = append(addrs, addrRow) + return nil + }) + if err != nil { + return nil, maybeConvertDbError(err) + } + + return addrs, nil +} + // fetchAllAddresses loads information about all addresses from the database. // The returned value is a slice of address rows for each specific address type. // The caller should use type assertions to ascertain the types. +// TODO(tuxcanfly): Switch over to an iterator to support the maximum of 2^62 - 2^32 - 2^31 + 2 addrs func fetchAllAddresses(tx walletdb.Tx) ([]interface{}, error) { bucket := tx.RootBucket().Bucket(addrBucketName) @@ -929,23 +1246,12 @@ func fetchAllAddresses(tx walletdb.Tx) ([]interface{}, error) { // Deserialize the address row first to determine the field // values. - row, err := deserializeAddressRow(k, v) - if err != nil { - return err - } - - var addrRow interface{} - switch row.addrType { - case adtChain: - addrRow, err = deserializeChainedAddress(k, row) - case adtImport: - addrRow, err = deserializeImportedAddress(k, row) - case adtScript: - addrRow, err = deserializeScriptAddress(k, row) - default: - str := fmt.Sprintf("unsupported address type '%d'", - row.addrType) - return managerError(ErrDatabase, str, nil) + addrRow, err := fetchAddressByHash(tx, k) + if merr, ok := err.(*ManagerError); ok { + desc := fmt.Sprintf("failed to fetch address hash '%s': %v", + k, merr.Description) + merr.Description = desc + return merr } if err != nil { return err @@ -985,12 +1291,16 @@ func deletePrivateKeys(tx walletdb.Tx) error { str := "failed to delete crypto script key" return managerError(ErrDatabase, str, err) } + if err := bucket.Delete(coinTypePrivKeyName); err != nil { + str := "failed to delete cointype private key" + return managerError(ErrDatabase, str, err) + } // Delete the account extended private key for all accounts. bucket = tx.RootBucket().Bucket(acctBucketName) err := bucket.ForEach(func(k, v []byte) error { // Skip buckets. - if v == nil || bytes.Equal(k, acctNumAcctsName) { + if v == nil { return nil } @@ -1036,14 +1346,14 @@ func deletePrivateKeys(tx walletdb.Tx) error { // Deserialize the address row first to determine the field // values. - row, err := deserializeAddressRow(k, v) + row, err := deserializeAddressRow(v) if err != nil { return err } switch row.addrType { case adtImport: - irow, err := deserializeImportedAddress(k, row) + irow, err := deserializeImportedAddress(row) if err != nil { return err } @@ -1059,7 +1369,7 @@ func deletePrivateKeys(tx walletdb.Tx) error { } case adtScript: - srow, err := deserializeScriptAddress(k, row) + srow, err := deserializeScriptAddress(row) if err != nil { return err } @@ -1283,6 +1593,28 @@ func createManagerNS(namespace walletdb.Namespace) error { return managerError(ErrDatabase, str, err) } + _, err = rootBucket.CreateBucket(acctNameIdxBucketName) + if err != nil { + str := "failed to create an account name index bucket" + return managerError(ErrDatabase, str, err) + } + + _, err = rootBucket.CreateBucket(acctIdIdxBucketName) + if err != nil { + str := "failed to create an account id index bucket" + return managerError(ErrDatabase, str, err) + } + + _, err = rootBucket.CreateBucket(metaBucketName) + if err != nil { + str := "failed to create a meta bucket" + return managerError(ErrDatabase, str, err) + } + + if err := putLastAccount(tx, DefaultAccountNum); err != nil { + return err + } + if err := putManagerVersion(tx, latestMgrVersion); err != nil { return err } @@ -1334,14 +1666,12 @@ func upgradeToVersion2(namespace walletdb.Namespace) error { // upgradeManager upgrades the data in the provided manager namespace to newer // versions as neeeded. -func upgradeManager(namespace walletdb.Namespace) error { - // Get the current version. +func upgradeManager(namespace walletdb.Namespace, pubPassPhrase []byte, config *Options) error { var version uint32 err := namespace.View(func(tx walletdb.Tx) error { - mainBucket := tx.RootBucket().Bucket(mainBucketName) - verBytes := mainBucket.Get(mgrVersionName) - version = binary.LittleEndian.Uint32(verBytes) - return nil + var err error + version, err = fetchManagerVersion(tx) + return err }) if err != nil { str := "failed to fetch version for update" @@ -1388,6 +1718,29 @@ func upgradeManager(namespace walletdb.Namespace) error { version = 2 } + if version < 3 { + if config.ObtainSeed == nil || config.ObtainPrivatePass == nil { + str := "failed to obtain seed and private passphrase required for upgrade" + return managerError(ErrDatabase, str, err) + } + + seed, err := config.ObtainSeed() + if err != nil { + return err + } + privPassPhrase, err := config.ObtainPrivatePass() + if err != nil { + return err + } + // Upgrade from version 2 to 3. + if err := upgradeToVersion3(namespace, seed, privPassPhrase, pubPassPhrase); err != nil { + return err + } + + // The manager is now at version 3. + version = 3 + } + // Ensure the manager is upraded to the latest version. This check is // to intentionally cause a failure if the manager version is updated // without writing code to handle the upgrade. @@ -1400,3 +1753,115 @@ func upgradeManager(namespace walletdb.Namespace) error { return nil } + +// upgradeToVersion3 upgrades the database from version 2 to version 3 +// The following buckets were introduced in version 3 to support account names: +// * acctNameIdxBucketName +// * acctIdIdxBucketName +// * metaBucketName +func upgradeToVersion3(namespace walletdb.Namespace, seed, privPassPhrase, pubPassPhrase []byte) error { + err := namespace.Update(func(tx walletdb.Tx) error { + currentMgrVersion := uint32(3) + rootBucket := tx.RootBucket() + + woMgr, err := loadManager(namespace, pubPassPhrase, &chaincfg.SimNetParams, nil) + if err != nil { + return err + } + defer woMgr.Close() + + err = woMgr.Unlock(privPassPhrase) + if err != nil { + return err + } + + // Derive the master extended key from the seed. + root, err := hdkeychain.NewMaster(seed) + if err != nil { + str := "failed to derive master extended key" + return managerError(ErrKeyChain, str, err) + } + + // Derive the cointype key according to BIP0044. + coinTypeKeyPriv, err := deriveCoinTypeKey(root, chaincfg.SimNetParams.HDCoinType) + if err != nil { + str := "failed to derive cointype extended key" + return managerError(ErrKeyChain, str, err) + } + + cryptoKeyPub := woMgr.cryptoKeyPub + cryptoKeyPriv := woMgr.cryptoKeyPriv + // Encrypt the cointype keys with the associated crypto keys. + coinTypeKeyPub, err := coinTypeKeyPriv.Neuter() + if err != nil { + str := "failed to convert cointype private key" + return managerError(ErrKeyChain, str, err) + } + coinTypePubEnc, err := cryptoKeyPub.Encrypt([]byte(coinTypeKeyPub.String())) + if err != nil { + str := "failed to encrypt cointype public key" + return managerError(ErrCrypto, str, err) + } + coinTypePrivEnc, err := cryptoKeyPriv.Encrypt([]byte(coinTypeKeyPriv.String())) + if err != nil { + str := "failed to encrypt cointype private key" + return managerError(ErrCrypto, str, err) + } + + // Save the encrypted cointype keys to the database. + err = putCoinTypeKeys(tx, coinTypePubEnc, coinTypePrivEnc) + if err != nil { + return err + } + + _, err = rootBucket.CreateBucket(acctNameIdxBucketName) + if err != nil { + str := "failed to create an account name index bucket" + return managerError(ErrDatabase, str, err) + } + + _, err = rootBucket.CreateBucket(acctIdIdxBucketName) + if err != nil { + str := "failed to create an account id index bucket" + return managerError(ErrDatabase, str, err) + } + + _, err = rootBucket.CreateBucket(metaBucketName) + if err != nil { + str := "failed to create a meta bucket" + return managerError(ErrDatabase, str, err) + } + + // Initialize metadata for all keys + if err := putLastAccount(tx, DefaultAccountNum); err != nil { + return err + } + + // Update default account indexes + if err := putAccountIdIndex(tx, DefaultAccountNum, DefaultAccountName); err != nil { + return err + } + if err := putAccountNameIndex(tx, DefaultAccountNum, DefaultAccountName); err != nil { + return err + } + // Update imported account indexes + if err := putAccountIdIndex(tx, ImportedAddrAccount, ImportedAddrAccountName); err != nil { + return err + } + if err := putAccountNameIndex(tx, ImportedAddrAccount, ImportedAddrAccountName); err != nil { + return err + } + + // Write current manager version + if err := putManagerVersion(tx, currentMgrVersion); err != nil { + return err + } + + // Save "" alias for default account name for backward compat + return putAccountNameIndex(tx, DefaultAccountNum, "") + }) + if err != nil { + return maybeConvertDbError(err) + } + return nil +} diff --git a/waddrmgr/error.go b/waddrmgr/error.go index fe060a4..82c63b2 100644 --- a/waddrmgr/error.go +++ b/waddrmgr/error.go @@ -114,8 +114,11 @@ const ( // the account manager. ErrAccountNotFound - // ErrDuplicate indicates that an address already exists. - ErrDuplicate + // ErrDuplicateAddress indicates an address already exists. + ErrDuplicateAddress + + // ErrDuplicateAccount indicates an account already exists. + ErrDuplicateAccount // ErrTooManyAddresses indicates that more than the maximum allowed number of // addresses per account have been requested. @@ -146,7 +149,8 @@ var errorCodeStrings = map[ErrorCode]string{ ErrInvalidAccount: "ErrInvalidAccount", ErrAddressNotFound: "ErrAddressNotFound", ErrAccountNotFound: "ErrAccountNotFound", - ErrDuplicate: "ErrDuplicate", + ErrDuplicateAddress: "ErrDuplicateAddress", + ErrDuplicateAccount: "ErrDuplicateAccount", ErrTooManyAddresses: "ErrTooManyAddresses", ErrWrongPassphrase: "ErrWrongPassphrase", ErrWrongNet: "ErrWrongNet", diff --git a/waddrmgr/error_test.go b/waddrmgr/error_test.go index 795fd7e..07dab02 100644 --- a/waddrmgr/error_test.go +++ b/waddrmgr/error_test.go @@ -43,7 +43,8 @@ func TestErrorCodeStringer(t *testing.T) { {waddrmgr.ErrInvalidAccount, "ErrInvalidAccount"}, {waddrmgr.ErrAddressNotFound, "ErrAddressNotFound"}, {waddrmgr.ErrAccountNotFound, "ErrAccountNotFound"}, - {waddrmgr.ErrDuplicate, "ErrDuplicate"}, + {waddrmgr.ErrDuplicateAddress, "ErrDuplicateAddress"}, + {waddrmgr.ErrDuplicateAccount, "ErrDuplicateAccount"}, {waddrmgr.ErrTooManyAddresses, "ErrTooManyAddresses"}, {waddrmgr.ErrWrongPassphrase, "ErrWrongPassphrase"}, {waddrmgr.ErrWrongNet, "ErrWrongNet"}, diff --git a/waddrmgr/manager.go b/waddrmgr/manager.go index 7c7cb21..c8e28e0 100644 --- a/waddrmgr/manager.go +++ b/waddrmgr/manager.go @@ -51,8 +51,14 @@ const ( // fit into that model. ImportedAddrAccount = MaxAccountNum + 1 // 2^31 - 1 - // defaultAccountNum is the number of the default account. - defaultAccountNum = 0 + // ImportedAddrAccountName is the name of the imported account. + ImportedAddrAccountName = "imported" + + // DefaultAccountNum is the number of the default account. + DefaultAccountNum = 0 + + // DefaultAccountName is the name of the default account. + DefaultAccountName = "default" // The hierarchy described by BIP0043 is: // m/'/* @@ -82,11 +88,30 @@ const ( saltSize = 32 ) +var ( + // reservedAccountNames is a set of account names reserved for internal + // purposes + reservedAccountNames = map[string]struct{}{ + "*": struct{}{}, + DefaultAccountName: struct{}{}, + ImportedAddrAccountName: struct{}{}, + } +) + // Options is used to hold the optional parameters passed to Create or Load. type Options struct { ScryptN int ScryptR int ScryptP int + // ObtainSeed is a callback function that is potentially invoked during + // upgrades. It is intended to be used to request the wallet seed + // from the user (or any other mechanism the caller deems fit). + ObtainSeed ObtainUserInputFunc + // ObtainPrivatePass is a callback function that is potentially invoked + // during upgrades. It is intended to be used to request the wallet + // private passphrase from the user (or any other mechanism the caller + // deems fit). + ObtainPrivatePass ObtainUserInputFunc } // defaultConfig is an instance of the Options struct initialized with default @@ -615,6 +640,12 @@ func (m *Manager) loadAndCacheAddress(address btcutil.Address) (ManagedAddress, return err }) if err != nil { + if merr, ok := err.(*ManagerError); ok { + desc := fmt.Sprintf("failed to fetch address '%s': %v", + address.ScriptAddress(), merr.Description) + merr.Description = desc + return nil, merr + } return nil, maybeConvertDbError(err) } @@ -655,6 +686,20 @@ func (m *Manager) Address(address btcutil.Address) (ManagedAddress, error) { return m.loadAndCacheAddress(address) } +// AddrAccount returns the account to which the given address belongs. +func (m *Manager) AddrAccount(address btcutil.Address) (uint32, error) { + var account uint32 + err := m.namespace.View(func(tx walletdb.Tx) error { + var err error + account, err = fetchAddrAccount(tx, address.ScriptAddress()) + return err + }) + if err != nil { + return 0, maybeConvertDbError(err) + } + return account, nil +} + // ChangePassphrase changes either the public or private passphrase to the // provided value depending on the private flag. In order to change the private // password, the address manager must not be watching-only. @@ -963,7 +1008,7 @@ func (m *Manager) ImportPrivateKey(wif *btcutil.WIF, bs *BlockStamp) (ManagedPub if alreadyExists { str := fmt.Sprintf("address for public key %x already exists", serializedPubKey) - return nil, managerError(ErrDuplicate, str, nil) + return nil, managerError(ErrDuplicateAddress, str, nil) } // Encrypt public key. @@ -1067,7 +1112,7 @@ func (m *Manager) ImportScript(script []byte, bs *BlockStamp) (ManagedScriptAddr if alreadyExists { str := fmt.Sprintf("address for script hash %x already exists", scriptHash) - return nil, managerError(ErrDuplicate, str, nil) + return nil, managerError(ErrDuplicateAddress, str, nil) } // Encrypt the script hash using the crypto public key so it is @@ -1176,6 +1221,29 @@ func (m *Manager) Lock() error { return nil } +// lookupAccount loads account number stored in the manager for the given +// account name +// +// This function MUST be called with the manager lock held for reads. +func (m *Manager) lookupAccount(name string) (uint32, error) { + var account uint32 + err := m.namespace.View(func(tx walletdb.Tx) error { + var err error + account, err = fetchAccountByName(tx, name) + return err + }) + return account, err +} + +// LookupAccount loads account number stored in the manager for the given +// account name +func (m *Manager) LookupAccount(name string) (uint32, error) { + m.mtx.RLock() + defer m.mtx.RUnlock() + + return m.lookupAccount(name) +} + // Unlock derives the master private key from the specified passphrase. An // invalid passphrase will return an error. Otherwise, the derived secret key // is stored in memory until the address manager is locked. Any failures that @@ -1543,6 +1611,254 @@ func (m *Manager) LastInternalAddress(account uint32) (ManagedAddress, error) { return acctInfo.lastInternalAddr, nil } +// ValidateAccountName validates the given account name and returns an error, if any. +func ValidateAccountName(name string) error { + if name == "" { + str := "invalid account name, cannot be blank" + return managerError(ErrInvalidAccount, str, nil) + } + if _, ok := reservedAccountNames[name]; ok { + str := "reserved account name" + return managerError(ErrInvalidAccount, str, nil) + } + return nil +} + +// NewAccount creates and returns a new account stored in the manager based +// on the given account name. If an account with the same name already exists, +// ErrDuplicateAccount will be returned. Since creating a new account requires +// access to the cointype keys (from which extended account keys are derived), +// it requires the manager to be unlocked. +func (m *Manager) NewAccount(name string) (uint32, error) { + if m.watchingOnly { + return 0, managerError(ErrWatchingOnly, errWatchingOnly, nil) + } + + m.mtx.Lock() + defer m.mtx.Unlock() + + if m.locked { + return 0, managerError(ErrLocked, errLocked, nil) + } + + // Validate account name + if err := ValidateAccountName(name); err != nil { + return 0, err + } + + // Check that account with the same name does not exist + _, err := m.lookupAccount(name) + if err == nil { + str := fmt.Sprintf("account with the same name already exists") + return 0, managerError(ErrDuplicateAccount, str, err) + } + + var account uint32 + var coinTypePrivEnc []byte + + // Fetch latest account, and create a new account in the same transaction + err = m.namespace.Update(func(tx walletdb.Tx) error { + var err error + // Fetch the latest account number to generate the next account number + account, err = fetchLastAccount(tx) + if err != nil { + return err + } + account++ + // Fetch the cointype key which will be used to derive the next account + // extended keys + _, coinTypePrivEnc, err = fetchCoinTypeKeys(tx) + if err != nil { + return err + } + + // Decrypt the cointype key + serializedKeyPriv, err := m.cryptoKeyPriv.Decrypt(coinTypePrivEnc) + if err != nil { + str := fmt.Sprintf("failed to decrypt cointype serialized private key") + return managerError(ErrLocked, str, err) + } + coinTypeKeyPriv, err := hdkeychain.NewKeyFromString(string(serializedKeyPriv)) + zero.Bytes(serializedKeyPriv) + if err != nil { + str := fmt.Sprintf("failed to create cointype extended private key") + return managerError(ErrKeyChain, str, err) + } + + // Derive the account key using the cointype key + acctKeyPriv, err := deriveAccountKey(coinTypeKeyPriv, account) + coinTypeKeyPriv.Zero() + if err != nil { + str := "failed to convert private key for account" + return managerError(ErrKeyChain, str, err) + } + acctKeyPub, err := acctKeyPriv.Neuter() + if err != nil { + str := "failed to convert public key for account" + return managerError(ErrKeyChain, str, err) + } + // Encrypt the default account keys with the associated crypto keys. + acctPubEnc, err := m.cryptoKeyPub.Encrypt([]byte(acctKeyPub.String())) + if err != nil { + str := "failed to encrypt public key for account" + return managerError(ErrCrypto, str, err) + } + acctPrivEnc, err := m.cryptoKeyPriv.Encrypt([]byte(acctKeyPriv.String())) + if err != nil { + str := "failed to encrypt private key for account" + return managerError(ErrCrypto, str, err) + } + // We have the encrypted account extended keys, so save them to the + // database + err = putAccountInfo(tx, account, acctPubEnc, acctPrivEnc, 0, 0, name) + if err != nil { + return err + } + + // Save last account metadata + if err := putLastAccount(tx, account); err != nil { + return err + } + return nil + }) + return account, err +} + +// RenameAccount renames an account stored in the manager based on the +// given account number with the given name. If an account with the same name +// already exists, ErrDuplicateAccount will be returned. +func (m *Manager) RenameAccount(account uint32, name string) error { + m.mtx.Lock() + defer m.mtx.Unlock() + + // Check that account with the new name does not exist + _, err := m.lookupAccount(name) + if err == nil { + str := fmt.Sprintf("account with the same name already exists") + return managerError(ErrDuplicateAccount, str, err) + } + // Validate account name + if err := ValidateAccountName(name); err != nil { + return err + } + + var rowInterface interface{} + err = m.namespace.Update(func(tx walletdb.Tx) error { + var err error + rowInterface, err = fetchAccountInfo(tx, account) + if err != nil { + return err + } + // Ensure the account type is a BIP0044 account. + row, ok := rowInterface.(*dbBIP0044AccountRow) + if !ok { + str := fmt.Sprintf("unsupported account type %T", row) + err = managerError(ErrDatabase, str, nil) + } + // Remove the old name key from the accout id index + if err = deleteAccountIdIndex(tx, account); err != nil { + return err + } + // Remove the old name key from the accout name index + if err = deleteAccountNameIndex(tx, row.name); err != nil { + return err + } + err = putAccountInfo(tx, account, row.pubKeyEncrypted, + row.privKeyEncrypted, row.nextExternalIndex, row.nextInternalIndex, name) + return err + }) + return err +} + +// AccountName returns the account name for the given account number +// stored in the manager. +func (m *Manager) AccountName(account uint32) (string, error) { + m.mtx.Lock() + defer m.mtx.Unlock() + + var acctName string + err := m.namespace.View(func(tx walletdb.Tx) error { + var err error + acctName, err = fetchAccountName(tx, account) + return err + }) + if err != nil { + return "", err + } + + return acctName, nil +} + +// AllAccounts returns a slice of all the accounts stored in the manager. +func (m *Manager) AllAccounts() ([]uint32, error) { + m.mtx.Lock() + defer m.mtx.Unlock() + + var accounts []uint32 + err := m.namespace.View(func(tx walletdb.Tx) error { + var err error + accounts, err = fetchAllAccounts(tx) + return err + }) + if err != nil { + return nil, err + } + + return accounts, nil +} + +// LastAccount returns the last account stored in the manager. +func (m *Manager) LastAccount() (uint32, error) { + m.mtx.Lock() + defer m.mtx.Unlock() + + var account uint32 + err := m.namespace.View(func(tx walletdb.Tx) error { + var err error + account, err = fetchLastAccount(tx) + return err + }) + return account, err +} + +// AllAccountAddresses returns a slice of addresses of an account stored in the manager. +func (m *Manager) AllAccountAddresses(account uint32) ([]ManagedAddress, error) { + m.mtx.Lock() + defer m.mtx.Unlock() + + // Load the raw address information from the database. + var rowInterfaces []interface{} + err := m.namespace.View(func(tx walletdb.Tx) error { + var err error + rowInterfaces, err = fetchAccountAddresses(tx, account) + return err + }) + if err != nil { + return nil, err + } + + addrs := make([]ManagedAddress, 0, len(rowInterfaces)) + for _, rowInterface := range rowInterfaces { + // Create a new managed address for the specific type of address + // based on type. + managedAddr, err := m.rowInterfaceToManaged(rowInterface) + if err != nil { + return nil, err + } + + addrs = append(addrs, managedAddr) + } + + return addrs, nil +} + +// ActiveAccountAddresses returns a slice of active addresses of an account +// stored in the manager. +// TODO(tuxcanfly): actually return only active addresses +func (m *Manager) ActiveAccountAddresses(account uint32) ([]ManagedAddress, error) { + return m.AllAccountAddresses(account) +} + // AllActiveAddresses returns a slice of all addresses stored in the manager. func (m *Manager) AllActiveAddresses() ([]btcutil.Address, error) { m.mtx.Lock() @@ -1667,26 +1983,20 @@ func newManager(namespace walletdb.Namespace, chainParams *chaincfg.Params, } } -// deriveAccountKey derives the extended key for an account according to the -// hierarchy described by BIP0044 given the master node. +// deriveCoinTypeKey derives the cointype key which can be used to derive the +// extended key for an account according to the hierarchy described by BIP0044 +// given the coin type key. // // In particular this is the hierarchical deterministic extended key path: -// m/44'/'/' -func deriveAccountKey(masterNode *hdkeychain.ExtendedKey, coinType uint32, - account uint32) (*hdkeychain.ExtendedKey, error) { - +// m/44'/' +func deriveCoinTypeKey(masterNode *hdkeychain.ExtendedKey, + coinType uint32) (*hdkeychain.ExtendedKey, error) { // Enforce maximum coin type. if coinType > maxCoinType { err := managerError(ErrCoinTypeTooHigh, errCoinTypeTooHigh, nil) return nil, err } - // Enforce maximum account number. - if account > MaxAccountNum { - err := managerError(ErrAccountNumTooHigh, errAcctTooHigh, nil) - return nil, err - } - // The hierarchy described by BIP0043 is: // m/'/* // This is further extended by BIP0044 to: @@ -1706,6 +2016,22 @@ func deriveAccountKey(masterNode *hdkeychain.ExtendedKey, coinType uint32, return nil, err } + return coinTypeKey, nil +} + +// deriveAccountKey derives the extended key for an account according to the +// hierarchy described by BIP0044 given the master node. +// +// In particular this is the hierarchical deterministic extended key path: +// m/44'/'/' +func deriveAccountKey(coinTypeKey *hdkeychain.ExtendedKey, + account uint32) (*hdkeychain.ExtendedKey, error) { + // Enforce maximum account number. + if account > MaxAccountNum { + err := managerError(ErrAccountNumTooHigh, errAcctTooHigh, nil) + return nil, err + } + // Derive the account key as a child of the coin type key. return coinTypeKey.Child(account + hdkeychain.HardenedKeyStart) } @@ -1858,7 +2184,7 @@ func Open(namespace walletdb.Namespace, pubPassphrase []byte, chainParams *chain } // Upgrade the manager to the latest version as needed. - if err := upgradeManager(namespace); err != nil { + if err := upgradeManager(namespace, pubPassphrase, config); err != nil { return nil, err } @@ -1916,8 +2242,15 @@ func Create(namespace walletdb.Namespace, seed, pubPassphrase, privPassphrase [] return nil, managerError(ErrKeyChain, str, err) } + // Derive the cointype key according to BIP0044. + coinTypeKeyPriv, err := deriveCoinTypeKey(root, chainParams.HDCoinType) + if err != nil { + str := "failed to derive cointype extended key" + return nil, managerError(ErrKeyChain, str, err) + } + // Derive the account key for the first account according to BIP0044. - acctKeyPriv, err := deriveAccountKey(root, chainParams.HDCoinType, 0) + acctKeyPriv, err := deriveAccountKey(coinTypeKeyPriv, 0) if err != nil { // The seed is unusable if the any of the children in the // required hierarchy can't be derived due to invalid child. @@ -2010,6 +2343,23 @@ func Create(namespace walletdb.Namespace, seed, pubPassphrase, privPassphrase [] return nil, managerError(ErrCrypto, str, err) } + // Encrypt the cointype keys with the associated crypto keys. + coinTypeKeyPub, err := coinTypeKeyPriv.Neuter() + if err != nil { + str := "failed to convert cointype private key" + return nil, managerError(ErrKeyChain, str, err) + } + coinTypePubEnc, err := cryptoKeyPub.Encrypt([]byte(coinTypeKeyPub.String())) + if err != nil { + str := "failed to encrypt cointype public key" + return nil, managerError(ErrCrypto, str, err) + } + coinTypePrivEnc, err := cryptoKeyPriv.Encrypt([]byte(coinTypeKeyPriv.String())) + if err != nil { + str := "failed to encrypt cointype private key" + return nil, managerError(ErrCrypto, str, err) + } + // Encrypt the default account keys with the associated crypto keys. acctPubEnc, err := cryptoKeyPub.Encrypt([]byte(acctKeyPub.String())) if err != nil { @@ -2048,6 +2398,12 @@ func Create(namespace walletdb.Namespace, seed, pubPassphrase, privPassphrase [] return err } + // Save the encrypted cointype keys to the database. + err = putCoinTypeKeys(tx, coinTypePubEnc, coinTypePrivEnc) + if err != nil { + return err + } + // Save the fact this is not a watching-only address manager to // the database. err = putWatchingOnly(tx, false) @@ -2071,24 +2427,33 @@ func Create(namespace walletdb.Namespace, seed, pubPassphrase, privPassphrase [] return err } - // Save the information for the default account to the database. - err = putAccountInfo(tx, defaultAccountNum, acctPubEnc, - acctPrivEnc, 0, 0, "") + // Save the information for the imported account to the database. + err = putAccountInfo(tx, ImportedAddrAccount, nil, + nil, 0, 0, ImportedAddrAccountName) if err != nil { return err } - return putNumAccounts(tx, 1) + // Save the information for the default account to the database. + err = putAccountInfo(tx, DefaultAccountNum, acctPubEnc, + acctPrivEnc, 0, 0, DefaultAccountName) + if err != nil { + return err + } + + // Save "" alias for default account name for backward compat + return putAccountNameIndex(tx, DefaultAccountNum, "") }) if err != nil { return nil, maybeConvertDbError(err) } // The new address manager is locked by default, so clear the master, - // crypto private, and crypto script keys from memory. + // crypto private, crypto script and cointype keys from memory. masterKeyPriv.Zero() cryptoKeyPriv.Zero() cryptoKeyScript.Zero() + coinTypeKeyPriv.Zero() return newManager(namespace, chainParams, masterKeyPub, masterKeyPriv, cryptoKeyPub, cryptoKeyPrivEnc, cryptoKeyScriptEnc, syncInfo, config, privPassphraseSalt), nil diff --git a/waddrmgr/manager_test.go b/waddrmgr/manager_test.go index b17ec6d..77a24a8 100644 --- a/waddrmgr/manager_test.go +++ b/waddrmgr/manager_test.go @@ -1158,6 +1158,221 @@ func testChangePassphrase(tc *testContext) bool { return true } +// testNewAccount tests the new account creation func of the address manager works +// as expected. +func testNewAccount(tc *testContext) bool { + if tc.watchingOnly { + // Creating new accounts in watching-only mode should return ErrWatchingOnly + _, err := tc.manager.NewAccount("test") + if !checkManagerError(tc.t, "Create account in watching-only mode", err, + waddrmgr.ErrWatchingOnly) { + tc.manager.Close() + return false + } + return true + } + // Creating new accounts when wallet is locked should return ErrLocked + _, err := tc.manager.NewAccount("test") + if !checkManagerError(tc.t, "Create account when wallet is locked", err, + waddrmgr.ErrLocked) { + tc.manager.Close() + return false + } + // Unlock the wallet to decrypt cointype keys required + // to derive account keys + if err := tc.manager.Unlock(privPassphrase); err != nil { + tc.t.Errorf("Unlock: unexpected error: %v", err) + return false + } + tc.unlocked = true + + // Get the next account number + expectedAccount := tc.account + 1 + if !tc.create { + // Existing wallet manager, so it already has "account-1", + // so increment the expected account number + expectedAccount++ + } + // Create accounts with names "account-1", "account-2", etc + testName := fmt.Sprintf("account-%d", expectedAccount) + account, err := tc.manager.NewAccount(testName) + if err != nil { + tc.t.Errorf("NewAccount: unexpected error: %v", err) + return false + } + if account != expectedAccount { + tc.t.Errorf("NewAccount "+ + "account mismatch -- got %d, "+ + "want %d", account, expectedAccount) + return false + } + // Test duplicate account name error + _, err = tc.manager.NewAccount(testName) + wantErrCode := waddrmgr.ErrDuplicateAccount + if !checkManagerError(tc.t, testName, err, wantErrCode) { + return false + } + // Test account name validation + testName = "*" + _, err = tc.manager.NewAccount(testName) + wantErrCode = waddrmgr.ErrInvalidAccount + if !checkManagerError(tc.t, testName, err, wantErrCode) { + return false + } + return true +} + +// testLookupAccount tests the basic account lookup func of the address manager works +// as expected. +func testLookupAccount(tc *testContext) bool { + // Lookup accounts created earlier in testNewAccount + expectedAccounts := map[string]uint32{ + waddrmgr.DefaultAccountName: waddrmgr.DefaultAccountNum, + "account-1": 1, + waddrmgr.ImportedAddrAccountName: waddrmgr.ImportedAddrAccount, + } + if !tc.create { + // Existing wallet manager will have 2 accounts + expectedAccounts["account-2"] = 2 + } + for acctName, expectedAccount := range expectedAccounts { + account, err := tc.manager.LookupAccount(acctName) + if err != nil { + tc.t.Errorf("LookupAccount: unexpected error: %v", err) + return false + } + if account != expectedAccount { + tc.t.Errorf("LookupAccount "+ + "account mismatch -- got %d, "+ + "want %d", account, expectedAccount) + return false + } + } + // Test account not found error + testName := "non existent account" + _, err := tc.manager.LookupAccount(testName) + wantErrCode := waddrmgr.ErrAccountNotFound + if !checkManagerError(tc.t, testName, err, wantErrCode) { + return false + } + return true +} + +// testRenameAccount tests the rename account func of the address manager works +// as expected. +func testRenameAccount(tc *testContext) bool { + acctName, err := tc.manager.AccountName(tc.account) + if err != nil { + tc.t.Errorf("AccountName: unexpected error: %v", err) + return false + } + testName := acctName + "-renamed" + err = tc.manager.RenameAccount(tc.account, testName) + if err != nil { + tc.t.Errorf("RenameAccount: unexpected error: %v", err) + return false + } + newName, err := tc.manager.AccountName(tc.account) + if err != nil { + tc.t.Errorf("AccountName: unexpected error: %v", err) + return false + } + if newName != testName { + tc.t.Errorf("RenameAccount "+ + "account name mismatch -- got %s, "+ + "want %s", newName, testName) + return false + } + // Test duplicate account name error + err = tc.manager.RenameAccount(tc.account, testName) + wantErrCode := waddrmgr.ErrDuplicateAccount + if !checkManagerError(tc.t, testName, err, wantErrCode) { + return false + } + // Test old account name is no longer valid + _, err = tc.manager.LookupAccount(acctName) + wantErrCode = waddrmgr.ErrAccountNotFound + if !checkManagerError(tc.t, testName, err, wantErrCode) { + return false + } + // Test account name validation + testName = "*" + err = tc.manager.RenameAccount(tc.account, testName) + wantErrCode = waddrmgr.ErrInvalidAccount + if !checkManagerError(tc.t, testName, err, wantErrCode) { + return false + } + return true +} + +// testAllAccounts tests the retrieve all accounts func of the address manager works +// as expected. +func testAllAccounts(tc *testContext) bool { + expectedAccounts := []uint32{0, 1} + if !tc.create { + // Existing wallet manager will have 3 accounts + expectedAccounts = append(expectedAccounts, 2) + } + // Imported account + expectedAccounts = append(expectedAccounts, waddrmgr.ImportedAddrAccount) + accounts, err := tc.manager.AllAccounts() + if err != nil { + tc.t.Errorf("AllAccounts: unexpected error: %v", err) + return false + } + if len(accounts) != len(expectedAccounts) { + tc.t.Errorf("AllAccounts: unexpected number of accounts - got "+ + "%d, want %d", len(accounts), + len(expectedAccounts)) + return false + } + for i, account := range accounts { + if expectedAccounts[i] != account { + tc.t.Errorf("AllAccounts %s: "+ + "account mismatch -- got %d, "+ + "want %d", i, account, expectedAccounts[i]) + } + } + return true +} + +// testActiveAccountAddresses tests the retrieve all account addrs func of the address manager works +// as expected. +func testActiveAccountAddresses(tc *testContext) bool { + expectedAddrs := []string{ + "1VTfwD4iHre2bMrR9qGiJMwoiZGQZ8e6s", + "1LJpGrAP1vWHuvfHqmUutQqFVYca2qwxhy", + "1Jc7An3JqjzRQULVr6Wh3iYR7miB6WPJCD", + "1AY6yAHvojvpFcevAichLMnJfxgE8eSe4N", + "1LTjSghkBecT59VjEKke331HxVdqcFwUDa", + "14wtcepMNiEazuN7YosWY8bwD9tcCtxXRB", + "1N3D8jy2aQuUsKBsDgZ6ZPTVR9VhHgJYpE", + "13TdEj4ehUuYFiSaB47eLVBwM2XhAhrK2J", + "15HNivzKhsLaMs1qRdQN1ifoJYUnJ2xW9z", + "13NhXy2nCLMwNug1TZ6uwaWnxp3uTqdDQq", + } + addrs, err := tc.manager.AllAccountAddresses(tc.account) + if err != nil { + tc.t.Errorf("ActiveAccountAddresses: unexpected error: %v", err) + return false + } + if len(addrs) != len(expectedAddrs) { + tc.t.Errorf("ActiveAccountAddresses: unexpected number of addrs - got "+ + "%d, want %d", len(addrs), + len(expectedAddrs)) + return false + } + for i, addr := range addrs { + if expectedAddrs[i] != addr.Address().EncodeAddress() { + tc.t.Errorf("ActiveAccountAddresses %s: "+ + "addr mismatch -- got %s, "+ + "want %s", i, addr.Address().EncodeAddress(), + expectedAddrs[i]) + } + } + return true +} + // testManagerAPI tests the functions provided by the Manager API as well as // the ManagedAddress, ManagedPubKeyAddress, and ManagedScriptAddress // interfaces. @@ -1169,6 +1384,13 @@ func testManagerAPI(tc *testContext) { testImportScript(tc) testMarkUsed(tc) testChangePassphrase(tc) + + // Reset default account + tc.account = 0 + testNewAccount(tc) + testLookupAccount(tc) + testAllAccounts(tc) + testActiveAccountAddresses(tc) } // testWatchingOnly tests various facets of a watching-only address diff --git a/wallet.go b/wallet.go index 1c81110..a7027c0 100644 --- a/wallet.go +++ b/wallet.go @@ -17,6 +17,7 @@ package main import ( + "bufio" "bytes" "encoding/base64" "encoding/hex" @@ -26,6 +27,7 @@ import ( "os" "path/filepath" "sort" + "strings" "sync" "time" @@ -34,10 +36,12 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcutil/hdkeychain" "github.com/btcsuite/btcwallet/chain" "github.com/btcsuite/btcwallet/txstore" "github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/walletdb" + "github.com/btcsuite/golangcrypto/ssh/terminal" ) // ErrNotSynced describes an error where an operation cannot complete @@ -56,8 +60,60 @@ const ( // provided by having all public data in the wallet encrypted by a // passphrase only known to them. defaultPubPassphrase = "public" + + // maxEmptyAccounts is the number of accounts to scan even if they have no + // transaction history. This is a deviation from BIP044 to make account + // creation more easier by allowing a limited number of empty accounts. + maxEmptyAccounts = 100 ) +// promptSeed is used to prompt for the wallet seed which maybe required during +// upgrades. +func promptSeed() ([]byte, error) { + reader := bufio.NewReader(os.Stdin) + for { + fmt.Print("Enter existing wallet seed: ") + seedStr, err := reader.ReadString('\n') + if err != nil { + return nil, err + } + seedStr = strings.TrimSpace(strings.ToLower(seedStr)) + + seed, err := hex.DecodeString(seedStr) + if err != nil || len(seed) < hdkeychain.MinSeedBytes || + len(seed) > hdkeychain.MaxSeedBytes { + + fmt.Printf("Invalid seed specified. Must be a "+ + "hexadecimal value that is at least %d bits and "+ + "at most %d bits\n", hdkeychain.MinSeedBytes*8, + hdkeychain.MaxSeedBytes*8) + continue + } + + return seed, nil + } +} + +// promptPrivPassPhrase is used to prompt for the private passphrase which maybe +// required during upgrades. +func promptPrivPassPhrase() ([]byte, error) { + prompt := "Enter the private passphrase of your wallet: " + for { + fmt.Print(prompt) + pass, err := terminal.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return nil, err + } + fmt.Print("\n") + pass = bytes.TrimSpace(pass) + if len(pass) == 0 { + continue + } + + return pass, nil + } +} + // networkDir returns the directory name of a network directory to hold wallet // files. func networkDir(dataDir string, chainParams *chaincfg.Params) string { @@ -171,6 +227,15 @@ func (w *Wallet) updateNotificationLock() { w.notificationLock = noopLocker{} } +// CreditAccount returns the first account that can be associated +// with the given credit. +// If no account is found, ErrAccountNotFound is returned. +func (w *Wallet) CreditAccount(c txstore.Credit) (uint32, error) { + _, addrs, _, _ := c.Addresses(activeNet.Params) + addr := addrs[0] + return w.Manager.AddrAccount(addr) +} + // ListenConnectedBlocks returns a channel that passes all blocks that a wallet // has been marked in sync with. The channel must be read, or other wallet // methods will block. @@ -470,6 +535,7 @@ func (w *Wallet) syncWithChain() error { type ( createTxRequest struct { + account uint32 pairs map[string]btcutil.Amount minconf int resp chan createTxResponse @@ -495,7 +561,7 @@ out: for { select { case txr := <-w.createTxRequests: - tx, err := w.txToPairs(txr.pairs, txr.minconf) + tx, err := w.txToPairs(txr.pairs, txr.account, txr.minconf) txr.resp <- createTxResponse{tx, err} case <-w.quit: @@ -511,8 +577,11 @@ out: // automatically included, if necessary. All transaction creation through // this function is serialized to prevent the creation of many transactions // which spend the same outputs. -func (w *Wallet) CreateSimpleTx(pairs map[string]btcutil.Amount, minconf int) (*CreatedTx, error) { +func (w *Wallet) CreateSimpleTx(account uint32, pairs map[string]btcutil.Amount, + minconf int) (*CreatedTx, error) { + req := createTxRequest{ + account: account, pairs: pairs, minconf: minconf, resp: make(chan createTxResponse), @@ -722,6 +791,22 @@ func (w *Wallet) AddressUsed(addr waddrmgr.ManagedAddress) bool { return addr.Used() } +// AccountUsed returns whether there are any recorded transactions spending to +// a given account. It returns true if atleast one address in the account was +// used and false if no address in the account was used. +func (w *Wallet) AccountUsed(account uint32) (bool, error) { + addrs, err := w.Manager.AllAccountAddresses(account) + if err != nil { + return false, err + } + for _, addr := range addrs { + if w.AddressUsed(addr) { + return true, nil + } + } + return false, nil +} + // CalculateBalance sums the amounts of all unspent transaction // outputs to addresses of a wallet and returns the balance. // @@ -735,19 +820,51 @@ func (w *Wallet) CalculateBalance(confirms int) (btcutil.Amount, error) { return w.TxStore.Balance(confirms, blk.Height) } +// CalculateAccountBalance sums the amounts of all unspent transaction +// outputs to the given account of a wallet and returns the balance. +func (w *Wallet) CalculateAccountBalance(account uint32, confirms int) (btcutil.Amount, error) { + var bal btcutil.Amount + + // Get current block. The block height used for calculating + // the number of tx confirmations. + blk := w.Manager.SyncedTo() + + unspent, err := w.TxStore.UnspentOutputs() + if err != nil { + return 0, err + } + for _, c := range unspent { + if c.IsCoinbase() { + if !c.Confirmed(blockchain.CoinbaseMaturity, blk.Height) { + continue + } + } + if c.Confirmed(confirms, blk.Height) { + creditAccount, err := w.CreditAccount(c) + if err != nil { + continue + } + if creditAccount == account { + bal += c.Amount() + } + } + } + return bal, nil +} + // CurrentAddress gets the most recently requested Bitcoin payment address // from a wallet. If the address has already been used (there is at least // one transaction spending to it in the blockchain or btcd mempool), the next // chained address is returned. -func (w *Wallet) CurrentAddress() (btcutil.Address, error) { - addr, err := w.Manager.LastExternalAddress(0) +func (w *Wallet) CurrentAddress(account uint32) (btcutil.Address, error) { + addr, err := w.Manager.LastExternalAddress(account) if err != nil { return nil, err } // Get next chained address if the last one has already been used. if w.AddressUsed(addr) { - return w.NewAddress() + return w.NewAddress(account) } return addr.Address(), nil @@ -773,7 +890,7 @@ func (w *Wallet) ListSinceBlock(since, curBlockHeight int32, continue } - jsonResults, err := txRecord.ToJSON("", curBlockHeight, + jsonResults, err := txRecord.ToJSON(waddrmgr.DefaultAccountName, curBlockHeight, w.Manager.ChainParams()) if err != nil { return nil, err @@ -798,7 +915,7 @@ func (w *Wallet) ListTransactions(from, count int) ([]btcjson.ListTransactionsRe lastLookupIdx := len(records) - count // Search in reverse order: lookup most recently-added first. for i := len(records) - 1; i >= from && i >= lastLookupIdx; i-- { - jsonResults, err := records[i].ToJSON("", blk.Height, + jsonResults, err := records[i].ToJSON(waddrmgr.DefaultAccountName, blk.Height, w.Manager.ChainParams()) if err != nil { return nil, err @@ -837,7 +954,7 @@ func (w *Wallet) ListAddressTransactions(pkHashes map[string]struct{}) ( if _, ok := pkHashes[string(apkh.ScriptAddress())]; !ok { continue } - jsonResult, err := c.ToJSON("", blk.Height, + jsonResult, err := c.ToJSON(waddrmgr.DefaultAccountName, blk.Height, w.Manager.ChainParams()) if err != nil { return nil, err @@ -862,7 +979,7 @@ func (w *Wallet) ListAllTransactions() ([]btcjson.ListTransactionsResult, error) // Search in reverse order: lookup most recently-added first. records := w.TxStore.Records() for i := len(records) - 1; i >= 0; i-- { - jsonResults, err := records[i].ToJSON("", blk.Height, + jsonResults, err := records[i].ToJSON(waddrmgr.DefaultAccountName, blk.Height, w.Manager.ChainParams()) if err != nil { return nil, err @@ -906,6 +1023,15 @@ func (w *Wallet) ListUnspent(minconf, maxconf int, continue } + creditAccount, err := w.CreditAccount(credit) + if err != nil { + continue + } + accountName, err := w.Manager.AccountName(creditAccount) + if err != nil { + return nil, err + } + _, addrs, _, _ := credit.Addresses(activeNet.Params) if filter { for _, addr := range addrs { @@ -920,9 +1046,9 @@ func (w *Wallet) ListUnspent(minconf, maxconf int, result := &btcjson.ListUnspentResult{ TxId: credit.Tx().Sha().String(), Vout: credit.OutputIndex, - Account: "", + Account: accountName, ScriptPubKey: hex.EncodeToString(credit.TxOut().PkScript), - Amount: credit.Amount().ToUnit(btcutil.AmountBTC), + Amount: credit.Amount().ToBTC(), Confirmations: int64(confs), } @@ -1197,9 +1323,8 @@ func (w *Wallet) SortedActivePaymentAddresses() ([]string, error) { } // NewAddress returns the next external chained address for a wallet. -func (w *Wallet) NewAddress() (btcutil.Address, error) { +func (w *Wallet) NewAddress(account uint32) (btcutil.Address, error) { // Get next address from wallet. - account := uint32(0) addrs, err := w.Manager.NextExternalAddresses(account, 1) if err != nil { return nil, err @@ -1218,9 +1343,8 @@ func (w *Wallet) NewAddress() (btcutil.Address, error) { } // NewChangeAddress returns a new change address for a wallet. -func (w *Wallet) NewChangeAddress() (btcutil.Address, error) { - // Get next chained change address from wallet for account 0. - account := uint32(0) +func (w *Wallet) NewChangeAddress(account uint32) (btcutil.Address, error) { + // Get next chained change address from wallet for account. addrs, err := w.Manager.NextInternalAddresses(account, 1) if err != nil { return nil, err @@ -1239,27 +1363,35 @@ func (w *Wallet) NewChangeAddress() (btcutil.Address, error) { return utilAddrs[0], nil } -// TotalReceived iterates through a wallet's transaction history, returning the -// total amount of bitcoins received for any wallet address. Amounts received -// through multisig transactions are ignored. -func (w *Wallet) TotalReceived(confirms int) (btcutil.Amount, error) { +// TotalReceivedForAccount iterates through a wallet's transaction history, +// returning the total amount of bitcoins received for a single wallet +// account. +func (w *Wallet) TotalReceivedForAccount(account uint32, confirms int) (btcutil.Amount, uint64, error) { blk := w.Manager.SyncedTo() + // Number of confirmations of the last transaction. + var confirmations uint64 + var amount btcutil.Amount for _, r := range w.TxStore.Records() { for _, c := range r.Credits() { - // Ignore change. - if c.Change() { + if !c.Confirmed(confirms, blk.Height) { + // Not enough confirmations, skip the current block. continue } - - // Tally if the appropiate number of block confirmations have passed. - if c.Confirmed(confirms, blk.Height) { + creditAccount, err := w.CreditAccount(c) + if err != nil { + continue + } + if creditAccount == account { amount += c.Amount() + confirmations = uint64(c.Confirmations(blk.Height)) + break } } } - return amount, nil + + return amount, confirmations, nil } // TotalReceivedForAddr iterates through a wallet's transaction history, @@ -1329,8 +1461,12 @@ func openWallet() (*Wallet, error) { // Open address manager and transaction store. var txs *txstore.Store + config := &waddrmgr.Options{ + ObtainSeed: promptSeed, + ObtainPrivatePass: promptPrivPassPhrase, + } mgr, err := waddrmgr.Open(namespace, []byte(cfg.WalletPass), - activeNet.Params, nil) + activeNet.Params, config) if err == nil { txs, err = txstore.OpenDir(netdir) }