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);
223 statusFail && *resOut.Source.Value.AssetId != *consensus.BTMAssetID {
228 OutputID: *tx.OutputID(i),
229 AssetAmount: out.AssetAmount,
230 ControlProgram: out.ControlProgram,
232 outputIndex: uint32(i),
233 sourceID: *resOut.Source.Ref,
234 sourcePos: resOut.Source.Position,
237 // coinbase utxo valid height
239 out.ValidHeight = b.Height + consensus.CoinbasePendingBlockNumber
241 outs = append(outs, out)
244 accOuts := loadAccountInfo(outs, w)
246 if err := upsertConfirmedAccountOutputs(accOuts, batch); err != nil {
247 log.WithField("err", err).Error("building new account outputs")
251 func prevoutDBKeys(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
252 for txIndex, tx := range b.Transactions {
253 for _, inpID := range tx.Tx.InputIDs {
254 sp, err := tx.Spend(inpID)
259 statusFail, _ := txStatus.GetStatus(txIndex)
260 if statusFail && *sp.WitnessDestination.Value.AssetId != *consensus.BTMAssetID {
264 resOut, ok := tx.Entries[*sp.SpentOutputId].(*bc.Output)
267 log.WithField("SpentOutputId", *sp.SpentOutputId).Info("the OutputId is retirement")
271 if segwit.IsP2WScript(resOut.ControlProgram.Code) {
272 // delete standard UTXOs
273 batch.Delete(account.StandardUTXOKey(*sp.SpentOutputId))
275 // delete contract UTXOs
276 batch.Delete(account.ContractUTXOKey(*sp.SpentOutputId))
283 // loadAccountInfo turns a set of output IDs into a set of
284 // outputs by adding account annotations. Outputs that can't be
285 // annotated are excluded from the result.
286 func loadAccountInfo(outs []*rawOutput, w *Wallet) []*accountOutput {
287 outsByScript := make(map[string][]*rawOutput, len(outs))
288 for _, out := range outs {
289 scriptStr := string(out.ControlProgram)
290 outsByScript[scriptStr] = append(outsByScript[scriptStr], out)
293 result := make([]*accountOutput, 0, len(outs))
294 cp := account.CtrlProgram{}
297 for s := range outsByScript {
298 // smart contract UTXO
299 if !segwit.IsP2WScript([]byte(s)) {
300 for _, out := range outsByScript[s] {
301 newOut := &accountOutput{
305 result = append(result, newOut)
311 sha3pool.Sum256(hash[:], []byte(s))
312 bytes := w.DB.Get(account.CPKey(hash))
317 err := json.Unmarshal(bytes, &cp)
322 isExist := w.DB.Get(account.Key(cp.AccountID))
327 for _, out := range outsByScript[s] {
328 newOut := &accountOutput{
330 AccountID: cp.AccountID,
332 keyIndex: cp.KeyIndex,
335 result = append(result, newOut)
342 // upsertConfirmedAccountOutputs records the account data for confirmed utxos.
343 // If the account utxo already exists (because it's from a local tx), the
344 // block confirmation data will in the row will be updated.
345 func upsertConfirmedAccountOutputs(outs []*accountOutput, batch db.Batch) error {
348 for _, out := range outs {
350 OutputID: out.OutputID,
351 SourceID: out.sourceID,
352 AssetID: *out.AssetId,
354 SourcePos: out.sourcePos,
355 ControlProgram: out.ControlProgram,
356 ControlProgramIndex: out.keyIndex,
357 AccountID: out.AccountID,
358 Address: out.Address,
359 ValidHeight: out.ValidHeight,
362 data, err := json.Marshal(u)
364 return errors.Wrap(err, "failed marshal accountutxo")
367 if segwit.IsP2WScript(out.ControlProgram) {
369 batch.Set(account.StandardUTXOKey(out.OutputID), data)
372 batch.Set(account.ContractUTXOKey(out.OutputID), data)
379 // filterAccountTxs related and build the fully annotated transactions.
380 func (w *Wallet) filterAccountTxs(b *types.Block, txStatus *bc.TransactionStatus) []*query.AnnotatedTx {
381 annotatedTxs := make([]*query.AnnotatedTx, 0, len(b.Transactions))
384 for pos, tx := range b.Transactions {
385 statusFail, _ := txStatus.GetStatus(pos)
386 for _, v := range tx.Outputs {
388 sha3pool.Sum256(hash[:], v.ControlProgram)
389 if bytes := w.DB.Get(account.CPKey(hash)); bytes != nil {
390 annotatedTxs = append(annotatedTxs, buildAnnotatedTransaction(tx, b, statusFail, pos))
391 continue transactionLoop
395 for _, v := range tx.Inputs {
396 outid, err := v.SpentOutputID()
400 if bytes := w.DB.Get(account.StandardUTXOKey(outid)); bytes != nil {
401 annotatedTxs = append(annotatedTxs, buildAnnotatedTransaction(tx, b, statusFail, pos))
402 continue transactionLoop
410 // GetTransactionByTxID get transaction by txID
411 func (w *Wallet) GetTransactionByTxID(txID string) (*query.AnnotatedTx, error) {
412 formatKey := w.DB.Get(calcTxIndexKey(txID))
413 if formatKey == nil {
414 return nil, fmt.Errorf("No transaction(tx_id=%s) ", txID)
417 annotatedTx := &query.AnnotatedTx{}
418 txInfo := w.DB.Get(calcAnnotatedKey(string(formatKey)))
419 if err := json.Unmarshal(txInfo, annotatedTx); err != nil {
423 return annotatedTx, nil
426 // GetTransactionsByTxID get account txs by account tx ID
427 func (w *Wallet) GetTransactionsByTxID(txID string) ([]*query.AnnotatedTx, error) {
428 var annotatedTxs []*query.AnnotatedTx
432 rawFormatKey := w.DB.Get(calcTxIndexKey(txID))
433 if rawFormatKey == nil {
434 return nil, fmt.Errorf("No transaction(txid=%s) ", txID)
436 formatKey = string(rawFormatKey)
439 txIter := w.DB.IteratorPrefix(calcAnnotatedKey(formatKey))
440 defer txIter.Release()
442 annotatedTx := &query.AnnotatedTx{}
443 if err := json.Unmarshal(txIter.Value(), annotatedTx); err != nil {
446 annotatedTxs = append([]*query.AnnotatedTx{annotatedTx}, annotatedTxs...)
449 return annotatedTxs, nil
452 // GetTransactionsSummary get transactions summary
453 func (w *Wallet) GetTransactionsSummary(transactions []*query.AnnotatedTx) []TxSummary {
456 for _, annotatedTx := range transactions {
457 tmpTxSummary := TxSummary{
458 Inputs: make([]Summary, len(annotatedTx.Inputs)),
459 Outputs: make([]Summary, len(annotatedTx.Outputs)),
461 Timestamp: annotatedTx.Timestamp,
464 for i, input := range annotatedTx.Inputs {
465 tmpTxSummary.Inputs[i].Type = input.Type
466 tmpTxSummary.Inputs[i].AccountID = input.AccountID
467 tmpTxSummary.Inputs[i].AccountAlias = input.AccountAlias
468 tmpTxSummary.Inputs[i].AssetID = input.AssetID
469 tmpTxSummary.Inputs[i].AssetAlias = input.AssetAlias
470 tmpTxSummary.Inputs[i].Amount = input.Amount
471 tmpTxSummary.Inputs[i].Arbitrary = input.Arbitrary
473 for j, output := range annotatedTx.Outputs {
474 tmpTxSummary.Outputs[j].Type = output.Type
475 tmpTxSummary.Outputs[j].AccountID = output.AccountID
476 tmpTxSummary.Outputs[j].AccountAlias = output.AccountAlias
477 tmpTxSummary.Outputs[j].AssetID = output.AssetID
478 tmpTxSummary.Outputs[j].AssetAlias = output.AssetAlias
479 tmpTxSummary.Outputs[j].Amount = output.Amount
482 Txs = append(Txs, tmpTxSummary)
488 func findTransactionsByAccount(annotatedTx *query.AnnotatedTx, accountID string) bool {
489 for _, input := range annotatedTx.Inputs {
490 if input.AccountID == accountID {
495 for _, output := range annotatedTx.Outputs {
496 if output.AccountID == accountID {
504 // GetTransactionsByAccountID get account txs by account ID
505 func (w *Wallet) GetTransactionsByAccountID(accountID string) ([]*query.AnnotatedTx, error) {
506 annotatedTxs := []*query.AnnotatedTx{}
508 txIter := w.DB.IteratorPrefix([]byte(TxPrefix))
509 defer txIter.Release()
511 annotatedTx := &query.AnnotatedTx{}
512 if err := json.Unmarshal(txIter.Value(), &annotatedTx); err != nil {
516 if findTransactionsByAccount(annotatedTx, accountID) {
517 annotatedTxs = append(annotatedTxs, annotatedTx)
521 return annotatedTxs, nil
524 // GetAccountUTXOs return all account unspent outputs
525 func (w *Wallet) GetAccountUTXOs(id string) []account.UTXO {
526 var accountUTXOs []account.UTXO
528 accountUTXOIter := w.DB.IteratorPrefix([]byte(account.UTXOPreFix + id))
529 defer accountUTXOIter.Release()
530 for accountUTXOIter.Next() {
531 accountUTXO := account.UTXO{}
532 if err := json.Unmarshal(accountUTXOIter.Value(), &accountUTXO); err != nil {
533 hashKey := accountUTXOIter.Key()[len(account.UTXOPreFix):]
534 log.WithField("UTXO hash", string(hashKey)).Warn("get account UTXO")
536 accountUTXOs = append(accountUTXOs, accountUTXO)
543 // GetAccountBalances return all account balances
544 func (w *Wallet) GetAccountBalances(id string) []AccountBalance {
545 return w.indexBalances(w.GetAccountUTXOs(""))
548 // AccountBalance account balance
549 type AccountBalance struct {
550 AccountID string `json:"account_id"`
551 Alias string `json:"account_alias"`
552 AssetAlias string `json:"asset_alias"`
553 AssetID string `json:"asset_id"`
554 Amount uint64 `json:"amount"`
557 func (w *Wallet) indexBalances(accountUTXOs []account.UTXO) []AccountBalance {
558 accBalance := make(map[string]map[string]uint64)
559 balances := make([]AccountBalance, 0)
560 tmpBalance := AccountBalance{}
562 for _, accountUTXO := range accountUTXOs {
563 assetID := accountUTXO.AssetID.String()
564 if _, ok := accBalance[accountUTXO.AccountID]; ok {
565 if _, ok := accBalance[accountUTXO.AccountID][assetID]; ok {
566 accBalance[accountUTXO.AccountID][assetID] += accountUTXO.Amount
568 accBalance[accountUTXO.AccountID][assetID] = accountUTXO.Amount
571 accBalance[accountUTXO.AccountID] = map[string]uint64{assetID: accountUTXO.Amount}
575 var sortedAccount []string
576 for k := range accBalance {
577 sortedAccount = append(sortedAccount, k)
579 sort.Strings(sortedAccount)
581 for _, id := range sortedAccount {
582 var sortedAsset []string
583 for k := range accBalance[id] {
584 sortedAsset = append(sortedAsset, k)
586 sort.Strings(sortedAsset)
588 for _, assetID := range sortedAsset {
589 alias := w.AccountMgr.GetAliasByID(id)
590 assetAlias := w.AssetReg.GetAliasByID(assetID)
591 tmpBalance.Alias = alias
592 tmpBalance.AccountID = id
593 tmpBalance.AssetID = assetID
594 tmpBalance.AssetAlias = assetAlias
595 tmpBalance.Amount = accBalance[id][assetID]
596 balances = append(balances, tmpBalance)