diff --git a/api/service/explorer/storage.go b/api/service/explorer/storage.go index a8fb9eea3e..b94b0ad8d1 100644 --- a/api/service/explorer/storage.go +++ b/api/service/explorer/storage.go @@ -8,6 +8,7 @@ import ( "github.com/ethereum/go-ethereum/rlp" "github.com/harmony-one/harmony/core/types" "github.com/harmony-one/harmony/internal/utils" + staking "github.com/harmony-one/harmony/staking/types" "github.com/syndtr/goleveldb/leveldb" "github.com/syndtr/goleveldb/leveldb/filter" "github.com/syndtr/goleveldb/leveldb/opt" @@ -83,25 +84,40 @@ func (storage *Storage) Dump(block *types.Block, height uint64) { // Store txs for _, tx := range block.Transactions() { explorerTransaction := GetTransaction(tx, block) - storage.UpdateAddress(batch, explorerTransaction, tx) + storage.UpdateTxAddress(batch, explorerTransaction, tx) + } + // Store staking txns + for _, tx := range block.StakingTransactions() { + explorerTransaction := GetStakingTransaction(tx, block) + storage.UpdateStakingTxAddress(batch, explorerTransaction, tx) } if err := storage.db.Write(batch, nil); err != nil { utils.Logger().Warn().Err(err).Msg("cannot write batch") } } -// UpdateAddress ... -func (storage *Storage) UpdateAddress(batch *leveldb.Batch, explorerTransaction *Transaction, tx *types.Transaction) { +// UpdateTxAddress ... +func (storage *Storage) UpdateTxAddress(batch *leveldb.Batch, explorerTransaction *Transaction, tx *types.Transaction) { + explorerTransaction.Type = Received + if explorerTransaction.To != "" { + storage.UpdateTxAddressStorage(batch, explorerTransaction.To, explorerTransaction, tx) + } + explorerTransaction.Type = Sent + storage.UpdateTxAddressStorage(batch, explorerTransaction.From, explorerTransaction, tx) +} + +// UpdateStakingTxAddress ... +func (storage *Storage) UpdateStakingTxAddress(batch *leveldb.Batch, explorerTransaction *StakingTransaction, tx *staking.StakingTransaction) { explorerTransaction.Type = Received if explorerTransaction.To != "" { - storage.UpdateAddressStorage(batch, explorerTransaction.To, explorerTransaction, tx) + storage.UpdateStakingTxAddressStorage(batch, explorerTransaction.To, explorerTransaction, tx) } explorerTransaction.Type = Sent - storage.UpdateAddressStorage(batch, explorerTransaction.From, explorerTransaction, tx) + storage.UpdateStakingTxAddressStorage(batch, explorerTransaction.From, explorerTransaction, tx) } -// UpdateAddressStorage updates specific addr Address. -func (storage *Storage) UpdateAddressStorage(batch *leveldb.Batch, addr string, explorerTransaction *Transaction, tx *types.Transaction) { +// UpdateTxAddressStorage updates specific addr tx Address. +func (storage *Storage) UpdateTxAddressStorage(batch *leveldb.Batch, addr string, explorerTransaction *Transaction, tx *types.Transaction) { var address Address key := GetAddressKey(addr) if data, err := storage.db.Get([]byte(key), nil); err == nil { @@ -119,6 +135,25 @@ func (storage *Storage) UpdateAddressStorage(batch *leveldb.Batch, addr string, } } +// UpdateStakingTxAddressStorage updates specific addr staking tx Address. +func (storage *Storage) UpdateStakingTxAddressStorage(batch *leveldb.Batch, addr string, explorerTransaction *StakingTransaction, tx *staking.StakingTransaction) { + var address Address + key := GetAddressKey(addr) + if data, err := storage.db.Get([]byte(key), nil); err == nil { + if err = rlp.DecodeBytes(data, &address); err != nil { + utils.Logger().Error().Err(err).Msg("Failed due to error") + } + } + address.ID = addr + address.StakingTXs = append(address.StakingTXs, explorerTransaction) + encoded, err := rlp.EncodeToBytes(address) + if err == nil { + batch.Put([]byte(key), encoded) + } else { + utils.Logger().Error().Err(err).Msg("cannot encode address") + } +} + // GetAddresses returns size of addresses from address with prefix. func (storage *Storage) GetAddresses(size int, prefix string) ([]string, error) { db := storage.GetDB() diff --git a/api/service/explorer/structs.go b/api/service/explorer/structs.go index 56c5cd38a0..480c3a105f 100644 --- a/api/service/explorer/structs.go +++ b/api/service/explorer/structs.go @@ -5,9 +5,11 @@ import ( "math/big" "strconv" + core2 "github.com/harmony-one/harmony/core" "github.com/harmony-one/harmony/core/types" "github.com/harmony-one/harmony/internal/common" "github.com/harmony-one/harmony/internal/utils" + staking "github.com/harmony-one/harmony/staking/types" ) /* @@ -27,9 +29,10 @@ type Data struct { // Address ... type Address struct { - ID string `json:"id"` - Balance *big.Int `json:"balance"` - TXs []*Transaction `json:"txs"` + ID string `json:"id"` + Balance *big.Int `json:"balance"` + TXs []*Transaction `json:"txs"` + StakingTXs []*StakingTransaction `json:"staking_txs"` } // Transaction ... @@ -79,3 +82,51 @@ func GetTransaction(tx *types.Transaction, addressBlock *types.Block) *Transacti Type: "", } } + +// StakingTransaction ... +type StakingTransaction struct { + ID string `json:"id"` + Timestamp string `json:"timestamp"` + From string `json:"from"` + To string `json:"to"` + Value *big.Int `json:"value"` + Bytes string `json:"bytes"` + Data string `json:"data"` + GasFee *big.Int `json:"gasFee"` + FromShard uint32 `json:"fromShard"` + ToShard uint32 `json:"toShard"` + Type string `json:"type"` +} + +// GetStakingTransaction ... +func GetStakingTransaction(tx *staking.StakingTransaction, addressBlock *types.Block) *StakingTransaction { + msg, err := core2.StakingToMessage(tx, addressBlock.Header().Number()) + if err != nil { + utils.Logger().Error().Err(err).Msg("Error when parsing tx into message") + } + gasFee := big.NewInt(0) + gasFee = gasFee.Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) + to := "" + if msg.To() != nil { + if to, err = common.AddressToBech32(*msg.To()); err != nil { + return nil + } + } + from := "" + if from, err = common.AddressToBech32(msg.From()); err != nil { + return nil + } + return &StakingTransaction{ + ID: tx.Hash().Hex(), + Timestamp: strconv.Itoa(int(addressBlock.Time().Int64() * 1000)), + From: from, + To: to, + Value: msg.Value(), + Bytes: strconv.Itoa(int(tx.Size())), + Data: hex.EncodeToString(msg.Data()), + GasFee: gasFee, + FromShard: tx.ShardID(), + ToShard: 0, + Type: string(msg.Type()), + } +} diff --git a/block/factory/factory.go b/block/factory/factory.go index d693b0921b..896a4fc280 100644 --- a/block/factory/factory.go +++ b/block/factory/factory.go @@ -30,7 +30,7 @@ func NewFactory(chainConfig *params.ChainConfig) Factory { func (f *factory) NewHeader(epoch *big.Int) *block.Header { var impl blockif.Header switch { - case f.chainConfig.IsPreStaking(epoch): + case f.chainConfig.IsPreStaking(epoch) || f.chainConfig.IsStaking(epoch): impl = v3.NewHeader() case f.chainConfig.IsCrossLink(epoch): impl = v2.NewHeader() diff --git a/core/rawdb/accessors_indexes.go b/core/rawdb/accessors_indexes.go index da00a06118..91b72ee858 100644 --- a/core/rawdb/accessors_indexes.go +++ b/core/rawdb/accessors_indexes.go @@ -45,7 +45,9 @@ func ReadTxLookupEntry(db DatabaseReader, hash common.Hash) (common.Hash, uint64 // WriteTxLookupEntries stores a positional metadata for every transaction from // a block, enabling hash based transaction and receipt lookups. func WriteTxLookupEntries(db DatabaseWriter, block *types.Block) { - for i, tx := range block.Transactions() { + // TODO: remove this hack with Tx and StakingTx structure unitification later + f := func(i int, tx *types.Transaction, stx *staking.StakingTransaction) { + isStaking := (stx != nil && tx == nil) entry := TxLookupEntry{ BlockHash: block.Hash(), BlockIndex: block.NumberU64(), @@ -53,25 +55,24 @@ func WriteTxLookupEntries(db DatabaseWriter, block *types.Block) { } data, err := rlp.EncodeToBytes(entry) if err != nil { - utils.Logger().Error().Err(err).Msg("Failed to encode transaction lookup entry") + utils.Logger().Error().Err(err).Bool("isStaking", isStaking).Msg("Failed to encode transaction lookup entry") + } + + var putErr error + if isStaking { + putErr = db.Put(txLookupKey(stx.Hash()), data) + } else { + putErr = db.Put(txLookupKey(tx.Hash()), data) } - if err := db.Put(txLookupKey(tx.Hash()), data); err != nil { - utils.Logger().Error().Err(err).Msg("Failed to store transaction lookup entry") + if putErr != nil { + utils.Logger().Error().Err(err).Bool("isStaking", isStaking).Msg("Failed to store transaction lookup entry") } } + for i, tx := range block.Transactions() { + f(i, tx, nil) + } for i, tx := range block.StakingTransactions() { - entry := TxLookupEntry{ - BlockHash: block.Hash(), - BlockIndex: block.NumberU64(), - Index: uint64(i), - } - data, err := rlp.EncodeToBytes(entry) - if err != nil { - utils.Logger().Error().Err(err).Msg("Failed to encode staking transaction lookup entry") - } - if err := db.Put(txLookupKey(tx.Hash()), data); err != nil { - utils.Logger().Error().Err(err).Msg("Failed to store staking transaction lookup entry") - } + f(i, nil, tx) } } @@ -110,6 +111,8 @@ func ReadTransaction(db DatabaseReader, hash common.Hash) (*types.Transaction, c // ReadStakingTransaction retrieves a specific staking transaction from the database, along with // its added positional metadata. +// TODO remove this duplicate function that is inevitable at the moment until the optimization on staking txn with +// unification of txn vs staking txn data structure. func ReadStakingTransaction(db DatabaseReader, hash common.Hash) (*staking.StakingTransaction, common.Hash, uint64, uint64) { blockHash, blockNumber, txIndex := ReadTxLookupEntry(db, hash) if blockHash == (common.Hash{}) { @@ -143,6 +146,7 @@ func ReadReceipt(db DatabaseReader, hash common.Hash) (*types.Receipt, common.Ha if blockHash == (common.Hash{}) { return nil, common.Hash{}, 0, 0 } + receipts := ReadReceipts(db, blockHash, blockNumber) if len(receipts) <= int(receiptIndex) { utils.Logger().Error(). diff --git a/core/rawdb/accessors_indexes_test.go b/core/rawdb/accessors_indexes_test.go index 2b0abcba6e..777edf314e 100644 --- a/core/rawdb/accessors_indexes_test.go +++ b/core/rawdb/accessors_indexes_test.go @@ -47,7 +47,17 @@ func TestLookupStorage(t *testing.T) { tx3 := types.NewTransaction(3, common.BytesToAddress([]byte{0x33}), 0, big.NewInt(333), 3333, big.NewInt(33333), []byte{0x33, 0x33, 0x33}) txs := []*types.Transaction{tx1, tx2, tx3} - block := types.NewBlock(blockfactory.NewTestHeader().With().Number(big.NewInt(314)).Header(), txs, types.Receipts{&types.Receipt{}, &types.Receipt{}, &types.Receipt{}}, nil, nil, nil) + stx := sampleCreateValidatorStakingTxn() + stxs := []*staking.StakingTransaction{stx} + + receipts := types.Receipts{ + &types.Receipt{}, + &types.Receipt{}, + &types.Receipt{}, + &types.Receipt{}, + } + + block := types.NewBlock(blockfactory.NewTestHeader().With().Number(big.NewInt(314)).Header(), txs, receipts, nil, nil, stxs) // Check that no transactions entries are in a pristine database for i, tx := range txs { @@ -55,6 +65,11 @@ func TestLookupStorage(t *testing.T) { t.Fatalf("tx #%d [%x]: non existent transaction returned: %v", i, tx.Hash(), txn) } } + for i, stx := range stxs { + if stxn, _, _, _ := ReadStakingTransaction(db, stx.Hash()); stxn != nil { + t.Fatalf("stx #%d [%x]: non existent staking transaction returned: %v", i, stxn.Hash(), stxn) + } + } // Insert all the transactions into the database, and verify contents WriteBlock(db, block) WriteTxLookupEntries(db, block) @@ -71,6 +86,18 @@ func TestLookupStorage(t *testing.T) { } } } + for i, stx := range stxs { + if txn, hash, number, index := ReadStakingTransaction(db, stx.Hash()); txn == nil { + t.Fatalf("tx #%d [%x]: staking transaction not found", i, stx.Hash()) + } else { + if hash != block.Hash() || number != block.NumberU64() || index != uint64(i) { + t.Fatalf("stx #%d [%x]: positional metadata mismatch: have %x/%d/%d, want %x/%v/%v", i, stx.Hash(), hash, number, index, block.Hash(), block.NumberU64(), i) + } + if stx.Hash() != txn.Hash() { + t.Fatalf("stx #%d [%x]: staking transaction mismatch: have %v, want %v", i, stx.Hash(), txn, stx) + } + } + } // Delete the transactions and check purge for i, tx := range txs { DeleteTxLookupEntry(db, tx.Hash()) @@ -78,12 +105,37 @@ func TestLookupStorage(t *testing.T) { t.Fatalf("tx #%d [%x]: deleted transaction returned: %v", i, tx.Hash(), txn) } } + for i, tx := range txs { + DeleteTxLookupEntry(db, tx.Hash()) + if stxn, _, _, _ := ReadStakingTransaction(db, tx.Hash()); stxn != nil { + t.Fatalf("stx #%d [%x]: deleted staking transaction returned: %v", i, stx.Hash(), stxn) + } + } } // Test that staking tx hash does not find a plain tx hash (and visa versa) within the same block func TestMixedLookupStorage(t *testing.T) { db := ethdb.NewMemDatabase() tx := types.NewTransaction(1, common.BytesToAddress([]byte{0x11}), 0, big.NewInt(111), 1111, big.NewInt(11111), []byte{0x11, 0x11, 0x11}) + stx := sampleCreateValidatorStakingTxn() + + txs := []*types.Transaction{tx} + stxs := []*staking.StakingTransaction{stx} + header := blockfactory.NewTestHeader().With().Number(big.NewInt(314)).Header() + block := types.NewBlock(header, txs, types.Receipts{&types.Receipt{}, &types.Receipt{}}, nil, nil, stxs) + + WriteBlock(db, block) + WriteTxLookupEntries(db, block) + + if recTx, _, _, _ := ReadStakingTransaction(db, tx.Hash()); recTx != nil { + t.Fatal("got staking transactions with plain tx hash") + } + if recTx, _, _, _ := ReadTransaction(db, stx.Hash()); recTx != nil { + t.Fatal("got plain transactions with staking tx hash") + } +} + +func sampleCreateValidatorStakingTxn() *staking.StakingTransaction { key, _ := crypto.GenerateKey() stakePayloadMaker := func() (staking.Directive, interface{}) { p := &bls.PublicKey{} @@ -123,17 +175,5 @@ func TestMixedLookupStorage(t *testing.T) { } } stx, _ := staking.NewStakingTransaction(0, 1e10, big.NewInt(10000), stakePayloadMaker) - txs := []*types.Transaction{tx} - stxs := []*staking.StakingTransaction{stx} - header := blockfactory.NewTestHeader().With().Number(big.NewInt(314)).Header() - block := types.NewBlock(header, txs, types.Receipts{&types.Receipt{}, &types.Receipt{}}, nil, nil, stxs) - WriteBlock(db, block) - WriteTxLookupEntries(db, block) - - if recTx, _, _, _ := ReadStakingTransaction(db, tx.Hash()); recTx != nil { - t.Fatal("got staking transactions with plain tx hash") - } - if recTx, _, _, _ := ReadTransaction(db, stx.Hash()); recTx != nil { - t.Fatal("got plain transactions with staking tx hash") - } + return stx } diff --git a/hmy/api_backend.go b/hmy/api_backend.go index b6f5075bfa..5f4988976f 100644 --- a/hmy/api_backend.go +++ b/hmy/api_backend.go @@ -239,6 +239,12 @@ func (b *APIBackend) GetTransactionsHistory(address, txType, order string) ([]co return hashes, err } +// GetStakingTransactionsHistory returns list of staking transactions hashes of address. +func (b *APIBackend) GetStakingTransactionsHistory(address, txType, order string) ([]common.Hash, error) { + hashes, err := b.hmy.nodeAPI.GetStakingTransactionsHistory(address, txType, order) + return hashes, err +} + // NetVersion returns net version func (b *APIBackend) NetVersion() uint64 { return b.hmy.NetVersion() @@ -281,18 +287,26 @@ func (b *APIBackend) GetValidators(epoch *big.Int) (*shard.Committee, error) { } // ResendCx retrieve blockHash from txID and add blockHash to CxPool for resending +// Note that cross shard txn is only for regular txns, not for staking txns, so the input txn hash +// is expected to be regular txn hash func (b *APIBackend) ResendCx(ctx context.Context, txID common.Hash) (uint64, bool) { blockHash, blockNum, index := b.hmy.BlockChain().ReadTxLookupEntry(txID) + if blockHash == (common.Hash{}) { + return 0, false + } + blk := b.hmy.BlockChain().GetBlockByHash(blockHash) if blk == nil { return 0, false } + txs := blk.Transactions() // a valid index is from 0 to len-1 if int(index) > len(txs)-1 { return 0, false } tx := txs[int(index)] + // check whether it is a valid cross shard tx if tx.ShardID() == tx.ToShardID() || blk.Header().ShardID() != tx.ShardID() { return 0, false diff --git a/hmy/backend.go b/hmy/backend.go index d6e377059d..5141fed179 100644 --- a/hmy/backend.go +++ b/hmy/backend.go @@ -50,6 +50,7 @@ type NodeAPI interface { GetBalanceOfAddress(address common.Address) (*big.Int, error) GetNonceOfAddress(address common.Address) uint64 GetTransactionsHistory(address, txType, order string) ([]common.Hash, error) + GetStakingTransactionsHistory(address, txType, order string) ([]common.Hash, error) IsCurrentlyLeader() bool ErroredStakingTransactionSink() []staking.RPCTransactionError ErroredTransactionSink() []types.RPCTransactionError diff --git a/internal/hmyapi/apiv1/backend.go b/internal/hmyapi/apiv1/backend.go index 76a753d415..8f2eab8a90 100644 --- a/internal/hmyapi/apiv1/backend.go +++ b/internal/hmyapi/apiv1/backend.go @@ -68,6 +68,8 @@ type Backend interface { GetShardID() uint32 // Get transactions history for an address GetTransactionsHistory(address, txType, order string) ([]common.Hash, error) + // Get staking transactions history for an address + GetStakingTransactionsHistory(address, txType, order string) ([]common.Hash, error) // retrieve the blockHash using txID and add blockHash to CxPool for resending ResendCx(ctx context.Context, txID common.Hash) (uint64, bool) IsLeader() bool diff --git a/internal/hmyapi/apiv1/transactionpool.go b/internal/hmyapi/apiv1/transactionpool.go index 6a3c019a49..728dbae02d 100644 --- a/internal/hmyapi/apiv1/transactionpool.go +++ b/internal/hmyapi/apiv1/transactionpool.go @@ -61,7 +61,7 @@ func (s *PublicTransactionPoolAPI) GetTransactionsHistory(ctx context.Context, a if err != nil { return nil, err } - result = ReturnWithPagination(hashes, args) + result = ReturnWithPagination(hashes, args.PageIndex, args.PageSize) if !args.FullTx { return map[string]interface{}{"transactions": result}, nil } diff --git a/internal/hmyapi/apiv1/types.go b/internal/hmyapi/apiv1/types.go index 3df5bb179c..a91c1348d9 100644 --- a/internal/hmyapi/apiv1/types.go +++ b/internal/hmyapi/apiv1/types.go @@ -203,7 +203,7 @@ func newRPCTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber return result } -// newRPCStakingTransaction returns a transaction that will serialize to the RPC +// newRPCStakingTransaction returns a staking transaction that will serialize to the RPC // representation, with the given location metadata set (if available). func newRPCStakingTransaction(tx *types2.StakingTransaction, blockHash common.Hash, blockNumber uint64, timestamp uint64, index uint64) *RPCStakingTransaction { from, err := tx.SenderAddress() @@ -350,7 +350,7 @@ func newRPCPendingTransaction(tx *types.Transaction) *RPCTransaction { return newRPCTransaction(tx, common.Hash{}, 0, 0, 0) } -// newRPCPendingStakingTransaction returns a pending transaction that will serialize to the RPC representation +// newRPCPendingStakingTransaction returns a pending staking transaction that will serialize to the RPC representation func newRPCPendingStakingTransaction(tx *types2.StakingTransaction) *RPCStakingTransaction { return newRPCStakingTransaction(tx, common.Hash{}, 0, 0, 0) } @@ -474,7 +474,7 @@ func newRPCTransactionFromBlockIndex(b *types.Block, index uint64) *RPCTransacti return newRPCTransaction(txs[index], b.Hash(), b.NumberU64(), b.Time().Uint64(), index) } -// newRPCStakingTransactionFromBlockHash returns a transaction that will serialize to the RPC representation. +// newRPCStakingTransactionFromBlockHash returns a staking transaction that will serialize to the RPC representation. func newRPCStakingTransactionFromBlockHash(b *types.Block, hash common.Hash) *RPCStakingTransaction { for idx, tx := range b.StakingTransactions() { if tx.Hash() == hash { @@ -484,7 +484,7 @@ func newRPCStakingTransactionFromBlockHash(b *types.Block, hash common.Hash) *RP return nil } -// newRPCStakingTransactionFromBlockIndex returns a transaction that will serialize to the RPC representation. +// newRPCStakingTransactionFromBlockIndex returns a staking transaction that will serialize to the RPC representation. func newRPCStakingTransactionFromBlockIndex(b *types.Block, index uint64) *RPCStakingTransaction { txs := b.StakingTransactions() if index >= uint64(len(txs)) { diff --git a/internal/hmyapi/apiv1/util.go b/internal/hmyapi/apiv1/util.go index 1bb36ae6cf..45f23dc806 100644 --- a/internal/hmyapi/apiv1/util.go +++ b/internal/hmyapi/apiv1/util.go @@ -17,19 +17,18 @@ const ( ) // ReturnWithPagination returns result with pagination (offset, page in TxHistoryArgs). -func ReturnWithPagination(hashes []common.Hash, args TxHistoryArgs) []common.Hash { - pageSize := defaultPageSize - pageIndex := args.PageIndex - if args.PageSize > 0 { - pageSize = args.PageSize +func ReturnWithPagination(hashes []common.Hash, pageIndex uint32, pageSize uint32) []common.Hash { + size := defaultPageSize + if pageSize > 0 { + size = pageSize } - if uint64(pageSize)*uint64(pageIndex) >= uint64(len(hashes)) { + if uint64(size)*uint64(pageIndex) >= uint64(len(hashes)) { return make([]common.Hash, 0) } - if uint64(pageSize)*uint64(pageIndex)+uint64(pageSize) > uint64(len(hashes)) { - return hashes[pageSize*pageIndex:] + if uint64(size)*uint64(pageIndex)+uint64(size) > uint64(len(hashes)) { + return hashes[size*pageIndex:] } - return hashes[pageSize*pageIndex : pageSize*pageIndex+pageSize] + return hashes[size*pageIndex : size*pageIndex+size] } // SubmitTransaction is a helper function that submits tx to txPool and logs a message. diff --git a/internal/hmyapi/apiv2/backend.go b/internal/hmyapi/apiv2/backend.go index 2c2e1298f0..ac6ffd00af 100644 --- a/internal/hmyapi/apiv2/backend.go +++ b/internal/hmyapi/apiv2/backend.go @@ -68,6 +68,8 @@ type Backend interface { GetShardID() uint32 // Get transactions history for an address GetTransactionsHistory(address, txType, order string) ([]common.Hash, error) + // Get staking transactions history for an address + GetStakingTransactionsHistory(address, txType, order string) ([]common.Hash, error) // retrieve the blockHash using txID and add blockHash to CxPool for resending ResendCx(ctx context.Context, txID common.Hash) (uint64, bool) IsLeader() bool diff --git a/internal/hmyapi/apiv2/transactionpool.go b/internal/hmyapi/apiv2/transactionpool.go index ccfb7474c6..4d2815bdfe 100644 --- a/internal/hmyapi/apiv2/transactionpool.go +++ b/internal/hmyapi/apiv2/transactionpool.go @@ -61,14 +61,16 @@ func (s *PublicTransactionPoolAPI) GetTransactionsHistory(ctx context.Context, a if err != nil { return nil, err } - result = ReturnWithPagination(hashes, args) + result = ReturnWithPagination(hashes, args.PageIndex, args.PageSize) if !args.FullTx { return map[string]interface{}{"transactions": result}, nil } txs := []*RPCTransaction{} for _, hash := range result { tx := s.GetTransactionByHash(ctx, hash) - txs = append(txs, tx) + if tx != nil { + txs = append(txs, tx) + } } return map[string]interface{}{"transactions": txs}, nil } @@ -105,7 +107,7 @@ func (s *PublicTransactionPoolAPI) GetTransactionByBlockHashAndIndex(ctx context return nil } -// GetTransactionByHash returns the transaction for the given hash +// GetTransactionByHash returns the plain transaction for the given hash func (s *PublicTransactionPoolAPI) GetTransactionByHash(ctx context.Context, hash common.Hash) *RPCTransaction { // Try to return an already finalized transaction tx, blockHash, blockNumber, index := rawdb.ReadTransaction(s.b.ChainDb(), hash) @@ -120,22 +122,55 @@ func (s *PublicTransactionPoolAPI) GetTransactionByHash(ctx context.Context, has return nil } -// GetStakingTransactionByHash returns the transaction for the given hash -func (s *PublicTransactionPoolAPI) GetStakingTransactionByHash(ctx context.Context, hash common.Hash) *RPCStakingTransaction { - // Try to return an already finalized transaction - stx, blockHash, blockNumber, index := rawdb.ReadStakingTransaction(s.b.ChainDb(), hash) - block, _ := s.b.GetBlock(ctx, blockHash) - if block == nil { - return nil +// GetStakingTransactionsHistory returns the list of transactions hashes that involve a particular address. +func (s *PublicTransactionPoolAPI) GetStakingTransactionsHistory(ctx context.Context, args TxHistoryArgs) (map[string]interface{}, error) { + address := args.Address + result := []common.Hash{} + var err error + if strings.HasPrefix(args.Address, "one1") { + address = args.Address + } else { + addr := internal_common.ParseAddr(args.Address) + address, err = internal_common.AddressToBech32(addr) + if err != nil { + return nil, err + } } - if stx != nil { - return newRPCStakingTransaction(stx, blockHash, blockNumber, block.Time().Uint64(), index) + hashes, err := s.b.GetStakingTransactionsHistory(address, args.TxType, args.Order) + if err != nil { + return nil, err } - // Transaction unknown, return as such - return nil + result = ReturnWithPagination(hashes, args.PageIndex, args.PageSize) + if !args.FullTx { + return map[string]interface{}{"staking_transactions": result}, nil + } + txs := []*RPCStakingTransaction{} + for _, hash := range result { + tx := s.GetStakingTransactionByHash(ctx, hash) + if tx != nil { + txs = append(txs, tx) + } + } + return map[string]interface{}{"staking_transactions": txs}, nil +} + +// GetBlockStakingTransactionCountByNumber returns the number of staking transactions in the block with the given block number. +func (s *PublicTransactionPoolAPI) GetBlockStakingTransactionCountByNumber(ctx context.Context, blockNr uint64) int { + if block, _ := s.b.BlockByNumber(ctx, rpc.BlockNumber(blockNr)); block != nil { + return len(block.StakingTransactions()) + } + return 0 } -// GetStakingTransactionByBlockNumberAndIndex returns the transaction for the given block number and index. +// GetBlockStakingTransactionCountByHash returns the number of staking transactions in the block with the given hash. +func (s *PublicTransactionPoolAPI) GetBlockStakingTransactionCountByHash(ctx context.Context, blockHash common.Hash) int { + if block, _ := s.b.GetBlock(ctx, blockHash); block != nil { + return len(block.StakingTransactions()) + } + return 0 +} + +// GetStakingTransactionByBlockNumberAndIndex returns the staking transaction for the given block number and index. func (s *PublicTransactionPoolAPI) GetStakingTransactionByBlockNumberAndIndex(ctx context.Context, blockNr uint64, index uint64) *RPCStakingTransaction { if block, _ := s.b.BlockByNumber(ctx, rpc.BlockNumber(blockNr)); block != nil { return newRPCStakingTransactionFromBlockIndex(block, index) @@ -151,7 +186,24 @@ func (s *PublicTransactionPoolAPI) GetStakingTransactionByBlockHashAndIndex(ctx return nil } -// GetTransactionCount returns the number of transactions the given address has sent for the given block number +// GetStakingTransactionByHash returns the staking transaction for the given hash +func (s *PublicTransactionPoolAPI) GetStakingTransactionByHash(ctx context.Context, hash common.Hash) *RPCStakingTransaction { + // Try to return an already finalized transaction + stx, blockHash, blockNumber, index := rawdb.ReadStakingTransaction(s.b.ChainDb(), hash) + block, _ := s.b.GetBlock(ctx, blockHash) + if block == nil { + return nil + } + if stx != nil { + return newRPCStakingTransaction(stx, blockHash, blockNumber, block.Time().Uint64(), index) + } + // Transaction unknown, return as such + return nil +} + +// GetTransactionCount returns the number of transactions the given address has sent from genesis to the input block number +// NOTE: unlike other txn apis where staking vs. regular txns are separate, +// the transaction count here includes the count of both regular and staking txns func (s *PublicTransactionPoolAPI) GetTransactionCount(ctx context.Context, addr string, blockNr uint64) (uint64, error) { address := internal_common.ParseAddr(addr) // Ask transaction pool for the nonce which includes pending transactions diff --git a/internal/hmyapi/apiv2/types.go b/internal/hmyapi/apiv2/types.go index 4be822dc35..41f6e6bdcd 100644 --- a/internal/hmyapi/apiv2/types.go +++ b/internal/hmyapi/apiv2/types.go @@ -204,7 +204,7 @@ func newRPCTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber return result } -// newRPCStakingTransaction returns a transaction that will serialize to the RPC +// newRPCStakingTransaction returns a staking transaction that will serialize to the RPC // representation, with the given location metadata set (if available). func newRPCStakingTransaction(tx *types2.StakingTransaction, blockHash common.Hash, blockNumber uint64, timestamp uint64, index uint64) *RPCStakingTransaction { from, err := tx.SenderAddress() @@ -475,7 +475,7 @@ func newRPCTransactionFromBlockIndex(b *types.Block, index uint64) *RPCTransacti return newRPCTransaction(txs[index], b.Hash(), b.NumberU64(), b.Time().Uint64(), index) } -// newRPCStakingTransactionFromBlockHash returns a transaction that will serialize to the RPC representation. +// newRPCStakingTransactionFromBlockHash returns a staking transaction that will serialize to the RPC representation. func newRPCStakingTransactionFromBlockHash(b *types.Block, hash common.Hash) *RPCStakingTransaction { for idx, tx := range b.StakingTransactions() { if tx.Hash() == hash { @@ -485,7 +485,7 @@ func newRPCStakingTransactionFromBlockHash(b *types.Block, hash common.Hash) *RP return nil } -// newRPCStakingTransactionFromBlockIndex returns a transaction that will serialize to the RPC representation. +// newRPCStakingTransactionFromBlockIndex returns a staking transaction that will serialize to the RPC representation. func newRPCStakingTransactionFromBlockIndex(b *types.Block, index uint64) *RPCStakingTransaction { txs := b.StakingTransactions() if index >= uint64(len(txs)) { diff --git a/internal/hmyapi/apiv2/util.go b/internal/hmyapi/apiv2/util.go index c378315c85..07762b1598 100644 --- a/internal/hmyapi/apiv2/util.go +++ b/internal/hmyapi/apiv2/util.go @@ -17,19 +17,18 @@ const ( ) // ReturnWithPagination returns result with pagination (offset, page in TxHistoryArgs). -func ReturnWithPagination(hashes []common.Hash, args TxHistoryArgs) []common.Hash { - pageSize := defaultPageSize - pageIndex := args.PageIndex - if args.PageSize > 0 { - pageSize = args.PageSize +func ReturnWithPagination(hashes []common.Hash, pageIndex uint32, pageSize uint32) []common.Hash { + size := defaultPageSize + if pageSize > 0 { + size = pageSize } - if uint64(pageSize)*uint64(pageIndex) >= uint64(len(hashes)) { + if uint64(size)*uint64(pageIndex) >= uint64(len(hashes)) { return make([]common.Hash, 0) } - if uint64(pageSize)*uint64(pageIndex)+uint64(pageSize) > uint64(len(hashes)) { - return hashes[pageSize*pageIndex:] + if uint64(size)*uint64(pageIndex)+uint64(size) > uint64(len(hashes)) { + return hashes[size*pageIndex:] } - return hashes[pageSize*pageIndex : pageSize*pageIndex+pageSize] + return hashes[size*pageIndex : size*pageIndex+size] } // SubmitTransaction is a helper function that submits tx to txPool and logs a message. diff --git a/internal/hmyapi/backend.go b/internal/hmyapi/backend.go index 99d612ed07..4cadeb7c7b 100644 --- a/internal/hmyapi/backend.go +++ b/internal/hmyapi/backend.go @@ -70,6 +70,8 @@ type Backend interface { GetShardID() uint32 // Get transactions history for an address GetTransactionsHistory(address, txType, order string) ([]common.Hash, error) + // Get staking transactions history for an address + GetStakingTransactionsHistory(address, txType, order string) ([]common.Hash, error) // retrieve the blockHash using txID and add blockHash to CxPool for resending ResendCx(ctx context.Context, txID common.Hash) (uint64, bool) IsLeader() bool diff --git a/node/node_explorer.go b/node/node_explorer.go index c54eae19d2..458bd8b7de 100644 --- a/node/node_explorer.go +++ b/node/node_explorer.go @@ -153,6 +153,7 @@ func (node *Node) GetTransactionsHistory(address, txType, order string) ([]commo key := explorer.GetAddressKey(address) bytes, err := explorer.GetStorageInstance(node.SelfPeer.IP, node.SelfPeer.Port, false).GetDB().Get([]byte(key), nil) if err != nil { + utils.Logger().Error().Err(err).Msg("[Explorer] Cannot get storage db instance") return make([]common.Hash, 0), nil } if err = rlp.DecodeBytes(bytes, &addressData); err != nil { @@ -177,3 +178,35 @@ func (node *Node) GetTransactionsHistory(address, txType, order string) ([]commo } return hashes, nil } + +// GetStakingTransactionsHistory returns list of staking transactions hashes of address. +func (node *Node) GetStakingTransactionsHistory(address, txType, order string) ([]common.Hash, error) { + addressData := &explorer.Address{} + key := explorer.GetAddressKey(address) + bytes, err := explorer.GetStorageInstance(node.SelfPeer.IP, node.SelfPeer.Port, false).GetDB().Get([]byte(key), nil) + if err != nil { + utils.Logger().Error().Err(err).Msg("[Explorer] Cannot get storage db instance") + return make([]common.Hash, 0), nil + } + if err = rlp.DecodeBytes(bytes, &addressData); err != nil { + utils.Logger().Error().Err(err).Msg("[Explorer] Cannot convert address data from DB") + return nil, err + } + if order == "DESC" { + sort.Slice(addressData.StakingTXs[:], func(i, j int) bool { + return addressData.StakingTXs[i].Timestamp > addressData.StakingTXs[j].Timestamp + }) + } else { + sort.Slice(addressData.StakingTXs[:], func(i, j int) bool { + return addressData.StakingTXs[i].Timestamp < addressData.StakingTXs[j].Timestamp + }) + } + hashes := make([]common.Hash, 0) + for _, tx := range addressData.StakingTXs { + if txType == "" || txType == "ALL" || txType == tx.Type { + hash := common.HexToHash(tx.ID) + hashes = append(hashes, hash) + } + } + return hashes, nil +}