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