diff --git a/account.go b/account.go index 3506bda..9049216 100644 --- a/account.go +++ b/account.go @@ -130,6 +130,31 @@ func (a *Account) Rollback(height int32, hash *btcwire.ShaHash) { } } +// AddressUsed returns whether there are any recorded transactions spending to +// a given address. Assumming correct TxStore usage, this will return true iff +// there are any transactions with outputs to this address in the blockchain or +// the btcd mempool. +func (a *Account) AddressUsed(pkHash []byte) bool { + // This can be optimized by recording this data as it is read when + // opening an account, and keeping it up to date each time a new + // received tx arrives. + + a.TxStore.RLock() + defer a.TxStore.RUnlock() + + for i := range a.TxStore.s { + rtx, ok := a.TxStore.s[i].(*tx.RecvTx) + if !ok { + continue + } + + if bytes.Equal(rtx.ReceiverHash, pkHash) { + return true + } + } + return false +} + // CalculateBalance sums the amounts of all unspent transaction // outputs to addresses of a wallet and returns the balance as a // float64. @@ -190,6 +215,28 @@ func (a *Account) CalculateAddressBalance(pubkeyHash []byte, confirms int) float return float64(bal) / float64(btcutil.SatoshiPerBitcoin) } +// CurrentAddress gets the most recently requested Bitcoin payment address +// from an account. 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 (a *Account) CurrentAddress() (string, error) { + a.mtx.RLock() + addr, err := a.Wallet.LastChainedAddress() + a.mtx.RUnlock() + + if err != nil { + return "", err + } + + // Get next chained address if the last one has already been used. + pkHash, _, _ := btcutil.DecodeAddress(addr) + if a.AddressUsed(pkHash) { + addr, err = a.NewAddress() + } + + return addr, err +} + // ListTransactions returns a slice of maps with details about a recorded // transaction. This is intended to be used for listtransactions RPC // replies. diff --git a/cmdmgr.go b/cmdmgr.go index 79d615e..908535d 100644 --- a/cmdmgr.go +++ b/cmdmgr.go @@ -44,6 +44,7 @@ var rpcHandlers = map[string]cmdHandler{ // Standard bitcoind methods (implemented) "dumpprivkey": DumpPrivKey, "getaccount": GetAccount, + "getaccountaddress": GetAccountAddress, "getaddressesbyaccount": GetAddressesByAccount, "getbalance": GetBalance, "getnewaddress": GetNewAddress, @@ -61,7 +62,6 @@ var rpcHandlers = map[string]cmdHandler{ "backupwallet": Unimplemented, "createmultisig": Unimplemented, "dumpwallet": Unimplemented, - "getaccountaddress": Unimplemented, "getrawchangeaddress": Unimplemented, "getreceivedbyaccount": Unimplemented, "getreceivedbyaddress": Unimplemented, @@ -369,6 +369,54 @@ func GetAccount(frontend chan []byte, icmd btcjson.Cmd) { ReplySuccess(frontend, cmd.Id(), aname) } +// GetAccountAddress replies to a getaccountaddress request with the most +// recently-created chained address that has not yet been used (does not yet +// appear in the blockchain, or any tx that has arrived in the btcd mempool). +// If the most recently-requested address has been used, a new address (the +// next chained address in the keypool) is used. This can fail if the keypool +// runs out (and will return btcjson.ErrWalletKeypoolRanOut if that happens). +func GetAccountAddress(frontend chan []byte, icmd btcjson.Cmd) { + // Type assert icmd to access parameters. + cmd, ok := icmd.(*btcjson.GetAccountAddressCmd) + if !ok { + ReplyError(frontend, icmd.Id(), &btcjson.ErrInternal) + return + } + + // Lookup account for this request. + a, err := accountstore.Account(cmd.Account) + switch err { + case nil: + break + + case ErrAcctNotExist: + ReplyError(frontend, cmd.Id(), + &btcjson.ErrWalletInvalidAccountName) + + default: // all other non-nil errors + e := &btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: err.Error(), + } + ReplyError(frontend, cmd.Id(), e) + } + + switch addr, err := a.CurrentAddress(); err { + case nil: + ReplySuccess(frontend, cmd.Id(), addr) + + case wallet.ErrWalletLocked: + ReplyError(frontend, cmd.Id(), &btcjson.ErrWalletKeypoolRanOut) + + default: // all other non-nil errors + e := &btcjson.Error{ + Code: btcjson.ErrWallet.Code, + Message: err.Error(), + } + ReplyError(frontend, cmd.Id(), e) + } +} + // GetAddressBalance replies to a getaddressbalance extension request // by replying with the current balance (sum of unspent transaction // output amounts) for a single address. diff --git a/wallet/wallet.go b/wallet/wallet.go index a93b4d7..862b0b1 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -851,6 +851,26 @@ func (w *Wallet) NextChainedAddress(bs *BlockStamp) (string, error) { return addr.paymentAddress(w.net) } +// LastChainedAddress returns the most recently requested chained +// address from calling NextChainedAddress, or the root address if +// no chained addresses have been requested. +func (w *Wallet) LastChainedAddress() (string, error) { + // Lookup pubkey hash for last used chained address. + pkHash, ok := w.chainIdxMap[w.highestUsed] + if !ok { + return "", errors.New("chain index references unknown address") + } + + // Lookup address with this pubkey hash. + addr, ok := w.addrMap[pkHash] + if !ok { + return "", errors.New("cannot find address by pubkey hash") + } + + // Create and return payment address from serialized pubkey. + return addr.paymentAddress(w.net) +} + // extendKeypool grows the keypool by n addresses. func (w *Wallet) extendKeypool(n uint, aeskey []byte, bs *BlockStamp) error { // Get last chained address. New chained addresses will be