7 log "github.com/sirupsen/logrus"
8 "github.com/tendermint/go-wire/data/base58"
9 "github.com/tendermint/tmlibs/db"
11 "github.com/bytom/account"
12 "github.com/bytom/asset"
13 "github.com/bytom/blockchain/pseudohsm"
14 "github.com/bytom/crypto/ed25519/chainkd"
15 "github.com/bytom/crypto/sha3pool"
16 "github.com/bytom/protocol"
17 "github.com/bytom/protocol/bc"
18 "github.com/bytom/protocol/bc/types"
24 //RecoveryIndex walletdb recovery cp number
25 const RecoveryIndex = 5000
27 var walletKey = []byte("walletInfo")
28 var dbKeyForimportingPrivateKey = []byte("importingKeysInfo")
30 //StatusInfo is base valid block info to handle orphan block rollback
31 type StatusInfo struct {
36 OnChainAddresses AddressSet
39 //KeyInfo is key import status
41 account account.Account
42 Alias string `json:"alias"`
43 XPub chainkd.XPub `json:"xpub"`
44 Percent uint8 `json:"percent"`
45 Complete bool `json:"complete"`
48 //Wallet is related to storing account unspent outputs
52 AccountMgr *account.Manager
53 AssetReg *asset.Registry
56 rescanProgress chan struct{}
57 ImportingPrivateKey bool
58 importingKeysInfo []KeyInfo
61 //NewWallet return a new wallet instance
62 func NewWallet(walletDB db.DB, account *account.Manager, asset *asset.Registry, hsm *pseudohsm.HSM, chain *protocol.Chain) (*Wallet, error) {
69 rescanProgress: make(chan struct{}, 1),
70 importingKeysInfo: make([]KeyInfo, 0),
73 if err := w.loadWalletInfo(); err != nil {
77 if err := w.loadKeysInfo(); err != nil {
81 w.ImportingPrivateKey = w.getImportKeyFlag()
88 //GetWalletInfo return stored wallet info and nil,if error,
89 //return initial wallet info and err
90 func (w *Wallet) loadWalletInfo() error {
91 if rawWallet := w.DB.Get(walletKey); rawWallet != nil {
92 return json.Unmarshal(rawWallet, &w.status)
95 w.status.OnChainAddresses = NewAddressSet()
96 block, err := w.chain.GetBlockByHeight(0)
100 return w.AttachBlock(block)
103 func (w *Wallet) commitWalletInfo(batch db.Batch) error {
104 rawWallet, err := json.Marshal(w.status)
106 log.WithField("err", err).Error("save wallet info")
110 batch.Set(walletKey, rawWallet)
115 //GetWalletInfo return stored wallet info and nil,if error,
116 //return initial wallet info and err
117 func (w *Wallet) loadKeysInfo() error {
118 if rawKeyInfo := w.DB.Get(dbKeyForimportingPrivateKey); rawKeyInfo != nil {
119 json.Unmarshal(rawKeyInfo, &w.importingKeysInfo)
125 func (w *Wallet) commitkeysInfo() error {
126 rawKeysInfo, err := json.Marshal(w.importingKeysInfo)
128 log.WithField("err", err).Error("save wallet info")
131 w.DB.Set(dbKeyForimportingPrivateKey, rawKeysInfo)
135 func (w *Wallet) getImportKeyFlag() bool {
136 for _, v := range w.importingKeysInfo {
137 if v.Complete == false {
144 // AttachBlock attach a new block
145 func (w *Wallet) AttachBlock(block *types.Block) error {
146 if block.PreviousBlockHash != w.status.WorkHash {
147 log.Warn("wallet skip attachBlock due to status hash not equal to previous hash")
151 blockHash := block.Hash()
152 txStatus, err := w.chain.GetTransactionStatus(&blockHash)
157 storeBatch := w.DB.NewBatch()
158 w.indexTransactions(storeBatch, block, txStatus)
159 w.buildAccountUTXOs(storeBatch, block, txStatus)
161 w.status.WorkHeight = block.Height
162 w.status.WorkHash = block.Hash()
163 if w.status.WorkHeight >= w.status.BestHeight {
164 w.status.BestHeight = w.status.WorkHeight
165 w.status.BestHash = w.status.WorkHash
167 return w.commitWalletInfo(storeBatch)
170 // DetachBlock detach a block and rollback state
171 func (w *Wallet) DetachBlock(block *types.Block) error {
172 blockHash := block.Hash()
173 txStatus, err := w.chain.GetTransactionStatus(&blockHash)
178 storeBatch := w.DB.NewBatch()
179 w.reverseAccountUTXOs(storeBatch, block, txStatus)
180 w.deleteTransactions(storeBatch, w.status.BestHeight)
182 w.status.BestHeight = block.Height - 1
183 w.status.BestHash = block.PreviousBlockHash
185 if w.status.WorkHeight > w.status.BestHeight {
186 w.status.WorkHeight = w.status.BestHeight
187 w.status.WorkHash = w.status.BestHash
190 return w.commitWalletInfo(storeBatch)
193 //WalletUpdate process every valid block and reverse every invalid block which need to rollback
194 func (w *Wallet) walletUpdater() {
196 getRescanNotification(w)
197 w.updateRescanStatus()
198 for !w.chain.InMainChain(w.status.BestHash) {
199 block, err := w.chain.GetBlockByHash(&w.status.BestHash)
201 log.WithField("err", err).Error("walletUpdater GetBlockByHash")
205 if err := w.DetachBlock(block); err != nil {
206 log.WithField("err", err).Error("walletUpdater detachBlock")
211 block, _ := w.chain.GetBlockByHeight(w.status.WorkHeight + 1)
213 <-w.chain.BlockWaiter(w.status.WorkHeight + 1)
217 if err := w.AttachBlock(block); err != nil {
218 log.WithField("err", err).Error("walletUpdater stop")
224 func getRescanNotification(w *Wallet) {
226 case <-w.rescanProgress:
227 w.status.WorkHeight = 0
228 block, _ := w.chain.GetBlockByHeight(w.status.WorkHeight)
229 w.status.WorkHash = block.Hash()
235 // ExportAccountPrivKey exports the account private key as a WIF for encoding as a string
236 // in the Wallet Import Formt.
237 func (w *Wallet) ExportAccountPrivKey(xpub chainkd.XPub, auth string) (*string, error) {
238 xprv, err := w.Hsm.LoadChainKDKey(xpub, auth)
243 sha3pool.Sum256(hashed[:], xprv[:])
245 tmp := append(xprv[:], hashed[:4]...)
246 res := base58.Encode(tmp)
250 // ImportAccountPrivKey imports the account key in the Wallet Import Formt.
251 func (w *Wallet) ImportAccountPrivKey(xprv chainkd.XPrv, keyAlias, auth string, index uint64, accountAlias string) (*pseudohsm.XPub, error) {
252 if w.Hsm.HasAlias(keyAlias) {
253 return nil, pseudohsm.ErrDuplicateKeyAlias
255 if w.Hsm.HasKey(xprv) {
256 return nil, pseudohsm.ErrDuplicateKey
259 if acc, _ := w.AccountMgr.FindByAlias(nil, accountAlias); acc != nil {
260 return nil, account.ErrDuplicateAlias
263 xpub, _, err := w.Hsm.ImportXPrvKey(auth, keyAlias, xprv)
268 newAccount, err := w.AccountMgr.Create(nil, []chainkd.XPub{xpub.XPub}, SINGLE, accountAlias)
270 w.Hsm.XDelete(xpub.XPub, auth)
273 if err := w.recoveryAccountWalletDB(newAccount, xpub, index, keyAlias); err != nil {
274 w.AccountMgr.DeleteAccount(newAccount.ID)
275 w.Hsm.XDelete(xpub.XPub, auth)
281 // ImportAccountXpubKey imports the account key in the Wallet Import Formt.
282 func (w *Wallet) ImportAccountXpubKey(xpubIndex int, xpub pseudohsm.XPub, cpIndex uint64) error {
283 accountAlias := fmt.Sprintf("recovery_%d", xpubIndex)
285 if acc, _ := w.AccountMgr.FindByAlias(nil, accountAlias); acc != nil {
286 return account.ErrDuplicateAlias
289 newAccount, err := w.AccountMgr.Create(nil, []chainkd.XPub{xpub.XPub}, SINGLE, accountAlias)
294 return w.recoveryAccountWalletDB(newAccount, &xpub, cpIndex, xpub.Alias)
297 func (w *Wallet) recoveryAccountWalletDB(account *account.Account, XPub *pseudohsm.XPub, index uint64, keyAlias string) error {
298 if err := w.createProgram(account, XPub, index); err != nil {
301 w.ImportingPrivateKey = true
308 w.importingKeysInfo = append(w.importingKeysInfo, tmp)
315 func (w *Wallet) createProgram(account *account.Account, XPub *pseudohsm.XPub, index uint64) error {
316 for i := uint64(0); i < index; i++ {
317 if _, err := w.AccountMgr.CreateAddress(nil, account.ID, false); err != nil {
324 func (w *Wallet) rescanBlocks() {
326 case w.rescanProgress <- struct{}{}:
332 //GetRescanStatus return key import rescan status
333 func (w *Wallet) GetRescanStatus() ([]KeyInfo, error) {
334 keysInfo := make([]KeyInfo, len(w.importingKeysInfo))
336 if rawKeyInfo := w.DB.Get(dbKeyForimportingPrivateKey); rawKeyInfo != nil {
337 if err := json.Unmarshal(rawKeyInfo, &keysInfo); err != nil {
345 //updateRescanStatus mark private key import process `Complete` if rescan finished
346 func (w *Wallet) updateRescanStatus() {
347 if !w.ImportingPrivateKey {
351 if w.status.WorkHeight < w.status.BestHeight {
352 percent := uint8(w.status.WorkHeight * 100 / w.status.BestHeight)
353 for _, keyInfo := range w.importingKeysInfo {
354 keyInfo.Percent = percent
360 w.ImportingPrivateKey = false
361 for _, keyInfo := range w.importingKeysInfo {
362 keyInfo.Percent = 100
363 keyInfo.Complete = true
365 if cps, err := w.AccountMgr.ListCtrlProgramsByAccountId(nil, keyInfo.account.ID); err == nil {
366 for _, cp := range cps {
367 if !w.status.OnChainAddresses.Contains(cp.Address) {
368 w.AccountMgr.DeleteAccountControlProgram(cp.ControlProgram)