From 85af882c130b282c19773bdb8bb5f5d56ffa7227 Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Mon, 23 Jun 2014 16:59:57 -0500 Subject: [PATCH] Implement lockunspent and listlockunspent. Closes #50. Closes #55. --- account.go | 41 +++++++++++++++++++++++++++++- acctmgr.go | 12 +++++---- createtx.go | 6 +++++ createtx_test.go | 3 ++- rpcserver.go | 54 ++++++++++++++++++++++++++++++++++++++-- txstore/serialization.go | 2 +- txstore/tx.go | 1 - 7 files changed, 108 insertions(+), 11 deletions(-) diff --git a/account.go b/account.go index dfae15c..83af455 100644 --- a/account.go +++ b/account.go @@ -35,7 +35,8 @@ import ( type Account struct { name string *wallet.Wallet - TxStore *txstore.Store + TxStore *txstore.Store + lockedOutpoints map[btcwire.OutPoint]struct{} } // Lock locks the underlying wallet for an account. @@ -432,6 +433,44 @@ func (a *Account) exportBase64() (map[string]string, error) { return m, nil } +// LockedOutpoint returns whether an outpoint has been marked as locked and +// should not be used as an input for created transactions. +func (a *Account) LockedOutpoint(op btcwire.OutPoint) bool { + _, locked := a.lockedOutpoints[op] + return locked +} + +// LockOutpoint marks an outpoint as locked, that is, it should not be used as +// an input for newly created transactions. +func (a *Account) LockOutpoint(op btcwire.OutPoint) { + a.lockedOutpoints[op] = struct{}{} +} + +// UnlockOutpoint marks an outpoint as unlocked, that is, it may be used as an +// input for newly created transactions. +func (a *Account) UnlockOutpoint(op btcwire.OutPoint) { + delete(a.lockedOutpoints, op) +} + +// ResetLockedOutpoints resets the set of locked outpoints so all may be used +// as inputs for new transactions. +func (a *Account) ResetLockedOutpoints() { + a.lockedOutpoints = map[btcwire.OutPoint]struct{}{} +} + +// LockedOutpoints returns a slice of currently locked outpoints. This is +// intended to be used by marshaling the result as a JSON array for +// listlockunspent RPC results. +func (a *Account) LockedOutpoints() []btcjson.TransactionInput { + locked := make([]btcjson.TransactionInput, len(a.lockedOutpoints)) + i := 0 + for op := range a.lockedOutpoints { + locked[i] = btcjson.TransactionInput{op.Hash.String(), op.Index} + i++ + } + return locked +} + // Track requests btcd to send notifications of new transactions for // each address stored in a wallet. func (a *Account) Track() { diff --git a/acctmgr.go b/acctmgr.go index 0294b7a..8a72add 100644 --- a/acctmgr.go +++ b/acctmgr.go @@ -177,9 +177,10 @@ func openSavedAccount(name string, cfg *config) (*Account, error) { wlt := new(wallet.Wallet) txs := txstore.New() a := &Account{ - name: name, - Wallet: wlt, - TxStore: txs, + name: name, + Wallet: wlt, + TxStore: txs, + lockedOutpoints: map[btcwire.OutPoint]struct{}{}, } walletPath := accountFilename("wallet.bin", name, netdir) @@ -708,8 +709,9 @@ func (am *AccountManager) CreateEncryptedWallet(passphrase []byte) error { // manager. Registering will fail if the new account can not be // written immediately to disk. a := &Account{ - Wallet: wlt, - TxStore: txstore.New(), + Wallet: wlt, + TxStore: txstore.New(), + lockedOutpoints: map[btcwire.OutPoint]struct{}{}, } if err := am.RegisterNewAccount(a); err != nil { return err diff --git a/createtx.go b/createtx.go index 998736f..e72f5bc 100644 --- a/createtx.go +++ b/createtx.go @@ -197,6 +197,12 @@ func (a *Account) txToPairs(pairs map[string]btcutil.Amount, continue } } + + // Locked unspent outputs are skipped. + if a.LockedOutpoint(*unspent[i].OutPoint()) { + continue + } + eligible = append(eligible, unspent[i]) } } diff --git a/createtx_test.go b/createtx_test.go index 55ddfc3..307956a 100644 --- a/createtx_test.go +++ b/createtx_test.go @@ -89,7 +89,8 @@ func TestFakeTxs(t *testing.T) { return } a := &Account{ - Wallet: w, + Wallet: w, + lockedOutpoints: map[btcwire.OutPoint]struct{}{}, } w.Unlock([]byte("banana")) diff --git a/rpcserver.go b/rpcserver.go index d4e309d..8d95b57 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -728,6 +728,7 @@ func (s *rpcServer) PostClientRPC(w http.ResponseWriter, r *http.Request) { id = cmd.Id() } if err != nil { + fmt.Printf("%s\n", rpcRequest) _, err := w.Write(marshalError(idPointer(cmd.Id()))) if err != nil { log.Warnf("Client sent invalid request but unable "+ @@ -828,10 +829,12 @@ var rpcHandlers = map[string]requestHandler{ "importprivkey": ImportPrivKey, "keypoolrefill": KeypoolRefill, "listaccounts": ListAccounts, + "listlockunspent": ListLockUnspent, "listreceivedbyaddress": ListReceivedByAddress, "listsinceblock": ListSinceBlock, "listtransactions": ListTransactions, "listunspent": ListUnspent, + "lockunspent": LockUnspent, "sendfrom": SendFrom, "sendmany": SendMany, "sendtoaddress": SendToAddress, @@ -853,9 +856,7 @@ var rpcHandlers = map[string]requestHandler{ "getwalletinfo": Unimplemented, "importwallet": Unimplemented, "listaddressgroupings": Unimplemented, - "listlockunspent": Unimplemented, "listreceivedbyaccount": Unimplemented, - "lockunspent": Unimplemented, "move": Unimplemented, "setaccount": Unimplemented, "stop": Unimplemented, @@ -1589,6 +1590,20 @@ func ListAccounts(icmd btcjson.Cmd) (interface{}, error) { return AcctMgr.ListAccounts(cmd.MinConf), nil } +// ListLockUnspent handles a listlockunspent request by returning an array of +// all locked outpoints. +func ListLockUnspent(icmd btcjson.Cmd) (interface{}, error) { + // Due to our poor account support, this assumes only the default + // account is available. When the keystore and account heirarchies are + // reversed, the locked outpoints mapping will cover all accounts. + a, err := AcctMgr.Account("") + if err != nil { + return nil, err + } + + return a.LockedOutpoints(), nil +} + // ListReceivedByAddress handles a listreceivedbyaddress request by returning // a slice of objects, each one containing: // "account": the account of the receiving address; @@ -1851,6 +1866,41 @@ func ListUnspent(icmd btcjson.Cmd) (interface{}, error) { return AcctMgr.ListUnspent(cmd.MinConf, cmd.MaxConf, addresses) } +// LockUnspent handles the lockunspent command. +func LockUnspent(icmd btcjson.Cmd) (interface{}, error) { + cmd, ok := icmd.(*btcjson.LockUnspentCmd) + if !ok { + return nil, btcjson.ErrInternal + } + + // Due to our poor account support, this assumes only the default + // account is available. When the keystore and account heirarchies are + // reversed, the locked outpoints mapping will cover all accounts. + a, err := AcctMgr.Account("") + if err != nil { + return nil, err + } + + switch { + case cmd.Unlock && len(cmd.Transactions) == 0: + a.ResetLockedOutpoints() + default: + for _, input := range cmd.Transactions { + txSha, err := btcwire.NewShaHashFromStr(input.Txid) + if err != nil { + return nil, ParseError{err} + } + op := btcwire.OutPoint{Hash: *txSha, Index: input.Vout} + if cmd.Unlock { + a.UnlockOutpoint(op) + } else { + a.LockOutpoint(op) + } + } + } + return true, nil +} + // sendPairs is a helper routine to reduce duplicated code when creating and // sending payment transactions. func sendPairs(icmd btcjson.Cmd, account string, amounts map[string]btcutil.Amount, diff --git a/txstore/serialization.go b/txstore/serialization.go index dbc6780..3f8197f 100644 --- a/txstore/serialization.go +++ b/txstore/serialization.go @@ -566,7 +566,7 @@ func (t *txRecord) ReadFrom(r io.Reader) (int64, error) { } } - c := &credit{change, false, spentBy} + c := &credit{change, spentBy} credits = append(credits, c) } diff --git a/txstore/tx.go b/txstore/tx.go index 1076f13..6181d06 100644 --- a/txstore/tx.go +++ b/txstore/tx.go @@ -250,7 +250,6 @@ type debits struct { // credit describes a transaction output which was or is spendable by wallet. type credit struct { change bool - locked bool spentBy *BlockTxKey // nil if unspent }