OSDN Git Service

fix paging (#235)
[bytom/vapor.git] / wallet / indexer.go
1 package wallet
2
3 import (
4         "encoding/binary"
5         "encoding/hex"
6         "encoding/json"
7         "fmt"
8         "sort"
9
10         log "github.com/sirupsen/logrus"
11
12         "github.com/vapor/account"
13         "github.com/vapor/asset"
14         "github.com/vapor/blockchain/query"
15         "github.com/vapor/consensus"
16         "github.com/vapor/crypto/sha3pool"
17         dbm "github.com/vapor/database/leveldb"
18         chainjson "github.com/vapor/encoding/json"
19         "github.com/vapor/errors"
20         "github.com/vapor/protocol/bc"
21         "github.com/vapor/protocol/bc/types"
22 )
23
24 const (
25         //TxPrefix is wallet database transactions prefix
26         TxPrefix = "TXS:"
27         //TxIndexPrefix is wallet database tx index prefix
28         TxIndexPrefix = "TID:"
29         //TxIndexPrefix is wallet database global tx index prefix
30         GlobalTxIndexPrefix = "GTID:"
31 )
32
33 var ErrAccntTxIDNotFound = errors.New("account TXID not found")
34
35 func formatKey(blockHeight uint64, position uint32) string {
36         return fmt.Sprintf("%016x%08x", blockHeight, position)
37 }
38
39 func calcAnnotatedKey(formatKey string) []byte {
40         return []byte(TxPrefix + formatKey)
41 }
42
43 func calcDeleteKey(blockHeight uint64) []byte {
44         return []byte(fmt.Sprintf("%s%016x", TxPrefix, blockHeight))
45 }
46
47 func calcTxIndexKey(txID string) []byte {
48         return []byte(TxIndexPrefix + txID)
49 }
50
51 func calcGlobalTxIndexKey(txID string) []byte {
52         return []byte(GlobalTxIndexPrefix + txID)
53 }
54
55 func calcGlobalTxIndex(blockHash *bc.Hash, position uint64) []byte {
56         txIdx := make([]byte, 40)
57         copy(txIdx[:32], blockHash.Bytes())
58         binary.BigEndian.PutUint64(txIdx[32:], position)
59         return txIdx
60 }
61
62 func parseGlobalTxIdx(globalTxIdx []byte) (*bc.Hash, uint64) {
63         var hashBytes [32]byte
64         copy(hashBytes[:], globalTxIdx[:32])
65         hash := bc.NewHash(hashBytes)
66         position := binary.BigEndian.Uint64(globalTxIdx[32:])
67         return &hash, position
68 }
69
70 // deleteTransaction delete transactions when orphan block rollback
71 func (w *Wallet) deleteTransactions(batch dbm.Batch, height uint64) {
72         tmpTx := query.AnnotatedTx{}
73         txIter := w.DB.IteratorPrefix(calcDeleteKey(height))
74         defer txIter.Release()
75
76         for txIter.Next() {
77                 if err := json.Unmarshal(txIter.Value(), &tmpTx); err == nil {
78                         batch.Delete(calcTxIndexKey(tmpTx.ID.String()))
79                 }
80                 batch.Delete(txIter.Key())
81         }
82 }
83
84 // saveExternalAssetDefinition save external and local assets definition,
85 // when query ,query local first and if have no then query external
86 // details see getAliasDefinition
87 func saveExternalAssetDefinition(b *types.Block, walletDB dbm.DB) {
88         storeBatch := walletDB.NewBatch()
89         defer storeBatch.Write()
90
91         for _, tx := range b.Transactions {
92                 for _, orig := range tx.Inputs {
93                         if cci, ok := orig.TypedInput.(*types.CrossChainInput); ok {
94                                 assetID := cci.AssetId
95                                 if assetExist := walletDB.Get(asset.ExtAssetKey(assetID)); assetExist == nil {
96                                         storeBatch.Set(asset.ExtAssetKey(assetID), cci.AssetDefinition)
97                                 }
98                         }
99                 }
100         }
101 }
102
103 // Summary is the struct of transaction's input and output summary
104 type Summary struct {
105         Type         string             `json:"type"`
106         AssetID      bc.AssetID         `json:"asset_id,omitempty"`
107         AssetAlias   string             `json:"asset_alias,omitempty"`
108         Amount       uint64             `json:"amount,omitempty"`
109         AccountID    string             `json:"account_id,omitempty"`
110         AccountAlias string             `json:"account_alias,omitempty"`
111         Arbitrary    chainjson.HexBytes `json:"arbitrary,omitempty"`
112 }
113
114 // TxSummary is the struct of transaction summary
115 type TxSummary struct {
116         ID        bc.Hash   `json:"tx_id"`
117         Timestamp uint64    `json:"block_time"`
118         Inputs    []Summary `json:"inputs"`
119         Outputs   []Summary `json:"outputs"`
120 }
121
122 // indexTransactions saves all annotated transactions to the database.
123 func (w *Wallet) indexTransactions(batch dbm.Batch, b *types.Block, txStatus *bc.TransactionStatus) error {
124         annotatedTxs := w.filterAccountTxs(b, txStatus)
125         saveExternalAssetDefinition(b, w.DB)
126         annotateTxsAccount(annotatedTxs, w.DB)
127
128         for _, tx := range annotatedTxs {
129                 rawTx, err := json.Marshal(tx)
130                 if err != nil {
131                         log.WithFields(log.Fields{"module": logModule, "err": err}).Error("inserting annotated_txs to db")
132                         return err
133                 }
134
135                 batch.Set(calcAnnotatedKey(formatKey(b.Height, uint32(tx.Position))), rawTx)
136                 batch.Set(calcTxIndexKey(tx.ID.String()), []byte(formatKey(b.Height, uint32(tx.Position))))
137
138                 // delete unconfirmed transaction
139                 batch.Delete(calcUnconfirmedTxKey(tx.ID.String()))
140         }
141
142         if !w.TxIndexFlag {
143                 return nil
144         }
145
146         for position, globalTx := range b.Transactions {
147                 blockHash := b.BlockHeader.Hash()
148                 batch.Set(calcGlobalTxIndexKey(globalTx.ID.String()), calcGlobalTxIndex(&blockHash, uint64(position)))
149         }
150
151         return nil
152 }
153
154 // filterAccountTxs related and build the fully annotated transactions.
155 func (w *Wallet) filterAccountTxs(b *types.Block, txStatus *bc.TransactionStatus) []*query.AnnotatedTx {
156         annotatedTxs := make([]*query.AnnotatedTx, 0, len(b.Transactions))
157
158 transactionLoop:
159         for pos, tx := range b.Transactions {
160                 statusFail, _ := txStatus.GetStatus(pos)
161                 for _, v := range tx.Outputs {
162                         var hash [32]byte
163                         sha3pool.Sum256(hash[:], v.ControlProgram())
164
165                         if bytes := w.DB.Get(account.ContractKey(hash)); bytes != nil {
166                                 annotatedTxs = append(annotatedTxs, w.buildAnnotatedTransaction(tx, b, statusFail, pos))
167                                 continue transactionLoop
168                         }
169                 }
170
171                 for _, v := range tx.Inputs {
172                         outid, err := v.SpentOutputID()
173                         if err != nil {
174                                 continue
175                         }
176                         if bytes := w.DB.Get(account.StandardUTXOKey(outid)); bytes != nil {
177                                 annotatedTxs = append(annotatedTxs, w.buildAnnotatedTransaction(tx, b, statusFail, pos))
178                                 continue transactionLoop
179                         }
180                 }
181         }
182
183         return annotatedTxs
184 }
185
186 // GetTransactionByTxID get transaction by txID
187 func (w *Wallet) GetTransactionByTxID(txID string) (*query.AnnotatedTx, error) {
188         if annotatedTx, err := w.getAccountTxByTxID(txID); err == nil {
189                 return annotatedTx, nil
190         } else if !w.TxIndexFlag {
191                 return nil, err
192         }
193
194         return w.getGlobalTxByTxID(txID)
195 }
196
197 func (w *Wallet) getAccountTxByTxID(txID string) (*query.AnnotatedTx, error) {
198         annotatedTx := &query.AnnotatedTx{}
199         formatKey := w.DB.Get(calcTxIndexKey(txID))
200         if formatKey == nil {
201                 return nil, ErrAccntTxIDNotFound
202         }
203
204         txInfo := w.DB.Get(calcAnnotatedKey(string(formatKey)))
205         if err := json.Unmarshal(txInfo, annotatedTx); err != nil {
206                 return nil, err
207         }
208
209         annotateTxsAsset(w, []*query.AnnotatedTx{annotatedTx})
210         return annotatedTx, nil
211 }
212
213 func (w *Wallet) getGlobalTxByTxID(txID string) (*query.AnnotatedTx, error) {
214         globalTxIdx := w.DB.Get(calcGlobalTxIndexKey(txID))
215         if globalTxIdx == nil {
216                 return nil, fmt.Errorf("No transaction(tx_id=%s) ", txID)
217         }
218
219         blockHash, pos := parseGlobalTxIdx(globalTxIdx)
220         block, err := w.chain.GetBlockByHash(blockHash)
221         if err != nil {
222                 return nil, err
223         }
224
225         txStatus, err := w.chain.GetTransactionStatus(blockHash)
226         if err != nil {
227                 return nil, err
228         }
229
230         statusFail, err := txStatus.GetStatus(int(pos))
231         if err != nil {
232                 return nil, err
233         }
234
235         tx := block.Transactions[int(pos)]
236         return w.buildAnnotatedTransaction(tx, block, statusFail, int(pos)), nil
237 }
238
239 // GetTransactionsSummary get transactions summary
240 func (w *Wallet) GetTransactionsSummary(transactions []*query.AnnotatedTx) []TxSummary {
241         Txs := []TxSummary{}
242
243         for _, annotatedTx := range transactions {
244                 tmpTxSummary := TxSummary{
245                         Inputs:    make([]Summary, len(annotatedTx.Inputs)),
246                         Outputs:   make([]Summary, len(annotatedTx.Outputs)),
247                         ID:        annotatedTx.ID,
248                         Timestamp: annotatedTx.Timestamp,
249                 }
250
251                 for i, input := range annotatedTx.Inputs {
252                         tmpTxSummary.Inputs[i].Type = input.Type
253                         tmpTxSummary.Inputs[i].AccountID = input.AccountID
254                         tmpTxSummary.Inputs[i].AccountAlias = input.AccountAlias
255                         tmpTxSummary.Inputs[i].AssetID = input.AssetID
256                         tmpTxSummary.Inputs[i].AssetAlias = input.AssetAlias
257                         tmpTxSummary.Inputs[i].Amount = input.Amount
258                         tmpTxSummary.Inputs[i].Arbitrary = input.Arbitrary
259                 }
260                 for j, output := range annotatedTx.Outputs {
261                         tmpTxSummary.Outputs[j].Type = output.Type
262                         tmpTxSummary.Outputs[j].AccountID = output.AccountID
263                         tmpTxSummary.Outputs[j].AccountAlias = output.AccountAlias
264                         tmpTxSummary.Outputs[j].AssetID = output.AssetID
265                         tmpTxSummary.Outputs[j].AssetAlias = output.AssetAlias
266                         tmpTxSummary.Outputs[j].Amount = output.Amount
267                 }
268
269                 Txs = append(Txs, tmpTxSummary)
270         }
271
272         return Txs
273 }
274
275 func findTransactionsByAccount(annotatedTx *query.AnnotatedTx, accountID string) bool {
276         for _, input := range annotatedTx.Inputs {
277                 if input.AccountID == accountID {
278                         return true
279                 }
280         }
281
282         for _, output := range annotatedTx.Outputs {
283                 if output.AccountID == accountID {
284                         return true
285                 }
286         }
287
288         return false
289 }
290
291 // GetTransactions get all walletDB transactions or unconfirmed transactions, and filter transactions by accountID and StartTxID optional
292 func (w *Wallet) GetTransactions(accountID string, StartTxID string, count uint, unconfirmed bool) ([]*query.AnnotatedTx, error) {
293         annotatedTxs := []*query.AnnotatedTx{}
294         var startKey []byte
295         preFix := TxPrefix
296
297         if StartTxID != "" {
298                 if unconfirmed {
299                         startKey = calcUnconfirmedTxKey(StartTxID)
300                 } else {
301                         formatKey := w.DB.Get(calcTxIndexKey(StartTxID))
302                         if formatKey == nil {
303                                 return nil, ErrAccntTxIDNotFound
304                         }
305                         startKey = calcAnnotatedKey(string(formatKey))
306                 }
307         }
308
309         if unconfirmed {
310                 preFix = UnconfirmedTxPrefix
311         }
312
313         itr := w.DB.IteratorPrefixWithStart([]byte(preFix), startKey, true)
314         defer itr.Release()
315
316         for txNum := count; itr.Next() && txNum > 0; txNum-- {
317                 annotatedTx := &query.AnnotatedTx{}
318                 if err := json.Unmarshal(itr.Value(), &annotatedTx); err != nil {
319                         return nil, err
320                 }
321
322                 if accountID == "" || findTransactionsByAccount(annotatedTx, accountID) {
323                         annotateTxsAsset(w, []*query.AnnotatedTx{annotatedTx})
324                         annotatedTxs = append([]*query.AnnotatedTx{annotatedTx}, annotatedTxs...)
325                 }
326         }
327
328         if unconfirmed {
329                 sort.Sort(SortByTimestamp(annotatedTxs))
330         } else {
331                 sort.Sort(SortByHeight(annotatedTxs))
332         }
333
334         return annotatedTxs, nil
335 }
336
337 // GetAccountBalances return all account balances
338 func (w *Wallet) GetAccountBalances(accountID string, id string) ([]AccountBalance, error) {
339         return w.indexBalances(w.GetAccountUtxos(accountID, "", false, false, false))
340 }
341
342 // AccountBalance account balance
343 type AccountBalance struct {
344         AccountID       string                 `json:"account_id"`
345         Alias           string                 `json:"account_alias"`
346         AssetAlias      string                 `json:"asset_alias"`
347         AssetID         string                 `json:"asset_id"`
348         Amount          uint64                 `json:"amount"`
349         AssetDefinition map[string]interface{} `json:"asset_definition"`
350 }
351
352 func (w *Wallet) indexBalances(accountUTXOs []*account.UTXO) ([]AccountBalance, error) {
353         accBalance := make(map[string]map[string]uint64)
354         balances := []AccountBalance{}
355
356         for _, accountUTXO := range accountUTXOs {
357                 assetID := accountUTXO.AssetID.String()
358                 if _, ok := accBalance[accountUTXO.AccountID]; ok {
359                         if _, ok := accBalance[accountUTXO.AccountID][assetID]; ok {
360                                 accBalance[accountUTXO.AccountID][assetID] += accountUTXO.Amount
361                         } else {
362                                 accBalance[accountUTXO.AccountID][assetID] = accountUTXO.Amount
363                         }
364                 } else {
365                         accBalance[accountUTXO.AccountID] = map[string]uint64{assetID: accountUTXO.Amount}
366                 }
367         }
368
369         var sortedAccount []string
370         for k := range accBalance {
371                 sortedAccount = append(sortedAccount, k)
372         }
373         sort.Strings(sortedAccount)
374
375         for _, id := range sortedAccount {
376                 var sortedAsset []string
377                 for k := range accBalance[id] {
378                         sortedAsset = append(sortedAsset, k)
379                 }
380                 sort.Strings(sortedAsset)
381
382                 for _, assetID := range sortedAsset {
383                         alias := w.AccountMgr.GetAliasByID(id)
384                         targetAsset, err := w.AssetReg.GetAsset(assetID)
385                         if err != nil {
386                                 return nil, err
387                         }
388
389                         assetAlias := *targetAsset.Alias
390                         balances = append(balances, AccountBalance{
391                                 Alias:           alias,
392                                 AccountID:       id,
393                                 AssetID:         assetID,
394                                 AssetAlias:      assetAlias,
395                                 Amount:          accBalance[id][assetID],
396                                 AssetDefinition: targetAsset.DefinitionMap,
397                         })
398                 }
399         }
400
401         return balances, nil
402 }
403
404 // GetAccountVotes return all account votes
405 func (w *Wallet) GetAccountVotes(accountID string, id string) ([]AccountVotes, error) {
406         return w.indexVotes(w.GetAccountUtxos(accountID, "", false, false, true))
407 }
408
409 type voteDetail struct {
410         Vote       string `json:"vote"`
411         VoteNumber uint64 `json:"vote_number"`
412 }
413
414 // AccountVotes account vote
415 type AccountVotes struct {
416         AccountID       string       `json:"account_id"`
417         Alias           string       `json:"account_alias"`
418         TotalVoteNumber uint64       `json:"total_vote_number"`
419         VoteDetails     []voteDetail `json:"vote_details"`
420 }
421
422 func (w *Wallet) indexVotes(accountUTXOs []*account.UTXO) ([]AccountVotes, error) {
423         accVote := make(map[string]map[string]uint64)
424         votes := []AccountVotes{}
425
426         for _, accountUTXO := range accountUTXOs {
427                 if accountUTXO.AssetID != *consensus.BTMAssetID || accountUTXO.Vote == nil {
428                         continue
429                 }
430                 xpub := hex.EncodeToString(accountUTXO.Vote)
431                 if _, ok := accVote[accountUTXO.AccountID]; ok {
432                         accVote[accountUTXO.AccountID][xpub] += accountUTXO.Amount
433                 } else {
434                         accVote[accountUTXO.AccountID] = map[string]uint64{xpub: accountUTXO.Amount}
435
436                 }
437         }
438
439         var sortedAccount []string
440         for k := range accVote {
441                 sortedAccount = append(sortedAccount, k)
442         }
443         sort.Strings(sortedAccount)
444
445         for _, id := range sortedAccount {
446                 var sortedXpub []string
447                 for k := range accVote[id] {
448                         sortedXpub = append(sortedXpub, k)
449                 }
450                 sort.Strings(sortedXpub)
451
452                 voteDetails := []voteDetail{}
453                 voteTotal := uint64(0)
454                 for _, xpub := range sortedXpub {
455                         voteDetails = append(voteDetails, voteDetail{
456                                 Vote:       xpub,
457                                 VoteNumber: accVote[id][xpub],
458                         })
459                         voteTotal += accVote[id][xpub]
460                 }
461                 alias := w.AccountMgr.GetAliasByID(id)
462                 votes = append(votes, AccountVotes{
463                         Alias:           alias,
464                         AccountID:       id,
465                         VoteDetails:     voteDetails,
466                         TotalVoteNumber: voteTotal,
467                 })
468         }
469
470         return votes, nil
471 }