OSDN Git Service

init push for spend 0 confirm utxo (#1112)
authorPaladz <yzhu101@uottawa.ca>
Thu, 5 Jul 2018 13:02:37 +0000 (21:02 +0800)
committerGitHub <noreply@github.com>
Thu, 5 Jul 2018 13:02:37 +0000 (21:02 +0800)
* init push for spend 0 confirm utxo

* elegant the code

* add unit test for utxo_keeper

* add unit test for wallet/utxo.go

* fix a unit test bug

* delete the unused control_receiver

27 files changed:
account/accounts.go
account/accounts_test.go
account/builder.go
account/image.go
account/reserve.go [deleted file]
account/reserve_test.go [deleted file]
account/utxo_keeper.go [new file with mode: 0644]
account/utxo_keeper_test.go [new file with mode: 0644]
api/accounts.go
api/query.go
api/receivers.go
api/request.go
api/transact.go
blockchain/txbuilder/actions.go
node/node.go
protocol/txpool.go
test/bench_blockchain_test.go
test/integration/standard_transaction_test.go
test/tx_test_util.go
test/wallet_test_util.go
wallet/indexer.go
wallet/unconfirmed.go
wallet/unconfirmed_test.go
wallet/utxo.go [new file with mode: 0644]
wallet/utxo_test.go [new file with mode: 0644]
wallet/wallet.go
wallet/wallet_test.go

index 72e11bd..9da0a8c 100644 (file)
@@ -2,11 +2,9 @@
 package account
 
 import (
-       "context"
        "encoding/json"
        "strings"
        "sync"
-       "time"
 
        "github.com/golang/groupcache/lru"
        log "github.com/sirupsen/logrus"
@@ -21,6 +19,7 @@ import (
        "github.com/bytom/crypto/sha3pool"
        "github.com/bytom/errors"
        "github.com/bytom/protocol"
+       "github.com/bytom/protocol/bc"
        "github.com/bytom/protocol/vm/vmutil"
 )
 
@@ -64,23 +63,27 @@ func contractIndexKey(accountID string) []byte {
        return append(contractIndexPrefix, []byte(accountID)...)
 }
 
-// NewManager creates a new account manager
-func NewManager(walletDB dbm.DB, chain *protocol.Chain) *Manager {
-       return &Manager{
-               db:          walletDB,
-               chain:       chain,
-               utxoDB:      newReserver(chain, walletDB),
-               cache:       lru.New(maxAccountCache),
-               aliasCache:  lru.New(maxAccountCache),
-               delayedACPs: make(map[*txbuilder.TemplateBuilder][]*CtrlProgram),
-       }
+// Account is structure of Bytom account
+type Account struct {
+       *signers.Signer
+       ID    string `json:"id"`
+       Alias string `json:"alias"`
+}
+
+//CtrlProgram is structure of account control program
+type CtrlProgram struct {
+       AccountID      string
+       Address        string
+       KeyIndex       uint64
+       ControlProgram []byte
+       Change         bool // Mark whether this control program is for UTXO change
 }
 
 // Manager stores accounts and their associated control programs.
 type Manager struct {
-       db     dbm.DB
-       chain  *protocol.Chain
-       utxoDB *reserver
+       db         dbm.DB
+       chain      *protocol.Chain
+       utxoKeeper *utxoKeeper
 
        cacheMu    sync.Mutex
        cache      *lru.Cache
@@ -93,71 +96,25 @@ type Manager struct {
        accountMu  sync.Mutex
 }
 
-// ExpireReservations removes reservations that have expired periodically.
-// It blocks until the context is canceled.
-func (m *Manager) ExpireReservations(ctx context.Context, period time.Duration) {
-       ticks := time.Tick(period)
-       for {
-               select {
-               case <-ctx.Done():
-                       log.Info("Deposed, ExpireReservations exiting")
-                       return
-               case <-ticks:
-                       err := m.utxoDB.ExpireReservations(ctx)
-                       if err != nil {
-                               log.WithField("error", err).Error("Expire reservations")
-                       }
-               }
-       }
-}
-
-// Account is structure of Bytom account
-type Account struct {
-       *signers.Signer
-       ID    string `json:"id"`
-       Alias string `json:"alias"`
-}
-
-func (m *Manager) getNextAccountIndex() uint64 {
-       m.accIndexMu.Lock()
-       defer m.accIndexMu.Unlock()
-
-       var nextIndex uint64 = 1
-       if rawIndexBytes := m.db.Get(accountIndexKey); rawIndexBytes != nil {
-               nextIndex = common.BytesToUnit64(rawIndexBytes) + 1
-       }
-
-       m.db.Set(accountIndexKey, common.Unit64ToBytes(nextIndex))
-       return nextIndex
-}
-
-func (m *Manager) getNextContractIndex(accountID string) uint64 {
-       m.accIndexMu.Lock()
-       defer m.accIndexMu.Unlock()
-
-       nextIndex := uint64(1)
-       if rawIndexBytes := m.db.Get(contractIndexKey(accountID)); rawIndexBytes != nil {
-               nextIndex = common.BytesToUnit64(rawIndexBytes) + 1
+// NewManager creates a new account manager
+func NewManager(walletDB dbm.DB, chain *protocol.Chain) *Manager {
+       return &Manager{
+               db:          walletDB,
+               chain:       chain,
+               utxoKeeper:  newUtxoKeeper(chain.BestBlockHeight, walletDB),
+               cache:       lru.New(maxAccountCache),
+               aliasCache:  lru.New(maxAccountCache),
+               delayedACPs: make(map[*txbuilder.TemplateBuilder][]*CtrlProgram),
        }
-
-       m.db.Set(contractIndexKey(accountID), common.Unit64ToBytes(nextIndex))
-       return nextIndex
 }
 
-// GetContractIndex return the current index
-func (m *Manager) GetContractIndex(accountID string) uint64 {
-       m.accIndexMu.Lock()
-       defer m.accIndexMu.Unlock()
-
-       index := uint64(1)
-       if rawIndexBytes := m.db.Get(contractIndexKey(accountID)); rawIndexBytes != nil {
-               index = common.BytesToUnit64(rawIndexBytes)
-       }
-       return index
+// AddUnconfirmedUtxo add untxo list to utxoKeeper
+func (m *Manager) AddUnconfirmedUtxo(utxos []*UTXO) {
+       m.utxoKeeper.AddUnconfirmedUtxo(utxos)
 }
 
 // Create creates a new Account.
-func (m *Manager) Create(ctx context.Context, xpubs []chainkd.XPub, quorum int, alias string) (*Account, error) {
+func (m *Manager) Create(xpubs []chainkd.XPub, quorum int, alias string) (*Account, error) {
        m.accountMu.Lock()
        defer m.accountMu.Unlock()
 
@@ -177,23 +134,51 @@ func (m *Manager) Create(ctx context.Context, xpubs []chainkd.XPub, quorum int,
        if err != nil {
                return nil, ErrMarshalAccount
        }
-       storeBatch := m.db.NewBatch()
 
        accountID := Key(id)
+       storeBatch := m.db.NewBatch()
        storeBatch.Set(accountID, rawAccount)
        storeBatch.Set(aliasKey(normalizedAlias), []byte(id))
        storeBatch.Write()
-
        return account, nil
 }
 
+// CreateAddress generate an address for the select account
+func (m *Manager) CreateAddress(accountID string, change bool) (cp *CtrlProgram, err error) {
+       account, err := m.FindByID(accountID)
+       if err != nil {
+               return nil, err
+       }
+       return m.createAddress(account, change)
+}
+
+// DeleteAccount deletes the account's ID or alias matching accountInfo.
+func (m *Manager) DeleteAccount(aliasOrID string) (err error) {
+       account := &Account{}
+       if account, err = m.FindByAlias(aliasOrID); err != nil {
+               if account, err = m.FindByID(aliasOrID); err != nil {
+                       return err
+               }
+       }
+
+       m.cacheMu.Lock()
+       m.aliasCache.Remove(account.Alias)
+       m.cacheMu.Unlock()
+
+       storeBatch := m.db.NewBatch()
+       storeBatch.Delete(aliasKey(account.Alias))
+       storeBatch.Delete(Key(account.ID))
+       storeBatch.Write()
+       return nil
+}
+
 // FindByAlias retrieves an account's Signer record by its alias
-func (m *Manager) FindByAlias(ctx context.Context, alias string) (*Account, error) {
+func (m *Manager) FindByAlias(alias string) (*Account, error) {
        m.cacheMu.Lock()
        cachedID, ok := m.aliasCache.Get(alias)
        m.cacheMu.Unlock()
        if ok {
-               return m.FindByID(ctx, cachedID.(string))
+               return m.FindByID(cachedID.(string))
        }
 
        rawID := m.db.Get(aliasKey(alias))
@@ -205,11 +190,11 @@ func (m *Manager) FindByAlias(ctx context.Context, alias string) (*Account, erro
        m.cacheMu.Lock()
        m.aliasCache.Add(alias, accountID)
        m.cacheMu.Unlock()
-       return m.FindByID(ctx, accountID)
+       return m.FindByID(accountID)
 }
 
 // FindByID returns an account's Signer record by its ID.
-func (m *Manager) FindByID(ctx context.Context, id string) (*Account, error) {
+func (m *Manager) FindByID(id string) (*Account, error) {
        m.cacheMu.Lock()
        cachedAccount, ok := m.cache.Get(id)
        m.cacheMu.Unlock()
@@ -233,138 +218,32 @@ func (m *Manager) FindByID(ctx context.Context, id string) (*Account, error) {
        return account, nil
 }
 
-// GetAliasByID return the account alias by given ID
-func (m *Manager) GetAliasByID(id string) string {
+// GetAccountByProgram return Account by given CtrlProgram
+func (m *Manager) GetAccountByProgram(program *CtrlProgram) (*Account, error) {
+       rawAccount := m.db.Get(Key(program.AccountID))
+       if rawAccount == nil {
+               return nil, ErrFindAccount
+       }
+
        account := &Account{}
+       return account, json.Unmarshal(rawAccount, account)
+}
 
+// GetAliasByID return the account alias by given ID
+func (m *Manager) GetAliasByID(id string) string {
        rawAccount := m.db.Get(Key(id))
        if rawAccount == nil {
-               log.Warn("fail to find account")
+               log.Warn("GetAliasByID fail to find account")
                return ""
        }
 
+       account := &Account{}
        if err := json.Unmarshal(rawAccount, account); err != nil {
                log.Warn(err)
-               return ""
        }
        return account.Alias
 }
 
-// CreateAddress generate an address for the select account
-func (m *Manager) CreateAddress(ctx context.Context, accountID string, change bool) (cp *CtrlProgram, err error) {
-       account, err := m.FindByID(ctx, accountID)
-       if err != nil {
-               return nil, err
-       }
-       return m.createAddress(ctx, account, change)
-}
-
-// CreateAddress generate an address for the select account
-func (m *Manager) createAddress(ctx context.Context, account *Account, change bool) (cp *CtrlProgram, err error) {
-       if len(account.XPubs) == 1 {
-               cp, err = m.createP2PKH(ctx, account, change)
-       } else {
-               cp, err = m.createP2SH(ctx, account, change)
-       }
-       if err != nil {
-               return nil, err
-       }
-
-       if err = m.insertAccountControlProgram(ctx, cp); err != nil {
-               return nil, err
-       }
-       return cp, nil
-}
-
-func (m *Manager) createP2PKH(ctx context.Context, account *Account, change bool) (*CtrlProgram, error) {
-       idx := m.getNextContractIndex(account.ID)
-       path := signers.Path(account.Signer, signers.AccountKeySpace, idx)
-       derivedXPubs := chainkd.DeriveXPubs(account.XPubs, path)
-       derivedPK := derivedXPubs[0].PublicKey()
-       pubHash := crypto.Ripemd160(derivedPK)
-
-       // TODO: pass different params due to config
-       address, err := common.NewAddressWitnessPubKeyHash(pubHash, &consensus.ActiveNetParams)
-       if err != nil {
-               return nil, err
-       }
-
-       control, err := vmutil.P2WPKHProgram([]byte(pubHash))
-       if err != nil {
-               return nil, err
-       }
-
-       return &CtrlProgram{
-               AccountID:      account.ID,
-               Address:        address.EncodeAddress(),
-               KeyIndex:       idx,
-               ControlProgram: control,
-               Change:         change,
-       }, nil
-}
-
-func (m *Manager) createP2SH(ctx context.Context, account *Account, change bool) (*CtrlProgram, error) {
-       idx := m.getNextContractIndex(account.ID)
-       path := signers.Path(account.Signer, signers.AccountKeySpace, idx)
-       derivedXPubs := chainkd.DeriveXPubs(account.XPubs, path)
-       derivedPKs := chainkd.XPubKeys(derivedXPubs)
-       signScript, err := vmutil.P2SPMultiSigProgram(derivedPKs, account.Quorum)
-       if err != nil {
-               return nil, err
-       }
-       scriptHash := crypto.Sha256(signScript)
-
-       // TODO: pass different params due to config
-       address, err := common.NewAddressWitnessScriptHash(scriptHash, &consensus.ActiveNetParams)
-       if err != nil {
-               return nil, err
-       }
-
-       control, err := vmutil.P2WSHProgram(scriptHash)
-       if err != nil {
-               return nil, err
-       }
-
-       return &CtrlProgram{
-               AccountID:      account.ID,
-               Address:        address.EncodeAddress(),
-               KeyIndex:       idx,
-               ControlProgram: control,
-               Change:         change,
-       }, nil
-}
-
-//CtrlProgram is structure of account control program
-type CtrlProgram struct {
-       AccountID      string
-       Address        string
-       KeyIndex       uint64
-       ControlProgram []byte
-       Change         bool // Mark whether this control program is for UTXO change
-}
-
-func (m *Manager) insertAccountControlProgram(ctx context.Context, progs ...*CtrlProgram) error {
-       var hash common.Hash
-       for _, prog := range progs {
-               accountCP, err := json.Marshal(prog)
-               if err != nil {
-                       return err
-               }
-
-               sha3pool.Sum256(hash[:], prog.ControlProgram)
-               m.db.Set(ContractKey(hash), accountCP)
-       }
-       return nil
-}
-
-// IsLocalControlProgram check is the input control program belong to local
-func (m *Manager) IsLocalControlProgram(prog []byte) bool {
-       var hash common.Hash
-       sha3pool.Sum256(hash[:], prog)
-       bytes := m.db.Get(ContractKey(hash))
-       return bytes != nil
-}
-
 // GetCoinbaseControlProgram will return a coinbase script
 func (m *Manager) GetCoinbaseControlProgram() ([]byte, error) {
        if data := m.db.Get(miningAddressKey); data != nil {
@@ -384,7 +263,7 @@ func (m *Manager) GetCoinbaseControlProgram() ([]byte, error) {
                return nil, err
        }
 
-       program, err := m.createAddress(nil, account, false)
+       program, err := m.createAddress(account, false)
        if err != nil {
                return nil, err
        }
@@ -398,26 +277,56 @@ func (m *Manager) GetCoinbaseControlProgram() ([]byte, error) {
        return program.ControlProgram, nil
 }
 
-// DeleteAccount deletes the account's ID or alias matching accountInfo.
-func (m *Manager) DeleteAccount(aliasOrID string) (err error) {
-       account := &Account{}
-       if account, err = m.FindByAlias(nil, aliasOrID); err != nil {
-               if account, err = m.FindByID(nil, aliasOrID); err != nil {
-                       return err
-               }
+// GetContractIndex return the current index
+func (m *Manager) GetContractIndex(accountID string) uint64 {
+       m.accIndexMu.Lock()
+       defer m.accIndexMu.Unlock()
+
+       index := uint64(1)
+       if rawIndexBytes := m.db.Get(contractIndexKey(accountID)); rawIndexBytes != nil {
+               index = common.BytesToUnit64(rawIndexBytes)
        }
+       return index
+}
 
-       storeBatch := m.db.NewBatch()
+// GetProgramByAddress return CtrlProgram by given address
+func (m *Manager) GetProgramByAddress(address string) (*CtrlProgram, error) {
+       addr, err := common.DecodeAddress(address, &consensus.ActiveNetParams)
+       if err != nil {
+               return nil, err
+       }
 
-       m.cacheMu.Lock()
-       m.aliasCache.Remove(account.Alias)
-       m.cacheMu.Unlock()
+       redeemContract := addr.ScriptAddress()
+       program := []byte{}
+       switch addr.(type) {
+       case *common.AddressWitnessPubKeyHash:
+               program, err = vmutil.P2WPKHProgram(redeemContract)
+       case *common.AddressWitnessScriptHash:
+               program, err = vmutil.P2WSHProgram(redeemContract)
+       default:
+               return nil, ErrInvalidAddress
+       }
+       if err != nil {
+               return nil, err
+       }
 
-       storeBatch.Delete(aliasKey(account.Alias))
-       storeBatch.Delete(Key(account.ID))
-       storeBatch.Write()
+       var hash [32]byte
+       sha3pool.Sum256(hash[:], program)
+       rawProgram := m.db.Get(ContractKey(hash))
+       if rawProgram == nil {
+               return nil, ErrFindCtrlProgram
+       }
 
-       return nil
+       cp := &CtrlProgram{}
+       return cp, json.Unmarshal(rawProgram, cp)
+}
+
+// IsLocalControlProgram check is the input control program belong to local
+func (m *Manager) IsLocalControlProgram(prog []byte) bool {
+       var hash common.Hash
+       sha3pool.Sum256(hash[:], prog)
+       bytes := m.db.Get(ContractKey(hash))
+       return bytes != nil
 }
 
 // ListAccounts will return the accounts in the db
@@ -433,13 +342,12 @@ func (m *Manager) ListAccounts(id string) ([]*Account, error) {
                }
                accounts = append(accounts, account)
        }
-
        return accounts, nil
 }
 
 // ListControlProgram return all the local control program
 func (m *Manager) ListControlProgram() ([]*CtrlProgram, error) {
-       var cps []*CtrlProgram
+       cps := []*CtrlProgram{}
        cpIter := m.db.IteratorPrefix(contractPrefix)
        defer cpIter.Release()
 
@@ -450,57 +358,117 @@ func (m *Manager) ListControlProgram() ([]*CtrlProgram, error) {
                }
                cps = append(cps, cp)
        }
-
        return cps, nil
 }
 
-// GetProgramByAddress return CtrlProgram by given address
-func (m *Manager) GetProgramByAddress(address string) (*CtrlProgram, error) {
-       addr, err := common.DecodeAddress(address, &consensus.ActiveNetParams)
+// RemoveUnconfirmedUtxo remove utxos from the utxoKeeper
+func (m *Manager) RemoveUnconfirmedUtxo(hashes []*bc.Hash) {
+       m.utxoKeeper.RemoveUnconfirmedUtxo(hashes)
+}
+
+// CreateAddress generate an address for the select account
+func (m *Manager) createAddress(account *Account, change bool) (cp *CtrlProgram, err error) {
+       if len(account.XPubs) == 1 {
+               cp, err = m.createP2PKH(account, change)
+       } else {
+               cp, err = m.createP2SH(account, change)
+       }
        if err != nil {
                return nil, err
        }
+       return cp, m.insertControlPrograms(cp)
+}
 
-       redeemContract := addr.ScriptAddress()
-       program := []byte{}
-       switch addr.(type) {
-       case *common.AddressWitnessPubKeyHash:
-               program, err = vmutil.P2WPKHProgram(redeemContract)
-       case *common.AddressWitnessScriptHash:
-               program, err = vmutil.P2WSHProgram(redeemContract)
-       default:
-               return nil, ErrInvalidAddress
+func (m *Manager) createP2PKH(account *Account, change bool) (*CtrlProgram, error) {
+       idx := m.getNextContractIndex(account.ID)
+       path := signers.Path(account.Signer, signers.AccountKeySpace, idx)
+       derivedXPubs := chainkd.DeriveXPubs(account.XPubs, path)
+       derivedPK := derivedXPubs[0].PublicKey()
+       pubHash := crypto.Ripemd160(derivedPK)
+
+       address, err := common.NewAddressWitnessPubKeyHash(pubHash, &consensus.ActiveNetParams)
+       if err != nil {
+               return nil, err
        }
+
+       control, err := vmutil.P2WPKHProgram([]byte(pubHash))
        if err != nil {
                return nil, err
        }
 
-       var hash [32]byte
-       cp := &CtrlProgram{}
-       sha3pool.Sum256(hash[:], program)
-       rawProgram := m.db.Get(ContractKey(hash))
-       if rawProgram == nil {
-               return nil, ErrFindCtrlProgram
+       return &CtrlProgram{
+               AccountID:      account.ID,
+               Address:        address.EncodeAddress(),
+               KeyIndex:       idx,
+               ControlProgram: control,
+               Change:         change,
+       }, nil
+}
+
+func (m *Manager) createP2SH(account *Account, change bool) (*CtrlProgram, error) {
+       idx := m.getNextContractIndex(account.ID)
+       path := signers.Path(account.Signer, signers.AccountKeySpace, idx)
+       derivedXPubs := chainkd.DeriveXPubs(account.XPubs, path)
+       derivedPKs := chainkd.XPubKeys(derivedXPubs)
+       signScript, err := vmutil.P2SPMultiSigProgram(derivedPKs, account.Quorum)
+       if err != nil {
+               return nil, err
+       }
+       scriptHash := crypto.Sha256(signScript)
+
+       address, err := common.NewAddressWitnessScriptHash(scriptHash, &consensus.ActiveNetParams)
+       if err != nil {
+               return nil, err
        }
 
-       if err := json.Unmarshal(rawProgram, cp); err != nil {
+       control, err := vmutil.P2WSHProgram(scriptHash)
+       if err != nil {
                return nil, err
        }
 
-       return cp, nil
+       return &CtrlProgram{
+               AccountID:      account.ID,
+               Address:        address.EncodeAddress(),
+               KeyIndex:       idx,
+               ControlProgram: control,
+               Change:         change,
+       }, nil
 }
 
-// GetAccountByProgram return Account by given CtrlProgram
-func (m *Manager) GetAccountByProgram(program *CtrlProgram) (*Account, error) {
-       rawAccount := m.db.Get(Key(program.AccountID))
-       if rawAccount == nil {
-               return nil, ErrFindAccount
+func (m *Manager) getNextAccountIndex() uint64 {
+       m.accIndexMu.Lock()
+       defer m.accIndexMu.Unlock()
+
+       var nextIndex uint64 = 1
+       if rawIndexBytes := m.db.Get(accountIndexKey); rawIndexBytes != nil {
+               nextIndex = common.BytesToUnit64(rawIndexBytes) + 1
        }
+       m.db.Set(accountIndexKey, common.Unit64ToBytes(nextIndex))
+       return nextIndex
+}
 
-       account := &Account{}
-       if err := json.Unmarshal(rawAccount, account); err != nil {
-               return nil, err
+func (m *Manager) getNextContractIndex(accountID string) uint64 {
+       m.accIndexMu.Lock()
+       defer m.accIndexMu.Unlock()
+
+       nextIndex := uint64(1)
+       if rawIndexBytes := m.db.Get(contractIndexKey(accountID)); rawIndexBytes != nil {
+               nextIndex = common.BytesToUnit64(rawIndexBytes) + 1
        }
+       m.db.Set(contractIndexKey(accountID), common.Unit64ToBytes(nextIndex))
+       return nextIndex
+}
 
-       return account, nil
+func (m *Manager) insertControlPrograms(progs ...*CtrlProgram) error {
+       var hash common.Hash
+       for _, prog := range progs {
+               accountCP, err := json.Marshal(prog)
+               if err != nil {
+                       return err
+               }
+
+               sha3pool.Sum256(hash[:], prog.ControlProgram)
+               m.db.Set(ContractKey(hash), accountCP)
+       }
+       return nil
 }
index 9cbe10d..f4481b4 100644 (file)
@@ -1,7 +1,6 @@
 package account
 
 import (
-       "context"
        "io/ioutil"
        "os"
        "strings"
@@ -19,7 +18,7 @@ import (
 func TestCreateAccountWithUppercase(t *testing.T) {
        m := mockAccountManager(t)
        alias := "UPPER"
-       account, err := m.Create(nil, []chainkd.XPub{testutil.TestXPub}, 1, alias)
+       account, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, alias)
 
        if err != nil {
                t.Fatal(err)
@@ -33,7 +32,7 @@ func TestCreateAccountWithUppercase(t *testing.T) {
 func TestCreateAccountWithSpaceTrimed(t *testing.T) {
        m := mockAccountManager(t)
        alias := " with space "
-       account, err := m.Create(nil, []chainkd.XPub{testutil.TestXPub}, 1, alias)
+       account, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, alias)
 
        if err != nil {
                t.Fatal(err)
@@ -43,12 +42,12 @@ func TestCreateAccountWithSpaceTrimed(t *testing.T) {
                t.Fatal("created account alias should be lowercase")
        }
 
-       nilAccount, err := m.FindByAlias(nil, alias)
+       nilAccount, err := m.FindByAlias(alias)
        if nilAccount != nil {
                t.Fatal("expected nil")
        }
 
-       target, err := m.FindByAlias(nil, strings.ToLower(strings.TrimSpace(alias)))
+       target, err := m.FindByAlias(strings.ToLower(strings.TrimSpace(alias)))
        if target == nil {
                t.Fatal("expected Account, but got nil")
        }
@@ -56,14 +55,12 @@ func TestCreateAccountWithSpaceTrimed(t *testing.T) {
 
 func TestCreateAccount(t *testing.T) {
        m := mockAccountManager(t)
-       ctx := context.Background()
-
-       account, err := m.Create(ctx, []chainkd.XPub{testutil.TestXPub}, 1, "test-alias")
+       account, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias")
        if err != nil {
                testutil.FatalErr(t, err)
        }
 
-       found, err := m.FindByID(ctx, account.ID)
+       found, err := m.FindByID(account.ID)
        if err != nil {
                t.Errorf("unexpected error %v", err)
        }
@@ -74,10 +71,9 @@ func TestCreateAccount(t *testing.T) {
 
 func TestCreateAccountReusedAlias(t *testing.T) {
        m := mockAccountManager(t)
-       ctx := context.Background()
-       m.createTestAccount(ctx, t, "test-alias", nil)
+       m.createTestAccount(t, "test-alias", nil)
 
-       _, err := m.Create(ctx, []chainkd.XPub{testutil.TestXPub}, 1, "test-alias")
+       _, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias")
        if errors.Root(err) != ErrDuplicateAlias {
                t.Errorf("expected %s when reusing an alias, got %v", ErrDuplicateAlias, err)
        }
@@ -85,14 +81,13 @@ func TestCreateAccountReusedAlias(t *testing.T) {
 
 func TestDeleteAccount(t *testing.T) {
        m := mockAccountManager(t)
-       ctx := context.Background()
 
-       account1, err := m.Create(ctx, []chainkd.XPub{testutil.TestXPub}, 1, "test-alias1")
+       account1, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias1")
        if err != nil {
                testutil.FatalErr(t, err)
        }
 
-       account2, err := m.Create(ctx, []chainkd.XPub{testutil.TestXPub}, 1, "test-alias2")
+       account2, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias2")
        if err != nil {
                testutil.FatalErr(t, err)
        }
@@ -101,7 +96,7 @@ func TestDeleteAccount(t *testing.T) {
                testutil.FatalErr(t, err)
        }
 
-       found, err := m.FindByID(ctx, account1.ID)
+       found, err := m.FindByID(account1.ID)
        if err != nil {
                t.Errorf("expected account %v should be deleted", found)
        }
@@ -110,7 +105,7 @@ func TestDeleteAccount(t *testing.T) {
                testutil.FatalErr(t, err)
        }
 
-       found, err = m.FindByID(ctx, account2.ID)
+       found, err = m.FindByID(account2.ID)
        if err != nil {
                t.Errorf("expected account %v should be deleted", found)
        }
@@ -118,10 +113,9 @@ func TestDeleteAccount(t *testing.T) {
 
 func TestFindByID(t *testing.T) {
        m := mockAccountManager(t)
-       ctx := context.Background()
-       account := m.createTestAccount(ctx, t, "", nil)
+       account := m.createTestAccount(t, "", nil)
 
-       found, err := m.FindByID(ctx, account.ID)
+       found, err := m.FindByID(account.ID)
        if err != nil {
                testutil.FatalErr(t, err)
        }
@@ -133,10 +127,9 @@ func TestFindByID(t *testing.T) {
 
 func TestFindByAlias(t *testing.T) {
        m := mockAccountManager(t)
-       ctx := context.Background()
-       account := m.createTestAccount(ctx, t, "some-alias", nil)
+       account := m.createTestAccount(t, "some-alias", nil)
 
-       found, err := m.FindByAlias(ctx, "some-alias")
+       found, err := m.FindByAlias("some-alias")
        if err != nil {
                testutil.FatalErr(t, err)
        }
@@ -166,8 +159,8 @@ func mockAccountManager(t *testing.T) *Manager {
        return NewManager(testDB, chain)
 }
 
-func (m *Manager) createTestAccount(ctx context.Context, t testing.TB, alias string, tags map[string]interface{}) *Account {
-       account, err := m.Create(ctx, []chainkd.XPub{testutil.TestXPub}, 1, alias)
+func (m *Manager) createTestAccount(t testing.TB, alias string, tags map[string]interface{}) *Account {
+       account, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, alias)
        if err != nil {
                testutil.FatalErr(t, err)
        }
index c11af40..da4cb81 100644 (file)
@@ -5,8 +5,6 @@ import (
        "encoding/hex"
        "encoding/json"
 
-       log "github.com/sirupsen/logrus"
-
        "github.com/bytom/blockchain/signers"
        "github.com/bytom/blockchain/txbuilder"
        "github.com/bytom/common"
@@ -22,14 +20,14 @@ import (
 //DecodeSpendAction unmarshal JSON-encoded data of spend action
 func (m *Manager) DecodeSpendAction(data []byte) (txbuilder.Action, error) {
        a := &spendAction{accounts: m}
-       err := json.Unmarshal(data, a)
-       return a, err
+       return a, json.Unmarshal(data, a)
 }
 
 type spendAction struct {
        accounts *Manager
        bc.AssetAmount
-       AccountID string `json:"account_id"`
+       AccountID      string `json:"account_id"`
+       UseUnconfirmed bool   `json:"use_unconfirmed"`
 }
 
 // MergeSpendAction merge common assetID and accountID spend action
@@ -43,6 +41,7 @@ func MergeSpendAction(actions []txbuilder.Action) []txbuilder.Action {
                        actionKey := act.AssetId.String() + act.AccountID
                        if tmpAct, ok := spendActionMap[actionKey]; ok {
                                tmpAct.Amount += act.Amount
+                               tmpAct.UseUnconfirmed = tmpAct.UseUnconfirmed || act.UseUnconfirmed
                        } else {
                                spendActionMap[actionKey] = act
                                resultActions = append(resultActions, act)
@@ -66,45 +65,38 @@ func (a *spendAction) Build(ctx context.Context, b *txbuilder.TemplateBuilder) e
                return txbuilder.MissingFieldsError(missing...)
        }
 
-       acct, err := a.accounts.FindByID(ctx, a.AccountID)
+       acct, err := a.accounts.FindByID(a.AccountID)
        if err != nil {
                return errors.Wrap(err, "get account info")
        }
 
-       src := source{
-               AssetID:   *a.AssetId,
-               AccountID: a.AccountID,
-       }
-       res, err := a.accounts.utxoDB.Reserve(src, a.Amount, b.MaxTime())
+       res, err := a.accounts.utxoKeeper.Reserve(a.AccountID, a.AssetId, a.Amount, a.UseUnconfirmed, b.MaxTime())
        if err != nil {
                return errors.Wrap(err, "reserving utxos")
        }
 
        // Cancel the reservation if the build gets rolled back.
-       b.OnRollback(canceler(ctx, a.accounts, res.ID))
-
-       for _, r := range res.UTXOs {
+       b.OnRollback(func() { a.accounts.utxoKeeper.Cancel(res.id) })
+       for _, r := range res.utxos {
                txInput, sigInst, err := UtxoToInputs(acct.Signer, r)
                if err != nil {
                        return errors.Wrap(err, "creating inputs")
                }
-               err = b.AddInput(txInput, sigInst)
-               if err != nil {
+
+               if err = b.AddInput(txInput, sigInst); err != nil {
                        return errors.Wrap(err, "adding inputs")
                }
        }
 
-       if res.Change > 0 {
-               acp, err := a.accounts.CreateAddress(ctx, a.AccountID, true)
+       if res.change > 0 {
+               acp, err := a.accounts.CreateAddress(a.AccountID, true)
                if err != nil {
                        return errors.Wrap(err, "creating control program")
                }
 
                // Don't insert the control program until callbacks are executed.
-               a.accounts.insertControlProgramDelayed(ctx, b, acp)
-
-               err = b.AddOutput(types.NewTxOutput(*a.AssetId, res.Change, acp.ControlProgram))
-               if err != nil {
+               a.accounts.insertControlProgramDelayed(b, acp)
+               if err = b.AddOutput(types.NewTxOutput(*a.AssetId, res.change, acp.ControlProgram)); err != nil {
                        return errors.Wrap(err, "adding change output")
                }
        }
@@ -114,14 +106,14 @@ func (a *spendAction) Build(ctx context.Context, b *txbuilder.TemplateBuilder) e
 //DecodeSpendUTXOAction unmarshal JSON-encoded data of spend utxo action
 func (m *Manager) DecodeSpendUTXOAction(data []byte) (txbuilder.Action, error) {
        a := &spendUTXOAction{accounts: m}
-       err := json.Unmarshal(data, a)
-       return a, err
+       return a, json.Unmarshal(data, a)
 }
 
 type spendUTXOAction struct {
-       accounts  *Manager
-       OutputID  *bc.Hash           `json:"output_id"`
-       Arguments []contractArgument `json:"arguments"`
+       accounts       *Manager
+       OutputID       *bc.Hash           `json:"output_id"`
+       UseUnconfirmed bool               `json:"use_unconfirmed"`
+       Arguments      []contractArgument `json:"arguments"`
 }
 
 // contractArgument for smart contract
@@ -146,22 +138,23 @@ func (a *spendUTXOAction) Build(ctx context.Context, b *txbuilder.TemplateBuilde
                return txbuilder.MissingFieldsError("output_id")
        }
 
-       res, err := a.accounts.utxoDB.ReserveUTXO(ctx, *a.OutputID, b.MaxTime())
+       res, err := a.accounts.utxoKeeper.ReserveParticular(*a.OutputID, a.UseUnconfirmed, b.MaxTime())
        if err != nil {
                return err
        }
-       b.OnRollback(canceler(ctx, a.accounts, res.ID))
 
+       b.OnRollback(func() { a.accounts.utxoKeeper.Cancel(res.id) })
        var accountSigner *signers.Signer
-       if len(res.Source.AccountID) != 0 {
-               account, err := a.accounts.FindByID(ctx, res.Source.AccountID)
+       if len(res.utxos[0].AccountID) != 0 {
+               account, err := a.accounts.FindByID(res.utxos[0].AccountID)
                if err != nil {
                        return err
                }
+
                accountSigner = account.Signer
        }
 
-       txInput, sigInst, err := UtxoToInputs(accountSigner, res.UTXOs[0])
+       txInput, sigInst, err := UtxoToInputs(accountSigner, res.utxos[0])
        if err != nil {
                return err
        }
@@ -205,15 +198,6 @@ func (a *spendUTXOAction) Build(ctx context.Context, b *txbuilder.TemplateBuilde
        return b.AddInput(txInput, sigInst)
 }
 
-// Best-effort cancellation attempt to put in txbuilder.BuildResult.Rollback.
-func canceler(ctx context.Context, m *Manager, rid uint64) func() {
-       return func() {
-               if err := m.utxoDB.Cancel(ctx, rid); err != nil {
-                       log.WithField("error", err).Error("Best-effort cancellation attempt to put in txbuilder.BuildResult.Rollback")
-               }
-       }
-}
-
 // UtxoToInputs convert an utxo to the txinput
 func UtxoToInputs(signer *signers.Signer, u *UTXO) (*types.TxInput, *txbuilder.SigningInstruction, error) {
        txInput := types.NewSpendInput(nil, u.SourceID, u.AssetID, u.Amount, u.SourcePos, u.ControlProgram)
@@ -263,7 +247,7 @@ func UtxoToInputs(signer *signers.Signer, u *UTXO) (*types.TxInput, *txbuilder.S
 // registers callbacks on the TemplateBuilder so that all of the template's
 // account control programs are batch inserted if building the rest of
 // the template is successful.
-func (m *Manager) insertControlProgramDelayed(ctx context.Context, b *txbuilder.TemplateBuilder, acp *CtrlProgram) {
+func (m *Manager) insertControlProgramDelayed(b *txbuilder.TemplateBuilder, acp *CtrlProgram) {
        m.delayedACPsMu.Lock()
        m.delayedACPs[b] = append(m.delayedACPs[b], acp)
        m.delayedACPsMu.Unlock()
@@ -283,6 +267,6 @@ func (m *Manager) insertControlProgramDelayed(ctx context.Context, b *txbuilder.
                if len(acps) == 0 {
                        return nil
                }
-               return m.insertAccountControlProgram(ctx, acps...)
+               return m.insertControlPrograms(acps...)
        })
 }
index 14d240d..264b6bd 100644 (file)
@@ -77,7 +77,7 @@ func (m *Manager) Restore(image *Image) error {
 
        for _, slice := range image.Slice {
                for i := uint64(1); i <= slice.ContractIndex; i++ {
-                       if _, err := m.createAddress(nil, slice.Account, false); err != nil {
+                       if _, err := m.createAddress(slice.Account, false); err != nil {
                                return err
                        }
                }
diff --git a/account/reserve.go b/account/reserve.go
deleted file mode 100644 (file)
index 6fe0665..0000000
+++ /dev/null
@@ -1,337 +0,0 @@
-package account
-
-import (
-       "context"
-       "encoding/json"
-       "sync"
-       "sync/atomic"
-       "time"
-
-       dbm "github.com/tendermint/tmlibs/db"
-
-       "github.com/bytom/errors"
-       "github.com/bytom/protocol"
-       "github.com/bytom/protocol/bc"
-)
-
-var (
-       // ErrInsufficient indicates the account doesn't contain enough
-       // units of the requested asset to satisfy the reservation.
-       // New units must be deposited into the account in order to
-       // satisfy the request; change will not be sufficient.
-       ErrInsufficient = errors.New("reservation found insufficient funds")
-
-       // ErrReserved indicates that a reservation could not be
-       // satisfied because some of the outputs were already reserved.
-       // When those reservations are finalized into a transaction
-       // (and no other transaction spends funds from the account),
-       // new change outputs will be created
-       // in sufficient amounts to satisfy the request.
-       ErrReserved = errors.New("reservation found outputs already reserved")
-       // ErrMatchUTXO indicates the account doesn't contain enough utxo to satisfy the reservation.
-       ErrMatchUTXO = errors.New("can't match enough valid utxos")
-       // ErrReservation indicates the reserver doesn't found the reservation with the provided ID.
-       ErrReservation = errors.New("couldn't find reservation")
-)
-
-// UTXO describes an individual account utxo.
-type UTXO struct {
-       OutputID            bc.Hash
-       SourceID            bc.Hash
-       AssetID             bc.AssetID
-       Amount              uint64
-       SourcePos           uint64
-       ControlProgram      []byte
-       AccountID           string
-       Address             string
-       ControlProgramIndex uint64
-       ValidHeight         uint64
-       Change              bool
-}
-
-func (u *UTXO) source() source {
-       return source{AssetID: u.AssetID, AccountID: u.AccountID}
-}
-
-// source describes the criteria to use when selecting UTXOs.
-type source struct {
-       AssetID   bc.AssetID
-       AccountID string
-}
-
-// reservation describes a reservation of a set of UTXOs belonging
-// to a particular account. Reservations are immutable.
-type reservation struct {
-       ID     uint64
-       Source source
-       UTXOs  []*UTXO
-       Change uint64
-       Expiry time.Time
-}
-
-func newReserver(c *protocol.Chain, walletdb dbm.DB) *reserver {
-       return &reserver{
-               c:            c,
-               db:           walletdb,
-               reservations: make(map[uint64]*reservation),
-               sources:      make(map[source]*sourceReserver),
-       }
-}
-
-// reserver implements a utxo reserver that stores reservations
-// in-memory. It relies on the account_utxos table for the source of
-// truth of valid UTXOs but tracks which of those UTXOs are reserved
-// in-memory.
-//
-// To reduce latency and prevent deadlock, no two mutexes (either on
-// reserver or sourceReserver) should be held at the same time
-type reserver struct {
-       // `sync/atomic` expects the first word in an allocated struct to be 64-bit
-       // aligned on both ARM and x86-32. See https://goo.gl/zW7dgq for more details.
-       nextReservationID uint64
-       c                 *protocol.Chain
-       db                dbm.DB
-
-       reservationsMu sync.Mutex
-       reservations   map[uint64]*reservation
-
-       sourcesMu sync.Mutex
-       sources   map[source]*sourceReserver
-}
-
-// Reserve selects and reserves UTXOs according to the criteria provided
-// in source. The resulting reservation expires at exp.
-func (re *reserver) Reserve(src source, amount uint64, exp time.Time) (res *reservation, err error) {
-       sourceReserver := re.source(src)
-
-       // Try to reserve the right amount.
-       rid := atomic.AddUint64(&re.nextReservationID, 1)
-       reserved, total, isImmature, err := sourceReserver.reserve(rid, amount)
-       if err != nil {
-               if isImmature {
-                       return nil, errors.WithDetail(err, "some coinbase utxos are immature")
-               }
-               return nil, err
-       }
-
-       res = &reservation{
-               ID:     rid,
-               Source: src,
-               UTXOs:  reserved,
-               Expiry: exp,
-       }
-
-       // Save the successful reservation.
-       re.reservationsMu.Lock()
-       defer re.reservationsMu.Unlock()
-       re.reservations[rid] = res
-
-       // Make change if necessary
-       if total > amount {
-               res.Change = total - amount
-       }
-       return res, nil
-}
-
-// ReserveUTXO reserves a specific utxo for spending. The resulting
-// reservation expires at exp.
-func (re *reserver) ReserveUTXO(ctx context.Context, out bc.Hash, exp time.Time) (*reservation, error) {
-       u, err := findSpecificUTXO(re.db, out)
-       if err != nil {
-               return nil, err
-       }
-
-       //u.ValidHeight > 0 means coinbase utxo
-       if u.ValidHeight > 0 && u.ValidHeight > re.c.BestBlockHeight() {
-               return nil, errors.WithDetail(ErrMatchUTXO, "this coinbase utxo is immature")
-       }
-
-       rid := atomic.AddUint64(&re.nextReservationID, 1)
-       err = re.source(u.source()).reserveUTXO(rid, u)
-       if err != nil {
-               return nil, err
-       }
-
-       res := &reservation{
-               ID:     rid,
-               Source: u.source(),
-               UTXOs:  []*UTXO{u},
-               Expiry: exp,
-       }
-       re.reservationsMu.Lock()
-       re.reservations[rid] = res
-       re.reservationsMu.Unlock()
-       return res, nil
-}
-
-// Cancel makes a best-effort attempt at canceling the reservation with
-// the provided ID.
-func (re *reserver) Cancel(ctx context.Context, rid uint64) error {
-       re.reservationsMu.Lock()
-       res, ok := re.reservations[rid]
-       delete(re.reservations, rid)
-       re.reservationsMu.Unlock()
-       if !ok {
-               return errors.Wrapf(ErrReservation, "rid=%d", rid)
-       }
-       re.source(res.Source).cancel(res)
-       /*if res.ClientToken != nil {
-               re.idempotency.Forget(*res.ClientToken)
-       }*/
-       return nil
-}
-
-// ExpireReservations cleans up all reservations that have expired,
-// making their UTXOs available for reservation again.
-func (re *reserver) ExpireReservations(ctx context.Context) error {
-       // Remove records of any reservations that have expired.
-       now := time.Now()
-       var canceled []*reservation
-       re.reservationsMu.Lock()
-       for rid, res := range re.reservations {
-               if res.Expiry.Before(now) {
-                       canceled = append(canceled, res)
-                       delete(re.reservations, rid)
-               }
-       }
-       re.reservationsMu.Unlock()
-
-       for _, res := range canceled {
-               re.source(res.Source).cancel(res)
-       }
-       return nil
-}
-
-func (re *reserver) source(src source) *sourceReserver {
-       re.sourcesMu.Lock()
-       defer re.sourcesMu.Unlock()
-
-       sr, ok := re.sources[src]
-       if ok {
-               return sr
-       }
-
-       sr = &sourceReserver{
-               db:            re.db,
-               src:           src,
-               reserved:      make(map[bc.Hash]uint64),
-               currentHeight: re.c.BestBlockHeight,
-       }
-       re.sources[src] = sr
-       return sr
-}
-
-type sourceReserver struct {
-       db            dbm.DB
-       src           source
-       currentHeight func() uint64
-       mu            sync.Mutex
-       reserved      map[bc.Hash]uint64
-}
-
-func (sr *sourceReserver) reserve(rid uint64, amount uint64) ([]*UTXO, uint64, bool, error) {
-       var (
-               reserved, unavailable uint64
-               reservedUTXOs         []*UTXO
-       )
-
-       utxos, isImmature, err := findMatchingUTXOs(sr.db, sr.src, sr.currentHeight)
-       if err != nil {
-               return nil, 0, isImmature, errors.Wrap(err)
-       }
-
-       sr.mu.Lock()
-       defer sr.mu.Unlock()
-       for _, u := range utxos {
-               // If the UTXO is already reserved, skip it.
-               if _, ok := sr.reserved[u.OutputID]; ok {
-                       unavailable += u.Amount
-                       continue
-               }
-
-               reserved += u.Amount
-               reservedUTXOs = append(reservedUTXOs, u)
-               if reserved >= amount {
-                       break
-               }
-       }
-       if reserved+unavailable < amount {
-               // Even if everything was available, this account wouldn't have
-               // enough to satisfy the request.
-               return nil, 0, isImmature, ErrInsufficient
-       }
-       if reserved < amount {
-               // The account has enough for the request, but some is tied up in
-               // other reservations.
-               return nil, 0, isImmature, ErrReserved
-       }
-
-       // We've found enough to satisfy the request.
-       for _, u := range reservedUTXOs {
-               sr.reserved[u.OutputID] = rid
-       }
-
-       return reservedUTXOs, reserved, isImmature, nil
-}
-
-func (sr *sourceReserver) reserveUTXO(rid uint64, utxo *UTXO) error {
-       sr.mu.Lock()
-       defer sr.mu.Unlock()
-
-       _, isReserved := sr.reserved[utxo.OutputID]
-       if isReserved {
-               return ErrReserved
-       }
-
-       sr.reserved[utxo.OutputID] = rid
-       return nil
-}
-
-func (sr *sourceReserver) cancel(res *reservation) {
-       sr.mu.Lock()
-       defer sr.mu.Unlock()
-       for _, utxo := range res.UTXOs {
-               delete(sr.reserved, utxo.OutputID)
-       }
-}
-
-func findMatchingUTXOs(db dbm.DB, src source, currentHeight func() uint64) ([]*UTXO, bool, error) {
-       utxos := []*UTXO{}
-       isImmature := false
-       utxoIter := db.IteratorPrefix([]byte(UTXOPreFix))
-       defer utxoIter.Release()
-
-       for utxoIter.Next() {
-               u := &UTXO{}
-               if err := json.Unmarshal(utxoIter.Value(), u); err != nil {
-                       return nil, false, errors.Wrap(err)
-               }
-
-               //u.ValidHeight > 0 means coinbase utxo
-               if u.ValidHeight > 0 && u.ValidHeight > currentHeight() {
-                       isImmature = true
-                       continue
-               }
-
-               if u.AccountID == src.AccountID && u.AssetID == src.AssetID {
-                       utxos = append(utxos, u)
-               }
-       }
-
-       if len(utxos) == 0 {
-               return nil, isImmature, ErrMatchUTXO
-       }
-       return utxos, isImmature, nil
-}
-
-func findSpecificUTXO(db dbm.DB, outHash bc.Hash) (*UTXO, error) {
-       u := &UTXO{}
-
-       data := db.Get(StandardUTXOKey(outHash))
-       if data == nil {
-               if data = db.Get(ContractUTXOKey(outHash)); data == nil {
-                       return nil, errors.Wrapf(ErrMatchUTXO, "output_id = %s", outHash.String())
-               }
-       }
-       return u, json.Unmarshal(data, u)
-}
diff --git a/account/reserve_test.go b/account/reserve_test.go
deleted file mode 100644 (file)
index 5899002..0000000
+++ /dev/null
@@ -1,140 +0,0 @@
-package account
-
-import (
-       "context"
-       "encoding/json"
-       "io/ioutil"
-       "os"
-       "testing"
-       "time"
-
-       dbm "github.com/tendermint/tmlibs/db"
-
-       "github.com/bytom/blockchain/pseudohsm"
-       "github.com/bytom/consensus"
-       "github.com/bytom/crypto/ed25519/chainkd"
-       "github.com/bytom/database/leveldb"
-       "github.com/bytom/database/storage"
-       "github.com/bytom/protocol"
-       "github.com/bytom/protocol/bc"
-       "github.com/bytom/protocol/state"
-)
-
-func TestCancelReservation(t *testing.T) {
-       dirPath, err := ioutil.TempDir(".", "")
-       if err != nil {
-               t.Fatal(err)
-       }
-       defer os.RemoveAll(dirPath)
-
-       testDB := dbm.NewDB("testdb", "leveldb", "temp")
-       defer os.RemoveAll("temp")
-
-       chain, err := mockChain(testDB)
-       if err != nil {
-               t.Fatal(err)
-       }
-
-       accountManager := NewManager(testDB, chain)
-       hsm, err := pseudohsm.New(dirPath)
-       if err != nil {
-               t.Fatal(err)
-       }
-
-       xpub1, err := hsm.XCreate("test_pub", "password")
-       if err != nil {
-               t.Fatal(err)
-       }
-
-       testAccount, err := accountManager.Create(nil, []chainkd.XPub{xpub1.XPub}, 1, "testAccount")
-       if err != nil {
-               t.Fatal(err)
-       }
-
-       controlProg, err := accountManager.CreateAddress(nil, testAccount.ID, false)
-       if err != nil {
-               t.Fatal(err)
-       }
-
-       utxo := mockUTXO(controlProg)
-
-       batch := testDB.NewBatch()
-
-       utxoE := struct {
-               hash      bc.Hash
-               utxoEntry *storage.UtxoEntry
-               exist     bool
-       }{
-               hash:      utxo.OutputID,
-               utxoEntry: storage.NewUtxoEntry(true, 0, false),
-               exist:     true,
-       }
-
-       view := state.NewUtxoViewpoint()
-       view.Entries[utxoE.hash] = utxoE.utxoEntry
-
-       leveldb.SaveUtxoView(batch, view)
-       batch.Write()
-
-       utxoDB := newReserver(chain, testDB)
-
-       batch = utxoDB.db.NewBatch()
-
-       data, err := json.Marshal(utxo)
-       if err != nil {
-               t.Fatal(err)
-       }
-
-       batch.Set(StandardUTXOKey(utxo.OutputID), data)
-       batch.Write()
-
-       outid := utxo.OutputID
-
-       ctx := context.Background()
-       res, err := utxoDB.ReserveUTXO(ctx, outid, time.Now())
-       if err != nil {
-               t.Fatal(err)
-       }
-
-       // Verify that the UTXO is reserved.
-       _, err = utxoDB.ReserveUTXO(ctx, outid, time.Now())
-       if err != ErrReserved {
-               t.Fatalf("got=%s want=%s", err, ErrReserved)
-       }
-
-       // Cancel the reservation.
-       err = utxoDB.Cancel(ctx, res.ID)
-       if err != nil {
-               t.Fatal(err)
-       }
-
-       // Reserving again should succeed.
-       _, err = utxoDB.ReserveUTXO(ctx, outid, time.Now())
-       if err != nil {
-               t.Fatal(err)
-       }
-}
-
-func mockChain(testDB dbm.DB) (*protocol.Chain, error) {
-       store := leveldb.NewStore(testDB)
-       txPool := protocol.NewTxPool()
-       chain, err := protocol.NewChain(store, txPool)
-       if err != nil {
-               return nil, err
-       }
-       return chain, nil
-}
-
-func mockUTXO(controlProg *CtrlProgram) *UTXO {
-       utxo := &UTXO{}
-       utxo.OutputID = bc.Hash{V0: 1}
-       utxo.SourceID = bc.Hash{V0: 2}
-       utxo.AssetID = *consensus.BTMAssetID
-       utxo.Amount = 1000000000
-       utxo.SourcePos = 0
-       utxo.ControlProgram = controlProg.ControlProgram
-       utxo.AccountID = controlProg.AccountID
-       utxo.Address = controlProg.Address
-       utxo.ControlProgramIndex = controlProg.KeyIndex
-       return utxo
-}
diff --git a/account/utxo_keeper.go b/account/utxo_keeper.go
new file mode 100644 (file)
index 0000000..0fc96bb
--- /dev/null
@@ -0,0 +1,257 @@
+package account
+
+import (
+       "encoding/json"
+       "sort"
+       "sync"
+       "sync/atomic"
+       "time"
+
+       log "github.com/sirupsen/logrus"
+       dbm "github.com/tendermint/tmlibs/db"
+
+       "github.com/bytom/errors"
+       "github.com/bytom/protocol/bc"
+)
+
+// pre-define error types
+var (
+       ErrInsufficient = errors.New("reservation found insufficient funds")
+       ErrImmature     = errors.New("reservation found immature funds")
+       ErrReserved     = errors.New("reservation found outputs already reserved")
+       ErrMatchUTXO    = errors.New("can't find utxo with given hash")
+       ErrReservation  = errors.New("couldn't find reservation")
+)
+
+// UTXO describes an individual account utxo.
+type UTXO struct {
+       OutputID            bc.Hash
+       SourceID            bc.Hash
+       AssetID             bc.AssetID
+       Amount              uint64
+       SourcePos           uint64
+       ControlProgram      []byte
+       AccountID           string
+       Address             string
+       ControlProgramIndex uint64
+       ValidHeight         uint64
+       Change              bool
+}
+
+// reservation describes a reservation of a set of UTXOs
+type reservation struct {
+       id     uint64
+       utxos  []*UTXO
+       change uint64
+       expiry time.Time
+}
+
+type utxoKeeper struct {
+       // `sync/atomic` expects the first word in an allocated struct to be 64-bit
+       // aligned on both ARM and x86-32. See https://goo.gl/zW7dgq for more details.
+       nextIndex     uint64
+       db            dbm.DB
+       mtx           sync.RWMutex
+       currentHeight func() uint64
+
+       unconfirmed  map[bc.Hash]*UTXO
+       reserved     map[bc.Hash]uint64
+       reservations map[uint64]*reservation
+}
+
+func newUtxoKeeper(f func() uint64, walletdb dbm.DB) *utxoKeeper {
+       uk := &utxoKeeper{
+               db:            walletdb,
+               currentHeight: f,
+               unconfirmed:   make(map[bc.Hash]*UTXO),
+               reserved:      make(map[bc.Hash]uint64),
+               reservations:  make(map[uint64]*reservation),
+       }
+       go uk.expireWorker()
+       return uk
+}
+
+func (uk *utxoKeeper) AddUnconfirmedUtxo(utxos []*UTXO) {
+       uk.mtx.Lock()
+       defer uk.mtx.Unlock()
+
+       for _, utxo := range utxos {
+               uk.unconfirmed[utxo.OutputID] = utxo
+       }
+}
+
+// Cancel canceling the reservation with the provided ID.
+func (uk *utxoKeeper) Cancel(rid uint64) {
+       uk.mtx.Lock()
+       uk.cancel(rid)
+       uk.mtx.Unlock()
+}
+
+func (uk *utxoKeeper) RemoveUnconfirmedUtxo(hashes []*bc.Hash) {
+       uk.mtx.Lock()
+       defer uk.mtx.Unlock()
+
+       for _, hash := range hashes {
+               delete(uk.unconfirmed, *hash)
+       }
+}
+
+func (uk *utxoKeeper) Reserve(accountID string, assetID *bc.AssetID, amount uint64, useUnconfirmed bool, exp time.Time) (*reservation, error) {
+       uk.mtx.Lock()
+       defer uk.mtx.Unlock()
+
+       utxos, immatureAmount := uk.findUtxos(accountID, assetID, useUnconfirmed)
+       optUtxos, optAmount, reservedAmount := uk.optUTXOs(utxos, amount)
+       if optAmount+reservedAmount+immatureAmount < amount {
+               return nil, ErrInsufficient
+       }
+
+       if optAmount+reservedAmount < amount {
+               return nil, ErrImmature
+       }
+
+       if optAmount < amount {
+               return nil, ErrReserved
+       }
+
+       result := &reservation{
+               id:     atomic.AddUint64(&uk.nextIndex, 1),
+               utxos:  optUtxos,
+               change: optAmount - amount,
+               expiry: exp,
+       }
+
+       uk.reservations[result.id] = result
+       for _, u := range optUtxos {
+               uk.reserved[u.OutputID] = result.id
+       }
+       return result, nil
+}
+
+func (uk *utxoKeeper) ReserveParticular(outHash bc.Hash, useUnconfirmed bool, exp time.Time) (*reservation, error) {
+       uk.mtx.Lock()
+       defer uk.mtx.Unlock()
+
+       if _, ok := uk.reserved[outHash]; ok {
+               return nil, ErrReserved
+       }
+
+       u, err := uk.findUtxo(outHash, useUnconfirmed)
+       if err != nil {
+               return nil, err
+       }
+
+       if u.ValidHeight > uk.currentHeight() {
+               return nil, ErrImmature
+       }
+
+       result := &reservation{
+               id:     atomic.AddUint64(&uk.nextIndex, 1),
+               utxos:  []*UTXO{u},
+               expiry: exp,
+       }
+       uk.reservations[result.id] = result
+       uk.reserved[u.OutputID] = result.id
+       return result, nil
+}
+
+func (uk *utxoKeeper) cancel(rid uint64) {
+       res, ok := uk.reservations[rid]
+       if !ok {
+               return
+       }
+
+       delete(uk.reservations, rid)
+       for _, utxo := range res.utxos {
+               delete(uk.reserved, utxo.OutputID)
+       }
+}
+
+func (uk *utxoKeeper) expireWorker() {
+       ticker := time.NewTicker(1000 * time.Millisecond)
+       for now := range ticker.C {
+               uk.expireReservation(now)
+       }
+}
+func (uk *utxoKeeper) expireReservation(t time.Time) {
+       uk.mtx.Lock()
+       defer uk.mtx.Unlock()
+
+       for rid, res := range uk.reservations {
+               if res.expiry.Before(t) {
+                       uk.cancel(rid)
+               }
+       }
+}
+
+func (uk *utxoKeeper) findUtxos(accountID string, assetID *bc.AssetID, useUnconfirmed bool) ([]*UTXO, uint64) {
+       immatureAmount := uint64(0)
+       currentHeight := uk.currentHeight()
+       utxos := []*UTXO{}
+       appendUtxo := func(u *UTXO) {
+               if u.AccountID != accountID || u.AssetID != *assetID {
+                       return
+               }
+               if u.ValidHeight > currentHeight {
+                       immatureAmount += u.Amount
+               } else {
+                       utxos = append(utxos, u)
+               }
+       }
+
+       utxoIter := uk.db.IteratorPrefix([]byte(UTXOPreFix))
+       defer utxoIter.Release()
+       for utxoIter.Next() {
+               u := &UTXO{}
+               if err := json.Unmarshal(utxoIter.Value(), u); err != nil {
+                       log.WithField("err", err).Error("utxoKeeper findUtxos fail on unmarshal utxo")
+                       continue
+               }
+               appendUtxo(u)
+       }
+       if !useUnconfirmed {
+               return utxos, immatureAmount
+       }
+
+       for _, u := range uk.unconfirmed {
+               appendUtxo(u)
+       }
+       return utxos, immatureAmount
+}
+
+func (uk *utxoKeeper) findUtxo(outHash bc.Hash, useUnconfirmed bool) (*UTXO, error) {
+       if u, ok := uk.unconfirmed[outHash]; useUnconfirmed && ok {
+               return u, nil
+       }
+
+       u := &UTXO{}
+       if data := uk.db.Get(StandardUTXOKey(outHash)); data != nil {
+               return u, json.Unmarshal(data, u)
+       }
+       if data := uk.db.Get(ContractUTXOKey(outHash)); data != nil {
+               return u, json.Unmarshal(data, u)
+       }
+       return nil, ErrMatchUTXO
+}
+
+func (uk *utxoKeeper) optUTXOs(utxos []*UTXO, amount uint64) ([]*UTXO, uint64, uint64) {
+       sort.Slice(utxos, func(i, j int) bool {
+               return utxos[i].Amount < utxos[j].Amount
+       })
+
+       var optAmount, reservedAmount uint64
+       optUtxos := []*UTXO{}
+       for _, u := range utxos {
+               if _, ok := uk.reserved[u.OutputID]; ok {
+                       reservedAmount += u.Amount
+                       continue
+               }
+
+               optAmount += u.Amount
+               optUtxos = append(optUtxos, u)
+               if optAmount >= amount {
+                       break
+               }
+       }
+       return optUtxos, optAmount, reservedAmount
+}
diff --git a/account/utxo_keeper_test.go b/account/utxo_keeper_test.go
new file mode 100644 (file)
index 0000000..dde1ee7
--- /dev/null
@@ -0,0 +1,1090 @@
+package account
+
+import (
+       "encoding/json"
+       "os"
+       "testing"
+       "time"
+
+       dbm "github.com/tendermint/tmlibs/db"
+
+       "github.com/bytom/protocol/bc"
+       "github.com/bytom/testutil"
+)
+
+func TestAddUnconfirmedUtxo(t *testing.T) {
+       cases := []struct {
+               before   utxoKeeper
+               after    utxoKeeper
+               addUtxos []*UTXO
+       }{
+               {
+                       before: utxoKeeper{
+                               unconfirmed: map[bc.Hash]*UTXO{},
+                       },
+                       after: utxoKeeper{
+                               unconfirmed: map[bc.Hash]*UTXO{},
+                       },
+                       addUtxos: []*UTXO{},
+               },
+               {
+                       before: utxoKeeper{
+                               unconfirmed: map[bc.Hash]*UTXO{},
+                       },
+                       after: utxoKeeper{
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+                               },
+                       },
+                       addUtxos: []*UTXO{
+                               &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+                       },
+               },
+               {
+                       before: utxoKeeper{
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+                               },
+                       },
+                       after: utxoKeeper{
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+                               },
+                       },
+                       addUtxos: []*UTXO{
+                               &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+                       },
+               },
+               {
+                       before: utxoKeeper{
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+                               },
+                       },
+                       after: utxoKeeper{
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+                                       bc.NewHash([32]byte{0x02}): &UTXO{OutputID: bc.NewHash([32]byte{0x02})},
+                                       bc.NewHash([32]byte{0x03}): &UTXO{OutputID: bc.NewHash([32]byte{0x03})},
+                               },
+                       },
+                       addUtxos: []*UTXO{
+                               &UTXO{OutputID: bc.NewHash([32]byte{0x02})},
+                               &UTXO{OutputID: bc.NewHash([32]byte{0x03})},
+                       },
+               },
+       }
+
+       for i, c := range cases {
+               c.before.AddUnconfirmedUtxo(c.addUtxos)
+               if !testutil.DeepEqual(c.before, c.after) {
+                       t.Errorf("case %d: got %v want %v", i, c.before, c.after)
+               }
+       }
+}
+
+func TestCancel(t *testing.T) {
+       cases := []struct {
+               before    utxoKeeper
+               after     utxoKeeper
+               cancelRid uint64
+       }{
+               {
+                       before: utxoKeeper{
+                               reserved:     map[bc.Hash]uint64{},
+                               reservations: map[uint64]*reservation{},
+                       },
+                       after: utxoKeeper{
+                               reserved:     map[bc.Hash]uint64{},
+                               reservations: map[uint64]*reservation{},
+                       },
+                       cancelRid: 0,
+               },
+               {
+                       before: utxoKeeper{
+                               reserved: map[bc.Hash]uint64{
+                                       bc.NewHash([32]byte{0x01}): 1,
+                               },
+                               reservations: map[uint64]*reservation{
+                                       1: &reservation{
+                                               id: 1,
+                                               utxos: []*UTXO{
+                                                       &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+                                               },
+                                               change: 9527,
+                                               expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+                                       },
+                               },
+                       },
+                       after: utxoKeeper{
+                               reserved:     map[bc.Hash]uint64{},
+                               reservations: map[uint64]*reservation{},
+                       },
+                       cancelRid: 1,
+               },
+               {
+                       before: utxoKeeper{
+                               reserved: map[bc.Hash]uint64{
+                                       bc.NewHash([32]byte{0x01}): 1,
+                               },
+                               reservations: map[uint64]*reservation{
+                                       1: &reservation{
+                                               id: 1,
+                                               utxos: []*UTXO{
+                                                       &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+                                               },
+                                               change: 9527,
+                                               expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+                                       },
+                               },
+                       },
+                       after: utxoKeeper{
+                               reserved: map[bc.Hash]uint64{
+                                       bc.NewHash([32]byte{0x01}): 1,
+                               },
+                               reservations: map[uint64]*reservation{
+                                       1: &reservation{
+                                               id: 1,
+                                               utxos: []*UTXO{
+                                                       &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+                                               },
+                                               change: 9527,
+                                               expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+                                       },
+                               },
+                       },
+                       cancelRid: 2,
+               },
+               {
+                       before: utxoKeeper{
+                               reserved: map[bc.Hash]uint64{
+                                       bc.NewHash([32]byte{0x01}): 1,
+                                       bc.NewHash([32]byte{0x02}): 3,
+                                       bc.NewHash([32]byte{0x03}): 3,
+                                       bc.NewHash([32]byte{0x04}): 3,
+                               },
+                               reservations: map[uint64]*reservation{
+                                       1: &reservation{
+                                               id: 1,
+                                               utxos: []*UTXO{
+                                                       &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+                                               },
+                                               change: 9527,
+                                               expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+                                       },
+                                       3: &reservation{
+                                               id: 3,
+                                               utxos: []*UTXO{
+                                                       &UTXO{OutputID: bc.NewHash([32]byte{0x02})},
+                                                       &UTXO{OutputID: bc.NewHash([32]byte{0x03})},
+                                                       &UTXO{OutputID: bc.NewHash([32]byte{0x04})},
+                                               },
+                                               change: 9528,
+                                               expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+                                       },
+                               },
+                       },
+                       after: utxoKeeper{
+                               reserved: map[bc.Hash]uint64{
+                                       bc.NewHash([32]byte{0x01}): 1,
+                               },
+                               reservations: map[uint64]*reservation{
+                                       1: &reservation{
+                                               id: 1,
+                                               utxos: []*UTXO{
+                                                       &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+                                               },
+                                               change: 9527,
+                                               expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+                                       },
+                               },
+                       },
+                       cancelRid: 3,
+               },
+       }
+
+       for i, c := range cases {
+               c.before.cancel(c.cancelRid)
+               if !testutil.DeepEqual(c.before, c.after) {
+                       t.Errorf("case %d: got %v want %v", i, c.before, c.after)
+               }
+       }
+}
+
+func TestRemoveUnconfirmedUtxo(t *testing.T) {
+       cases := []struct {
+               before      utxoKeeper
+               after       utxoKeeper
+               removeUtxos []*bc.Hash
+       }{
+               {
+                       before: utxoKeeper{
+                               unconfirmed: map[bc.Hash]*UTXO{},
+                       },
+                       after: utxoKeeper{
+                               unconfirmed: map[bc.Hash]*UTXO{},
+                       },
+                       removeUtxos: []*bc.Hash{},
+               },
+               {
+                       before: utxoKeeper{
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.Hash{V0: 1}: &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+                               },
+                       },
+                       after: utxoKeeper{
+                               unconfirmed: map[bc.Hash]*UTXO{},
+                       },
+                       removeUtxos: []*bc.Hash{
+                               &bc.Hash{V0: 1},
+                       },
+               },
+               {
+                       before: utxoKeeper{
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.Hash{V0: 1}: &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+                                       bc.Hash{V0: 2}: &UTXO{OutputID: bc.NewHash([32]byte{0x02})},
+                                       bc.Hash{V0: 3}: &UTXO{OutputID: bc.NewHash([32]byte{0x03})},
+                                       bc.Hash{V0: 4}: &UTXO{OutputID: bc.NewHash([32]byte{0x04})},
+                                       bc.Hash{V0: 5}: &UTXO{OutputID: bc.NewHash([32]byte{0x05})},
+                               },
+                       },
+                       after: utxoKeeper{
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.Hash{V0: 2}: &UTXO{OutputID: bc.NewHash([32]byte{0x02})},
+                                       bc.Hash{V0: 4}: &UTXO{OutputID: bc.NewHash([32]byte{0x04})},
+                               },
+                       },
+                       removeUtxos: []*bc.Hash{
+                               &bc.Hash{V0: 1},
+                               &bc.Hash{V0: 3},
+                               &bc.Hash{V0: 5},
+                       },
+               },
+       }
+
+       for i, c := range cases {
+               c.before.RemoveUnconfirmedUtxo(c.removeUtxos)
+               if !testutil.DeepEqual(c.before, c.after) {
+                       t.Errorf("case %d: got %v want %v", i, c.before, c.after)
+               }
+       }
+}
+
+func TestReserve(t *testing.T) {
+       currentHeight := func() uint64 { return 9527 }
+       testDB := dbm.NewDB("testdb", "leveldb", "temp")
+       defer os.RemoveAll("temp")
+
+       cases := []struct {
+               before        utxoKeeper
+               after         utxoKeeper
+               err           error
+               reserveAmount uint64
+               exp           time.Time
+       }{
+               {
+                       before: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               reserved:      map[bc.Hash]uint64{},
+                               reservations:  map[uint64]*reservation{},
+                       },
+                       after: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               reserved:      map[bc.Hash]uint64{},
+                               reservations:  map[uint64]*reservation{},
+                       },
+                       reserveAmount: 1,
+                       err:           ErrInsufficient,
+               },
+               {
+                       before: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{
+                                               OutputID:  bc.NewHash([32]byte{0x01}),
+                                               AccountID: "testAccount",
+                                               Amount:    3,
+                                       },
+                               },
+                               reserved:     map[bc.Hash]uint64{},
+                               reservations: map[uint64]*reservation{},
+                       },
+                       after: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{
+                                               OutputID:  bc.NewHash([32]byte{0x01}),
+                                               AccountID: "testAccount",
+                                               Amount:    3,
+                                       },
+                               },
+                               reserved:     map[bc.Hash]uint64{},
+                               reservations: map[uint64]*reservation{},
+                       },
+                       reserveAmount: 4,
+                       err:           ErrInsufficient,
+               },
+               {
+                       before: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{
+                                               OutputID:    bc.NewHash([32]byte{0x01}),
+                                               AccountID:   "testAccount",
+                                               Amount:      3,
+                                               ValidHeight: 9528,
+                                       },
+                               },
+                               reserved:     map[bc.Hash]uint64{},
+                               reservations: map[uint64]*reservation{},
+                       },
+                       after: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{
+                                               OutputID:    bc.NewHash([32]byte{0x01}),
+                                               AccountID:   "testAccount",
+                                               Amount:      3,
+                                               ValidHeight: 9528,
+                                       },
+                               },
+                               reserved:     map[bc.Hash]uint64{},
+                               reservations: map[uint64]*reservation{},
+                       },
+                       reserveAmount: 3,
+                       err:           ErrImmature,
+               },
+               {
+                       before: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{
+                                               OutputID:  bc.NewHash([32]byte{0x01}),
+                                               AccountID: "testAccount",
+                                               Amount:    3,
+                                       },
+                               },
+                               reserved: map[bc.Hash]uint64{
+                                       bc.NewHash([32]byte{0x01}): 0,
+                               },
+                               reservations: map[uint64]*reservation{},
+                       },
+                       after: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{
+                                               OutputID:  bc.NewHash([32]byte{0x01}),
+                                               AccountID: "testAccount",
+                                               Amount:    3,
+                                       },
+                               },
+                               reserved: map[bc.Hash]uint64{
+                                       bc.NewHash([32]byte{0x01}): 0,
+                               },
+                               reservations: map[uint64]*reservation{},
+                       },
+                       reserveAmount: 3,
+                       err:           ErrReserved,
+               },
+               {
+                       before: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{
+                                               OutputID:  bc.NewHash([32]byte{0x01}),
+                                               AccountID: "testAccount",
+                                               Amount:    3,
+                                       },
+                               },
+                               reserved:     map[bc.Hash]uint64{},
+                               reservations: map[uint64]*reservation{},
+                       },
+                       after: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{
+                                               OutputID:  bc.NewHash([32]byte{0x01}),
+                                               AccountID: "testAccount",
+                                               Amount:    3,
+                                       },
+                               },
+                               reserved: map[bc.Hash]uint64{
+                                       bc.NewHash([32]byte{0x01}): 1,
+                               },
+                               reservations: map[uint64]*reservation{
+                                       1: &reservation{
+                                               id: 1,
+                                               utxos: []*UTXO{
+                                                       &UTXO{
+                                                               OutputID:  bc.NewHash([32]byte{0x01}),
+                                                               AccountID: "testAccount",
+                                                               Amount:    3,
+                                                       },
+                                               },
+                                               change: 1,
+                                               expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+                                       },
+                               },
+                       },
+                       reserveAmount: 2,
+                       err:           nil,
+                       exp:           time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+               },
+               {
+                       before: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               nextIndex:     1,
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{
+                                               OutputID:  bc.NewHash([32]byte{0x01}),
+                                               AccountID: "testAccount",
+                                               Amount:    3,
+                                       },
+                                       bc.NewHash([32]byte{0x02}): &UTXO{
+                                               OutputID:  bc.NewHash([32]byte{0x02}),
+                                               AccountID: "testAccount",
+                                               Amount:    5,
+                                       },
+                                       bc.NewHash([32]byte{0x03}): &UTXO{
+                                               OutputID:  bc.NewHash([32]byte{0x03}),
+                                               AccountID: "testAccount",
+                                               Amount:    7,
+                                       },
+                               },
+                               reserved: map[bc.Hash]uint64{
+                                       bc.NewHash([32]byte{0x01}): 1,
+                               },
+                               reservations: map[uint64]*reservation{},
+                       },
+                       after: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{
+                                               OutputID:  bc.NewHash([32]byte{0x01}),
+                                               AccountID: "testAccount",
+                                               Amount:    3,
+                                       },
+                                       bc.NewHash([32]byte{0x02}): &UTXO{
+                                               OutputID:  bc.NewHash([32]byte{0x02}),
+                                               AccountID: "testAccount",
+                                               Amount:    5,
+                                       },
+                                       bc.NewHash([32]byte{0x03}): &UTXO{
+                                               OutputID:  bc.NewHash([32]byte{0x03}),
+                                               AccountID: "testAccount",
+                                               Amount:    7,
+                                       },
+                               },
+                               reserved: map[bc.Hash]uint64{
+                                       bc.NewHash([32]byte{0x01}): 1,
+                                       bc.NewHash([32]byte{0x02}): 2,
+                                       bc.NewHash([32]byte{0x03}): 2,
+                               },
+                               reservations: map[uint64]*reservation{
+                                       2: &reservation{
+                                               id: 2,
+                                               utxos: []*UTXO{
+                                                       &UTXO{
+                                                               OutputID:  bc.NewHash([32]byte{0x02}),
+                                                               AccountID: "testAccount",
+                                                               Amount:    5,
+                                                       },
+                                                       &UTXO{
+                                                               OutputID:  bc.NewHash([32]byte{0x03}),
+                                                               AccountID: "testAccount",
+                                                               Amount:    7,
+                                                       },
+                                               },
+                                               change: 4,
+                                               expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+                                       },
+                               },
+                       },
+                       reserveAmount: 8,
+                       err:           nil,
+                       exp:           time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+               },
+       }
+
+       for i, c := range cases {
+               if _, err := c.before.Reserve("testAccount", &bc.AssetID{}, c.reserveAmount, true, c.exp); err != c.err {
+                       t.Errorf("case %d: got error %v want error %v", i, err, c.err)
+               }
+               checkUtxoKeeperEqual(t, i, &c.before, &c.after)
+       }
+}
+
+func TestReserveParticular(t *testing.T) {
+       currentHeight := func() uint64 { return 9527 }
+       testDB := dbm.NewDB("testdb", "leveldb", "temp")
+       defer os.RemoveAll("temp")
+
+       cases := []struct {
+               before      utxoKeeper
+               after       utxoKeeper
+               err         error
+               reserveHash bc.Hash
+               exp         time.Time
+       }{
+               {
+                       before: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{
+                                               OutputID:  bc.NewHash([32]byte{0x01}),
+                                               AccountID: "testAccount",
+                                               Amount:    3,
+                                       },
+                               },
+                               reserved: map[bc.Hash]uint64{
+                                       bc.NewHash([32]byte{0x01}): 0,
+                               },
+                               reservations: map[uint64]*reservation{},
+                       },
+                       after: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{
+                                               OutputID:  bc.NewHash([32]byte{0x01}),
+                                               AccountID: "testAccount",
+                                               Amount:    3,
+                                       },
+                               },
+                               reserved: map[bc.Hash]uint64{
+                                       bc.NewHash([32]byte{0x01}): 0,
+                               },
+                               reservations: map[uint64]*reservation{},
+                       },
+                       reserveHash: bc.NewHash([32]byte{0x01}),
+                       err:         ErrReserved,
+               },
+               {
+                       before: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{
+                                               OutputID:    bc.NewHash([32]byte{0x01}),
+                                               AccountID:   "testAccount",
+                                               Amount:      3,
+                                               ValidHeight: 9528,
+                                       },
+                               },
+                               reserved:     map[bc.Hash]uint64{},
+                               reservations: map[uint64]*reservation{},
+                       },
+                       after: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{
+                                               OutputID:    bc.NewHash([32]byte{0x01}),
+                                               AccountID:   "testAccount",
+                                               Amount:      3,
+                                               ValidHeight: 9528,
+                                       },
+                               },
+                               reserved:     map[bc.Hash]uint64{},
+                               reservations: map[uint64]*reservation{},
+                       },
+                       reserveHash: bc.NewHash([32]byte{0x01}),
+                       err:         ErrImmature,
+               },
+               {
+                       before: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{
+                                               OutputID:  bc.NewHash([32]byte{0x01}),
+                                               AccountID: "testAccount",
+                                               Amount:    3,
+                                       },
+                               },
+                               reserved:     map[bc.Hash]uint64{},
+                               reservations: map[uint64]*reservation{},
+                       },
+                       after: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{
+                                               OutputID:  bc.NewHash([32]byte{0x01}),
+                                               AccountID: "testAccount",
+                                               Amount:    3,
+                                       },
+                               },
+                               reserved: map[bc.Hash]uint64{
+                                       bc.NewHash([32]byte{0x01}): 1,
+                               },
+                               reservations: map[uint64]*reservation{
+                                       1: &reservation{
+                                               id: 1,
+                                               utxos: []*UTXO{
+                                                       &UTXO{
+                                                               OutputID:  bc.NewHash([32]byte{0x01}),
+                                                               AccountID: "testAccount",
+                                                               Amount:    3,
+                                                       },
+                                               },
+                                               change: 0,
+                                               expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+                                       },
+                               },
+                       },
+                       reserveHash: bc.NewHash([32]byte{0x01}),
+                       err:         nil,
+                       exp:         time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC),
+               },
+       }
+
+       for i, c := range cases {
+               if _, err := c.before.ReserveParticular(c.reserveHash, true, c.exp); err != c.err {
+                       t.Errorf("case %d: got error %v want error %v", i, err, c.err)
+               }
+               checkUtxoKeeperEqual(t, i, &c.before, &c.after)
+       }
+}
+
+func TestExpireReservation(t *testing.T) {
+       before := &utxoKeeper{
+               reservations: map[uint64]*reservation{
+                       1: &reservation{expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC)},
+                       2: &reservation{expiry: time.Date(3016, 8, 10, 0, 0, 0, 0, time.UTC)},
+                       3: &reservation{expiry: time.Date(2016, 8, 10, 0, 0, 0, 0, time.UTC)},
+                       4: &reservation{expiry: time.Date(3016, 8, 10, 0, 0, 0, 0, time.UTC)},
+                       5: &reservation{expiry: time.Date(3016, 8, 10, 0, 0, 0, 0, time.UTC)},
+               },
+       }
+       after := &utxoKeeper{
+               reservations: map[uint64]*reservation{
+                       2: &reservation{expiry: time.Date(3016, 8, 10, 0, 0, 0, 0, time.UTC)},
+                       4: &reservation{expiry: time.Date(3016, 8, 10, 0, 0, 0, 0, time.UTC)},
+                       5: &reservation{expiry: time.Date(3016, 8, 10, 0, 0, 0, 0, time.UTC)},
+               },
+       }
+       before.expireReservation(time.Date(2017, 8, 10, 0, 0, 0, 0, time.UTC))
+       checkUtxoKeeperEqual(t, 0, before, after)
+}
+
+func TestFindUtxos(t *testing.T) {
+       currentHeight := func() uint64 { return 9527 }
+       testDB := dbm.NewDB("testdb", "leveldb", "temp")
+       defer os.RemoveAll("temp")
+
+       cases := []struct {
+               uk             utxoKeeper
+               dbUtxos        []*UTXO
+               useUnconfirmed bool
+               wantUtxos      []*UTXO
+               immatureAmount uint64
+       }{
+               {
+                       uk: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed:   map[bc.Hash]*UTXO{},
+                       },
+                       dbUtxos:        []*UTXO{},
+                       useUnconfirmed: true,
+                       wantUtxos:      []*UTXO{},
+                       immatureAmount: 0,
+               },
+               {
+                       uk: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed:   map[bc.Hash]*UTXO{},
+                       },
+                       dbUtxos: []*UTXO{
+                               &UTXO{
+                                       OutputID:  bc.NewHash([32]byte{0x01}),
+                                       AccountID: "testAccount",
+                                       Amount:    3,
+                               },
+                               &UTXO{
+                                       OutputID:  bc.NewHash([32]byte{0x02}),
+                                       AccountID: "testAccount",
+                                       AssetID:   bc.AssetID{V0: 6},
+                                       Amount:    3,
+                               },
+                       },
+                       useUnconfirmed: false,
+                       wantUtxos: []*UTXO{
+                               &UTXO{
+                                       OutputID:  bc.NewHash([32]byte{0x01}),
+                                       AccountID: "testAccount",
+                                       Amount:    3,
+                               },
+                       },
+                       immatureAmount: 0,
+               },
+               {
+                       uk: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed:   map[bc.Hash]*UTXO{},
+                       },
+                       dbUtxos: []*UTXO{
+                               &UTXO{
+                                       OutputID:    bc.NewHash([32]byte{0x02}),
+                                       AccountID:   "testAccount",
+                                       Amount:      3,
+                                       ValidHeight: 9528,
+                               },
+                       },
+                       useUnconfirmed: false,
+                       wantUtxos:      []*UTXO{},
+                       immatureAmount: 3,
+               },
+               {
+                       uk: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{
+                                               OutputID:  bc.NewHash([32]byte{0x01}),
+                                               AccountID: "testAccount",
+                                               Amount:    3,
+                                       },
+                               },
+                       },
+                       dbUtxos: []*UTXO{
+                               &UTXO{
+                                       OutputID:  bc.NewHash([32]byte{0x02}),
+                                       AccountID: "testAccount",
+                                       Amount:    3,
+                               },
+                       },
+                       useUnconfirmed: false,
+                       wantUtxos: []*UTXO{
+                               &UTXO{
+                                       OutputID:  bc.NewHash([32]byte{0x02}),
+                                       AccountID: "testAccount",
+                                       Amount:    3,
+                               },
+                       },
+                       immatureAmount: 0,
+               },
+               {
+                       uk: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x11}): &UTXO{
+                                               OutputID:  bc.NewHash([32]byte{0x01}),
+                                               AccountID: "testAccount",
+                                               Amount:    3,
+                                       },
+                               },
+                       },
+                       dbUtxos: []*UTXO{
+                               &UTXO{
+                                       OutputID:  bc.NewHash([32]byte{0x02}),
+                                       AccountID: "testAccount",
+                                       Amount:    3,
+                               },
+                       },
+                       useUnconfirmed: true,
+                       wantUtxos: []*UTXO{
+                               &UTXO{
+                                       OutputID:  bc.NewHash([32]byte{0x02}),
+                                       AccountID: "testAccount",
+                                       Amount:    3,
+                               },
+                               &UTXO{
+                                       OutputID:  bc.NewHash([32]byte{0x01}),
+                                       AccountID: "testAccount",
+                                       Amount:    3,
+                               },
+                       },
+                       immatureAmount: 0,
+               },
+               {
+                       uk: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{
+                                               OutputID:  bc.NewHash([32]byte{0x01}),
+                                               AccountID: "testAccount",
+                                               Amount:    1,
+                                       },
+                                       bc.NewHash([32]byte{0x02}): &UTXO{
+                                               OutputID:  bc.NewHash([32]byte{0x02}),
+                                               AccountID: "notMe",
+                                               Amount:    2,
+                                       },
+                               },
+                       },
+                       dbUtxos: []*UTXO{
+                               &UTXO{
+                                       OutputID:  bc.NewHash([32]byte{0x03}),
+                                       AccountID: "testAccount",
+                                       Amount:    3,
+                               },
+                               &UTXO{
+                                       OutputID:  bc.NewHash([32]byte{0x04}),
+                                       AccountID: "notMe",
+                                       Amount:    4,
+                               },
+                       },
+                       useUnconfirmed: true,
+                       wantUtxos: []*UTXO{
+                               &UTXO{
+                                       OutputID:  bc.NewHash([32]byte{0x03}),
+                                       AccountID: "testAccount",
+                                       Amount:    3,
+                               },
+                               &UTXO{
+                                       OutputID:  bc.NewHash([32]byte{0x01}),
+                                       AccountID: "testAccount",
+                                       Amount:    1,
+                               },
+                       },
+                       immatureAmount: 0,
+               },
+       }
+
+       for i, c := range cases {
+               for _, u := range c.dbUtxos {
+                       data, err := json.Marshal(u)
+                       if err != nil {
+                               t.Error(err)
+                       }
+                       testDB.Set(StandardUTXOKey(u.OutputID), data)
+               }
+
+               gotUtxos, immatureAmount := c.uk.findUtxos("testAccount", &bc.AssetID{}, c.useUnconfirmed)
+               if !testutil.DeepEqual(gotUtxos, c.wantUtxos) {
+                       t.Errorf("case %d: got %v want %v", i, gotUtxos, c.wantUtxos)
+               }
+               if immatureAmount != c.immatureAmount {
+                       t.Errorf("case %d: got %v want %v", i, immatureAmount, c.immatureAmount)
+               }
+
+               for _, u := range c.dbUtxos {
+                       testDB.Delete(StandardUTXOKey(u.OutputID))
+               }
+       }
+}
+
+func TestFindUtxo(t *testing.T) {
+       currentHeight := func() uint64 { return 9527 }
+       testDB := dbm.NewDB("testdb", "leveldb", "temp")
+       defer os.RemoveAll("temp")
+
+       cases := []struct {
+               uk             utxoKeeper
+               dbUtxos        map[string]*UTXO
+               outHash        bc.Hash
+               useUnconfirmed bool
+               wantUtxo       *UTXO
+               err            error
+       }{
+               {
+                       uk: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed:   map[bc.Hash]*UTXO{},
+                       },
+                       dbUtxos:  map[string]*UTXO{},
+                       outHash:  bc.NewHash([32]byte{0x01}),
+                       wantUtxo: nil,
+                       err:      ErrMatchUTXO,
+               },
+               {
+                       uk: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+                               },
+                       },
+                       dbUtxos:        map[string]*UTXO{},
+                       outHash:        bc.NewHash([32]byte{0x01}),
+                       wantUtxo:       nil,
+                       useUnconfirmed: false,
+                       err:            ErrMatchUTXO,
+               },
+               {
+                       uk: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed: map[bc.Hash]*UTXO{
+                                       bc.NewHash([32]byte{0x01}): &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+                               },
+                       },
+                       dbUtxos:        map[string]*UTXO{},
+                       outHash:        bc.NewHash([32]byte{0x01}),
+                       wantUtxo:       &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+                       useUnconfirmed: true,
+                       err:            nil,
+               },
+               {
+                       uk: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed:   map[bc.Hash]*UTXO{},
+                       },
+                       dbUtxos: map[string]*UTXO{
+                               string(StandardUTXOKey(bc.NewHash([32]byte{0x01}))): &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+                       },
+                       outHash:        bc.NewHash([32]byte{0x01}),
+                       wantUtxo:       &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+                       useUnconfirmed: false,
+                       err:            nil,
+               },
+               {
+                       uk: utxoKeeper{
+                               db:            testDB,
+                               currentHeight: currentHeight,
+                               unconfirmed:   map[bc.Hash]*UTXO{},
+                       },
+                       dbUtxos: map[string]*UTXO{
+                               string(ContractUTXOKey(bc.NewHash([32]byte{0x01}))): &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+                       },
+                       outHash:        bc.NewHash([32]byte{0x01}),
+                       wantUtxo:       &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
+                       useUnconfirmed: false,
+                       err:            nil,
+               },
+       }
+
+       for i, c := range cases {
+               for k, u := range c.dbUtxos {
+                       data, err := json.Marshal(u)
+                       if err != nil {
+                               t.Error(err)
+                       }
+                       testDB.Set([]byte(k), data)
+               }
+
+               gotUtxo, err := c.uk.findUtxo(c.outHash, c.useUnconfirmed)
+               if !testutil.DeepEqual(gotUtxo, c.wantUtxo) {
+                       t.Errorf("case %d: got %v want %v", i, gotUtxo, c.wantUtxo)
+               }
+               if err != c.err {
+                       t.Errorf("case %d: got %v want %v", i, err, c.err)
+               }
+
+               for _, u := range c.dbUtxos {
+                       testDB.Delete(StandardUTXOKey(u.OutputID))
+               }
+       }
+}
+
+func TestOptUTXOs(t *testing.T) {
+       cases := []struct {
+               uk             utxoKeeper
+               input          []*UTXO
+               inputAmount    uint64
+               wantUtxos      []*UTXO
+               optAmount      uint64
+               reservedAmount uint64
+       }{
+               {
+                       uk: utxoKeeper{
+                               reserved: map[bc.Hash]uint64{
+                                       bc.NewHash([32]byte{0x01}): 1,
+                               },
+                       },
+                       input:          []*UTXO{},
+                       inputAmount:    13,
+                       wantUtxos:      []*UTXO{},
+                       optAmount:      0,
+                       reservedAmount: 0,
+               },
+               {
+                       uk: utxoKeeper{
+                               reserved: map[bc.Hash]uint64{
+                                       bc.NewHash([32]byte{0x01}): 1,
+                               },
+                       },
+                       input: []*UTXO{
+                               &UTXO{
+                                       OutputID: bc.NewHash([32]byte{0x01}),
+                                       Amount:   1,
+                               },
+                       },
+                       inputAmount:    13,
+                       wantUtxos:      []*UTXO{},
+                       optAmount:      0,
+                       reservedAmount: 1,
+               },
+               {
+                       uk: utxoKeeper{
+                               reserved: map[bc.Hash]uint64{
+                                       bc.NewHash([32]byte{0x01}): 1,
+                               },
+                       },
+                       input: []*UTXO{
+                               &UTXO{
+                                       OutputID: bc.NewHash([32]byte{0x01}),
+                                       Amount:   1,
+                               },
+                               &UTXO{
+                                       OutputID: bc.NewHash([32]byte{0x02}),
+                                       Amount:   3,
+                               },
+                               &UTXO{
+                                       OutputID: bc.NewHash([32]byte{0x03}),
+                                       Amount:   5,
+                               },
+                       },
+                       inputAmount: 13,
+                       wantUtxos: []*UTXO{
+                               &UTXO{
+                                       OutputID: bc.NewHash([32]byte{0x02}),
+                                       Amount:   3,
+                               },
+                               &UTXO{
+                                       OutputID: bc.NewHash([32]byte{0x03}),
+                                       Amount:   5,
+                               },
+                       },
+                       optAmount:      8,
+                       reservedAmount: 1,
+               },
+       }
+
+       for i, c := range cases {
+               got, optAmount, reservedAmount := c.uk.optUTXOs(c.input, c.inputAmount)
+               if !testutil.DeepEqual(got, c.wantUtxos) {
+                       t.Errorf("case %d: utxos got %v want %v", i, got, c.wantUtxos)
+               }
+               if optAmount != c.optAmount {
+                       t.Errorf("case %d: utxos got %v want %v", i, optAmount, c.optAmount)
+               }
+               if reservedAmount != c.reservedAmount {
+                       t.Errorf("case %d: reservedAmount got %v want %v", i, reservedAmount, c.reservedAmount)
+               }
+       }
+}
+
+func checkUtxoKeeperEqual(t *testing.T, i int, a, b *utxoKeeper) {
+       if !testutil.DeepEqual(a.unconfirmed, b.unconfirmed) {
+               t.Errorf("case %d: unconfirmed got %v want %v", i, a.unconfirmed, b.unconfirmed)
+       }
+       if !testutil.DeepEqual(a.reserved, b.reserved) {
+               t.Errorf("case %d: reserved got %v want %v", i, a.reserved, b.reserved)
+       }
+       if !testutil.DeepEqual(a.reservations, b.reservations) {
+               t.Errorf("case %d: reservations got %v want %v", i, a.reservations, b.reservations)
+       }
+}
index a1bf521..a6560e6 100644 (file)
@@ -19,7 +19,7 @@ func (a *API) createAccount(ctx context.Context, ins struct {
        Quorum    int            `json:"quorum"`
        Alias     string         `json:"alias"`
 }) Response {
-       acc, err := a.wallet.AccountMgr.Create(ctx, ins.RootXPubs, ins.Quorum, ins.Alias)
+       acc, err := a.wallet.AccountMgr.Create(ins.RootXPubs, ins.Quorum, ins.Alias)
        if err != nil {
                return NewErrorResponse(err)
        }
@@ -102,13 +102,13 @@ func (a *API) listAddresses(ctx context.Context, ins struct {
        accountID := ins.AccountID
        var target *account.Account
        if ins.AccountAlias != "" {
-               acc, err := a.wallet.AccountMgr.FindByAlias(ctx, ins.AccountAlias)
+               acc, err := a.wallet.AccountMgr.FindByAlias(ins.AccountAlias)
                if err != nil {
                        return NewErrorResponse(err)
                }
                target = acc
        } else {
-               acc, err := a.wallet.AccountMgr.FindByID(ctx, accountID)
+               acc, err := a.wallet.AccountMgr.FindByID(accountID)
                if err != nil {
                        return NewErrorResponse(err)
                }
index bbb66f9..131cc8b 100644 (file)
@@ -238,7 +238,7 @@ func (a *API) listUnspentOutputs(ctx context.Context, filter struct {
        ID            string `json:"id"`
        SmartContract bool   `json:"smart_contract"`
 }) Response {
-       accountUTXOs := a.wallet.GetAccountUTXOs(filter.ID, filter.SmartContract)
+       accountUTXOs := a.wallet.GetAccountUtxos(filter.ID, filter.SmartContract)
 
        UTXOs := []query.AnnotatedUTXO{}
        for _, utxo := range accountUTXOs {
@@ -284,7 +284,7 @@ type AccountPubkey struct {
 func (a *API) listPubKeys(ctx context.Context, ins struct {
        AccountID string `json:"account_id"`
 }) Response {
-       account, err := a.wallet.AccountMgr.FindByID(ctx, ins.AccountID)
+       account, err := a.wallet.AccountMgr.FindByID(ins.AccountID)
        if err != nil {
                return NewErrorResponse(err)
        }
index a7dfcab..380eb78 100644 (file)
@@ -12,7 +12,7 @@ func (a *API) createAccountReceiver(ctx context.Context, ins struct {
 }) Response {
        accountID := ins.AccountID
        if ins.AccountAlias != "" {
-               account, err := a.wallet.AccountMgr.FindByAlias(ctx, ins.AccountAlias)
+               account, err := a.wallet.AccountMgr.FindByAlias(ins.AccountAlias)
                if err != nil {
                        return NewErrorResponse(err)
                }
@@ -20,7 +20,7 @@ func (a *API) createAccountReceiver(ctx context.Context, ins struct {
                accountID = account.ID
        }
 
-       program, err := a.wallet.AccountMgr.CreateAddress(ctx, accountID, false)
+       program, err := a.wallet.AccountMgr.CreateAddress(accountID, false)
        if err != nil {
                return NewErrorResponse(err)
        }
index de4c6b6..f40ef46 100644 (file)
@@ -60,7 +60,7 @@ func (a *API) completeMissingAccountId(m map[string]interface{}, index int, ctx
        id, _ := m["account_id"].(string)
        alias, _ := m["account_alias"].(string)
        if id == "" && alias != "" {
-               acc, err := a.wallet.AccountMgr.FindByAlias(ctx, alias)
+               acc, err := a.wallet.AccountMgr.FindByAlias(alias)
                if err != nil {
                        return errors.WithDetailf(err, "invalid account alias %s on action %d", alias, index)
                }
index befa286..1406c83 100644 (file)
@@ -29,7 +29,6 @@ func (a *API) actionDecoder(action string) (func([]byte) (txbuilder.Action, erro
        decoders := map[string]func([]byte) (txbuilder.Action, error){
                "control_address":              txbuilder.DecodeControlAddressAction,
                "control_program":              txbuilder.DecodeControlProgramAction,
-               "control_receiver":             txbuilder.DecodeControlReceiverAction,
                "issue":                        a.wallet.AssetReg.DecodeIssueAction,
                "retire":                       txbuilder.DecodeRetireAction,
                "spend_account":                a.wallet.AccountMgr.DecodeSpendAction,
index 8751a83..c758ba5 100644 (file)
@@ -16,41 +16,6 @@ import (
 
 var retirementProgram = []byte{byte(vm.OP_FAIL)}
 
-// DecodeControlReceiverAction convert input data to action struct
-func DecodeControlReceiverAction(data []byte) (Action, error) {
-       a := new(controlReceiverAction)
-       err := stdjson.Unmarshal(data, a)
-       return a, err
-}
-
-type controlReceiverAction struct {
-       bc.AssetAmount
-       Receiver *Receiver `json:"receiver"`
-}
-
-func (a *controlReceiverAction) Build(ctx context.Context, b *TemplateBuilder) error {
-       var missing []string
-       if a.Receiver == nil {
-               missing = append(missing, "receiver")
-       } else {
-               if len(a.Receiver.ControlProgram) == 0 {
-                       missing = append(missing, "receiver.control_program")
-               }
-       }
-       if a.AssetId.IsZero() {
-               missing = append(missing, "asset_id")
-       }
-       if a.Amount == 0 {
-               missing = append(missing, "amount")
-       }
-       if len(missing) > 0 {
-               return MissingFieldsError(missing...)
-       }
-
-       out := types.NewTxOutput(*a.AssetId, a.Amount, a.Receiver.ControlProgram)
-       return b.AddOutput(out)
-}
-
 // DecodeControlAddressAction convert input data to action struct
 func DecodeControlAddressAction(data []byte) (Action, error) {
        a := new(controlAddressAction)
index a2b9cb5..f68e3a9 100644 (file)
@@ -7,7 +7,6 @@ import (
        _ "net/http/pprof"
        "os"
        "path/filepath"
-       "time"
 
        "github.com/prometheus/prometheus/util/flock"
        log "github.com/sirupsen/logrus"
@@ -35,9 +34,8 @@ import (
 )
 
 const (
-       webAddress               = "http://127.0.0.1:9888"
-       expireReservationsPeriod = time.Second
-       maxNewBlockChSize        = 1024
+       webAddress        = "http://127.0.0.1:9888"
+       maxNewBlockChSize = 1024
 )
 
 type Node struct {
@@ -117,9 +115,6 @@ func NewNode(config *cfg.Config) *Node {
                if config.Wallet.Rescan {
                        wallet.RescanBlocks()
                }
-
-               // Clean up expired UTXO reservations periodically.
-               go accounts.ExpireReservations(ctx, expireReservationsPeriod)
        }
        newBlockCh := make(chan *bc.Hash, maxNewBlockChSize)
 
@@ -159,14 +154,23 @@ func NewNode(config *cfg.Config) *Node {
 
 // newPoolTxListener listener transaction from txPool, and send it to syncManager and wallet
 func newPoolTxListener(txPool *protocol.TxPool, syncManager *netsync.SyncManager, wallet *w.Wallet) {
-       newTxCh := txPool.GetNewTxCh()
+       txMsgCh := txPool.GetMsgCh()
        syncManagerTxCh := syncManager.GetNewTxCh()
 
        for {
-               newTx := <-newTxCh
-               syncManagerTxCh <- newTx
-               if wallet != nil {
-                       wallet.GetNewTxCh() <- newTx
+               msg := <-txMsgCh
+               switch msg.MsgType {
+               case protocol.MsgNewTx:
+                       syncManagerTxCh <- msg.Tx
+                       if wallet != nil {
+                               wallet.AddUnconfirmedTx(msg.TxDesc)
+                       }
+               case protocol.MsgRemoveTx:
+                       if wallet != nil {
+                               wallet.RemoveUnconfirmedTx(msg.TxDesc)
+                       }
+               default:
+                       log.Warn("got unknow message type from the txPool channel")
                }
        }
 }
index 4017db7..9a5ee8b 100644 (file)
@@ -16,9 +16,14 @@ import (
        log "github.com/sirupsen/logrus"
 )
 
+const (
+       MsgNewTx = iota
+       MsgRemoveTx
+)
+
 var (
        maxCachedErrTxs = 1000
-       maxNewTxChSize  = 1000
+       maxMsgChSize    = 1000
        maxNewTxNum     = 10000
 
        // ErrTransactionNotExist is the pre-defined error message
@@ -29,12 +34,18 @@ var (
 
 // TxDesc store tx and related info for mining strategy
 type TxDesc struct {
-       Tx       *types.Tx
-       Added    time.Time
-       Height   uint64
-       Weight   uint64
-       Fee      uint64
-       FeePerKB uint64
+       Tx         *types.Tx
+       Added      time.Time
+       StatusFail bool
+       Height     uint64
+       Weight     uint64
+       Fee        uint64
+       FeePerKB   uint64
+}
+
+type TxPoolMsg struct {
+       *TxDesc
+       MsgType int
 }
 
 // TxPool is use for store the unconfirmed transaction
@@ -44,7 +55,7 @@ type TxPool struct {
        pool        map[bc.Hash]*TxDesc
        utxo        map[bc.Hash]bc.Hash
        errCache    *lru.Cache
-       newTxCh     chan *types.Tx
+       msgCh       chan *TxPoolMsg
 }
 
 // NewTxPool init a new TxPool
@@ -54,17 +65,17 @@ func NewTxPool() *TxPool {
                pool:        make(map[bc.Hash]*TxDesc),
                utxo:        make(map[bc.Hash]bc.Hash),
                errCache:    lru.New(maxCachedErrTxs),
-               newTxCh:     make(chan *types.Tx, maxNewTxChSize),
+               msgCh:       make(chan *TxPoolMsg, maxMsgChSize),
        }
 }
 
 // GetNewTxCh return a unconfirmed transaction feed channel
-func (tp *TxPool) GetNewTxCh() chan *types.Tx {
-       return tp.newTxCh
+func (tp *TxPool) GetMsgCh() <-chan *TxPoolMsg {
+       return tp.msgCh
 }
 
 // AddTransaction add a verified transaction to pool
-func (tp *TxPool) AddTransaction(tx *types.Tx, gasOnlyTx bool, height, fee uint64) (*TxDesc, error) {
+func (tp *TxPool) AddTransaction(tx *types.Tx, statusFail bool, height, fee uint64) (*TxDesc, error) {
        tp.mtx.Lock()
        defer tp.mtx.Unlock()
 
@@ -73,12 +84,13 @@ func (tp *TxPool) AddTransaction(tx *types.Tx, gasOnlyTx bool, height, fee uint6
        }
 
        txD := &TxDesc{
-               Tx:       tx,
-               Added:    time.Now(),
-               Weight:   tx.TxData.SerializedSize,
-               Height:   height,
-               Fee:      fee,
-               FeePerKB: fee * 1000 / tx.TxHeader.SerializedSize,
+               Tx:         tx,
+               Added:      time.Now(),
+               StatusFail: statusFail,
+               Weight:     tx.TxData.SerializedSize,
+               Height:     height,
+               Fee:        fee,
+               FeePerKB:   fee * 1000 / tx.TxHeader.SerializedSize,
        }
 
        tp.pool[tx.Tx.ID] = txD
@@ -90,12 +102,12 @@ func (tp *TxPool) AddTransaction(tx *types.Tx, gasOnlyTx bool, height, fee uint6
                        // error due to it's a retirement, utxo doesn't care this output type so skip it
                        continue
                }
-               if !gasOnlyTx || *output.Source.Value.AssetId == *consensus.BTMAssetID {
+               if !statusFail || *output.Source.Value.AssetId == *consensus.BTMAssetID {
                        tp.utxo[*id] = tx.Tx.ID
                }
        }
 
-       tp.newTxCh <- tx
+       tp.msgCh <- &TxPoolMsg{TxDesc: txD, MsgType: MsgNewTx}
        log.WithField("tx_id", tx.Tx.ID.String()).Debug("Add tx to mempool")
        return txD, nil
 }
@@ -136,6 +148,7 @@ func (tp *TxPool) RemoveTransaction(txHash *bc.Hash) {
        delete(tp.pool, *txHash)
        atomic.StoreInt64(&tp.lastUpdated, time.Now().Unix())
 
+       tp.msgCh <- &TxPoolMsg{TxDesc: txD, MsgType: MsgRemoveTx}
        log.WithField("tx_id", txHash).Debug("remove tx from mempool")
 }
 
index 3946135..8121d5f 100644 (file)
@@ -191,7 +191,7 @@ func InsertChain(chain *protocol.Chain, txPool *protocol.TxPool, txs []*types.Tx
 }
 
 func processNewTxch(txPool *protocol.TxPool) {
-       newTxCh := txPool.GetNewTxCh()
+       newTxCh := txPool.GetMsgCh()
        for tx := range newTxCh {
                if tx == nil {
                }
@@ -407,12 +407,12 @@ func MockTxsP2PKH(keyDirPath string, testDB dbm.DB, txNumber, otherAssetNum int)
        txs := []*types.Tx{}
        for i := 0; i < txNumber; i++ {
                testAccountAlias := fmt.Sprintf("testAccount%d", i)
-               testAccount, err := accountManager.Create(nil, []chainkd.XPub{xpub.XPub}, 1, testAccountAlias)
+               testAccount, err := accountManager.Create([]chainkd.XPub{xpub.XPub}, 1, testAccountAlias)
                if err != nil {
                        return nil, err
                }
 
-               controlProg, err := accountManager.CreateAddress(nil, testAccount.ID, false)
+               controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
                if err != nil {
                        return nil, err
                }
@@ -454,12 +454,12 @@ func MockTxsP2SH(keyDirPath string, testDB dbm.DB, txNumber, otherAssetNum int)
        txs := []*types.Tx{}
        for i := 0; i < txNumber; i++ {
                testAccountAlias := fmt.Sprintf("testAccount%d", i)
-               testAccount, err := accountManager.Create(nil, []chainkd.XPub{xpub1.XPub, xpub2.XPub}, 2, testAccountAlias)
+               testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub, xpub2.XPub}, 2, testAccountAlias)
                if err != nil {
                        return nil, err
                }
 
-               controlProg, err := accountManager.CreateAddress(nil, testAccount.ID, false)
+               controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
                if err != nil {
                        return nil, err
                }
@@ -500,12 +500,12 @@ func MockTxsMultiSign(keyDirPath string, testDB dbm.DB, txNumber, otherAssetNum
        txs := []*types.Tx{}
        for i := 0; i < txNumber; i++ {
                testAccountAlias := fmt.Sprintf("testAccount%d", i)
-               testAccount, err := accountManager.Create(nil, []chainkd.XPub{xpub1.XPub, xpub2.XPub}, 2, testAccountAlias)
+               testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub, xpub2.XPub}, 2, testAccountAlias)
                if err != nil {
                        return nil, err
                }
 
-               controlProg, err := accountManager.CreateAddress(nil, testAccount.ID, false)
+               controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
                if err != nil {
                        return nil, err
                }
index 685979c..3ea937f 100644 (file)
@@ -41,12 +41,12 @@ func TestP2PKH(t *testing.T) {
                t.Fatal(err)
        }
 
-       testAccount, err := accountManager.Create(nil, []chainkd.XPub{xpub.XPub}, 1, "testAccount")
+       testAccount, err := accountManager.Create([]chainkd.XPub{xpub.XPub}, 1, "testAccount")
        if err != nil {
                t.Fatal(err)
        }
 
-       controlProg, err := accountManager.CreateAddress(nil, testAccount.ID, false)
+       controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
        if err != nil {
                t.Fatal(err)
        }
@@ -98,12 +98,12 @@ func TestP2SH(t *testing.T) {
                t.Fatal(err)
        }
 
-       testAccount, err := accountManager.Create(nil, []chainkd.XPub{xpub1.XPub, xpub2.XPub}, 2, "testAccount")
+       testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub, xpub2.XPub}, 2, "testAccount")
        if err != nil {
                t.Fatal(err)
        }
 
-       controlProg, err := accountManager.CreateAddress(nil, testAccount.ID, false)
+       controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
        if err != nil {
                t.Fatal(err)
        }
@@ -155,12 +155,12 @@ func TestMutilNodeSign(t *testing.T) {
                t.Fatal(err)
        }
 
-       testAccount, err := accountManager.Create(nil, []chainkd.XPub{xpub1.XPub, xpub2.XPub}, 2, "testAccount")
+       testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub, xpub2.XPub}, 2, "testAccount")
        if err != nil {
                t.Fatal(err)
        }
 
-       controlProg, err := accountManager.CreateAddress(nil, testAccount.ID, false)
+       controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
        if err != nil {
                t.Fatal(err)
        }
index dc67567..fab74e6 100644 (file)
@@ -71,12 +71,12 @@ func (g *TxGenerator) createAccount(name string, keys []string, quorum int) erro
                }
                xpubs = append(xpubs, *xpub)
        }
-       _, err := g.AccountManager.Create(nil, xpubs, quorum, name)
+       _, err := g.AccountManager.Create(xpubs, quorum, name)
        return err
 }
 
 func (g *TxGenerator) createAsset(accountAlias string, assetAlias string) (*asset.Asset, error) {
-       acc, err := g.AccountManager.FindByAlias(nil, accountAlias)
+       acc, err := g.AccountManager.FindByAlias(accountAlias)
        if err != nil {
                return nil, err
        }
@@ -128,11 +128,11 @@ func (g *TxGenerator) assetAmount(assetAlias string, amount uint64) (*bc.AssetAm
 }
 
 func (g *TxGenerator) createControlProgram(accountAlias string, change bool) (*account.CtrlProgram, error) {
-       acc, err := g.AccountManager.FindByAlias(nil, accountAlias)
+       acc, err := g.AccountManager.FindByAlias(accountAlias)
        if err != nil {
                return nil, err
        }
-       return g.AccountManager.CreateAddress(nil, acc.ID, change)
+       return g.AccountManager.CreateAddress(acc.ID, change)
 }
 
 // AddSpendInput add a spend input
@@ -142,7 +142,7 @@ func (g *TxGenerator) AddSpendInput(accountAlias, assetAlias string, amount uint
                return err
        }
 
-       acc, err := g.AccountManager.FindByAlias(nil, accountAlias)
+       acc, err := g.AccountManager.FindByAlias(accountAlias)
        if err != nil {
                return err
        }
@@ -170,7 +170,7 @@ func (g *TxGenerator) AddTxInput(txInput *types.TxInput, signInstruction *txbuil
 
 // AddTxInputFromUtxo add a tx input which spent the utxo
 func (g *TxGenerator) AddTxInputFromUtxo(utxo *account.UTXO, accountAlias string) error {
-       acc, err := g.AccountManager.FindByAlias(nil, accountAlias)
+       acc, err := g.AccountManager.FindByAlias(accountAlias)
        if err != nil {
                return err
        }
index ae78997..997d27c 100644 (file)
@@ -128,12 +128,12 @@ type walletTestContext struct {
 }
 
 func (ctx *walletTestContext) createControlProgram(accountName string, change bool) (*account.CtrlProgram, error) {
-       acc, err := ctx.Wallet.AccountMgr.FindByAlias(nil, accountName)
+       acc, err := ctx.Wallet.AccountMgr.FindByAlias(accountName)
        if err != nil {
                return nil, err
        }
 
-       return ctx.Wallet.AccountMgr.CreateAddress(nil, acc.ID, change)
+       return ctx.Wallet.AccountMgr.CreateAddress(acc.ID, change)
 }
 
 func (ctx *walletTestContext) getPubkey(keyAlias string) *chainkd.XPub {
@@ -147,7 +147,7 @@ func (ctx *walletTestContext) getPubkey(keyAlias string) *chainkd.XPub {
 }
 
 func (ctx *walletTestContext) createAsset(accountAlias string, assetAlias string) (*asset.Asset, error) {
-       acc, err := ctx.Wallet.AccountMgr.FindByAlias(nil, accountAlias)
+       acc, err := ctx.Wallet.AccountMgr.FindByAlias(accountAlias)
        if err != nil {
                return nil, err
        }
@@ -176,7 +176,7 @@ func (ctx *walletTestContext) createAccount(name string, keys []string, quorum i
                }
                xpubs = append(xpubs, *xpub)
        }
-       _, err := ctx.Wallet.AccountMgr.Create(nil, xpubs, quorum, name)
+       _, err := ctx.Wallet.AccountMgr.Create(xpubs, quorum, name)
        return err
 }
 
index acbfa15..ef252c4 100644 (file)
@@ -11,34 +11,12 @@ import (
        "github.com/bytom/account"
        "github.com/bytom/asset"
        "github.com/bytom/blockchain/query"
-       "github.com/bytom/consensus"
-       "github.com/bytom/consensus/segwit"
        "github.com/bytom/crypto/sha3pool"
        chainjson "github.com/bytom/encoding/json"
-       "github.com/bytom/errors"
        "github.com/bytom/protocol/bc"
        "github.com/bytom/protocol/bc/types"
 )
 
-type rawOutput struct {
-       OutputID bc.Hash
-       bc.AssetAmount
-       ControlProgram []byte
-       txHash         bc.Hash
-       outputIndex    uint32
-       sourceID       bc.Hash
-       sourcePos      uint64
-       ValidHeight    uint64
-}
-
-type accountOutput struct {
-       rawOutput
-       AccountID string
-       Address   string
-       keyIndex  uint64
-       change    bool
-}
-
 const (
        //TxPrefix is wallet database transactions prefix
        TxPrefix = "TXS:"
@@ -65,85 +43,17 @@ func calcTxIndexKey(txID string) []byte {
 // deleteTransaction delete transactions when orphan block rollback
 func (w *Wallet) deleteTransactions(batch db.Batch, height uint64) {
        tmpTx := query.AnnotatedTx{}
-
        txIter := w.DB.IteratorPrefix(calcDeleteKey(height))
        defer txIter.Release()
 
        for txIter.Next() {
                if err := json.Unmarshal(txIter.Value(), &tmpTx); err == nil {
-                       // delete index
                        batch.Delete(calcTxIndexKey(tmpTx.ID.String()))
                }
-
                batch.Delete(txIter.Key())
        }
 }
 
-// ReverseAccountUTXOs process the invalid blocks when orphan block rollback
-func (w *Wallet) reverseAccountUTXOs(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
-       var err error
-
-       // unknow how many spent and retire outputs
-       reverseOuts := []*rawOutput{}
-
-       // handle spent UTXOs
-       for txIndex, tx := range b.Transactions {
-               for _, inpID := range tx.Tx.InputIDs {
-                       // spend and retire
-                       sp, err := tx.Spend(inpID)
-                       if err != nil {
-                               continue
-                       }
-
-                       resOut, ok := tx.Entries[*sp.SpentOutputId].(*bc.Output)
-                       if !ok {
-                               continue
-                       }
-
-                       statusFail, _ := txStatus.GetStatus(txIndex)
-                       if statusFail && *resOut.Source.Value.AssetId != *consensus.BTMAssetID {
-                               continue
-                       }
-
-                       out := &rawOutput{
-                               OutputID:       *sp.SpentOutputId,
-                               AssetAmount:    *resOut.Source.Value,
-                               ControlProgram: resOut.ControlProgram.Code,
-                               txHash:         tx.ID,
-                               sourceID:       *resOut.Source.Ref,
-                               sourcePos:      resOut.Source.Position,
-                       }
-                       reverseOuts = append(reverseOuts, out)
-               }
-       }
-
-       accOuts := loadAccountInfo(reverseOuts, w)
-       if err = upsertConfirmedAccountOutputs(accOuts, batch); err != nil {
-               log.WithField("err", err).Error("reversing account spent and retire outputs")
-               return
-       }
-
-       // handle new UTXOs
-       for _, tx := range b.Transactions {
-               for j := range tx.Outputs {
-                       resOutID := tx.ResultIds[j]
-                       resOut, ok := tx.Entries[*resOutID].(*bc.Output)
-                       if !ok {
-                               // retirement
-                               continue
-                       }
-
-                       if segwit.IsP2WScript(resOut.ControlProgram.Code) {
-                               // delete standard UTXOs
-                               batch.Delete(account.StandardUTXOKey(*resOutID))
-                       } else {
-                               // delete contract UTXOs
-                               batch.Delete(account.ContractUTXOKey(*resOutID))
-                       }
-               }
-       }
-}
-
 // saveExternalAssetDefinition save external and local assets definition,
 // when query ,query local first and if have no then query external
 // details see getAliasDefinition
@@ -206,178 +116,6 @@ func (w *Wallet) indexTransactions(batch db.Batch, b *types.Block, txStatus *bc.
        return nil
 }
 
-// buildAccountUTXOs process valid blocks to build account unspent outputs db
-func (w *Wallet) buildAccountUTXOs(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
-       // get the spent UTXOs and delete the UTXOs from DB
-       prevoutDBKeys(batch, b, txStatus)
-
-       // handle new UTXOs
-       var outs []*rawOutput
-       for txIndex, tx := range b.Transactions {
-               for i, out := range tx.Outputs {
-                       resOutID := tx.ResultIds[i]
-                       resOut, ok := tx.Entries[*resOutID].(*bc.Output)
-                       if !ok {
-                               continue
-                       }
-
-                       if statusFail, _ := txStatus.GetStatus(txIndex); statusFail && *resOut.Source.Value.AssetId != *consensus.BTMAssetID {
-                               continue
-                       }
-
-                       out := &rawOutput{
-                               OutputID:       *tx.OutputID(i),
-                               AssetAmount:    out.AssetAmount,
-                               ControlProgram: out.ControlProgram,
-                               txHash:         tx.ID,
-                               outputIndex:    uint32(i),
-                               sourceID:       *resOut.Source.Ref,
-                               sourcePos:      resOut.Source.Position,
-                       }
-
-                       // coinbase utxo valid height
-                       if txIndex == 0 {
-                               out.ValidHeight = b.Height + consensus.CoinbasePendingBlockNumber
-                       }
-                       outs = append(outs, out)
-               }
-       }
-       accOuts := loadAccountInfo(outs, w)
-
-       if err := upsertConfirmedAccountOutputs(accOuts, batch); err != nil {
-               log.WithField("err", err).Error("building new account outputs")
-       }
-}
-
-func prevoutDBKeys(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
-       for txIndex, tx := range b.Transactions {
-               for _, inpID := range tx.Tx.InputIDs {
-                       sp, err := tx.Spend(inpID)
-                       if err != nil {
-                               continue
-                       }
-
-                       statusFail, _ := txStatus.GetStatus(txIndex)
-                       if statusFail && *sp.WitnessDestination.Value.AssetId != *consensus.BTMAssetID {
-                               continue
-                       }
-
-                       resOut, ok := tx.Entries[*sp.SpentOutputId].(*bc.Output)
-                       if !ok {
-                               // retirement
-                               log.WithField("SpentOutputId", *sp.SpentOutputId).Info("the OutputId is retirement")
-                               continue
-                       }
-
-                       if segwit.IsP2WScript(resOut.ControlProgram.Code) {
-                               // delete standard UTXOs
-                               batch.Delete(account.StandardUTXOKey(*sp.SpentOutputId))
-                       } else {
-                               // delete contract UTXOs
-                               batch.Delete(account.ContractUTXOKey(*sp.SpentOutputId))
-                       }
-               }
-       }
-       return
-}
-
-// loadAccountInfo turns a set of output IDs into a set of
-// outputs by adding account annotations.  Outputs that can't be
-// annotated are excluded from the result.
-func loadAccountInfo(outs []*rawOutput, w *Wallet) []*accountOutput {
-       outsByScript := make(map[string][]*rawOutput, len(outs))
-       for _, out := range outs {
-               scriptStr := string(out.ControlProgram)
-               outsByScript[scriptStr] = append(outsByScript[scriptStr], out)
-       }
-
-       result := make([]*accountOutput, 0, len(outs))
-       cp := account.CtrlProgram{}
-
-       var hash [32]byte
-       for s := range outsByScript {
-               // smart contract UTXO
-               if !segwit.IsP2WScript([]byte(s)) {
-                       for _, out := range outsByScript[s] {
-                               newOut := &accountOutput{
-                                       rawOutput: *out,
-                                       change:    false,
-                               }
-                               result = append(result, newOut)
-                       }
-
-                       continue
-               }
-
-               sha3pool.Sum256(hash[:], []byte(s))
-               bytes := w.DB.Get(account.ContractKey(hash))
-               if bytes == nil {
-                       continue
-               }
-
-               err := json.Unmarshal(bytes, &cp)
-               if err != nil {
-                       continue
-               }
-
-               isExist := w.DB.Get(account.Key(cp.AccountID))
-               if isExist == nil {
-                       continue
-               }
-
-               for _, out := range outsByScript[s] {
-                       newOut := &accountOutput{
-                               rawOutput: *out,
-                               AccountID: cp.AccountID,
-                               Address:   cp.Address,
-                               keyIndex:  cp.KeyIndex,
-                               change:    cp.Change,
-                       }
-                       result = append(result, newOut)
-               }
-       }
-
-       return result
-}
-
-// upsertConfirmedAccountOutputs records the account data for confirmed utxos.
-// If the account utxo already exists (because it's from a local tx), the
-// block confirmation data will in the row will be updated.
-func upsertConfirmedAccountOutputs(outs []*accountOutput, batch db.Batch) error {
-       var u *account.UTXO
-
-       for _, out := range outs {
-               u = &account.UTXO{
-                       OutputID:            out.OutputID,
-                       SourceID:            out.sourceID,
-                       AssetID:             *out.AssetId,
-                       Amount:              out.Amount,
-                       SourcePos:           out.sourcePos,
-                       ControlProgram:      out.ControlProgram,
-                       ControlProgramIndex: out.keyIndex,
-                       AccountID:           out.AccountID,
-                       Address:             out.Address,
-                       ValidHeight:         out.ValidHeight,
-                       Change:              out.change,
-               }
-
-               data, err := json.Marshal(u)
-               if err != nil {
-                       return errors.Wrap(err, "failed marshal accountutxo")
-               }
-
-               if segwit.IsP2WScript(out.ControlProgram) {
-                       // standard UTXOs
-                       batch.Set(account.StandardUTXOKey(out.OutputID), data)
-               } else {
-                       // contract UTXOs
-                       batch.Set(account.ContractUTXOKey(out.OutputID), data)
-               }
-
-       }
-       return nil
-}
-
 // filterAccountTxs related and build the fully annotated transactions.
 func (w *Wallet) filterAccountTxs(b *types.Block, txStatus *bc.TransactionStatus) []*query.AnnotatedTx {
        annotatedTxs := make([]*query.AnnotatedTx, 0, len(b.Transactions))
@@ -499,33 +237,9 @@ func (w *Wallet) GetTransactions(accountID string) ([]*query.AnnotatedTx, error)
        return annotatedTxs, nil
 }
 
-// GetAccountUTXOs return all account unspent outputs
-func (w *Wallet) GetAccountUTXOs(id string, isSmartContract bool) []account.UTXO {
-       accountUTXO := account.UTXO{}
-       accountUTXOs := []account.UTXO{}
-
-       prefix := account.UTXOPreFix
-       if isSmartContract {
-               prefix = account.SUTXOPrefix
-       }
-       accountUTXOIter := w.DB.IteratorPrefix([]byte(prefix + id))
-       defer accountUTXOIter.Release()
-       for accountUTXOIter.Next() {
-               if err := json.Unmarshal(accountUTXOIter.Value(), &accountUTXO); err != nil {
-                       hashKey := accountUTXOIter.Key()[len(prefix):]
-                       log.WithField("UTXO hash", string(hashKey)).Warn("get account UTXO")
-                       continue
-               }
-
-               accountUTXOs = append(accountUTXOs, accountUTXO)
-       }
-
-       return accountUTXOs
-}
-
 // GetAccountBalances return all account balances
 func (w *Wallet) GetAccountBalances(id string) ([]AccountBalance, error) {
-       return w.indexBalances(w.GetAccountUTXOs("", false))
+       return w.indexBalances(w.GetAccountUtxos("", false))
 }
 
 // AccountBalance account balance
@@ -538,7 +252,7 @@ type AccountBalance struct {
        AssetDefinition map[string]interface{} `json:"asset_definition"`
 }
 
-func (w *Wallet) indexBalances(accountUTXOs []account.UTXO) ([]AccountBalance, error) {
+func (w *Wallet) indexBalances(accountUTXOs []*account.UTXO) ([]AccountBalance, error) {
        accBalance := make(map[string]map[string]uint64)
        balances := []AccountBalance{}
 
index f932dff..b59d24a 100644 (file)
@@ -6,9 +6,12 @@ import (
        "sort"
        "time"
 
+       log "github.com/sirupsen/logrus"
+
        "github.com/bytom/account"
        "github.com/bytom/blockchain/query"
        "github.com/bytom/crypto/sha3pool"
+       "github.com/bytom/protocol"
        "github.com/bytom/protocol/bc/types"
 )
 
@@ -21,6 +24,67 @@ func calcUnconfirmedTxKey(formatKey string) []byte {
        return []byte(UnconfirmedTxPrefix + formatKey)
 }
 
+// SortByTimestamp implements sort.Interface for AnnotatedTx slices
+type SortByTimestamp []*query.AnnotatedTx
+
+func (a SortByTimestamp) Len() int           { return len(a) }
+func (a SortByTimestamp) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a SortByTimestamp) Less(i, j int) bool { return a[i].Timestamp > a[j].Timestamp }
+
+// AddUnconfirmedTx handle wallet status update when tx add into txpool
+func (w *Wallet) AddUnconfirmedTx(txD *protocol.TxDesc) {
+       if err := w.saveUnconfirmedTx(txD.Tx); err != nil {
+               log.WithField("err", err).Error("wallet fail on saveUnconfirmedTx")
+       }
+
+       utxos := txOutToUtxos(txD.Tx, txD.StatusFail, 0)
+       utxos = w.filterAccountUtxo(utxos)
+       w.AccountMgr.AddUnconfirmedUtxo(utxos)
+}
+
+// GetUnconfirmedTxs get account unconfirmed transactions, filter transactions by accountID when accountID is not empty
+func (w *Wallet) GetUnconfirmedTxs(accountID string) ([]*query.AnnotatedTx, error) {
+       annotatedTxs := []*query.AnnotatedTx{}
+       txIter := w.DB.IteratorPrefix([]byte(UnconfirmedTxPrefix))
+       defer txIter.Release()
+
+       for txIter.Next() {
+               annotatedTx := &query.AnnotatedTx{}
+               if err := json.Unmarshal(txIter.Value(), &annotatedTx); err != nil {
+                       return nil, err
+               }
+
+               if accountID == "" || findTransactionsByAccount(annotatedTx, accountID) {
+                       annotateTxsAsset(w, []*query.AnnotatedTx{annotatedTx})
+                       annotatedTxs = append([]*query.AnnotatedTx{annotatedTx}, annotatedTxs...)
+               }
+       }
+
+       sort.Sort(SortByTimestamp(annotatedTxs))
+       return annotatedTxs, nil
+}
+
+// GetUnconfirmedTxByTxID get unconfirmed transaction by txID
+func (w *Wallet) GetUnconfirmedTxByTxID(txID string) (*query.AnnotatedTx, error) {
+       annotatedTx := &query.AnnotatedTx{}
+       txInfo := w.DB.Get(calcUnconfirmedTxKey(txID))
+       if txInfo == nil {
+               return nil, fmt.Errorf("No transaction(tx_id=%s) from txpool", txID)
+       }
+
+       if err := json.Unmarshal(txInfo, annotatedTx); err != nil {
+               return nil, err
+       }
+
+       annotateTxsAsset(w, []*query.AnnotatedTx{annotatedTx})
+       return annotatedTx, nil
+}
+
+// RemoveUnconfirmedTx handle wallet status update when tx removed from txpool
+func (w *Wallet) RemoveUnconfirmedTx(txD *protocol.TxDesc) {
+       w.AccountMgr.RemoveUnconfirmedUtxo(txD.Tx.ResultIds)
+}
+
 func (w *Wallet) buildAnnotatedUnconfirmedTx(tx *types.Tx) *query.AnnotatedTx {
        annotatedTx := &query.AnnotatedTx{
                ID:        tx.ID,
@@ -36,7 +100,6 @@ func (w *Wallet) buildAnnotatedUnconfirmedTx(tx *types.Tx) *query.AnnotatedTx {
        for i := range tx.Outputs {
                annotatedTx.Outputs = append(annotatedTx.Outputs, w.BuildAnnotatedOutput(tx, i))
        }
-
        return annotatedTx
 }
 
@@ -59,12 +122,11 @@ func (w *Wallet) checkRelatedTransaction(tx *types.Tx) bool {
                        return true
                }
        }
-
        return false
 }
 
 // SaveUnconfirmedTx save unconfirmed annotated transaction to the database
-func (w *Wallet) SaveUnconfirmedTx(tx *types.Tx) error {
+func (w *Wallet) saveUnconfirmedTx(tx *types.Tx) error {
        if !w.checkRelatedTransaction(tx) {
                return nil
        }
@@ -83,49 +145,3 @@ func (w *Wallet) SaveUnconfirmedTx(tx *types.Tx) error {
        w.DB.Set(calcUnconfirmedTxKey(tx.ID.String()), rawTx)
        return nil
 }
-
-// GetUnconfirmedTxByTxID get unconfirmed transaction by txID
-func (w *Wallet) GetUnconfirmedTxByTxID(txID string) (*query.AnnotatedTx, error) {
-       annotatedTx := &query.AnnotatedTx{}
-       txInfo := w.DB.Get(calcUnconfirmedTxKey(txID))
-       if txInfo == nil {
-               return nil, fmt.Errorf("No transaction(tx_id=%s) from txpool", txID)
-       }
-
-       if err := json.Unmarshal(txInfo, annotatedTx); err != nil {
-               return nil, err
-       }
-       annotateTxsAsset(w, []*query.AnnotatedTx{annotatedTx})
-
-       return annotatedTx, nil
-}
-
-// SortByTimestamp implements sort.Interface for AnnotatedTx slices
-type SortByTimestamp []*query.AnnotatedTx
-
-func (a SortByTimestamp) Len() int           { return len(a) }
-func (a SortByTimestamp) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
-func (a SortByTimestamp) Less(i, j int) bool { return a[i].Timestamp > a[j].Timestamp }
-
-// GetUnconfirmedTxs get account unconfirmed transactions, filter transactions by accountID when accountID is not empty
-func (w *Wallet) GetUnconfirmedTxs(accountID string) ([]*query.AnnotatedTx, error) {
-       annotatedTxs := []*query.AnnotatedTx{}
-
-       txIter := w.DB.IteratorPrefix([]byte(UnconfirmedTxPrefix))
-       defer txIter.Release()
-       for txIter.Next() {
-               annotatedTx := &query.AnnotatedTx{}
-               if err := json.Unmarshal(txIter.Value(), &annotatedTx); err != nil {
-                       return nil, err
-               }
-
-               if accountID == "" || findTransactionsByAccount(annotatedTx, accountID) {
-                       annotateTxsAsset(w, []*query.AnnotatedTx{annotatedTx})
-                       annotatedTxs = append([]*query.AnnotatedTx{annotatedTx}, annotatedTxs...)
-               }
-       }
-
-       // sort SortByTimestamp by timestamp
-       sort.Sort(SortByTimestamp(annotatedTxs))
-       return annotatedTxs, nil
-}
index a607030..92ab081 100644 (file)
@@ -38,12 +38,12 @@ func TestWalletUnconfirmedTxs(t *testing.T) {
                t.Fatal(err)
        }
 
-       testAccount, err := accountManager.Create(nil, []chainkd.XPub{xpub1.XPub}, 1, "testAccount")
+       testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub}, 1, "testAccount")
        if err != nil {
                t.Fatal(err)
        }
 
-       controlProg, err := accountManager.CreateAddress(nil, testAccount.ID, false)
+       controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
        if err != nil {
                t.Fatal(err)
        }
@@ -68,7 +68,7 @@ func TestWalletUnconfirmedTxs(t *testing.T) {
                t.Fatal(err)
        }
        testTx := types.NewTx(*txData)
-       w.SaveUnconfirmedTx(testTx)
+       w.saveUnconfirmedTx(testTx)
 
        txs := AnnotatedTxs([]*types.Tx{testTx}, w)
        wantTx := txs[0]
diff --git a/wallet/utxo.go b/wallet/utxo.go
new file mode 100644 (file)
index 0000000..338fddc
--- /dev/null
@@ -0,0 +1,224 @@
+package wallet
+
+import (
+       "encoding/json"
+
+       log "github.com/sirupsen/logrus"
+       "github.com/tendermint/tmlibs/db"
+
+       "github.com/bytom/account"
+       "github.com/bytom/consensus"
+       "github.com/bytom/consensus/segwit"
+       "github.com/bytom/crypto/sha3pool"
+       "github.com/bytom/errors"
+       "github.com/bytom/protocol/bc"
+       "github.com/bytom/protocol/bc/types"
+)
+
+// GetAccountUtxos return all account unspent outputs
+func (w *Wallet) GetAccountUtxos(id string, isSmartContract bool) []*account.UTXO {
+       prefix := account.UTXOPreFix
+       if isSmartContract {
+               prefix = account.SUTXOPrefix
+       }
+       accountUtxos := []*account.UTXO{}
+       accountUtxoIter := w.DB.IteratorPrefix([]byte(prefix + id))
+       defer accountUtxoIter.Release()
+
+       for accountUtxoIter.Next() {
+               accountUtxo := &account.UTXO{}
+               if err := json.Unmarshal(accountUtxoIter.Value(), accountUtxo); err != nil {
+                       log.WithField("err", err).Warn("GetAccountUtxos fail on unmarshal utxo")
+                       continue
+               }
+
+               accountUtxos = append(accountUtxos, accountUtxo)
+       }
+       return accountUtxos
+}
+
+func (w *Wallet) attachUtxos(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
+       for txIndex, tx := range b.Transactions {
+               statusFail, err := txStatus.GetStatus(txIndex)
+               if err != nil {
+                       log.WithField("err", err).Error("attachUtxos fail on get tx status")
+                       continue
+               }
+
+               inputUtxos := txInToUtxos(tx, statusFail)
+               for _, inputUtxo := range inputUtxos {
+                       if segwit.IsP2WScript(inputUtxo.ControlProgram) {
+                               batch.Delete(account.StandardUTXOKey(inputUtxo.OutputID))
+                       } else {
+                               batch.Delete(account.ContractUTXOKey(inputUtxo.OutputID))
+                       }
+               }
+       }
+
+       utxos := []*account.UTXO{}
+       for txIndex, tx := range b.Transactions {
+               statusFail, err := txStatus.GetStatus(txIndex)
+               if err != nil {
+                       log.WithField("err", err).Error("attachUtxos fail on get txStatus")
+                       continue
+               }
+
+               validHeight := uint64(0)
+               if txIndex == 0 {
+                       validHeight = b.Height + consensus.CoinbasePendingBlockNumber
+               }
+               outputUtxos := txOutToUtxos(tx, statusFail, validHeight)
+               utxos = append(utxos, outputUtxos...)
+       }
+
+       utxos = w.filterAccountUtxo(utxos)
+       if err := batchSaveUtxos(utxos, batch); err != nil {
+               log.WithField("err", err).Error("attachUtxos fail on batchSaveUtxos")
+       }
+}
+
+func (w *Wallet) detachUtxos(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
+       utxos := []*account.UTXO{}
+       for txIndex, tx := range b.Transactions {
+               statusFail, err := txStatus.GetStatus(txIndex)
+               if err != nil {
+                       log.WithField("err", err).Error("detachUtxos fail on get tx status")
+                       continue
+               }
+
+               inputUtxos := txInToUtxos(tx, statusFail)
+               utxos = append(utxos, inputUtxos...)
+       }
+
+       utxos = w.filterAccountUtxo(utxos)
+       if err := batchSaveUtxos(utxos, batch); err != nil {
+               log.WithField("err", err).Error("detachUtxos fail on batchSaveUtxos")
+               return
+       }
+
+       for _, tx := range b.Transactions {
+               for j := range tx.Outputs {
+                       resOut, err := tx.Output(*tx.ResultIds[j])
+                       if err != nil {
+                               continue
+                       }
+
+                       if segwit.IsP2WScript(resOut.ControlProgram.Code) {
+                               batch.Delete(account.StandardUTXOKey(*tx.ResultIds[j]))
+                       } else {
+                               batch.Delete(account.ContractUTXOKey(*tx.ResultIds[j]))
+                       }
+               }
+       }
+}
+
+func (w *Wallet) filterAccountUtxo(utxos []*account.UTXO) []*account.UTXO {
+       outsByScript := make(map[string][]*account.UTXO, len(utxos))
+       for _, utxo := range utxos {
+               scriptStr := string(utxo.ControlProgram)
+               outsByScript[scriptStr] = append(outsByScript[scriptStr], utxo)
+       }
+
+       result := make([]*account.UTXO, 0, len(utxos))
+       for s := range outsByScript {
+               if !segwit.IsP2WScript([]byte(s)) {
+                       for _, utxo := range outsByScript[s] {
+                               result = append(result, utxo)
+                       }
+                       continue
+               }
+
+               var hash [32]byte
+               sha3pool.Sum256(hash[:], []byte(s))
+               data := w.DB.Get(account.ContractKey(hash))
+               if data == nil {
+                       continue
+               }
+
+               cp := &account.CtrlProgram{}
+               if err := json.Unmarshal(data, cp); err != nil {
+                       log.WithField("err", err).Error("filterAccountUtxo fail on unmarshal control program")
+                       continue
+               }
+
+               for _, utxo := range outsByScript[s] {
+                       utxo.AccountID = cp.AccountID
+                       utxo.Address = cp.Address
+                       utxo.ControlProgramIndex = cp.KeyIndex
+                       utxo.Change = cp.Change
+                       result = append(result, utxo)
+               }
+       }
+       return result
+}
+
+func batchSaveUtxos(utxos []*account.UTXO, batch db.Batch) error {
+       for _, utxo := range utxos {
+               data, err := json.Marshal(utxo)
+               if err != nil {
+                       return errors.Wrap(err, "failed marshal accountutxo")
+               }
+
+               if segwit.IsP2WScript(utxo.ControlProgram) {
+                       batch.Set(account.StandardUTXOKey(utxo.OutputID), data)
+               } else {
+                       batch.Set(account.ContractUTXOKey(utxo.OutputID), data)
+               }
+       }
+       return nil
+}
+
+func txInToUtxos(tx *types.Tx, statusFail bool) []*account.UTXO {
+       utxos := []*account.UTXO{}
+       for _, inpID := range tx.Tx.InputIDs {
+               sp, err := tx.Spend(inpID)
+               if err != nil {
+                       continue
+               }
+
+               resOut, err := tx.Output(*sp.SpentOutputId)
+               if err != nil {
+                       log.WithField("err", err).Error("txInToUtxos fail on get resOut")
+                       continue
+               }
+
+               if statusFail && *resOut.Source.Value.AssetId != *consensus.BTMAssetID {
+                       continue
+               }
+
+               utxos = append(utxos, &account.UTXO{
+                       OutputID:       *sp.SpentOutputId,
+                       AssetID:        *resOut.Source.Value.AssetId,
+                       Amount:         resOut.Source.Value.Amount,
+                       ControlProgram: resOut.ControlProgram.Code,
+                       SourceID:       *resOut.Source.Ref,
+                       SourcePos:      resOut.Source.Position,
+               })
+       }
+       return utxos
+}
+
+func txOutToUtxos(tx *types.Tx, statusFail bool, vaildHeight uint64) []*account.UTXO {
+       utxos := []*account.UTXO{}
+       for i, out := range tx.Outputs {
+               bcOut, err := tx.Output(*tx.ResultIds[i])
+               if err != nil {
+                       continue
+               }
+
+               if statusFail && *out.AssetAmount.AssetId != *consensus.BTMAssetID {
+                       continue
+               }
+
+               utxos = append(utxos, &account.UTXO{
+                       OutputID:       *tx.OutputID(i),
+                       AssetID:        *out.AssetAmount.AssetId,
+                       Amount:         out.Amount,
+                       ControlProgram: out.ControlProgram,
+                       SourceID:       *bcOut.Source.Ref,
+                       SourcePos:      bcOut.Source.Position,
+                       ValidHeight:    vaildHeight,
+               })
+       }
+       return utxos
+}
diff --git a/wallet/utxo_test.go b/wallet/utxo_test.go
new file mode 100644 (file)
index 0000000..47cbaa9
--- /dev/null
@@ -0,0 +1,545 @@
+package wallet
+
+import (
+       "encoding/hex"
+       "encoding/json"
+       "fmt"
+       "os"
+       "sort"
+       "testing"
+
+       dbm "github.com/tendermint/tmlibs/db"
+
+       "github.com/bytom/account"
+       "github.com/bytom/consensus"
+       "github.com/bytom/protocol/bc"
+       "github.com/bytom/protocol/bc/types"
+       "github.com/bytom/testutil"
+)
+
+func TestGetAccountUtxos(t *testing.T) {
+       testDB := dbm.NewDB("testdb", "leveldb", "temp")
+       defer os.RemoveAll("temp")
+
+       cases := []struct {
+               dbUtxos         map[string]*account.UTXO
+               id              string
+               isSmartContract bool
+               wantUtxos       []*account.UTXO
+       }{
+               {
+                       dbUtxos:         map[string]*account.UTXO{},
+                       id:              "",
+                       isSmartContract: false,
+                       wantUtxos:       []*account.UTXO{},
+               },
+               {
+                       dbUtxos: map[string]*account.UTXO{
+                               string(account.StandardUTXOKey(bc.Hash{V0: 1})): &account.UTXO{
+                                       OutputID: bc.Hash{V0: 1},
+                               },
+                               string(account.StandardUTXOKey(bc.Hash{V0: 2})): &account.UTXO{
+                                       OutputID: bc.Hash{V0: 2},
+                               },
+                               string(account.StandardUTXOKey(bc.Hash{V0: 3})): &account.UTXO{
+                                       OutputID: bc.Hash{V0: 3},
+                               },
+                               string(account.ContractUTXOKey(bc.Hash{V0: 4})): &account.UTXO{
+                                       OutputID: bc.Hash{V0: 4},
+                               },
+                       },
+                       id:              "",
+                       isSmartContract: false,
+                       wantUtxos: []*account.UTXO{
+                               &account.UTXO{OutputID: bc.Hash{V0: 1}},
+                               &account.UTXO{OutputID: bc.Hash{V0: 2}},
+                               &account.UTXO{OutputID: bc.Hash{V0: 3}},
+                       },
+               },
+               {
+                       dbUtxos: map[string]*account.UTXO{
+                               string(account.StandardUTXOKey(bc.Hash{V0: 1})): &account.UTXO{
+                                       OutputID: bc.Hash{V0: 1},
+                               },
+                               string(account.StandardUTXOKey(bc.Hash{V0: 2})): &account.UTXO{
+                                       OutputID: bc.Hash{V0: 2},
+                               },
+                               string(account.StandardUTXOKey(bc.Hash{V0: 3})): &account.UTXO{
+                                       OutputID: bc.Hash{V0: 3},
+                               },
+                               string(account.ContractUTXOKey(bc.Hash{V0: 4})): &account.UTXO{
+                                       OutputID: bc.Hash{V0: 4},
+                               },
+                       },
+                       id:              "",
+                       isSmartContract: true,
+                       wantUtxos: []*account.UTXO{
+                               &account.UTXO{OutputID: bc.Hash{V0: 4}},
+                       },
+               },
+               {
+                       dbUtxos: map[string]*account.UTXO{
+                               string(account.StandardUTXOKey(bc.Hash{V0: 1})): &account.UTXO{
+                                       OutputID: bc.Hash{V0: 1},
+                               },
+                               string(account.StandardUTXOKey(bc.Hash{V0: 1, V1: 2})): &account.UTXO{
+                                       OutputID: bc.Hash{V0: 1, V1: 2},
+                               },
+                               string(account.StandardUTXOKey(bc.Hash{V0: 2})): &account.UTXO{
+                                       OutputID: bc.Hash{V0: 2},
+                               },
+                               string(account.StandardUTXOKey(bc.Hash{V0: 2, V1: 2})): &account.UTXO{
+                                       OutputID: bc.Hash{V0: 2, V1: 2},
+                               },
+                       },
+                       id:              "0000000000000002",
+                       isSmartContract: false,
+                       wantUtxos: []*account.UTXO{
+                               &account.UTXO{OutputID: bc.Hash{V0: 2}},
+                               &account.UTXO{OutputID: bc.Hash{V0: 2, V1: 2}},
+                       },
+               },
+       }
+
+       w := &Wallet{DB: testDB}
+       for i, c := range cases {
+               for k, u := range c.dbUtxos {
+                       data, err := json.Marshal(u)
+                       if err != nil {
+                               t.Error(err)
+                       }
+                       testDB.Set([]byte(k), data)
+               }
+
+               gotUtxos := w.GetAccountUtxos(c.id, c.isSmartContract)
+               if !testutil.DeepEqual(gotUtxos, c.wantUtxos) {
+                       t.Errorf("case %d: got %v want %v", i, gotUtxos, c.wantUtxos)
+               }
+
+               for k := range c.dbUtxos {
+                       testDB.Delete([]byte(k))
+               }
+       }
+}
+
+func TestFilterAccountUtxo(t *testing.T) {
+       testDB := dbm.NewDB("testdb", "leveldb", "temp")
+       defer os.RemoveAll("temp")
+
+       cases := []struct {
+               dbPrograms map[string]*account.CtrlProgram
+               input      []*account.UTXO
+               wantUtxos  []*account.UTXO
+       }{
+               {
+                       dbPrograms: map[string]*account.CtrlProgram{},
+                       input:      []*account.UTXO{},
+                       wantUtxos:  []*account.UTXO{},
+               },
+               {
+                       dbPrograms: map[string]*account.CtrlProgram{
+                               "436f6e74726163743a2a37a64a4e15a772ab43bf3f5956d0d1f353946496788e7f40d0ff1796286a6f": &account.CtrlProgram{
+                                       AccountID: "testAccount",
+                                       Address:   "testAddress",
+                                       KeyIndex:  53,
+                                       Change:    true,
+                               },
+                       },
+                       input: []*account.UTXO{
+                               &account.UTXO{
+                                       ControlProgram: []byte{0x00, 0x14, 0x62, 0x50, 0x18, 0xb6, 0x85, 0x77, 0xba, 0x9b, 0x26, 0x19, 0xc8, 0x1d, 0x2e, 0x96, 0xba, 0x22, 0xbe, 0x77, 0x77, 0xd7},
+                                       AssetID:        bc.AssetID{V0: 1},
+                                       Amount:         3,
+                               },
+                               &account.UTXO{
+                                       ControlProgram: []byte{0x91},
+                                       AssetID:        bc.AssetID{V0: 1},
+                                       Amount:         4,
+                               },
+                       },
+                       wantUtxos: []*account.UTXO{
+                               &account.UTXO{
+                                       ControlProgram:      []byte{0x00, 0x14, 0x62, 0x50, 0x18, 0xb6, 0x85, 0x77, 0xba, 0x9b, 0x26, 0x19, 0xc8, 0x1d, 0x2e, 0x96, 0xba, 0x22, 0xbe, 0x77, 0x77, 0xd7},
+                                       AssetID:             bc.AssetID{V0: 1},
+                                       Amount:              3,
+                                       AccountID:           "testAccount",
+                                       Address:             "testAddress",
+                                       ControlProgramIndex: 53,
+                                       Change:              true,
+                               },
+                               &account.UTXO{
+                                       ControlProgram: []byte{0x91},
+                                       AssetID:        bc.AssetID{V0: 1},
+                                       Amount:         4,
+                               },
+                       },
+               },
+               {
+                       dbPrograms: map[string]*account.CtrlProgram{},
+                       input: []*account.UTXO{
+                               &account.UTXO{
+                                       ControlProgram: []byte{0x00, 0x14, 0x62, 0x50, 0x18, 0xb6, 0x85, 0x77, 0xba, 0x9b, 0x26, 0x19, 0xc8, 0x1d, 0x2e, 0x96, 0xba, 0x22, 0xbe, 0x77, 0x77, 0xd7},
+                                       AssetID:        bc.AssetID{V0: 1},
+                                       Amount:         3,
+                               },
+                               &account.UTXO{
+                                       ControlProgram: []byte{0x91},
+                                       AssetID:        bc.AssetID{V0: 1},
+                                       Amount:         3,
+                               },
+                       },
+                       wantUtxos: []*account.UTXO{
+                               &account.UTXO{
+                                       ControlProgram: []byte{0x91},
+                                       AssetID:        bc.AssetID{V0: 1},
+                                       Amount:         3,
+                               },
+                       },
+               },
+               {
+                       dbPrograms: map[string]*account.CtrlProgram{
+                               "436f6e74726163743a2a37a64a4e15a772ab43bf3f5956d0d1f353946496788e7f40d0ff1796286a6f": &account.CtrlProgram{
+                                       AccountID: "testAccount",
+                                       Address:   "testAddress",
+                                       KeyIndex:  53,
+                                       Change:    true,
+                               },
+                               "436f6e74726163743adb4d86262c12ba70d50b3ca3ae102d5682436243bd1e8c79569603f75675036a": &account.CtrlProgram{
+                                       AccountID: "testAccount2",
+                                       Address:   "testAddress2",
+                                       KeyIndex:  72,
+                                       Change:    false,
+                               },
+                       },
+                       input: []*account.UTXO{
+                               &account.UTXO{
+                                       ControlProgram: []byte{0x00, 0x14, 0x62, 0x50, 0x18, 0xb6, 0x85, 0x77, 0xba, 0x9b, 0x26, 0x19, 0xc8, 0x1d, 0x2e, 0x96, 0xba, 0x22, 0xbe, 0x77, 0x77, 0xd7},
+                                       AssetID:        bc.AssetID{V0: 1},
+                                       Amount:         3,
+                               },
+                               &account.UTXO{
+                                       ControlProgram: []byte{0x00, 0x14, 0x62, 0x50, 0x18, 0xb6, 0x85, 0x77, 0xba, 0x9b, 0x26, 0x19, 0xc8, 0x1d, 0x2e, 0x96, 0xba, 0x22, 0xbe, 0x77, 0x77, 0xd7},
+                                       AssetID:        bc.AssetID{V0: 1},
+                                       Amount:         5,
+                               },
+                               &account.UTXO{
+                                       ControlProgram: []byte{0x00, 0x14, 0xc6, 0xbf, 0x22, 0x19, 0x64, 0x2a, 0xc5, 0x9e, 0x5b, 0xe4, 0xeb, 0xdf, 0x5b, 0x22, 0x49, 0x56, 0xa7, 0x98, 0xa4, 0xdf},
+                                       AssetID:        bc.AssetID{V0: 1},
+                                       Amount:         7,
+                               },
+                       },
+                       wantUtxos: []*account.UTXO{
+                               &account.UTXO{
+                                       ControlProgram:      []byte{0x00, 0x14, 0x62, 0x50, 0x18, 0xb6, 0x85, 0x77, 0xba, 0x9b, 0x26, 0x19, 0xc8, 0x1d, 0x2e, 0x96, 0xba, 0x22, 0xbe, 0x77, 0x77, 0xd7},
+                                       AssetID:             bc.AssetID{V0: 1},
+                                       Amount:              3,
+                                       AccountID:           "testAccount",
+                                       Address:             "testAddress",
+                                       ControlProgramIndex: 53,
+                                       Change:              true,
+                               },
+                               &account.UTXO{
+                                       ControlProgram:      []byte{0x00, 0x14, 0x62, 0x50, 0x18, 0xb6, 0x85, 0x77, 0xba, 0x9b, 0x26, 0x19, 0xc8, 0x1d, 0x2e, 0x96, 0xba, 0x22, 0xbe, 0x77, 0x77, 0xd7},
+                                       AssetID:             bc.AssetID{V0: 1},
+                                       Amount:              5,
+                                       AccountID:           "testAccount",
+                                       Address:             "testAddress",
+                                       ControlProgramIndex: 53,
+                                       Change:              true,
+                               },
+                               &account.UTXO{
+                                       ControlProgram:      []byte{0x00, 0x14, 0xc6, 0xbf, 0x22, 0x19, 0x64, 0x2a, 0xc5, 0x9e, 0x5b, 0xe4, 0xeb, 0xdf, 0x5b, 0x22, 0x49, 0x56, 0xa7, 0x98, 0xa4, 0xdf},
+                                       AssetID:             bc.AssetID{V0: 1},
+                                       Amount:              7,
+                                       AccountID:           "testAccount2",
+                                       Address:             "testAddress2",
+                                       ControlProgramIndex: 72,
+                                       Change:              false,
+                               },
+                       },
+               },
+       }
+
+       w := &Wallet{DB: testDB}
+       for i, c := range cases {
+               for s, p := range c.dbPrograms {
+                       data, err := json.Marshal(p)
+                       if err != nil {
+                               t.Error(err)
+                       }
+                       key, err := hex.DecodeString(s)
+                       if err != nil {
+                               t.Error(err)
+                       }
+                       testDB.Set(key, data)
+               }
+
+               gotUtxos := w.filterAccountUtxo(c.input)
+               sort.Slice(gotUtxos[:], func(i, j int) bool {
+                       return gotUtxos[i].Amount < gotUtxos[j].Amount
+               })
+
+               if !testutil.DeepEqual(gotUtxos, c.wantUtxos) {
+                       t.Errorf("case %d: got %v want %v", i, gotUtxos, c.wantUtxos)
+               }
+               for s := range c.dbPrograms {
+                       key, err := hex.DecodeString(s)
+                       if err != nil {
+                               t.Error(err)
+                       }
+                       testDB.Delete(key)
+               }
+       }
+}
+
+func TestTxInToUtxos(t *testing.T) {
+       cases := []struct {
+               tx         *types.Tx
+               statusFail bool
+               wantUtxos  []*account.UTXO
+       }{
+               {
+                       tx: types.NewTx(types.TxData{
+                               Inputs: []*types.TxInput{
+                                       types.NewCoinbaseInput([]byte{0x51}),
+                               },
+                               Outputs: []*types.TxOutput{
+                                       types.NewTxOutput(*consensus.BTMAssetID, 41250000000, []byte{0x51}),
+                               },
+                       }),
+                       statusFail: false,
+                       wantUtxos:  []*account.UTXO{},
+               },
+               {
+                       tx: types.NewTx(types.TxData{
+                               Inputs: []*types.TxInput{
+                                       types.NewIssuanceInput([]byte{}, 4125, []byte{0x51}, [][]byte{}, []byte{}),
+                               },
+                               Outputs: []*types.TxOutput{
+                                       types.NewTxOutput(*consensus.BTMAssetID, 4125, []byte{0x51}),
+                               },
+                       }),
+                       statusFail: false,
+                       wantUtxos:  []*account.UTXO{},
+               },
+               {
+                       tx: types.NewTx(types.TxData{
+                               Inputs: []*types.TxInput{
+                                       types.NewSpendInput([][]byte{}, bc.Hash{V0: 1}, bc.AssetID{V0: 1}, 1, 1, []byte{0x51}),
+                                       types.NewSpendInput([][]byte{}, bc.Hash{V0: 2}, bc.AssetID{V0: 1}, 3, 2, []byte{0x52}),
+                                       types.NewSpendInput([][]byte{}, bc.Hash{V0: 3}, *consensus.BTMAssetID, 5, 3, []byte{0x53}),
+                                       types.NewSpendInput([][]byte{}, bc.Hash{V0: 4}, *consensus.BTMAssetID, 7, 4, []byte{0x54}),
+                               },
+                               Outputs: []*types.TxOutput{
+                                       types.NewTxOutput(bc.AssetID{V0: 1}, 4, []byte{0x51}),
+                                       types.NewTxOutput(*consensus.BTMAssetID, 12, []byte{0x53}),
+                               },
+                       }),
+                       statusFail: false,
+                       wantUtxos: []*account.UTXO{
+                               &account.UTXO{
+                                       OutputID:       bc.NewHash([32]byte{0x9c, 0x0d, 0xfd, 0x17, 0x4a, 0x63, 0x02, 0x0f, 0xf1, 0xda, 0xe9, 0x2d, 0xc8, 0xdb, 0x3c, 0x1c, 0x46, 0xda, 0xf2, 0x8f, 0xef, 0xe9, 0x5c, 0xef, 0x38, 0x82, 0xe0, 0xaf, 0xc5, 0x28, 0x39, 0x3c}),
+                                       AssetID:        bc.AssetID{V0: 1},
+                                       Amount:         1,
+                                       ControlProgram: []byte{0x51},
+                                       SourceID:       bc.Hash{V0: 1},
+                                       SourcePos:      1,
+                               },
+                               &account.UTXO{
+                                       OutputID:       bc.NewHash([32]byte{0xc1, 0xf6, 0xa3, 0xb8, 0x32, 0x20, 0xbc, 0xde, 0x95, 0x23, 0x23, 0xf8, 0xe4, 0xe9, 0x82, 0x45, 0x86, 0xfb, 0xf9, 0x43, 0x0d, 0xfb, 0xfc, 0x7c, 0xcf, 0xf9, 0x6c, 0x2a, 0xbf, 0x35, 0xb7, 0xe1}),
+                                       AssetID:        bc.AssetID{V0: 1},
+                                       Amount:         3,
+                                       ControlProgram: []byte{0x52},
+                                       SourceID:       bc.Hash{V0: 2},
+                                       SourcePos:      2,
+                               },
+                               &account.UTXO{
+                                       OutputID:       bc.NewHash([32]byte{0x3d, 0x0c, 0x52, 0xb0, 0x59, 0xe6, 0xdd, 0xcd, 0x31, 0xe1, 0x25, 0xc4, 0x33, 0xfc, 0x2c, 0x9c, 0xbe, 0xe3, 0x0c, 0x3e, 0x72, 0xb4, 0xac, 0x36, 0x5b, 0xbb, 0x8d, 0x44, 0xa0, 0x82, 0x27, 0xe4}),
+                                       AssetID:        *consensus.BTMAssetID,
+                                       Amount:         5,
+                                       ControlProgram: []byte{0x53},
+                                       SourceID:       bc.Hash{V0: 3},
+                                       SourcePos:      3,
+                               },
+                               &account.UTXO{
+                                       OutputID:       bc.NewHash([32]byte{0x88, 0x9e, 0x88, 0xd6, 0x21, 0x74, 0x7f, 0xa0, 0x01, 0x79, 0x29, 0x9a, 0xd8, 0xa3, 0xe9, 0xc7, 0x67, 0xac, 0x39, 0x1c, 0x2b, 0x3a, 0xd1, 0x56, 0x50, 0x42, 0xe1, 0x6b, 0x43, 0x2c, 0x05, 0x91}),
+                                       AssetID:        *consensus.BTMAssetID,
+                                       Amount:         7,
+                                       ControlProgram: []byte{0x54},
+                                       SourceID:       bc.Hash{V0: 4},
+                                       SourcePos:      4,
+                               },
+                       },
+               },
+               {
+                       tx: types.NewTx(types.TxData{
+                               Inputs: []*types.TxInput{
+                                       types.NewSpendInput([][]byte{}, bc.Hash{V0: 1}, bc.AssetID{V0: 1}, 1, 1, []byte{0x51}),
+                                       types.NewSpendInput([][]byte{}, bc.Hash{V0: 2}, bc.AssetID{V0: 1}, 3, 2, []byte{0x52}),
+                                       types.NewSpendInput([][]byte{}, bc.Hash{V0: 3}, *consensus.BTMAssetID, 5, 3, []byte{0x53}),
+                                       types.NewSpendInput([][]byte{}, bc.Hash{V0: 4}, *consensus.BTMAssetID, 7, 4, []byte{0x54}),
+                               },
+                               Outputs: []*types.TxOutput{
+                                       types.NewTxOutput(bc.AssetID{V0: 1}, 4, []byte{0x51}),
+                                       types.NewTxOutput(*consensus.BTMAssetID, 12, []byte{0x53}),
+                               },
+                       }),
+                       statusFail: true,
+                       wantUtxos: []*account.UTXO{
+                               &account.UTXO{
+                                       OutputID:       bc.NewHash([32]byte{0x3d, 0x0c, 0x52, 0xb0, 0x59, 0xe6, 0xdd, 0xcd, 0x31, 0xe1, 0x25, 0xc4, 0x33, 0xfc, 0x2c, 0x9c, 0xbe, 0xe3, 0x0c, 0x3e, 0x72, 0xb4, 0xac, 0x36, 0x5b, 0xbb, 0x8d, 0x44, 0xa0, 0x82, 0x27, 0xe4}),
+                                       AssetID:        *consensus.BTMAssetID,
+                                       Amount:         5,
+                                       ControlProgram: []byte{0x53},
+                                       SourceID:       bc.Hash{V0: 3},
+                                       SourcePos:      3,
+                               },
+                               &account.UTXO{
+                                       OutputID:       bc.NewHash([32]byte{0x88, 0x9e, 0x88, 0xd6, 0x21, 0x74, 0x7f, 0xa0, 0x01, 0x79, 0x29, 0x9a, 0xd8, 0xa3, 0xe9, 0xc7, 0x67, 0xac, 0x39, 0x1c, 0x2b, 0x3a, 0xd1, 0x56, 0x50, 0x42, 0xe1, 0x6b, 0x43, 0x2c, 0x05, 0x91}),
+                                       AssetID:        *consensus.BTMAssetID,
+                                       Amount:         7,
+                                       ControlProgram: []byte{0x54},
+                                       SourceID:       bc.Hash{V0: 4},
+                                       SourcePos:      4,
+                               },
+                       },
+               },
+       }
+
+       for i, c := range cases {
+               if gotUtxos := txInToUtxos(c.tx, c.statusFail); !testutil.DeepEqual(gotUtxos, c.wantUtxos) {
+                       for k, v := range gotUtxos {
+                               data, _ := json.Marshal(v)
+                               fmt.Println(k, string(data))
+                       }
+                       for k, v := range c.wantUtxos {
+                               data, _ := json.Marshal(v)
+                               fmt.Println(k, string(data))
+                       }
+                       t.Errorf("case %d: got %v want %v", i, gotUtxos, c.wantUtxos)
+               }
+       }
+}
+
+func TestTxOutToUtxos(t *testing.T) {
+       cases := []struct {
+               tx          *types.Tx
+               statusFail  bool
+               vaildHeight uint64
+               wantUtxos   []*account.UTXO
+       }{
+               {
+                       tx: types.NewTx(types.TxData{
+                               Inputs: []*types.TxInput{
+                                       types.NewCoinbaseInput([]byte{0x51}),
+                               },
+                               Outputs: []*types.TxOutput{
+                                       types.NewTxOutput(*consensus.BTMAssetID, 41250000000, []byte{0x51}),
+                               },
+                       }),
+                       statusFail:  false,
+                       vaildHeight: 98,
+                       wantUtxos: []*account.UTXO{
+                               &account.UTXO{
+                                       OutputID:       bc.NewHash([32]byte{0x67, 0x13, 0xec, 0x34, 0x57, 0xf8, 0xdc, 0xf1, 0xb2, 0x95, 0x71, 0x4c, 0xe0, 0xca, 0x94, 0xef, 0x81, 0x84, 0x6b, 0xa4, 0xc0, 0x8b, 0xb4, 0x40, 0xd3, 0xc0, 0x55, 0xc2, 0x7a, 0x9c, 0x04, 0x0a}),
+                                       AssetID:        *consensus.BTMAssetID,
+                                       Amount:         41250000000,
+                                       ControlProgram: []byte{0x51},
+                                       SourceID:       bc.NewHash([32]byte{0xb4, 0x7e, 0x94, 0x31, 0x88, 0xfe, 0xd3, 0xe9, 0xac, 0x99, 0x7c, 0xfc, 0x99, 0x6d, 0xd7, 0x4d, 0x04, 0x10, 0x77, 0xcb, 0x1c, 0xf8, 0x95, 0x14, 0x00, 0xe3, 0x42, 0x00, 0x8d, 0x05, 0xec, 0xdc}),
+                                       SourcePos:      0,
+                                       ValidHeight:    98,
+                               },
+                       },
+               },
+               {
+                       tx: types.NewTx(types.TxData{
+                               Inputs: []*types.TxInput{
+                                       types.NewSpendInput([][]byte{}, bc.Hash{V0: 1}, bc.AssetID{V0: 1}, 5, 1, []byte{0x51}),
+                                       types.NewSpendInput([][]byte{}, bc.Hash{V0: 2}, *consensus.BTMAssetID, 7, 1, []byte{0x51}),
+                               },
+                               Outputs: []*types.TxOutput{
+                                       types.NewTxOutput(bc.AssetID{V0: 1}, 2, []byte{0x51}),
+                                       types.NewTxOutput(bc.AssetID{V0: 1}, 3, []byte{0x52}),
+                                       types.NewTxOutput(*consensus.BTMAssetID, 2, []byte{0x53}),
+                                       types.NewTxOutput(*consensus.BTMAssetID, 5, []byte{0x54}),
+                               },
+                       }),
+                       statusFail:  false,
+                       vaildHeight: 0,
+                       wantUtxos: []*account.UTXO{
+                               &account.UTXO{
+                                       OutputID:       bc.NewHash([32]byte{0xff, 0xcd, 0xc4, 0xdc, 0xe8, 0x7e, 0xce, 0x93, 0x4b, 0x14, 0x2b, 0x2b, 0x84, 0xf2, 0x4d, 0x08, 0xca, 0x9f, 0x0b, 0x97, 0xa3, 0x0e, 0x38, 0x5a, 0xb0, 0xa7, 0x1e, 0x8f, 0x22, 0x55, 0xa6, 0x19}),
+                                       AssetID:        bc.AssetID{V0: 1},
+                                       Amount:         2,
+                                       ControlProgram: []byte{0x51},
+                                       SourceID:       bc.NewHash([32]byte{0x39, 0x4f, 0x89, 0xd4, 0xdc, 0x26, 0xb9, 0x57, 0x91, 0x2f, 0xe9, 0x7f, 0xba, 0x51, 0x68, 0xcf, 0xe4, 0xae, 0x0c, 0xef, 0x79, 0x56, 0xa0, 0x45, 0xda, 0x27, 0xdc, 0x69, 0xd8, 0xef, 0x32, 0x61}),
+                                       SourcePos:      0,
+                               },
+                               &account.UTXO{
+                                       OutputID:       bc.NewHash([32]byte{0x89, 0xcd, 0x38, 0x92, 0x6f, 0xee, 0xc6, 0x10, 0xae, 0x61, 0xef, 0x62, 0x70, 0x88, 0x94, 0x7c, 0x26, 0xaa, 0xfb, 0x05, 0xa2, 0x0a, 0x63, 0x9d, 0x21, 0x22, 0x0c, 0xe3, 0xc2, 0xe5, 0xf9, 0xbf}),
+                                       AssetID:        bc.AssetID{V0: 1},
+                                       Amount:         3,
+                                       ControlProgram: []byte{0x52},
+                                       SourceID:       bc.NewHash([32]byte{0x39, 0x4f, 0x89, 0xd4, 0xdc, 0x26, 0xb9, 0x57, 0x91, 0x2f, 0xe9, 0x7f, 0xba, 0x51, 0x68, 0xcf, 0xe4, 0xae, 0x0c, 0xef, 0x79, 0x56, 0xa0, 0x45, 0xda, 0x27, 0xdc, 0x69, 0xd8, 0xef, 0x32, 0x61}),
+                                       SourcePos:      1,
+                               },
+                               &account.UTXO{
+                                       OutputID:       bc.NewHash([32]byte{0xcf, 0xb9, 0xeb, 0xa3, 0xc8, 0xe8, 0xf1, 0x5a, 0x5c, 0x70, 0xf8, 0x9e, 0x7d, 0x9e, 0xf7, 0xb2, 0x66, 0x42, 0x8c, 0x97, 0x8e, 0xc2, 0x4d, 0x4b, 0x28, 0x57, 0xa7, 0x61, 0x1c, 0xf1, 0xea, 0x9d}),
+                                       AssetID:        *consensus.BTMAssetID,
+                                       Amount:         2,
+                                       ControlProgram: []byte{0x53},
+                                       SourceID:       bc.NewHash([32]byte{0x39, 0x4f, 0x89, 0xd4, 0xdc, 0x26, 0xb9, 0x57, 0x91, 0x2f, 0xe9, 0x7f, 0xba, 0x51, 0x68, 0xcf, 0xe4, 0xae, 0x0c, 0xef, 0x79, 0x56, 0xa0, 0x45, 0xda, 0x27, 0xdc, 0x69, 0xd8, 0xef, 0x32, 0x61}),
+                                       SourcePos:      2,
+                               },
+                               &account.UTXO{
+                                       OutputID:       bc.NewHash([32]byte{0x21, 0xf2, 0xe4, 0xee, 0xec, 0x1f, 0x82, 0xd8, 0xf2, 0xe1, 0x2b, 0x9e, 0x72, 0xfa, 0x91, 0x2b, 0x8c, 0xce, 0xbd, 0x18, 0x6d, 0x16, 0xf8, 0xc4, 0xf1, 0x71, 0x9d, 0x6b, 0x44, 0x41, 0xde, 0xb9}),
+                                       AssetID:        *consensus.BTMAssetID,
+                                       Amount:         5,
+                                       ControlProgram: []byte{0x54},
+                                       SourceID:       bc.NewHash([32]byte{0x39, 0x4f, 0x89, 0xd4, 0xdc, 0x26, 0xb9, 0x57, 0x91, 0x2f, 0xe9, 0x7f, 0xba, 0x51, 0x68, 0xcf, 0xe4, 0xae, 0x0c, 0xef, 0x79, 0x56, 0xa0, 0x45, 0xda, 0x27, 0xdc, 0x69, 0xd8, 0xef, 0x32, 0x61}),
+                                       SourcePos:      3,
+                               },
+                       },
+               },
+               {
+                       tx: types.NewTx(types.TxData{
+                               Inputs: []*types.TxInput{
+                                       types.NewSpendInput([][]byte{}, bc.Hash{V0: 1}, bc.AssetID{V0: 1}, 5, 1, []byte{0x51}),
+                                       types.NewSpendInput([][]byte{}, bc.Hash{V0: 2}, *consensus.BTMAssetID, 7, 1, []byte{0x51}),
+                               },
+                               Outputs: []*types.TxOutput{
+                                       types.NewTxOutput(bc.AssetID{V0: 1}, 2, []byte{0x51}),
+                                       types.NewTxOutput(bc.AssetID{V0: 1}, 3, []byte{0x52}),
+                                       types.NewTxOutput(*consensus.BTMAssetID, 2, []byte{0x53}),
+                                       types.NewTxOutput(*consensus.BTMAssetID, 5, []byte{0x54}),
+                               },
+                       }),
+                       statusFail:  true,
+                       vaildHeight: 0,
+                       wantUtxos: []*account.UTXO{
+                               &account.UTXO{
+                                       OutputID:       bc.NewHash([32]byte{0xcf, 0xb9, 0xeb, 0xa3, 0xc8, 0xe8, 0xf1, 0x5a, 0x5c, 0x70, 0xf8, 0x9e, 0x7d, 0x9e, 0xf7, 0xb2, 0x66, 0x42, 0x8c, 0x97, 0x8e, 0xc2, 0x4d, 0x4b, 0x28, 0x57, 0xa7, 0x61, 0x1c, 0xf1, 0xea, 0x9d}),
+                                       AssetID:        *consensus.BTMAssetID,
+                                       Amount:         2,
+                                       ControlProgram: []byte{0x53},
+                                       SourceID:       bc.NewHash([32]byte{0x39, 0x4f, 0x89, 0xd4, 0xdc, 0x26, 0xb9, 0x57, 0x91, 0x2f, 0xe9, 0x7f, 0xba, 0x51, 0x68, 0xcf, 0xe4, 0xae, 0x0c, 0xef, 0x79, 0x56, 0xa0, 0x45, 0xda, 0x27, 0xdc, 0x69, 0xd8, 0xef, 0x32, 0x61}),
+                                       SourcePos:      2,
+                               },
+                               &account.UTXO{
+                                       OutputID:       bc.NewHash([32]byte{0x21, 0xf2, 0xe4, 0xee, 0xec, 0x1f, 0x82, 0xd8, 0xf2, 0xe1, 0x2b, 0x9e, 0x72, 0xfa, 0x91, 0x2b, 0x8c, 0xce, 0xbd, 0x18, 0x6d, 0x16, 0xf8, 0xc4, 0xf1, 0x71, 0x9d, 0x6b, 0x44, 0x41, 0xde, 0xb9}),
+                                       AssetID:        *consensus.BTMAssetID,
+                                       Amount:         5,
+                                       ControlProgram: []byte{0x54},
+                                       SourceID:       bc.NewHash([32]byte{0x39, 0x4f, 0x89, 0xd4, 0xdc, 0x26, 0xb9, 0x57, 0x91, 0x2f, 0xe9, 0x7f, 0xba, 0x51, 0x68, 0xcf, 0xe4, 0xae, 0x0c, 0xef, 0x79, 0x56, 0xa0, 0x45, 0xda, 0x27, 0xdc, 0x69, 0xd8, 0xef, 0x32, 0x61}),
+                                       SourcePos:      3,
+                               },
+                       },
+               },
+       }
+
+       for i, c := range cases {
+               if gotUtxos := txOutToUtxos(c.tx, c.statusFail, c.vaildHeight); !testutil.DeepEqual(gotUtxos, c.wantUtxos) {
+                       t.Errorf("case %d: got %v want %v", i, gotUtxos, c.wantUtxos)
+               }
+       }
+}
index 19bb013..4fb6467 100644 (file)
@@ -18,8 +18,6 @@ import (
 const (
        //SINGLE single sign
        SINGLE = 1
-
-       maxTxChanSize = 10000 // txChanSize is the size of channel listening to Txpool newTxCh
 )
 
 var walletKey = []byte("walletInfo")
@@ -42,7 +40,6 @@ type Wallet struct {
        Hsm        *pseudohsm.HSM
        chain      *protocol.Chain
        rescanCh   chan struct{}
-       newTxCh    chan *types.Tx
 }
 
 //NewWallet return a new wallet instance
@@ -54,7 +51,6 @@ func NewWallet(walletDB db.DB, account *account.Manager, asset *asset.Registry,
                chain:      chain,
                Hsm:        hsm,
                rescanCh:   make(chan struct{}, 1),
-               newTxCh:    make(chan *types.Tx, maxTxChanSize),
        }
 
        if err := w.loadWalletInfo(); err != nil {
@@ -62,8 +58,6 @@ func NewWallet(walletDB db.DB, account *account.Manager, asset *asset.Registry,
        }
 
        go w.walletUpdater()
-       go w.UnconfirmedTxCollector()
-
        return w, nil
 }
 
@@ -111,7 +105,7 @@ func (w *Wallet) AttachBlock(block *types.Block) error {
 
        storeBatch := w.DB.NewBatch()
        w.indexTransactions(storeBatch, block, txStatus)
-       w.buildAccountUTXOs(storeBatch, block, txStatus)
+       w.attachUtxos(storeBatch, block, txStatus)
 
        w.status.WorkHeight = block.Height
        w.status.WorkHash = block.Hash()
@@ -134,7 +128,7 @@ func (w *Wallet) DetachBlock(block *types.Block) error {
        }
 
        storeBatch := w.DB.NewBatch()
-       w.reverseAccountUTXOs(storeBatch, block, txStatus)
+       w.detachUtxos(storeBatch, block, txStatus)
        w.deleteTransactions(storeBatch, w.status.BestHeight)
 
        w.status.BestHeight = block.Height - 1
@@ -210,18 +204,6 @@ func (w *Wallet) walletBlockWaiter() {
        }
 }
 
-// GetNewTxCh return a unconfirmed transaction feed channel
-func (w *Wallet) GetNewTxCh() chan *types.Tx {
-       return w.newTxCh
-}
-
-// UnconfirmedTxCollector collector unconfirmed transaction
-func (w *Wallet) UnconfirmedTxCollector() {
-       for {
-               w.SaveUnconfirmedTx(<-w.newTxCh)
-       }
-}
-
 // GetWalletStatusInfo return current wallet StatusInfo
 func (w *Wallet) GetWalletStatusInfo() StatusInfo {
        w.rw.RLock()
@@ -229,12 +211,3 @@ func (w *Wallet) GetWalletStatusInfo() StatusInfo {
 
        return w.status
 }
-
-func (w *Wallet) createProgram(account *account.Account, XPub *pseudohsm.XPub, index uint64) error {
-       for i := uint64(0); i < index; i++ {
-               if _, err := w.AccountMgr.CreateAddress(nil, account.ID, false); err != nil {
-                       return err
-               }
-       }
-       return nil
-}
index 29d39d5..b1de966 100644 (file)
@@ -49,12 +49,12 @@ func TestWalletUpdate(t *testing.T) {
                t.Fatal(err)
        }
 
-       testAccount, err := accountManager.Create(nil, []chainkd.XPub{xpub1.XPub}, 1, "testAccount")
+       testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub}, 1, "testAccount")
        if err != nil {
                t.Fatal(err)
        }
 
-       controlProg, err := accountManager.CreateAddress(nil, testAccount.ID, false)
+       controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
        if err != nil {
                t.Fatal(err)
        }
@@ -81,6 +81,7 @@ func TestWalletUpdate(t *testing.T) {
        tx := types.NewTx(*txData)
        block := mockSingleBlock(tx)
        txStatus := bc.NewTransactionStatus()
+       txStatus.SetStatus(0, false)
        store.SaveBlock(block, txStatus)
 
        w := mockWallet(testDB, accountManager, reg, chain)