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 {
42 //TxPrefix is wallet database transactions prefix
44 //TxIndexPrefix is wallet database tx index prefix
45 TxIndexPrefix = "TID:"
48 func formatKey(blockHeight uint64, position uint32) string {
49 return fmt.Sprintf("%016x%08x", blockHeight, position)
52 func calcAnnotatedKey(formatKey string) []byte {
53 return []byte(TxPrefix + formatKey)
56 func calcDeleteKey(blockHeight uint64) []byte {
57 return []byte(fmt.Sprintf("%s%016x", TxPrefix, blockHeight))
60 func calcTxIndexKey(txID string) []byte {
61 return []byte(TxIndexPrefix + txID)
64 // deleteTransaction delete transactions when orphan block rollback
65 func (w *Wallet) deleteTransactions(batch db.Batch, height uint64) {
66 tmpTx := query.AnnotatedTx{}
68 txIter := w.DB.IteratorPrefix(calcDeleteKey(height))
69 defer txIter.Release()
72 if err := json.Unmarshal(txIter.Value(), &tmpTx); err == nil {
74 batch.Delete(calcTxIndexKey(tmpTx.ID.String()))
77 batch.Delete(txIter.Key())
81 // ReverseAccountUTXOs process the invalid blocks when orphan block rollback
82 func (w *Wallet) reverseAccountUTXOs(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
85 // unknow how many spent and retire outputs
86 reverseOuts := []*rawOutput{}
89 for txIndex, tx := range b.Transactions {
90 for _, inpID := range tx.Tx.InputIDs {
92 sp, err := tx.Spend(inpID)
97 resOut, ok := tx.Entries[*sp.SpentOutputId].(*bc.Output)
102 statusFail, _ := txStatus.GetStatus(txIndex)
103 if statusFail && *resOut.Source.Value.AssetId != *consensus.BTMAssetID {
108 OutputID: *sp.SpentOutputId,
109 AssetAmount: *resOut.Source.Value,
110 ControlProgram: resOut.ControlProgram.Code,
112 sourceID: *resOut.Source.Ref,
113 sourcePos: resOut.Source.Position,
115 reverseOuts = append(reverseOuts, out)
119 accOuts := loadAccountInfo(reverseOuts, w)
120 if err = upsertConfirmedAccountOutputs(accOuts, batch); err != nil {
121 log.WithField("err", err).Error("reversing account spent and retire outputs")
126 for _, tx := range b.Transactions {
127 for j := range tx.Outputs {
128 resOutID := tx.ResultIds[j]
129 resOut, ok := tx.Entries[*resOutID].(*bc.Output)
135 if segwit.IsP2WScript(resOut.ControlProgram.Code) {
136 // delete standard UTXOs
137 batch.Delete(account.StandardUTXOKey(*resOutID))
139 // delete contract UTXOs
140 batch.Delete(account.ContractUTXOKey(*resOutID))
146 // saveExternalAssetDefinition save external and local assets definition,
147 // when query ,query local first and if have no then query external
148 // details see getAliasDefinition
149 func saveExternalAssetDefinition(b *types.Block, walletDB db.DB) {
150 storeBatch := walletDB.NewBatch()
151 defer storeBatch.Write()
153 for _, tx := range b.Transactions {
154 for _, orig := range tx.Inputs {
155 if ii, ok := orig.TypedInput.(*types.IssuanceInput); ok {
156 if isValidJSON(ii.AssetDefinition) {
157 assetID := ii.AssetID()
158 if assetExist := walletDB.Get(asset.CalcExtAssetKey(&assetID)); assetExist == nil {
159 storeBatch.Set(asset.CalcExtAssetKey(&assetID), ii.AssetDefinition)
167 // Summary is the struct of transaction's input and output summary
168 type Summary struct {
169 Type string `json:"type"`
170 AssetID bc.AssetID `json:"asset_id,omitempty"`
171 AssetAlias string `json:"asset_alias,omitempty"`
172 Amount uint64 `json:"amount,omitempty"`
173 AccountID string `json:"account_id,omitempty"`
174 AccountAlias string `json:"account_alias,omitempty"`
175 Arbitrary chainjson.HexBytes `json:"arbitrary,omitempty"`
178 // TxSummary is the struct of transaction summary
179 type TxSummary struct {
180 ID bc.Hash `json:"tx_id"`
181 Timestamp uint64 `json:"block_time"`
182 Inputs []Summary `json:"inputs"`
183 Outputs []Summary `json:"outputs"`
186 // indexTransactions saves all annotated transactions to the database.
187 func (w *Wallet) indexTransactions(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) error {
188 annotatedTxs := w.filterAccountTxs(b, txStatus)
189 saveExternalAssetDefinition(b, w.DB)
190 annotateTxsAsset(w, annotatedTxs)
191 annotateTxsAccount(annotatedTxs, w.DB)
193 for _, tx := range annotatedTxs {
194 rawTx, err := json.Marshal(tx)
196 log.WithField("err", err).Error("inserting annotated_txs to db")
200 batch.Set(calcAnnotatedKey(formatKey(b.Height, uint32(tx.Position))), rawTx)
201 batch.Set(calcTxIndexKey(tx.ID.String()), []byte(formatKey(b.Height, uint32(tx.Position))))
206 // buildAccountUTXOs process valid blocks to build account unspent outputs db
207 func (w *Wallet) buildAccountUTXOs(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
208 // get the spent UTXOs and delete the UTXOs from DB
209 prevoutDBKeys(batch, b, txStatus)
212 var outs []*rawOutput
213 for txIndex, tx := range b.Transactions {
214 for i, out := range tx.Outputs {
215 resOutID := tx.ResultIds[i]
216 resOut, ok := tx.Entries[*resOutID].(*bc.Output)
221 if statusFail, _ := txStatus.GetStatus(txIndex); statusFail && *resOut.Source.Value.AssetId != *consensus.BTMAssetID {
226 OutputID: *tx.OutputID(i),
227 AssetAmount: out.AssetAmount,
228 ControlProgram: out.ControlProgram,
230 outputIndex: uint32(i),
231 sourceID: *resOut.Source.Ref,
232 sourcePos: resOut.Source.Position,
235 // coinbase utxo valid height
237 out.ValidHeight = b.Height + consensus.CoinbasePendingBlockNumber
239 outs = append(outs, out)
242 accOuts := loadAccountInfo(outs, w)
244 if err := upsertConfirmedAccountOutputs(accOuts, batch); err != nil {
245 log.WithField("err", err).Error("building new account outputs")
249 func prevoutDBKeys(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
250 for txIndex, tx := range b.Transactions {
251 for _, inpID := range tx.Tx.InputIDs {
252 sp, err := tx.Spend(inpID)
257 statusFail, _ := txStatus.GetStatus(txIndex)
258 if statusFail && *sp.WitnessDestination.Value.AssetId != *consensus.BTMAssetID {
262 resOut, ok := tx.Entries[*sp.SpentOutputId].(*bc.Output)
265 log.WithField("SpentOutputId", *sp.SpentOutputId).Info("the OutputId is retirement")
269 if segwit.IsP2WScript(resOut.ControlProgram.Code) {
270 // delete standard UTXOs
271 batch.Delete(account.StandardUTXOKey(*sp.SpentOutputId))
273 // delete contract UTXOs
274 batch.Delete(account.ContractUTXOKey(*sp.SpentOutputId))
281 // loadAccountInfo turns a set of output IDs into a set of
282 // outputs by adding account annotations. Outputs that can't be
283 // annotated are excluded from the result.
284 func loadAccountInfo(outs []*rawOutput, w *Wallet) []*accountOutput {
285 outsByScript := make(map[string][]*rawOutput, len(outs))
286 for _, out := range outs {
287 scriptStr := string(out.ControlProgram)
288 outsByScript[scriptStr] = append(outsByScript[scriptStr], out)
291 result := make([]*accountOutput, 0, len(outs))
292 cp := account.CtrlProgram{}
295 for s := range outsByScript {
296 // smart contract UTXO
297 if !segwit.IsP2WScript([]byte(s)) {
298 for _, out := range outsByScript[s] {
299 newOut := &accountOutput{
302 result = append(result, newOut)
308 sha3pool.Sum256(hash[:], []byte(s))
309 bytes := w.DB.Get(account.CPKey(hash))
314 err := json.Unmarshal(bytes, &cp)
319 isExist := w.DB.Get(account.Key(cp.AccountID))
324 for _, out := range outsByScript[s] {
325 newOut := &accountOutput{
327 AccountID: cp.AccountID,
329 keyIndex: cp.KeyIndex,
331 result = append(result, newOut)
338 // upsertConfirmedAccountOutputs records the account data for confirmed utxos.
339 // If the account utxo already exists (because it's from a local tx), the
340 // block confirmation data will in the row will be updated.
341 func upsertConfirmedAccountOutputs(outs []*accountOutput, batch db.Batch) error {
344 for _, out := range outs {
346 OutputID: out.OutputID,
347 SourceID: out.sourceID,
348 AssetID: *out.AssetId,
350 SourcePos: out.sourcePos,
351 ControlProgram: out.ControlProgram,
352 ControlProgramIndex: out.keyIndex,
353 AccountID: out.AccountID,
354 Address: out.Address,
355 ValidHeight: out.ValidHeight,
358 data, err := json.Marshal(u)
360 return errors.Wrap(err, "failed marshal accountutxo")
363 if segwit.IsP2WScript(out.ControlProgram) {
365 batch.Set(account.StandardUTXOKey(out.OutputID), data)
368 batch.Set(account.ContractUTXOKey(out.OutputID), data)
375 // filterAccountTxs related and build the fully annotated transactions.
376 func (w *Wallet) filterAccountTxs(b *types.Block, txStatus *bc.TransactionStatus) []*query.AnnotatedTx {
377 annotatedTxs := make([]*query.AnnotatedTx, 0, len(b.Transactions))
380 for pos, tx := range b.Transactions {
381 statusFail, _ := txStatus.GetStatus(pos)
382 for _, v := range tx.Outputs {
384 sha3pool.Sum256(hash[:], v.ControlProgram)
385 if bytes := w.DB.Get(account.CPKey(hash)); bytes != nil {
386 annotatedTxs = append(annotatedTxs, w.buildAnnotatedTransaction(tx, b, statusFail, pos))
387 continue transactionLoop
391 for _, v := range tx.Inputs {
392 outid, err := v.SpentOutputID()
396 if bytes := w.DB.Get(account.StandardUTXOKey(outid)); bytes != nil {
397 annotatedTxs = append(annotatedTxs, w.buildAnnotatedTransaction(tx, b, statusFail, pos))
398 continue transactionLoop
406 // GetTransactionByTxID get transaction by txID
407 func (w *Wallet) GetTransactionByTxID(txID string) (*query.AnnotatedTx, error) {
408 formatKey := w.DB.Get(calcTxIndexKey(txID))
409 if formatKey == nil {
410 return nil, fmt.Errorf("No transaction(tx_id=%s) ", txID)
413 annotatedTx := &query.AnnotatedTx{}
414 txInfo := w.DB.Get(calcAnnotatedKey(string(formatKey)))
415 if err := json.Unmarshal(txInfo, annotatedTx); err != nil {
419 return annotatedTx, nil
422 // GetTransactionsByTxID get account txs by account tx ID
423 func (w *Wallet) GetTransactionsByTxID(txID string) ([]*query.AnnotatedTx, error) {
424 var annotatedTxs []*query.AnnotatedTx
428 rawFormatKey := w.DB.Get(calcTxIndexKey(txID))
429 if rawFormatKey == nil {
430 return nil, fmt.Errorf("No transaction(txid=%s) ", txID)
432 formatKey = string(rawFormatKey)
435 txIter := w.DB.IteratorPrefix(calcAnnotatedKey(formatKey))
436 defer txIter.Release()
438 annotatedTx := &query.AnnotatedTx{}
439 if err := json.Unmarshal(txIter.Value(), annotatedTx); err != nil {
442 annotatedTxs = append([]*query.AnnotatedTx{annotatedTx}, annotatedTxs...)
445 return annotatedTxs, nil
448 // GetTransactionsSummary get transactions summary
449 func (w *Wallet) GetTransactionsSummary(transactions []*query.AnnotatedTx) []TxSummary {
452 for _, annotatedTx := range transactions {
453 tmpTxSummary := TxSummary{
454 Inputs: make([]Summary, len(annotatedTx.Inputs)),
455 Outputs: make([]Summary, len(annotatedTx.Outputs)),
457 Timestamp: annotatedTx.Timestamp,
460 for i, input := range annotatedTx.Inputs {
461 tmpTxSummary.Inputs[i].Type = input.Type
462 tmpTxSummary.Inputs[i].AccountID = input.AccountID
463 tmpTxSummary.Inputs[i].AccountAlias = input.AccountAlias
464 tmpTxSummary.Inputs[i].AssetID = input.AssetID
465 tmpTxSummary.Inputs[i].AssetAlias = input.AssetAlias
466 tmpTxSummary.Inputs[i].Amount = input.Amount
467 tmpTxSummary.Inputs[i].Arbitrary = input.Arbitrary
469 for j, output := range annotatedTx.Outputs {
470 tmpTxSummary.Outputs[j].Type = output.Type
471 tmpTxSummary.Outputs[j].AccountID = output.AccountID
472 tmpTxSummary.Outputs[j].AccountAlias = output.AccountAlias
473 tmpTxSummary.Outputs[j].AssetID = output.AssetID
474 tmpTxSummary.Outputs[j].AssetAlias = output.AssetAlias
475 tmpTxSummary.Outputs[j].Amount = output.Amount
478 Txs = append(Txs, tmpTxSummary)
484 func findTransactionsByAccount(annotatedTx *query.AnnotatedTx, accountID string) bool {
485 for _, input := range annotatedTx.Inputs {
486 if input.AccountID == accountID {
491 for _, output := range annotatedTx.Outputs {
492 if output.AccountID == accountID {
500 // GetTransactionsByAccountID get account txs by account ID
501 func (w *Wallet) GetTransactionsByAccountID(accountID string) ([]*query.AnnotatedTx, error) {
502 var annotatedTxs []*query.AnnotatedTx
504 txIter := w.DB.IteratorPrefix([]byte(TxPrefix))
505 defer txIter.Release()
507 annotatedTx := &query.AnnotatedTx{}
508 if err := json.Unmarshal(txIter.Value(), &annotatedTx); err != nil {
512 if findTransactionsByAccount(annotatedTx, accountID) {
513 annotatedTxs = append(annotatedTxs, annotatedTx)
517 return annotatedTxs, nil
520 // GetAccountUTXOs return all account unspent outputs
521 func (w *Wallet) GetAccountUTXOs(id string) []account.UTXO {
522 var accountUTXOs []account.UTXO
524 accountUTXOIter := w.DB.IteratorPrefix([]byte(account.UTXOPreFix + id))
525 defer accountUTXOIter.Release()
526 for accountUTXOIter.Next() {
527 accountUTXO := account.UTXO{}
528 if err := json.Unmarshal(accountUTXOIter.Value(), &accountUTXO); err != nil {
529 hashKey := accountUTXOIter.Key()[len(account.UTXOPreFix):]
530 log.WithField("UTXO hash", string(hashKey)).Warn("get account UTXO")
532 accountUTXOs = append(accountUTXOs, accountUTXO)
539 // GetAccountBalances return all account balances
540 func (w *Wallet) GetAccountBalances(id string) []AccountBalance {
541 return w.indexBalances(w.GetAccountUTXOs(""))
544 // AccountBalance account balance
545 type AccountBalance struct {
546 AccountID string `json:"account_id"`
547 Alias string `json:"account_alias"`
548 AssetAlias string `json:"asset_alias"`
549 AssetID string `json:"asset_id"`
550 Amount uint64 `json:"amount"`
553 func (w *Wallet) indexBalances(accountUTXOs []account.UTXO) []AccountBalance {
554 accBalance := make(map[string]map[string]uint64)
555 balances := make([]AccountBalance, 0)
556 tmpBalance := AccountBalance{}
558 for _, accountUTXO := range accountUTXOs {
559 assetID := accountUTXO.AssetID.String()
560 if _, ok := accBalance[accountUTXO.AccountID]; ok {
561 if _, ok := accBalance[accountUTXO.AccountID][assetID]; ok {
562 accBalance[accountUTXO.AccountID][assetID] += accountUTXO.Amount
564 accBalance[accountUTXO.AccountID][assetID] = accountUTXO.Amount
567 accBalance[accountUTXO.AccountID] = map[string]uint64{assetID: accountUTXO.Amount}
571 var sortedAccount []string
572 for k := range accBalance {
573 sortedAccount = append(sortedAccount, k)
575 sort.Strings(sortedAccount)
577 for _, id := range sortedAccount {
578 var sortedAsset []string
579 for k := range accBalance[id] {
580 sortedAsset = append(sortedAsset, k)
582 sort.Strings(sortedAsset)
584 for _, assetID := range sortedAsset {
585 alias := w.AccountMgr.GetAliasByID(id)
586 assetAlias := w.AssetReg.GetAliasByID(assetID)
587 tmpBalance.Alias = alias
588 tmpBalance.AccountID = id
589 tmpBalance.AssetID = assetID
590 tmpBalance.AssetAlias = assetAlias
591 tmpBalance.Amount = accBalance[id][assetID]
592 balances = append(balances, tmpBalance)