OSDN Git Service

Wallet store interface (#217)
authorChengcheng Zhang <943420582@qq.com>
Thu, 11 Jul 2019 06:02:58 +0000 (14:02 +0800)
committerPaladz <yzhu101@uottawa.ca>
Thu, 11 Jul 2019 06:02:58 +0000 (14:02 +0800)
* update

* sort wallet store functions

* update

* update

* update

* remove DeleteAccountByAlias

* remove DeleteAccountUTXOs

* update

* update

* update

* remove deleteAccountControlPrograms

* remove DeleteBip44ContractIndex DeleteContractIndex DeleteControlProgram

* rename GetAccountByID to GetAccount

* update GetAsset

* move TestReserve to account

* move TestReserveParticular to account

* move TestFindUtxos to account

* update

* delete utxo_keeper_test

* update

* update

* move TestReserveBtmUtxoChain to account

* update

* update

* update

* remove mock/UTXO.go

* update ListTransactions

* update

* update

* update AccountStorer

* update SetAccountIndex

* update

* update

* update

* update DeleteAccountUTXOs

* update DeleteAccount

* update

* update ListUTXOs

* update

* update

* update getAccountFromACP

* update Restore

* update Restore

* update Restore

* update

* update

* update GetCoinbaseCtrlProgram

* update Create

* update Create

* update SaveAccount

* update UpdateAccountAlias

* update SetAccountIndex

* update FindByAlias

* update WalletStore

* update

* remove GetAccount

* update

* udpate

* update

* add SetStandardUTXO

* remove SetStandardUTXO in walletstore

* update

* remove DeleteStandardUTXO

* update

* update

* update

* update

* update

* add mockAccountStore in wallet_test

* update CalcGlobalTxIndex in wallet_test

* add mockWalletStore in wallet_test

* update TestWalletVersion

* move TestEncodeDecodeGlobalTxIndex

* add test/wallet_test.go

* update

* update

* update TestXPubsRecoveryLock

* update

* update TestExtendScanAddresses

* update TestRecoveryFromXPubs

* add mock wallet store

* update

* add mock account store

* update TestRecoveryByRescanAccount

* update

* update recovery_test.go

* update TestWalletUnconfirmedTxs

* update TestGetAccountUtxos

* update

* update

* update

* update

* update

* update

* update id

* update

* update

* update

* update

* update TestGetAccountUtxos

* update

* update

* update

* update

* update

* update

* update

* update

* update

* add MockAccountStore

* remove mock

* remove mock

* update

* remove test mock

* comment some test functions

* update SetWalletInfo

* update

* update

* update loadWalletInfo

* update

* update

* rename RecoveryState

* update

* update commitStatusInfo

* update

* update

* update

* remove recoveryKey

* update

* update

* update

* rename calcGlobalTxIndexKey to CalcGlobalTxIndexKey

* update

* update

* update TestWalletUpdate

* remove comments

* update wallet_test

* update LoadWalletInfo

* rename w.status

* update TestRescanWallet

* add TestMemPoolTxQueryLoop

* update TestMemPoolTxQueryLoop

* update

* update

* fix TestFilterAccountUtxo

* update

* update

* refine code

* remove TestRescanWallet

* add account store

* comment test

* update TestFilterAccountUtxo

* fix w.AccountMgr.GetControlProgram

* remove GetControlProgram

* remove comment

* update

* update

* update

* update

* rename accountDB to db

* rename walletDB to db

* update

* update

* update

* update

* update

* update

* update

* update

* update deleteAccountUTXOs

* update

* add deleteAccountControlPrograms

* update ListAccountUTXOs

* update GetAccountByProgram

* update

* update

* update

* update

* update

* rm key

* update utxo_keeper

* update DeleteTransactions

* update Bip44ContractIndexKey

* update dbm

* update TestFilterAccountUtxo

* update

* add accountstore

* update TestFilterAccountUtxo

* updata TestFilterAccountUtxo

* move db account store prefix

* move db wallet store prefix

* add InitStore

* update

* rename CommitStore

* add InitStore

* rename CommitStore

* fix bug

* update

* update

* update

* update

* update

* update

* update

* update

* move accountAliasKey

* update

* rename InitStore to InitBatch

* rename CommitStore to CommitBatch

* update

* update

* update

* update

* update

* update

* update

* fix TestWalletUpdate

* fix bugs

* remove loop

* fix bug

* update

* update

* update Restore

* update Restore

* update saveExternalAssetDefinition

* move ErrAccntTxIDNotFound

* update GetCoinbaseCtrlProgram

* update getExternalDefinition

32 files changed:
account/accounts.go
account/builder.go
account/builder_test.go
account/image.go
account/indexer.go
account/store.go [new file with mode: 0644]
account/utxo_keeper.go
account/utxo_keeper_test.go
database/account_store.go [new file with mode: 0644]
database/wallet_store.go [new file with mode: 0644]
database/wallet_store_test.go [new file with mode: 0644]
node/node.go
test/accounts_test.go [moved from account/accounts_test.go with 52% similarity]
test/bench_blockchain_test.go
test/builder_test.go [new file with mode: 0644]
test/chain_test_util.go
test/integration/standard_transaction_test.go
test/performance/mining_test.go
test/tx_test_util.go
test/wallet_test.go [new file with mode: 0644]
test/wallet_test_util.go
wallet/annotated.go
wallet/indexer.go
wallet/recovery.go
wallet/recovery_test.go
wallet/store.go [new file with mode: 0644]
wallet/unconfirmed.go
wallet/unconfirmed_test.go
wallet/utxo.go
wallet/utxo_test.go
wallet/wallet.go
wallet/wallet_test.go

index e26833b..ed86cc7 100644 (file)
@@ -2,9 +2,7 @@
 package account
 
 import (
-       "encoding/json"
        "reflect"
-       "sort"
        "strings"
        "sync"
 
@@ -20,7 +18,6 @@ import (
        "github.com/vapor/crypto"
        "github.com/vapor/crypto/ed25519/chainkd"
        "github.com/vapor/crypto/sha3pool"
-       dbm "github.com/vapor/database/leveldb"
        "github.com/vapor/errors"
        "github.com/vapor/protocol"
        "github.com/vapor/protocol/bc"
@@ -36,57 +33,22 @@ const (
        logModule        = "account"
 )
 
-var (
-       accountIndexPrefix  = []byte("AccountIndex:")
-       accountPrefix       = []byte("Account:")
-       aliasPrefix         = []byte("AccountAlias:")
-       contractIndexPrefix = []byte("ContractIndex")
-       contractPrefix      = []byte("Contract:")
-       miningAddressKey    = []byte("MiningAddress")
-       CoinbaseAbKey       = []byte("CoinbaseArbitrary")
-)
-
 // pre-define errors for supporting bytom errorFormatter
 var (
-       ErrDuplicateAlias  = errors.New("Duplicate account alias")
-       ErrDuplicateIndex  = errors.New("Duplicate account with same xPubs and index")
-       ErrFindAccount     = errors.New("Failed to find account")
-       ErrMarshalAccount  = errors.New("Failed to marshal account")
-       ErrInvalidAddress  = errors.New("Invalid address")
-       ErrFindCtrlProgram = errors.New("Failed to find account control program")
-       ErrDeriveRule      = errors.New("Invalid key derivation rule")
-       ErrContractIndex   = errors.New("Exceeded maximum addresses per account")
-       ErrAccountIndex    = errors.New("Exceeded maximum accounts per xpub")
-       ErrFindTransaction = errors.New("No transaction")
-       ErrAccountIDEmpty  = errors.New("account_id is empty")
+       ErrDuplicateAlias    = errors.New("Duplicate account alias")
+       ErrDuplicateIndex    = errors.New("Duplicate account with same xPubs and index")
+       ErrFindAccount       = errors.New("Failed to find account")
+       ErrMarshalAccount    = errors.New("Failed to marshal account")
+       ErrInvalidAddress    = errors.New("Invalid address")
+       ErrFindCtrlProgram   = errors.New("Failed to find account control program")
+       ErrDeriveRule        = errors.New("Invalid key derivation rule")
+       ErrContractIndex     = errors.New("Exceeded maximum addresses per account")
+       ErrAccountIndex      = errors.New("Exceeded maximum accounts per xpub")
+       ErrFindTransaction   = errors.New("No transaction")
+       ErrFindMiningAddress = errors.New("Failed to find mining address")
+       ErrAccountIDEmpty    = errors.New("account_id is empty")
 )
 
-// ContractKey account control promgram store prefix
-func ContractKey(hash common.Hash) []byte {
-       return append(contractPrefix, hash[:]...)
-}
-
-// Key account store prefix
-func Key(name string) []byte {
-       return append(accountPrefix, []byte(name)...)
-}
-
-func aliasKey(name string) []byte {
-       return append(aliasPrefix, []byte(name)...)
-}
-
-func bip44ContractIndexKey(accountID string, change bool) []byte {
-       key := append(contractIndexPrefix, accountID...)
-       if change {
-               return append(key, []byte{1}...)
-       }
-       return append(key, []byte{0}...)
-}
-
-func contractIndexKey(accountID string) []byte {
-       return append(contractIndexPrefix, []byte(accountID)...)
-}
-
 // Account is structure of Bytom account
 type Account struct {
        *signers.Signer
@@ -105,7 +67,7 @@ type CtrlProgram struct {
 
 // Manager stores accounts and their associated control programs.
 type Manager struct {
-       db         dbm.DB
+       store      AccountStore
        chain      *protocol.Chain
        utxoKeeper *utxoKeeper
 
@@ -121,11 +83,11 @@ type Manager struct {
 }
 
 // NewManager creates a new account manager
-func NewManager(walletDB dbm.DB, chain *protocol.Chain) *Manager {
+func NewManager(store AccountStore, chain *protocol.Chain) *Manager {
        return &Manager{
-               db:          walletDB,
+               store:       store,
                chain:       chain,
-               utxoKeeper:  newUtxoKeeper(chain.BestBlockHeight, walletDB),
+               utxoKeeper:  newUtxoKeeper(chain.BestBlockHeight, store),
                cache:       lru.New(maxAccountCache),
                aliasCache:  lru.New(maxAccountCache),
                delayedACPs: make(map[*txbuilder.TemplateBuilder][]*CtrlProgram),
@@ -152,19 +114,19 @@ func CreateAccount(xpubs []chainkd.XPub, quorum int, alias string, acctIndex uin
        return &Account{Signer: signer, ID: id, Alias: strings.ToLower(strings.TrimSpace(alias))}, nil
 }
 
-func (m *Manager) saveAccount(account *Account, updateIndex bool) error {
-       rawAccount, err := json.Marshal(account)
-       if err != nil {
-               return ErrMarshalAccount
+func (m *Manager) saveAccount(account *Account) error {
+       newStore := m.store.InitBatch()
+
+       // update account index
+       newStore.SetAccountIndex(account)
+       if err := newStore.SetAccount(account); err != nil {
+               return err
        }
 
-       storeBatch := m.db.NewBatch()
-       storeBatch.Set(Key(account.ID), rawAccount)
-       storeBatch.Set(aliasKey(account.Alias), []byte(account.ID))
-       if updateIndex {
-               storeBatch.Set(GetAccountIndexKey(account.XPubs), common.Unit64ToBytes(account.KeyIndex))
+       if err := newStore.CommitBatch(); err != nil {
+               return err
        }
-       storeBatch.Write()
+
        return nil
 }
 
@@ -173,24 +135,23 @@ func (m *Manager) SaveAccount(account *Account) error {
        m.accountMu.Lock()
        defer m.accountMu.Unlock()
 
-       if existed := m.db.Get(aliasKey(account.Alias)); existed != nil {
+       _, err := m.store.GetAccountByAlias(account.Alias)
+       if err == nil {
                return ErrDuplicateAlias
        }
 
-       acct, err := m.GetAccountByXPubsIndex(account.XPubs, account.KeyIndex)
-       if err != nil {
+       if err != ErrFindAccount {
                return err
        }
 
-       if acct != nil {
+       acct, err := m.GetAccountByXPubsIndex(account.XPubs, account.KeyIndex)
+       if err != nil && err != ErrFindAccount {
+               return err
+       } else if acct != nil {
                return ErrDuplicateIndex
        }
 
-       currentIndex := uint64(0)
-       if rawIndexBytes := m.db.Get(GetAccountIndexKey(account.XPubs)); rawIndexBytes != nil {
-               currentIndex = common.BytesToUnit64(rawIndexBytes)
-       }
-       return m.saveAccount(account, account.KeyIndex > currentIndex)
+       return m.saveAccount(account)
 }
 
 // Create creates and save a new Account.
@@ -198,27 +159,31 @@ func (m *Manager) Create(xpubs []chainkd.XPub, quorum int, alias string, deriveR
        m.accountMu.Lock()
        defer m.accountMu.Unlock()
 
-       if existed := m.db.Get(aliasKey(alias)); existed != nil {
+       _, err := m.store.GetAccountByAlias(alias)
+       if err == nil {
                return nil, ErrDuplicateAlias
+       } else if err != ErrFindAccount {
+               return nil, err
        }
 
        acctIndex := uint64(1)
-       if rawIndexBytes := m.db.Get(GetAccountIndexKey(xpubs)); rawIndexBytes != nil {
-               acctIndex = common.BytesToUnit64(rawIndexBytes) + 1
+       if currentIndex := m.store.GetAccountIndex(xpubs); currentIndex != 0 {
+               acctIndex = currentIndex + 1
        }
+
        account, err := CreateAccount(xpubs, quorum, alias, acctIndex, deriveRule)
        if err != nil {
                return nil, err
        }
 
-       if err := m.saveAccount(account, true); err != nil {
+       if err := m.saveAccount(account); err != nil {
                return nil, err
        }
 
        return account, nil
 }
 
-func (m *Manager) UpdateAccountAlias(accountID string, newAlias string) (err error) {
+func (m *Manager) UpdateAccountAlias(accountID string, newAlias string) error {
        m.accountMu.Lock()
        defer m.accountMu.Unlock()
 
@@ -226,28 +191,37 @@ func (m *Manager) UpdateAccountAlias(accountID string, newAlias string) (err err
        if err != nil {
                return err
        }
-       oldAlias := account.Alias
+       oldAccount := *account
 
        normalizedAlias := strings.ToLower(strings.TrimSpace(newAlias))
-       if existed := m.db.Get(aliasKey(normalizedAlias)); existed != nil {
+       _, err = m.store.GetAccountByAlias(normalizedAlias)
+       if err == nil {
                return ErrDuplicateAlias
        }
+       if err != ErrFindAccount {
+               return err
+       }
 
        m.cacheMu.Lock()
-       m.aliasCache.Remove(oldAlias)
+       m.aliasCache.Remove(oldAccount.Alias)
        m.cacheMu.Unlock()
 
        account.Alias = normalizedAlias
-       rawAccount, err := json.Marshal(account)
-       if err != nil {
-               return ErrMarshalAccount
+
+       newStore := m.store.InitBatch()
+
+       if err := newStore.DeleteAccount(&oldAccount); err != nil {
+               return err
+       }
+
+       if err := newStore.SetAccount(account); err != nil {
+               return err
+       }
+
+       if err := newStore.CommitBatch(); err != nil {
+               return err
        }
 
-       storeBatch := m.db.NewBatch()
-       storeBatch.Delete(aliasKey(oldAlias))
-       storeBatch.Set(Key(accountID), rawAccount)
-       storeBatch.Set(aliasKey(normalizedAlias), []byte(accountID))
-       storeBatch.Write()
        return nil
 }
 
@@ -303,45 +277,8 @@ func (m *Manager) CreateBatchAddresses(accountID string, change bool, stopIndex
        return nil
 }
 
-// deleteAccountControlPrograms deletes control program matching accountID
-func (m *Manager) deleteAccountControlPrograms(accountID string) error {
-       cps, err := m.ListControlProgram()
-       if err != nil {
-               return err
-       }
-
-       var hash common.Hash
-       for _, cp := range cps {
-               if cp.AccountID == accountID {
-                       sha3pool.Sum256(hash[:], cp.ControlProgram)
-                       m.db.Delete(ContractKey(hash))
-               }
-       }
-       m.db.Delete(bip44ContractIndexKey(accountID, false))
-       m.db.Delete(bip44ContractIndexKey(accountID, true))
-       m.db.Delete(contractIndexKey(accountID))
-       return nil
-}
-
-// deleteAccountUtxos deletes utxos matching accountID
-func (m *Manager) deleteAccountUtxos(accountID string) error {
-       accountUtxoIter := m.db.IteratorPrefix([]byte(UTXOPreFix))
-       defer accountUtxoIter.Release()
-       for accountUtxoIter.Next() {
-               accountUtxo := &UTXO{}
-               if err := json.Unmarshal(accountUtxoIter.Value(), accountUtxo); err != nil {
-                       return err
-               }
-
-               if accountID == accountUtxo.AccountID {
-                       m.db.Delete(StandardUTXOKey(accountUtxo.OutputID))
-               }
-       }
-       return nil
-}
-
 // DeleteAccount deletes the account's ID or alias matching account ID.
-func (m *Manager) DeleteAccount(accountID string) (err error) {
+func (m *Manager) DeleteAccount(accountID string) error {
        m.accountMu.Lock()
        defer m.accountMu.Unlock()
 
@@ -350,22 +287,11 @@ func (m *Manager) DeleteAccount(accountID string) (err error) {
                return err
        }
 
-       if err := m.deleteAccountControlPrograms(accountID); err != nil {
-               return err
-       }
-       if err := m.deleteAccountUtxos(accountID); 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
+       return m.store.DeleteAccount(account)
 }
 
 // FindByAlias retrieves an account's Signer record by its alias
@@ -377,16 +303,7 @@ func (m *Manager) FindByAlias(alias string) (*Account, error) {
                return m.FindByID(cachedID.(string))
        }
 
-       rawID := m.db.Get(aliasKey(alias))
-       if rawID == nil {
-               return nil, ErrFindAccount
-       }
-
-       accountID := string(rawID)
-       m.cacheMu.Lock()
-       m.aliasCache.Add(alias, accountID)
-       m.cacheMu.Unlock()
-       return m.FindByID(accountID)
+       return m.store.GetAccountByAlias(alias)
 }
 
 // FindByID returns an account's Signer record by its ID.
@@ -398,13 +315,8 @@ func (m *Manager) FindByID(id string) (*Account, error) {
                return cachedAccount.(*Account), nil
        }
 
-       rawAccount := m.db.Get(Key(id))
-       if rawAccount == nil {
-               return nil, ErrFindAccount
-       }
-
-       account := &Account{}
-       if err := json.Unmarshal(rawAccount, account); err != nil {
+       account, err := m.store.GetAccountByID(id)
+       if err != nil {
                return nil, err
        }
 
@@ -416,13 +328,7 @@ func (m *Manager) FindByID(id string) (*Account, error) {
 
 // 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)
+       return m.store.GetAccountByID(program.AccountID)
 }
 
 // GetAccountByXPubsIndex get account by xPubs and index
@@ -437,26 +343,21 @@ func (m *Manager) GetAccountByXPubsIndex(xPubs []chainkd.XPub, index uint64) (*A
                        return account, nil
                }
        }
-       return nil, nil
+       return nil, ErrFindAccount
 }
 
 // GetAliasByID return the account alias by given ID
 func (m *Manager) GetAliasByID(id string) string {
-       rawAccount := m.db.Get(Key(id))
-       if rawAccount == nil {
+       account, err := m.store.GetAccountByID(id)
+       if err != nil {
                log.Warn("GetAliasByID fail to find account")
                return ""
        }
-
-       account := &Account{}
-       if err := json.Unmarshal(rawAccount, account); err != nil {
-               log.Warn(err)
-       }
        return account.Alias
 }
 
 func (m *Manager) GetCoinbaseArbitrary() []byte {
-       if arbitrary := m.db.Get(CoinbaseAbKey); arbitrary != nil {
+       if arbitrary := m.store.GetCoinbaseArbitrary(); arbitrary != nil {
                return arbitrary
        }
        return []byte{}
@@ -469,28 +370,32 @@ func (m *Manager) GetCoinbaseControlProgram() ([]byte, error) {
                log.Warningf("GetCoinbaseControlProgram: can't find any account in db")
                return vmutil.DefaultCoinbaseProgram()
        }
+
        if err != nil {
                return nil, err
        }
+
        return cp.ControlProgram, nil
 }
 
 // GetCoinbaseCtrlProgram will return the coinbase CtrlProgram
 func (m *Manager) GetCoinbaseCtrlProgram() (*CtrlProgram, error) {
-       if data := m.db.Get(miningAddressKey); data != nil {
-               cp := &CtrlProgram{}
-               return cp, json.Unmarshal(data, cp)
+       if cp, err := m.store.GetMiningAddress(); err == nil {
+               return cp, nil
+       } else if err != ErrFindMiningAddress {
+               return nil, err
        }
 
-       accountIter := m.db.IteratorPrefix([]byte(accountPrefix))
-       defer accountIter.Release()
-       if !accountIter.Next() {
-               return nil, ErrFindAccount
+       account := new(Account)
+       accounts, err := m.store.ListAccounts("")
+       if err != nil {
+               return nil, err
        }
 
-       account := &Account{}
-       if err := json.Unmarshal(accountIter.Value(), account); err != nil {
-               return nil, err
+       if len(accounts) > 0 {
+               account = accounts[0]
+       } else {
+               return nil, ErrFindAccount
        }
 
        program, err := m.CreateAddress(account.ID, false)
@@ -498,31 +403,16 @@ func (m *Manager) GetCoinbaseCtrlProgram() (*CtrlProgram, error) {
                return nil, err
        }
 
-       rawCP, err := json.Marshal(program)
-       if err != nil {
+       if err := m.store.SetMiningAddress(program); err != nil {
                return nil, err
        }
 
-       m.db.Set(miningAddressKey, rawCP)
        return program, nil
 }
 
-// GetContractIndex return the current index
-func (m *Manager) GetContractIndex(accountID string) uint64 {
-       index := uint64(0)
-       if rawIndexBytes := m.db.Get(contractIndexKey(accountID)); rawIndexBytes != nil {
-               index = common.BytesToUnit64(rawIndexBytes)
-       }
-       return index
-}
-
 // GetBip44ContractIndex return the current bip44 contract index
 func (m *Manager) GetBip44ContractIndex(accountID string, change bool) uint64 {
-       index := uint64(0)
-       if rawIndexBytes := m.db.Get(bip44ContractIndexKey(accountID, change)); rawIndexBytes != nil {
-               index = common.BytesToUnit64(rawIndexBytes)
-       }
-       return index
+       return m.store.GetBip44ContractIndex(accountID, change)
 }
 
 // GetLocalCtrlProgramByAddress return CtrlProgram by given address
@@ -534,13 +424,13 @@ func (m *Manager) GetLocalCtrlProgramByAddress(address string) (*CtrlProgram, er
 
        var hash [32]byte
        sha3pool.Sum256(hash[:], program)
-       rawProgram := m.db.Get(ContractKey(hash))
-       if rawProgram == nil {
-               return nil, ErrFindCtrlProgram
+
+       cp, err := m.store.GetControlProgram(bc.NewHash(hash))
+       if err != nil {
+               return nil, err
        }
 
-       cp := &CtrlProgram{}
-       return cp, json.Unmarshal(rawProgram, cp)
+       return cp, nil
 }
 
 // GetMiningAddress will return the mining address
@@ -549,47 +439,29 @@ func (m *Manager) GetMiningAddress() (string, error) {
        if err != nil {
                return "", err
        }
+
        return cp.Address, nil
 }
 
 // IsLocalControlProgram check is the input control program belong to local
 func (m *Manager) IsLocalControlProgram(prog []byte) bool {
-       var hash common.Hash
+       var hash [32]byte
        sha3pool.Sum256(hash[:], prog)
-       bytes := m.db.Get(ContractKey(hash))
-       return bytes != nil
+       cp, err := m.store.GetControlProgram(bc.NewHash(hash))
+       if err != nil || cp == nil {
+               return false
+       }
+       return true
 }
 
 // ListAccounts will return the accounts in the db
 func (m *Manager) ListAccounts(id string) ([]*Account, error) {
-       accounts := []*Account{}
-       accountIter := m.db.IteratorPrefix(Key(strings.TrimSpace(id)))
-       defer accountIter.Release()
-
-       for accountIter.Next() {
-               account := &Account{}
-               if err := json.Unmarshal(accountIter.Value(), &account); err != nil {
-                       return nil, err
-               }
-               accounts = append(accounts, account)
-       }
-       return accounts, nil
+       return m.store.ListAccounts(id)
 }
 
 // ListControlProgram return all the local control program
 func (m *Manager) ListControlProgram() ([]*CtrlProgram, error) {
-       cps := []*CtrlProgram{}
-       cpIter := m.db.IteratorPrefix(contractPrefix)
-       defer cpIter.Release()
-
-       for cpIter.Next() {
-               cp := &CtrlProgram{}
-               if err := json.Unmarshal(cpIter.Value(), cp); err != nil {
-                       return nil, err
-               }
-               cps = append(cps, cp)
-       }
-       return cps, nil
+       return m.store.ListControlPrograms()
 }
 
 func (m *Manager) ListUnconfirmedUtxo(accountID string, isSmartContract bool) []*UTXO {
@@ -619,17 +491,15 @@ func (m *Manager) SetMiningAddress(miningAddress string) (string, error) {
                Address:        miningAddress,
                ControlProgram: program,
        }
-       rawCP, err := json.Marshal(cp)
-       if err != nil {
-               return "", err
+       if err := m.store.SetMiningAddress(cp); err != nil {
+               return cp.Address, err
        }
 
-       m.db.Set(miningAddressKey, rawCP)
        return m.GetMiningAddress()
 }
 
 func (m *Manager) SetCoinbaseArbitrary(arbitrary []byte) {
-       m.db.Set(CoinbaseAbKey, arbitrary)
+       m.store.SetCoinbaseArbitrary(arbitrary)
 }
 
 // CreateCtrlProgram generate an address for the select account
@@ -644,9 +514,11 @@ func CreateCtrlProgram(account *Account, addrIdx uint64, change bool) (cp *CtrlP
        } else {
                cp, err = createP2SH(account, path)
        }
+
        if err != nil {
                return nil, err
        }
+
        cp.KeyIndex, cp.Change = addrIdx, change
        return cp, nil
 }
@@ -680,8 +552,8 @@ func createP2SH(account *Account, path [][]byte) (*CtrlProgram, error) {
        if err != nil {
                return nil, err
        }
-       scriptHash := crypto.Sha256(signScript)
 
+       scriptHash := crypto.Sha256(signScript)
        address, err := common.NewAddressWitnessScriptHash(scriptHash, &consensus.ActiveNetParams)
        if err != nil {
                return nil, err
@@ -699,24 +571,16 @@ func createP2SH(account *Account, path [][]byte) (*CtrlProgram, error) {
        }, nil
 }
 
-func GetAccountIndexKey(xpubs []chainkd.XPub) []byte {
-       var hash [32]byte
-       var xPubs []byte
-       cpy := append([]chainkd.XPub{}, xpubs[:]...)
-       sort.Sort(signers.SortKeys(cpy))
-       for _, xpub := range cpy {
-               xPubs = append(xPubs, xpub[:]...)
-       }
-       sha3pool.Sum256(hash[:], xPubs)
-       return append(accountIndexPrefix, hash[:]...)
+func (m *Manager) GetContractIndex(accountID string) uint64 {
+       return m.store.GetContractIndex(accountID)
 }
 
 func (m *Manager) getCurrentContractIndex(account *Account, change bool) (uint64, error) {
        switch account.DeriveRule {
        case signers.BIP0032:
-               return m.GetContractIndex(account.ID), nil
+               return m.store.GetContractIndex(account.ID), nil
        case signers.BIP0044:
-               return m.GetBip44ContractIndex(account.ID, change), nil
+               return m.store.GetBip44ContractIndex(account.ID, change), nil
        }
        return 0, ErrDeriveRule
 }
@@ -726,6 +590,7 @@ func (m *Manager) getProgramByAddress(address string) ([]byte, error) {
        if err != nil {
                return nil, err
        }
+
        redeemContract := addr.ScriptAddress()
        program := []byte{}
        switch addr.(type) {
@@ -739,11 +604,12 @@ func (m *Manager) getProgramByAddress(address string) ([]byte, error) {
        if err != nil {
                return nil, err
        }
+
        return program, nil
 }
 
 func (m *Manager) saveControlProgram(prog *CtrlProgram, updateIndex bool) error {
-       var hash common.Hash
+       var hash [32]byte
 
        sha3pool.Sum256(hash[:], prog.ControlProgram)
        acct, err := m.GetAccountByProgram(prog)
@@ -751,24 +617,22 @@ func (m *Manager) saveControlProgram(prog *CtrlProgram, updateIndex bool) error
                return err
        }
 
-       accountCP, err := json.Marshal(prog)
-       if err != nil {
-               return err
+       newStore := m.store.InitBatch()
+
+       if err := newStore.SetControlProgram(bc.NewHash(hash), prog); err != nil {
+               return nil
        }
 
-       storeBatch := m.db.NewBatch()
-       storeBatch.Set(ContractKey(hash), accountCP)
        if updateIndex {
                switch acct.DeriveRule {
                case signers.BIP0032:
-                       storeBatch.Set(contractIndexKey(acct.ID), common.Unit64ToBytes(prog.KeyIndex))
+                       newStore.SetContractIndex(acct.ID, prog.KeyIndex)
                case signers.BIP0044:
-                       storeBatch.Set(bip44ContractIndexKey(acct.ID, prog.Change), common.Unit64ToBytes(prog.KeyIndex))
+                       newStore.SetBip44ContractIndex(acct.ID, prog.Change, prog.KeyIndex)
                }
        }
-       storeBatch.Write()
 
-       return nil
+       return newStore.CommitBatch()
 }
 
 // SaveControlPrograms save account control programs
@@ -791,3 +655,15 @@ func (m *Manager) SaveControlPrograms(progs ...*CtrlProgram) error {
        }
        return nil
 }
+
+func (m *Manager) SetStandardUTXO(outputID bc.Hash, utxo *UTXO) error {
+       return m.store.SetStandardUTXO(outputID, utxo)
+}
+
+func (m *Manager) DeleteStandardUTXO(outputID bc.Hash) {
+       m.store.DeleteStandardUTXO(outputID)
+}
+
+func (m *Manager) GetControlProgram(hash bc.Hash) (*CtrlProgram, error) {
+       return m.store.GetControlProgram(hash)
+}
index 5f2448a..143ad24 100644 (file)
@@ -90,7 +90,7 @@ func (m *Manager) reserveBtmUtxoChain(builder *txbuilder.TemplateBuilder, accoun
        return utxos, nil
 }
 
-func (m *Manager) buildBtmTxChain(utxos []*UTXO, signer *signers.Signer) ([]*txbuilder.Template, *UTXO, error) {
+func (m *Manager) BuildBtmTxChain(utxos []*UTXO, signer *signers.Signer) ([]*txbuilder.Template, *UTXO, error) {
        if len(utxos) == 0 {
                return nil, nil, errors.New("mergeSpendActionUTXO utxos num 0")
        }
@@ -166,6 +166,7 @@ func SpendAccountChain(ctx context.Context, builder *txbuilder.TemplateBuilder,
        if !ok {
                return nil, errors.New("fail to convert the spend action")
        }
+
        if *act.AssetId != *consensus.BTMAssetID {
                return nil, errors.New("spend chain action only support BTM")
        }
@@ -180,7 +181,7 @@ func SpendAccountChain(ctx context.Context, builder *txbuilder.TemplateBuilder,
                return nil, err
        }
 
-       tpls, utxo, err := act.accounts.buildBtmTxChain(utxos, acct.Signer)
+       tpls, utxo, err := act.accounts.BuildBtmTxChain(utxos, acct.Signer)
        if err != nil {
                return nil, err
        }
@@ -324,6 +325,7 @@ func UtxoToInputs(signer *signers.Signer, u *UTXO) (*types.TxInput, *txbuilder.S
        if err != nil {
                return nil, nil, err
        }
+
        if u.Address == "" {
                sigInst.AddWitnessKeys(signer.XPubs, path, signer.Quorum)
                return txInput, sigInst, nil
@@ -382,6 +384,7 @@ func (m *Manager) insertControlProgramDelayed(b *txbuilder.TemplateBuilder, acp
                if len(acps) == 0 {
                        return nil
                }
+
                return m.SaveControlPrograms(acps...)
        })
 }
index dbc2655..c5f2cbd 100644 (file)
 package account
 
 import (
-       "encoding/json"
        "testing"
        "time"
 
-       "github.com/vapor/blockchain/signers"
        "github.com/vapor/blockchain/txbuilder"
        "github.com/vapor/consensus"
-       "github.com/vapor/crypto/ed25519/chainkd"
        "github.com/vapor/protocol/bc"
        "github.com/vapor/testutil"
 )
 
-func TestReserveBtmUtxoChain(t *testing.T) {
-       chainTxUtxoNum = 3
-       utxos := []*UTXO{}
-       m := mockAccountManager(t)
-       for i := uint64(1); i <= 20; i++ {
-               utxo := &UTXO{
-                       OutputID:  bc.Hash{V0: i},
-                       AccountID: "TestAccountID",
-                       AssetID:   *consensus.BTMAssetID,
-                       Amount:    i * chainTxMergeGas,
-               }
-               utxos = append(utxos, utxo)
-
-               data, err := json.Marshal(utxo)
-               if err != nil {
-                       t.Fatal(err)
-               }
-
-               m.db.Set(StandardUTXOKey(utxo.OutputID), data)
-       }
-
-       cases := []struct {
-               amount uint64
-               want   []uint64
-               err    bool
-       }{
-               {
-                       amount: 1 * chainTxMergeGas,
-                       want:   []uint64{1},
-               },
-               {
-                       amount: 888888 * chainTxMergeGas,
-                       want:   []uint64{},
-                       err:    true,
-               },
-               {
-                       amount: 7 * chainTxMergeGas,
-                       want:   []uint64{4, 3, 1},
-               },
-               {
-                       amount: 15 * chainTxMergeGas,
-                       want:   []uint64{5, 4, 3, 2, 1, 6},
-               },
-               {
-                       amount: 163 * chainTxMergeGas,
-                       want:   []uint64{20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 2, 1, 3},
-               },
-       }
-
-       for i, c := range cases {
-               m.utxoKeeper.expireReservation(time.Unix(999999999, 0))
-               utxos, err := m.reserveBtmUtxoChain(&txbuilder.TemplateBuilder{}, "TestAccountID", c.amount, false)
-
-               if err != nil != c.err {
-                       t.Fatalf("case %d got err %v want err = %v", i, err, c.err)
-               }
-
-               got := []uint64{}
-               for _, utxo := range utxos {
-                       got = append(got, utxo.Amount/chainTxMergeGas)
-               }
-
-               if !testutil.DeepEqual(got, c.want) {
-                       t.Fatalf("case %d got %d want %d", i, got, c.want)
-               }
-       }
-
-}
-
-func TestBuildBtmTxChain(t *testing.T) {
-       chainTxUtxoNum = 3
-       m := mockAccountManager(t)
-       cases := []struct {
-               inputUtxo  []uint64
-               wantInput  [][]uint64
-               wantOutput [][]uint64
-               wantUtxo   uint64
-       }{
-               {
-                       inputUtxo:  []uint64{5},
-                       wantInput:  [][]uint64{},
-                       wantOutput: [][]uint64{},
-                       wantUtxo:   5 * chainTxMergeGas,
-               },
-               {
-                       inputUtxo: []uint64{5, 4},
-                       wantInput: [][]uint64{
-                               []uint64{5, 4},
-                       },
-                       wantOutput: [][]uint64{
-                               []uint64{8},
-                       },
-                       wantUtxo: 8 * chainTxMergeGas,
-               },
-               {
-                       inputUtxo: []uint64{5, 4, 1, 1},
-                       wantInput: [][]uint64{
-                               []uint64{5, 4, 1},
-                               []uint64{1, 9},
-                       },
-                       wantOutput: [][]uint64{
-                               []uint64{9},
-                               []uint64{9},
-                       },
-                       wantUtxo: 9 * chainTxMergeGas,
-               },
-               {
-                       inputUtxo: []uint64{22, 123, 53, 234, 23, 4, 2423, 24, 23, 43, 34, 234, 234, 24},
-                       wantInput: [][]uint64{
-                               []uint64{22, 123, 53},
-                               []uint64{234, 23, 4},
-                               []uint64{2423, 24, 23},
-                               []uint64{43, 34, 234},
-                               []uint64{234, 24, 197},
-                               []uint64{260, 2469, 310},
-                               []uint64{454, 3038},
-                       },
-                       wantOutput: [][]uint64{
-                               []uint64{197},
-                               []uint64{260},
-                               []uint64{2469},
-                               []uint64{310},
-                               []uint64{454},
-                               []uint64{3038},
-                               []uint64{3491},
-                       },
-                       wantUtxo: 3491 * chainTxMergeGas,
-               },
-       }
-
-       acct, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, "testAccount", signers.BIP0044)
-       if err != nil {
-               t.Fatal(err)
-       }
-
-       acp, err := m.CreateAddress(acct.ID, false)
-       if err != nil {
-               t.Fatal(err)
-       }
-
-       for caseIndex, c := range cases {
-               utxos := []*UTXO{}
-               for _, amount := range c.inputUtxo {
-                       utxos = append(utxos, &UTXO{
-                               Amount:         amount * chainTxMergeGas,
-                               AssetID:        *consensus.BTMAssetID,
-                               Address:        acp.Address,
-                               ControlProgram: acp.ControlProgram,
-                       })
-               }
-
-               tpls, gotUtxo, err := m.buildBtmTxChain(utxos, acct.Signer)
-               if err != nil {
-                       t.Fatal(err)
-               }
-
-               for i, tpl := range tpls {
-                       gotInput := []uint64{}
-                       for _, input := range tpl.Transaction.Inputs {
-                               gotInput = append(gotInput, input.Amount()/chainTxMergeGas)
-                       }
-
-                       gotOutput := []uint64{}
-                       for _, output := range tpl.Transaction.Outputs {
-                               gotOutput = append(gotOutput, output.AssetAmount().Amount/chainTxMergeGas)
-                       }
-
-                       if !testutil.DeepEqual(c.wantInput[i], gotInput) {
-                               t.Fatalf("case %d tx %d input got %d want %d", caseIndex, i, gotInput, c.wantInput[i])
-                       }
-                       if !testutil.DeepEqual(c.wantOutput[i], gotOutput) {
-                               t.Fatalf("case %d tx %d output got %d want %d", caseIndex, i, gotOutput, c.wantOutput[i])
-                       }
-               }
-
-               if c.wantUtxo != gotUtxo.Amount {
-                       t.Fatalf("case %d got utxo=%d want utxo=%d", caseIndex, gotUtxo.Amount, c.wantUtxo)
-               }
-       }
-
-}
-
 func TestMergeSpendAction(t *testing.T) {
        testBTM := &bc.AssetID{}
        if err := testBTM.UnmarshalText([]byte("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")); err != nil {
@@ -586,3 +401,66 @@ func TestCalcMergeGas(t *testing.T) {
                }
        }
 }
+
+func TestReserveBtmUtxoChain(t *testing.T) {
+       chainTxUtxoNum = 3
+       utxos := []*UTXO{}
+       m := mockAccountManager(t)
+       for i := uint64(1); i <= 20; i++ {
+               utxo := &UTXO{
+                       OutputID:  bc.Hash{V0: i},
+                       AccountID: "TestAccountID",
+                       AssetID:   *consensus.BTMAssetID,
+                       Amount:    i * chainTxMergeGas,
+               }
+               utxos = append(utxos, utxo)
+
+               m.store.SetStandardUTXO(utxo.OutputID, utxo)
+       }
+
+       cases := []struct {
+               amount uint64
+               want   []uint64
+               err    bool
+       }{
+               {
+                       amount: 1 * chainTxMergeGas,
+                       want:   []uint64{1},
+               },
+               {
+                       amount: 888888 * chainTxMergeGas,
+                       want:   []uint64{},
+                       err:    true,
+               },
+               {
+                       amount: 7 * chainTxMergeGas,
+                       want:   []uint64{4, 3, 1},
+               },
+               {
+                       amount: 15 * chainTxMergeGas,
+                       want:   []uint64{5, 4, 3, 2, 1, 6},
+               },
+               {
+                       amount: 163 * chainTxMergeGas,
+                       want:   []uint64{20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 2, 1, 3},
+               },
+       }
+
+       for i, c := range cases {
+               m.utxoKeeper.expireReservation(time.Unix(999999999, 0))
+               utxos, err := m.reserveBtmUtxoChain(&txbuilder.TemplateBuilder{}, "TestAccountID", c.amount, false)
+
+               if err != nil != c.err {
+                       t.Fatalf("case %d got err %v want err = %v", i, err, c.err)
+               }
+
+               got := []uint64{}
+               for _, utxo := range utxos {
+                       got = append(got, utxo.Amount/chainTxMergeGas)
+               }
+
+               if !testutil.DeepEqual(got, c.want) {
+                       t.Fatalf("case %d got %d want %d", i, got, c.want)
+               }
+       }
+}
index 3562c5e..b3ce30e 100644 (file)
@@ -2,8 +2,6 @@
 package account
 
 import (
-       "encoding/json"
-
        log "github.com/sirupsen/logrus"
 )
 
@@ -27,17 +25,15 @@ func (m *Manager) Backup() (*Image, error) {
                Slice: []*ImageSlice{},
        }
 
-       accountIter := m.db.IteratorPrefix(accountPrefix)
-       defer accountIter.Release()
-       for accountIter.Next() {
-               a := &Account{}
-               if err := json.Unmarshal(accountIter.Value(), a); err != nil {
-                       return nil, err
-               }
+       accounts, err := m.store.ListAccounts("")
+       if err != nil {
+               return nil, err
+       }
 
+       for _, account := range accounts {
                image.Slice = append(image.Slice, &ImageSlice{
-                       Account:       a,
-                       ContractIndex: m.GetContractIndex(a.ID),
+                       Account:       account,
+                       ContractIndex: m.store.GetContractIndex(account.ID),
                })
        }
        return image, nil
@@ -48,29 +44,30 @@ func (m *Manager) Restore(image *Image) error {
        m.accountMu.Lock()
        defer m.accountMu.Unlock()
 
-       storeBatch := m.db.NewBatch()
+       newStore := m.store.InitBatch()
+
        for _, slice := range image.Slice {
-               if existed := m.db.Get(Key(slice.Account.ID)); existed != nil {
+               if _, err := newStore.GetAccountByID(slice.Account.ID); err == nil {
                        log.WithFields(log.Fields{
                                "module": logModule,
                                "alias":  slice.Account.Alias,
                                "id":     slice.Account.ID,
                        }).Warning("skip restore account due to already existed")
                        continue
+               } else if err != ErrFindAccount {
+                       return err
                }
-               if existed := m.db.Get(aliasKey(slice.Account.Alias)); existed != nil {
+
+               if _, err := newStore.GetAccountByAlias(slice.Account.Alias); err == nil {
                        return ErrDuplicateAlias
+               } else if err != ErrFindAccount {
+                       return err
                }
 
-               rawAccount, err := json.Marshal(slice.Account)
-               if err != nil {
-                       return ErrMarshalAccount
+               if err := newStore.SetAccount(slice.Account); err != nil {
+                       return err
                }
-
-               storeBatch.Set(Key(slice.Account.ID), rawAccount)
-               storeBatch.Set(aliasKey(slice.Account.Alias), []byte(slice.Account.ID))
        }
 
-       storeBatch.Write()
-       return nil
+       return newStore.CommitBatch()
 }
index dd96a7e..7e8b3de 100644 (file)
@@ -2,28 +2,8 @@ package account
 
 import (
        "github.com/vapor/blockchain/query"
-       "github.com/vapor/protocol/bc"
 )
 
-const (
-       //UTXOPreFix is StandardUTXOKey prefix
-       UTXOPreFix = "ACU:"
-       //SUTXOPrefix is ContractUTXOKey prefix
-       SUTXOPrefix = "SCU:"
-)
-
-// StandardUTXOKey makes an account unspent outputs key to store
-func StandardUTXOKey(id bc.Hash) []byte {
-       name := id.String()
-       return []byte(UTXOPreFix + name)
-}
-
-// ContractUTXOKey makes a smart contract unspent outputs key to store
-func ContractUTXOKey(id bc.Hash) []byte {
-       name := id.String()
-       return []byte(SUTXOPrefix + name)
-}
-
 //Annotated init an annotated account object
 func Annotated(a *Account) *query.AnnotatedAccount {
        return &query.AnnotatedAccount{
diff --git a/account/store.go b/account/store.go
new file mode 100644 (file)
index 0000000..b502980
--- /dev/null
@@ -0,0 +1,34 @@
+package account
+
+import (
+       "github.com/vapor/crypto/ed25519/chainkd"
+       "github.com/vapor/protocol/bc"
+)
+
+// AccountStore interface contains account storage functions.
+type AccountStore interface {
+       InitBatch() AccountStore
+       CommitBatch() error
+       DeleteAccount(*Account) error
+       DeleteStandardUTXO(bc.Hash)
+       GetAccountByAlias(string) (*Account, error)
+       GetAccountByID(string) (*Account, error)
+       GetAccountIndex([]chainkd.XPub) uint64
+       GetBip44ContractIndex(string, bool) uint64
+       GetCoinbaseArbitrary() []byte
+       GetContractIndex(string) uint64
+       GetControlProgram(bc.Hash) (*CtrlProgram, error)
+       GetMiningAddress() (*CtrlProgram, error)
+       GetUTXO(bc.Hash) (*UTXO, error)
+       ListAccounts(string) ([]*Account, error)
+       ListControlPrograms() ([]*CtrlProgram, error)
+       ListUTXOs() ([]*UTXO, error)
+       SetAccount(*Account) error
+       SetAccountIndex(*Account)
+       SetBip44ContractIndex(string, bool, uint64)
+       SetCoinbaseArbitrary([]byte)
+       SetContractIndex(string, uint64)
+       SetControlProgram(bc.Hash, *CtrlProgram) error
+       SetMiningAddress(*CtrlProgram) error
+       SetStandardUTXO(bc.Hash, *UTXO) error
+}
index ed4d685..4af1a5d 100644 (file)
@@ -3,15 +3,12 @@ package account
 import (
        "bytes"
        "container/list"
-       "encoding/json"
        "sort"
        "sync"
        "sync/atomic"
        "time"
 
        log "github.com/sirupsen/logrus"
-
-       dbm "github.com/vapor/database/leveldb"
        "github.com/vapor/errors"
        "github.com/vapor/protocol/bc"
 )
@@ -56,7 +53,7 @@ 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
+       store         AccountStore
        mtx           sync.RWMutex
        currentHeight func() uint64
 
@@ -65,9 +62,9 @@ type utxoKeeper struct {
        reservations map[uint64]*reservation
 }
 
-func newUtxoKeeper(f func() uint64, walletdb dbm.DB) *utxoKeeper {
+func newUtxoKeeper(f func() uint64, store AccountStore) *utxoKeeper {
        uk := &utxoKeeper{
-               db:            walletdb,
+               store:         store,
                currentHeight: f,
                unconfirmed:   make(map[bc.Hash]*UTXO),
                reserved:      make(map[bc.Hash]uint64),
@@ -216,6 +213,7 @@ func (uk *utxoKeeper) findUtxos(accountID string, assetID *bc.AssetID, useUnconf
                if u.AccountID != accountID || u.AssetID != *assetID || !bytes.Equal(u.Vote, vote) {
                        return
                }
+
                if u.ValidHeight > currentHeight {
                        immatureAmount += u.Amount
                } else {
@@ -223,16 +221,15 @@ func (uk *utxoKeeper) findUtxos(accountID string, assetID *bc.AssetID, useUnconf
                }
        }
 
-       utxoIter := uk.db.IteratorPrefix([]byte(UTXOPreFix))
-       defer utxoIter.Release()
-       for utxoIter.Next() {
-               u := &UTXO{}
-               if err := json.Unmarshal(utxoIter.Value(), u); err != nil {
-                       log.WithFields(log.Fields{"module": logModule, "err": err}).Error("utxoKeeper findUtxos fail on unmarshal utxo")
-                       continue
-               }
-               appendUtxo(u)
+       UTXOs, err := uk.store.ListUTXOs()
+       if err != nil {
+               log.WithFields(log.Fields{"module": logModule, "err": err}).Error("utxoKeeper findUtxos fail on unmarshal utxo")
        }
+
+       for _, UTXO := range UTXOs {
+               appendUtxo(UTXO)
+       }
+
        if !useUnconfirmed {
                return utxos, immatureAmount
        }
@@ -247,15 +244,7 @@ func (uk *utxoKeeper) findUtxo(outHash bc.Hash, useUnconfirmed bool) (*UTXO, err
        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
+       return uk.store.GetUTXO(outHash)
 }
 
 func (uk *utxoKeeper) optUTXOs(utxos []*UTXO, amount uint64) ([]*UTXO, uint64, uint64) {
index 6cd4879..50b2d77 100644 (file)
@@ -2,10 +2,14 @@ package account
 
 import (
        "encoding/json"
+       "io/ioutil"
        "os"
        "testing"
        "time"
 
+       "github.com/golang/groupcache/lru"
+       "github.com/vapor/blockchain/txbuilder"
+       "github.com/vapor/crypto/ed25519/chainkd"
        dbm "github.com/vapor/database/leveldb"
        "github.com/vapor/protocol/bc"
        "github.com/vapor/testutil"
@@ -278,6 +282,8 @@ func TestReserve(t *testing.T) {
                os.RemoveAll("temp")
        }()
 
+       accountStore := newMockAccountStore(testDB)
+
        cases := []struct {
                before        utxoKeeper
                after         utxoKeeper
@@ -288,13 +294,13 @@ func TestReserve(t *testing.T) {
        }{
                {
                        before: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                reserved:      map[bc.Hash]uint64{},
                                reservations:  map[uint64]*reservation{},
                        },
                        after: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                reserved:      map[bc.Hash]uint64{},
                                reservations:  map[uint64]*reservation{},
@@ -304,7 +310,7 @@ func TestReserve(t *testing.T) {
                },
                {
                        before: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x01}): &UTXO{
@@ -317,7 +323,7 @@ func TestReserve(t *testing.T) {
                                reservations: map[uint64]*reservation{},
                        },
                        after: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x01}): &UTXO{
@@ -334,7 +340,7 @@ func TestReserve(t *testing.T) {
                },
                {
                        before: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x01}): &UTXO{
@@ -348,7 +354,7 @@ func TestReserve(t *testing.T) {
                                reservations: map[uint64]*reservation{},
                        },
                        after: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x01}): &UTXO{
@@ -366,7 +372,7 @@ func TestReserve(t *testing.T) {
                },
                {
                        before: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x01}): &UTXO{
@@ -381,7 +387,7 @@ func TestReserve(t *testing.T) {
                                reservations: map[uint64]*reservation{},
                        },
                        after: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x01}): &UTXO{
@@ -400,7 +406,7 @@ func TestReserve(t *testing.T) {
                },
                {
                        before: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x01}): &UTXO{
@@ -413,7 +419,7 @@ func TestReserve(t *testing.T) {
                                reservations: map[uint64]*reservation{},
                        },
                        after: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x01}): &UTXO{
@@ -446,7 +452,7 @@ func TestReserve(t *testing.T) {
                },
                {
                        before: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                nextIndex:     1,
                                unconfirmed: map[bc.Hash]*UTXO{
@@ -472,7 +478,7 @@ func TestReserve(t *testing.T) {
                                reservations: map[uint64]*reservation{},
                        },
                        after: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x01}): &UTXO{
@@ -522,7 +528,7 @@ func TestReserve(t *testing.T) {
                },
                {
                        before: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x01}): &UTXO{
@@ -536,7 +542,7 @@ func TestReserve(t *testing.T) {
                                reservations: map[uint64]*reservation{},
                        },
                        after: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x01}): &UTXO{
@@ -585,6 +591,8 @@ func TestReserveParticular(t *testing.T) {
        testDB := dbm.NewDB("testdb", "leveldb", "temp")
        defer os.RemoveAll("temp")
 
+       accountStore := newMockAccountStore(testDB)
+
        cases := []struct {
                before      utxoKeeper
                after       utxoKeeper
@@ -594,7 +602,7 @@ func TestReserveParticular(t *testing.T) {
        }{
                {
                        before: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x01}): &UTXO{
@@ -609,7 +617,7 @@ func TestReserveParticular(t *testing.T) {
                                reservations: map[uint64]*reservation{},
                        },
                        after: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x01}): &UTXO{
@@ -628,7 +636,7 @@ func TestReserveParticular(t *testing.T) {
                },
                {
                        before: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x01}): &UTXO{
@@ -642,7 +650,7 @@ func TestReserveParticular(t *testing.T) {
                                reservations: map[uint64]*reservation{},
                        },
                        after: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x01}): &UTXO{
@@ -660,7 +668,7 @@ func TestReserveParticular(t *testing.T) {
                },
                {
                        before: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x01}): &UTXO{
@@ -673,7 +681,7 @@ func TestReserveParticular(t *testing.T) {
                                reservations: map[uint64]*reservation{},
                        },
                        after: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x01}): &UTXO{
@@ -743,6 +751,8 @@ func TestFindUtxos(t *testing.T) {
                os.RemoveAll("temp")
        }()
 
+       accountStore := newMockAccountStore(testDB)
+
        cases := []struct {
                uk             utxoKeeper
                dbUtxos        []*UTXO
@@ -753,7 +763,7 @@ func TestFindUtxos(t *testing.T) {
        }{
                {
                        uk: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed:   map[bc.Hash]*UTXO{},
                        },
@@ -764,7 +774,7 @@ func TestFindUtxos(t *testing.T) {
                },
                {
                        uk: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed:   map[bc.Hash]*UTXO{},
                        },
@@ -793,7 +803,7 @@ func TestFindUtxos(t *testing.T) {
                },
                {
                        uk: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed:   map[bc.Hash]*UTXO{},
                        },
@@ -811,7 +821,7 @@ func TestFindUtxos(t *testing.T) {
                },
                {
                        uk: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x01}): &UTXO{
@@ -840,7 +850,7 @@ func TestFindUtxos(t *testing.T) {
                },
                {
                        uk: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x11}): &UTXO{
@@ -874,7 +884,7 @@ func TestFindUtxos(t *testing.T) {
                },
                {
                        uk: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x01}): &UTXO{
@@ -918,7 +928,7 @@ func TestFindUtxos(t *testing.T) {
                },
                {
                        uk: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed:   map[bc.Hash]*UTXO{},
                        },
@@ -946,11 +956,9 @@ func TestFindUtxos(t *testing.T) {
 
        for i, c := range cases {
                for _, u := range c.dbUtxos {
-                       data, err := json.Marshal(u)
-                       if err != nil {
+                       if err := c.uk.store.SetStandardUTXO(u.OutputID, u); err != nil {
                                t.Error(err)
                        }
-                       testDB.Set(StandardUTXOKey(u.OutputID), data)
                }
 
                gotUtxos, immatureAmount := c.uk.findUtxos("testAccount", &bc.AssetID{}, c.useUnconfirmed, c.vote)
@@ -962,16 +970,18 @@ func TestFindUtxos(t *testing.T) {
                }
 
                for _, u := range c.dbUtxos {
-                       testDB.Delete(StandardUTXOKey(u.OutputID))
+                       c.uk.store.DeleteStandardUTXO(u.OutputID)
                }
        }
 }
 
-func TestFindUtxo(t *testing.T) {
+func TestFindUTXO(t *testing.T) {
        currentHeight := func() uint64 { return 9527 }
        testDB := dbm.NewDB("testdb", "leveldb", "temp")
        defer os.RemoveAll("temp")
 
+       accountStore := newMockAccountStore(testDB)
+
        cases := []struct {
                uk             utxoKeeper
                dbUtxos        map[string]*UTXO
@@ -982,7 +992,7 @@ func TestFindUtxo(t *testing.T) {
        }{
                {
                        uk: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed:   map[bc.Hash]*UTXO{},
                        },
@@ -993,7 +1003,7 @@ func TestFindUtxo(t *testing.T) {
                },
                {
                        uk: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x01}): &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
@@ -1007,7 +1017,7 @@ func TestFindUtxo(t *testing.T) {
                },
                {
                        uk: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed: map[bc.Hash]*UTXO{
                                        bc.NewHash([32]byte{0x01}): &UTXO{OutputID: bc.NewHash([32]byte{0x01})},
@@ -1021,7 +1031,7 @@ func TestFindUtxo(t *testing.T) {
                },
                {
                        uk: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed:   map[bc.Hash]*UTXO{},
                        },
@@ -1035,7 +1045,7 @@ func TestFindUtxo(t *testing.T) {
                },
                {
                        uk: utxoKeeper{
-                               db:            testDB,
+                               store:         accountStore,
                                currentHeight: currentHeight,
                                unconfirmed:   map[bc.Hash]*UTXO{},
                        },
@@ -1067,7 +1077,7 @@ func TestFindUtxo(t *testing.T) {
                }
 
                for _, u := range c.dbUtxos {
-                       testDB.Delete(StandardUTXOKey(u.OutputID))
+                       c.uk.store.DeleteStandardUTXO(u.OutputID)
                }
        }
 }
@@ -1300,3 +1310,165 @@ func checkUtxoKeeperEqual(t *testing.T, i int, a, b *utxoKeeper) {
                t.Errorf("case %d: reservations got %v want %v", i, a.reservations, b.reservations)
        }
 }
+
+const (
+       utxoPrefix byte = iota //UTXOPrefix is StandardUTXOKey prefix
+       contractPrefix
+       contractIndexPrefix
+       accountPrefix // AccountPrefix is account ID prefix
+       accountIndexPrefix
+)
+
+// leveldb key prefix
+var (
+       colon               byte = 0x3a
+       accountStore             = []byte("AS:")
+       UTXOPrefix               = append(accountStore, utxoPrefix, colon)
+       ContractPrefix           = append(accountStore, contractPrefix, colon)
+       ContractIndexPrefix      = append(accountStore, contractIndexPrefix, colon)
+       AccountPrefix            = append(accountStore, accountPrefix, colon) // AccountPrefix is account ID prefix
+       AccountIndexPrefix       = append(accountStore, accountIndexPrefix, colon)
+)
+
+const (
+       sutxoPrefix byte = iota //SUTXOPrefix is ContractUTXOKey prefix
+       accountAliasPrefix
+       txPrefix            //TxPrefix is wallet database transactions prefix
+       txIndexPrefix       //TxIndexPrefix is wallet database tx index prefix
+       unconfirmedTxPrefix //UnconfirmedTxPrefix is txpool unconfirmed transactions prefix
+       globalTxIndexPrefix //GlobalTxIndexPrefix is wallet database global tx index prefix
+       walletKey
+       miningAddressKey
+       coinbaseAbKey
+       recoveryKey //recoveryKey key for db store recovery info.
+)
+
+var (
+       walletStore         = []byte("WS:")
+       SUTXOPrefix         = append(walletStore, sutxoPrefix, colon)
+       AccountAliasPrefix  = append(walletStore, accountAliasPrefix, colon)
+       TxPrefix            = append(walletStore, txPrefix, colon)            //TxPrefix is wallet database transactions prefix
+       TxIndexPrefix       = append(walletStore, txIndexPrefix, colon)       //TxIndexPrefix is wallet database tx index prefix
+       UnconfirmedTxPrefix = append(walletStore, unconfirmedTxPrefix, colon) //UnconfirmedTxPrefix is txpool unconfirmed transactions prefix
+       GlobalTxIndexPrefix = append(walletStore, globalTxIndexPrefix, colon) //GlobalTxIndexPrefix is wallet database global tx index prefix
+       WalletKey           = append(walletStore, walletKey)
+       MiningAddressKey    = append(walletStore, miningAddressKey)
+       CoinbaseAbKey       = append(walletStore, coinbaseAbKey)
+       RecoveryKey         = append(walletStore, recoveryKey)
+)
+
+type mockAccountStore struct {
+       db    dbm.DB
+       batch dbm.Batch
+}
+
+// NewAccountStore create new AccountStore.
+func newMockAccountStore(db dbm.DB) *mockAccountStore {
+       return &mockAccountStore{
+               db:    db,
+               batch: nil,
+       }
+}
+
+// StandardUTXOKey makes an account unspent outputs key to store
+func StandardUTXOKey(id bc.Hash) []byte {
+       return append(UTXOPrefix, id.Bytes()...)
+}
+
+// ContractUTXOKey makes a smart contract unspent outputs key to store
+func ContractUTXOKey(id bc.Hash) []byte {
+       return append(SUTXOPrefix, id.Bytes()...)
+}
+
+func (store *mockAccountStore) InitBatch() AccountStore                         { return nil }
+func (store *mockAccountStore) CommitBatch() error                              { return nil }
+func (store *mockAccountStore) DeleteAccount(*Account) error                    { return nil }
+func (store *mockAccountStore) GetAccountByAlias(string) (*Account, error)      { return nil, nil }
+func (store *mockAccountStore) GetAccountByID(string) (*Account, error)         { return nil, nil }
+func (store *mockAccountStore) GetAccountIndex([]chainkd.XPub) uint64           { return 0 }
+func (store *mockAccountStore) GetBip44ContractIndex(string, bool) uint64       { return 0 }
+func (store *mockAccountStore) GetCoinbaseArbitrary() []byte                    { return nil }
+func (store *mockAccountStore) GetContractIndex(string) uint64                  { return 0 }
+func (store *mockAccountStore) GetControlProgram(bc.Hash) (*CtrlProgram, error) { return nil, nil }
+func (store *mockAccountStore) GetMiningAddress() (*CtrlProgram, error)         { return nil, nil }
+func (store *mockAccountStore) ListAccounts(string) ([]*Account, error)         { return nil, nil }
+func (store *mockAccountStore) ListControlPrograms() ([]*CtrlProgram, error)    { return nil, nil }
+func (store *mockAccountStore) SetAccount(*Account) error                       { return nil }
+func (store *mockAccountStore) SetAccountIndex(*Account)                        { return }
+func (store *mockAccountStore) SetBip44ContractIndex(string, bool, uint64)      { return }
+func (store *mockAccountStore) SetCoinbaseArbitrary([]byte)                     { return }
+func (store *mockAccountStore) SetContractIndex(string, uint64)                 { return }
+func (store *mockAccountStore) SetControlProgram(bc.Hash, *CtrlProgram) error   { return nil }
+func (store *mockAccountStore) SetMiningAddress(*CtrlProgram) error             { return nil }
+
+// DeleteStandardUTXO delete utxo by outpu id
+func (store *mockAccountStore) DeleteStandardUTXO(outputID bc.Hash) {
+       if store.batch == nil {
+               store.db.Delete(StandardUTXOKey(outputID))
+       } else {
+               store.batch.Delete(StandardUTXOKey(outputID))
+       }
+}
+
+// GetUTXO get standard utxo by id
+func (store *mockAccountStore) GetUTXO(outid bc.Hash) (*UTXO, error) {
+       u := new(UTXO)
+       if data := store.db.Get(StandardUTXOKey(outid)); data != nil {
+               return u, json.Unmarshal(data, u)
+       }
+       if data := store.db.Get(ContractUTXOKey(outid)); data != nil {
+               return u, json.Unmarshal(data, u)
+       }
+       return nil, ErrMatchUTXO
+}
+
+// ListUTXOs get utxos by accountID
+func (store *mockAccountStore) ListUTXOs() ([]*UTXO, error) {
+       utxoIter := store.db.IteratorPrefix([]byte(UTXOPrefix))
+       defer utxoIter.Release()
+
+       utxos := []*UTXO{}
+       for utxoIter.Next() {
+               utxo := new(UTXO)
+               if err := json.Unmarshal(utxoIter.Value(), utxo); err != nil {
+                       return nil, err
+               }
+               utxos = append(utxos, utxo)
+       }
+       return utxos, nil
+}
+
+// SetStandardUTXO set standard utxo
+func (store *mockAccountStore) SetStandardUTXO(outputID bc.Hash, utxo *UTXO) error {
+       data, err := json.Marshal(utxo)
+       if err != nil {
+               return err
+       }
+       if store.batch == nil {
+               store.db.Set(StandardUTXOKey(outputID), data)
+       } else {
+               store.batch.Set(StandardUTXOKey(outputID), data)
+       }
+       return nil
+}
+
+func mockAccountManager(t *testing.T) *Manager {
+       dirPath, err := ioutil.TempDir(".", "")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(dirPath)
+
+       testDB := dbm.NewDB("testdb", "memdb", dirPath)
+       accountStore := newMockAccountStore(testDB)
+       bestBlockHeight := func() uint64 { return 9527 }
+
+       return &Manager{
+               store:       accountStore,
+               chain:       nil,
+               utxoKeeper:  newUtxoKeeper(bestBlockHeight, accountStore),
+               cache:       lru.New(maxAccountCache),
+               aliasCache:  lru.New(maxAccountCache),
+               delayedACPs: make(map[*txbuilder.TemplateBuilder][]*CtrlProgram),
+       }
+}
diff --git a/database/account_store.go b/database/account_store.go
new file mode 100644 (file)
index 0000000..619dd8d
--- /dev/null
@@ -0,0 +1,436 @@
+package database
+
+import (
+       "encoding/json"
+       "sort"
+       "strings"
+
+       acc "github.com/vapor/account"
+       "github.com/vapor/blockchain/signers"
+       "github.com/vapor/common"
+       "github.com/vapor/crypto/ed25519/chainkd"
+       "github.com/vapor/crypto/sha3pool"
+       dbm "github.com/vapor/database/leveldb"
+       "github.com/vapor/errors"
+       "github.com/vapor/protocol/bc"
+)
+
+const (
+       utxoPrefix byte = iota //UTXOPrefix is StandardUTXOKey prefix
+       contractPrefix
+       contractIndexPrefix
+       accountPrefix // AccountPrefix is account ID prefix
+       accountIndexPrefix
+)
+
+// leveldb key prefix
+var (
+       accountStore        = []byte("AS:")
+       UTXOPrefix          = append(accountStore, utxoPrefix, colon)
+       ContractPrefix      = append(accountStore, contractPrefix, colon)
+       ContractIndexPrefix = append(accountStore, contractIndexPrefix, colon)
+       AccountPrefix       = append(accountStore, accountPrefix, colon) // AccountPrefix is account ID prefix
+       AccountIndexPrefix  = append(accountStore, accountIndexPrefix, colon)
+)
+
+func accountIndexKey(xpubs []chainkd.XPub) []byte {
+       var hash [32]byte
+       var xPubs []byte
+       cpy := append([]chainkd.XPub{}, xpubs[:]...)
+       sort.Sort(signers.SortKeys(cpy))
+       for _, xpub := range cpy {
+               xPubs = append(xPubs, xpub[:]...)
+       }
+       sha3pool.Sum256(hash[:], xPubs)
+       return append(AccountIndexPrefix, hash[:]...)
+}
+
+func Bip44ContractIndexKey(accountID string, change bool) []byte {
+       key := append(ContractIndexPrefix, []byte(accountID)...)
+       if change {
+               return append(key, 0x01)
+       }
+       return append(key, 0x00)
+}
+
+// ContractKey account control promgram store prefix
+func ContractKey(hash bc.Hash) []byte {
+       return append(ContractPrefix, hash.Bytes()...)
+}
+
+// AccountIDKey account id store prefix
+func AccountIDKey(accountID string) []byte {
+       return append(AccountPrefix, []byte(accountID)...)
+}
+
+// StandardUTXOKey makes an account unspent outputs key to store
+func StandardUTXOKey(id bc.Hash) []byte {
+       return append(UTXOPrefix, id.Bytes()...)
+}
+
+func accountAliasKey(name string) []byte {
+       return append(AccountAliasPrefix, []byte(name)...)
+}
+
+// AccountStore satisfies AccountStore interface.
+type AccountStore struct {
+       db    dbm.DB
+       batch dbm.Batch
+}
+
+// NewAccountStore create new AccountStore.
+func NewAccountStore(db dbm.DB) *AccountStore {
+       return &AccountStore{
+               db:    db,
+               batch: nil,
+       }
+}
+
+// InitBatch initial new account store
+func (store *AccountStore) InitBatch() acc.AccountStore {
+       newStore := NewAccountStore(store.db)
+       newStore.batch = newStore.db.NewBatch()
+       return newStore
+}
+
+// CommitBatch commit batch
+func (store *AccountStore) CommitBatch() error {
+       if store.batch == nil {
+               return errors.New("AccountStore commit fail, store batch is nil.")
+       }
+       store.batch.Write()
+       store.batch = nil
+       return nil
+}
+
+// DeleteAccount set account account ID, account alias and raw account.
+func (store *AccountStore) DeleteAccount(account *acc.Account) error {
+       batch := store.db.NewBatch()
+       if store.batch != nil {
+               batch = store.batch
+       }
+
+       // delete account utxos
+       store.deleteAccountUTXOs(account.ID, batch)
+
+       // delete account control program
+       if err := store.deleteAccountControlPrograms(account.ID, batch); err != nil {
+               return err
+       }
+
+       // delete bip44 contract index
+       batch.Delete(Bip44ContractIndexKey(account.ID, false))
+       batch.Delete(Bip44ContractIndexKey(account.ID, true))
+
+       // delete contract index
+       batch.Delete(contractIndexKey(account.ID))
+
+       // delete account id
+       batch.Delete(AccountIDKey(account.ID))
+       batch.Delete(accountAliasKey(account.Alias))
+       if store.batch == nil {
+               batch.Write()
+       }
+       return nil
+}
+
+// deleteAccountUTXOs delete account utxos by accountID
+func (store *AccountStore) deleteAccountUTXOs(accountID string, batch dbm.Batch) error {
+       accountUtxoIter := store.db.IteratorPrefix(UTXOPrefix)
+       defer accountUtxoIter.Release()
+
+       for accountUtxoIter.Next() {
+               accountUtxo := new(acc.UTXO)
+               if err := json.Unmarshal(accountUtxoIter.Value(), accountUtxo); err != nil {
+                       return err
+               }
+
+               if accountID == accountUtxo.AccountID {
+                       batch.Delete(StandardUTXOKey(accountUtxo.OutputID))
+               }
+       }
+
+       return nil
+}
+
+// deleteAccountControlPrograms deletes account control program
+func (store *AccountStore) deleteAccountControlPrograms(accountID string, batch dbm.Batch) error {
+       cps, err := store.ListControlPrograms()
+       if err != nil {
+               return err
+       }
+
+       var hash [32]byte
+       for _, cp := range cps {
+               if cp.AccountID == accountID {
+                       sha3pool.Sum256(hash[:], cp.ControlProgram)
+                       batch.Delete(ContractKey(bc.NewHash(hash)))
+               }
+       }
+       return nil
+}
+
+// DeleteStandardUTXO delete utxo by outpu id
+func (store *AccountStore) DeleteStandardUTXO(outputID bc.Hash) {
+       if store.batch == nil {
+               store.db.Delete(StandardUTXOKey(outputID))
+       } else {
+               store.batch.Delete(StandardUTXOKey(outputID))
+       }
+}
+
+// GetAccountByAlias get account by account alias
+func (store *AccountStore) GetAccountByAlias(accountAlias string) (*acc.Account, error) {
+       accountID := store.db.Get(accountAliasKey(accountAlias))
+       if accountID == nil {
+               return nil, acc.ErrFindAccount
+       }
+
+       return store.GetAccountByID(string(accountID))
+}
+
+// GetAccountByID get account by accountID
+func (store *AccountStore) GetAccountByID(accountID string) (*acc.Account, error) {
+       rawAccount := store.db.Get(AccountIDKey(accountID))
+       if rawAccount == nil {
+               return nil, acc.ErrFindAccount
+       }
+
+       account := new(acc.Account)
+       if err := json.Unmarshal(rawAccount, account); err != nil {
+               return nil, err
+       }
+
+       return account, nil
+}
+
+// GetAccountIndex get account index by account xpubs
+func (store *AccountStore) GetAccountIndex(xpubs []chainkd.XPub) uint64 {
+       currentIndex := uint64(0)
+       if rawIndexBytes := store.db.Get(accountIndexKey(xpubs)); rawIndexBytes != nil {
+               currentIndex = common.BytesToUnit64(rawIndexBytes)
+       }
+       return currentIndex
+}
+
+// GetBip44ContractIndex get bip44 contract index
+func (store *AccountStore) GetBip44ContractIndex(accountID string, change bool) uint64 {
+       index := uint64(0)
+       if rawIndexBytes := store.db.Get(Bip44ContractIndexKey(accountID, change)); rawIndexBytes != nil {
+               index = common.BytesToUnit64(rawIndexBytes)
+       }
+       return index
+}
+
+// GetCoinbaseArbitrary get coinbase arbitrary
+func (store *AccountStore) GetCoinbaseArbitrary() []byte {
+       return store.db.Get(CoinbaseAbKey)
+}
+
+// GetContractIndex get contract index
+func (store *AccountStore) GetContractIndex(accountID string) uint64 {
+       index := uint64(0)
+       if rawIndexBytes := store.db.Get(contractIndexKey(accountID)); rawIndexBytes != nil {
+               index = common.BytesToUnit64(rawIndexBytes)
+       }
+       return index
+}
+
+// GetControlProgram get control program
+func (store *AccountStore) GetControlProgram(hash bc.Hash) (*acc.CtrlProgram, error) {
+       rawProgram := store.db.Get(ContractKey(hash))
+       if rawProgram == nil {
+               return nil, acc.ErrFindCtrlProgram
+       }
+
+       cp := new(acc.CtrlProgram)
+       if err := json.Unmarshal(rawProgram, cp); err != nil {
+               return nil, err
+       }
+
+       return cp, nil
+}
+
+// GetMiningAddress get mining address
+func (store *AccountStore) GetMiningAddress() (*acc.CtrlProgram, error) {
+       rawCP := store.db.Get(MiningAddressKey)
+       if rawCP == nil {
+               return nil, acc.ErrFindMiningAddress
+       }
+
+       cp := new(acc.CtrlProgram)
+       if err := json.Unmarshal(rawCP, cp); err != nil {
+               return nil, err
+       }
+
+       return cp, nil
+}
+
+// GetUTXO get standard utxo by id
+func (store *AccountStore) GetUTXO(outid bc.Hash) (*acc.UTXO, error) {
+       u := new(acc.UTXO)
+       if data := store.db.Get(StandardUTXOKey(outid)); data != nil {
+               return u, json.Unmarshal(data, u)
+       }
+
+       if data := store.db.Get(ContractUTXOKey(outid)); data != nil {
+               return u, json.Unmarshal(data, u)
+       }
+
+       return nil, acc.ErrMatchUTXO
+}
+
+// ListAccounts get all accounts which name prfix is id.
+func (store *AccountStore) ListAccounts(id string) ([]*acc.Account, error) {
+       accounts := []*acc.Account{}
+       accountIter := store.db.IteratorPrefix(AccountIDKey(strings.TrimSpace(id)))
+       defer accountIter.Release()
+
+       for accountIter.Next() {
+               account := new(acc.Account)
+               if err := json.Unmarshal(accountIter.Value(), account); err != nil {
+                       return nil, err
+               }
+
+               accounts = append(accounts, account)
+       }
+       return accounts, nil
+}
+
+// ListControlPrograms get all local control programs
+func (store *AccountStore) ListControlPrograms() ([]*acc.CtrlProgram, error) {
+       cps := []*acc.CtrlProgram{}
+       cpIter := store.db.IteratorPrefix(ContractPrefix)
+       defer cpIter.Release()
+
+       for cpIter.Next() {
+               cp := new(acc.CtrlProgram)
+               if err := json.Unmarshal(cpIter.Value(), cp); err != nil {
+                       return nil, err
+               }
+
+               cps = append(cps, cp)
+       }
+       return cps, nil
+}
+
+// ListUTXOs get utxos by accountID
+func (store *AccountStore) ListUTXOs() ([]*acc.UTXO, error) {
+       utxoIter := store.db.IteratorPrefix(UTXOPrefix)
+       defer utxoIter.Release()
+
+       utxos := []*acc.UTXO{}
+       for utxoIter.Next() {
+               utxo := new(acc.UTXO)
+               if err := json.Unmarshal(utxoIter.Value(), utxo); err != nil {
+                       return nil, err
+               }
+
+               utxos = append(utxos, utxo)
+       }
+       return utxos, nil
+}
+
+// SetAccount set account account ID, account alias and raw account.
+func (store *AccountStore) SetAccount(account *acc.Account) error {
+       rawAccount, err := json.Marshal(account)
+       if err != nil {
+               return acc.ErrMarshalAccount
+       }
+
+       batch := store.db.NewBatch()
+       if store.batch != nil {
+               batch = store.batch
+       }
+
+       batch.Set(AccountIDKey(account.ID), rawAccount)
+       batch.Set(accountAliasKey(account.Alias), []byte(account.ID))
+
+       if store.batch == nil {
+               batch.Write()
+       }
+       return nil
+}
+
+// SetAccountIndex update account index
+func (store *AccountStore) SetAccountIndex(account *acc.Account) {
+       currentIndex := store.GetAccountIndex(account.XPubs)
+       if account.KeyIndex > currentIndex {
+               if store.batch == nil {
+                       store.db.Set(accountIndexKey(account.XPubs), common.Unit64ToBytes(account.KeyIndex))
+               } else {
+                       store.batch.Set(accountIndexKey(account.XPubs), common.Unit64ToBytes(account.KeyIndex))
+               }
+       }
+}
+
+// SetBip44ContractIndex set contract index
+func (store *AccountStore) SetBip44ContractIndex(accountID string, change bool, index uint64) {
+       if store.batch == nil {
+               store.db.Set(Bip44ContractIndexKey(accountID, change), common.Unit64ToBytes(index))
+       } else {
+               store.batch.Set(Bip44ContractIndexKey(accountID, change), common.Unit64ToBytes(index))
+       }
+}
+
+// SetCoinbaseArbitrary set coinbase arbitrary
+func (store *AccountStore) SetCoinbaseArbitrary(arbitrary []byte) {
+       if store.batch == nil {
+               store.db.Set(CoinbaseAbKey, arbitrary)
+       } else {
+               store.batch.Set(CoinbaseAbKey, arbitrary)
+       }
+}
+
+// SetContractIndex set contract index
+func (store *AccountStore) SetContractIndex(accountID string, index uint64) {
+       if store.batch == nil {
+               store.db.Set(contractIndexKey(accountID), common.Unit64ToBytes(index))
+       } else {
+               store.batch.Set(contractIndexKey(accountID), common.Unit64ToBytes(index))
+       }
+}
+
+// SetControlProgram set raw program
+func (store *AccountStore) SetControlProgram(hash bc.Hash, program *acc.CtrlProgram) error {
+       accountCP, err := json.Marshal(program)
+       if err != nil {
+               return err
+       }
+       if store.batch == nil {
+               store.db.Set(ContractKey(hash), accountCP)
+       } else {
+               store.batch.Set(ContractKey(hash), accountCP)
+       }
+       return nil
+}
+
+// SetMiningAddress set mining address
+func (store *AccountStore) SetMiningAddress(program *acc.CtrlProgram) error {
+       rawProgram, err := json.Marshal(program)
+       if err != nil {
+               return err
+       }
+
+       if store.batch == nil {
+               store.db.Set(MiningAddressKey, rawProgram)
+       } else {
+               store.batch.Set(MiningAddressKey, rawProgram)
+       }
+       return nil
+}
+
+// SetStandardUTXO set standard utxo
+func (store *AccountStore) SetStandardUTXO(outputID bc.Hash, utxo *acc.UTXO) error {
+       data, err := json.Marshal(utxo)
+       if err != nil {
+               return err
+       }
+
+       if store.batch == nil {
+               store.db.Set(StandardUTXOKey(outputID), data)
+       } else {
+               store.batch.Set(StandardUTXOKey(outputID), data)
+       }
+       return nil
+}
diff --git a/database/wallet_store.go b/database/wallet_store.go
new file mode 100644 (file)
index 0000000..85eb2e1
--- /dev/null
@@ -0,0 +1,500 @@
+package database
+
+import (
+       "encoding/binary"
+       "encoding/hex"
+       "encoding/json"
+       "fmt"
+
+       log "github.com/sirupsen/logrus"
+
+       acc "github.com/vapor/account"
+       "github.com/vapor/asset"
+       "github.com/vapor/blockchain/query"
+       dbm "github.com/vapor/database/leveldb"
+       "github.com/vapor/errors"
+       "github.com/vapor/protocol/bc"
+       "github.com/vapor/wallet"
+)
+
+const (
+       sutxoPrefix byte = iota //SUTXOPrefix is ContractUTXOKey prefix
+       accountAliasPrefix
+       txPrefix            //TxPrefix is wallet database transactions prefix
+       txIndexPrefix       //TxIndexPrefix is wallet database tx index prefix
+       unconfirmedTxPrefix //UnconfirmedTxPrefix is txpool unconfirmed transactions prefix
+       globalTxIndexPrefix //GlobalTxIndexPrefix is wallet database global tx index prefix
+       walletKey
+       miningAddressKey
+       coinbaseAbKey
+       recoveryKey //recoveryKey key for db store recovery info.
+)
+
+var (
+       walletStore         = []byte("WS:")
+       SUTXOPrefix         = append(walletStore, sutxoPrefix, colon)
+       AccountAliasPrefix  = append(walletStore, accountAliasPrefix, colon)
+       TxPrefix            = append(walletStore, txPrefix, colon)            //TxPrefix is wallet database transactions prefix
+       TxIndexPrefix       = append(walletStore, txIndexPrefix, colon)       //TxIndexPrefix is wallet database tx index prefix
+       UnconfirmedTxPrefix = append(walletStore, unconfirmedTxPrefix, colon) //UnconfirmedTxPrefix is txpool unconfirmed transactions prefix
+       GlobalTxIndexPrefix = append(walletStore, globalTxIndexPrefix, colon) //GlobalTxIndexPrefix is wallet database global tx index prefix
+       WalletKey           = append(walletStore, walletKey)
+       MiningAddressKey    = append(walletStore, miningAddressKey)
+       CoinbaseAbKey       = append(walletStore, coinbaseAbKey)
+       RecoveryKey         = append(walletStore, recoveryKey)
+)
+
+// ContractUTXOKey makes a smart contract unspent outputs key to store
+func ContractUTXOKey(id bc.Hash) []byte {
+       return append(SUTXOPrefix, id.Bytes()...)
+}
+
+func calcDeleteKey(blockHeight uint64) []byte {
+       return []byte(fmt.Sprintf("%s%016x", TxPrefix, blockHeight))
+}
+
+func calcTxIndexKey(txID string) []byte {
+       return append(TxIndexPrefix, []byte(txID)...)
+}
+
+func calcAnnotatedKey(formatKey string) []byte {
+       return append(TxPrefix, []byte(formatKey)...)
+}
+
+func calcUnconfirmedTxKey(formatKey string) []byte {
+       return append(UnconfirmedTxPrefix, []byte(formatKey)...)
+}
+
+func CalcGlobalTxIndexKey(txID string) []byte {
+       return append(GlobalTxIndexPrefix, []byte(txID)...)
+}
+
+func CalcGlobalTxIndex(blockHash *bc.Hash, position uint64) []byte {
+       txIdx := make([]byte, 40)
+       copy(txIdx[:32], blockHash.Bytes())
+       binary.BigEndian.PutUint64(txIdx[32:], position)
+       return txIdx
+}
+
+func formatKey(blockHeight uint64, position uint32) string {
+       return fmt.Sprintf("%016x%08x", blockHeight, position)
+}
+
+func contractIndexKey(accountID string) []byte {
+       return append(ContractIndexPrefix, []byte(accountID)...)
+}
+
+// WalletStore store wallet using leveldb
+type WalletStore struct {
+       db    dbm.DB
+       batch dbm.Batch
+}
+
+// NewWalletStore create new WalletStore struct
+func NewWalletStore(db dbm.DB) *WalletStore {
+       return &WalletStore{
+               db:    db,
+               batch: nil,
+       }
+}
+
+// InitBatch initial new wallet store
+func (store *WalletStore) InitBatch() wallet.WalletStore {
+       newStore := NewWalletStore(store.db)
+       newStore.batch = newStore.db.NewBatch()
+       return newStore
+}
+
+// CommitBatch commit batch
+func (store *WalletStore) CommitBatch() error {
+       if store.batch == nil {
+               return errors.New("WalletStore commit fail, store batch is nil.")
+       }
+
+       store.batch.Write()
+       store.batch = nil
+       return nil
+}
+
+// DeleteContractUTXO delete contract utxo by outputID
+func (store *WalletStore) DeleteContractUTXO(outputID bc.Hash) {
+       if store.batch == nil {
+               store.db.Delete(ContractUTXOKey(outputID))
+       } else {
+               store.batch.Delete(ContractUTXOKey(outputID))
+       }
+}
+
+// DeleteRecoveryStatus delete recovery status
+func (store *WalletStore) DeleteRecoveryStatus() {
+       if store.batch == nil {
+               store.db.Delete(RecoveryKey)
+       } else {
+               store.batch.Delete(RecoveryKey)
+       }
+}
+
+// DeleteTransactions delete transactions when orphan block rollback
+func (store *WalletStore) DeleteTransactions(height uint64) {
+       batch := store.db.NewBatch()
+       if store.batch != nil {
+               batch = store.batch
+       }
+       txIter := store.db.IteratorPrefix(calcDeleteKey(height))
+       defer txIter.Release()
+
+       for txIter.Next() {
+               tmpTx := new(query.AnnotatedTx)
+               if err := json.Unmarshal(txIter.Value(), tmpTx); err == nil {
+                       batch.Delete(calcTxIndexKey(tmpTx.ID.String()))
+               } else {
+                       log.WithFields(log.Fields{"module": logModule, "err": err}).Warning("fail on DeleteTransactions.")
+               }
+               batch.Delete(txIter.Key())
+       }
+       if store.batch == nil {
+               batch.Write()
+       }
+}
+
+// DeleteUnconfirmedTransaction delete unconfirmed tx by txID
+func (store *WalletStore) DeleteUnconfirmedTransaction(txID string) {
+       if store.batch == nil {
+               store.db.Delete(calcUnconfirmedTxKey(txID))
+       } else {
+               store.batch.Delete(calcUnconfirmedTxKey(txID))
+       }
+}
+
+// DeleteWalletTransactions delete all txs in wallet
+func (store *WalletStore) DeleteWalletTransactions() {
+       batch := store.db.NewBatch()
+       if store.batch != nil {
+               batch = store.batch
+       }
+       txIter := store.db.IteratorPrefix(TxPrefix)
+       defer txIter.Release()
+
+       for txIter.Next() {
+               batch.Delete(txIter.Key())
+       }
+
+       txIndexIter := store.db.IteratorPrefix(TxIndexPrefix)
+       defer txIndexIter.Release()
+
+       for txIndexIter.Next() {
+               batch.Delete(txIndexIter.Key())
+       }
+       if store.batch == nil {
+               batch.Write()
+       }
+}
+
+// DeleteWalletUTXOs delete all txs in wallet
+func (store *WalletStore) DeleteWalletUTXOs() {
+       batch := store.db.NewBatch()
+       if store.batch != nil {
+               batch = store.batch
+       }
+
+       ruIter := store.db.IteratorPrefix(UTXOPrefix)
+       defer ruIter.Release()
+
+       for ruIter.Next() {
+               batch.Delete(ruIter.Key())
+       }
+
+       suIter := store.db.IteratorPrefix(SUTXOPrefix)
+       defer suIter.Release()
+
+       for suIter.Next() {
+               batch.Delete(suIter.Key())
+       }
+       if store.batch == nil {
+               batch.Write()
+       }
+}
+
+// GetAsset get asset by assetID
+func (store *WalletStore) GetAsset(assetID *bc.AssetID) (*asset.Asset, error) {
+       definitionByte := store.db.Get(asset.ExtAssetKey(assetID))
+       if definitionByte == nil {
+               return nil, wallet.ErrGetAsset
+       }
+
+       definitionMap := make(map[string]interface{})
+       if err := json.Unmarshal(definitionByte, &definitionMap); err != nil {
+               return nil, err
+       }
+
+       alias := assetID.String()
+       externalAsset := &asset.Asset{
+               AssetID:           *assetID,
+               Alias:             &alias,
+               DefinitionMap:     definitionMap,
+               RawDefinitionByte: definitionByte,
+       }
+       return externalAsset, nil
+}
+
+// GetGlobalTransactionIndex get global tx by txID
+func (store *WalletStore) GetGlobalTransactionIndex(txID string) []byte {
+       return store.db.Get(CalcGlobalTxIndexKey(txID))
+}
+
+// GetStandardUTXO get standard utxo by id
+func (store *WalletStore) GetStandardUTXO(outid bc.Hash) (*acc.UTXO, error) {
+       rawUTXO := store.db.Get(StandardUTXOKey(outid))
+       if rawUTXO == nil {
+               return nil, fmt.Errorf("failed get standard UTXO, outputID: %s ", outid.String())
+       }
+
+       UTXO := new(acc.UTXO)
+       if err := json.Unmarshal(rawUTXO, UTXO); err != nil {
+               return nil, err
+       }
+
+       return UTXO, nil
+}
+
+// GetTransaction get tx by txid
+func (store *WalletStore) GetTransaction(txID string) (*query.AnnotatedTx, error) {
+       formatKey := store.db.Get(calcTxIndexKey(txID))
+       if formatKey == nil {
+               return nil, wallet.ErrAccntTxIDNotFound
+       }
+
+       rawTx := store.db.Get(calcAnnotatedKey(string(formatKey)))
+       tx := new(query.AnnotatedTx)
+       if err := json.Unmarshal(rawTx, tx); err != nil {
+               return nil, err
+       }
+
+       return tx, nil
+}
+
+// GetUnconfirmedTransaction get unconfirmed tx by txID
+func (store *WalletStore) GetUnconfirmedTransaction(txID string) (*query.AnnotatedTx, error) {
+       rawUnconfirmedTx := store.db.Get(calcUnconfirmedTxKey(txID))
+       if rawUnconfirmedTx == nil {
+               return nil, fmt.Errorf("failed get unconfirmed tx, txID: %s ", txID)
+       }
+
+       tx := new(query.AnnotatedTx)
+       if err := json.Unmarshal(rawUnconfirmedTx, tx); err != nil {
+               return nil, err
+       }
+
+       return tx, nil
+}
+
+// GetRecoveryStatus delete recovery status
+func (store *WalletStore) GetRecoveryStatus() (*wallet.RecoveryState, error) {
+       rawStatus := store.db.Get(RecoveryKey)
+       if rawStatus == nil {
+               return nil, wallet.ErrGetRecoveryStatus
+       }
+
+       state := new(wallet.RecoveryState)
+       if err := json.Unmarshal(rawStatus, state); err != nil {
+               return nil, err
+       }
+
+       return state, nil
+}
+
+// GetWalletInfo get wallet information
+func (store *WalletStore) GetWalletInfo() (*wallet.StatusInfo, error) {
+       rawStatus := store.db.Get(WalletKey)
+       if rawStatus == nil {
+               return nil, wallet.ErrGetWalletStatusInfo
+       }
+
+       status := new(wallet.StatusInfo)
+       if err := json.Unmarshal(rawStatus, status); err != nil {
+               return nil, err
+       }
+
+       return status, nil
+}
+
+// ListAccountUTXOs get all account unspent outputs
+func (store *WalletStore) ListAccountUTXOs(id string, isSmartContract bool) ([]*acc.UTXO, error) {
+       prefix := UTXOPrefix
+       if isSmartContract {
+               prefix = SUTXOPrefix
+       }
+
+       idBytes, err := hex.DecodeString(id)
+       if err != nil {
+               return nil, err
+       }
+
+       accountUtxoIter := store.db.IteratorPrefix(append(prefix, idBytes...))
+       defer accountUtxoIter.Release()
+
+       confirmedUTXOs := []*acc.UTXO{}
+       for accountUtxoIter.Next() {
+               utxo := new(acc.UTXO)
+               if err := json.Unmarshal(accountUtxoIter.Value(), utxo); err != nil {
+                       return nil, err
+               }
+
+               confirmedUTXOs = append(confirmedUTXOs, utxo)
+       }
+       return confirmedUTXOs, nil
+}
+
+func (store *WalletStore) ListTransactions(accountID string, StartTxID string, count uint, unconfirmed bool) ([]*query.AnnotatedTx, error) {
+       annotatedTxs := []*query.AnnotatedTx{}
+       var startKey []byte
+       preFix := TxPrefix
+
+       if StartTxID != "" {
+               if unconfirmed {
+                       startKey = calcUnconfirmedTxKey(StartTxID)
+               } else {
+                       formatKey := store.db.Get(calcTxIndexKey(StartTxID))
+                       if formatKey == nil {
+                               return nil, wallet.ErrAccntTxIDNotFound
+                       }
+
+                       startKey = calcAnnotatedKey(string(formatKey))
+               }
+       }
+
+       if unconfirmed {
+               preFix = UnconfirmedTxPrefix
+       }
+
+       itr := store.db.IteratorPrefixWithStart(preFix, startKey, true)
+       defer itr.Release()
+
+       for txNum := count; itr.Next() && txNum > 0; {
+               annotatedTx := new(query.AnnotatedTx)
+               if err := json.Unmarshal(itr.Value(), annotatedTx); err != nil {
+                       return nil, err
+               }
+
+               annotatedTxs = append(annotatedTxs, annotatedTx)
+               txNum--
+       }
+
+       return annotatedTxs, nil
+}
+
+// ListUnconfirmedTransactions get all unconfirmed txs
+func (store *WalletStore) ListUnconfirmedTransactions() ([]*query.AnnotatedTx, error) {
+       annotatedTxs := []*query.AnnotatedTx{}
+       txIter := store.db.IteratorPrefix(UnconfirmedTxPrefix)
+       defer txIter.Release()
+
+       for txIter.Next() {
+               annotatedTx := new(query.AnnotatedTx)
+               if err := json.Unmarshal(txIter.Value(), annotatedTx); err != nil {
+                       return nil, err
+               }
+
+               annotatedTxs = append(annotatedTxs, annotatedTx)
+       }
+       return annotatedTxs, nil
+}
+
+// SetAssetDefinition set assetID and definition
+func (store *WalletStore) SetAssetDefinition(assetID *bc.AssetID, definition []byte) {
+       if store.batch == nil {
+               store.db.Set(asset.ExtAssetKey(assetID), definition)
+       } else {
+               store.batch.Set(asset.ExtAssetKey(assetID), definition)
+       }
+}
+
+// SetContractUTXO set standard utxo
+func (store *WalletStore) SetContractUTXO(outputID bc.Hash, utxo *acc.UTXO) error {
+       data, err := json.Marshal(utxo)
+       if err != nil {
+               return err
+       }
+
+       if store.batch == nil {
+               store.db.Set(ContractUTXOKey(outputID), data)
+       } else {
+               store.batch.Set(ContractUTXOKey(outputID), data)
+       }
+       return nil
+}
+
+// SetGlobalTransactionIndex set global tx index by blockhash and position
+func (store *WalletStore) SetGlobalTransactionIndex(globalTxID string, blockHash *bc.Hash, position uint64) {
+       if store.batch == nil {
+               store.db.Set(CalcGlobalTxIndexKey(globalTxID), CalcGlobalTxIndex(blockHash, position))
+       } else {
+               store.batch.Set(CalcGlobalTxIndexKey(globalTxID), CalcGlobalTxIndex(blockHash, position))
+       }
+}
+
+// SetRecoveryStatus set recovery status
+func (store *WalletStore) SetRecoveryStatus(recoveryState *wallet.RecoveryState) error {
+       rawStatus, err := json.Marshal(recoveryState)
+       if err != nil {
+               return err
+       }
+
+       if store.batch == nil {
+               store.db.Set(RecoveryKey, rawStatus)
+       } else {
+               store.batch.Set(RecoveryKey, rawStatus)
+       }
+       return nil
+}
+
+// SetTransaction set raw transaction by block height and tx position
+func (store *WalletStore) SetTransaction(height uint64, tx *query.AnnotatedTx) error {
+       batch := store.db.NewBatch()
+       if store.batch != nil {
+               batch = store.batch
+       }
+
+       rawTx, err := json.Marshal(tx)
+       if err != nil {
+               return err
+       }
+
+       batch.Set(calcAnnotatedKey(formatKey(height, tx.Position)), rawTx)
+       batch.Set(calcTxIndexKey(tx.ID.String()), []byte(formatKey(height, tx.Position)))
+
+       if store.batch == nil {
+               batch.Write()
+       }
+       return nil
+}
+
+// SetUnconfirmedTransaction set unconfirmed tx by txID
+func (store *WalletStore) SetUnconfirmedTransaction(txID string, tx *query.AnnotatedTx) error {
+       rawTx, err := json.Marshal(tx)
+       if err != nil {
+               return err
+       }
+
+       if store.batch == nil {
+               store.db.Set(calcUnconfirmedTxKey(txID), rawTx)
+       } else {
+               store.batch.Set(calcUnconfirmedTxKey(txID), rawTx)
+       }
+       return nil
+}
+
+// SetWalletInfo get wallet information
+func (store *WalletStore) SetWalletInfo(status *wallet.StatusInfo) error {
+       rawWallet, err := json.Marshal(status)
+       if err != nil {
+               return err
+       }
+
+       if store.batch == nil {
+               store.db.Set(WalletKey, rawWallet)
+       } else {
+               store.batch.Set(WalletKey, rawWallet)
+       }
+       return nil
+}
diff --git a/database/wallet_store_test.go b/database/wallet_store_test.go
new file mode 100644 (file)
index 0000000..f2aa943
--- /dev/null
@@ -0,0 +1,44 @@
+package database
+
+import (
+       "io/ioutil"
+       "os"
+       "reflect"
+       "testing"
+
+       "github.com/vapor/blockchain/pseudohsm"
+       "github.com/vapor/crypto/ed25519/chainkd"
+)
+
+func TestAccountIndexKey(t *testing.T) {
+       dirPath, err := ioutil.TempDir(".", "TestAccount")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(dirPath)
+
+       hsm, err := pseudohsm.New(dirPath)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       xpub1, _, err := hsm.XCreate("TestAccountIndex1", "password", "en")
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       xpub2, _, err := hsm.XCreate("TestAccountIndex2", "password", "en")
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       xpubs1 := []chainkd.XPub{xpub1.XPub, xpub2.XPub}
+       xpubs2 := []chainkd.XPub{xpub2.XPub, xpub1.XPub}
+       if !reflect.DeepEqual(accountIndexKey(xpubs1), accountIndexKey(xpubs2)) {
+               t.Fatal("accountIndexKey test err")
+       }
+
+       if reflect.DeepEqual(xpubs1, xpubs2) {
+               t.Fatal("accountIndexKey test err")
+       }
+}
index d82c155..764edf4 100644 (file)
@@ -110,9 +110,11 @@ func NewNode(config *cfg.Config) *Node {
 
        if !config.Wallet.Disable {
                walletDB := dbm.NewDB("wallet", config.DBBackend, config.DBDir())
-               accounts = account.NewManager(walletDB, chain)
+               walletStore := database.NewWalletStore(walletDB)
+               accountStore := database.NewAccountStore(walletDB)
+               accounts = account.NewManager(accountStore, chain)
                assets = asset.NewRegistry(walletDB, chain)
-               wallet, err = w.NewWallet(walletDB, accounts, assets, hsm, chain, dispatcher, config.Wallet.TxIndex)
+               wallet, err = w.NewWallet(walletStore, accounts, assets, hsm, chain, dispatcher, config.Wallet.TxIndex)
                if err != nil {
                        log.WithFields(log.Fields{"module": logModule, "error": err}).Error("init NewWallet")
                }
similarity index 52%
rename from account/accounts_test.go
rename to test/accounts_test.go
index d2722bb..ba95631 100644 (file)
@@ -1,13 +1,12 @@
-package account
+package test
 
 import (
        "io/ioutil"
        "os"
-       "reflect"
        "strings"
        "testing"
 
-       "github.com/vapor/blockchain/pseudohsm"
+       acc "github.com/vapor/account"
        "github.com/vapor/blockchain/signers"
        "github.com/vapor/config"
        "github.com/vapor/crypto/ed25519/chainkd"
@@ -22,7 +21,7 @@ import (
 func TestCreateAccountWithUppercase(t *testing.T) {
        m := mockAccountManager(t)
        alias := "UPPER"
-       account, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, alias, signers.BIP0044)
+       account, err := m.Manager.Create([]chainkd.XPub{testutil.TestXPub}, 1, alias, signers.BIP0044)
 
        if err != nil {
                t.Fatal(err)
@@ -36,7 +35,7 @@ func TestCreateAccountWithUppercase(t *testing.T) {
 func TestCreateAccountWithSpaceTrimed(t *testing.T) {
        m := mockAccountManager(t)
        alias := " with space "
-       account, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, alias, signers.BIP0044)
+       account, err := m.Manager.Create([]chainkd.XPub{testutil.TestXPub}, 1, alias, signers.BIP0044)
 
        if err != nil {
                t.Fatal(err)
@@ -46,12 +45,12 @@ func TestCreateAccountWithSpaceTrimed(t *testing.T) {
                t.Fatal("created account alias should be lowercase")
        }
 
-       nilAccount, err := m.FindByAlias(alias)
+       nilAccount, err := m.Manager.FindByAlias(alias)
        if nilAccount != nil {
                t.Fatal("expected nil")
        }
 
-       target, err := m.FindByAlias(strings.ToLower(strings.TrimSpace(alias)))
+       target, err := m.Manager.FindByAlias(strings.ToLower(strings.TrimSpace(alias)))
        if target == nil {
                t.Fatal("expected Account, but got nil")
        }
@@ -59,15 +58,16 @@ func TestCreateAccountWithSpaceTrimed(t *testing.T) {
 
 func TestCreateAccount(t *testing.T) {
        m := mockAccountManager(t)
-       account, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias", signers.BIP0044)
+       account, err := m.Manager.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias", signers.BIP0044)
        if err != nil {
                t.Fatal(err)
        }
 
-       found, err := m.FindByID(account.ID)
+       found, err := m.Manager.FindByID(account.ID)
        if err != nil {
                t.Errorf("unexpected error %v", err)
        }
+
        if !testutil.DeepEqual(account, found) {
                t.Errorf("expected account %v to be recorded as %v", account, found)
        }
@@ -77,9 +77,9 @@ func TestCreateAccountReusedAlias(t *testing.T) {
        m := mockAccountManager(t)
        m.createTestAccount(t, "test-alias", nil)
 
-       _, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias", signers.BIP0044)
-       if errors.Root(err) != ErrDuplicateAlias {
-               t.Errorf("expected %s when reusing an alias, got %v", ErrDuplicateAlias, err)
+       _, err := m.Manager.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias", signers.BIP0044)
+       if errors.Root(err) != acc.ErrDuplicateAlias {
+               t.Errorf("expected %s when reusing an alias, got %v", acc.ErrDuplicateAlias, err)
        }
 }
 
@@ -89,56 +89,58 @@ func TestUpdateAccountAlias(t *testing.T) {
 
        m := mockAccountManager(t)
        account := m.createTestAccount(t, oldAlias, nil)
-       if err := m.UpdateAccountAlias("testID", newAlias); err == nil {
-               t.Fatal("expected error when using an invalid account id")
+       err := m.Manager.UpdateAccountAlias("testID", newAlias)
+       if err == nil {
+               t.Errorf("expected error when using an invalid account id")
        }
 
-       err := m.UpdateAccountAlias(account.ID, oldAlias)
-       if errors.Root(err) != ErrDuplicateAlias {
-               t.Errorf("expected %s when using a duplicate alias, got %v", ErrDuplicateAlias, err)
+       err = m.Manager.UpdateAccountAlias(account.ID, oldAlias)
+       if errors.Root(err) != acc.ErrDuplicateAlias {
+               t.Errorf("expected %s when using a duplicate alias, got %v", acc.ErrDuplicateAlias, err)
        }
 
-       if err := m.UpdateAccountAlias(account.ID, newAlias); err != nil {
+       err = m.Manager.UpdateAccountAlias(account.ID, newAlias)
+       if err != nil {
                t.Errorf("expected account %v alias should be update", account)
        }
 
-       updatedAccount, err := m.FindByID(account.ID)
+       updatedAccount, err := m.Manager.FindByID(account.ID)
        if err != nil {
                t.Errorf("unexpected error %v", err)
        }
 
        if updatedAccount.Alias != newAlias {
-               t.Fatalf("alias:\ngot:  %v\nwant: %v", updatedAccount.Alias, newAlias)
+               t.Errorf("alias:\ngot:  %v\nwant: %v", updatedAccount.Alias, newAlias)
        }
 
-       if _, err = m.FindByAlias(oldAlias); errors.Root(err) != ErrFindAccount {
-               t.Errorf("expected %s when using a old alias, got %v", ErrFindAccount, err)
+       if _, err = m.Manager.FindByAlias(oldAlias); errors.Root(err) != acc.ErrFindAccount {
+               t.Errorf("expected %s when using a old alias, got %v", acc.ErrFindAccount, err)
        }
 }
 
 func TestDeleteAccount(t *testing.T) {
        m := mockAccountManager(t)
 
-       account1, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias1", signers.BIP0044)
+       account1, err := m.Manager.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias1", signers.BIP0044)
        if err != nil {
                t.Fatal(err)
        }
 
-       account2, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias2", signers.BIP0044)
+       account2, err := m.Manager.Create([]chainkd.XPub{testutil.TestXPub}, 1, "test-alias2", signers.BIP0044)
        if err != nil {
                t.Fatal(err)
        }
 
-       found, err := m.FindByID(account1.ID)
+       found, err := m.Manager.FindByID(account1.ID)
        if err != nil {
                t.Errorf("expected account %v should be deleted", found)
        }
 
-       if err = m.DeleteAccount(account2.ID); err != nil {
+       if err = m.Manager.DeleteAccount(account2.ID); err != nil {
                t.Fatal(err)
        }
 
-       found, err = m.FindByID(account2.ID)
+       found, err = m.Manager.FindByID(account2.ID)
        if err != nil {
                t.Errorf("expected account %v should be deleted", found)
        }
@@ -148,7 +150,7 @@ func TestFindByID(t *testing.T) {
        m := mockAccountManager(t)
        account := m.createTestAccount(t, "", nil)
 
-       found, err := m.FindByID(account.ID)
+       found, err := m.Manager.FindByID(account.ID)
        if err != nil {
                t.Fatal(err)
        }
@@ -162,7 +164,7 @@ func TestFindByAlias(t *testing.T) {
        m := mockAccountManager(t)
        account := m.createTestAccount(t, "some-alias", nil)
 
-       found, err := m.FindByAlias("some-alias")
+       found, err := m.Manager.FindByAlias("some-alias")
        if err != nil {
                t.Fatal(err)
        }
@@ -172,40 +174,11 @@ func TestFindByAlias(t *testing.T) {
        }
 }
 
-func TestGetAccountIndexKey(t *testing.T) {
-       dirPath, err := ioutil.TempDir(".", "TestAccount")
-       if err != nil {
-               t.Fatal(err)
-       }
-       defer os.RemoveAll(dirPath)
-
-       hsm, err := pseudohsm.New(dirPath)
-       if err != nil {
-               t.Fatal(err)
-       }
-
-       xpub1, _, err := hsm.XCreate("TestAccountIndex1", "password", "en")
-       if err != nil {
-               t.Fatal(err)
-       }
-
-       xpub2, _, err := hsm.XCreate("TestAccountIndex2", "password", "en")
-       if err != nil {
-               t.Fatal(err)
-       }
-
-       xpubs1 := []chainkd.XPub{xpub1.XPub, xpub2.XPub}
-       xpubs2 := []chainkd.XPub{xpub2.XPub, xpub1.XPub}
-       if !reflect.DeepEqual(GetAccountIndexKey(xpubs1), GetAccountIndexKey(xpubs2)) {
-               t.Fatal("GetAccountIndexKey test err")
-       }
-
-       if reflect.DeepEqual(xpubs1, xpubs2) {
-               t.Fatal("GetAccountIndexKey test err")
-       }
+type mockAccManager struct {
+       Manager *acc.Manager
 }
 
-func mockAccountManager(t *testing.T) *Manager {
+func mockAccountManager(t *testing.T) *mockAccManager {
        dirPath, err := ioutil.TempDir(".", "")
        if err != nil {
                t.Fatal(err)
@@ -215,6 +188,7 @@ func mockAccountManager(t *testing.T) *Manager {
        testDB := dbm.NewDB("testdb", "memdb", dirPath)
        dispatcher := event.NewDispatcher()
        store := database.NewStore(testDB)
+       accountStore := database.NewAccountStore(testDB)
        txPool := protocol.NewTxPool(store, dispatcher)
        config.CommonConfig = config.DefaultConfig()
        chain, err := protocol.NewChain(store, txPool, dispatcher)
@@ -222,15 +196,14 @@ func mockAccountManager(t *testing.T) *Manager {
                t.Fatal(err)
        }
 
-       return NewManager(testDB, chain)
+       return &mockAccManager{acc.NewManager(accountStore, chain)}
 }
 
-func (m *Manager) createTestAccount(t testing.TB, alias string, tags map[string]interface{}) *Account {
-       account, err := m.Create([]chainkd.XPub{testutil.TestXPub}, 1, alias, signers.BIP0044)
+func (m *mockAccManager) createTestAccount(t testing.TB, alias string, tags map[string]interface{}) *acc.Account {
+       account, err := m.Manager.Create([]chainkd.XPub{testutil.TestXPub}, 1, alias, signers.BIP0044)
        if err != nil {
                t.Fatal(err)
        }
 
        return account
-
 }
index 0d548fe..092811d 100644 (file)
@@ -159,7 +159,7 @@ func InsertChain(chain *protocol.Chain, txPool *protocol.TxPool, txs []*types.Tx
                }
        }
 
-       block, err := proposal.NewBlockTemplate(chain, txPool, nil, uint64(time.Now().UnixNano() / 1e6))
+       block, err := proposal.NewBlockTemplate(chain, txPool, nil, uint64(time.Now().UnixNano()/1e6))
        if err != nil {
                return err
        }
@@ -368,7 +368,8 @@ func SetUtxoView(db dbm.DB, view *state.UtxoViewpoint) error {
 
 //-------------------------Mock actual transaction----------------------------------
 func MockTxsP2PKH(keyDirPath string, testDB dbm.DB, txNumber, otherAssetNum int) ([]*types.Tx, error) {
-       accountManager := account.NewManager(testDB, nil)
+       accountStore := database.NewAccountStore(testDB)
+       accountManager := account.NewManager(accountStore, nil)
        hsm, err := pseudohsm.New(keyDirPath)
        if err != nil {
                return nil, err
@@ -410,7 +411,8 @@ func MockTxsP2PKH(keyDirPath string, testDB dbm.DB, txNumber, otherAssetNum int)
 }
 
 func MockTxsP2SH(keyDirPath string, testDB dbm.DB, txNumber, otherAssetNum int) ([]*types.Tx, error) {
-       accountManager := account.NewManager(testDB, nil)
+       accountStore := database.NewAccountStore(testDB)
+       accountManager := account.NewManager(accountStore, nil)
        hsm, err := pseudohsm.New(keyDirPath)
        if err != nil {
                return nil, err
@@ -457,7 +459,8 @@ func MockTxsP2SH(keyDirPath string, testDB dbm.DB, txNumber, otherAssetNum int)
 }
 
 func MockTxsMultiSign(keyDirPath string, testDB dbm.DB, txNumber, otherAssetNum int) ([]*types.Tx, error) {
-       accountManager := account.NewManager(testDB, nil)
+       accountStore := database.NewAccountStore(testDB)
+       accountManager := account.NewManager(accountStore, nil)
        hsm, err := pseudohsm.New(keyDirPath)
        if err != nil {
                return nil, err
diff --git a/test/builder_test.go b/test/builder_test.go
new file mode 100644 (file)
index 0000000..4328dbc
--- /dev/null
@@ -0,0 +1,130 @@
+package test
+
+import (
+       "testing"
+
+       acc "github.com/vapor/account"
+       "github.com/vapor/blockchain/signers"
+       "github.com/vapor/consensus"
+       "github.com/vapor/crypto/ed25519/chainkd"
+       "github.com/vapor/testutil"
+)
+
+var (
+       //chainTxUtxoNum maximum utxo quantity in a tx
+       chainTxUtxoNum = 5
+       //chainTxMergeGas chain tx gas
+       chainTxMergeGas = uint64(10000000)
+)
+
+func TestBuildBtmTxChain(t *testing.T) {
+       chainTxUtxoNum = 3
+       m := mockAccountManager(t)
+       cases := []struct {
+               inputUtxo  []uint64
+               wantInput  [][]uint64
+               wantOutput [][]uint64
+               wantUtxo   uint64
+       }{
+               {
+                       inputUtxo:  []uint64{5},
+                       wantInput:  [][]uint64{},
+                       wantOutput: [][]uint64{},
+                       wantUtxo:   5 * chainTxMergeGas,
+               },
+               {
+                       inputUtxo: []uint64{5, 4},
+                       wantInput: [][]uint64{
+                               []uint64{5, 4},
+                       },
+                       wantOutput: [][]uint64{
+                               []uint64{8},
+                       },
+                       wantUtxo: 8 * chainTxMergeGas,
+               },
+               {
+                       inputUtxo: []uint64{5, 4, 1, 1},
+                       wantInput: [][]uint64{
+                               []uint64{5, 4, 1, 1},
+                               []uint64{1, 9},
+                       },
+                       wantOutput: [][]uint64{
+                               []uint64{10},
+                               []uint64{9},
+                       },
+                       wantUtxo: 10 * chainTxMergeGas,
+               },
+               {
+                       inputUtxo: []uint64{22, 123, 53, 234, 23, 4, 2423, 24, 23, 43, 34, 234, 234, 24},
+                       wantInput: [][]uint64{
+                               []uint64{22, 123, 53, 234, 23},
+                               []uint64{4, 2423, 24, 23, 43},
+                               []uint64{34, 234, 234, 24, 454},
+                               []uint64{2516, 979},
+                               []uint64{234, 24, 197},
+                               []uint64{260, 2469, 310},
+                               []uint64{454, 3038},
+                       },
+                       wantOutput: [][]uint64{
+                               []uint64{454},
+                               []uint64{2516},
+                               []uint64{979},
+                               []uint64{3494},
+                               []uint64{454},
+                               []uint64{3038},
+                               []uint64{3491},
+                       },
+                       wantUtxo: 3494 * chainTxMergeGas,
+               },
+       }
+
+       acct, err := m.Manager.Create([]chainkd.XPub{testutil.TestXPub}, 1, "testAccount", signers.BIP0044)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       acp, err := m.Manager.CreateAddress(acct.ID, false)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       for caseIndex, c := range cases {
+               utxos := []*acc.UTXO{}
+               for _, amount := range c.inputUtxo {
+                       utxos = append(utxos, &acc.UTXO{
+                               Amount:         amount * chainTxMergeGas,
+                               AssetID:        *consensus.BTMAssetID,
+                               Address:        acp.Address,
+                               ControlProgram: acp.ControlProgram,
+                       })
+               }
+
+               tpls, gotUtxo, err := m.Manager.BuildBtmTxChain(utxos, acct.Signer)
+               if err != nil {
+                       t.Fatal(err)
+               }
+
+               for i, tpl := range tpls {
+                       gotInput := []uint64{}
+                       for _, input := range tpl.Transaction.Inputs {
+                               gotInput = append(gotInput, input.Amount()/chainTxMergeGas)
+                       }
+
+                       gotOutput := []uint64{}
+                       for _, output := range tpl.Transaction.Outputs {
+                               gotOutput = append(gotOutput, output.AssetAmount().Amount/chainTxMergeGas)
+                       }
+
+                       if !testutil.DeepEqual(c.wantInput[i], gotInput) {
+                               t.Errorf("case %d tx %d input got %d want %d", caseIndex, i, gotInput, c.wantInput[i])
+                       }
+                       if !testutil.DeepEqual(c.wantOutput[i], gotOutput) {
+                               t.Errorf("case %d tx %d output got %d want %d", caseIndex, i, gotOutput, c.wantOutput[i])
+                       }
+               }
+
+               if c.wantUtxo != gotUtxo.Amount {
+                       t.Errorf("case %d got utxo=%d want utxo=%d", caseIndex, gotUtxo.Amount, c.wantUtxo)
+               }
+       }
+}
index 9181053..2f5fdb0 100644 (file)
@@ -110,12 +110,12 @@ func (ctx *chainTestContext) getUtxoEntries() map[string]*storage.UtxoEntry {
        defer iter.Release()
 
        for iter.Next() {
-               utxoEntry := storage.UtxoEntry{}
-               if err := proto.Unmarshal(iter.Value(), &utxoEntry); err != nil {
+               utxoEntry := new(storage.UtxoEntry)
+               if err := proto.Unmarshal(iter.Value(), utxoEntry); err != nil {
                        return nil
                }
                key := string(iter.Key())
-               utxoEntries[key] = &utxoEntry
+               utxoEntries[key] = utxoEntry
        }
        return utxoEntries
 }
index fee4e21..a82961d 100644 (file)
@@ -10,6 +10,7 @@ import (
        "github.com/vapor/blockchain/pseudohsm"
        "github.com/vapor/blockchain/signers"
        "github.com/vapor/crypto/ed25519/chainkd"
+       "github.com/vapor/database"
        dbm "github.com/vapor/database/leveldb"
        "github.com/vapor/protocol/bc/types"
        "github.com/vapor/protocol/validation"
@@ -31,7 +32,8 @@ func TestP2PKH(t *testing.T) {
                t.Fatal(err)
        }
 
-       accountManager := account.NewManager(testDB, chain)
+       accountStore := database.NewAccountStore(testDB)
+       accountManager := account.NewManager(accountStore, chain)
        hsm, err := pseudohsm.New(dirPath)
        if err != nil {
                t.Fatal(err)
@@ -82,7 +84,8 @@ func TestBip0032P2PKH(t *testing.T) {
                t.Fatal(err)
        }
 
-       accountManager := account.NewManager(testDB, chain)
+       accountStore := database.NewAccountStore(testDB)
+       accountManager := account.NewManager(accountStore, chain)
        hsm, err := pseudohsm.New(dirPath)
        if err != nil {
                t.Fatal(err)
@@ -109,7 +112,7 @@ func TestBip0032P2PKH(t *testing.T) {
                t.Fatal(err)
        }
 
-       testDB.Set(account.Key(testAccount.ID), rawAccount)
+       testDB.Set(database.AccountIDKey(testAccount.ID), rawAccount)
        controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
        if err != nil {
                t.Fatal(err)
@@ -146,7 +149,8 @@ func TestP2SH(t *testing.T) {
                t.Fatal(err)
        }
 
-       accountManager := account.NewManager(testDB, chain)
+       accountStore := database.NewAccountStore(testDB)
+       accountManager := account.NewManager(accountStore, chain)
        hsm, err := pseudohsm.New(dirPath)
        if err != nil {
                t.Fatal(err)
@@ -207,7 +211,8 @@ func TestBip0032P2SH(t *testing.T) {
                t.Fatal(err)
        }
 
-       accountManager := account.NewManager(testDB, chain)
+       accountStore := database.NewAccountStore(testDB)
+       accountManager := account.NewManager(accountStore, chain)
        hsm, err := pseudohsm.New(dirPath)
        if err != nil {
                t.Fatal(err)
@@ -238,7 +243,7 @@ func TestBip0032P2SH(t *testing.T) {
                t.Fatal(err)
        }
 
-       testDB.Set(account.Key(testAccount.ID), rawAccount)
+       testDB.Set(database.AccountIDKey(testAccount.ID), rawAccount)
 
        controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
        if err != nil {
@@ -280,7 +285,8 @@ func TestMutilNodeSign(t *testing.T) {
                t.Fatal(err)
        }
 
-       accountManager := account.NewManager(testDB, chain)
+       accountStore := database.NewAccountStore(testDB)
+       accountManager := account.NewManager(accountStore, chain)
        hsm, err := pseudohsm.New(dirPath)
        if err != nil {
                t.Fatal(err)
@@ -351,7 +357,8 @@ func TestBip0032MutilNodeSign(t *testing.T) {
                t.Fatal(err)
        }
 
-       accountManager := account.NewManager(testDB, chain)
+       accountStore := database.NewAccountStore(testDB)
+       accountManager := account.NewManager(accountStore, chain)
        hsm, err := pseudohsm.New(dirPath)
        if err != nil {
                t.Fatal(err)
@@ -383,7 +390,7 @@ func TestBip0032MutilNodeSign(t *testing.T) {
                t.Fatal(err)
        }
 
-       testDB.Set(account.Key(testAccount.ID), rawAccount)
+       testDB.Set(database.AccountIDKey(testAccount.ID), rawAccount)
 
        controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
        if err != nil {
index 8739008..0f3f0d6 100644 (file)
@@ -6,6 +6,7 @@ import (
        "time"
 
        "github.com/vapor/account"
+       "github.com/vapor/database"
        dbm "github.com/vapor/database/leveldb"
        "github.com/vapor/proposal"
        "github.com/vapor/test"
@@ -20,10 +21,11 @@ func BenchmarkNewBlockTpl(b *testing.B) {
        if err != nil {
                b.Fatal(err)
        }
-       accountManager := account.NewManager(testDB, chain)
+       accountStore := database.NewAccountStore(testDB)
+       accountManager := account.NewManager(accountStore, chain)
 
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
-               proposal.NewBlockTemplate(chain, txPool, accountManager, uint64(time.Now().UnixNano() / 1e6))
+               proposal.NewBlockTemplate(chain, txPool, accountManager, uint64(time.Now().UnixNano()/1e6))
        }
 }
index 11c6313..f8ce45d 100644 (file)
@@ -14,6 +14,7 @@ import (
        "github.com/vapor/consensus"
        "github.com/vapor/crypto/ed25519/chainkd"
        "github.com/vapor/crypto/sha3pool"
+       "github.com/vapor/database"
        dbm "github.com/vapor/database/leveldb"
        "github.com/vapor/errors"
        "github.com/vapor/protocol/bc"
@@ -267,7 +268,7 @@ func SignInstructionFor(input *types.SpendInput, db dbm.DB, signer *signers.Sign
        cp := account.CtrlProgram{}
        var hash [32]byte
        sha3pool.Sum256(hash[:], input.ControlProgram)
-       bytes := db.Get(account.ContractKey(hash))
+       bytes := db.Get(database.ContractKey(bc.NewHash(hash)))
        if bytes == nil {
                return nil, fmt.Errorf("can't find CtrlProgram for the SpendInput")
        }
diff --git a/test/wallet_test.go b/test/wallet_test.go
new file mode 100644 (file)
index 0000000..fe4509c
--- /dev/null
@@ -0,0 +1,326 @@
+package test
+
+import (
+       "io/ioutil"
+       "os"
+       "reflect"
+       "testing"
+       "time"
+
+       "github.com/vapor/account"
+       "github.com/vapor/asset"
+       "github.com/vapor/blockchain/pseudohsm"
+       "github.com/vapor/blockchain/signers"
+       "github.com/vapor/blockchain/txbuilder"
+       "github.com/vapor/config"
+       "github.com/vapor/consensus"
+       "github.com/vapor/crypto/ed25519/chainkd"
+       "github.com/vapor/database"
+       dbm "github.com/vapor/database/leveldb"
+       "github.com/vapor/event"
+       "github.com/vapor/protocol"
+       "github.com/vapor/protocol/bc"
+       "github.com/vapor/protocol/bc/types"
+       wt "github.com/vapor/wallet"
+)
+
+func TestWalletUpdate(t *testing.T) {
+       dirPath, err := ioutil.TempDir(".", "")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(dirPath)
+
+       config.CommonConfig = config.DefaultConfig()
+       testDB := dbm.NewDB("testdb", "leveldb", "temp")
+       defer func() {
+               testDB.Close()
+               os.RemoveAll("temp")
+       }()
+
+       store := database.NewStore(testDB)
+       walletStore := database.NewWalletStore(testDB)
+       dispatcher := event.NewDispatcher()
+       txPool := protocol.NewTxPool(store, dispatcher)
+
+       chain, err := protocol.NewChain(store, txPool, dispatcher)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       accountStore := database.NewAccountStore(testDB)
+       accountManager := account.NewManager(accountStore, chain)
+       hsm, err := pseudohsm.New(dirPath)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       xpub1, _, err := hsm.XCreate("test_pub1", "password", "en")
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub}, 1, "testAccount", signers.BIP0044)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       controlProg.KeyIndex = 1
+
+       reg := asset.NewRegistry(testDB, chain)
+       asset := bc.AssetID{V0: 5}
+
+       utxos := []*account.UTXO{}
+       btmUtxo := mockUTXO(controlProg, consensus.BTMAssetID)
+       utxos = append(utxos, btmUtxo)
+       OtherUtxo := mockUTXO(controlProg, &asset)
+       utxos = append(utxos, OtherUtxo)
+
+       _, txData, err := mockTxData(utxos, testAccount)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       tx := types.NewTx(*txData)
+       block := mockSingleBlock(tx)
+       txStatus := bc.NewTransactionStatus()
+       txStatus.SetStatus(0, false)
+       txStatus.SetStatus(1, false)
+       store.SaveBlock(block, txStatus)
+
+       w := newMockWallet(walletStore, accountManager, reg, chain, dispatcher, true)
+       err = w.AttachBlock(block)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       if _, err := w.GetTransactionByTxID(tx.ID.String()); err != nil {
+               t.Fatal(err)
+       }
+
+       wants, err := w.GetTransactions(testAccount.ID, "", 1, false)
+       if len(wants) != 1 {
+               t.Fatal(err)
+       }
+
+       if wants[0].ID != tx.ID {
+               t.Fatal("account txID mismatch")
+       }
+
+       for position, tx := range block.Transactions {
+               get := testDB.Get(database.CalcGlobalTxIndexKey(tx.ID.String()))
+               bh := block.BlockHeader.Hash()
+               expect := database.CalcGlobalTxIndex(&bh, uint64(position))
+               if !reflect.DeepEqual(get, expect) {
+                       t.Fatalf("position#%d: compare retrieved globalTxIdx err", position)
+               }
+       }
+}
+
+func TestRescanWallet(t *testing.T) {
+       // prepare wallet & db.
+       dirPath, err := ioutil.TempDir(".", "")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(dirPath)
+
+       config.CommonConfig = config.DefaultConfig()
+       testDB := dbm.NewDB("testdb", "leveldb", "temp")
+       walletStore := database.NewWalletStore(testDB)
+       defer func() {
+               testDB.Close()
+               os.RemoveAll("temp")
+       }()
+
+       store := database.NewStore(testDB)
+       dispatcher := event.NewDispatcher()
+       txPool := protocol.NewTxPool(store, dispatcher)
+       chain, err := protocol.NewChain(store, txPool, dispatcher)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       statusInfo := wt.StatusInfo{
+               Version:  uint(1),
+               WorkHash: bc.Hash{V0: 0xff},
+       }
+       if err := walletStore.SetWalletInfo(&statusInfo); err != nil {
+               t.Fatal(err)
+       }
+       walletInfo, err := walletStore.GetWalletInfo()
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       accountStore := database.NewAccountStore(testDB)
+       accountManager := account.NewManager(accountStore, chain)
+       w := newMockWallet(walletStore, accountManager, nil, chain, dispatcher, false)
+       if err != nil {
+               t.Fatal(err)
+       }
+       w.Status = *walletInfo
+
+       // rescan wallet.
+       if err := w.LoadWalletInfo(); err != nil {
+               t.Fatal(err)
+       }
+
+       block := config.GenesisBlock()
+       if w.Status.WorkHash != block.Hash() {
+               t.Fatal("reattach from genesis block")
+       }
+}
+
+func TestMemPoolTxQueryLoop(t *testing.T) {
+       dirPath, err := ioutil.TempDir(".", "")
+       if err != nil {
+               t.Fatal(err)
+       }
+       config.CommonConfig = config.DefaultConfig()
+       testDB := dbm.NewDB("testdb", "leveldb", dirPath)
+       defer func() {
+               testDB.Close()
+               os.RemoveAll(dirPath)
+       }()
+
+       store := database.NewStore(testDB)
+       dispatcher := event.NewDispatcher()
+       txPool := protocol.NewTxPool(store, dispatcher)
+
+       chain, err := protocol.NewChain(store, txPool, dispatcher)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       accountStore := database.NewAccountStore(testDB)
+       accountManager := account.NewManager(accountStore, chain)
+       hsm, err := pseudohsm.New(dirPath)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       xpub1, _, err := hsm.XCreate("test_pub1", "password", "en")
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub}, 1, "testAccount", signers.BIP0044)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       controlProg.KeyIndex = 1
+
+       reg := asset.NewRegistry(testDB, chain)
+       asset := bc.AssetID{V0: 5}
+
+       utxos := []*account.UTXO{}
+       btmUtxo := mockUTXO(controlProg, consensus.BTMAssetID)
+       utxos = append(utxos, btmUtxo)
+       OtherUtxo := mockUTXO(controlProg, &asset)
+       utxos = append(utxos, OtherUtxo)
+
+       _, txData, err := mockTxData(utxos, testAccount)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       tx := types.NewTx(*txData)
+       txStatus := bc.NewTransactionStatus()
+       txStatus.SetStatus(0, false)
+       walletStore := database.NewWalletStore(testDB)
+       w := newMockWallet(walletStore, accountManager, reg, chain, dispatcher, false)
+       go w.MemPoolTxQueryLoop()
+       w.EventDispatcher.Post(protocol.TxMsgEvent{TxMsg: &protocol.TxPoolMsg{TxDesc: &protocol.TxDesc{Tx: tx}, MsgType: protocol.MsgNewTx}})
+       time.Sleep(time.Millisecond * 10)
+       if _, err := w.GetUnconfirmedTxByTxID(tx.ID.String()); err != nil {
+               t.Fatal("dispatch new tx msg error:", err)
+       }
+       w.EventDispatcher.Post(protocol.TxMsgEvent{TxMsg: &protocol.TxPoolMsg{TxDesc: &protocol.TxDesc{Tx: tx}, MsgType: protocol.MsgRemoveTx}})
+       time.Sleep(time.Millisecond * 10)
+       txs, err := w.GetUnconfirmedTxs(testAccount.ID)
+       if err != nil {
+               t.Fatal("get unconfirmed tx error:", err)
+       }
+
+       if len(txs) != 0 {
+               t.Fatal("dispatch remove tx msg error")
+       }
+
+       w.EventDispatcher.Post(protocol.TxMsgEvent{TxMsg: &protocol.TxPoolMsg{TxDesc: &protocol.TxDesc{Tx: tx}, MsgType: 2}})
+}
+
+func mockUTXO(controlProg *account.CtrlProgram, assetID *bc.AssetID) *account.UTXO {
+       utxo := &account.UTXO{}
+       utxo.OutputID = bc.Hash{V0: 1}
+       utxo.SourceID = bc.Hash{V0: 2}
+       utxo.AssetID = *assetID
+       utxo.Amount = 1000000000
+       utxo.SourcePos = 0
+       utxo.ControlProgram = controlProg.ControlProgram
+       utxo.AccountID = controlProg.AccountID
+       utxo.Address = controlProg.Address
+       utxo.ControlProgramIndex = controlProg.KeyIndex
+       return utxo
+}
+
+func mockTxData(utxos []*account.UTXO, testAccount *account.Account) (*txbuilder.Template, *types.TxData, error) {
+       tplBuilder := txbuilder.NewBuilder(time.Now())
+
+       for _, utxo := range utxos {
+               txInput, sigInst, err := account.UtxoToInputs(testAccount.Signer, utxo)
+               if err != nil {
+                       return nil, nil, err
+               }
+               tplBuilder.AddInput(txInput, sigInst)
+
+               out := &types.TxOutput{}
+               if utxo.AssetID == *consensus.BTMAssetID {
+                       out = types.NewIntraChainOutput(utxo.AssetID, 100, utxo.ControlProgram)
+               } else {
+                       out = types.NewIntraChainOutput(utxo.AssetID, utxo.Amount, utxo.ControlProgram)
+               }
+               tplBuilder.AddOutput(out)
+       }
+
+       return tplBuilder.Build()
+}
+
+type mockWallet struct {
+       Wallet *wt.Wallet
+}
+
+func newMockWallet(store wt.WalletStore, account *account.Manager, asset *asset.Registry, chain *protocol.Chain, dispatcher *event.Dispatcher, txIndexFlag bool) *wt.Wallet {
+       wallet := &wt.Wallet{
+               Store:           store,
+               AccountMgr:      account,
+               AssetReg:        asset,
+               Chain:           chain,
+               RecoveryMgr:     wt.NewRecoveryManager(store, account),
+               EventDispatcher: dispatcher,
+               TxIndexFlag:     txIndexFlag,
+       }
+       wallet.TxMsgSub, _ = wallet.EventDispatcher.Subscribe(protocol.TxMsgEvent{})
+       return wallet
+}
+
+func mockSingleBlock(tx *types.Tx) *types.Block {
+       return &types.Block{
+               BlockHeader: types.BlockHeader{
+                       Version: 1,
+                       Height:  1,
+               },
+               Transactions: []*types.Tx{config.GenesisTx(), tx},
+       }
+}
index b03b430..570aecc 100644 (file)
@@ -12,6 +12,7 @@ import (
        "github.com/vapor/blockchain/pseudohsm"
        "github.com/vapor/blockchain/signers"
        "github.com/vapor/crypto/ed25519/chainkd"
+       "github.com/vapor/database"
        dbm "github.com/vapor/database/leveldb"
        "github.com/vapor/event"
        "github.com/vapor/protocol"
@@ -241,10 +242,12 @@ func (cfg *walletTestConfig) Run() error {
                return err
        }
        walletDB := dbm.NewDB("wallet", "leveldb", path.Join(dirPath, "wallet_db"))
-       accountManager := account.NewManager(walletDB, chain)
+       walletStore := database.NewWalletStore(walletDB)
+       accountStore := database.NewAccountStore(walletDB)
+       accountManager := account.NewManager(accountStore, chain)
        assets := asset.NewRegistry(walletDB, chain)
        dispatcher := event.NewDispatcher()
-       wallet, err := w.NewWallet(walletDB, accountManager, assets, hsm, chain, dispatcher, false)
+       wallet, err := w.NewWallet(walletStore, accountManager, assets, hsm, chain, dispatcher, false)
        if err != nil {
                return err
        }
index db305e5..fe05f7b 100644 (file)
@@ -2,7 +2,6 @@ package wallet
 
 import (
        "encoding/json"
-       "fmt"
 
        log "github.com/sirupsen/logrus"
 
@@ -13,7 +12,6 @@ import (
        "github.com/vapor/consensus"
        "github.com/vapor/consensus/segwit"
        "github.com/vapor/crypto/sha3pool"
-       dbm "github.com/vapor/database/leveldb"
        "github.com/vapor/protocol/bc"
        "github.com/vapor/protocol/bc/types"
 )
@@ -33,29 +31,18 @@ func annotateTxsAsset(w *Wallet, txs []*query.AnnotatedTx) {
 }
 
 func (w *Wallet) getExternalDefinition(assetID *bc.AssetID) json.RawMessage {
-       // definitionByte := w.DB.Get(asset.ExtAssetKey(assetID))
-       definitionByte := w.DB.Get(asset.ExtAssetKey(assetID))
-       if definitionByte == nil {
-               return nil
+       externalAsset, err := w.Store.GetAsset(assetID)
+       if err != nil {
+               log.WithFields(log.Fields{"module": logModule, "err": err, "assetID": assetID.String()}).Info("fail on get asset definition.")
        }
-
-       definitionMap := make(map[string]interface{})
-       if err := json.Unmarshal(definitionByte, &definitionMap); err != nil {
+       if externalAsset == nil {
                return nil
        }
 
-       alias := assetID.String()
-       externalAsset := &asset.Asset{
-               AssetID:           *assetID,
-               Alias:             &alias,
-               DefinitionMap:     definitionMap,
-               RawDefinitionByte: definitionByte,
+       if err := w.AssetReg.SaveAsset(externalAsset, *externalAsset.Alias); err != nil {
+               log.WithFields(log.Fields{"module": logModule, "err": err, "assetAlias": *externalAsset.Alias}).Info("fail on save external asset to internal asset DB")
        }
-
-       if err := w.AssetReg.SaveAsset(externalAsset, alias); err != nil {
-               log.WithFields(log.Fields{"module": logModule, "err": err, "assetID": alias}).Warning("fail on save external asset to internal asset DB")
-       }
-       return definitionByte
+       return json.RawMessage(externalAsset.RawDefinitionByte)
 }
 
 func (w *Wallet) getAliasDefinition(assetID bc.AssetID) (string, json.RawMessage) {
@@ -83,14 +70,14 @@ func (w *Wallet) getAliasDefinition(assetID bc.AssetID) (string, json.RawMessage
 }
 
 // annotateTxs adds account data to transactions
-func annotateTxsAccount(txs []*query.AnnotatedTx, walletDB dbm.DB) {
+func (w *Wallet) annotateTxsAccount(txs []*query.AnnotatedTx) {
        for i, tx := range txs {
                for j, input := range tx.Inputs {
                        //issue asset tx input SpentOutputID is nil
                        if input.SpentOutputID == nil {
                                continue
                        }
-                       localAccount, err := getAccountFromACP(input.ControlProgram, walletDB)
+                       localAccount, err := w.getAccountFromACP(input.ControlProgram)
                        if localAccount == nil || err != nil {
                                continue
                        }
@@ -98,7 +85,7 @@ func annotateTxsAccount(txs []*query.AnnotatedTx, walletDB dbm.DB) {
                        txs[i].Inputs[j].AccountID = localAccount.ID
                }
                for j, output := range tx.Outputs {
-                       localAccount, err := getAccountFromACP(output.ControlProgram, walletDB)
+                       localAccount, err := w.getAccountFromACP(output.ControlProgram)
                        if localAccount == nil || err != nil {
                                continue
                        }
@@ -108,32 +95,20 @@ func annotateTxsAccount(txs []*query.AnnotatedTx, walletDB dbm.DB) {
        }
 }
 
-func getAccountFromACP(program []byte, walletDB dbm.DB) (*account.Account, error) {
-       var hash common.Hash
-       accountCP := account.CtrlProgram{}
-       localAccount := account.Account{}
-
+func (w *Wallet) getAccountFromACP(program []byte) (*account.Account, error) {
+       var hash [32]byte
        sha3pool.Sum256(hash[:], program)
-
-       rawProgram := walletDB.Get(account.ContractKey(hash))
-       if rawProgram == nil {
-               return nil, fmt.Errorf("failed get account control program:%x ", hash)
-       }
-
-       if err := json.Unmarshal(rawProgram, &accountCP); err != nil {
+       accountCP, err := w.AccountMgr.GetControlProgram(bc.NewHash(hash))
+       if err != nil {
                return nil, err
        }
 
-       accountValue := walletDB.Get(account.Key(accountCP.AccountID))
-       if accountValue == nil {
-               return nil, fmt.Errorf("failed get account:%s ", accountCP.AccountID)
-       }
-
-       if err := json.Unmarshal(accountValue, &localAccount); err != nil {
+       account, err := w.AccountMgr.FindByID(accountCP.AccountID)
+       if err != nil {
                return nil, err
        }
 
-       return &localAccount, nil
+       return account, nil
 }
 
 var emptyJSONObject = json.RawMessage(`{}`)
@@ -169,6 +144,8 @@ func (w *Wallet) BuildAnnotatedInput(tx *types.Tx, i uint32) *query.AnnotatedInp
        if orig.InputType() != types.CoinbaseInputType {
                in.AssetID = orig.AssetID()
                in.Amount = orig.Amount()
+       } else {
+               in.AssetID = *consensus.BTMAssetID
        }
 
        id := tx.Tx.InputIDs[i]
index 73d6b79..317ee0e 100644 (file)
@@ -3,62 +3,20 @@ package wallet
 import (
        "encoding/binary"
        "encoding/hex"
-       "encoding/json"
        "fmt"
        "sort"
 
        log "github.com/sirupsen/logrus"
 
        "github.com/vapor/account"
-       "github.com/vapor/asset"
        "github.com/vapor/blockchain/query"
        "github.com/vapor/consensus"
        "github.com/vapor/crypto/sha3pool"
-       dbm "github.com/vapor/database/leveldb"
        chainjson "github.com/vapor/encoding/json"
-       "github.com/vapor/errors"
        "github.com/vapor/protocol/bc"
        "github.com/vapor/protocol/bc/types"
 )
 
-const (
-       //TxPrefix is wallet database transactions prefix
-       TxPrefix = "TXS:"
-       //TxIndexPrefix is wallet database tx index prefix
-       TxIndexPrefix = "TID:"
-       //TxIndexPrefix is wallet database global tx index prefix
-       GlobalTxIndexPrefix = "GTID:"
-)
-
-var ErrAccntTxIDNotFound = errors.New("account TXID not found")
-
-func formatKey(blockHeight uint64, position uint32) string {
-       return fmt.Sprintf("%016x%08x", blockHeight, position)
-}
-
-func calcAnnotatedKey(formatKey string) []byte {
-       return []byte(TxPrefix + formatKey)
-}
-
-func calcDeleteKey(blockHeight uint64) []byte {
-       return []byte(fmt.Sprintf("%s%016x", TxPrefix, blockHeight))
-}
-
-func calcTxIndexKey(txID string) []byte {
-       return []byte(TxIndexPrefix + txID)
-}
-
-func calcGlobalTxIndexKey(txID string) []byte {
-       return []byte(GlobalTxIndexPrefix + txID)
-}
-
-func calcGlobalTxIndex(blockHash *bc.Hash, position uint64) []byte {
-       txIdx := make([]byte, 40)
-       copy(txIdx[:32], blockHash.Bytes())
-       binary.BigEndian.PutUint64(txIdx[32:], position)
-       return txIdx
-}
-
 func parseGlobalTxIdx(globalTxIdx []byte) (*bc.Hash, uint64) {
        var hashBytes [32]byte
        copy(hashBytes[:], globalTxIdx[:32])
@@ -67,37 +25,31 @@ func parseGlobalTxIdx(globalTxIdx []byte) (*bc.Hash, uint64) {
        return &hash, position
 }
 
-// deleteTransaction delete transactions when orphan block rollback
-func (w *Wallet) deleteTransactions(batch dbm.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 {
-                       batch.Delete(calcTxIndexKey(tmpTx.ID.String()))
-               }
-               batch.Delete(txIter.Key())
-       }
-}
-
 // saveExternalAssetDefinition save external and local assets definition,
 // when query ,query local first and if have no then query external
 // details see getAliasDefinition
-func saveExternalAssetDefinition(b *types.Block, walletDB dbm.DB) {
-       storeBatch := walletDB.NewBatch()
-       defer storeBatch.Write()
+func saveExternalAssetDefinition(b *types.Block, store WalletStore) error {
+       newStore := store.InitBatch()
 
        for _, tx := range b.Transactions {
                for _, orig := range tx.Inputs {
                        if cci, ok := orig.TypedInput.(*types.CrossChainInput); ok {
                                assetID := cci.AssetId
-                               if assetExist := walletDB.Get(asset.ExtAssetKey(assetID)); assetExist == nil {
-                                       storeBatch.Set(asset.ExtAssetKey(assetID), cci.AssetDefinition)
+                               if _, err := newStore.GetAsset(assetID); err == nil {
+                                       continue
+                               } else if err != ErrGetAsset {
+                                       return err
                                }
+
+                               newStore.SetAssetDefinition(assetID, cci.AssetDefinition)
                        }
                }
        }
+       if err := newStore.CommitBatch(); err != nil {
+               return err
+       }
+
+       return nil
 }
 
 // Summary is the struct of transaction's input and output summary
@@ -120,23 +72,13 @@ type TxSummary struct {
 }
 
 // indexTransactions saves all annotated transactions to the database.
-func (w *Wallet) indexTransactions(batch dbm.Batch, b *types.Block, txStatus *bc.TransactionStatus) error {
-       annotatedTxs := w.filterAccountTxs(b, txStatus)
-       saveExternalAssetDefinition(b, w.DB)
-       annotateTxsAccount(annotatedTxs, w.DB)
-
+func (w *Wallet) indexTransactions(b *types.Block, txStatus *bc.TransactionStatus, annotatedTxs []*query.AnnotatedTx, store WalletStore) error {
        for _, tx := range annotatedTxs {
-               rawTx, err := json.Marshal(tx)
-               if err != nil {
-                       log.WithFields(log.Fields{"module": logModule, "err": err}).Error("inserting annotated_txs to db")
+               if err := w.Store.SetTransaction(b.Height, tx); err != nil {
                        return err
                }
 
-               batch.Set(calcAnnotatedKey(formatKey(b.Height, uint32(tx.Position))), rawTx)
-               batch.Set(calcTxIndexKey(tx.ID.String()), []byte(formatKey(b.Height, uint32(tx.Position))))
-
-               // delete unconfirmed transaction
-               batch.Delete(calcUnconfirmedTxKey(tx.ID.String()))
+               store.DeleteUnconfirmedTransaction(tx.ID.String())
        }
 
        if !w.TxIndexFlag {
@@ -145,7 +87,7 @@ func (w *Wallet) indexTransactions(batch dbm.Batch, b *types.Block, txStatus *bc
 
        for position, globalTx := range b.Transactions {
                blockHash := b.BlockHeader.Hash()
-               batch.Set(calcGlobalTxIndexKey(globalTx.ID.String()), calcGlobalTxIndex(&blockHash, uint64(position)))
+               store.SetGlobalTransactionIndex(globalTx.ID.String(), &blockHash, uint64(position))
        }
 
        return nil
@@ -161,21 +103,25 @@ transactionLoop:
                for _, v := range tx.Outputs {
                        var hash [32]byte
                        sha3pool.Sum256(hash[:], v.ControlProgram())
-
-                       if bytes := w.DB.Get(account.ContractKey(hash)); bytes != nil {
+                       if _, err := w.AccountMgr.GetControlProgram(bc.NewHash(hash)); err == nil {
                                annotatedTxs = append(annotatedTxs, w.buildAnnotatedTransaction(tx, b, statusFail, pos))
                                continue transactionLoop
+                       } else {
+                               log.WithFields(log.Fields{"module": logModule, "err": err, "hash": hex.EncodeToString(hash[:])}).Info("filterAccountTxs fail.")
                        }
                }
 
                for _, v := range tx.Inputs {
                        outid, err := v.SpentOutputID()
                        if err != nil {
+                               log.WithFields(log.Fields{"module": logModule, "err": err, "outputID": hex.EncodeToString(outid.Bytes())}).Info("filterAccountTxs fail.")
                                continue
                        }
-                       if bytes := w.DB.Get(account.StandardUTXOKey(outid)); bytes != nil {
+                       if _, err = w.Store.GetStandardUTXO(outid); err == nil {
                                annotatedTxs = append(annotatedTxs, w.buildAnnotatedTransaction(tx, b, statusFail, pos))
                                continue transactionLoop
+                       } else {
+                               log.WithFields(log.Fields{"module": logModule, "err": err, "outputID": hex.EncodeToString(outid.Bytes())}).Info("filterAccountTxs fail.")
                        }
                }
        }
@@ -195,14 +141,8 @@ func (w *Wallet) GetTransactionByTxID(txID string) (*query.AnnotatedTx, error) {
 }
 
 func (w *Wallet) getAccountTxByTxID(txID string) (*query.AnnotatedTx, error) {
-       annotatedTx := &query.AnnotatedTx{}
-       formatKey := w.DB.Get(calcTxIndexKey(txID))
-       if formatKey == nil {
-               return nil, ErrAccntTxIDNotFound
-       }
-
-       txInfo := w.DB.Get(calcAnnotatedKey(string(formatKey)))
-       if err := json.Unmarshal(txInfo, annotatedTx); err != nil {
+       annotatedTx, err := w.Store.GetTransaction(txID)
+       if err != nil {
                return nil, err
        }
 
@@ -211,18 +151,18 @@ func (w *Wallet) getAccountTxByTxID(txID string) (*query.AnnotatedTx, error) {
 }
 
 func (w *Wallet) getGlobalTxByTxID(txID string) (*query.AnnotatedTx, error) {
-       globalTxIdx := w.DB.Get(calcGlobalTxIndexKey(txID))
+       globalTxIdx := w.Store.GetGlobalTransactionIndex(txID)
        if globalTxIdx == nil {
                return nil, fmt.Errorf("No transaction(tx_id=%s) ", txID)
        }
 
        blockHash, pos := parseGlobalTxIdx(globalTxIdx)
-       block, err := w.chain.GetBlockByHash(blockHash)
+       block, err := w.Chain.GetBlockByHash(blockHash)
        if err != nil {
                return nil, err
        }
 
-       txStatus, err := w.chain.GetTransactionStatus(blockHash)
+       txStatus, err := w.Chain.GetTransactionStatus(blockHash)
        if err != nil {
                return nil, err
        }
@@ -291,38 +231,16 @@ func findTransactionsByAccount(annotatedTx *query.AnnotatedTx, accountID string)
 // GetTransactions get all walletDB transactions or unconfirmed transactions, and filter transactions by accountID and StartTxID optional
 func (w *Wallet) GetTransactions(accountID string, StartTxID string, count uint, unconfirmed bool) ([]*query.AnnotatedTx, error) {
        annotatedTxs := []*query.AnnotatedTx{}
-       var startKey []byte
-       preFix := TxPrefix
-
-       if StartTxID != "" {
-               if unconfirmed {
-                       startKey = calcUnconfirmedTxKey(StartTxID)
-               } else {
-                       formatKey := w.DB.Get(calcTxIndexKey(StartTxID))
-                       if formatKey == nil {
-                               return nil, ErrAccntTxIDNotFound
-                       }
-                       startKey = calcAnnotatedKey(string(formatKey))
-               }
-       }
-
-       if unconfirmed {
-               preFix = UnconfirmedTxPrefix
+       annotatedTxs, err := w.Store.ListTransactions(accountID, StartTxID, count, unconfirmed)
+       if err != nil {
+               return nil, err
        }
 
-       itr := w.DB.IteratorPrefixWithStart([]byte(preFix), startKey, true)
-       defer itr.Release()
-
-       for txNum := count; itr.Next() && txNum > 0; {
-               annotatedTx := &query.AnnotatedTx{}
-               if err := json.Unmarshal(itr.Value(), &annotatedTx); err != nil {
-                       return nil, err
-               }
-
+       newAnnotatedTxs := []*query.AnnotatedTx{}
+       for _, annotatedTx := range annotatedTxs {
                if accountID == "" || findTransactionsByAccount(annotatedTx, accountID) {
                        annotateTxsAsset(w, []*query.AnnotatedTx{annotatedTx})
-                       annotatedTxs = append([]*query.AnnotatedTx{annotatedTx}, annotatedTxs...)
-                       txNum--
+                       newAnnotatedTxs = append([]*query.AnnotatedTx{annotatedTx}, newAnnotatedTxs...)
                }
        }
 
@@ -332,7 +250,7 @@ func (w *Wallet) GetTransactions(accountID string, StartTxID string, count uint,
                sort.Sort(SortByHeight(annotatedTxs))
        }
 
-       return annotatedTxs, nil
+       return newAnnotatedTxs, nil
 }
 
 // GetAccountBalances return all account balances
index 2933ad6..a2aae5a 100644 (file)
@@ -1,7 +1,6 @@
 package wallet
 
 import (
-       "encoding/json"
        "fmt"
        "reflect"
        "sync"
@@ -12,7 +11,6 @@ import (
        "github.com/vapor/blockchain/signers"
        "github.com/vapor/crypto/ed25519/chainkd"
        "github.com/vapor/crypto/sha3pool"
-       dbm "github.com/vapor/database/leveldb"
        "github.com/vapor/errors"
        "github.com/vapor/protocol/bc"
        "github.com/vapor/protocol/bc/types"
@@ -28,15 +26,14 @@ const (
        addrRecoveryWindow = uint64(128)
 )
 
-//recoveryKey key for db store recovery info.
 var (
-       recoveryKey = []byte("RecoveryInfo")
-
        // ErrRecoveryBusy another recovery in progress, can not get recovery manager lock
        ErrRecoveryBusy = errors.New("another recovery in progress")
 
        // ErrInvalidAcctID can not find account by account id
-       ErrInvalidAcctID = errors.New("invalid account id")
+       ErrInvalidAcctID     = errors.New("invalid account id")
+       ErrGetRecoveryStatus = errors.New("failed to get recovery status.")
+       ErrRecoveryStatus    = errors.New("recovery status is nil.")
 )
 
 // branchRecoveryState maintains the required state in-order to properly
@@ -127,8 +124,8 @@ func newAddressRecoveryState(recoveryWindow uint64, account *account.Account) *a
        }
 }
 
-// recoveryState used to record the status of a recovery process.
-type recoveryState struct {
+// RecoveryState used to record the status of a recovery process.
+type RecoveryState struct {
        // XPubs recovery account xPubs
        XPubs []chainkd.XPub
 
@@ -145,8 +142,8 @@ type recoveryState struct {
        AccountsStatus map[string]*addressRecoveryState
 }
 
-func newRecoveryState() *recoveryState {
-       return &recoveryState{
+func newRecoveryState() *RecoveryState {
+       return &RecoveryState{
                AccountsStatus: make(map[string]*addressRecoveryState),
                StartTime:      time.Now(),
        }
@@ -155,7 +152,7 @@ func newRecoveryState() *recoveryState {
 // stateForScope returns a ScopeRecoveryState for the provided key scope. If one
 // does not already exist, a new one will be generated with the RecoveryState's
 // recoveryWindow.
-func (rs *recoveryState) stateForScope(account *account.Account) {
+func (rs *RecoveryState) stateForScope(account *account.Account) {
        // If the account recovery state already exists, return it.
        if _, ok := rs.AccountsStatus[account.ID]; ok {
                return
@@ -170,7 +167,7 @@ func (rs *recoveryState) stateForScope(account *account.Account) {
 type recoveryManager struct {
        mu sync.Mutex
 
-       db         dbm.DB
+       store      WalletStore
        accountMgr *account.Manager
 
        locked int32
@@ -179,17 +176,17 @@ type recoveryManager struct {
 
        // state encapsulates and allocates the necessary recovery state for all
        // key scopes and subsidiary derivation paths.
-       state *recoveryState
+       state *RecoveryState
 
        //addresses all addresses derivation lookahead used when
        // attempting to recover the set of used addresses.
        addresses map[bc.Hash]*account.CtrlProgram
 }
 
-// newRecoveryManager create recovery manger.
-func newRecoveryManager(db dbm.DB, accountMgr *account.Manager) *recoveryManager {
+// NewRecoveryManager create recovery manger.
+func NewRecoveryManager(store WalletStore, accountMgr *account.Manager) *recoveryManager {
        return &recoveryManager{
-               db:         db,
+               store:      store,
                accountMgr: accountMgr,
                addresses:  make(map[bc.Hash]*account.CtrlProgram),
                state:      newRecoveryState(),
@@ -249,13 +246,7 @@ func (m *recoveryManager) AcctResurrect(xPubs []chainkd.XPub) error {
 }
 
 func (m *recoveryManager) commitStatusInfo() error {
-       rawStatus, err := json.Marshal(m.state)
-       if err != nil {
-               return err
-       }
-
-       m.db.Set(recoveryKey, rawStatus)
-       return nil
+       return m.store.SetRecoveryStatus(m.state)
 }
 
 func genAcctAlias(xPubs []chainkd.XPub, index uint64) string {
@@ -364,7 +355,7 @@ func (m *recoveryManager) FilterRecoveryTxs(b *types.Block) error {
 }
 
 func (m *recoveryManager) finished() {
-       m.db.Delete(recoveryKey)
+       m.store.DeleteRecoveryStatus()
        m.started = false
        m.addresses = make(map[bc.Hash]*account.CtrlProgram)
        m.state = newRecoveryState()
@@ -375,14 +366,17 @@ func (m *recoveryManager) LoadStatusInfo() error {
        m.mu.Lock()
        defer m.mu.Unlock()
 
-       rawStatus := m.db.Get(recoveryKey)
-       if rawStatus == nil {
+       if m.state == nil {
+               return ErrRecoveryStatus
+       }
+       status, err := m.store.GetRecoveryStatus()
+       if err == ErrGetRecoveryStatus {
                return nil
        }
-
-       if err := json.Unmarshal(rawStatus, m.state); err != nil {
+       if err != nil {
                return err
        }
+       m.state = status
 
        if m.state.XPubs != nil && !m.tryStartXPubsRec() {
                return ErrRecoveryBusy
index 0b6fb56..c4ec390 100644 (file)
@@ -10,6 +10,7 @@ import (
        "time"
 
        "github.com/vapor/account"
+       acc "github.com/vapor/account"
        "github.com/vapor/blockchain/pseudohsm"
        "github.com/vapor/blockchain/signers"
        "github.com/vapor/blockchain/txbuilder"
@@ -141,6 +142,7 @@ func TestXPubsRecoveryLock(t *testing.T) {
        defer os.RemoveAll(dirPath)
 
        testDB := dbm.NewDB("testdb", "leveldb", dirPath)
+       walletStore := NewMockWalletStore(testDB)
        hsm, err := pseudohsm.New(dirPath)
        if err != nil {
                t.Fatal(err)
@@ -151,8 +153,9 @@ func TestXPubsRecoveryLock(t *testing.T) {
                t.Fatal(err)
        }
 
-       acctMgr := account.NewManager(testDB, nil)
-       recoveryMgr := newRecoveryManager(testDB, acctMgr)
+       acctStore := NewMockAccountStore(testDB)
+       acctMgr := account.NewManager(acctStore, nil)
+       recoveryMgr := NewRecoveryManager(walletStore, acctMgr)
        recoveryMgr.state = newRecoveryState()
        recoveryMgr.state.XPubs = []chainkd.XPub{xpub.XPub}
        recoveryMgr.state.XPubsStatus = newBranchRecoveryState(acctRecoveryWindow)
@@ -190,6 +193,7 @@ func TestExtendScanAddresses(t *testing.T) {
        defer os.RemoveAll(dirPath)
 
        testDB := dbm.NewDB("testdb", "leveldb", dirPath)
+       walletStore := NewMockWalletStore(testDB)
        hsm, err := pseudohsm.New(dirPath)
        if err != nil {
                t.Fatal(err)
@@ -200,8 +204,9 @@ func TestExtendScanAddresses(t *testing.T) {
                t.Fatal(err)
        }
 
-       acctMgr := account.NewManager(testDB, nil)
-       recoveryMgr := newRecoveryManager(testDB, acctMgr)
+       acctStore := NewMockAccountStore(testDB)
+       acctMgr := account.NewManager(acctStore, nil)
+       recoveryMgr := NewRecoveryManager(walletStore, acctMgr)
        acc1 := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub.XPub}, KeyIndex: 1, DeriveRule: signers.BIP0044}}
        acc2 := &account.Account{ID: "testB", Alias: "test2"}
        acc3 := &account.Account{ID: "testC", Alias: "test3", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub.XPub}, KeyIndex: 2, DeriveRule: 3}}
@@ -246,6 +251,7 @@ func TestRecoveryFromXPubs(t *testing.T) {
 
        testDB := dbm.NewDB("testdb", "leveldb", dirPath)
        recoveryDB := dbm.NewDB("recdb", "leveldb", dirPath)
+       recoveryStore := NewMockWalletStore(recoveryDB)
        hsm, err := pseudohsm.New(dirPath)
        if err != nil {
                t.Fatal(err)
@@ -256,10 +262,12 @@ func TestRecoveryFromXPubs(t *testing.T) {
                t.Fatal(err)
        }
 
-       acctMgr := account.NewManager(testDB, nil)
+       acctStore := NewMockAccountStore(testDB)
+       acctMgr := account.NewManager(acctStore, nil)
        txs, err := MockTxsP2PKH(acctMgr, xpub.XPub, false)
-       recAcctMgr := account.NewManager(recoveryDB, nil)
-       recoveryMgr := newRecoveryManager(recoveryDB, recAcctMgr)
+       recActStore := NewMockAccountStore(recoveryDB)
+       recAcctMgr := account.NewManager(recActStore, nil)
+       recoveryMgr := NewRecoveryManager(recoveryStore, recAcctMgr)
 
        cases := []struct {
                xPubs []chainkd.XPub
@@ -290,7 +298,7 @@ func TestRecoveryFromXPubs(t *testing.T) {
 
                for _, acct := range Accounts {
                        tmp, err := recAcctMgr.GetAccountByXPubsIndex(acct.XPubs, acct.KeyIndex)
-                       if err != nil {
+                       if err != nil && err != acc.ErrFindAccount {
                                t.Fatal("recovery from XPubs err:", err)
                        }
 
@@ -320,6 +328,7 @@ func TestRecoveryByRescanAccount(t *testing.T) {
 
        testDB := dbm.NewDB("testdb", "leveldb", dirPath)
        recoveryDB := dbm.NewDB("recdb", "leveldb", dirPath)
+       recoveryStore := NewMockWalletStore(recoveryDB)
        hsm, err := pseudohsm.New(dirPath)
        if err != nil {
                t.Fatal(err)
@@ -330,7 +339,8 @@ func TestRecoveryByRescanAccount(t *testing.T) {
                t.Fatal(err)
        }
 
-       acctMgr := account.NewManager(testDB, nil)
+       acctStore := NewMockAccountStore(testDB)
+       acctMgr := account.NewManager(acctStore, nil)
        txs, err := MockTxsP2PKH(acctMgr, xpub.XPub, true)
        if err != nil {
                t.Fatal("recovery by rescan account err:", err)
@@ -341,14 +351,15 @@ func TestRecoveryByRescanAccount(t *testing.T) {
                t.Fatal("recovery by rescan account err:", err)
        }
 
-       recAcctMgr := account.NewManager(recoveryDB, nil)
+       recActStore := NewMockAccountStore(recoveryDB)
+       recAcctMgr := account.NewManager(recActStore, nil)
        for _, acct := range allAccounts {
                if err := recAcctMgr.SaveAccount(acct); err != nil {
                        t.Fatal("recovery by rescan account err:", err)
                }
        }
 
-       recoveryMgr := newRecoveryManager(recoveryDB, recAcctMgr)
+       recoveryMgr := NewRecoveryManager(recoveryStore, recAcctMgr)
 
        acct := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub.XPub}, KeyIndex: 1, DeriveRule: 3}}
 
@@ -376,7 +387,7 @@ func TestRecoveryByRescanAccount(t *testing.T) {
 
                for _, acct := range accounts {
                        tmp, err := recAcctMgr.GetAccountByXPubsIndex(acct.XPubs, acct.KeyIndex)
-                       if err != nil {
+                       if err != nil && err != acc.ErrFindAccount {
                                t.Fatal("recovery from XPubs err:", err)
                        }
 
@@ -408,6 +419,7 @@ func TestReportFound(t *testing.T) {
        defer os.RemoveAll(dirPath)
 
        testDB := dbm.NewDB("testdb", "leveldb", dirPath)
+       testStore := NewMockWalletStore(testDB)
        hsm, err := pseudohsm.New(dirPath)
        if err != nil {
                t.Fatal(err)
@@ -423,8 +435,9 @@ func TestReportFound(t *testing.T) {
                t.Fatal(err)
        }
 
-       acctMgr := account.NewManager(testDB, nil)
-       recoveryMgr := newRecoveryManager(testDB, acctMgr)
+       acctStore := NewMockAccountStore(testDB)
+       acctMgr := account.NewManager(acctStore, nil)
+       recoveryMgr := NewRecoveryManager(testStore, acctMgr)
        acc1 := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub1.XPub}, KeyIndex: 1, DeriveRule: signers.BIP0044}}
        acc2 := &account.Account{ID: "testB", Alias: "test2", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub2.XPub}, KeyIndex: 1, DeriveRule: signers.BIP0032}}
        acc3 := &account.Account{ID: "testC", Alias: "test3", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub2.XPub}, KeyIndex: 2, DeriveRule: signers.BIP0044}}
@@ -495,6 +508,7 @@ func TestLoadStatusInfo(t *testing.T) {
        defer os.RemoveAll(dirPath)
 
        testDB := dbm.NewDB("testdb", "leveldb", "temp")
+       testStore := NewMockWalletStore(testDB)
        defer os.RemoveAll("temp")
 
        hsm, err := pseudohsm.New(dirPath)
@@ -507,8 +521,9 @@ func TestLoadStatusInfo(t *testing.T) {
                t.Fatal(err)
        }
 
-       acctMgr := account.NewManager(testDB, nil)
-       recoveryMgr := newRecoveryManager(testDB, acctMgr)
+       acctStore := NewMockAccountStore(testDB)
+       acctMgr := account.NewManager(acctStore, nil)
+       recoveryMgr := NewRecoveryManager(testStore, acctMgr)
        // StatusInit init recovery status manager.
        recoveryMgr.state = newRecoveryState()
        recoveryMgr.state.XPubs = []chainkd.XPub{xpub.XPub}
@@ -521,7 +536,7 @@ func TestLoadStatusInfo(t *testing.T) {
 
        recoveryMgr.commitStatusInfo()
 
-       recoveryMgrRestore := newRecoveryManager(testDB, acctMgr)
+       recoveryMgrRestore := NewRecoveryManager(testStore, acctMgr)
        if err := recoveryMgrRestore.LoadStatusInfo(); err != nil {
                t.Fatal("TestLoadStatusInfo err:", err)
        }
@@ -569,10 +584,12 @@ func TestLock(t *testing.T) {
        defer os.RemoveAll(dirPath)
 
        testDB := dbm.NewDB("testdb", "leveldb", "temp")
+       testStore := NewMockWalletStore(testDB)
        defer os.RemoveAll("temp")
 
-       acctMgr := account.NewManager(testDB, nil)
-       recoveryMgr := newRecoveryManager(testDB, acctMgr)
+       acctStore := NewMockAccountStore(testDB)
+       acctMgr := account.NewManager(acctStore, nil)
+       recoveryMgr := NewRecoveryManager(testStore, acctMgr)
        if !recoveryMgr.tryStartXPubsRec() {
                t.Fatal("recovery manager try lock test err")
        }
@@ -610,15 +627,6 @@ func TestStateForScope(t *testing.T) {
        }
 }
 
-func bip44ContractIndexKey(accountID string, change bool) []byte {
-       contractIndexPrefix := []byte("ContractIndex")
-       key := append(contractIndexPrefix, accountID...)
-       if change {
-               return append(key, []byte{1}...)
-       }
-       return append(key, []byte{0}...)
-}
-
 func TestContractIndexResidue(t *testing.T) {
        dirPath, err := ioutil.TempDir(".", "")
        if err != nil {
@@ -627,6 +635,7 @@ func TestContractIndexResidue(t *testing.T) {
        defer os.RemoveAll(dirPath)
 
        testDB := dbm.NewDB("testdb", "leveldb", dirPath)
+       testStore := NewMockWalletStore(testDB)
        hsm, err := pseudohsm.New(dirPath)
        if err != nil {
                t.Fatal(err)
@@ -638,14 +647,15 @@ func TestContractIndexResidue(t *testing.T) {
        }
 
        contractIndexResidue := uint64(5)
-       acctMgr := account.NewManager(testDB, nil)
-       recoveryMgr := newRecoveryManager(testDB, acctMgr)
+       acctStore := NewMockAccountStore(testDB)
+       acctMgr := account.NewManager(acctStore, nil)
+       recoveryMgr := NewRecoveryManager(testStore, acctMgr)
        acct := &account.Account{ID: "testA", Alias: "test1", Signer: &signers.Signer{XPubs: []chainkd.XPub{xpub1.XPub}, KeyIndex: 1, DeriveRule: signers.BIP0044}}
 
        cp1 := &account.CtrlProgram{AccountID: acct.ID, Address: "address1", KeyIndex: 10, Change: false}
 
        setContractIndexKey := func(acctMgr *account.Manager, accountID string, change bool) {
-               testDB.Set(bip44ContractIndexKey(accountID, change), common.Unit64ToBytes(contractIndexResidue))
+               testDB.Set(Bip44ContractIndexKey(accountID, change), common.Unit64ToBytes(contractIndexResidue))
        }
 
        delAccount := func(acctMgr *account.Manager, accountID string, change bool) {
diff --git a/wallet/store.go b/wallet/store.go
new file mode 100644 (file)
index 0000000..90b6050
--- /dev/null
@@ -0,0 +1,37 @@
+package wallet
+
+import (
+       acc "github.com/vapor/account"
+       "github.com/vapor/asset"
+       "github.com/vapor/blockchain/query"
+       "github.com/vapor/protocol/bc"
+)
+
+// WalletStore interface contains wallet storage functions.
+type WalletStore interface {
+       InitBatch() WalletStore
+       CommitBatch() error
+       DeleteContractUTXO(bc.Hash)
+       DeleteRecoveryStatus()
+       DeleteTransactions(uint64)
+       DeleteUnconfirmedTransaction(string)
+       DeleteWalletTransactions()
+       DeleteWalletUTXOs()
+       GetAsset(*bc.AssetID) (*asset.Asset, error)
+       GetGlobalTransactionIndex(string) []byte
+       GetStandardUTXO(bc.Hash) (*acc.UTXO, error)
+       GetTransaction(string) (*query.AnnotatedTx, error)
+       GetUnconfirmedTransaction(string) (*query.AnnotatedTx, error)
+       GetRecoveryStatus() (*RecoveryState, error)
+       GetWalletInfo() (*StatusInfo, error)
+       ListAccountUTXOs(string, bool) ([]*acc.UTXO, error)
+       ListTransactions(string, string, uint, bool) ([]*query.AnnotatedTx, error)
+       ListUnconfirmedTransactions() ([]*query.AnnotatedTx, error)
+       SetAssetDefinition(*bc.AssetID, []byte)
+       SetContractUTXO(bc.Hash, *acc.UTXO) error
+       SetGlobalTransactionIndex(string, *bc.Hash, uint64)
+       SetRecoveryStatus(*RecoveryState) error
+       SetTransaction(uint64, *query.AnnotatedTx) error
+       SetUnconfirmedTransaction(string, *query.AnnotatedTx) error
+       SetWalletInfo(*StatusInfo) error
+}
index 843c289..8effda0 100644 (file)
@@ -1,14 +1,14 @@
 package wallet
 
 import (
-       "encoding/json"
        "fmt"
        "sort"
        "time"
 
+       "github.com/vapor/protocol/bc"
+
        log "github.com/sirupsen/logrus"
 
-       "github.com/vapor/account"
        "github.com/vapor/blockchain/query"
        "github.com/vapor/crypto/sha3pool"
        "github.com/vapor/protocol"
@@ -16,16 +16,10 @@ import (
 )
 
 const (
-       //UnconfirmedTxPrefix is txpool unconfirmed transactions prefix
-       UnconfirmedTxPrefix      = "UTXS:"
        UnconfirmedTxCheckPeriod = 30 * time.Minute
        MaxUnconfirmedTxDuration = 24 * time.Hour
 )
 
-func calcUnconfirmedTxKey(formatKey string) []byte {
-       return []byte(UnconfirmedTxPrefix + formatKey)
-}
-
 // SortByTimestamp implements sort.Interface for AnnotatedTx slices
 type SortByTimestamp []*query.AnnotatedTx
 
@@ -54,37 +48,33 @@ func (w *Wallet) AddUnconfirmedTx(txD *protocol.TxDesc) {
 // 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
-               }
+       annotatedTxs, err := w.Store.ListUnconfirmedTransactions()
+       if err != nil {
+               return nil, err
+       }
 
+       newAnnotatedTxs := []*query.AnnotatedTx{}
+       for _, annotatedTx := range annotatedTxs {
                if accountID == "" || findTransactionsByAccount(annotatedTx, accountID) {
                        annotateTxsAsset(w, []*query.AnnotatedTx{annotatedTx})
-                       annotatedTxs = append([]*query.AnnotatedTx{annotatedTx}, annotatedTxs...)
+                       newAnnotatedTxs = append([]*query.AnnotatedTx{annotatedTx}, newAnnotatedTxs...)
                }
        }
 
-       sort.Sort(SortByTimestamp(annotatedTxs))
-       return annotatedTxs, nil
+       sort.Sort(SortByTimestamp(newAnnotatedTxs))
+       return newAnnotatedTxs, 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 {
+       annotatedTx, err := w.Store.GetUnconfirmedTransaction(txID)
+       if err != nil {
                return nil, err
        }
 
+       if annotatedTx == nil {
+               return nil, fmt.Errorf("No transaction(tx_id=%s) from txpool", txID)
+       }
        annotateTxsAsset(w, []*query.AnnotatedTx{annotatedTx})
        return annotatedTx, nil
 }
@@ -94,7 +84,8 @@ func (w *Wallet) RemoveUnconfirmedTx(txD *protocol.TxDesc) {
        if !w.checkRelatedTransaction(txD.Tx) {
                return
        }
-       w.DB.Delete(calcUnconfirmedTxKey(txD.Tx.ID.String()))
+
+       w.Store.DeleteUnconfirmedTransaction(txD.Tx.ID.String())
        w.AccountMgr.RemoveUnconfirmedUtxo(txD.Tx.ResultIds)
 }
 
@@ -121,7 +112,12 @@ func (w *Wallet) checkRelatedTransaction(tx *types.Tx) bool {
        for _, v := range tx.Outputs {
                var hash [32]byte
                sha3pool.Sum256(hash[:], v.ControlProgram())
-               if bytes := w.DB.Get(account.ContractKey(hash)); bytes != nil {
+               cp, err := w.AccountMgr.GetControlProgram(bc.NewHash(hash))
+               if err != nil {
+                       log.WithFields(log.Fields{"module": logModule, "err": err, "hash": string(hash[:])}).Error("checkRelatedTransaction fail.")
+                       continue
+               }
+               if cp != nil {
                        return true
                }
        }
@@ -131,7 +127,12 @@ func (w *Wallet) checkRelatedTransaction(tx *types.Tx) bool {
                if err != nil {
                        continue
                }
-               if bytes := w.DB.Get(account.StandardUTXOKey(outid)); bytes != nil {
+               utxo, err := w.Store.GetStandardUTXO(outid)
+               if err != nil {
+                       log.WithFields(log.Fields{"module": logModule, "err": err, "outputID": outid.String()}).Error("checkRelatedTransaction fail.")
+                       continue
+               }
+               if utxo != nil {
                        return true
                }
        }
@@ -148,14 +149,11 @@ func (w *Wallet) saveUnconfirmedTx(tx *types.Tx) error {
        annotatedTx := w.buildAnnotatedUnconfirmedTx(tx)
        annotatedTxs := []*query.AnnotatedTx{}
        annotatedTxs = append(annotatedTxs, annotatedTx)
-       annotateTxsAccount(annotatedTxs, w.DB)
+       w.annotateTxsAccount(annotatedTxs)
 
-       rawTx, err := json.Marshal(annotatedTxs[0])
-       if err != nil {
+       if err := w.Store.SetUnconfirmedTransaction(tx.ID.String(), annotatedTxs[0]); err != nil {
                return err
        }
-
-       w.DB.Set(calcUnconfirmedTxKey(tx.ID.String()), rawTx)
        return nil
 }
 
@@ -164,9 +162,10 @@ func (w *Wallet) delExpiredTxs() error {
        if err != nil {
                return err
        }
+
        for _, tx := range AnnotatedTx {
                if time.Now().After(time.Unix(int64(tx.Timestamp), 0).Add(MaxUnconfirmedTxDuration)) {
-                       w.DB.Delete(calcUnconfirmedTxKey(tx.ID.String()))
+                       w.Store.DeleteUnconfirmedTransaction(tx.ID.String())
                }
        }
        return nil
@@ -178,8 +177,10 @@ func (w *Wallet) delUnconfirmedTx() {
                log.WithFields(log.Fields{"module": logModule, "err": err}).Error("wallet fail on delUnconfirmedTx")
                return
        }
+
        ticker := time.NewTicker(UnconfirmedTxCheckPeriod)
        defer ticker.Stop()
+
        for {
                <-ticker.C
                if err := w.delExpiredTxs(); err != nil {
index 60ece60..5904845 100644 (file)
@@ -27,9 +27,11 @@ func TestWalletUnconfirmedTxs(t *testing.T) {
        defer os.RemoveAll(dirPath)
 
        testDB := dbm.NewDB("testdb", "leveldb", "temp")
+       testStore := NewMockWalletStore(testDB)
        defer os.RemoveAll("temp")
 
-       accountManager := account.NewManager(testDB, nil)
+       accountStore := NewMockAccountStore(testDB)
+       accountManager := account.NewManager(accountStore, nil)
        hsm, err := pseudohsm.New(dirPath)
        if err != nil {
                t.Fatal(err)
@@ -56,7 +58,7 @@ func TestWalletUnconfirmedTxs(t *testing.T) {
        asset := bc.AssetID{V0: 5}
 
        dispatcher := event.NewDispatcher()
-       w := mockWallet(testDB, accountManager, reg, nil, dispatcher, false)
+       w := mockWallet(testStore, accountManager, reg, nil, dispatcher, false)
        utxos := []*account.UTXO{}
        btmUtxo := mockUTXO(controlProg, consensus.BTMAssetID)
        utxos = append(utxos, btmUtxo)
@@ -126,7 +128,7 @@ func AnnotatedTxs(txs []*types.Tx, w *Wallet) []*query.AnnotatedTx {
                annotatedTxs = append(annotatedTxs, annotatedTx)
        }
 
-       annotateTxsAccount(annotatedTxs, w.DB)
+       w.annotateTxsAccount(annotatedTxs)
        annotateTxsAsset(w, annotatedTxs)
 
        return annotatedTxs
index 1eec505..0419daf 100644 (file)
@@ -1,54 +1,42 @@
 package wallet
 
 import (
-       "encoding/json"
-
        log "github.com/sirupsen/logrus"
 
        "github.com/vapor/account"
        "github.com/vapor/consensus"
        "github.com/vapor/consensus/segwit"
        "github.com/vapor/crypto/sha3pool"
-       dbm "github.com/vapor/database/leveldb"
-       "github.com/vapor/errors"
        "github.com/vapor/protocol/bc"
        "github.com/vapor/protocol/bc/types"
 )
 
 // GetAccountUtxos return all account unspent outputs
 func (w *Wallet) GetAccountUtxos(accountID string, id string, unconfirmed, isSmartContract bool, vote bool) []*account.UTXO {
-       prefix := account.UTXOPreFix
-       if isSmartContract {
-               prefix = account.SUTXOPrefix
-       }
-
        accountUtxos := []*account.UTXO{}
        if unconfirmed {
                accountUtxos = w.AccountMgr.ListUnconfirmedUtxo(accountID, isSmartContract)
        }
 
-       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.WithFields(log.Fields{"module": logModule, "err": err}).Warn("GetAccountUtxos fail on unmarshal utxo")
-                       continue
-               }
+       confirmedUTXOs, err := w.Store.ListAccountUTXOs(id, isSmartContract)
+       if err != nil {
+               log.WithFields(log.Fields{"module": logModule, "err": err}).Error("GetAccountUtxos fail.")
+       }
+       accountUtxos = append(accountUtxos, confirmedUTXOs...)
 
+       newAccountUtxos := []*account.UTXO{}
+       for _, accountUtxo := range accountUtxos {
                if vote && accountUtxo.Vote == nil {
                        continue
                }
-
                if accountID == accountUtxo.AccountID || accountID == "" {
-                       accountUtxos = append(accountUtxos, accountUtxo)
+                       newAccountUtxos = append(newAccountUtxos, accountUtxo)
                }
        }
-       return accountUtxos
+       return newAccountUtxos
 }
 
-func (w *Wallet) attachUtxos(batch dbm.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
+func (w *Wallet) attachUtxos(b *types.Block, txStatus *bc.TransactionStatus, store WalletStore) {
        for txIndex, tx := range b.Transactions {
                statusFail, err := txStatus.GetStatus(txIndex)
                if err != nil {
@@ -60,22 +48,22 @@ func (w *Wallet) attachUtxos(batch dbm.Batch, b *types.Block, txStatus *bc.Trans
                inputUtxos := txInToUtxos(tx, statusFail)
                for _, inputUtxo := range inputUtxos {
                        if segwit.IsP2WScript(inputUtxo.ControlProgram) {
-                               batch.Delete(account.StandardUTXOKey(inputUtxo.OutputID))
+                               w.AccountMgr.DeleteStandardUTXO(inputUtxo.OutputID)
                        } else {
-                               batch.Delete(account.ContractUTXOKey(inputUtxo.OutputID))
+                               store.DeleteContractUTXO(inputUtxo.OutputID)
                        }
                }
 
                //hand update the transaction output utxos
                outputUtxos := txOutToUtxos(tx, statusFail, b.Height)
                utxos := w.filterAccountUtxo(outputUtxos)
-               if err := batchSaveUtxos(utxos, batch); err != nil {
-                       log.WithFields(log.Fields{"module": logModule, "err": err}).Error("attachUtxos fail on batchSaveUtxos")
+               if err := w.saveUtxos(utxos, store); err != nil {
+                       log.WithFields(log.Fields{"module": logModule, "err": err}).Error("attachUtxos fail on saveUtxos")
                }
        }
 }
 
-func (w *Wallet) detachUtxos(batch dbm.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
+func (w *Wallet) detachUtxos(b *types.Block, txStatus *bc.TransactionStatus, store WalletStore) {
        for txIndex := len(b.Transactions) - 1; txIndex >= 0; txIndex-- {
                tx := b.Transactions[txIndex]
                for j := range tx.Outputs {
@@ -93,9 +81,9 @@ func (w *Wallet) detachUtxos(batch dbm.Batch, b *types.Block, txStatus *bc.Trans
                        }
 
                        if segwit.IsP2WScript(code) {
-                               batch.Delete(account.StandardUTXOKey(*tx.ResultIds[j]))
+                               w.AccountMgr.DeleteStandardUTXO(*tx.ResultIds[j])
                        } else {
-                               batch.Delete(account.ContractUTXOKey(*tx.ResultIds[j]))
+                               store.DeleteContractUTXO(*tx.ResultIds[j])
                        }
                }
 
@@ -107,7 +95,7 @@ func (w *Wallet) detachUtxos(batch dbm.Batch, b *types.Block, txStatus *bc.Trans
 
                inputUtxos := txInToUtxos(tx, statusFail)
                utxos := w.filterAccountUtxo(inputUtxos)
-               if err := batchSaveUtxos(utxos, batch); err != nil {
+               if err := w.saveUtxos(utxos, store); err != nil {
                        log.WithFields(log.Fields{"module": logModule, "err": err}).Error("detachUtxos fail on batchSaveUtxos")
                        return
                }
@@ -132,14 +120,12 @@ func (w *Wallet) filterAccountUtxo(utxos []*account.UTXO) []*account.UTXO {
 
                var hash [32]byte
                sha3pool.Sum256(hash[:], []byte(s))
-               data := w.DB.Get(account.ContractKey(hash))
-               if data == nil {
+               cp, err := w.AccountMgr.GetControlProgram(bc.NewHash(hash))
+               if err != nil {
+                       log.WithFields(log.Fields{"module": logModule, "err": err}).Error("filterAccountUtxo fail.")
                        continue
                }
-
-               cp := &account.CtrlProgram{}
-               if err := json.Unmarshal(data, cp); err != nil {
-                       log.WithFields(log.Fields{"module": logModule, "err": err}).Error("filterAccountUtxo fail on unmarshal control program")
+               if cp == nil {
                        continue
                }
 
@@ -154,17 +140,16 @@ func (w *Wallet) filterAccountUtxo(utxos []*account.UTXO) []*account.UTXO {
        return result
 }
 
-func batchSaveUtxos(utxos []*account.UTXO, batch dbm.Batch) error {
+func (w *Wallet) saveUtxos(utxos []*account.UTXO, store WalletStore) 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)
+                       if err := w.AccountMgr.SetStandardUTXO(utxo.OutputID, utxo); err != nil {
+                               return err
+                       }
                } else {
-                       batch.Set(account.ContractUTXOKey(utxo.OutputID), data)
+                       if err := store.SetContractUTXO(utxo.OutputID, utxo); err != nil {
+                               return err
+                       }
                }
        }
        return nil
index 5744392..2df108c 100644 (file)
@@ -18,6 +18,7 @@ import (
 
 func TestGetAccountUtxos(t *testing.T) {
        testDB := dbm.NewDB("testdb", "leveldb", "temp")
+       testStore := NewMockWalletStore(testDB)
        defer func() {
                testDB.Close()
                os.RemoveAll("temp")
@@ -40,16 +41,16 @@ func TestGetAccountUtxos(t *testing.T) {
                },
                {
                        dbUtxos: map[string]*account.UTXO{
-                               string(account.StandardUTXOKey(bc.Hash{V0: 1})): &account.UTXO{
+                               string(StandardUTXOKey(bc.Hash{V0: 1})): &account.UTXO{
                                        OutputID: bc.Hash{V0: 1},
                                },
-                               string(account.StandardUTXOKey(bc.Hash{V0: 2})): &account.UTXO{
+                               string(StandardUTXOKey(bc.Hash{V0: 2})): &account.UTXO{
                                        OutputID: bc.Hash{V0: 2},
                                },
-                               string(account.StandardUTXOKey(bc.Hash{V0: 3})): &account.UTXO{
+                               string(StandardUTXOKey(bc.Hash{V0: 3})): &account.UTXO{
                                        OutputID: bc.Hash{V0: 3},
                                },
-                               string(account.ContractUTXOKey(bc.Hash{V0: 4})): &account.UTXO{
+                               string(ContractUTXOKey(bc.Hash{V0: 4})): &account.UTXO{
                                        OutputID: bc.Hash{V0: 4},
                                },
                        },
@@ -64,16 +65,16 @@ func TestGetAccountUtxos(t *testing.T) {
                },
                {
                        dbUtxos: map[string]*account.UTXO{
-                               string(account.StandardUTXOKey(bc.Hash{V0: 1})): &account.UTXO{
+                               string(StandardUTXOKey(bc.Hash{V0: 1})): &account.UTXO{
                                        OutputID: bc.Hash{V0: 1},
                                },
-                               string(account.StandardUTXOKey(bc.Hash{V0: 2})): &account.UTXO{
+                               string(StandardUTXOKey(bc.Hash{V0: 2})): &account.UTXO{
                                        OutputID: bc.Hash{V0: 2},
                                },
-                               string(account.StandardUTXOKey(bc.Hash{V0: 3})): &account.UTXO{
+                               string(StandardUTXOKey(bc.Hash{V0: 3})): &account.UTXO{
                                        OutputID: bc.Hash{V0: 3},
                                },
-                               string(account.ContractUTXOKey(bc.Hash{V0: 4})): &account.UTXO{
+                               string(ContractUTXOKey(bc.Hash{V0: 4})): &account.UTXO{
                                        OutputID: bc.Hash{V0: 4},
                                },
                        },
@@ -92,16 +93,16 @@ func TestGetAccountUtxos(t *testing.T) {
                },
                {
                        dbUtxos: map[string]*account.UTXO{
-                               string(account.StandardUTXOKey(bc.Hash{V0: 1})): &account.UTXO{
+                               string(StandardUTXOKey(bc.Hash{V0: 1})): &account.UTXO{
                                        OutputID: bc.Hash{V0: 1},
                                },
-                               string(account.StandardUTXOKey(bc.Hash{V0: 1, V1: 2})): &account.UTXO{
+                               string(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{
+                               string(StandardUTXOKey(bc.Hash{V0: 2})): &account.UTXO{
                                        OutputID: bc.Hash{V0: 2},
                                },
-                               string(account.StandardUTXOKey(bc.Hash{V0: 2, V1: 2})): &account.UTXO{
+                               string(StandardUTXOKey(bc.Hash{V0: 2, V1: 2})): &account.UTXO{
                                        OutputID: bc.Hash{V0: 2, V1: 2},
                                },
                        },
@@ -121,10 +122,10 @@ func TestGetAccountUtxos(t *testing.T) {
                },
                {
                        dbUtxos: map[string]*account.UTXO{
-                               string(account.StandardUTXOKey(bc.Hash{V0: 3})): &account.UTXO{
+                               string(StandardUTXOKey(bc.Hash{V0: 3})): &account.UTXO{
                                        OutputID: bc.Hash{V0: 3},
                                },
-                               string(account.ContractUTXOKey(bc.Hash{V0: 4})): &account.UTXO{
+                               string(ContractUTXOKey(bc.Hash{V0: 4})): &account.UTXO{
                                        OutputID: bc.Hash{V0: 4},
                                },
                        },
@@ -153,10 +154,10 @@ func TestGetAccountUtxos(t *testing.T) {
                },
                {
                        dbUtxos: map[string]*account.UTXO{
-                               string(account.StandardUTXOKey(bc.Hash{V0: 3})): &account.UTXO{
+                               string(StandardUTXOKey(bc.Hash{V0: 3})): &account.UTXO{
                                        OutputID: bc.Hash{V0: 3},
                                },
-                               string(account.ContractUTXOKey(bc.Hash{V0: 4})): &account.UTXO{
+                               string(ContractUTXOKey(bc.Hash{V0: 4})): &account.UTXO{
                                        OutputID: bc.Hash{V0: 4},
                                },
                        },
@@ -185,7 +186,7 @@ func TestGetAccountUtxos(t *testing.T) {
                },
        }
 
-       w := &Wallet{DB: testDB}
+       w := &Wallet{Store: testStore}
        for i, c := range cases {
                for k, u := range c.dbUtxos {
                        data, err := json.Marshal(u)
@@ -195,7 +196,8 @@ func TestGetAccountUtxos(t *testing.T) {
                        testDB.Set([]byte(k), data)
                }
 
-               w.AccountMgr = account.NewManager(testDB, nil)
+               acccountStore := NewMockAccountStore(testDB)
+               w.AccountMgr = account.NewManager(acccountStore, nil)
                w.AccountMgr.AddUnconfirmedUtxo(c.unconfirmedUtxos)
                gotUtxos := w.GetAccountUtxos("", c.id, c.unconfirmed, c.isSmartContract, false)
                if !testutil.DeepEqual(gotUtxos, c.wantUtxos) {
@@ -210,6 +212,7 @@ func TestGetAccountUtxos(t *testing.T) {
 
 func TestFilterAccountUtxo(t *testing.T) {
        testDB := dbm.NewDB("testdb", "leveldb", "temp")
+       testStore := NewMockWalletStore(testDB)
        defer func() {
                testDB.Close()
                os.RemoveAll("temp")
@@ -227,7 +230,7 @@ func TestFilterAccountUtxo(t *testing.T) {
                },
                {
                        dbPrograms: map[string]*account.CtrlProgram{
-                               "436f6e74726163743a2a37a64a4e15a772ab43bf3f5956d0d1f353946496788e7f40d0ff1796286a6f": &account.CtrlProgram{
+                               "41533a013a2a37a64a4e15a772ab43bf3f5956d0d1f353946496788e7f40d0ff1796286a6f": &account.CtrlProgram{
                                        AccountID: "testAccount",
                                        Address:   "testAddress",
                                        KeyIndex:  53,
@@ -287,13 +290,13 @@ func TestFilterAccountUtxo(t *testing.T) {
                },
                {
                        dbPrograms: map[string]*account.CtrlProgram{
-                               "436f6e74726163743a2a37a64a4e15a772ab43bf3f5956d0d1f353946496788e7f40d0ff1796286a6f": &account.CtrlProgram{
+                               "41533a013a2a37a64a4e15a772ab43bf3f5956d0d1f353946496788e7f40d0ff1796286a6f": &account.CtrlProgram{
                                        AccountID: "testAccount",
                                        Address:   "testAddress",
                                        KeyIndex:  53,
                                        Change:    true,
                                },
-                               "436f6e74726163743adb4d86262c12ba70d50b3ca3ae102d5682436243bd1e8c79569603f75675036a": &account.CtrlProgram{
+                               "41533a013adb4d86262c12ba70d50b3ca3ae102d5682436243bd1e8c79569603f75675036a": &account.CtrlProgram{
                                        AccountID: "testAccount2",
                                        Address:   "testAddress2",
                                        KeyIndex:  72,
@@ -349,7 +352,12 @@ func TestFilterAccountUtxo(t *testing.T) {
                },
        }
 
-       w := &Wallet{DB: testDB}
+       accountStore := NewMockAccountStore(testDB)
+       accountManager := account.NewManager(accountStore, nil)
+       w := &Wallet{
+               Store:      testStore,
+               AccountMgr: accountManager,
+       }
        for i, c := range cases {
                for s, p := range c.dbPrograms {
                        data, err := json.Marshal(p)
index 92a24b9..ab865bb 100644 (file)
@@ -1,7 +1,6 @@
 package wallet
 
 import (
-       "encoding/json"
        "sync"
 
        log "github.com/sirupsen/logrus"
@@ -9,7 +8,6 @@ import (
        "github.com/vapor/account"
        "github.com/vapor/asset"
        "github.com/vapor/blockchain/pseudohsm"
-       dbm "github.com/vapor/database/leveldb"
        "github.com/vapor/errors"
        "github.com/vapor/event"
        "github.com/vapor/protocol"
@@ -25,10 +23,12 @@ const (
 
 var (
        currentVersion = uint(1)
-       walletKey      = []byte("walletInfo")
 
        errBestBlockNotFoundInCore = errors.New("best block not found in core")
        errWalletVersionMismatch   = errors.New("wallet version mismatch")
+       ErrGetWalletStatusInfo     = errors.New("failed get wallet info")
+       ErrGetAsset                = errors.New("Failed to find asset definition")
+       ErrAccntTxIDNotFound       = errors.New("account TXID not found")
 )
 
 //StatusInfo is base valid block info to handle orphan block rollback
@@ -42,36 +42,36 @@ type StatusInfo struct {
 
 //Wallet is related to storing account unspent outputs
 type Wallet struct {
-       DB              dbm.DB
+       Store           WalletStore
        rw              sync.RWMutex
-       status          StatusInfo
+       Status          StatusInfo
        TxIndexFlag     bool
        AccountMgr      *account.Manager
        AssetReg        *asset.Registry
        Hsm             *pseudohsm.HSM
-       chain           *protocol.Chain
+       Chain           *protocol.Chain
        RecoveryMgr     *recoveryManager
-       eventDispatcher *event.Dispatcher
-       txMsgSub        *event.Subscription
+       EventDispatcher *event.Dispatcher
+       TxMsgSub        *event.Subscription
 
        rescanCh chan struct{}
 }
 
 //NewWallet return a new wallet instance
-func NewWallet(walletDB dbm.DB, account *account.Manager, asset *asset.Registry, hsm *pseudohsm.HSM, chain *protocol.Chain, dispatcher *event.Dispatcher, txIndexFlag bool) (*Wallet, error) {
+func NewWallet(store WalletStore, account *account.Manager, asset *asset.Registry, hsm *pseudohsm.HSM, chain *protocol.Chain, dispatcher *event.Dispatcher, txIndexFlag bool) (*Wallet, error) {
        w := &Wallet{
-               DB:              walletDB,
+               Store:           store,
                AccountMgr:      account,
                AssetReg:        asset,
-               chain:           chain,
+               Chain:           chain,
                Hsm:             hsm,
-               RecoveryMgr:     newRecoveryManager(walletDB, account),
-               eventDispatcher: dispatcher,
+               RecoveryMgr:     NewRecoveryManager(store, account),
+               EventDispatcher: dispatcher,
                rescanCh:        make(chan struct{}, 1),
                TxIndexFlag:     txIndexFlag,
        }
 
-       if err := w.loadWalletInfo(); err != nil {
+       if err := w.LoadWalletInfo(); err != nil {
                return nil, err
        }
 
@@ -80,22 +80,22 @@ func NewWallet(walletDB dbm.DB, account *account.Manager, asset *asset.Registry,
        }
 
        var err error
-       w.txMsgSub, err = w.eventDispatcher.Subscribe(protocol.TxMsgEvent{})
+       w.TxMsgSub, err = w.EventDispatcher.Subscribe(protocol.TxMsgEvent{})
        if err != nil {
                return nil, err
        }
 
        go w.walletUpdater()
        go w.delUnconfirmedTx()
-       go w.memPoolTxQueryLoop()
+       go w.MemPoolTxQueryLoop()
        return w, nil
 }
 
-// memPoolTxQueryLoop constantly pass a transaction accepted by mempool to the wallet.
-func (w *Wallet) memPoolTxQueryLoop() {
+// MemPoolTxQueryLoop constantly pass a transaction accepted by mempool to the wallet.
+func (w *Wallet) MemPoolTxQueryLoop() {
        for {
                select {
-               case obj, ok := <-w.txMsgSub.Chan():
+               case obj, ok := <-w.TxMsgSub.Chan():
                        if !ok {
                                log.WithFields(log.Fields{"module": logModule}).Warning("tx pool tx msg subscription channel closed")
                                return
@@ -120,51 +120,50 @@ func (w *Wallet) memPoolTxQueryLoop() {
 }
 
 func (w *Wallet) checkWalletInfo() error {
-       if w.status.Version != currentVersion {
+       if w.Status.Version != currentVersion {
                return errWalletVersionMismatch
-       } else if !w.chain.BlockExist(&w.status.BestHash) {
+       } else if !w.Chain.BlockExist(&w.Status.BestHash) {
                return errBestBlockNotFoundInCore
        }
 
        return nil
 }
 
-//loadWalletInfo return stored wallet info and nil,
+//LoadWalletInfo return stored wallet info and nil,
 //if error, return initial wallet info and err
-func (w *Wallet) loadWalletInfo() error {
-       if rawWallet := w.DB.Get(walletKey); rawWallet != nil {
-               if err := json.Unmarshal(rawWallet, &w.status); err != nil {
-                       return err
-               }
+func (w *Wallet) LoadWalletInfo() error {
+       walletStatus, err := w.Store.GetWalletInfo()
+       if walletStatus == nil && err != ErrGetWalletStatusInfo {
+               return err
+       }
 
-               err := w.checkWalletInfo()
+       if walletStatus != nil {
+               w.Status = *walletStatus
+               err = w.checkWalletInfo()
                if err == nil {
                        return nil
                }
 
                log.WithFields(log.Fields{"module": logModule}).Warn(err.Error())
-               w.deleteAccountTxs()
-               w.deleteUtxos()
+               w.Store.DeleteWalletTransactions()
+               w.Store.DeleteWalletUTXOs()
        }
 
-       w.status.Version = currentVersion
-       w.status.WorkHash = bc.Hash{}
-       block, err := w.chain.GetBlockByHeight(0)
+       w.Status.Version = currentVersion
+       w.Status.WorkHash = bc.Hash{}
+       block, err := w.Chain.GetBlockByHeight(0)
        if err != nil {
                return err
        }
+
        return w.AttachBlock(block)
 }
 
-func (w *Wallet) commitWalletInfo(batch dbm.Batch) error {
-       rawWallet, err := json.Marshal(w.status)
-       if err != nil {
+func (w *Wallet) commitWalletInfo(store WalletStore) error {
+       if err := store.SetWalletInfo(&w.Status); err != nil {
                log.WithFields(log.Fields{"module": logModule, "err": err}).Error("save wallet info")
                return err
        }
-
-       batch.Set(walletKey, rawWallet)
-       batch.Write()
        return nil
 }
 
@@ -173,13 +172,13 @@ func (w *Wallet) AttachBlock(block *types.Block) error {
        w.rw.Lock()
        defer w.rw.Unlock()
 
-       if block.PreviousBlockHash != w.status.WorkHash {
+       if block.PreviousBlockHash != w.Status.WorkHash {
                log.Warn("wallet skip attachBlock due to status hash not equal to previous hash")
                return nil
        }
 
        blockHash := block.Hash()
-       txStatus, err := w.chain.GetTransactionStatus(&blockHash)
+       txStatus, err := w.Chain.GetTransactionStatus(&blockHash)
        if err != nil {
                return err
        }
@@ -189,19 +188,35 @@ func (w *Wallet) AttachBlock(block *types.Block) error {
                w.RecoveryMgr.finished()
        }
 
-       storeBatch := w.DB.NewBatch()
-       if err := w.indexTransactions(storeBatch, block, txStatus); err != nil {
+       annotatedTxs := w.filterAccountTxs(block, txStatus)
+       if err := saveExternalAssetDefinition(block, w.Store); err != nil {
+               return err
+       }
+
+       w.annotateTxsAccount(annotatedTxs)
+
+       newStore := w.Store.InitBatch()
+       if err := w.indexTransactions(block, txStatus, annotatedTxs, newStore); err != nil {
+               return err
+       }
+
+       w.attachUtxos(block, txStatus, newStore)
+       w.Status.WorkHeight = block.Height
+       w.Status.WorkHash = block.Hash()
+       if w.Status.WorkHeight >= w.Status.BestHeight {
+               w.Status.BestHeight = w.Status.WorkHeight
+               w.Status.BestHash = w.Status.WorkHash
+       }
+
+       if err := w.commitWalletInfo(newStore); err != nil {
                return err
        }
 
-       w.attachUtxos(storeBatch, block, txStatus)
-       w.status.WorkHeight = block.Height
-       w.status.WorkHash = block.Hash()
-       if w.status.WorkHeight >= w.status.BestHeight {
-               w.status.BestHeight = w.status.WorkHeight
-               w.status.BestHash = w.status.WorkHash
+       if err := newStore.CommitBatch(); err != nil {
+               return err
        }
-       return w.commitWalletInfo(storeBatch)
+
+       return nil
 }
 
 // DetachBlock detach a block and rollback state
@@ -210,32 +225,40 @@ func (w *Wallet) DetachBlock(block *types.Block) error {
        defer w.rw.Unlock()
 
        blockHash := block.Hash()
-       txStatus, err := w.chain.GetTransactionStatus(&blockHash)
+       txStatus, err := w.Chain.GetTransactionStatus(&blockHash)
        if err != nil {
                return err
        }
 
-       storeBatch := w.DB.NewBatch()
-       w.detachUtxos(storeBatch, block, txStatus)
-       w.deleteTransactions(storeBatch, w.status.BestHeight)
+       newStore := w.Store.InitBatch()
 
-       w.status.BestHeight = block.Height - 1
-       w.status.BestHash = block.PreviousBlockHash
+       w.detachUtxos(block, txStatus, newStore)
+       newStore.DeleteTransactions(w.Status.BestHeight)
 
-       if w.status.WorkHeight > w.status.BestHeight {
-               w.status.WorkHeight = w.status.BestHeight
-               w.status.WorkHash = w.status.BestHash
+       w.Status.BestHeight = block.Height - 1
+       w.Status.BestHash = block.PreviousBlockHash
+
+       if w.Status.WorkHeight > w.Status.BestHeight {
+               w.Status.WorkHeight = w.Status.BestHeight
+               w.Status.WorkHash = w.Status.BestHash
+       }
+       if err := w.commitWalletInfo(newStore); err != nil {
+               return err
        }
 
-       return w.commitWalletInfo(storeBatch)
+       if err := newStore.CommitBatch(); err != nil {
+               return err
+       }
+
+       return nil
 }
 
 //WalletUpdate process every valid block and reverse every invalid block which need to rollback
 func (w *Wallet) walletUpdater() {
        for {
                w.getRescanNotification()
-               for !w.chain.InMainChain(w.status.BestHash) {
-                       block, err := w.chain.GetBlockByHash(&w.status.BestHash)
+               for !w.Chain.InMainChain(w.Status.BestHash) {
+                       block, err := w.Chain.GetBlockByHash(&w.Status.BestHash)
                        if err != nil {
                                log.WithFields(log.Fields{"module": logModule, "err": err}).Error("walletUpdater GetBlockByHash")
                                return
@@ -247,7 +270,7 @@ func (w *Wallet) walletUpdater() {
                        }
                }
 
-               block, _ := w.chain.GetBlockByHeight(w.status.WorkHeight + 1)
+               block, _ := w.Chain.GetBlockByHeight(w.Status.WorkHeight + 1)
                if block == nil {
                        w.walletBlockWaiter()
                        continue
@@ -269,43 +292,6 @@ func (w *Wallet) RescanBlocks() {
        }
 }
 
-// deleteAccountTxs deletes all txs in wallet
-func (w *Wallet) deleteAccountTxs() {
-       storeBatch := w.DB.NewBatch()
-
-       txIter := w.DB.IteratorPrefix([]byte(TxPrefix))
-       defer txIter.Release()
-
-       for txIter.Next() {
-               storeBatch.Delete(txIter.Key())
-       }
-
-       txIndexIter := w.DB.IteratorPrefix([]byte(TxIndexPrefix))
-       defer txIndexIter.Release()
-
-       for txIndexIter.Next() {
-               storeBatch.Delete(txIndexIter.Key())
-       }
-
-       storeBatch.Write()
-}
-
-func (w *Wallet) deleteUtxos() {
-       storeBatch := w.DB.NewBatch()
-       ruIter := w.DB.IteratorPrefix([]byte(account.UTXOPreFix))
-       defer ruIter.Release()
-       for ruIter.Next() {
-               storeBatch.Delete(ruIter.Key())
-       }
-
-       suIter := w.DB.IteratorPrefix([]byte(account.SUTXOPrefix))
-       defer suIter.Release()
-       for suIter.Next() {
-               storeBatch.Delete(suIter.Key())
-       }
-       storeBatch.Write()
-}
-
 // DeleteAccount deletes account matching accountID, then rescan wallet
 func (w *Wallet) DeleteAccount(accountID string) (err error) {
        w.rw.Lock()
@@ -315,7 +301,7 @@ func (w *Wallet) DeleteAccount(accountID string) (err error) {
                return err
        }
 
-       w.deleteAccountTxs()
+       w.Store.DeleteWalletTransactions()
        w.RescanBlocks()
        return nil
 }
@@ -328,7 +314,7 @@ func (w *Wallet) UpdateAccountAlias(accountID string, newAlias string) (err erro
                return err
        }
 
-       w.deleteAccountTxs()
+       w.Store.DeleteWalletTransactions()
        w.RescanBlocks()
        return nil
 }
@@ -343,14 +329,14 @@ func (w *Wallet) getRescanNotification() {
 }
 
 func (w *Wallet) setRescanStatus() {
-       block, _ := w.chain.GetBlockByHeight(0)
-       w.status.WorkHash = bc.Hash{}
+       block, _ := w.Chain.GetBlockByHeight(0)
+       w.Status.WorkHash = bc.Hash{}
        w.AttachBlock(block)
 }
 
 func (w *Wallet) walletBlockWaiter() {
        select {
-       case <-w.chain.BlockWaiter(w.status.WorkHeight + 1):
+       case <-w.Chain.BlockWaiter(w.Status.WorkHeight + 1):
        case <-w.rescanCh:
                w.setRescanStatus()
        }
@@ -361,5 +347,5 @@ func (w *Wallet) GetWalletStatusInfo() StatusInfo {
        w.rw.RLock()
        defer w.rw.RUnlock()
 
-       return w.status
+       return w.Status
 }
index ac966f4..49ad693 100644 (file)
@@ -1,23 +1,30 @@
 package wallet
 
 import (
+       "encoding/binary"
+       "encoding/hex"
        "encoding/json"
+       "fmt"
        "io/ioutil"
        "os"
-       "reflect"
+       "sort"
+       "strings"
        "testing"
        "time"
 
        "github.com/vapor/account"
+       acc "github.com/vapor/account"
        "github.com/vapor/asset"
-       "github.com/vapor/blockchain/pseudohsm"
+       "github.com/vapor/blockchain/query"
        "github.com/vapor/blockchain/signers"
        "github.com/vapor/blockchain/txbuilder"
+       "github.com/vapor/common"
        "github.com/vapor/config"
        "github.com/vapor/consensus"
        "github.com/vapor/crypto/ed25519/chainkd"
-       "github.com/vapor/database"
+       "github.com/vapor/crypto/sha3pool"
        dbm "github.com/vapor/database/leveldb"
+       "github.com/vapor/errors"
        "github.com/vapor/event"
        "github.com/vapor/protocol"
        "github.com/vapor/protocol/bc"
@@ -33,7 +40,7 @@ func TestEncodeDecodeGlobalTxIndex(t *testing.T) {
                Position:  1,
        }
 
-       globalTxIdx := calcGlobalTxIndex(&want.BlockHash, want.Position)
+       globalTxIdx := CalcGlobalTxIndex(&want.BlockHash, want.Position)
        blockHashGot, positionGot := parseGlobalTxIdx(globalTxIdx)
        if *blockHashGot != want.BlockHash {
                t.Errorf("blockHash mismatch. Get: %v. Expect: %v", *blockHashGot, want.BlockHash)
@@ -53,35 +60,25 @@ func TestWalletVersion(t *testing.T) {
        defer os.RemoveAll(dirPath)
 
        testDB := dbm.NewDB("testdb", "leveldb", "temp")
+       walletStore := NewMockWalletStore(testDB)
        defer func() {
                testDB.Close()
                os.RemoveAll("temp")
        }()
 
        dispatcher := event.NewDispatcher()
-       w := mockWallet(testDB, nil, nil, nil, dispatcher, false)
+       w := mockWallet(walletStore, nil, nil, nil, dispatcher, false)
 
-       // legacy status test case
-       type legacyStatusInfo struct {
-               WorkHeight uint64
-               WorkHash   bc.Hash
-               BestHeight uint64
-               BestHash   bc.Hash
-       }
-       rawWallet, err := json.Marshal(legacyStatusInfo{})
-       if err != nil {
-               t.Fatal("Marshal legacyStatusInfo")
-       }
-
-       w.DB.Set(walletKey, rawWallet)
-       rawWallet = w.DB.Get(walletKey)
-       if rawWallet == nil {
-               t.Fatal("fail to load wallet StatusInfo")
+       walletStatus := new(StatusInfo)
+       if err := w.Store.SetWalletInfo(walletStatus); err != nil {
+               t.Fatal(err)
        }
 
-       if err := json.Unmarshal(rawWallet, &w.status); err != nil {
+       status, err := w.Store.GetWalletInfo()
+       if err != nil {
                t.Fatal(err)
        }
+       w.Status = *status
 
        if err := w.checkWalletInfo(); err != errWalletVersionMismatch {
                t.Fatal("fail to detect legacy wallet version")
@@ -89,315 +86,946 @@ func TestWalletVersion(t *testing.T) {
 
        // lower wallet version test case
        lowerVersion := StatusInfo{Version: currentVersion - 1}
-       rawWallet, err = json.Marshal(lowerVersion)
-       if err != nil {
-               t.Fatal("save wallet info")
-       }
-
-       w.DB.Set(walletKey, rawWallet)
-       rawWallet = w.DB.Get(walletKey)
-       if rawWallet == nil {
-               t.Fatal("fail to load wallet StatusInfo")
+       if err := w.Store.SetWalletInfo(&lowerVersion); err != nil {
+               t.Fatal(err)
        }
 
-       if err := json.Unmarshal(rawWallet, &w.status); err != nil {
+       status, err = w.Store.GetWalletInfo()
+       if err != nil {
                t.Fatal(err)
        }
+       w.Status = *status
 
        if err := w.checkWalletInfo(); err != errWalletVersionMismatch {
                t.Fatal("fail to detect expired wallet version")
        }
 }
 
-func TestWalletUpdate(t *testing.T) {
-       dirPath, err := ioutil.TempDir(".", "")
-       if err != nil {
-               t.Fatal(err)
-       }
-       defer os.RemoveAll(dirPath)
+func mockUTXO(controlProg *account.CtrlProgram, assetID *bc.AssetID) *account.UTXO {
+       utxo := &account.UTXO{}
+       utxo.OutputID = bc.Hash{V0: 1}
+       utxo.SourceID = bc.Hash{V0: 2}
+       utxo.AssetID = *assetID
+       utxo.Amount = 1000000000
+       utxo.SourcePos = 0
+       utxo.ControlProgram = controlProg.ControlProgram
+       utxo.AccountID = controlProg.AccountID
+       utxo.Address = controlProg.Address
+       utxo.ControlProgramIndex = controlProg.KeyIndex
+       return utxo
+}
 
-       config.CommonConfig = config.DefaultConfig()
-       testDB := dbm.NewDB("testdb", "leveldb", "temp")
-       defer func() {
-               testDB.Close()
-               os.RemoveAll("temp")
-       }()
+func mockTxData(utxos []*account.UTXO, testAccount *account.Account) (*txbuilder.Template, *types.TxData, error) {
+       tplBuilder := txbuilder.NewBuilder(time.Now())
 
-       store := database.NewStore(testDB)
-       dispatcher := event.NewDispatcher()
-       txPool := protocol.NewTxPool(store, dispatcher)
+       for _, utxo := range utxos {
+               txInput, sigInst, err := account.UtxoToInputs(testAccount.Signer, utxo)
+               if err != nil {
+                       return nil, nil, err
+               }
+               tplBuilder.AddInput(txInput, sigInst)
 
-       chain, err := protocol.NewChain(store, txPool, dispatcher)
-       if err != nil {
-               t.Fatal(err)
+               out := &types.TxOutput{}
+               if utxo.AssetID == *consensus.BTMAssetID {
+                       out = types.NewIntraChainOutput(utxo.AssetID, 100, utxo.ControlProgram)
+               } else {
+                       out = types.NewIntraChainOutput(utxo.AssetID, utxo.Amount, utxo.ControlProgram)
+               }
+               tplBuilder.AddOutput(out)
        }
 
-       accountManager := account.NewManager(testDB, chain)
-       hsm, err := pseudohsm.New(dirPath)
-       if err != nil {
-               t.Fatal(err)
+       return tplBuilder.Build()
+}
+
+func mockWallet(store WalletStore, account *account.Manager, asset *asset.Registry, chain *protocol.Chain, dispatcher *event.Dispatcher, txIndexFlag bool) *Wallet {
+       wallet := &Wallet{
+               Store:           store,
+               AccountMgr:      account,
+               AssetReg:        asset,
+               Chain:           chain,
+               RecoveryMgr:     NewRecoveryManager(store, account),
+               EventDispatcher: dispatcher,
+               TxIndexFlag:     txIndexFlag,
        }
+       wallet.TxMsgSub, _ = wallet.EventDispatcher.Subscribe(protocol.TxMsgEvent{})
+       return wallet
+}
 
-       xpub1, _, err := hsm.XCreate("test_pub1", "password", "en")
-       if err != nil {
-               t.Fatal(err)
+func mockSingleBlock(tx *types.Tx) *types.Block {
+       return &types.Block{
+               BlockHeader: types.BlockHeader{
+                       Version: 1,
+                       Height:  1,
+               },
+               Transactions: []*types.Tx{config.GenesisTx(), tx},
        }
+}
 
-       testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub}, 1, "testAccount", signers.BIP0044)
-       if err != nil {
-               t.Fatal(err)
+// errors
+var (
+       errAccntTxIDNotFound = errors.New("account TXID not found")
+       errGetAsset          = errors.New("Failed to find asset definition")
+)
+
+const (
+       utxoPrefix byte = iota //UTXOPrefix is StandardUTXOKey prefix
+       contractPrefix
+       contractIndexPrefix
+       accountPrefix // AccountPrefix is account ID prefix
+       accountIndexPrefix
+)
+
+// leveldb key prefix
+var (
+       colon               byte = 0x3a
+       accountStore             = []byte("AS:")
+       UTXOPrefix               = append(accountStore, utxoPrefix, colon)
+       ContractPrefix           = append(accountStore, contractPrefix, colon)
+       ContractIndexPrefix      = append(accountStore, contractIndexPrefix, colon)
+       AccountPrefix            = append(accountStore, accountPrefix, colon) // AccountPrefix is account ID prefix
+       AccountIndexPrefix       = append(accountStore, accountIndexPrefix, colon)
+)
+
+const (
+       sutxoPrefix byte = iota //SUTXOPrefix is ContractUTXOKey prefix
+       accountAliasPrefix
+       txPrefix            //TxPrefix is wallet database transactions prefix
+       txIndexPrefix       //TxIndexPrefix is wallet database tx index prefix
+       unconfirmedTxPrefix //UnconfirmedTxPrefix is txpool unconfirmed transactions prefix
+       globalTxIndexPrefix //GlobalTxIndexPrefix is wallet database global tx index prefix
+       walletKey
+       miningAddressKey
+       coinbaseAbKey
+       recoveryKey //recoveryKey key for db store recovery info.
+)
+
+var (
+       walletStore         = []byte("WS:")
+       SUTXOPrefix         = append(walletStore, sutxoPrefix, colon)
+       AccountAliasPrefix  = append(walletStore, accountAliasPrefix, colon)
+       TxPrefix            = append(walletStore, txPrefix, colon)            //TxPrefix is wallet database transactions prefix
+       TxIndexPrefix       = append(walletStore, txIndexPrefix, colon)       //TxIndexPrefix is wallet database tx index prefix
+       UnconfirmedTxPrefix = append(walletStore, unconfirmedTxPrefix, colon) //UnconfirmedTxPrefix is txpool unconfirmed transactions prefix
+       GlobalTxIndexPrefix = append(walletStore, globalTxIndexPrefix, colon) //GlobalTxIndexPrefix is wallet database global tx index prefix
+       WalletKey           = append(walletStore, walletKey)
+       MiningAddressKey    = append(walletStore, miningAddressKey)
+       CoinbaseAbKey       = append(walletStore, coinbaseAbKey)
+       RecoveryKey         = append(walletStore, recoveryKey)
+)
+
+func accountIndexKey(xpubs []chainkd.XPub) []byte {
+       var hash [32]byte
+       var xPubs []byte
+       cpy := append([]chainkd.XPub{}, xpubs[:]...)
+       sort.Sort(signers.SortKeys(cpy))
+       for _, xpub := range cpy {
+               xPubs = append(xPubs, xpub[:]...)
        }
+       sha3pool.Sum256(hash[:], xPubs)
+       return append([]byte(AccountIndexPrefix), hash[:]...)
+}
 
-       controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
-       if err != nil {
-               t.Fatal(err)
+func Bip44ContractIndexKey(accountID string, change bool) []byte {
+       key := append([]byte(ContractIndexPrefix), accountID...)
+       if change {
+               return append(key, []byte{1}...)
        }
+       return append(key, []byte{0}...)
+}
 
-       controlProg.KeyIndex = 1
+// ContractKey account control promgram store prefix
+func ContractKey(hash bc.Hash) []byte {
+       return append([]byte(ContractPrefix), hash.Bytes()...)
+}
 
-       reg := asset.NewRegistry(testDB, chain)
-       asset := bc.AssetID{V0: 5}
+// AccountIDKey account id store prefix
+func AccountIDKey(accountID string) []byte {
+       return append([]byte(AccountPrefix), []byte(accountID)...)
+}
 
-       utxos := []*account.UTXO{}
-       btmUtxo := mockUTXO(controlProg, consensus.BTMAssetID)
-       utxos = append(utxos, btmUtxo)
-       OtherUtxo := mockUTXO(controlProg, &asset)
-       utxos = append(utxos, OtherUtxo)
+// StandardUTXOKey makes an account unspent outputs key to store
+func StandardUTXOKey(id bc.Hash) []byte {
+       return append(UTXOPrefix, id.Bytes()...)
+}
 
-       _, txData, err := mockTxData(utxos, testAccount)
-       if err != nil {
-               t.Fatal(err)
+// ContractUTXOKey makes a smart contract unspent outputs key to store
+func ContractUTXOKey(id bc.Hash) []byte {
+       return append(SUTXOPrefix, id.Bytes()...)
+}
+
+func calcDeleteKey(blockHeight uint64) []byte {
+       return []byte(fmt.Sprintf("%s%016x", TxPrefix, blockHeight))
+}
+
+func calcTxIndexKey(txID string) []byte {
+       return append(TxIndexPrefix, []byte(txID)...)
+}
+
+func calcAnnotatedKey(formatKey string) []byte {
+       return append(TxPrefix, []byte(formatKey)...)
+}
+
+func calcUnconfirmedTxKey(formatKey string) []byte {
+       return append(UnconfirmedTxPrefix, []byte(formatKey)...)
+}
+
+func CalcGlobalTxIndexKey(txID string) []byte {
+       return append(GlobalTxIndexPrefix, []byte(txID)...)
+}
+
+func CalcGlobalTxIndex(blockHash *bc.Hash, position uint64) []byte {
+       txIdx := make([]byte, 40)
+       copy(txIdx[:32], blockHash.Bytes())
+       binary.BigEndian.PutUint64(txIdx[32:], position)
+       return txIdx
+}
+
+func formatKey(blockHeight uint64, position uint32) string {
+       return fmt.Sprintf("%016x%08x", blockHeight, position)
+}
+
+func contractIndexKey(accountID string) []byte {
+       return append([]byte(ContractIndexPrefix), []byte(accountID)...)
+}
+
+func accountAliasKey(name string) []byte {
+       return append([]byte(AccountAliasPrefix), []byte(name)...)
+}
+
+// MockWalletStore store wallet using leveldb
+type MockWalletStore struct {
+       db    dbm.DB
+       batch dbm.Batch
+}
+
+// NewMockWalletStore create new MockWalletStore struct
+func NewMockWalletStore(db dbm.DB) *MockWalletStore {
+       return &MockWalletStore{
+               db:    db,
+               batch: nil,
        }
+}
 
-       tx := types.NewTx(*txData)
-       block := mockSingleBlock(tx)
-       txStatus := bc.NewTransactionStatus()
-       txStatus.SetStatus(0, false)
-       txStatus.SetStatus(1, false)
-       store.SaveBlock(block, txStatus)
+// InitBatch initial new wallet store
+func (store *MockWalletStore) InitBatch() WalletStore {
+       newStore := NewMockWalletStore(store.db)
+       newStore.batch = newStore.db.NewBatch()
+       return newStore
+}
 
-       w := mockWallet(testDB, accountManager, reg, chain, dispatcher, true)
-       err = w.AttachBlock(block)
-       if err != nil {
-               t.Fatal(err)
+// CommitBatch commit batch
+func (store *MockWalletStore) CommitBatch() error {
+       if store.batch == nil {
+               return errors.New("MockWalletStore commit fail, store batch is nil.")
        }
+       store.batch.Write()
+       store.batch = nil
+       return nil
+}
 
-       if _, err := w.GetTransactionByTxID(tx.ID.String()); err != nil {
-               t.Fatal(err)
+// DeleteContractUTXO delete contract utxo by outputID
+func (store *MockWalletStore) DeleteContractUTXO(outputID bc.Hash) {
+       if store.batch == nil {
+               store.db.Delete(ContractUTXOKey(outputID))
+       } else {
+               store.batch.Delete(ContractUTXOKey(outputID))
        }
+}
 
-       wants, err := w.GetTransactions(testAccount.ID, "", 1, false)
-       if len(wants) != 1 {
-               t.Fatal(err)
+// DeleteRecoveryStatus delete recovery status
+func (store *MockWalletStore) DeleteRecoveryStatus() {
+       if store.batch == nil {
+               store.db.Delete(RecoveryKey)
+       } else {
+               store.batch.Delete(RecoveryKey)
        }
+}
 
-       if wants[0].ID != tx.ID {
-               t.Fatal("account txID mismatch")
+// DeleteTransactions delete transactions when orphan block rollback
+func (store *MockWalletStore) DeleteTransactions(height uint64) {
+       batch := store.db.NewBatch()
+       if store.batch != nil {
+               batch = store.batch
        }
+       txIter := store.db.IteratorPrefix(calcDeleteKey(height))
+       defer txIter.Release()
 
-       for position, tx := range block.Transactions {
-               get := w.DB.Get(calcGlobalTxIndexKey(tx.ID.String()))
-               bh := block.BlockHeader.Hash()
-               expect := calcGlobalTxIndex(&bh, uint64(position))
-               if !reflect.DeepEqual(get, expect) {
-                       t.Fatalf("position#%d: compare retrieved globalTxIdx err", position)
+       tmpTx := query.AnnotatedTx{}
+       for txIter.Next() {
+               if err := json.Unmarshal(txIter.Value(), &tmpTx); err == nil {
+                       batch.Delete(calcTxIndexKey(tmpTx.ID.String()))
                }
+               batch.Delete(txIter.Key())
+       }
+       if store.batch == nil {
+               batch.Write()
        }
 }
 
-func TestRescanWallet(t *testing.T) {
-       // prepare wallet & db
-       dirPath, err := ioutil.TempDir(".", "")
-       if err != nil {
-               t.Fatal(err)
+// DeleteUnconfirmedTransaction delete unconfirmed tx by txID
+func (store *MockWalletStore) DeleteUnconfirmedTransaction(txID string) {
+       if store.batch == nil {
+               store.db.Delete(calcUnconfirmedTxKey(txID))
+       } else {
+               store.batch.Delete(calcUnconfirmedTxKey(txID))
        }
-       defer os.RemoveAll(dirPath)
+}
 
-       config.CommonConfig = config.DefaultConfig()
-       testDB := dbm.NewDB("testdb", "leveldb", "temp")
-       defer func() {
-               testDB.Close()
-               os.RemoveAll("temp")
-       }()
+// DeleteWalletTransactions delete all txs in wallet
+func (store *MockWalletStore) DeleteWalletTransactions() {
+       batch := store.db.NewBatch()
+       if store.batch != nil {
+               batch = store.batch
+       }
+       txIter := store.db.IteratorPrefix([]byte(TxPrefix))
+       defer txIter.Release()
 
-       store := database.NewStore(testDB)
-       dispatcher := event.NewDispatcher()
-       txPool := protocol.NewTxPool(store, dispatcher)
-       chain, err := protocol.NewChain(store, txPool, dispatcher)
-       if err != nil {
-               t.Fatal(err)
+       for txIter.Next() {
+               batch.Delete(txIter.Key())
        }
 
-       statusInfo := StatusInfo{
-               Version:  currentVersion,
-               WorkHash: bc.Hash{V0: 0xff},
+       txIndexIter := store.db.IteratorPrefix([]byte(TxIndexPrefix))
+       defer txIndexIter.Release()
+
+       for txIndexIter.Next() {
+               batch.Delete(txIndexIter.Key())
        }
-       rawWallet, err := json.Marshal(statusInfo)
-       if err != nil {
-               t.Fatal("save wallet info")
+       if store.batch == nil {
+               batch.Write()
        }
+}
 
-       w := mockWallet(testDB, nil, nil, chain, dispatcher, false)
-       w.DB.Set(walletKey, rawWallet)
-       rawWallet = w.DB.Get(walletKey)
-       if rawWallet == nil {
-               t.Fatal("fail to load wallet StatusInfo")
+// DeleteWalletUTXOs delete all txs in wallet
+func (store *MockWalletStore) DeleteWalletUTXOs() {
+       batch := store.db.NewBatch()
+       if store.batch != nil {
+               batch = store.batch
+       }
+       ruIter := store.db.IteratorPrefix([]byte(UTXOPrefix))
+       defer ruIter.Release()
+       for ruIter.Next() {
+               batch.Delete(ruIter.Key())
        }
 
-       if err := json.Unmarshal(rawWallet, &w.status); err != nil {
-               t.Fatal(err)
+       suIter := store.db.IteratorPrefix([]byte(SUTXOPrefix))
+       defer suIter.Release()
+       for suIter.Next() {
+               batch.Delete(suIter.Key())
+       }
+       if store.batch == nil {
+               batch.Write()
        }
+}
 
-       // rescan wallet
-       if err := w.loadWalletInfo(); err != nil {
-               t.Fatal(err)
+// GetAsset get asset by assetID
+func (store *MockWalletStore) GetAsset(assetID *bc.AssetID) (*asset.Asset, error) {
+       definitionByte := store.db.Get(asset.ExtAssetKey(assetID))
+       if definitionByte == nil {
+               return nil, errGetAsset
+       }
+       definitionMap := make(map[string]interface{})
+       if err := json.Unmarshal(definitionByte, &definitionMap); err != nil {
+               return nil, err
        }
+       alias := assetID.String()
+       externalAsset := &asset.Asset{
+               AssetID:           *assetID,
+               Alias:             &alias,
+               DefinitionMap:     definitionMap,
+               RawDefinitionByte: definitionByte,
+       }
+       return externalAsset, nil
+}
 
-       block := config.GenesisBlock()
-       if w.status.WorkHash != block.Hash() {
-               t.Fatal("reattach from genesis block")
+// GetGlobalTransactionIndex get global tx by txID
+func (store *MockWalletStore) GetGlobalTransactionIndex(txID string) []byte {
+       return store.db.Get(CalcGlobalTxIndexKey(txID))
+}
+
+// GetStandardUTXO get standard utxo by id
+func (store *MockWalletStore) GetStandardUTXO(outid bc.Hash) (*acc.UTXO, error) {
+       rawUTXO := store.db.Get(StandardUTXOKey(outid))
+       if rawUTXO == nil {
+               return nil, fmt.Errorf("failed get standard UTXO, outputID: %s ", outid.String())
+       }
+       UTXO := new(acc.UTXO)
+       if err := json.Unmarshal(rawUTXO, UTXO); err != nil {
+               return nil, err
        }
+       return UTXO, nil
 }
 
-func TestMemPoolTxQueryLoop(t *testing.T) {
-       dirPath, err := ioutil.TempDir(".", "")
+// GetTransaction get tx by txid
+func (store *MockWalletStore) GetTransaction(txID string) (*query.AnnotatedTx, error) {
+       formatKey := store.db.Get(calcTxIndexKey(txID))
+       if formatKey == nil {
+               return nil, errAccntTxIDNotFound
+       }
+       rawTx := store.db.Get(calcAnnotatedKey(string(formatKey)))
+       tx := new(query.AnnotatedTx)
+       if err := json.Unmarshal(rawTx, tx); err != nil {
+               return nil, err
+       }
+       return tx, nil
+}
+
+// GetUnconfirmedTransaction get unconfirmed tx by txID
+func (store *MockWalletStore) GetUnconfirmedTransaction(txID string) (*query.AnnotatedTx, error) {
+       rawUnconfirmedTx := store.db.Get(calcUnconfirmedTxKey(txID))
+       if rawUnconfirmedTx == nil {
+               return nil, fmt.Errorf("failed get unconfirmed tx, txID: %s ", txID)
+       }
+       tx := new(query.AnnotatedTx)
+       if err := json.Unmarshal(rawUnconfirmedTx, tx); err != nil {
+               return nil, err
+       }
+       return tx, nil
+}
+
+// GetRecoveryStatus delete recovery status
+func (store *MockWalletStore) GetRecoveryStatus() (*RecoveryState, error) {
+       rawStatus := store.db.Get(RecoveryKey)
+       if rawStatus == nil {
+               return nil, ErrGetRecoveryStatus
+       }
+       state := new(RecoveryState)
+       if err := json.Unmarshal(rawStatus, state); err != nil {
+               return nil, err
+       }
+       return state, nil
+}
+
+// GetWalletInfo get wallet information
+func (store *MockWalletStore) GetWalletInfo() (*StatusInfo, error) {
+       rawStatus := store.db.Get([]byte(WalletKey))
+       if rawStatus == nil {
+               return nil, fmt.Errorf("failed get wallet info")
+       }
+       status := new(StatusInfo)
+       if err := json.Unmarshal(rawStatus, status); err != nil {
+               return nil, err
+       }
+       return status, nil
+}
+
+// ListAccountUTXOs get all account unspent outputs
+func (store *MockWalletStore) ListAccountUTXOs(id string, isSmartContract bool) ([]*acc.UTXO, error) {
+       prefix := UTXOPrefix
+       if isSmartContract {
+               prefix = SUTXOPrefix
+       }
+
+       idBytes, err := hex.DecodeString(id)
        if err != nil {
-               t.Fatal(err)
+               return nil, err
        }
-       config.CommonConfig = config.DefaultConfig()
-       testDB := dbm.NewDB("testdb", "leveldb", dirPath)
-       defer func() {
-               testDB.Close()
-               os.RemoveAll(dirPath)
-       }()
 
-       store := database.NewStore(testDB)
-       dispatcher := event.NewDispatcher()
-       txPool := protocol.NewTxPool(store, dispatcher)
+       accountUtxoIter := store.db.IteratorPrefix(append(prefix, idBytes...))
+       defer accountUtxoIter.Release()
+
+       confirmedUTXOs := []*acc.UTXO{}
+       for accountUtxoIter.Next() {
+               utxo := new(acc.UTXO)
+               if err := json.Unmarshal(accountUtxoIter.Value(), utxo); err != nil {
+                       return nil, err
+               }
+
+               confirmedUTXOs = append(confirmedUTXOs, utxo)
+       }
+       return confirmedUTXOs, nil
+}
+
+func (store *MockWalletStore) ListTransactions(accountID string, StartTxID string, count uint, unconfirmed bool) ([]*query.AnnotatedTx, error) {
+       annotatedTxs := []*query.AnnotatedTx{}
+       var startKey []byte
+       preFix := TxPrefix
+
+       if StartTxID != "" {
+               if unconfirmed {
+                       startKey = calcUnconfirmedTxKey(StartTxID)
+               } else {
+                       formatKey := store.db.Get(calcTxIndexKey(StartTxID))
+                       if formatKey == nil {
+                               return nil, errAccntTxIDNotFound
+                       }
+                       startKey = calcAnnotatedKey(string(formatKey))
+               }
+       }
+
+       if unconfirmed {
+               preFix = UnconfirmedTxPrefix
+       }
+
+       itr := store.db.IteratorPrefixWithStart([]byte(preFix), startKey, true)
+       defer itr.Release()
 
-       chain, err := protocol.NewChain(store, txPool, dispatcher)
+       for txNum := count; itr.Next() && txNum > 0; txNum-- {
+               annotatedTx := new(query.AnnotatedTx)
+               if err := json.Unmarshal(itr.Value(), &annotatedTx); err != nil {
+                       return nil, err
+               }
+               annotatedTxs = append(annotatedTxs, annotatedTx)
+       }
+
+       return annotatedTxs, nil
+}
+
+// ListUnconfirmedTransactions get all unconfirmed txs
+func (store *MockWalletStore) ListUnconfirmedTransactions() ([]*query.AnnotatedTx, error) {
+       annotatedTxs := []*query.AnnotatedTx{}
+       txIter := store.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
+               }
+               annotatedTxs = append(annotatedTxs, annotatedTx)
+       }
+       return annotatedTxs, nil
+}
+
+// SetAssetDefinition set assetID and definition
+func (store *MockWalletStore) SetAssetDefinition(assetID *bc.AssetID, definition []byte) {
+       if store.batch == nil {
+               store.db.Set(asset.ExtAssetKey(assetID), definition)
+       } else {
+               store.batch.Set(asset.ExtAssetKey(assetID), definition)
+       }
+}
+
+// SetContractUTXO set standard utxo
+func (store *MockWalletStore) SetContractUTXO(outputID bc.Hash, utxo *acc.UTXO) error {
+       data, err := json.Marshal(utxo)
        if err != nil {
-               t.Fatal(err)
+               return err
        }
+       if store.batch == nil {
+               store.db.Set(ContractUTXOKey(outputID), data)
+       } else {
+               store.batch.Set(ContractUTXOKey(outputID), data)
+       }
+       return nil
+}
 
-       accountManager := account.NewManager(testDB, chain)
-       hsm, err := pseudohsm.New(dirPath)
+// SetGlobalTransactionIndex set global tx index by blockhash and position
+func (store *MockWalletStore) SetGlobalTransactionIndex(globalTxID string, blockHash *bc.Hash, position uint64) {
+       if store.batch == nil {
+               store.db.Set(CalcGlobalTxIndexKey(globalTxID), CalcGlobalTxIndex(blockHash, position))
+       } else {
+               store.batch.Set(CalcGlobalTxIndexKey(globalTxID), CalcGlobalTxIndex(blockHash, position))
+       }
+}
+
+// SetRecoveryStatus set recovery status
+func (store *MockWalletStore) SetRecoveryStatus(recoveryState *RecoveryState) error {
+       rawStatus, err := json.Marshal(recoveryState)
        if err != nil {
-               t.Fatal(err)
+               return err
+       }
+       if store.batch == nil {
+               store.db.Set(RecoveryKey, rawStatus)
+       } else {
+               store.batch.Set(RecoveryKey, rawStatus)
+       }
+       return nil
+}
+
+// SetTransaction set raw transaction by block height and tx position
+func (store *MockWalletStore) SetTransaction(height uint64, tx *query.AnnotatedTx) error {
+       batch := store.db.NewBatch()
+       if store.batch != nil {
+               batch = store.batch
        }
 
-       xpub1, _, err := hsm.XCreate("test_pub1", "password", "en")
+       rawTx, err := json.Marshal(tx)
        if err != nil {
-               t.Fatal(err)
+               return err
        }
+       batch.Set(calcAnnotatedKey(formatKey(height, tx.Position)), rawTx)
+       batch.Set(calcTxIndexKey(tx.ID.String()), []byte(formatKey(height, tx.Position)))
+
+       if store.batch == nil {
+               batch.Write()
+       }
+       return nil
+}
 
-       testAccount, err := accountManager.Create([]chainkd.XPub{xpub1.XPub}, 1, "testAccount", signers.BIP0044)
+// SetUnconfirmedTransaction set unconfirmed tx by txID
+func (store *MockWalletStore) SetUnconfirmedTransaction(txID string, tx *query.AnnotatedTx) error {
+       rawTx, err := json.Marshal(tx)
        if err != nil {
-               t.Fatal(err)
+               return err
+       }
+       if store.batch == nil {
+               store.db.Set(calcUnconfirmedTxKey(txID), rawTx)
+       } else {
+               store.batch.Set(calcUnconfirmedTxKey(txID), rawTx)
        }
+       return nil
+}
 
-       controlProg, err := accountManager.CreateAddress(testAccount.ID, false)
+// SetWalletInfo get wallet information
+func (store *MockWalletStore) SetWalletInfo(status *StatusInfo) error {
+       rawWallet, err := json.Marshal(status)
        if err != nil {
-               t.Fatal(err)
+               return err
+       }
+
+       if store.batch == nil {
+               store.db.Set([]byte(WalletKey), rawWallet)
+       } else {
+               store.batch.Set([]byte(WalletKey), rawWallet)
        }
+       return nil
+}
+
+type MockAccountStore struct {
+       db    dbm.DB
+       batch dbm.Batch
+}
 
-       controlProg.KeyIndex = 1
+// NewAccountStore create new MockAccountStore.
+func NewMockAccountStore(db dbm.DB) *MockAccountStore {
+       return &MockAccountStore{
+               db:    db,
+               batch: nil,
+       }
+}
 
-       reg := asset.NewRegistry(testDB, chain)
-       asset := bc.AssetID{V0: 5}
+// InitBatch initial new account store
+func (store *MockAccountStore) InitBatch() acc.AccountStore {
+       newStore := NewMockAccountStore(store.db)
+       newStore.batch = newStore.db.NewBatch()
+       return newStore
+}
 
-       utxos := []*account.UTXO{}
-       btmUtxo := mockUTXO(controlProg, consensus.BTMAssetID)
-       utxos = append(utxos, btmUtxo)
-       OtherUtxo := mockUTXO(controlProg, &asset)
-       utxos = append(utxos, OtherUtxo)
+// CommitBatch commit batch
+func (store *MockAccountStore) CommitBatch() error {
+       if store.batch == nil {
+               return errors.New("MockAccountStore commit fail, store batch is nil.")
+       }
+       store.batch.Write()
+       store.batch = nil
+       return nil
+}
 
-       _, txData, err := mockTxData(utxos, testAccount)
-       if err != nil {
-               t.Fatal(err)
+// DeleteAccount set account account ID, account alias and raw account.
+func (store *MockAccountStore) DeleteAccount(account *acc.Account) error {
+       batch := store.db.NewBatch()
+       if store.batch != nil {
+               batch = store.batch
        }
 
-       tx := types.NewTx(*txData)
-       //block := mockSingleBlock(tx)
-       txStatus := bc.NewTransactionStatus()
-       txStatus.SetStatus(0, false)
-       w, err := NewWallet(testDB, accountManager, reg, hsm, chain, dispatcher, false)
-       go w.memPoolTxQueryLoop()
-       w.eventDispatcher.Post(protocol.TxMsgEvent{TxMsg: &protocol.TxPoolMsg{TxDesc: &protocol.TxDesc{Tx: tx}, MsgType: protocol.MsgNewTx}})
-       time.Sleep(time.Millisecond * 10)
-       if _, err = w.GetUnconfirmedTxByTxID(tx.ID.String()); err != nil {
-               t.Fatal("disaptch new tx msg error:", err)
-       }
-       w.eventDispatcher.Post(protocol.TxMsgEvent{TxMsg: &protocol.TxPoolMsg{TxDesc: &protocol.TxDesc{Tx: tx}, MsgType: protocol.MsgRemoveTx}})
-       time.Sleep(time.Millisecond * 10)
-       txs, err := w.GetUnconfirmedTxs(testAccount.ID)
+       // delete account utxos
+       store.deleteAccountUTXOs(account.ID, batch)
+
+       // delete account control program
+       if err := store.deleteAccountControlPrograms(account.ID, batch); err != nil {
+               return err
+       }
+
+       // delete bip44 contract index
+       batch.Delete(Bip44ContractIndexKey(account.ID, false))
+       batch.Delete(Bip44ContractIndexKey(account.ID, true))
+
+       // delete contract index
+       batch.Delete(contractIndexKey(account.ID))
+
+       // delete account id
+       batch.Delete(AccountIDKey(account.ID))
+       batch.Delete(accountAliasKey(account.Alias))
+       if store.batch == nil {
+               batch.Write()
+       }
+       return nil
+}
+
+// deleteAccountUTXOs delete account utxos by accountID
+func (store *MockAccountStore) deleteAccountUTXOs(accountID string, batch dbm.Batch) error {
+       accountUtxoIter := store.db.IteratorPrefix([]byte(UTXOPrefix))
+       defer accountUtxoIter.Release()
+
+       for accountUtxoIter.Next() {
+               accountUtxo := new(acc.UTXO)
+               if err := json.Unmarshal(accountUtxoIter.Value(), accountUtxo); err != nil {
+                       return err
+               }
+
+               if accountID == accountUtxo.AccountID {
+                       batch.Delete(StandardUTXOKey(accountUtxo.OutputID))
+               }
+       }
+
+       return nil
+}
+
+// deleteAccountControlPrograms deletes account control program
+func (store *MockAccountStore) deleteAccountControlPrograms(accountID string, batch dbm.Batch) error {
+       cps, err := store.ListControlPrograms()
        if err != nil {
-               t.Fatal("get unconfirmed tx error:", err)
+               return err
        }
 
-       if len(txs) != 0 {
-               t.Fatal("disaptch remove tx msg error")
+       var hash [32]byte
+       for _, cp := range cps {
+               if cp.AccountID == accountID {
+                       sha3pool.Sum256(hash[:], cp.ControlProgram)
+                       batch.Delete(ContractKey(bc.NewHash(hash)))
+               }
        }
+       return nil
+}
 
-       w.eventDispatcher.Post(protocol.TxMsgEvent{TxMsg: &protocol.TxPoolMsg{TxDesc: &protocol.TxDesc{Tx: tx}, MsgType: 2}})
+// DeleteStandardUTXO delete utxo by outpu id
+func (store *MockAccountStore) DeleteStandardUTXO(outputID bc.Hash) {
+       if store.batch == nil {
+               store.db.Delete(StandardUTXOKey(outputID))
+       } else {
+               store.batch.Delete(StandardUTXOKey(outputID))
+       }
 }
 
-func mockUTXO(controlProg *account.CtrlProgram, assetID *bc.AssetID) *account.UTXO {
-       utxo := &account.UTXO{}
-       utxo.OutputID = bc.Hash{V0: 1}
-       utxo.SourceID = bc.Hash{V0: 2}
-       utxo.AssetID = *assetID
-       utxo.Amount = 1000000000
-       utxo.SourcePos = 0
-       utxo.ControlProgram = controlProg.ControlProgram
-       utxo.AccountID = controlProg.AccountID
-       utxo.Address = controlProg.Address
-       utxo.ControlProgramIndex = controlProg.KeyIndex
-       return utxo
+// GetAccountByAlias get account by account alias
+func (store *MockAccountStore) GetAccountByAlias(accountAlias string) (*acc.Account, error) {
+       accountID := store.db.Get(accountAliasKey(accountAlias))
+       if accountID == nil {
+               return nil, acc.ErrFindAccount
+       }
+       return store.GetAccountByID(string(accountID))
 }
 
-func mockTxData(utxos []*account.UTXO, testAccount *account.Account) (*txbuilder.Template, *types.TxData, error) {
-       tplBuilder := txbuilder.NewBuilder(time.Now())
+// GetAccountByID get account by accountID
+func (store *MockAccountStore) GetAccountByID(accountID string) (*acc.Account, error) {
+       rawAccount := store.db.Get(AccountIDKey(accountID))
+       if rawAccount == nil {
+               return nil, acc.ErrFindAccount
+       }
+       account := new(acc.Account)
+       if err := json.Unmarshal(rawAccount, account); err != nil {
+               return nil, err
+       }
+       return account, nil
+}
 
-       for _, utxo := range utxos {
-               txInput, sigInst, err := account.UtxoToInputs(testAccount.Signer, utxo)
-               if err != nil {
-                       return nil, nil, err
+// GetAccountIndex get account index by account xpubs
+func (store *MockAccountStore) GetAccountIndex(xpubs []chainkd.XPub) uint64 {
+       currentIndex := uint64(0)
+       if rawIndexBytes := store.db.Get(accountIndexKey(xpubs)); rawIndexBytes != nil {
+               currentIndex = common.BytesToUnit64(rawIndexBytes)
+       }
+       return currentIndex
+}
+
+// GetBip44ContractIndex get bip44 contract index
+func (store *MockAccountStore) GetBip44ContractIndex(accountID string, change bool) uint64 {
+       index := uint64(0)
+       if rawIndexBytes := store.db.Get(Bip44ContractIndexKey(accountID, change)); rawIndexBytes != nil {
+               index = common.BytesToUnit64(rawIndexBytes)
+       }
+       return index
+}
+
+// GetCoinbaseArbitrary get coinbase arbitrary
+func (store *MockAccountStore) GetCoinbaseArbitrary() []byte {
+       return store.db.Get([]byte(CoinbaseAbKey))
+}
+
+// GetContractIndex get contract index
+func (store *MockAccountStore) GetContractIndex(accountID string) uint64 {
+       index := uint64(0)
+       if rawIndexBytes := store.db.Get(contractIndexKey(accountID)); rawIndexBytes != nil {
+               index = common.BytesToUnit64(rawIndexBytes)
+       }
+       return index
+}
+
+// GetControlProgram get control program
+func (store *MockAccountStore) GetControlProgram(hash bc.Hash) (*acc.CtrlProgram, error) {
+       rawProgram := store.db.Get(ContractKey(hash))
+       if rawProgram == nil {
+               return nil, acc.ErrFindCtrlProgram
+       }
+       cp := new(acc.CtrlProgram)
+       if err := json.Unmarshal(rawProgram, cp); err != nil {
+               return nil, err
+       }
+       return cp, nil
+}
+
+// GetMiningAddress get mining address
+func (store *MockAccountStore) GetMiningAddress() (*acc.CtrlProgram, error) {
+       rawCP := store.db.Get([]byte(MiningAddressKey))
+       if rawCP == nil {
+               return nil, acc.ErrFindMiningAddress
+       }
+       cp := new(acc.CtrlProgram)
+       if err := json.Unmarshal(rawCP, cp); err != nil {
+               return nil, err
+       }
+       return cp, nil
+}
+
+// GetUTXO get standard utxo by id
+func (store *MockAccountStore) GetUTXO(outid bc.Hash) (*acc.UTXO, error) {
+       u := new(acc.UTXO)
+       if data := store.db.Get(StandardUTXOKey(outid)); data != nil {
+               return u, json.Unmarshal(data, u)
+       }
+       if data := store.db.Get(ContractUTXOKey(outid)); data != nil {
+               return u, json.Unmarshal(data, u)
+       }
+       return nil, acc.ErrMatchUTXO
+}
+
+// ListAccounts get all accounts which name prfix is id.
+func (store *MockAccountStore) ListAccounts(id string) ([]*acc.Account, error) {
+       accounts := []*acc.Account{}
+       accountIter := store.db.IteratorPrefix(AccountIDKey(strings.TrimSpace(id)))
+       defer accountIter.Release()
+
+       for accountIter.Next() {
+               account := new(acc.Account)
+               if err := json.Unmarshal(accountIter.Value(), &account); err != nil {
+                       return nil, err
                }
-               tplBuilder.AddInput(txInput, sigInst)
+               accounts = append(accounts, account)
+       }
+       return accounts, nil
+}
 
-               out := &types.TxOutput{}
-               if utxo.AssetID == *consensus.BTMAssetID {
-                       out = types.NewIntraChainOutput(utxo.AssetID, 100, utxo.ControlProgram)
+// ListControlPrograms get all local control programs
+func (store *MockAccountStore) ListControlPrograms() ([]*acc.CtrlProgram, error) {
+       cps := []*acc.CtrlProgram{}
+       cpIter := store.db.IteratorPrefix([]byte(ContractPrefix))
+       defer cpIter.Release()
+
+       for cpIter.Next() {
+               cp := new(acc.CtrlProgram)
+               if err := json.Unmarshal(cpIter.Value(), cp); err != nil {
+                       return nil, err
+               }
+               cps = append(cps, cp)
+       }
+       return cps, nil
+}
+
+// ListUTXOs get utxos by accountID
+func (store *MockAccountStore) ListUTXOs() ([]*acc.UTXO, error) {
+       utxoIter := store.db.IteratorPrefix([]byte(UTXOPrefix))
+       defer utxoIter.Release()
+
+       utxos := []*acc.UTXO{}
+       for utxoIter.Next() {
+               utxo := new(acc.UTXO)
+               if err := json.Unmarshal(utxoIter.Value(), utxo); err != nil {
+                       return nil, err
+               }
+               utxos = append(utxos, utxo)
+       }
+       return utxos, nil
+}
+
+// SetAccount set account account ID, account alias and raw account.
+func (store *MockAccountStore) SetAccount(account *acc.Account) error {
+       rawAccount, err := json.Marshal(account)
+       if err != nil {
+               return acc.ErrMarshalAccount
+       }
+
+       batch := store.db.NewBatch()
+       if store.batch != nil {
+               batch = store.batch
+       }
+
+       batch.Set(AccountIDKey(account.ID), rawAccount)
+       batch.Set(accountAliasKey(account.Alias), []byte(account.ID))
+
+       if store.batch == nil {
+               batch.Write()
+       }
+       return nil
+}
+
+// SetAccountIndex update account index
+func (store *MockAccountStore) SetAccountIndex(account *acc.Account) {
+       currentIndex := store.GetAccountIndex(account.XPubs)
+       if account.KeyIndex > currentIndex {
+               if store.batch == nil {
+                       store.db.Set(accountIndexKey(account.XPubs), common.Unit64ToBytes(account.KeyIndex))
                } else {
-                       out = types.NewIntraChainOutput(utxo.AssetID, utxo.Amount, utxo.ControlProgram)
+                       store.batch.Set(accountIndexKey(account.XPubs), common.Unit64ToBytes(account.KeyIndex))
                }
-               tplBuilder.AddOutput(out)
        }
+}
 
-       return tplBuilder.Build()
+// SetBip44ContractIndex set contract index
+func (store *MockAccountStore) SetBip44ContractIndex(accountID string, change bool, index uint64) {
+       if store.batch == nil {
+               store.db.Set(Bip44ContractIndexKey(accountID, change), common.Unit64ToBytes(index))
+       } else {
+               store.batch.Set(Bip44ContractIndexKey(accountID, change), common.Unit64ToBytes(index))
+       }
 }
 
-func mockWallet(walletDB dbm.DB, account *account.Manager, asset *asset.Registry, chain *protocol.Chain, dispatcher *event.Dispatcher, txIndexFlag bool) *Wallet {
-       wallet := &Wallet{
-               DB:              walletDB,
-               AccountMgr:      account,
-               AssetReg:        asset,
-               chain:           chain,
-               RecoveryMgr:     newRecoveryManager(walletDB, account),
-               eventDispatcher: dispatcher,
-               TxIndexFlag:     txIndexFlag,
+// SetCoinbaseArbitrary set coinbase arbitrary
+func (store *MockAccountStore) SetCoinbaseArbitrary(arbitrary []byte) {
+       if store.batch == nil {
+               store.db.Set([]byte(CoinbaseAbKey), arbitrary)
+       } else {
+               store.batch.Set([]byte(CoinbaseAbKey), arbitrary)
        }
-       wallet.txMsgSub, _ = wallet.eventDispatcher.Subscribe(protocol.TxMsgEvent{})
-       return wallet
 }
 
-func mockSingleBlock(tx *types.Tx) *types.Block {
-       return &types.Block{
-               BlockHeader: types.BlockHeader{
-                       Version: 1,
-                       Height:  1,
-               },
-               Transactions: []*types.Tx{config.GenesisTx(), tx},
+// SetContractIndex set contract index
+func (store *MockAccountStore) SetContractIndex(accountID string, index uint64) {
+       if store.batch == nil {
+               store.db.Set(contractIndexKey(accountID), common.Unit64ToBytes(index))
+       } else {
+               store.batch.Set(contractIndexKey(accountID), common.Unit64ToBytes(index))
+       }
+}
+
+// SetControlProgram set raw program
+func (store *MockAccountStore) SetControlProgram(hash bc.Hash, program *acc.CtrlProgram) error {
+       accountCP, err := json.Marshal(program)
+       if err != nil {
+               return err
+       }
+       if store.batch == nil {
+               store.db.Set(ContractKey(hash), accountCP)
+       } else {
+               store.batch.Set(ContractKey(hash), accountCP)
+       }
+       return nil
+}
+
+// SetMiningAddress set mining address
+func (store *MockAccountStore) SetMiningAddress(program *acc.CtrlProgram) error {
+       rawProgram, err := json.Marshal(program)
+       if err != nil {
+               return err
+       }
+
+       if store.batch == nil {
+               store.db.Set([]byte(MiningAddressKey), rawProgram)
+       } else {
+               store.batch.Set([]byte(MiningAddressKey), rawProgram)
+       }
+       return nil
+}
+
+// SetStandardUTXO set standard utxo
+func (store *MockAccountStore) SetStandardUTXO(outputID bc.Hash, utxo *acc.UTXO) error {
+       data, err := json.Marshal(utxo)
+       if err != nil {
+               return err
+       }
+       if store.batch == nil {
+               store.db.Set(StandardUTXOKey(outputID), data)
+       } else {
+               store.batch.Set(StandardUTXOKey(outputID), data)
        }
+       return nil
 }