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"
24 // ErrNotFoundTx means errors occurred in actions
25 ErrNotFoundTx = errors.New("not found transaction in the wallet db")
28 type rawOutput struct {
39 type accountOutput struct {
48 //TxPrefix is wallet database transactions prefix
50 //TxIndexPrefix is wallet database tx index prefix
51 TxIndexPrefix = "TID:"
54 func formatKey(blockHeight uint64, position uint32) string {
55 return fmt.Sprintf("%016x%08x", blockHeight, position)
58 func calcAnnotatedKey(formatKey string) []byte {
59 return []byte(TxPrefix + formatKey)
62 func calcDeleteKey(blockHeight uint64) []byte {
63 return []byte(fmt.Sprintf("%s%016x", TxPrefix, blockHeight))
66 func calcTxIndexKey(txID string) []byte {
67 return []byte(TxIndexPrefix + txID)
70 // deleteTransaction delete transactions when orphan block rollback
71 func (w *Wallet) deleteTransactions(batch db.Batch, height uint64) {
72 tmpTx := query.AnnotatedTx{}
74 txIter := w.DB.IteratorPrefix(calcDeleteKey(height))
75 defer txIter.Release()
78 if err := json.Unmarshal(txIter.Value(), &tmpTx); err == nil {
80 batch.Delete(calcTxIndexKey(tmpTx.ID.String()))
83 batch.Delete(txIter.Key())
87 // ReverseAccountUTXOs process the invalid blocks when orphan block rollback
88 func (w *Wallet) reverseAccountUTXOs(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
91 // unknow how many spent and retire outputs
92 reverseOuts := []*rawOutput{}
95 for txIndex, tx := range b.Transactions {
96 for _, inpID := range tx.Tx.InputIDs {
98 sp, err := tx.Spend(inpID)
103 resOut, ok := tx.Entries[*sp.SpentOutputId].(*bc.Output)
108 statusFail, _ := txStatus.GetStatus(txIndex)
109 if statusFail && *resOut.Source.Value.AssetId != *consensus.BTMAssetID {
114 OutputID: *sp.SpentOutputId,
115 AssetAmount: *resOut.Source.Value,
116 ControlProgram: resOut.ControlProgram.Code,
118 sourceID: *resOut.Source.Ref,
119 sourcePos: resOut.Source.Position,
121 reverseOuts = append(reverseOuts, out)
125 accOuts := loadAccountInfo(reverseOuts, w)
126 if err = upsertConfirmedAccountOutputs(accOuts, batch); err != nil {
127 log.WithField("err", err).Error("reversing account spent and retire outputs")
132 for _, tx := range b.Transactions {
133 for j := range tx.Outputs {
134 resOutID := tx.ResultIds[j]
135 resOut, ok := tx.Entries[*resOutID].(*bc.Output)
141 if segwit.IsP2WScript(resOut.ControlProgram.Code) {
142 // delete standard UTXOs
143 batch.Delete(account.StandardUTXOKey(*resOutID))
145 // delete contract UTXOs
146 batch.Delete(account.ContractUTXOKey(*resOutID))
152 // saveExternalAssetDefinition save external and local assets definition,
153 // when query ,query local first and if have no then query external
154 // details see getAliasDefinition
155 func saveExternalAssetDefinition(b *types.Block, walletDB db.DB) {
156 storeBatch := walletDB.NewBatch()
157 defer storeBatch.Write()
159 for _, tx := range b.Transactions {
160 for _, orig := range tx.Inputs {
161 if ii, ok := orig.TypedInput.(*types.IssuanceInput); ok {
162 if isValidJSON(ii.AssetDefinition) {
163 assetID := ii.AssetID()
164 if assetExist := walletDB.Get(asset.ExtAssetKey(&assetID)); assetExist == nil {
165 storeBatch.Set(asset.ExtAssetKey(&assetID), ii.AssetDefinition)
173 // Summary is the struct of transaction's input and output summary
174 type Summary struct {
175 Type string `json:"type"`
176 AssetID bc.AssetID `json:"asset_id,omitempty"`
177 AssetAlias string `json:"asset_alias,omitempty"`
178 Amount uint64 `json:"amount,omitempty"`
179 AccountID string `json:"account_id,omitempty"`
180 AccountAlias string `json:"account_alias,omitempty"`
181 Arbitrary chainjson.HexBytes `json:"arbitrary,omitempty"`
184 // TxSummary is the struct of transaction summary
185 type TxSummary struct {
186 ID bc.Hash `json:"tx_id"`
187 Timestamp uint64 `json:"block_time"`
188 Inputs []Summary `json:"inputs"`
189 Outputs []Summary `json:"outputs"`
192 // indexTransactions saves all annotated transactions to the database.
193 func (w *Wallet) indexTransactions(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) error {
194 annotatedTxs := w.filterAccountTxs(b, txStatus)
195 saveExternalAssetDefinition(b, w.DB)
196 annotateTxsAccount(annotatedTxs, w.DB)
198 for _, tx := range annotatedTxs {
199 rawTx, err := json.Marshal(tx)
201 log.WithField("err", err).Error("inserting annotated_txs to db")
205 batch.Set(calcAnnotatedKey(formatKey(b.Height, uint32(tx.Position))), rawTx)
206 batch.Set(calcTxIndexKey(tx.ID.String()), []byte(formatKey(b.Height, uint32(tx.Position))))
211 // buildAccountUTXOs process valid blocks to build account unspent outputs db
212 func (w *Wallet) buildAccountUTXOs(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
213 // get the spent UTXOs and delete the UTXOs from DB
214 prevoutDBKeys(batch, b, txStatus)
217 var outs []*rawOutput
218 for txIndex, tx := range b.Transactions {
219 for i, out := range tx.Outputs {
220 resOutID := tx.ResultIds[i]
221 resOut, ok := tx.Entries[*resOutID].(*bc.Output)
226 if statusFail, _ := txStatus.GetStatus(txIndex); statusFail && *resOut.Source.Value.AssetId != *consensus.BTMAssetID {
231 OutputID: *tx.OutputID(i),
232 AssetAmount: out.AssetAmount,
233 ControlProgram: out.ControlProgram,
235 outputIndex: uint32(i),
236 sourceID: *resOut.Source.Ref,
237 sourcePos: resOut.Source.Position,
240 // coinbase utxo valid height
242 out.ValidHeight = b.Height + consensus.CoinbasePendingBlockNumber
244 outs = append(outs, out)
247 accOuts := loadAccountInfo(outs, w)
249 if err := upsertConfirmedAccountOutputs(accOuts, batch); err != nil {
250 log.WithField("err", err).Error("building new account outputs")
254 func prevoutDBKeys(batch db.Batch, b *types.Block, txStatus *bc.TransactionStatus) {
255 for txIndex, tx := range b.Transactions {
256 for _, inpID := range tx.Tx.InputIDs {
257 sp, err := tx.Spend(inpID)
262 statusFail, _ := txStatus.GetStatus(txIndex)
263 if statusFail && *sp.WitnessDestination.Value.AssetId != *consensus.BTMAssetID {
267 resOut, ok := tx.Entries[*sp.SpentOutputId].(*bc.Output)
270 log.WithField("SpentOutputId", *sp.SpentOutputId).Info("the OutputId is retirement")
274 if segwit.IsP2WScript(resOut.ControlProgram.Code) {
275 // delete standard UTXOs
276 batch.Delete(account.StandardUTXOKey(*sp.SpentOutputId))
278 // delete contract UTXOs
279 batch.Delete(account.ContractUTXOKey(*sp.SpentOutputId))
286 // loadAccountInfo turns a set of output IDs into a set of
287 // outputs by adding account annotations. Outputs that can't be
288 // annotated are excluded from the result.
289 func loadAccountInfo(outs []*rawOutput, w *Wallet) []*accountOutput {
290 outsByScript := make(map[string][]*rawOutput, len(outs))
291 for _, out := range outs {
292 scriptStr := string(out.ControlProgram)
293 outsByScript[scriptStr] = append(outsByScript[scriptStr], out)
296 result := make([]*accountOutput, 0, len(outs))
297 cp := account.CtrlProgram{}
300 for s := range outsByScript {
301 // smart contract UTXO
302 if !segwit.IsP2WScript([]byte(s)) {
303 for _, out := range outsByScript[s] {
304 newOut := &accountOutput{
308 result = append(result, newOut)
314 sha3pool.Sum256(hash[:], []byte(s))
315 bytes := w.DB.Get(account.ContractKey(hash))
320 err := json.Unmarshal(bytes, &cp)
325 isExist := w.DB.Get(account.Key(cp.AccountID))
330 for _, out := range outsByScript[s] {
331 newOut := &accountOutput{
333 AccountID: cp.AccountID,
335 keyIndex: cp.KeyIndex,
338 result = append(result, newOut)
345 // upsertConfirmedAccountOutputs records the account data for confirmed utxos.
346 // If the account utxo already exists (because it's from a local tx), the
347 // block confirmation data will in the row will be updated.
348 func upsertConfirmedAccountOutputs(outs []*accountOutput, batch db.Batch) error {
351 for _, out := range outs {
353 OutputID: out.OutputID,
354 SourceID: out.sourceID,
355 AssetID: *out.AssetId,
357 SourcePos: out.sourcePos,
358 ControlProgram: out.ControlProgram,
359 ControlProgramIndex: out.keyIndex,
360 AccountID: out.AccountID,
361 Address: out.Address,
362 ValidHeight: out.ValidHeight,
366 data, err := json.Marshal(u)
368 return errors.Wrap(err, "failed marshal accountutxo")
371 if segwit.IsP2WScript(out.ControlProgram) {
373 batch.Set(account.StandardUTXOKey(out.OutputID), data)
376 batch.Set(account.ContractUTXOKey(out.OutputID), data)
383 // filterAccountTxs related and build the fully annotated transactions.
384 func (w *Wallet) filterAccountTxs(b *types.Block, txStatus *bc.TransactionStatus) []*query.AnnotatedTx {
385 annotatedTxs := make([]*query.AnnotatedTx, 0, len(b.Transactions))
388 for pos, tx := range b.Transactions {
389 statusFail, _ := txStatus.GetStatus(pos)
390 for _, v := range tx.Outputs {
392 sha3pool.Sum256(hash[:], v.ControlProgram)
393 if bytes := w.DB.Get(account.ContractKey(hash)); bytes != nil {
394 annotatedTxs = append(annotatedTxs, w.buildAnnotatedTransaction(tx, b, statusFail, pos))
395 continue transactionLoop
399 for _, v := range tx.Inputs {
400 outid, err := v.SpentOutputID()
404 if bytes := w.DB.Get(account.StandardUTXOKey(outid)); bytes != nil {
405 annotatedTxs = append(annotatedTxs, w.buildAnnotatedTransaction(tx, b, statusFail, pos))
406 continue transactionLoop
414 // GetTransaction search confirmed or unconfirmed transaction by txID
415 func (w *Wallet) GetTransaction(txID string) (*query.AnnotatedTx, error) {
416 annotatedTx, err := w.GetTransactionByTxID(txID)
417 if errors.Root(err) != ErrNotFoundTx {
421 annotatedTx, err = w.GetUnconfirmedTxByTxID(txID)
426 return annotatedTx, nil
429 // GetTransactionByTxID get transaction by txID
430 func (w *Wallet) GetTransactionByTxID(txID string) (*query.AnnotatedTx, error) {
431 formatKey := w.DB.Get(calcTxIndexKey(txID))
432 if formatKey == nil {
433 return nil, errors.WithData(ErrNotFoundTx, "not found tx=%s from blockchain", txID)
436 annotatedTx := &query.AnnotatedTx{}
437 txInfo := w.DB.Get(calcAnnotatedKey(string(formatKey)))
438 if err := json.Unmarshal(txInfo, annotatedTx); err != nil {
442 return annotatedTx, nil
445 // GetTransactionsSummary get transactions summary
446 func (w *Wallet) GetTransactionsSummary(transactions []*query.AnnotatedTx) []TxSummary {
449 for _, annotatedTx := range transactions {
450 tmpTxSummary := TxSummary{
451 Inputs: make([]Summary, len(annotatedTx.Inputs)),
452 Outputs: make([]Summary, len(annotatedTx.Outputs)),
454 Timestamp: annotatedTx.Timestamp,
457 for i, input := range annotatedTx.Inputs {
458 tmpTxSummary.Inputs[i].Type = input.Type
459 tmpTxSummary.Inputs[i].AccountID = input.AccountID
460 tmpTxSummary.Inputs[i].AccountAlias = input.AccountAlias
461 tmpTxSummary.Inputs[i].AssetID = input.AssetID
462 tmpTxSummary.Inputs[i].AssetAlias = input.AssetAlias
463 tmpTxSummary.Inputs[i].Amount = input.Amount
464 tmpTxSummary.Inputs[i].Arbitrary = input.Arbitrary
466 for j, output := range annotatedTx.Outputs {
467 tmpTxSummary.Outputs[j].Type = output.Type
468 tmpTxSummary.Outputs[j].AccountID = output.AccountID
469 tmpTxSummary.Outputs[j].AccountAlias = output.AccountAlias
470 tmpTxSummary.Outputs[j].AssetID = output.AssetID
471 tmpTxSummary.Outputs[j].AssetAlias = output.AssetAlias
472 tmpTxSummary.Outputs[j].Amount = output.Amount
475 Txs = append(Txs, tmpTxSummary)
481 func findTransactionsByAccount(annotatedTx *query.AnnotatedTx, accountID string) bool {
482 for _, input := range annotatedTx.Inputs {
483 if input.AccountID == accountID {
488 for _, output := range annotatedTx.Outputs {
489 if output.AccountID == accountID {
497 // GetTransactions get all walletDB transactions, and filter transactions by accountID optional
498 func (w *Wallet) GetTransactions(accountID string) ([]*query.AnnotatedTx, error) {
499 annotatedTxs := []*query.AnnotatedTx{}
500 annotatedAccTxs := []*query.AnnotatedTx{}
502 txIter := w.DB.IteratorPrefix([]byte(TxPrefix))
503 defer txIter.Release()
505 annotatedTx := &query.AnnotatedTx{}
506 if err := json.Unmarshal(txIter.Value(), &annotatedTx); err != nil {
510 annotateTxsAsset(w, []*query.AnnotatedTx{annotatedTx})
511 annotatedTxs = append(annotatedTxs, annotatedTx)
512 if findTransactionsByAccount(annotatedTx, accountID) {
513 annotatedAccTxs = append(annotatedAccTxs, annotatedTx)
518 return annotatedAccTxs, nil
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, error) {
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"`
554 AssetDefinition map[string]interface{} `json:"asset_definition"`
557 func (w *Wallet) indexBalances(accountUTXOs []account.UTXO) ([]AccountBalance, error) {
558 accBalance := make(map[string]map[string]uint64)
559 balances := make([]AccountBalance, 0)
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 targetAsset, err := w.AssetReg.GetAsset(assetID)
594 assetAlias := *targetAsset.Alias
595 balances = append(balances, AccountBalance{
599 AssetAlias: assetAlias,
600 Amount: accBalance[id][assetID],
601 AssetDefinition: targetAsset.DefinitionMap,