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