8 log "github.com/sirupsen/logrus"
9 "github.com/tendermint/tmlibs/db"
11 "github.com/bytom/account"
12 "github.com/bytom/asset"
13 "github.com/bytom/blockchain/query"
14 "github.com/bytom/consensus"
15 "github.com/bytom/consensus/segwit"
16 "github.com/bytom/crypto/sha3pool"
17 chainjson "github.com/bytom/encoding/json"
18 "github.com/bytom/errors"
19 "github.com/bytom/protocol/bc"
20 "github.com/bytom/protocol/bc/types"
23 type rawOutput struct {
34 type accountOutput struct {
43 //TxPrefix is wallet database transactions prefix
45 //TxIndexPrefix is wallet database tx index prefix
46 TxIndexPrefix = "TID:"
49 func formatKey(blockHeight uint64, position uint32) string {
50 return fmt.Sprintf("%016x%08x", blockHeight, position)
53 func calcAnnotatedKey(formatKey string) []byte {
54 return []byte(TxPrefix + formatKey)
57 func calcDeleteKey(blockHeight uint64) []byte {
58 return []byte(fmt.Sprintf("%s%016x", TxPrefix, blockHeight))
61 func calcTxIndexKey(txID string) []byte {
62 return []byte(TxIndexPrefix + txID)
65 // deleteTransaction delete transactions when orphan block rollback
66 func (w *Wallet) deleteTransactions(batch db.Batch, height uint64) {
67 tmpTx := query.AnnotatedTx{}
69 txIter := w.DB.IteratorPrefix(calcDeleteKey(height))
70 defer txIter.Release()
73 if err := json.Unmarshal(txIter.Value(), &tmpTx); err == nil {
75 batch.Delete(calcTxIndexKey(tmpTx.ID.String()))
78 batch.Delete(txIter.Key())
82 // ReverseAccountUTXOs process the invalid blocks when orphan block rollback
83 func (w *Wallet) reverseAccountUTXOs(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
86 // unknow how many spent and retire outputs
87 reverseOuts := []*rawOutput{}
90 for txIndex, tx := range b.Transactions {
91 for _, inpID := range tx.Tx.InputIDs {
93 sp, err := tx.Spend(inpID)
98 resOut, ok := tx.Entries[*sp.SpentOutputId].(*bc.Output)
103 statusFail, _ := txStatus.GetStatus(txIndex)
104 if statusFail && *resOut.Source.Value.AssetId != *consensus.BTMAssetID {
109 OutputID: *sp.SpentOutputId,
110 AssetAmount: *resOut.Source.Value,
111 ControlProgram: resOut.ControlProgram.Code,
113 sourceID: *resOut.Source.Ref,
114 sourcePos: resOut.Source.Position,
116 reverseOuts = append(reverseOuts, out)
120 accOuts := loadAccountInfo(reverseOuts, w)
121 if err = upsertConfirmedAccountOutputs(accOuts, batch); err != nil {
122 log.WithField("err", err).Error("reversing account spent and retire outputs")
127 for _, tx := range b.Transactions {
128 for j := range tx.Outputs {
129 resOutID := tx.ResultIds[j]
130 resOut, ok := tx.Entries[*resOutID].(*bc.Output)
136 if segwit.IsP2WScript(resOut.ControlProgram.Code) {
137 // delete standard UTXOs
138 batch.Delete(account.StandardUTXOKey(*resOutID))
140 // delete contract UTXOs
141 batch.Delete(account.ContractUTXOKey(*resOutID))
147 // saveExternalAssetDefinition save external and local assets definition,
148 // when query ,query local first and if have no then query external
149 // details see getAliasDefinition
150 func saveExternalAssetDefinition(b *types.Block, walletDB db.DB) {
151 storeBatch := walletDB.NewBatch()
152 defer storeBatch.Write()
154 for _, tx := range b.Transactions {
155 for _, orig := range tx.Inputs {
156 if ii, ok := orig.TypedInput.(*types.IssuanceInput); ok {
157 if isValidJSON(ii.AssetDefinition) {
158 assetID := ii.AssetID()
159 if assetExist := walletDB.Get(asset.CalcExtAssetKey(&assetID)); assetExist == nil {
160 storeBatch.Set(asset.CalcExtAssetKey(&assetID), ii.AssetDefinition)
168 // Summary is the struct of transaction's input and output summary
169 type Summary struct {
170 Type string `json:"type"`
171 AssetID bc.AssetID `json:"asset_id,omitempty"`
172 AssetAlias string `json:"asset_alias,omitempty"`
173 Amount uint64 `json:"amount,omitempty"`
174 AccountID string `json:"account_id,omitempty"`
175 AccountAlias string `json:"account_alias,omitempty"`
176 Arbitrary chainjson.HexBytes `json:"arbitrary,omitempty"`
179 // TxSummary is the struct of transaction summary
180 type TxSummary struct {
181 ID bc.Hash `json:"tx_id"`
182 Timestamp uint64 `json:"block_time"`
183 Inputs []Summary `json:"inputs"`
184 Outputs []Summary `json:"outputs"`
187 // indexTransactions saves all annotated transactions to the database.
188 func (w *Wallet) indexTransactions(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) error {
189 annotatedTxs := w.filterAccountTxs(b, txStatus)
190 saveExternalAssetDefinition(b, w.DB)
191 annotateTxsAsset(w, annotatedTxs)
192 annotateTxsAccount(annotatedTxs, w.DB)
194 for _, tx := range annotatedTxs {
195 rawTx, err := json.Marshal(tx)
197 log.WithField("err", err).Error("inserting annotated_txs to db")
201 batch.Set(calcAnnotatedKey(formatKey(b.Height, uint32(tx.Position))), rawTx)
202 batch.Set(calcTxIndexKey(tx.ID.String()), []byte(formatKey(b.Height, uint32(tx.Position))))
207 // buildAccountUTXOs process valid blocks to build account unspent outputs db
208 func (w *Wallet) buildAccountUTXOs(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
209 // get the spent UTXOs and delete the UTXOs from DB
210 prevoutDBKeys(batch, b, txStatus)
213 var outs []*rawOutput
214 for txIndex, tx := range b.Transactions {
215 for i, out := range tx.Outputs {
216 resOutID := tx.ResultIds[i]
217 resOut, ok := tx.Entries[*resOutID].(*bc.Output)
222 if statusFail, _ := txStatus.GetStatus(txIndex); statusFail && *resOut.Source.Value.AssetId != *consensus.BTMAssetID {
227 OutputID: *tx.OutputID(i),
228 AssetAmount: out.AssetAmount,
229 ControlProgram: out.ControlProgram,
231 outputIndex: uint32(i),
232 sourceID: *resOut.Source.Ref,
233 sourcePos: resOut.Source.Position,
236 // coinbase utxo valid height
238 out.ValidHeight = b.Height + consensus.CoinbasePendingBlockNumber
240 outs = append(outs, out)
243 accOuts := loadAccountInfo(outs, w)
245 if err := upsertConfirmedAccountOutputs(accOuts, batch); err != nil {
246 log.WithField("err", err).Error("building new account outputs")
250 func prevoutDBKeys(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
251 for txIndex, tx := range b.Transactions {
252 for _, inpID := range tx.Tx.InputIDs {
253 sp, err := tx.Spend(inpID)
258 statusFail, _ := txStatus.GetStatus(txIndex)
259 if statusFail && *sp.WitnessDestination.Value.AssetId != *consensus.BTMAssetID {
263 resOut, ok := tx.Entries[*sp.SpentOutputId].(*bc.Output)
266 log.WithField("SpentOutputId", *sp.SpentOutputId).Info("the OutputId is retirement")
270 if segwit.IsP2WScript(resOut.ControlProgram.Code) {
271 // delete standard UTXOs
272 batch.Delete(account.StandardUTXOKey(*sp.SpentOutputId))
274 // delete contract UTXOs
275 batch.Delete(account.ContractUTXOKey(*sp.SpentOutputId))
282 // loadAccountInfo turns a set of output IDs into a set of
283 // outputs by adding account annotations. Outputs that can't be
284 // annotated are excluded from the result.
285 func loadAccountInfo(outs []*rawOutput, w *Wallet) []*accountOutput {
286 outsByScript := make(map[string][]*rawOutput, len(outs))
287 for _, out := range outs {
288 scriptStr := string(out.ControlProgram)
289 outsByScript[scriptStr] = append(outsByScript[scriptStr], out)
292 result := make([]*accountOutput, 0, len(outs))
293 cp := account.CtrlProgram{}
296 for s := range outsByScript {
297 // smart contract UTXO
298 if !segwit.IsP2WScript([]byte(s)) {
299 for _, out := range outsByScript[s] {
300 newOut := &accountOutput{
304 result = append(result, newOut)
310 sha3pool.Sum256(hash[:], []byte(s))
311 bytes := w.DB.Get(account.CPKey(hash))
316 err := json.Unmarshal(bytes, &cp)
321 isExist := w.DB.Get(account.Key(cp.AccountID))
326 for _, out := range outsByScript[s] {
327 newOut := &accountOutput{
329 AccountID: cp.AccountID,
331 keyIndex: cp.KeyIndex,
334 result = append(result, newOut)
341 // upsertConfirmedAccountOutputs records the account data for confirmed utxos.
342 // If the account utxo already exists (because it's from a local tx), the
343 // block confirmation data will in the row will be updated.
344 func upsertConfirmedAccountOutputs(outs []*accountOutput, batch db.Batch) error {
347 for _, out := range outs {
349 OutputID: out.OutputID,
350 SourceID: out.sourceID,
351 AssetID: *out.AssetId,
353 SourcePos: out.sourcePos,
354 ControlProgram: out.ControlProgram,
355 ControlProgramIndex: out.keyIndex,
356 AccountID: out.AccountID,
357 Address: out.Address,
358 ValidHeight: out.ValidHeight,
361 data, err := json.Marshal(u)
363 return errors.Wrap(err, "failed marshal accountutxo")
366 if segwit.IsP2WScript(out.ControlProgram) {
368 batch.Set(account.StandardUTXOKey(out.OutputID), data)
371 batch.Set(account.ContractUTXOKey(out.OutputID), data)
378 // filterAccountTxs related and build the fully annotated transactions.
379 func (w *Wallet) filterAccountTxs(b *types.Block, txStatus *bc.TransactionStatus) []*query.AnnotatedTx {
380 annotatedTxs := make([]*query.AnnotatedTx, 0, len(b.Transactions))
383 for pos, tx := range b.Transactions {
384 statusFail, _ := txStatus.GetStatus(pos)
385 for _, v := range tx.Outputs {
387 sha3pool.Sum256(hash[:], v.ControlProgram)
388 if bytes := w.DB.Get(account.CPKey(hash)); bytes != nil {
389 annotatedTxs = append(annotatedTxs, w.buildAnnotatedTransaction(tx, b, statusFail, pos))
390 continue transactionLoop
394 for _, v := range tx.Inputs {
395 outid, err := v.SpentOutputID()
399 if bytes := w.DB.Get(account.StandardUTXOKey(outid)); bytes != nil {
400 annotatedTxs = append(annotatedTxs, w.buildAnnotatedTransaction(tx, b, statusFail, pos))
401 continue transactionLoop
409 // GetTransactionByTxID get transaction by txID
410 func (w *Wallet) GetTransactionByTxID(txID string) (*query.AnnotatedTx, error) {
411 formatKey := w.DB.Get(calcTxIndexKey(txID))
412 if formatKey == nil {
413 return nil, fmt.Errorf("No transaction(tx_id=%s) ", txID)
416 annotatedTx := &query.AnnotatedTx{}
417 txInfo := w.DB.Get(calcAnnotatedKey(string(formatKey)))
418 if err := json.Unmarshal(txInfo, annotatedTx); err != nil {
422 return annotatedTx, nil
425 // GetTransactionsByTxID get account txs by account tx ID
426 func (w *Wallet) GetTransactionsByTxID(txID string) ([]*query.AnnotatedTx, error) {
427 var annotatedTxs []*query.AnnotatedTx
431 rawFormatKey := w.DB.Get(calcTxIndexKey(txID))
432 if rawFormatKey == nil {
433 return nil, fmt.Errorf("No transaction(txid=%s) ", txID)
435 formatKey = string(rawFormatKey)
438 txIter := w.DB.IteratorPrefix(calcAnnotatedKey(formatKey))
439 defer txIter.Release()
441 annotatedTx := &query.AnnotatedTx{}
442 if err := json.Unmarshal(txIter.Value(), annotatedTx); err != nil {
445 annotatedTxs = append([]*query.AnnotatedTx{annotatedTx}, annotatedTxs...)
448 return annotatedTxs, nil
451 // GetTransactionsSummary get transactions summary
452 func (w *Wallet) GetTransactionsSummary(transactions []*query.AnnotatedTx) []TxSummary {
455 for _, annotatedTx := range transactions {
456 tmpTxSummary := TxSummary{
457 Inputs: make([]Summary, len(annotatedTx.Inputs)),
458 Outputs: make([]Summary, len(annotatedTx.Outputs)),
460 Timestamp: annotatedTx.Timestamp,
463 for i, input := range annotatedTx.Inputs {
464 tmpTxSummary.Inputs[i].Type = input.Type
465 tmpTxSummary.Inputs[i].AccountID = input.AccountID
466 tmpTxSummary.Inputs[i].AccountAlias = input.AccountAlias
467 tmpTxSummary.Inputs[i].AssetID = input.AssetID
468 tmpTxSummary.Inputs[i].AssetAlias = input.AssetAlias
469 tmpTxSummary.Inputs[i].Amount = input.Amount
470 tmpTxSummary.Inputs[i].Arbitrary = input.Arbitrary
472 for j, output := range annotatedTx.Outputs {
473 tmpTxSummary.Outputs[j].Type = output.Type
474 tmpTxSummary.Outputs[j].AccountID = output.AccountID
475 tmpTxSummary.Outputs[j].AccountAlias = output.AccountAlias
476 tmpTxSummary.Outputs[j].AssetID = output.AssetID
477 tmpTxSummary.Outputs[j].AssetAlias = output.AssetAlias
478 tmpTxSummary.Outputs[j].Amount = output.Amount
481 Txs = append(Txs, tmpTxSummary)
487 func findTransactionsByAccount(annotatedTx *query.AnnotatedTx, accountID string) bool {
488 for _, input := range annotatedTx.Inputs {
489 if input.AccountID == accountID {
494 for _, output := range annotatedTx.Outputs {
495 if output.AccountID == accountID {
503 // GetTransactionsByAccountID get account txs by account ID
504 func (w *Wallet) GetTransactionsByAccountID(accountID string) ([]*query.AnnotatedTx, error) {
505 var annotatedTxs []*query.AnnotatedTx
507 txIter := w.DB.IteratorPrefix([]byte(TxPrefix))
508 defer txIter.Release()
510 annotatedTx := &query.AnnotatedTx{}
511 if err := json.Unmarshal(txIter.Value(), &annotatedTx); err != nil {
515 if findTransactionsByAccount(annotatedTx, accountID) {
516 annotatedTxs = append(annotatedTxs, annotatedTx)
520 return annotatedTxs, nil
523 // GetAccountUTXOs return all account unspent outputs
524 func (w *Wallet) GetAccountUTXOs(id string) []account.UTXO {
525 var accountUTXOs []account.UTXO
527 accountUTXOIter := w.DB.IteratorPrefix([]byte(account.UTXOPreFix + id))
528 defer accountUTXOIter.Release()
529 for accountUTXOIter.Next() {
530 accountUTXO := account.UTXO{}
531 if err := json.Unmarshal(accountUTXOIter.Value(), &accountUTXO); err != nil {
532 hashKey := accountUTXOIter.Key()[len(account.UTXOPreFix):]
533 log.WithField("UTXO hash", string(hashKey)).Warn("get account UTXO")
535 accountUTXOs = append(accountUTXOs, accountUTXO)
542 // GetAccountBalances return all account balances
543 func (w *Wallet) GetAccountBalances(id string) []AccountBalance {
544 return w.indexBalances(w.GetAccountUTXOs(""))
547 // AccountBalance account balance
548 type AccountBalance struct {
549 AccountID string `json:"account_id"`
550 Alias string `json:"account_alias"`
551 AssetAlias string `json:"asset_alias"`
552 AssetID string `json:"asset_id"`
553 Amount uint64 `json:"amount"`
556 func (w *Wallet) indexBalances(accountUTXOs []account.UTXO) []AccountBalance {
557 accBalance := make(map[string]map[string]uint64)
558 balances := make([]AccountBalance, 0)
559 tmpBalance := AccountBalance{}
561 for _, accountUTXO := range accountUTXOs {
562 assetID := accountUTXO.AssetID.String()
563 if _, ok := accBalance[accountUTXO.AccountID]; ok {
564 if _, ok := accBalance[accountUTXO.AccountID][assetID]; ok {
565 accBalance[accountUTXO.AccountID][assetID] += accountUTXO.Amount
567 accBalance[accountUTXO.AccountID][assetID] = accountUTXO.Amount
570 accBalance[accountUTXO.AccountID] = map[string]uint64{assetID: accountUTXO.Amount}
574 var sortedAccount []string
575 for k := range accBalance {
576 sortedAccount = append(sortedAccount, k)
578 sort.Strings(sortedAccount)
580 for _, id := range sortedAccount {
581 var sortedAsset []string
582 for k := range accBalance[id] {
583 sortedAsset = append(sortedAsset, k)
585 sort.Strings(sortedAsset)
587 for _, assetID := range sortedAsset {
588 alias := w.AccountMgr.GetAliasByID(id)
589 assetAlias := w.AssetReg.GetAliasByID(assetID)
590 tmpBalance.Alias = alias
591 tmpBalance.AccountID = id
592 tmpBalance.AssetID = assetID
593 tmpBalance.AssetAlias = assetAlias
594 tmpBalance.Amount = accBalance[id][assetID]
595 balances = append(balances, tmpBalance)