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.ExtAssetKey(&assetID)); assetExist == nil {
160 storeBatch.Set(asset.ExtAssetKey(&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 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{
303 result = append(result, newOut)
309 sha3pool.Sum256(hash[:], []byte(s))
310 bytes := w.DB.Get(account.ContractKey(hash))
315 err := json.Unmarshal(bytes, &cp)
320 isExist := w.DB.Get(account.Key(cp.AccountID))
325 for _, out := range outsByScript[s] {
326 newOut := &accountOutput{
328 AccountID: cp.AccountID,
330 keyIndex: cp.KeyIndex,
333 result = append(result, newOut)
340 // upsertConfirmedAccountOutputs records the account data for confirmed utxos.
341 // If the account utxo already exists (because it's from a local tx), the
342 // block confirmation data will in the row will be updated.
343 func upsertConfirmedAccountOutputs(outs []*accountOutput, batch db.Batch) error {
346 for _, out := range outs {
348 OutputID: out.OutputID,
349 SourceID: out.sourceID,
350 AssetID: *out.AssetId,
352 SourcePos: out.sourcePos,
353 ControlProgram: out.ControlProgram,
354 ControlProgramIndex: out.keyIndex,
355 AccountID: out.AccountID,
356 Address: out.Address,
357 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.ContractKey(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 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 annotateTxsAsset(w, []*query.AnnotatedTx{annotatedTx})
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 annotateTxsAsset(w, []*query.AnnotatedTx{annotatedTx})
518 annotatedTxs = append(annotatedTxs, annotatedTx)
522 return annotatedTxs, nil
525 // GetAccountUTXOs return all account unspent outputs
526 func (w *Wallet) GetAccountUTXOs(id string) []account.UTXO {
527 var accountUTXOs []account.UTXO
529 accountUTXOIter := w.DB.IteratorPrefix([]byte(account.UTXOPreFix + id))
530 defer accountUTXOIter.Release()
531 for accountUTXOIter.Next() {
532 accountUTXO := account.UTXO{}
533 if err := json.Unmarshal(accountUTXOIter.Value(), &accountUTXO); err != nil {
534 hashKey := accountUTXOIter.Key()[len(account.UTXOPreFix):]
535 log.WithField("UTXO hash", string(hashKey)).Warn("get account UTXO")
537 accountUTXOs = append(accountUTXOs, accountUTXO)
544 // GetAccountBalances return all account balances
545 func (w *Wallet) GetAccountBalances(id string) ([]AccountBalance, error) {
546 return w.indexBalances(w.GetAccountUTXOs(""))
549 // AccountBalance account balance
550 type AccountBalance struct {
551 AccountID string `json:"account_id"`
552 Alias string `json:"account_alias"`
553 AssetAlias string `json:"asset_alias"`
554 AssetID string `json:"asset_id"`
555 Amount uint64 `json:"amount"`
556 AssetDefinition map[string]interface{} `json:"asset_definition"`
559 func (w *Wallet) indexBalances(accountUTXOs []account.UTXO) ([]AccountBalance, error) {
560 accBalance := make(map[string]map[string]uint64)
561 balances := make([]AccountBalance, 0)
563 for _, accountUTXO := range accountUTXOs {
564 assetID := accountUTXO.AssetID.String()
565 if _, ok := accBalance[accountUTXO.AccountID]; ok {
566 if _, ok := accBalance[accountUTXO.AccountID][assetID]; ok {
567 accBalance[accountUTXO.AccountID][assetID] += accountUTXO.Amount
569 accBalance[accountUTXO.AccountID][assetID] = accountUTXO.Amount
572 accBalance[accountUTXO.AccountID] = map[string]uint64{assetID: accountUTXO.Amount}
576 var sortedAccount []string
577 for k := range accBalance {
578 sortedAccount = append(sortedAccount, k)
580 sort.Strings(sortedAccount)
582 for _, id := range sortedAccount {
583 var sortedAsset []string
584 for k := range accBalance[id] {
585 sortedAsset = append(sortedAsset, k)
587 sort.Strings(sortedAsset)
589 for _, assetID := range sortedAsset {
590 alias := w.AccountMgr.GetAliasByID(id)
591 targetAsset, err := w.AssetReg.GetAsset(assetID)
596 assetAlias := *targetAsset.Alias
597 balances = append(balances, AccountBalance{
601 AssetAlias: assetAlias,
602 Amount: accBalance[id][assetID],
603 AssetDefinition: targetAsset.DefinitionMap,