OSDN Git Service

feat: add cross-chain output (#56)
[bytom/vapor.git] / wallet / indexer.go
1 package wallet
2
3 import (
4         "encoding/binary"
5         "encoding/json"
6         "fmt"
7         "sort"
8
9         log "github.com/sirupsen/logrus"
10
11         "github.com/vapor/account"
12         "github.com/vapor/asset"
13         "github.com/vapor/blockchain/query"
14         "github.com/vapor/crypto/sha3pool"
15         dbm "github.com/vapor/database/leveldb"
16         chainjson "github.com/vapor/encoding/json"
17         "github.com/vapor/errors"
18         "github.com/vapor/protocol/bc"
19         "github.com/vapor/protocol/bc/types"
20 )
21
22 const (
23         //TxPrefix is wallet database transactions prefix
24         TxPrefix = "TXS:"
25         //TxIndexPrefix is wallet database tx index prefix
26         TxIndexPrefix = "TID:"
27         //TxIndexPrefix is wallet database global tx index prefix
28         GlobalTxIndexPrefix = "GTID:"
29 )
30
31 var errAccntTxIDNotFound = errors.New("account TXID not found")
32
33 func formatKey(blockHeight uint64, position uint32) string {
34         return fmt.Sprintf("%016x%08x", blockHeight, position)
35 }
36
37 func calcAnnotatedKey(formatKey string) []byte {
38         return []byte(TxPrefix + formatKey)
39 }
40
41 func calcDeleteKey(blockHeight uint64) []byte {
42         return []byte(fmt.Sprintf("%s%016x", TxPrefix, blockHeight))
43 }
44
45 func calcTxIndexKey(txID string) []byte {
46         return []byte(TxIndexPrefix + txID)
47 }
48
49 func calcGlobalTxIndexKey(txID string) []byte {
50         return []byte(GlobalTxIndexPrefix + txID)
51 }
52
53 func calcGlobalTxIndex(blockHash *bc.Hash, position uint64) []byte {
54         txIdx := make([]byte, 40)
55         copy(txIdx[:32], blockHash.Bytes())
56         binary.BigEndian.PutUint64(txIdx[32:], position)
57         return txIdx
58 }
59
60 func parseGlobalTxIdx(globalTxIdx []byte) (*bc.Hash, uint64) {
61         var hashBytes [32]byte
62         copy(hashBytes[:], globalTxIdx[:32])
63         hash := bc.NewHash(hashBytes)
64         position := binary.BigEndian.Uint64(globalTxIdx[32:])
65         return &hash, position
66 }
67
68 // deleteTransaction delete transactions when orphan block rollback
69 func (w *Wallet) deleteTransactions(batch dbm.Batch, height uint64) {
70         tmpTx := query.AnnotatedTx{}
71         txIter := w.DB.IteratorPrefix(calcDeleteKey(height))
72         defer txIter.Release()
73
74         for txIter.Next() {
75                 if err := json.Unmarshal(txIter.Value(), &tmpTx); err == nil {
76                         batch.Delete(calcTxIndexKey(tmpTx.ID.String()))
77                 }
78                 batch.Delete(txIter.Key())
79         }
80 }
81
82 // saveExternalAssetDefinition save external and local assets definition,
83 // when query ,query local first and if have no then query external
84 // details see getAliasDefinition
85 func saveExternalAssetDefinition(b *types.Block, walletDB dbm.DB) {
86         storeBatch := walletDB.NewBatch()
87         defer storeBatch.Write()
88
89         for _, tx := range b.Transactions {
90                 for _, orig := range tx.Inputs {
91                         if ii, ok := orig.TypedInput.(*types.IssuanceInput); ok {
92                                 if isValidJSON(ii.AssetDefinition) {
93                                         assetID := ii.AssetID()
94                                         if assetExist := walletDB.Get(asset.ExtAssetKey(&assetID)); assetExist == nil {
95                                                 storeBatch.Set(asset.ExtAssetKey(&assetID), ii.AssetDefinition)
96                                         }
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, and filter transactions by accountID optional
292 func (w *Wallet) GetTransactions(accountID string) ([]*query.AnnotatedTx, error) {
293         annotatedTxs := []*query.AnnotatedTx{}
294
295         txIter := w.DB.IteratorPrefix([]byte(TxPrefix))
296         defer txIter.Release()
297         for txIter.Next() {
298                 annotatedTx := &query.AnnotatedTx{}
299                 if err := json.Unmarshal(txIter.Value(), &annotatedTx); err != nil {
300                         return nil, err
301                 }
302
303                 if accountID == "" || findTransactionsByAccount(annotatedTx, accountID) {
304                         annotateTxsAsset(w, []*query.AnnotatedTx{annotatedTx})
305                         annotatedTxs = append([]*query.AnnotatedTx{annotatedTx}, annotatedTxs...)
306                 }
307         }
308
309         return annotatedTxs, nil
310 }
311
312 // GetAccountBalances return all account balances
313 func (w *Wallet) GetAccountBalances(accountID string, id string) ([]AccountBalance, error) {
314         return w.indexBalances(w.GetAccountUtxos(accountID, "", false, false))
315 }
316
317 // AccountBalance account balance
318 type AccountBalance struct {
319         AccountID       string                 `json:"account_id"`
320         Alias           string                 `json:"account_alias"`
321         AssetAlias      string                 `json:"asset_alias"`
322         AssetID         string                 `json:"asset_id"`
323         Amount          uint64                 `json:"amount"`
324         AssetDefinition map[string]interface{} `json:"asset_definition"`
325 }
326
327 func (w *Wallet) indexBalances(accountUTXOs []*account.UTXO) ([]AccountBalance, error) {
328         accBalance := make(map[string]map[string]uint64)
329         balances := []AccountBalance{}
330
331         for _, accountUTXO := range accountUTXOs {
332                 assetID := accountUTXO.AssetID.String()
333                 if _, ok := accBalance[accountUTXO.AccountID]; ok {
334                         if _, ok := accBalance[accountUTXO.AccountID][assetID]; ok {
335                                 accBalance[accountUTXO.AccountID][assetID] += accountUTXO.Amount
336                         } else {
337                                 accBalance[accountUTXO.AccountID][assetID] = accountUTXO.Amount
338                         }
339                 } else {
340                         accBalance[accountUTXO.AccountID] = map[string]uint64{assetID: accountUTXO.Amount}
341                 }
342         }
343
344         var sortedAccount []string
345         for k := range accBalance {
346                 sortedAccount = append(sortedAccount, k)
347         }
348         sort.Strings(sortedAccount)
349
350         for _, id := range sortedAccount {
351                 var sortedAsset []string
352                 for k := range accBalance[id] {
353                         sortedAsset = append(sortedAsset, k)
354                 }
355                 sort.Strings(sortedAsset)
356
357                 for _, assetID := range sortedAsset {
358                         alias := w.AccountMgr.GetAliasByID(id)
359                         targetAsset, err := w.AssetReg.GetAsset(assetID)
360                         if err != nil {
361                                 return nil, err
362                         }
363
364                         assetAlias := *targetAsset.Alias
365                         balances = append(balances, AccountBalance{
366                                 Alias:           alias,
367                                 AccountID:       id,
368                                 AssetID:         assetID,
369                                 AssetAlias:      assetAlias,
370                                 Amount:          accBalance[id][assetID],
371                                 AssetDefinition: targetAsset.DefinitionMap,
372                         })
373                 }
374         }
375
376         return balances, nil
377 }