Rosetta Implementation Cleanup (Stage 3 of Node API Overhaul) (#3390)

* [core] Add FindLogsWithTopic & unit test

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>

* [hmy] Add GetDetailedBlockSignerInfo

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>

* [hmy] Add IsCommitteeSelectionBlock

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>

* [test] Add test transaction creation helpers

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>

* [rosetta] Refactor account.go & add tests

* Move TestNewAccountIdentifier & TestGetAddress to account_test.go

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>

* [rosetta] Move Operation & Tx formatting to own files

* Move Respective unit tests to own files
* Expose GetOperations & GetStakingOperations
* Expose FormatTransaction, FormatCrossShardReceiverTransaction,
FormatGenesisTransaction, FormatPreStakingRewardTransaction & FormatUndelegationPayoutTransaction

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>

* [rosetta] Move TransactionMetadata to transaction_construction.go

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>

* [rosetta] Update construction to use new helpers & formatters

* Make docs consistent for mempool.go

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>

* [rosetta] Move all special tx & blk handling to own file

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>

* [rosetta] Remove all moved fns, methods & tests from block.go

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>

* Fix lint & imports

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>

* [rosetta] Rename all tx related files for clarity

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>

* [rosetta] Rename DefaultSenderAddress to FormatDefaultSenderAddress

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>

* [rosetta] Rename Currency to NativeCurrency

* This is in anticipation of HRC20 token support with rosetta
* Rename various native operation functions accordingly
* Add documentation to explain what a native token is

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>

* [rosetta] Fix pre-staking block reward calculation

* Move getPreStakingRewardTransactionIdentifiers to block_special.go
* Add epoch to block metadata
* Update unit tests

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>

* Add IsLastBlockInEpoch method to Block & Header

* Refactor all uses of length check `ShardState`
* [hmy] Refactor IsCommitteeSelectionBlock to use chain.IsCommitteeSelectionBlock
* Address PR comments

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>

* [rosetta] Update var names in preStakingRewardBlockTransaction

Signed-off-by: Daniel Van Der Maden <dvandermaden0@berkeley.edu>
pull/3397/head
Daniel Van Der Maden 4 years ago committed by GitHub
parent a2266d0d7a
commit b088d6dc77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      block/header.go
  2. 9
      consensus/consensus_service.go
  3. 2
      core/blockchain.go
  4. 4
      core/offchain.go
  5. 5
      core/types/block.go
  6. 17
      core/types/receipt.go
  7. 114
      core/types/receipt_test.go
  8. 45
      hmy/blockchain.go
  9. 6
      hmy/staking.go
  10. 12
      internal/chain/engine.go
  11. 2
      node/node_explorer.go
  12. 2
      node/node_handler.go
  13. 2
      node/worker/worker.go
  14. 20
      rosetta/common/config.go
  15. 27
      rosetta/common/operations.go
  16. 4
      rosetta/common/operations_test.go
  17. 44
      rosetta/services/account.go
  18. 72
      rosetta/services/account_test.go
  19. 1116
      rosetta/services/block.go
  20. 334
      rosetta/services/block_special.go
  21. 77
      rosetta/services/block_special_test.go
  22. 1326
      rosetta/services/block_test.go
  23. 12
      rosetta/services/construction_check.go
  24. 12
      rosetta/services/construction_check_test.go
  25. 9
      rosetta/services/construction_create_test.go
  26. 6
      rosetta/services/construction_parse.go
  27. 13
      rosetta/services/construction_parse_test.go
  28. 6
      rosetta/services/mempool.go
  29. 29
      rosetta/services/tx_construction.go
  30. 22
      rosetta/services/tx_construction_test.go
  31. 277
      rosetta/services/tx_format.go
  32. 504
      rosetta/services/tx_format_test.go
  33. 385
      rosetta/services/tx_operation.go
  34. 12
      rosetta/services/tx_operation_components.go
  35. 54
      rosetta/services/tx_operation_components_test.go
  36. 594
      rosetta/services/tx_operation_test.go
  37. 81
      test/helpers/transaction.go

@ -116,6 +116,12 @@ func (h *Header) With() HeaderFieldSetter {
return HeaderFieldSetter{h: h}
}
// IsLastBlockInEpoch returns True if it is the last block of the epoch.
// Note that the last block contains the shard state of the next epoch.
func (h *Header) IsLastBlockInEpoch() bool {
return len(h.ShardState()) > 0
}
// HeaderRegistry is the taggedrlp type registry for versioned headers.
var HeaderRegistry = taggedrlp.NewRegistry()

@ -287,7 +287,7 @@ func (consensus *Consensus) UpdateConsensusInformation() Mode {
nextEpoch := new(big.Int).Add(curHeader.Epoch(), common.Big1)
// Overwrite nextEpoch if the shard state has a epoch number
if len(curHeader.ShardState()) > 0 {
if curHeader.IsLastBlockInEpoch() {
nextShardState, err := curHeader.GetShardState()
if err != nil {
return Syncing
@ -300,8 +300,7 @@ func (consensus *Consensus) UpdateConsensusInformation() Mode {
consensus.BlockPeriod = 5 * time.Second
isFirstTimeStaking := consensus.ChainReader.Config().IsStaking(nextEpoch) &&
len(curHeader.ShardState()) > 0 &&
!consensus.ChainReader.Config().IsStaking(curEpoch)
curHeader.IsLastBlockInEpoch() && !consensus.ChainReader.Config().IsStaking(curEpoch)
haventUpdatedDecider := consensus.ChainReader.Config().IsStaking(curEpoch) &&
consensus.Decider.Policy() != quorum.SuperMajorityStake
@ -331,7 +330,7 @@ func (consensus *Consensus) UpdateConsensusInformation() Mode {
consensus.getLogger().Info().Msg("[UpdateConsensusInformation] Updating.....")
// genesis block is a special case that will have shard state and needs to skip processing
isNotGenesisBlock := curHeader.Number().Cmp(big.NewInt(0)) > 0
if len(curHeader.ShardState()) > 0 && isNotGenesisBlock {
if curHeader.IsLastBlockInEpoch() && isNotGenesisBlock {
nextShardState, err := committee.WithStakingEnabled.ReadFromDB(
nextEpoch, consensus.ChainReader,
@ -401,7 +400,7 @@ func (consensus *Consensus) UpdateConsensusInformation() Mode {
Msg("[UpdateConsensusInformation] changing committee")
// take care of possible leader change during the epoch
if len(curHeader.ShardState()) == 0 && curHeader.Number().Uint64() != 0 {
if !curHeader.IsLastBlockInEpoch() && curHeader.Number().Uint64() != 0 {
leaderPubKey, err := consensus.getLeaderPubKeyFromCoinbase(curHeader)
if err != nil || leaderPubKey == nil {
consensus.getLogger().Error().Err(err).

@ -1166,7 +1166,7 @@ func (bc *BlockChain) WriteBlockWithState(
// Flush trie state into disk if it's archival node or the block is epoch block
triedb := bc.stateCache.TrieDB()
if bc.cacheConfig.Disabled || len(block.Header().ShardState()) > 0 {
if bc.cacheConfig.Disabled || block.IsLastBlockInEpoch() {
if err := triedb.Commit(root, false); err != nil {
if isUnrecoverableErr(err) {
fmt.Printf("Unrecoverable error when committing triedb: %v\nExitting\n", err)

@ -39,7 +39,7 @@ func (bc *BlockChain) CommitOffChainData(
isStaking := bc.chainConfig.IsStaking(block.Epoch())
isPreStaking := bc.chainConfig.IsPreStaking(block.Epoch())
header := block.Header()
isNewEpoch := len(header.ShardState()) > 0
isNewEpoch := block.IsLastBlockInEpoch()
// Cross-shard txns
epoch := block.Header().Epoch()
if bc.chainConfig.HasCrossTxFields(block.Epoch()) {
@ -317,7 +317,7 @@ func (bc *BlockChain) writeValidatorStats(
func (bc *BlockChain) getNextBlockEpoch(header *block.Header) (*big.Int, error) {
nextBlockEpoch := header.Epoch()
if len(header.ShardState()) > 0 {
if header.IsLastBlockInEpoch() {
nextBlockEpoch = new(big.Int).Add(header.Epoch(), common.Big1)
decodeShardState, err := shard.DecodeWrapper(header.ShardState())
if err != nil {

@ -436,6 +436,11 @@ func (b *Block) Uncles() []*block.Header {
return b.uncles
}
// IsLastBlockInEpoch returns if its the last block of the epoch.
func (b *Block) IsLastBlockInEpoch() bool {
return b.header.IsLastBlockInEpoch()
}
// Transactions returns transactions.
func (b *Block) Transactions() Transactions {
return b.transactions

@ -23,6 +23,7 @@ import (
"unsafe"
"github.com/ethereum/go-ethereum/common"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rlp"
@ -218,3 +219,19 @@ func (r Receipts) ToShardID(i int) uint32 {
func (r Receipts) MaxToShardID() uint32 {
return 0
}
// FindLogsWithTopic returns all the logs that contain the given receipt
func FindLogsWithTopic(
receipt *Receipt, targetTopic ethcommon.Hash,
) []*Log {
logs := []*Log{}
for _, log := range receipt.Logs {
for _, topic := range log.Topics {
if topic == targetTopic {
logs = append(logs, log)
break
}
}
}
return logs
}

@ -0,0 +1,114 @@
package types
import (
"reflect"
"testing"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/harmony-one/harmony/staking"
)
func TestFindLogsWithTopic(t *testing.T) {
tests := []struct {
receipt *Receipt
topic ethcommon.Hash
expectedResponse []*Log
}{
// test 0
{
receipt: &Receipt{
Logs: []*Log{
{
Topics: []ethcommon.Hash{
staking.IsValidatorKey,
staking.IsValidator,
},
},
{
Topics: []ethcommon.Hash{
crypto.Keccak256Hash([]byte("test")),
},
},
{
Topics: []ethcommon.Hash{
staking.CollectRewardsTopic,
},
},
},
},
topic: staking.IsValidatorKey,
expectedResponse: []*Log{
{
Topics: []ethcommon.Hash{
staking.IsValidatorKey,
staking.IsValidator,
},
},
},
},
// test 1
{
receipt: &Receipt{
Logs: []*Log{
{
Topics: []ethcommon.Hash{
staking.IsValidatorKey,
staking.IsValidator,
},
},
{
Topics: []ethcommon.Hash{
crypto.Keccak256Hash([]byte("test")),
},
},
{
Topics: []ethcommon.Hash{
staking.CollectRewardsTopic,
},
},
},
},
topic: staking.CollectRewardsTopic,
expectedResponse: []*Log{
{
Topics: []ethcommon.Hash{
staking.CollectRewardsTopic,
},
},
},
},
// test 2
{
receipt: &Receipt{
Logs: []*Log{
{
Topics: []ethcommon.Hash{
staking.IsValidatorKey,
},
},
{
Topics: []ethcommon.Hash{
crypto.Keccak256Hash([]byte("test")),
},
},
{
Topics: []ethcommon.Hash{
staking.CollectRewardsTopic,
},
},
},
},
topic: staking.IsValidator,
expectedResponse: []*Log{},
},
}
for i, test := range tests {
response := FindLogsWithTopic(test.receipt, test.topic)
if !reflect.DeepEqual(test.expectedResponse, response) {
t.Errorf("Failed test %v, expected %v, got %v", i, test.expectedResponse, response)
}
}
}

@ -73,6 +73,51 @@ func (hmy *Harmony) GetBlockSigners(
return committee.Slots, mask, nil
}
// DetailedBlockSignerInfo contains all of the block singing information
type DetailedBlockSignerInfo struct {
// Signers is a map of addresses in the Signers for the block to
// all of the serialized BLS keys that signed said block.
Signers map[common.Address][]bls.SerializedPublicKey
// Committee when the block was signed.
Committee shard.SlotList
// TotalKeysSigned is the total number of bls keys that signed the block.
TotalKeysSigned uint
// Mask is the bitmap Mask for the block.
Mask *bls.Mask
BlockHash common.Hash
}
// GetDetailedBlockSignerInfo fetches the block signer information for any non-genesis block
func (hmy *Harmony) GetDetailedBlockSignerInfo(
ctx context.Context, blk *types.Block,
) (*DetailedBlockSignerInfo, error) {
slotList, mask, err := hmy.GetBlockSigners(
ctx, rpc.BlockNumber(blk.Number().Uint64()),
)
if err != nil {
return nil, err
}
totalSigners := uint(0)
sigInfos := map[common.Address][]bls.SerializedPublicKey{}
for _, slot := range slotList {
if _, ok := sigInfos[slot.EcdsaAddress]; !ok {
sigInfos[slot.EcdsaAddress] = []bls.SerializedPublicKey{}
}
if ok, err := mask.KeyEnabled(slot.BLSPublicKey); ok && err == nil {
sigInfos[slot.EcdsaAddress] = append(sigInfos[slot.EcdsaAddress], slot.BLSPublicKey)
totalSigners++
}
}
return &DetailedBlockSignerInfo{
Signers: sigInfos,
Committee: slotList,
TotalKeysSigned: totalSigners,
Mask: mask,
BlockHash: blk.Hash(),
}, nil
}
// GetLatestChainHeaders ..
func (hmy *Harmony) GetLatestChainHeaders() *block.HeaderPair {
return &block.HeaderPair{

@ -8,6 +8,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rpc"
"github.com/harmony-one/harmony/block"
"github.com/harmony-one/harmony/consensus/quorum"
"github.com/harmony-one/harmony/core/rawdb"
"github.com/harmony-one/harmony/core/types"
@ -133,6 +134,11 @@ func (hmy *Harmony) IsPreStakingEpoch(epoch *big.Int) bool {
return hmy.BlockChain.Config().IsPreStaking(epoch)
}
// IsCommitteeSelectionBlock checks if the given block is the committee selection block
func (hmy *Harmony) IsCommitteeSelectionBlock(header *block.Header) bool {
return chain.IsCommitteeSelectionBlock(hmy.BlockChain, header)
}
// GetDelegationLockingPeriodInEpoch ...
func (hmy *Harmony) GetDelegationLockingPeriodInEpoch(epoch *big.Int) int {
return chain.GetLockPeriodInEpoch(hmy.BlockChain, epoch)

@ -212,13 +212,11 @@ func (e *engineImpl) Finalize(
) (*types.Block, reward.Reader, error) {
isBeaconChain := header.ShardID() == shard.BeaconChainShardID
isNewEpoch := len(header.ShardState()) > 0
inPreStakingEra := chain.Config().IsPreStaking(header.Epoch())
inStakingEra := chain.Config().IsStaking(header.Epoch())
// Process Undelegations, set LastEpochInCommittee and set EPoS status
// Needs to be before AccumulateRewardsAndCountSigs
if isBeaconChain && isNewEpoch && inPreStakingEra {
if IsCommitteeSelectionBlock(chain, header) {
if err := payoutUndelegations(chain, header, state); err != nil {
return nil, nil, err
}
@ -312,6 +310,14 @@ func payoutUndelegations(
return nil
}
// IsCommitteeSelectionBlock checks if the given header is for the committee selection block
// which can only occur on beacon chain and if epoch > pre-staking epoch.
func IsCommitteeSelectionBlock(chain engine.ChainReader, header *block.Header) bool {
isBeaconChain := header.ShardID() == shard.BeaconChainShardID
inPreStakingEra := chain.Config().IsPreStaking(header.Epoch())
return isBeaconChain && header.IsLastBlockInEpoch() && inPreStakingEra
}
func setLastEpochInCommittee(header *block.Header, state *state.DB) error {
newShardState, err := header.GetShardState()
if err != nil {

@ -123,7 +123,7 @@ func (node *Node) explorerMessageHandler(ctx context.Context, msg *msg_pb.Messag
func (node *Node) AddNewBlockForExplorer(block *types.Block) {
utils.Logger().Info().Uint64("blockHeight", block.NumberU64()).Msg("[Explorer] Adding new block for explorer node")
if _, err := node.Blockchain().InsertChain([]*types.Block{block}, true); err == nil {
if len(block.Header().ShardState()) > 0 {
if block.IsLastBlockInEpoch() {
node.Consensus.UpdateConsensusInformation()
}
// Clean up the blocks to avoid OOM.

@ -396,7 +396,7 @@ func (node *Node) PostConsensusProcessing(newBlock *types.Block) error {
node.BroadcastMissingCXReceipts()
// Update consensus keys at last so the change of leader status doesn't mess up normal flow
if len(newBlock.Header().ShardState()) > 0 {
if newBlock.IsLastBlockInEpoch() {
node.Consensus.SetMode(node.Consensus.UpdateConsensusInformation())
}
if h := node.NodeConfig.WebHooks.Hooks; h != nil {

@ -317,7 +317,7 @@ func (w *Worker) GetNewEpoch() *big.Int {
// have an epoch and it will decide the next epoch for following blocks
epoch = new(big.Int).Set(shardState.Epoch)
} else {
if len(parent.Header().ShardState()) > 0 && parent.NumberU64() != 0 {
if parent.IsLastBlockInEpoch() && parent.NumberU64() != 0 {
// if parent has proposed a new shard state it increases by 1, except for genesis block.
epoch = epoch.Add(epoch, common.Big1)
}

@ -17,11 +17,11 @@ const (
// Blockchain ..
Blockchain = "Harmony"
// Symbol ..
Symbol = "ONE"
// NativeSymbol ..
NativeSymbol = "ONE"
// Decimals ..
Decimals = 18
// NativePrecision in the number of decimal places
NativePrecision = 18
// CurveType ..
CurveType = types.Secp256k1
@ -40,14 +40,14 @@ var (
// IdleTimeout ..
IdleTimeout = 120 * time.Second
// Currency ..
Currency = types.Currency{
Symbol: Symbol,
Decimals: Decimals,
// NativeCurrency ..
NativeCurrency = types.Currency{
Symbol: NativeSymbol,
Decimals: NativePrecision,
}
// CurrencyHash for quick equivalent checks
CurrencyHash = types.Hash(Currency)
// NativeCurrencyHash for quick equivalent checks
NativeCurrencyHash = types.Hash(NativeCurrency)
)
// SyncStatus ..

@ -10,27 +10,30 @@ import (
staking "github.com/harmony-one/harmony/staking/types"
)
// Invariant: A transaction can only contain 1 type of operation(s) other than gas expenditure.
// Invariant: A transaction can only contain 1 type of native operation(s) other than gas expenditure.
const (
// ExpendGasOperation ..
// ExpendGasOperation is an operation that only affects the native currency.
ExpendGasOperation = "Gas"
// TransferOperation ..
TransferOperation = "Transfer"
// TransferNativeOperation is an operation that only affects the native currency.
TransferNativeOperation = "NativeTransfer"
// CrossShardTransferOperation ..
CrossShardTransferOperation = "CrossShardTransfer"
// CrossShardTransferNativeOperation is an operation that only affects the native currency.
CrossShardTransferNativeOperation = "NativeCrossShardTransfer"
// ContractCreationOperation ..
// ContractCreationOperation is an operation that only affects the native currency.
ContractCreationOperation = "ContractCreation"
// GenesisFundsOperation ..
// GenesisFundsOperation is a special operation for genesis block only.
// Note that no transaction can be constructed with this operation.
GenesisFundsOperation = "Genesis"
// PreStakingBlockRewardOperation ..
// PreStakingBlockRewardOperation is a special operation for pre-staking era only.
// Note that no transaction can be constructed with this operation.
PreStakingBlockRewardOperation = "PreStakingBlockReward"
// UndelegationPayoutOperation ..
// UndelegationPayoutOperation is a special operation for committee election block only.
// Note that no transaction can be constructed with this operation.
UndelegationPayoutOperation = "UndelegationPayout"
)
@ -38,8 +41,8 @@ var (
// PlainOperationTypes ..
PlainOperationTypes = []string{
ExpendGasOperation,
TransferOperation,
CrossShardTransferOperation,
TransferNativeOperation,
CrossShardTransferNativeOperation,
ContractCreationOperation,
GenesisFundsOperation,
PreStakingBlockRewardOperation,

@ -50,8 +50,8 @@ func TestPlainOperationTypes(t *testing.T) {
plainOperationTypes := PlainOperationTypes
referenceOperationTypes := []string{
ExpendGasOperation,
TransferOperation,
CrossShardTransferOperation,
TransferNativeOperation,
CrossShardTransferNativeOperation,
ContractCreationOperation,
GenesisFundsOperation,
PreStakingBlockRewardOperation,

@ -2,6 +2,7 @@ package services
import (
"context"
"fmt"
"github.com/coinbase/rosetta-sdk-go/server"
"github.com/coinbase/rosetta-sdk-go/types"
@ -10,6 +11,7 @@ import (
hmyTypes "github.com/harmony-one/harmony/core/types"
"github.com/harmony-one/harmony/hmy"
internalCommon "github.com/harmony-one/harmony/internal/common"
"github.com/harmony-one/harmony/rosetta/common"
)
@ -25,7 +27,7 @@ func NewAccountAPI(hmy *hmy.Harmony) server.AccountAPIServicer {
}
}
// AccountBalance ...
// AccountBalance implements the /account/balance endpoint
func (s *AccountAPI) AccountBalance(
ctx context.Context, request *types.AccountBalanceRequest,
) (*types.AccountBalanceResponse, *types.Error) {
@ -73,7 +75,7 @@ func (s *AccountAPI) AccountBalance(
amount := types.Amount{
Value: balance.String(),
Currency: &common.Currency,
Currency: &common.NativeCurrency,
}
respBlock := types.BlockIdentifier{
@ -86,3 +88,41 @@ func (s *AccountAPI) AccountBalance(
Balances: []*types.Amount{&amount},
}, nil
}
// AccountMetadata used for account identifiers
type AccountMetadata struct {
Address string `json:"hex_address"`
}
// newAccountIdentifier ..
func newAccountIdentifier(
address ethCommon.Address,
) (*types.AccountIdentifier, *types.Error) {
b32Address, err := internalCommon.AddressToBech32(address)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
metadata, err := types.MarshalMap(AccountMetadata{Address: address.String()})
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
return &types.AccountIdentifier{
Address: b32Address,
Metadata: metadata,
}, nil
}
// getAddress ..
func getAddress(
identifier *types.AccountIdentifier,
) (ethCommon.Address, error) {
if identifier == nil {
return ethCommon.Address{}, fmt.Errorf("identifier cannot be nil")
}
return internalCommon.Bech32ToAddress(identifier.Address)
}

@ -0,0 +1,72 @@
package services
import (
"reflect"
"testing"
"github.com/coinbase/rosetta-sdk-go/types"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
internalCommon "github.com/harmony-one/harmony/internal/common"
)
func TestNewAccountIdentifier(t *testing.T) {
key, err := crypto.GenerateKey()
if err != nil {
t.Fatalf(err.Error())
}
addr := crypto.PubkeyToAddress(key.PublicKey)
b32Addr, err := internalCommon.AddressToBech32(addr)
if err != nil {
t.Fatalf(err.Error())
}
metadata, err := types.MarshalMap(AccountMetadata{Address: addr.String()})
if err != nil {
t.Fatalf(err.Error())
}
referenceAccID := &types.AccountIdentifier{
Address: b32Addr,
Metadata: metadata,
}
testAccID, rosettaError := newAccountIdentifier(addr)
if rosettaError != nil {
t.Fatalf("unexpected rosetta error: %v", rosettaError)
}
if !reflect.DeepEqual(referenceAccID, testAccID) {
t.Errorf("reference ID %v != testID %v", referenceAccID, testAccID)
}
}
func TestGetAddress(t *testing.T) {
key, err := crypto.GenerateKey()
if err != nil {
t.Fatalf(err.Error())
}
addr := crypto.PubkeyToAddress(key.PublicKey)
b32Addr, err := internalCommon.AddressToBech32(addr)
if err != nil {
t.Fatalf(err.Error())
}
testAccID := &types.AccountIdentifier{
Address: b32Addr,
}
testAddr, err := getAddress(testAccID)
if err != nil {
t.Fatal(err)
}
if testAddr != addr {
t.Errorf("expected %v to be %v", testAddr.String(), addr.String())
}
defaultAddr := ethcommon.Address{}
testAddr, err = getAddress(nil)
if err == nil {
t.Error("expected err for nil identifier")
}
if testAddr != defaultAddr {
t.Errorf("expected errored addres to be %v not %v", defaultAddr.String(), testAddr.String())
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,334 @@
package services
import (
"context"
"fmt"
"math/big"
"strings"
"github.com/coinbase/rosetta-sdk-go/types"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/harmony-one/harmony/core"
hmytypes "github.com/harmony-one/harmony/core/types"
internalCommon "github.com/harmony-one/harmony/internal/common"
nodeconfig "github.com/harmony-one/harmony/internal/configs/node"
shardingconfig "github.com/harmony-one/harmony/internal/configs/sharding"
"github.com/harmony-one/harmony/rosetta/common"
"github.com/harmony-one/harmony/rpc"
"github.com/harmony-one/harmony/shard"
)
// SpecialTransactionSuffix enum for all special transactions
type SpecialTransactionSuffix uint
// Special transaction suffixes that are specific to the rosetta package
const (
SpecialGenesisTxID SpecialTransactionSuffix = iota
SpecialPreStakingRewardTxID
SpecialUndelegationPayoutTxID
)
// Length for special case transaction identifiers
const (
blockHashStrLen = 64
bech32AddrStrLen = 42
)
// String ..
func (s SpecialTransactionSuffix) String() string {
return [...]string{"genesis", "reward", "undelegation"}[s]
}
// getSpecialCaseTransactionIdentifier fetches 'transaction identifiers' for a given block-hash and suffix.
// Special cases include genesis transactions, pre-staking era block rewards, and undelegation payouts.
// Must include block hash to guarantee uniqueness of tx identifiers.
func getSpecialCaseTransactionIdentifier(
blockHash ethcommon.Hash, address ethcommon.Address, suffix SpecialTransactionSuffix,
) *types.TransactionIdentifier {
return &types.TransactionIdentifier{
Hash: fmt.Sprintf("%v_%v_%v",
blockHash.String(), internalCommon.MustAddressToBech32(address), suffix.String(),
),
}
}
// unpackSpecialCaseTransactionIdentifier returns the suffix & blockHash if the txID is formatted correctly.
func unpackSpecialCaseTransactionIdentifier(
txID *types.TransactionIdentifier, expectedSuffix SpecialTransactionSuffix,
) (ethcommon.Hash, ethcommon.Address, *types.Error) {
hash := txID.Hash
hash = strings.TrimPrefix(hash, "0x")
hash = strings.TrimPrefix(hash, "0X")
minCharCount := blockHashStrLen + bech32AddrStrLen + 2
if len(hash) < minCharCount || string(hash[blockHashStrLen]) != "_" ||
string(hash[minCharCount-1]) != "_" || expectedSuffix.String() != hash[minCharCount:] {
return ethcommon.Hash{}, ethcommon.Address{}, common.NewError(common.CatchAllError, map[string]interface{}{
"message": "unknown special case transaction ID format",
})
}
blkHash := ethcommon.HexToHash(hash[:blockHashStrLen])
addr := internalCommon.MustBech32ToAddress(hash[blockHashStrLen+1 : minCharCount-1])
return blkHash, addr, nil
}
// genesisBlock is a special handler for the genesis block.
func (s *BlockAPI) genesisBlock(
ctx context.Context, request *types.BlockRequest, blk *hmytypes.Block,
) (response *types.BlockResponse, rosettaError *types.Error) {
var currBlockID, prevBlockID *types.BlockIdentifier
currBlockID = &types.BlockIdentifier{
Index: blk.Number().Int64(),
Hash: blk.Hash().String(),
}
prevBlockID = currBlockID
metadata, err := types.MarshalMap(BlockMetadata{
Epoch: blk.Epoch(),
})
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
responseBlock := &types.Block{
BlockIdentifier: currBlockID,
ParentBlockIdentifier: prevBlockID,
Timestamp: blk.Time().Int64() * 1e3, // Timestamp must be in ms.
Transactions: []*types.Transaction{}, // Do not return tx details as it is optional.
Metadata: metadata,
}
otherTransactions := []*types.TransactionIdentifier{}
// Report initial genesis funds as transactions to fit API.
for _, tx := range getPseudoTransactionForGenesis(getGenesisSpec(blk.ShardID())) {
if tx.To() == nil {
return nil, common.NewError(common.CatchAllError, nil)
}
otherTransactions = append(
otherTransactions, getSpecialCaseTransactionIdentifier(blk.Hash(), *tx.To(), SpecialGenesisTxID),
)
}
return &types.BlockResponse{
Block: responseBlock,
OtherTransactions: otherTransactions,
}, nil
}
// getPseudoTransactionForGenesis to create unsigned transaction that contain genesis funds.
// Note that this is for internal usage only. Genesis funds are not transactions.
func getPseudoTransactionForGenesis(spec *core.Genesis) []*hmytypes.Transaction {
txs := []*hmytypes.Transaction{}
for acc, bal := range spec.Alloc {
txs = append(txs, hmytypes.NewTransaction(
0, acc, spec.ShardID, bal.Balance, 0, big.NewInt(0), spec.ExtraData,
))
}
return txs
}
// specialGenesisBlockTransaction is a special handler for genesis block transactions
func (s *BlockAPI) specialGenesisBlockTransaction(
ctx context.Context, request *types.BlockTransactionRequest,
) (response *types.BlockTransactionResponse, rosettaError *types.Error) {
genesisBlock, err := s.hmy.BlockByNumber(ctx, rpc.BlockNumber(0).EthBlockNumber())
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
blkHash, address, rosettaError := unpackSpecialCaseTransactionIdentifier(
request.TransactionIdentifier, SpecialGenesisTxID,
)
if rosettaError != nil {
return nil, rosettaError
}
if blkHash.String() != genesisBlock.Hash().String() {
return nil, &common.TransactionNotFoundError
}
txs, rosettaError := FormatGenesisTransaction(request.TransactionIdentifier, address, s.hmy.ShardID)
if rosettaError != nil {
return nil, rosettaError
}
return &types.BlockTransactionResponse{Transaction: txs}, nil
}
// getPreStakingRewardTransactionIdentifiers is only used for the /block endpoint
// rewards for signing block n is paid out on block n+1
func (s *BlockAPI) getPreStakingRewardTransactionIdentifiers(
ctx context.Context, currBlock *hmytypes.Block,
) ([]*types.TransactionIdentifier, *types.Error) {
if currBlock.Number().Cmp(big.NewInt(1)) != 1 {
return nil, nil
}
blockNumToBeRewarded := currBlock.Number().Uint64() - 1
rewardedBlock, err := s.hmy.BlockByNumber(ctx, rpc.BlockNumber(blockNumToBeRewarded).EthBlockNumber())
if err != nil {
return nil, common.NewError(common.BlockNotFoundError, map[string]interface{}{
"message": err.Error(),
})
}
blockSigInfo, err := s.hmy.GetDetailedBlockSignerInfo(ctx, rewardedBlock)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
txIDs := []*types.TransactionIdentifier{}
for acc, signedBlsKeys := range blockSigInfo.Signers {
if len(signedBlsKeys) > 0 {
txIDs = append(txIDs, getSpecialCaseTransactionIdentifier(currBlock.Hash(), acc, SpecialPreStakingRewardTxID))
}
}
return txIDs, nil
}
// specialBlockTransaction is a formatter for special, non-genesis, transactions
func (s *BlockAPI) specialBlockTransaction(
ctx context.Context, request *types.BlockTransactionRequest,
) (*types.BlockTransactionResponse, *types.Error) {
// If no transaction info is found, check for special case transactions.
blk, rosettaError := s.getBlock(ctx, &types.PartialBlockIdentifier{Index: &request.BlockIdentifier.Index})
if rosettaError != nil {
return nil, rosettaError
}
if s.hmy.IsCommitteeSelectionBlock(blk.Header()) {
// Note that undelegation payout MUST be checked before reporting error in pre-staking & staking era.
response, rosettaError := s.undelegationPayoutBlockTransaction(ctx, request.TransactionIdentifier, blk)
if rosettaError != nil && !s.hmy.IsStakingEpoch(blk.Epoch()) && s.hmy.IsPreStakingEpoch(blk.Epoch()) {
// Handle edge case special transaction for pre-staking era
return s.preStakingRewardBlockTransaction(ctx, request.TransactionIdentifier, blk)
}
return response, rosettaError
}
if !s.hmy.IsStakingEpoch(blk.Epoch()) {
return s.preStakingRewardBlockTransaction(ctx, request.TransactionIdentifier, blk)
}
return nil, &common.TransactionNotFoundError
}
// preStakingRewardBlockTransaction is a special handler for pre-staking era
func (s *BlockAPI) preStakingRewardBlockTransaction(
ctx context.Context, txID *types.TransactionIdentifier, blk *hmytypes.Block,
) (*types.BlockTransactionResponse, *types.Error) {
if blk.Number().Cmp(big.NewInt(1)) != 1 {
return nil, common.NewError(common.TransactionNotFoundError, map[string]interface{}{
"message": "block does not contain any pre-staking era block rewards",
})
}
blkHash, address, rosettaError := unpackSpecialCaseTransactionIdentifier(txID, SpecialPreStakingRewardTxID)
if rosettaError != nil {
return nil, rosettaError
}
blockNumOfSigsForReward := blk.Number().Uint64() - 1
signedBlock, err := s.hmy.BlockByNumber(ctx, rpc.BlockNumber(blockNumOfSigsForReward).EthBlockNumber())
if err != nil {
return nil, common.NewError(common.BlockNotFoundError, map[string]interface{}{
"message": err.Error(),
})
}
if blkHash.String() != blk.Hash().String() {
return nil, common.NewError(common.SanityCheckError, map[string]interface{}{
"message": fmt.Sprintf(
"block hash %v != requested block hash %v in tx ID", blkHash.String(), blk.Hash().String(),
),
})
}
blockSignerInfo, err := s.hmy.GetDetailedBlockSignerInfo(ctx, signedBlock)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
transactions, rosettaError := FormatPreStakingRewardTransaction(txID, blockSignerInfo, address)
if rosettaError != nil {
return nil, rosettaError
}
return &types.BlockTransactionResponse{Transaction: transactions}, nil
}
// undelegationPayoutBlockTransaction is a special handler for undelegation payout transactions
func (s *BlockAPI) undelegationPayoutBlockTransaction(
ctx context.Context, txID *types.TransactionIdentifier, blk *hmytypes.Block,
) (*types.BlockTransactionResponse, *types.Error) {
blkHash, address, rosettaError := unpackSpecialCaseTransactionIdentifier(txID, SpecialUndelegationPayoutTxID)
if rosettaError != nil {
return nil, rosettaError
}
if blkHash.String() != blk.Hash().String() {
return nil, common.NewError(common.SanityCheckError, map[string]interface{}{
"message": fmt.Sprintf(
"block hash %v != requested block hash %v in tx ID", blkHash.String(), blk.Hash().String(),
),
})
}
delegatorPayouts, err := s.hmy.GetUndelegationPayouts(ctx, blk.Epoch())
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
transactions, rosettaError := FormatUndelegationPayoutTransaction(txID, delegatorPayouts, address)
if rosettaError != nil {
return nil, rosettaError
}
return &types.BlockTransactionResponse{Transaction: transactions}, nil
}
// getAllUndelegationPayoutTransactions is only used for the /block endpoint
func (s *BlockAPI) getAllUndelegationPayoutTransactions(
ctx context.Context, blk *hmytypes.Block,
) ([]*types.Transaction, *types.Error) {
if !s.hmy.IsCommitteeSelectionBlock(blk.Header()) {
return []*types.Transaction{}, nil
}
delegatorPayouts, err := s.hmy.GetUndelegationPayouts(ctx, blk.Epoch())
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
transactions := []*types.Transaction{}
for delegator, payout := range delegatorPayouts {
accID, rosettaError := newAccountIdentifier(delegator)
if rosettaError != nil {
return nil, rosettaError
}
transactions = append(transactions, &types.Transaction{
TransactionIdentifier: getSpecialCaseTransactionIdentifier(
blk.Hash(), delegator, SpecialUndelegationPayoutTxID,
),
Operations: []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: 0, // There is no gas expenditure for undelegation payout
},
Type: common.UndelegationPayoutOperation,
Status: common.SuccessOperationStatus.Status,
Account: accID,
Amount: &types.Amount{
Value: payout.String(),
Currency: &common.NativeCurrency,
},
},
},
})
}
return transactions, nil
}
// getGenesisSpec ..
func getGenesisSpec(shardID uint32) *core.Genesis {
if shard.Schedule.GetNetworkID() == shardingconfig.MainNet {
return core.NewGenesisSpec(nodeconfig.Mainnet, shardID)
}
if shard.Schedule.GetNetworkID() == shardingconfig.LocalNet {
return core.NewGenesisSpec(nodeconfig.Localnet, shardID)
}
return core.NewGenesisSpec(nodeconfig.Testnet, shardID)
}

@ -0,0 +1,77 @@
package services
import (
"fmt"
"math/big"
"reflect"
"testing"
"github.com/coinbase/rosetta-sdk-go/types"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/harmony-one/harmony/core"
internalCommon "github.com/harmony-one/harmony/internal/common"
nodeconfig "github.com/harmony-one/harmony/internal/configs/node"
"github.com/harmony-one/harmony/rosetta/common"
)
var (
oneBig = big.NewInt(1e18)
tenOnes = new(big.Int).Mul(big.NewInt(10), oneBig)
twelveOnes = new(big.Int).Mul(big.NewInt(12), oneBig)
gasPrice = big.NewInt(10000)
)
func TestGetPseudoTransactionForGenesis(t *testing.T) {
genesisSpec := core.NewGenesisSpec(nodeconfig.Testnet, 0)
txs := getPseudoTransactionForGenesis(genesisSpec)
for acc := range genesisSpec.Alloc {
found := false
for _, tx := range txs {
if acc == *tx.To() {
found = true
break
}
}
if !found {
t.Error("unable to find genesis account in generated pseudo transactions")
}
}
}
func TestSpecialCaseTransactionIdentifier(t *testing.T) {
testBlkHash := ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238")
testB32Address := "one10g7kfque6ew2jjfxxa6agkdwk4wlyjuncp6gwz"
testAddress := internalCommon.MustBech32ToAddress(testB32Address)
refTxID := &types.TransactionIdentifier{
Hash: fmt.Sprintf("%v_%v_%v", testBlkHash.String(), testB32Address, SpecialGenesisTxID.String()),
}
specialTxID := getSpecialCaseTransactionIdentifier(
testBlkHash, testAddress, SpecialGenesisTxID,
)
if !reflect.DeepEqual(refTxID, specialTxID) {
t.Fatal("invalid for mate for special case TxID")
}
unpackedBlkHash, unpackedAddress, rosettaError := unpackSpecialCaseTransactionIdentifier(
specialTxID, SpecialGenesisTxID,
)
if rosettaError != nil {
t.Fatal(rosettaError)
}
if unpackedAddress != testAddress {
t.Errorf("expected unpacked address to be %v not %v", testAddress.String(), unpackedAddress.String())
}
if unpackedBlkHash.String() != testBlkHash.String() {
t.Errorf("expected blk hash to be %v not %v", unpackedBlkHash.String(), testBlkHash.String())
}
_, _, rosettaError = unpackSpecialCaseTransactionIdentifier(
&types.TransactionIdentifier{Hash: ""}, SpecialGenesisTxID,
)
if rosettaError == nil {
t.Fatal("expected rosetta error")
}
if rosettaError.Code != common.CatchAllError.Code {
t.Error("expected error code to be catch call error")
}
}

File diff suppressed because it is too large Load Diff

@ -183,11 +183,11 @@ func (s *ConstructAPI) ConstructionMetadata(
if options.GasPriceMultiplier != nil && *options.GasPriceMultiplier > 1 {
gasMul = *options.GasPriceMultiplier
}
suggestedFee, suggestedPrice := getSuggestedFeeAndPrice(gasMul, new(big.Int).SetUint64(estGasUsed))
sugNativeFee, sugNativePrice := getSuggestedNativeFeeAndPrice(gasMul, new(big.Int).SetUint64(estGasUsed))
metadata, err := types.MarshalMap(ConstructMetadata{
Nonce: nonce,
GasPrice: suggestedPrice,
GasPrice: sugNativePrice,
GasLimit: estGasUsed,
Transaction: options.TransactionMetadata,
})
@ -198,12 +198,12 @@ func (s *ConstructAPI) ConstructionMetadata(
}
return &types.ConstructionMetadataResponse{
Metadata: metadata,
SuggestedFee: suggestedFee,
SuggestedFee: sugNativeFee,
}, nil
}
// getSuggestedFeeAndPrice ..
func getSuggestedFeeAndPrice(
// getSuggestedNativeFeeAndPrice ..
func getSuggestedNativeFeeAndPrice(
gasMul float64, estGasUsed *big.Int,
) ([]*types.Amount, *big.Int) {
if estGasUsed == nil {
@ -218,7 +218,7 @@ func getSuggestedFeeAndPrice(
return []*types.Amount{
{
Value: fmt.Sprintf("%v", new(big.Int).Mul(gasPrice, estGasUsed)),
Currency: &common.Currency,
Currency: &common.NativeCurrency,
},
}, gasPrice
}

@ -28,7 +28,7 @@ func TestConstructMetadataOptions(t *testing.T) {
{
Metadata: ConstructMetadataOptions{
TransactionMetadata: refTxMedata,
OperationType: common.TransferOperation,
OperationType: common.TransferNativeOperation,
GasPriceMultiplier: nil,
},
ExpectError: false,
@ -36,7 +36,7 @@ func TestConstructMetadataOptions(t *testing.T) {
{
Metadata: ConstructMetadataOptions{
TransactionMetadata: refTxMedata,
OperationType: common.TransferOperation,
OperationType: common.TransferNativeOperation,
GasPriceMultiplier: &refGasPrice,
},
ExpectError: false,
@ -44,7 +44,7 @@ func TestConstructMetadataOptions(t *testing.T) {
{
Metadata: ConstructMetadataOptions{
TransactionMetadata: nil,
OperationType: common.TransferOperation,
OperationType: common.TransferNativeOperation,
GasPriceMultiplier: &refGasPrice,
},
ExpectError: true,
@ -52,7 +52,7 @@ func TestConstructMetadataOptions(t *testing.T) {
{
Metadata: ConstructMetadataOptions{
TransactionMetadata: nil,
OperationType: common.TransferOperation,
OperationType: common.TransferNativeOperation,
GasPriceMultiplier: nil,
},
ExpectError: true,
@ -170,7 +170,7 @@ func TestConstructMetadata(t *testing.T) {
}
}
func TestGetSuggestedFeeAndPrice(t *testing.T) {
func TestGetSuggestedNativeFeeAndPrice(t *testing.T) {
refEstGasUsed := big.NewInt(1000000)
cases := []struct {
@ -218,7 +218,7 @@ func TestGetSuggestedFeeAndPrice(t *testing.T) {
}
for i, test := range cases {
refAmounts, refPrice := getSuggestedFeeAndPrice(test.GasMul, test.EstGasUsed)
refAmounts, refPrice := getSuggestedNativeFeeAndPrice(test.GasMul, test.EstGasUsed)
if len(refAmounts) != 1 {
t.Errorf("expect exactly 1 amount for case %v", i)
continue

@ -11,6 +11,7 @@ import (
hmytypes "github.com/harmony-one/harmony/core/types"
stakingTypes "github.com/harmony-one/harmony/staking/types"
"github.com/harmony-one/harmony/test/helpers"
)
func TestUnpackWrappedTransactionFromString(t *testing.T) {
@ -27,8 +28,8 @@ func TestUnpackWrappedTransactionFromString(t *testing.T) {
signer := hmytypes.NewEIP155Signer(big.NewInt(0))
// Test plain transactions
tx, err := createTestTransaction(
signer, 0, 1, 2, refEstGasUsed.Uint64(), big.NewInt(1e10), []byte{0x01, 0x02},
tx, err := helpers.CreateTestTransaction(
signer, 0, 1, 2, refEstGasUsed.Uint64(), gasPrice, big.NewInt(1e10), []byte{0x01, 0x02},
)
if err != nil {
t.Fatal(err)
@ -62,13 +63,13 @@ func TestUnpackWrappedTransactionFromString(t *testing.T) {
if err != nil {
t.Fatalf(err.Error())
}
stx, err := createTestStakingTransaction(func() (stakingTypes.Directive, interface{}) {
stx, err := helpers.CreateTestStakingTransaction(func() (stakingTypes.Directive, interface{}) {
return stakingTypes.DirectiveDelegate, stakingTypes.Delegate{
DelegatorAddress: refAddr,
ValidatorAddress: crypto.PubkeyToAddress(receiverKey.PublicKey),
Amount: tenOnes,
}
}, refKey, 10, refEstGasUsed.Uint64())
}, refKey, 10, refEstGasUsed.Uint64(), gasPrice)
if err != nil {
t.Fatal(err)
}

@ -41,11 +41,11 @@ func parseUnsignedTransaction(
intendedReceipt := &hmyTypes.Receipt{
GasUsed: tx.Gas(),
}
formattedTx, rosettaError := formatTransaction(tx, intendedReceipt)
formattedTx, rosettaError := FormatTransaction(tx, intendedReceipt)
if rosettaError != nil {
return nil, rosettaError
}
tempAccID, rosettaError := newAccountIdentifier(DefaultSenderAddress)
tempAccID, rosettaError := newAccountIdentifier(FormatDefaultSenderAddress)
if rosettaError != nil {
return nil, rosettaError
}
@ -82,7 +82,7 @@ func parseSignedTransaction(
intendedReceipt := &hmyTypes.Receipt{
GasUsed: tx.Gas(),
}
formattedTx, rosettaError := formatTransaction(tx, intendedReceipt)
formattedTx, rosettaError := FormatTransaction(tx, intendedReceipt)
if rosettaError != nil {
return nil, rosettaError
}

@ -11,6 +11,7 @@ import (
hmytypes "github.com/harmony-one/harmony/core/types"
internalCommon "github.com/harmony-one/harmony/internal/common"
stakingTypes "github.com/harmony-one/harmony/staking/types"
"github.com/harmony-one/harmony/test/helpers"
)
var (
@ -20,8 +21,8 @@ var (
func TestParseUnsignedTransaction(t *testing.T) {
refEstGasUsed := big.NewInt(100000)
testTx, err := createTestTransaction(
tempTestSigner, 0, 1, 2, refEstGasUsed.Uint64(), big.NewInt(1e18), []byte{},
testTx, err := helpers.CreateTestTransaction(
tempTestSigner, 0, 1, 2, refEstGasUsed.Uint64(), gasPrice, big.NewInt(1e18), []byte{},
)
if err != nil {
t.Fatal(err)
@ -37,7 +38,7 @@ func TestParseUnsignedTransaction(t *testing.T) {
refTestReceipt := &hmytypes.Receipt{
GasUsed: testTx.Gas(),
}
refFormattedTx, rosettaError := formatTransaction(testTx, refTestReceipt)
refFormattedTx, rosettaError := FormatTransaction(testTx, refTestReceipt)
if rosettaError != nil {
t.Fatal(rosettaError)
}
@ -83,8 +84,8 @@ func TestParseUnsignedTransactionStaking(t *testing.T) {
func TestParseSignedTransaction(t *testing.T) {
refEstGasUsed := big.NewInt(100000)
testTx, err := createTestTransaction(
tempTestSigner, 0, 1, 2, refEstGasUsed.Uint64(), big.NewInt(1e18), []byte{},
testTx, err := helpers.CreateTestTransaction(
tempTestSigner, 0, 1, 2, refEstGasUsed.Uint64(), gasPrice, big.NewInt(1e18), []byte{},
)
if err != nil {
t.Fatal(err)
@ -100,7 +101,7 @@ func TestParseSignedTransaction(t *testing.T) {
refTestReceipt := &hmytypes.Receipt{
GasUsed: testTx.Gas(),
}
refFormattedTx, rosettaError := formatTransaction(testTx, refTestReceipt)
refFormattedTx, rosettaError := FormatTransaction(testTx, refTestReceipt)
if rosettaError != nil {
t.Fatal(rosettaError)
}

@ -25,7 +25,7 @@ func NewMempoolAPI(hmy *hmy.Harmony) server.MempoolAPIServicer {
}
}
// Mempool ...
// Mempool implements the /mempool endpoint.
func (s *MempoolAPI) Mempool(
ctx context.Context, req *types.NetworkRequest,
) (*types.MempoolResponse, *types.Error) {
@ -50,7 +50,7 @@ func (s *MempoolAPI) Mempool(
}, nil
}
// MempoolTransaction ...
// MempoolTransaction implements the /mempool/transaction endpoint.
func (s *MempoolAPI) MempoolTransaction(
ctx context.Context, req *types.MempoolTransactionRequest,
) (*types.MempoolTransactionResponse, *types.Error) {
@ -84,7 +84,7 @@ func (s *MempoolAPI) MempoolTransaction(
GasUsed: poolTx.Gas(),
}
respTx, err := formatTransaction(poolTx, estReceipt)
respTx, err := FormatTransaction(poolTx, estReceipt)
if err != nil {
return nil, err
}

@ -1,6 +1,7 @@
package services
import (
"encoding/json"
"fmt"
"github.com/coinbase/rosetta-sdk-go/types"
@ -11,6 +12,30 @@ import (
"github.com/harmony-one/harmony/rosetta/common"
)
// TransactionMetadata contains all (optional) information for a transaction.
type TransactionMetadata struct {
// CrossShardIdentifier is the transaction identifier on the from/source shard
CrossShardIdentifier *types.TransactionIdentifier `json:"cross_shard_transaction_identifier,omitempty"`
ToShardID *uint32 `json:"to_shard,omitempty"`
FromShardID *uint32 `json:"from_shard,omitempty"`
Data *string `json:"data,omitempty"`
Logs []*hmyTypes.Log `json:"logs,omitempty"`
}
// UnmarshalFromInterface ..
func (t *TransactionMetadata) UnmarshalFromInterface(metaData interface{}) error {
var args TransactionMetadata
dat, err := json.Marshal(metaData)
if err != nil {
return err
}
if err := json.Unmarshal(dat, &args); err != nil {
return err
}
*t = args
return nil
}
// ConstructTransaction object (unsigned).
// TODO (dm): implement staking transaction construction
func ConstructTransaction(
@ -30,7 +55,7 @@ func ConstructTransaction(
var tx hmyTypes.PoolTransaction
switch components.Type {
case common.CrossShardTransferOperation:
case common.CrossShardTransferNativeOperation:
if tx, rosettaError = constructCrossShardTransaction(components, metadata, sourceShardID); rosettaError != nil {
return nil, rosettaError
}
@ -38,7 +63,7 @@ func ConstructTransaction(
if tx, rosettaError = constructContractCreationTransaction(components, metadata, sourceShardID); rosettaError != nil {
return nil, rosettaError
}
case common.TransferOperation:
case common.TransferNativeOperation:
if tx, rosettaError = constructPlainTransaction(components, metadata, sourceShardID); rosettaError != nil {
return nil, rosettaError
}

@ -27,7 +27,7 @@ func TestConstructPlainTransaction(t *testing.T) {
refDataBytes := []byte{0xEE, 0xEE, 0xEE}
refData := hexutil.Encode(refDataBytes)
refComponents := &OperationComponents{
Type: common.TransferOperation,
Type: common.TransferNativeOperation,
From: refFrom,
To: refTo,
Amount: big.NewInt(12000),
@ -116,7 +116,7 @@ func TestConstructPlainTransaction(t *testing.T) {
// test invalid receiver
_, rosettaError = constructPlainTransaction(&OperationComponents{
Type: common.TransferOperation,
Type: common.TransferNativeOperation,
From: refFrom,
To: nil,
Amount: big.NewInt(12000),
@ -126,7 +126,7 @@ func TestConstructPlainTransaction(t *testing.T) {
t.Error("expected error")
}
_, rosettaError = constructPlainTransaction(&OperationComponents{
Type: common.TransferOperation,
Type: common.TransferNativeOperation,
From: refFrom,
To: &types.AccountIdentifier{
Address: "",
@ -140,7 +140,7 @@ func TestConstructPlainTransaction(t *testing.T) {
// test valid nil sender
_, rosettaError = constructPlainTransaction(&OperationComponents{
Type: common.TransferOperation,
Type: common.TransferNativeOperation,
From: nil,
To: refTo,
Amount: big.NewInt(12000),
@ -179,7 +179,7 @@ func TestConstructCrossShardTransaction(t *testing.T) {
refDataBytes := []byte{0xEE, 0xEE, 0xEE}
refData := hexutil.Encode(refDataBytes)
refComponents := &OperationComponents{
Type: common.CrossShardTransferOperation,
Type: common.CrossShardTransferNativeOperation,
From: refFrom,
To: refTo,
Amount: big.NewInt(12000),
@ -238,7 +238,7 @@ func TestConstructCrossShardTransaction(t *testing.T) {
// test invalid receiver
_, rosettaError = constructCrossShardTransaction(&OperationComponents{
Type: common.CrossShardTransferOperation,
Type: common.CrossShardTransferNativeOperation,
From: refFrom,
To: nil,
Amount: big.NewInt(12000),
@ -248,7 +248,7 @@ func TestConstructCrossShardTransaction(t *testing.T) {
t.Error("expected error")
}
_, rosettaError = constructCrossShardTransaction(&OperationComponents{
Type: common.CrossShardTransferOperation,
Type: common.CrossShardTransferNativeOperation,
From: refFrom,
To: &types.AccountIdentifier{
Address: "",
@ -262,7 +262,7 @@ func TestConstructCrossShardTransaction(t *testing.T) {
// test valid nil sender
_, rosettaError = constructCrossShardTransaction(&OperationComponents{
Type: common.CrossShardTransferOperation,
Type: common.CrossShardTransferNativeOperation,
From: nil,
To: refTo,
Amount: big.NewInt(12000),
@ -432,7 +432,7 @@ func TestConstructTransaction(t *testing.T) {
// test valid cross-shard transfer (negative test cases are in TestConstructCrossShardTransaction)
generalTx, rosettaError := ConstructTransaction(&OperationComponents{
Type: common.CrossShardTransferOperation,
Type: common.CrossShardTransferNativeOperation,
From: refFrom,
To: refTo,
Amount: big.NewInt(12000),
@ -485,7 +485,7 @@ func TestConstructTransaction(t *testing.T) {
// test valid transfer (negative test cases are in TestConstructPlainTransaction)
generalTx, rosettaError = ConstructTransaction(&OperationComponents{
Type: common.TransferOperation,
Type: common.TransferNativeOperation,
From: refFrom,
To: refTo,
Amount: big.NewInt(12000),
@ -512,7 +512,7 @@ func TestConstructTransaction(t *testing.T) {
// test invalid sender shard
badShard := refShard + refToShard + 1
_, rosettaError = ConstructTransaction(&OperationComponents{
Type: common.TransferOperation,
Type: common.TransferNativeOperation,
From: refFrom,
To: refTo,
Amount: big.NewInt(12000),

@ -0,0 +1,277 @@
package services
import (
"encoding/hex"
"fmt"
"math/big"
"github.com/coinbase/rosetta-sdk-go/types"
ethcommon "github.com/ethereum/go-ethereum/common"
hmytypes "github.com/harmony-one/harmony/core/types"
"github.com/harmony-one/harmony/hmy"
internalCommon "github.com/harmony-one/harmony/internal/common"
"github.com/harmony-one/harmony/rosetta/common"
stakingNetwork "github.com/harmony-one/harmony/staking/network"
stakingTypes "github.com/harmony-one/harmony/staking/types"
)
var (
// FormatDefaultSenderAddress ..
FormatDefaultSenderAddress = ethcommon.HexToAddress("0xEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE")
)
// FormatTransaction for staking, cross-shard sender, and plain transactions
func FormatTransaction(
tx hmytypes.PoolTransaction, receipt *hmytypes.Receipt,
) (fmtTx *types.Transaction, rosettaError *types.Error) {
var operations []*types.Operation
var isCrossShard, isStaking bool
var toShard uint32
switch tx.(type) {
case *stakingTypes.StakingTransaction:
isStaking = true
stakingTx := tx.(*stakingTypes.StakingTransaction)
operations, rosettaError = GetOperationsFromStakingTransaction(stakingTx, receipt)
if rosettaError != nil {
return nil, rosettaError
}
isCrossShard = false
toShard = stakingTx.ShardID()
case *hmytypes.Transaction:
isStaking = false
plainTx := tx.(*hmytypes.Transaction)
operations, rosettaError = GetNativeOperationsFromTransaction(plainTx, receipt)
if rosettaError != nil {
return nil, rosettaError
}
isCrossShard = plainTx.ShardID() != plainTx.ToShardID()
toShard = plainTx.ToShardID()
default:
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": "unknown transaction type",
})
}
fromShard := tx.ShardID()
txID := &types.TransactionIdentifier{Hash: tx.Hash().String()}
// Set all possible metadata
var txMetadata TransactionMetadata
if isCrossShard {
txMetadata.CrossShardIdentifier = txID
txMetadata.ToShardID = &toShard
txMetadata.FromShardID = &fromShard
}
if len(tx.Data()) > 0 && !isStaking {
hexData := hex.EncodeToString(tx.Data())
txMetadata.Data = &hexData
txMetadata.Logs = receipt.Logs
}
metadata, err := types.MarshalMap(txMetadata)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
return &types.Transaction{
TransactionIdentifier: txID,
Operations: operations,
Metadata: metadata,
}, nil
}
// FormatCrossShardReceiverTransaction for cross-shard payouts on destination shard
func FormatCrossShardReceiverTransaction(
cxReceipt *hmytypes.CXReceipt,
) (txs *types.Transaction, rosettaError *types.Error) {
ctxID := &types.TransactionIdentifier{Hash: cxReceipt.TxHash.String()}
senderAccountID, rosettaError := newAccountIdentifier(cxReceipt.From)
if rosettaError != nil {
return nil, rosettaError
}
receiverAccountID, rosettaError := newAccountIdentifier(*cxReceipt.To)
if rosettaError != nil {
return nil, rosettaError
}
metadata, err := types.MarshalMap(TransactionMetadata{
CrossShardIdentifier: ctxID,
ToShardID: &cxReceipt.ToShardID,
FromShardID: &cxReceipt.ShardID,
})
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
opMetadata, err := types.MarshalMap(common.CrossShardTransactionOperationMetadata{
From: senderAccountID,
To: receiverAccountID,
})
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
return &types.Transaction{
TransactionIdentifier: ctxID,
Metadata: metadata,
Operations: []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: 0, // There is no gas expenditure for cross-shard transaction payout
},
Type: common.CrossShardTransferNativeOperation,
Status: common.SuccessOperationStatus.Status,
Account: receiverAccountID,
Amount: &types.Amount{
Value: cxReceipt.Amount.String(),
Currency: &common.NativeCurrency,
},
Metadata: opMetadata,
},
},
}, nil
}
// FormatGenesisTransaction for genesis block's initial balances
func FormatGenesisTransaction(
txID *types.TransactionIdentifier, targetAddr ethcommon.Address, shardID uint32,
) (fmtTx *types.Transaction, rosettaError *types.Error) {
var b32Addr string
targetB32Addr := internalCommon.MustAddressToBech32(targetAddr)
for _, tx := range getPseudoTransactionForGenesis(getGenesisSpec(shardID)) {
if tx.To() == nil {
return nil, common.NewError(common.CatchAllError, nil)
}
b32Addr = internalCommon.MustAddressToBech32(*tx.To())
if targetB32Addr == b32Addr {
accID, rosettaError := newAccountIdentifier(*tx.To())
if rosettaError != nil {
return nil, rosettaError
}
return &types.Transaction{
TransactionIdentifier: txID,
Operations: []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: 0,
},
Type: common.GenesisFundsOperation,
Status: common.SuccessOperationStatus.Status,
Account: accID,
Amount: &types.Amount{
Value: tx.Value().String(),
Currency: &common.NativeCurrency,
},
},
},
}, nil
}
}
return nil, &common.TransactionNotFoundError
}
// FormatPreStakingRewardTransaction for block rewards in pre-staking era for a given Bech-32 address.
func FormatPreStakingRewardTransaction(
txID *types.TransactionIdentifier, blockSigInfo *hmy.DetailedBlockSignerInfo, address ethcommon.Address,
) (*types.Transaction, *types.Error) {
signatures, ok := blockSigInfo.Signers[address]
if !ok || len(signatures) == 0 {
return nil, &common.TransactionNotFoundError
}
accID, rosettaError := newAccountIdentifier(address)
if rosettaError != nil {
return nil, rosettaError
}
// Calculate rewards exactly like `AccumulateRewardsAndCountSigs` but short circuit when possible.
// WARNING: must do calculation in the order of the committee to get accurate values.
i := 0
last := big.NewInt(0)
rewardsForThisBlock := big.NewInt(0)
count := big.NewInt(int64(blockSigInfo.TotalKeysSigned))
for _, slot := range blockSigInfo.Committee {
rewardsForThisAddr := big.NewInt(0)
if keys, ok := blockSigInfo.Signers[slot.EcdsaAddress]; ok {
for range keys {
cur := big.NewInt(0)
cur.Mul(stakingNetwork.BlockReward, big.NewInt(int64(i+1))).Div(cur, count)
reward := big.NewInt(0).Sub(cur, last)
rewardsForThisAddr = new(big.Int).Add(reward, rewardsForThisAddr)
last = cur
i++
}
}
if slot.EcdsaAddress == address {
rewardsForThisBlock = rewardsForThisAddr
if !(rewardsForThisAddr.Cmp(big.NewInt(0)) > 0) {
return nil, common.NewError(common.SanityCheckError, map[string]interface{}{
"message": "expected non-zero block reward in pre-staking era for block signer",
})
}
break
}
}
return &types.Transaction{
TransactionIdentifier: txID,
Operations: []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: 0,
},
Type: common.PreStakingBlockRewardOperation,
Status: common.SuccessOperationStatus.Status,
Account: accID,
Amount: &types.Amount{
Value: rewardsForThisBlock.String(),
Currency: &common.NativeCurrency,
},
},
},
}, nil
}
// FormatUndelegationPayoutTransaction for undelegation payouts at committee selection block
func FormatUndelegationPayoutTransaction(
txID *types.TransactionIdentifier, delegatorPayouts hmy.UndelegationPayouts, address ethcommon.Address,
) (*types.Transaction, *types.Error) {
accID, rosettaError := newAccountIdentifier(address)
if rosettaError != nil {
return nil, rosettaError
}
payout, ok := delegatorPayouts[address]
if !ok {
return nil, &common.TransactionNotFoundError
}
return &types.Transaction{
TransactionIdentifier: txID,
Operations: []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: 0,
},
Type: common.UndelegationPayoutOperation,
Status: common.SuccessOperationStatus.Status,
Account: accID,
Amount: &types.Amount{
Value: payout.String(),
Currency: &common.NativeCurrency,
},
},
},
}, nil
}
// negativeBigValue formats a transaction value as a string
func negativeBigValue(num *big.Int) string {
value := "0"
if num != nil && num.Cmp(big.NewInt(0)) != 0 {
value = fmt.Sprintf("-%v", new(big.Int).Abs(num))
}
return value
}

@ -0,0 +1,504 @@
package services
import (
"crypto/ecdsa"
"fmt"
"math/big"
"reflect"
"testing"
"github.com/coinbase/rosetta-sdk-go/types"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
hmytypes "github.com/harmony-one/harmony/core/types"
"github.com/harmony-one/harmony/crypto/bls"
"github.com/harmony-one/harmony/hmy"
"github.com/harmony-one/harmony/internal/params"
"github.com/harmony-one/harmony/rosetta/common"
"github.com/harmony-one/harmony/shard"
stakingNetwork "github.com/harmony-one/harmony/staking/network"
stakingTypes "github.com/harmony-one/harmony/staking/types"
"github.com/harmony-one/harmony/test/helpers"
)
// Invariant: A transaction can only contain 1 type of native operation(s) other than gas expenditure.
func assertNativeOperationTypeUniquenessInvariant(operations []*types.Operation) error {
foundType := ""
for _, op := range operations {
if op.Type == common.ExpendGasOperation {
continue
}
if foundType == "" {
foundType = op.Type
}
if op.Type != foundType {
return fmt.Errorf("found more than 1 type in given set of operations")
}
}
return nil
}
// Note that this test only checks the general format of each type transaction on Harmony.
// The detailed operation checks for each type of transaction is done in separate unit tests.
func TestFormatTransactionIntegration(t *testing.T) {
gasLimit := uint64(1e18)
gasUsed := uint64(1e5)
senderKey, err := crypto.GenerateKey()
if err != nil {
t.Fatalf(err.Error())
}
receiverKey, err := crypto.GenerateKey()
if err != nil {
t.Fatalf(err.Error())
}
testFormatStakingTransaction(t, gasLimit, gasUsed, senderKey, receiverKey)
testFormatPlainTransaction(t, gasLimit, gasUsed, senderKey, receiverKey)
// Note that cross-shard receiver operations/transactions are formatted via
// FormatCrossShardReceiverTransaction, thus, it is not tested here -- but tested on its own.
testFormatCrossShardSenderTransaction(t, gasLimit, gasUsed, senderKey, receiverKey)
}
func testFormatStakingTransaction(
t *testing.T, gasLimit, gasUsed uint64, senderKey, receiverKey *ecdsa.PrivateKey,
) {
senderAddr := crypto.PubkeyToAddress(senderKey.PublicKey)
receiverAddr := crypto.PubkeyToAddress(receiverKey.PublicKey)
tx, err := helpers.CreateTestStakingTransaction(func() (stakingTypes.Directive, interface{}) {
return stakingTypes.DirectiveDelegate, stakingTypes.Delegate{
DelegatorAddress: senderAddr,
ValidatorAddress: receiverAddr,
Amount: tenOnes,
}
}, senderKey, 0, gasLimit, gasPrice)
if err != nil {
t.Fatal(err.Error())
}
senderAccID, rosettaError := newAccountIdentifier(senderAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
receipt := &hmytypes.Receipt{
Status: hmytypes.ReceiptStatusSuccessful,
GasUsed: gasUsed,
}
rosettaTx, rosettaError := FormatTransaction(tx, receipt)
if rosettaError != nil {
t.Fatal(rosettaError)
}
if len(rosettaTx.Operations) != 2 {
t.Error("Expected 2 operations")
}
if err := assertNativeOperationTypeUniquenessInvariant(rosettaTx.Operations); err != nil {
t.Error(err)
}
if rosettaTx.TransactionIdentifier.Hash != tx.Hash().String() {
t.Error("Invalid transaction")
}
if rosettaTx.Operations[0].Type != common.ExpendGasOperation {
t.Error("Expected 1st operation to be gas type")
}
if rosettaTx.Operations[1].Type != tx.StakingType().String() {
t.Error("Expected 2nd operation to be staking type")
}
if reflect.DeepEqual(rosettaTx.Operations[1].Metadata, map[string]interface{}{}) {
t.Error("Expected staking operation to have some metadata")
}
if !reflect.DeepEqual(rosettaTx.Metadata, map[string]interface{}{}) {
t.Error("Expected transaction to have no metadata")
}
if !reflect.DeepEqual(rosettaTx.Operations[0].Account, senderAccID) {
t.Error("Expected sender to pay gas fee")
}
}
func testFormatPlainTransaction(
t *testing.T, gasLimit, gasUsed uint64, senderKey, receiverKey *ecdsa.PrivateKey,
) {
// Note that post EIP-155 epoch singer is tested in detailed tests.
signer := hmytypes.HomesteadSigner{}
tx, err := helpers.CreateTestTransaction(
signer, 0, 0, 0, 1e18, gasPrice, big.NewInt(1), []byte("test"),
)
if err != nil {
t.Fatal(err.Error())
}
senderAddr, err := tx.SenderAddress()
if err != nil {
t.Fatal(err.Error())
}
senderAccID, rosettaError := newAccountIdentifier(senderAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
receipt := &hmytypes.Receipt{
Status: hmytypes.ReceiptStatusSuccessful,
GasUsed: gasUsed,
}
rosettaTx, rosettaError := FormatTransaction(tx, receipt)
if rosettaError != nil {
t.Fatal(rosettaError)
}
if len(rosettaTx.Operations) != 3 {
t.Error("Expected 3 operations")
}
if err := assertNativeOperationTypeUniquenessInvariant(rosettaTx.Operations); err != nil {
t.Error(err)
}
if rosettaTx.TransactionIdentifier.Hash != tx.Hash().String() {
t.Error("Invalid transaction")
}
if rosettaTx.Operations[0].Type != common.ExpendGasOperation {
t.Error("Expected 1st operation to be gas")
}
if rosettaTx.Operations[1].Type != common.TransferNativeOperation {
t.Error("Expected 2nd operation to transfer related")
}
if rosettaTx.Operations[1].Metadata != nil {
t.Error("Expected 1st operation to have no metadata")
}
if rosettaTx.Operations[2].Metadata != nil {
t.Error("Expected 2nd operation to have no metadata")
}
if reflect.DeepEqual(rosettaTx.Metadata, map[string]interface{}{}) {
t.Error("Expected transaction to have some metadata")
}
if !reflect.DeepEqual(rosettaTx.Operations[0].Account, senderAccID) {
t.Error("Expected sender to pay gas fee")
}
}
func TestFormatGenesisTransaction(t *testing.T) {
genesisSpec := getGenesisSpec(0)
testBlkHash := ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238")
for acc := range genesisSpec.Alloc {
txID := getSpecialCaseTransactionIdentifier(testBlkHash, acc, SpecialGenesisTxID)
tx, rosettaError := FormatGenesisTransaction(txID, acc, 0)
if rosettaError != nil {
t.Fatal(rosettaError)
}
if !reflect.DeepEqual(txID, tx.TransactionIdentifier) {
t.Error("expected transaction ID of formatted tx to be same as requested")
}
if len(tx.Operations) != 1 {
t.Error("expected exactly 1 operation")
}
if err := assertNativeOperationTypeUniquenessInvariant(tx.Operations); err != nil {
t.Error(err)
}
if tx.Operations[0].OperationIdentifier.Index != 0 {
t.Error("expected operational ID to be 0")
}
if tx.Operations[0].Type != common.GenesisFundsOperation {
t.Error("expected operation to be genesis funds operations")
}
if tx.Operations[0].Status != common.SuccessOperationStatus.Status {
t.Error("expected successful operation status")
}
}
}
func TestFormatPreStakingRewardTransactionSuccess(t *testing.T) {
testKey, err := crypto.GenerateKey()
if err != nil {
t.Fatal(err)
}
testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
testBlockSigInfo := &hmy.DetailedBlockSignerInfo{
Signers: map[ethcommon.Address][]bls.SerializedPublicKey{
testAddr: { // Only care about length for this test
bls.SerializedPublicKey{},
bls.SerializedPublicKey{},
},
},
Committee: shard.SlotList{
{
EcdsaAddress: testAddr,
},
},
TotalKeysSigned: 150,
BlockHash: ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238"),
}
refTxID := getSpecialCaseTransactionIdentifier(testBlockSigInfo.BlockHash, testAddr, SpecialPreStakingRewardTxID)
tx, rosettaError := FormatPreStakingRewardTransaction(refTxID, testBlockSigInfo, testAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
if !reflect.DeepEqual(tx.TransactionIdentifier, refTxID) {
t.Errorf("Expected TxID %v got %v", refTxID, tx.TransactionIdentifier)
}
if len(tx.Operations) != 1 {
t.Fatal("Expected exactly 1 operation")
}
if err := assertNativeOperationTypeUniquenessInvariant(tx.Operations); err != nil {
t.Error(err)
}
if tx.Operations[0].OperationIdentifier.Index != 0 {
t.Error("expected operational ID to be 0")
}
if tx.Operations[0].Type != common.PreStakingBlockRewardOperation {
t.Error("expected operation type to be pre-staking era block rewards")
}
if tx.Operations[0].Status != common.SuccessOperationStatus.Status {
t.Error("expected successful operation status")
}
// Expect: myNumberOfSigForBlock * (totalAmountOfRewardsPerBlock / numOfSigsForBlock) to be my block reward amount
refAmount := new(big.Int).Mul(new(big.Int).Quo(stakingNetwork.BlockReward, big.NewInt(150)), big.NewInt(2))
fmtRefAmount := fmt.Sprintf("%v", refAmount)
if tx.Operations[0].Amount.Value != fmtRefAmount {
t.Errorf("expected operation amount to be %v not %v", fmtRefAmount, tx.Operations[0].Amount.Value)
}
testBlockSigInfo = &hmy.DetailedBlockSignerInfo{
Signers: map[ethcommon.Address][]bls.SerializedPublicKey{
testAddr: { // Only care about length for this test
bls.SerializedPublicKey{},
bls.SerializedPublicKey{},
},
},
Committee: shard.SlotList{},
TotalKeysSigned: 150,
BlockHash: ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238"),
}
tx, rosettaError = FormatPreStakingRewardTransaction(refTxID, testBlockSigInfo, testAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
if len(tx.Operations) != 1 {
t.Fatal("expected exactly 1 operation")
}
amt, err := types.AmountValue(tx.Operations[0].Amount)
if err != nil {
t.Fatal(err)
}
if amt.Cmp(big.NewInt(0)) != 0 {
t.Error("expected amount to be 0")
}
}
func TestFormatPreStakingRewardTransactionFail(t *testing.T) {
testKey, err := crypto.GenerateKey()
if err != nil {
t.Fatal(err)
}
testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
testBlockSigInfo := &hmy.DetailedBlockSignerInfo{
Signers: map[ethcommon.Address][]bls.SerializedPublicKey{
testAddr: {},
},
Committee: shard.SlotList{
{
EcdsaAddress: testAddr,
},
},
TotalKeysSigned: 150,
BlockHash: ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238"),
}
testTxID := getSpecialCaseTransactionIdentifier(testBlockSigInfo.BlockHash, testAddr, SpecialPreStakingRewardTxID)
_, rosettaError := FormatPreStakingRewardTransaction(testTxID, testBlockSigInfo, testAddr)
if rosettaError == nil {
t.Fatal("expected rosetta error")
}
if !reflect.DeepEqual(&common.TransactionNotFoundError, rosettaError) {
t.Error("expected transaction not found error")
}
testBlockSigInfo = &hmy.DetailedBlockSignerInfo{
Signers: map[ethcommon.Address][]bls.SerializedPublicKey{},
Committee: shard.SlotList{
{
EcdsaAddress: testAddr,
},
},
TotalKeysSigned: 150,
BlockHash: ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238"),
}
_, rosettaError = FormatPreStakingRewardTransaction(testTxID, testBlockSigInfo, testAddr)
if rosettaError == nil {
t.Fatal("expected rosetta error")
}
if !reflect.DeepEqual(&common.TransactionNotFoundError, rosettaError) {
t.Error("expected transaction not found error")
}
}
func TestFormatUndelegationPayoutTransaction(t *testing.T) {
testKey, err := crypto.GenerateKey()
if err != nil {
t.Fatal(err)
}
testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
testPayout := big.NewInt(1e10)
testDelegatorPayouts := hmy.UndelegationPayouts{
testAddr: testPayout,
}
testBlockHash := ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238")
testTxID := getSpecialCaseTransactionIdentifier(testBlockHash, testAddr, SpecialUndelegationPayoutTxID)
tx, rosettaError := FormatUndelegationPayoutTransaction(testTxID, testDelegatorPayouts, testAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
if len(tx.Operations) != 1 {
t.Fatal("expected tx operations to be of length 1")
}
if err := assertNativeOperationTypeUniquenessInvariant(tx.Operations); err != nil {
t.Error(err)
}
if tx.Operations[0].OperationIdentifier.Index != 0 {
t.Error("Expect first operation to be index 0")
}
if tx.Operations[0].Type != common.UndelegationPayoutOperation {
t.Errorf("Expect operation type to be: %v", common.UndelegationPayoutOperation)
}
if tx.Operations[0].Status != common.SuccessOperationStatus.Status {
t.Error("expected successful operation status")
}
if tx.Operations[0].Amount.Value != fmt.Sprintf("%v", testPayout) {
t.Errorf("expect payout to be %v", testPayout)
}
_, rosettaError = FormatUndelegationPayoutTransaction(testTxID, hmy.UndelegationPayouts{}, testAddr)
if rosettaError == nil {
t.Fatal("Expect error for no payouts found")
}
if rosettaError.Code != common.TransactionNotFoundError.Code {
t.Errorf("expect error code %v", common.TransactionNotFoundError.Code)
}
}
func testFormatCrossShardSenderTransaction(
t *testing.T, gasLimit, gasUsed uint64, senderKey, receiverKey *ecdsa.PrivateKey,
) {
// Note that post EIP-155 epoch singer is tested in detailed tests.
signer := hmytypes.HomesteadSigner{}
tx, err := helpers.CreateTestTransaction(
signer, 0, 1, 0, 1e18, gasPrice, big.NewInt(1), []byte("test"),
)
if err != nil {
t.Fatal(err.Error())
}
senderAddr, err := tx.SenderAddress()
if err != nil {
t.Fatal(err.Error())
}
senderAccID, rosettaError := newAccountIdentifier(senderAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
receipt := &hmytypes.Receipt{
Status: hmytypes.ReceiptStatusSuccessful,
GasUsed: gasUsed,
}
rosettaTx, rosettaError := FormatTransaction(tx, receipt)
if rosettaError != nil {
t.Fatal(rosettaError)
}
if len(rosettaTx.Operations) != 2 {
t.Error("Expected 2 operations")
}
if err := assertNativeOperationTypeUniquenessInvariant(rosettaTx.Operations); err != nil {
t.Error(err)
}
if rosettaTx.TransactionIdentifier.Hash != tx.Hash().String() {
t.Error("Invalid transaction")
}
if rosettaTx.Operations[0].Type != common.ExpendGasOperation {
t.Error("Expected 1st operation to be gas")
}
if rosettaTx.Operations[1].Type != common.CrossShardTransferNativeOperation {
t.Error("Expected 2nd operation to cross-shard transfer related")
}
if reflect.DeepEqual(rosettaTx.Operations[1].Metadata, map[string]interface{}{}) {
t.Error("Expected 1st operation to have metadata")
}
if reflect.DeepEqual(rosettaTx.Metadata, map[string]interface{}{}) {
t.Error("Expected transaction to have some metadata")
}
if !reflect.DeepEqual(rosettaTx.Operations[0].Account, senderAccID) {
t.Error("Expected sender to pay gas fee")
}
}
func TestFormatCrossShardReceiverTransaction(t *testing.T) {
signer := hmytypes.NewEIP155Signer(params.TestChainConfig.ChainID)
tx, err := helpers.CreateTestTransaction(
signer, 0, 1, 0, 1e18, gasPrice, big.NewInt(1), []byte{},
)
if err != nil {
t.Fatal(err.Error())
}
senderAddr, err := tx.SenderAddress()
if err != nil {
t.Fatal(err.Error())
}
senderAccID, rosettaError := newAccountIdentifier(senderAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
receiverAccID, rosettaError := newAccountIdentifier(*tx.To())
if rosettaError != nil {
t.Fatal(rosettaError)
}
cxReceipt := &hmytypes.CXReceipt{
TxHash: tx.Hash(),
From: senderAddr,
To: tx.To(),
ShardID: 0,
ToShardID: 1,
Amount: tx.Value(),
}
opMetadata, err := types.MarshalMap(common.CrossShardTransactionOperationMetadata{
From: senderAccID,
To: receiverAccID,
})
if err != nil {
t.Error(err)
}
refCxID := &types.TransactionIdentifier{Hash: tx.Hash().String()}
refOperations := []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: 0, // There is no gas expenditure for cross-shard payout
},
Type: common.CrossShardTransferNativeOperation,
Status: common.SuccessOperationStatus.Status,
Account: receiverAccID,
Amount: &types.Amount{
Value: fmt.Sprintf("%v", tx.Value().Uint64()),
Currency: &common.NativeCurrency,
},
Metadata: opMetadata,
},
}
to := tx.ToShardID()
from := tx.ShardID()
refMetadata, err := types.MarshalMap(TransactionMetadata{
CrossShardIdentifier: refCxID,
ToShardID: &to,
FromShardID: &from,
})
refRosettaTx := &types.Transaction{
TransactionIdentifier: refCxID,
Operations: refOperations,
Metadata: refMetadata,
}
rosettaTx, rosettaError := FormatCrossShardReceiverTransaction(cxReceipt)
if rosettaError != nil {
t.Fatal(rosettaError)
}
if !reflect.DeepEqual(rosettaTx, refRosettaTx) {
t.Errorf("Expected transaction to be %v not %v", refRosettaTx, rosettaTx)
}
if err := assertNativeOperationTypeUniquenessInvariant(rosettaTx.Operations); err != nil {
t.Error(err)
}
}

@ -0,0 +1,385 @@
package services
import (
"fmt"
"math/big"
"github.com/coinbase/rosetta-sdk-go/types"
ethcommon "github.com/ethereum/go-ethereum/common"
hmytypes "github.com/harmony-one/harmony/core/types"
"github.com/harmony-one/harmony/internal/utils"
"github.com/harmony-one/harmony/rosetta/common"
rpcV2 "github.com/harmony-one/harmony/rpc/v2"
"github.com/harmony-one/harmony/staking"
stakingTypes "github.com/harmony-one/harmony/staking/types"
)
// GetNativeOperationsFromTransaction for one of the following transactions:
// contract creation, cross-shard sender, same-shard transfer.
// Native operations only include operations that affect the native currency balance of an account.
func GetNativeOperationsFromTransaction(
tx *hmytypes.Transaction, receipt *hmytypes.Receipt,
) ([]*types.Operation, *types.Error) {
senderAddress, err := tx.SenderAddress()
if err != nil {
senderAddress = FormatDefaultSenderAddress
}
accountID, rosettaError := newAccountIdentifier(senderAddress)
if rosettaError != nil {
return nil, rosettaError
}
// All operations excepts for cross-shard tx payout expend gas
gasExpended := new(big.Int).Mul(new(big.Int).SetUint64(receipt.GasUsed), tx.GasPrice())
gasOperations := newNativeOperations(gasExpended, accountID)
// Handle different cases of plain transactions
var txOperations []*types.Operation
if tx.To() == nil {
txOperations, rosettaError = newContractCreationNativeOperations(
gasOperations[0].OperationIdentifier, tx, receipt, senderAddress,
)
} else if tx.ShardID() != tx.ToShardID() {
txOperations, rosettaError = newCrossShardSenderTransferNativeOperations(
gasOperations[0].OperationIdentifier, tx, senderAddress,
)
} else {
txOperations, rosettaError = newTransferNativeOperations(
gasOperations[0].OperationIdentifier, tx, receipt, senderAddress,
)
}
if rosettaError != nil {
return nil, rosettaError
}
return append(gasOperations, txOperations...), nil
}
// GetOperationsFromStakingTransaction for all staking directives
// Note that only native operations can come from staking transactions.
func GetOperationsFromStakingTransaction(
tx *stakingTypes.StakingTransaction, receipt *hmytypes.Receipt,
) ([]*types.Operation, *types.Error) {
senderAddress, err := tx.SenderAddress()
if err != nil {
senderAddress = FormatDefaultSenderAddress
}
accountID, rosettaError := newAccountIdentifier(senderAddress)
if rosettaError != nil {
return nil, rosettaError
}
// All operations excepts for cross-shard tx payout expend gas
gasExpended := new(big.Int).Mul(new(big.Int).SetUint64(receipt.GasUsed), tx.GasPrice())
gasOperations := newNativeOperations(gasExpended, accountID)
// Format staking message for metadata using decimal numbers (hence usage of rpcV2)
rpcStakingTx, err := rpcV2.NewStakingTransaction(tx, ethcommon.Hash{}, 0, 0, 0)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
metadata, err := types.MarshalMap(rpcStakingTx.Msg)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
// Set correct amount depending on staking message directive that apply balance changes INSTANTLY
var amount *types.Amount
switch tx.StakingType() {
case stakingTypes.DirectiveCreateValidator:
if amount, rosettaError = getAmountFromCreateValidatorMessage(tx.Data()); rosettaError != nil {
return nil, rosettaError
}
case stakingTypes.DirectiveDelegate:
if amount, rosettaError = getAmountFromDelegateMessage(receipt, tx.Data()); rosettaError != nil {
return nil, rosettaError
}
case stakingTypes.DirectiveCollectRewards:
if amount, rosettaError = getAmountFromCollectRewards(receipt, senderAddress); rosettaError != nil {
return nil, rosettaError
}
default:
amount = &types.Amount{
Value: "0", // All other staking transactions do not apply balance changes instantly or at all
Currency: &common.NativeCurrency,
}
}
return append(gasOperations, &types.Operation{
OperationIdentifier: &types.OperationIdentifier{
Index: gasOperations[0].OperationIdentifier.Index + 1,
},
RelatedOperations: []*types.OperationIdentifier{
gasOperations[0].OperationIdentifier,
},
Type: tx.StakingType().String(),
Status: common.SuccessOperationStatus.Status,
Account: accountID,
Amount: amount,
Metadata: metadata,
}), nil
}
func getAmountFromCreateValidatorMessage(data []byte) (*types.Amount, *types.Error) {
msg, err := stakingTypes.RLPDecodeStakeMsg(data, stakingTypes.DirectiveCreateValidator)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
stkMsg, ok := msg.(*stakingTypes.CreateValidator)
if !ok {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": "unable to parse staking message for create validator tx",
})
}
return &types.Amount{
Value: negativeBigValue(stkMsg.Amount),
Currency: &common.NativeCurrency,
}, nil
}
func getAmountFromDelegateMessage(receipt *hmytypes.Receipt, data []byte) (*types.Amount, *types.Error) {
msg, err := stakingTypes.RLPDecodeStakeMsg(data, stakingTypes.DirectiveDelegate)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
stkMsg, ok := msg.(*stakingTypes.Delegate)
if !ok {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": "unable to parse staking message for delegate tx",
})
}
stkAmount := stkMsg.Amount
logs := hmytypes.FindLogsWithTopic(receipt, staking.DelegateTopic)
for _, log := range logs {
if len(log.Data) > ethcommon.AddressLength {
validatorAddress := ethcommon.BytesToAddress(log.Data[:ethcommon.AddressLength])
if log.Address == stkMsg.DelegatorAddress && stkMsg.ValidatorAddress == validatorAddress {
// Remove re-delegation amount as funds were never credited to account's balance.
stkAmount = new(big.Int).Sub(stkAmount, new(big.Int).SetBytes(log.Data[ethcommon.AddressLength:]))
break
}
}
}
return &types.Amount{
Value: negativeBigValue(stkAmount),
Currency: &common.NativeCurrency,
}, nil
}
func getAmountFromCollectRewards(
receipt *hmytypes.Receipt, senderAddress ethcommon.Address,
) (*types.Amount, *types.Error) {
var amount *types.Amount
logs := hmytypes.FindLogsWithTopic(receipt, staking.CollectRewardsTopic)
for _, log := range logs {
if log.Address == senderAddress {
amount = &types.Amount{
Value: big.NewInt(0).SetBytes(log.Data).String(),
Currency: &common.NativeCurrency,
}
break
}
}
if amount == nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": fmt.Sprintf("collect rewards amount not found for %v", senderAddress),
})
}
return amount, nil
}
// newTransferNativeOperations extracts & formats the native operation(s) for plain transaction,
// including contract transactions.
func newTransferNativeOperations(
startingOperationID *types.OperationIdentifier,
tx *hmytypes.Transaction, receipt *hmytypes.Receipt, senderAddress ethcommon.Address,
) ([]*types.Operation, *types.Error) {
if tx.To() == nil {
return nil, common.NewError(common.CatchAllError, nil)
}
receiverAddress := *tx.To()
// Common elements
opType := common.TransferNativeOperation
opStatus := common.SuccessOperationStatus.Status
if receipt.Status == hmytypes.ReceiptStatusFailed {
if len(tx.Data()) > 0 {
opStatus = common.ContractFailureOperationStatus.Status
} else {
// Should never see a failed non-contract related transaction on chain
opStatus = common.FailureOperationStatus.Status
utils.Logger().Warn().Msgf("Failed transaction on chain: %v", tx.Hash().String())
}
}
// Subtraction operation elements
subOperationID := &types.OperationIdentifier{
Index: startingOperationID.Index + 1,
}
subRelatedID := []*types.OperationIdentifier{
startingOperationID,
}
subAccountID, rosettaError := newAccountIdentifier(senderAddress)
if rosettaError != nil {
return nil, rosettaError
}
subAmount := &types.Amount{
Value: negativeBigValue(tx.Value()),
Currency: &common.NativeCurrency,
}
// Addition operation elements
addOperationID := &types.OperationIdentifier{
Index: subOperationID.Index + 1,
}
addRelatedID := []*types.OperationIdentifier{
subOperationID,
}
addAccountID, rosettaError := newAccountIdentifier(receiverAddress)
if rosettaError != nil {
return nil, rosettaError
}
addAmount := &types.Amount{
Value: tx.Value().String(),
Currency: &common.NativeCurrency,
}
return []*types.Operation{
{
OperationIdentifier: subOperationID,
RelatedOperations: subRelatedID,
Type: opType,
Status: opStatus,
Account: subAccountID,
Amount: subAmount,
},
{
OperationIdentifier: addOperationID,
RelatedOperations: addRelatedID,
Type: opType,
Status: opStatus,
Account: addAccountID,
Amount: addAmount,
},
}, nil
}
// newCrossShardSenderTransferNativeOperations extracts & formats the native operation(s)
// for cross-shard-tx on the sender's shard.
func newCrossShardSenderTransferNativeOperations(
startingOperationID *types.OperationIdentifier,
tx *hmytypes.Transaction, senderAddress ethcommon.Address,
) ([]*types.Operation, *types.Error) {
if tx.To() == nil {
return nil, common.NewError(common.CatchAllError, nil)
}
senderAccountID, rosettaError := newAccountIdentifier(senderAddress)
if rosettaError != nil {
return nil, rosettaError
}
receiverAccountID, rosettaError := newAccountIdentifier(*tx.To())
if rosettaError != nil {
return nil, rosettaError
}
metadata, err := types.MarshalMap(common.CrossShardTransactionOperationMetadata{
From: senderAccountID,
To: receiverAccountID,
})
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
return []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: startingOperationID.Index + 1,
},
RelatedOperations: []*types.OperationIdentifier{
startingOperationID,
},
Type: common.CrossShardTransferNativeOperation,
Status: common.SuccessOperationStatus.Status,
Account: senderAccountID,
Amount: &types.Amount{
Value: negativeBigValue(tx.Value()),
Currency: &common.NativeCurrency,
},
Metadata: metadata,
},
}, nil
}
// newContractCreationNativeOperations extracts & formats the native operation(s) for a contract creation tx
func newContractCreationNativeOperations(
startingOperationID *types.OperationIdentifier,
tx *hmytypes.Transaction, txReceipt *hmytypes.Receipt, senderAddress ethcommon.Address,
) ([]*types.Operation, *types.Error) {
senderAccountID, rosettaError := newAccountIdentifier(senderAddress)
if rosettaError != nil {
return nil, rosettaError
}
// Set execution status as necessary
status := common.SuccessOperationStatus.Status
if txReceipt.Status == hmytypes.ReceiptStatusFailed {
status = common.ContractFailureOperationStatus.Status
}
contractAddressID, rosettaError := newAccountIdentifier(txReceipt.ContractAddress)
if rosettaError != nil {
return nil, rosettaError
}
return []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: startingOperationID.Index + 1,
},
RelatedOperations: []*types.OperationIdentifier{
startingOperationID,
},
Type: common.ContractCreationOperation,
Status: status,
Account: senderAccountID,
Amount: &types.Amount{
Value: negativeBigValue(tx.Value()),
Currency: &common.NativeCurrency,
},
Metadata: map[string]interface{}{
"contract_address": contractAddressID,
},
},
}, nil
}
// newNativeOperations creates a new operation with the gas fee as the first operation.
// Note: the gas fee is gasPrice * gasUsed.
func newNativeOperations(
gasFeeInATTO *big.Int, accountID *types.AccountIdentifier,
) []*types.Operation {
return []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: 0, // gas operation is always first
},
Type: common.ExpendGasOperation,
Status: common.SuccessOperationStatus.Status,
Account: accountID,
Amount: &types.Amount{
Value: negativeBigValue(gasFeeInATTO),
Currency: &common.NativeCurrency,
},
},
}
}

@ -57,7 +57,7 @@ func GetOperationComponents(
return getTransferOperationComponents(operations)
}
switch operations[0].Type {
case common.CrossShardTransferOperation:
case common.CrossShardTransferNativeOperation:
return getCrossShardOperationComponents(operations[0])
case common.ContractCreationOperation:
return getContractCreationOperationComponents(operations[0])
@ -78,7 +78,7 @@ func getTransferOperationComponents(
})
}
op0, op1 := operations[0], operations[1]
if op0.Type != common.TransferOperation || op1.Type != common.TransferOperation {
if op0.Type != common.TransferNativeOperation || op1.Type != common.TransferNativeOperation {
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
"message": "invalid operation type(s) for same shard transfer",
})
@ -101,8 +101,8 @@ func getTransferOperationComponents(
"message": "amount taken from sender is not exactly paid out to receiver for same shard transfer",
})
}
if types.Hash(op0.Amount.Currency) != common.CurrencyHash ||
types.Hash(op1.Amount.Currency) != common.CurrencyHash {
if types.Hash(op0.Amount.Currency) != common.NativeCurrencyHash ||
types.Hash(op1.Amount.Currency) != common.NativeCurrencyHash {
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
"message": "invalid currency for provided amounts",
})
@ -170,7 +170,7 @@ func getCrossShardOperationComponents(
"message": "sender amount must not be positive for cross shard transfer",
})
}
if types.Hash(operation.Amount.Currency) != common.CurrencyHash {
if types.Hash(operation.Amount.Currency) != common.NativeCurrencyHash {
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
"message": "invalid currency for provided amounts",
})
@ -215,7 +215,7 @@ func getContractCreationOperationComponents(
"message": "sender amount must not be positive for contract creation",
})
}
if types.Hash(operation.Amount.Currency) != common.CurrencyHash {
if types.Hash(operation.Amount.Currency) != common.NativeCurrencyHash {
return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{
"message": "invalid currency for provided amounts",
})

@ -14,7 +14,7 @@ import (
func TestGetContractCreationOperationComponents(t *testing.T) {
refAmount := &types.Amount{
Value: "-12000",
Currency: &common.Currency,
Currency: &common.NativeCurrency,
}
refKey := internalCommon.MustGeneratePrivateKey()
refFrom, rosettaError := newAccountIdentifier(crypto.PubkeyToAddress(refKey.PublicKey))
@ -57,7 +57,7 @@ func TestGetContractCreationOperationComponents(t *testing.T) {
Type: common.ContractCreationOperation,
Amount: &types.Amount{
Value: "12000",
Currency: &common.Currency,
Currency: &common.NativeCurrency,
},
Account: refFrom,
})
@ -101,7 +101,7 @@ func TestGetContractCreationOperationComponents(t *testing.T) {
func TestGetCrossShardOperationComponents(t *testing.T) {
refAmount := &types.Amount{
Value: "-12000",
Currency: &common.Currency,
Currency: &common.NativeCurrency,
}
refFromKey := internalCommon.MustGeneratePrivateKey()
refFrom, rosettaError := newAccountIdentifier(crypto.PubkeyToAddress(refFromKey.PublicKey))
@ -124,7 +124,7 @@ func TestGetCrossShardOperationComponents(t *testing.T) {
// test valid operations
refOperation := &types.Operation{
Type: common.CrossShardTransferOperation,
Type: common.CrossShardTransferNativeOperation,
Amount: refAmount,
Account: refFrom,
Metadata: refMetadataMap,
@ -148,7 +148,7 @@ func TestGetCrossShardOperationComponents(t *testing.T) {
// test nil amount
_, rosettaError = getCrossShardOperationComponents(&types.Operation{
Type: common.CrossShardTransferOperation,
Type: common.CrossShardTransferNativeOperation,
Amount: nil,
Account: refFrom,
Metadata: refMetadataMap,
@ -159,10 +159,10 @@ func TestGetCrossShardOperationComponents(t *testing.T) {
// test positive amount
_, rosettaError = getCrossShardOperationComponents(&types.Operation{
Type: common.CrossShardTransferOperation,
Type: common.CrossShardTransferNativeOperation,
Amount: &types.Amount{
Value: "12000",
Currency: &common.Currency,
Currency: &common.NativeCurrency,
},
Account: refFrom,
Metadata: refMetadataMap,
@ -173,7 +173,7 @@ func TestGetCrossShardOperationComponents(t *testing.T) {
// test different/unsupported currency
_, rosettaError = getCrossShardOperationComponents(&types.Operation{
Type: common.CrossShardTransferOperation,
Type: common.CrossShardTransferNativeOperation,
Amount: &types.Amount{
Value: "-12000",
Currency: &types.Currency{
@ -190,7 +190,7 @@ func TestGetCrossShardOperationComponents(t *testing.T) {
// test nil account
_, rosettaError = getCrossShardOperationComponents(&types.Operation{
Type: common.CrossShardTransferOperation,
Type: common.CrossShardTransferNativeOperation,
Amount: refAmount,
Account: nil,
Metadata: refMetadataMap,
@ -201,7 +201,7 @@ func TestGetCrossShardOperationComponents(t *testing.T) {
// test no metadata
_, rosettaError = getCrossShardOperationComponents(&types.Operation{
Type: common.CrossShardTransferOperation,
Type: common.CrossShardTransferNativeOperation,
Amount: refAmount,
Account: refFrom,
})
@ -224,7 +224,7 @@ func TestGetCrossShardOperationComponents(t *testing.T) {
t.Fatal(err)
}
_, rosettaError = getCrossShardOperationComponents(&types.Operation{
Type: common.CrossShardTransferOperation,
Type: common.CrossShardTransferNativeOperation,
Amount: refAmount,
Account: refFrom,
Metadata: badMetadataMap,
@ -243,11 +243,11 @@ func TestGetCrossShardOperationComponents(t *testing.T) {
func TestGetTransferOperationComponents(t *testing.T) {
refFromAmount := &types.Amount{
Value: "-12000",
Currency: &common.Currency,
Currency: &common.NativeCurrency,
}
refToAmount := &types.Amount{
Value: "12000",
Currency: &common.Currency,
Currency: &common.NativeCurrency,
}
refFromKey := internalCommon.MustGeneratePrivateKey()
refFrom, rosettaError := newAccountIdentifier(crypto.PubkeyToAddress(refFromKey.PublicKey))
@ -266,7 +266,7 @@ func TestGetTransferOperationComponents(t *testing.T) {
OperationIdentifier: &types.OperationIdentifier{
Index: 0,
},
Type: common.TransferOperation,
Type: common.TransferNativeOperation,
Amount: refFromAmount,
Account: refFrom,
},
@ -279,7 +279,7 @@ func TestGetTransferOperationComponents(t *testing.T) {
Index: 0,
},
},
Type: common.TransferOperation,
Type: common.TransferNativeOperation,
Amount: refToAmount,
Account: refTo,
},
@ -345,20 +345,20 @@ func TestGetTransferOperationComponents(t *testing.T) {
// test invalid operation
refOperations[0].Type = common.ExpendGasOperation
refOperations[1].Type = common.TransferOperation
refOperations[1].Type = common.TransferNativeOperation
_, rosettaError = getTransferOperationComponents(refOperations)
if rosettaError == nil {
t.Error("expected error")
}
// test invalid operation sender
refOperations[0].Type = common.TransferOperation
refOperations[0].Type = common.TransferNativeOperation
refOperations[1].Type = common.ExpendGasOperation
_, rosettaError = getTransferOperationComponents(refOperations)
if rosettaError == nil {
t.Error("expected error")
}
refOperations[1].Type = common.TransferOperation
refOperations[1].Type = common.TransferNativeOperation
// test nil amount
refOperations[0].Amount = nil
@ -385,7 +385,7 @@ func TestGetTransferOperationComponents(t *testing.T) {
refOperations[0].Account = refFrom
refOperations[1].Amount = &types.Amount{
Value: "0",
Currency: &common.Currency,
Currency: &common.NativeCurrency,
}
refOperations[1].Account = refTo
_, rosettaError = getTransferOperationComponents(refOperations)
@ -396,7 +396,7 @@ func TestGetTransferOperationComponents(t *testing.T) {
// test uneven amount sender
refOperations[0].Amount = &types.Amount{
Value: "0",
Currency: &common.Currency,
Currency: &common.NativeCurrency,
}
refOperations[0].Account = refFrom
refOperations[1].Amount = refToAmount
@ -439,7 +439,7 @@ func TestGetTransferOperationComponents(t *testing.T) {
if rosettaError == nil {
t.Error("expected error")
}
refOperations[0].Amount.Currency = &common.Currency
refOperations[0].Amount.Currency = &common.NativeCurrency
// test invalid currency sender
refOperations[0].Amount = refFromAmount
@ -454,7 +454,7 @@ func TestGetTransferOperationComponents(t *testing.T) {
if rosettaError == nil {
t.Error("expected error")
}
refOperations[1].Amount.Currency = &common.Currency
refOperations[1].Amount.Currency = &common.NativeCurrency
// test invalid related operation
refOperations[1].RelatedOperations[0].Index = 2
@ -493,11 +493,11 @@ func TestGetTransferOperationComponents(t *testing.T) {
func TestGetOperationComponents(t *testing.T) {
refFromAmount := &types.Amount{
Value: "-12000",
Currency: &common.Currency,
Currency: &common.NativeCurrency,
}
refToAmount := &types.Amount{
Value: "12000",
Currency: &common.Currency,
Currency: &common.NativeCurrency,
}
refFromKey := internalCommon.MustGeneratePrivateKey()
refFrom, rosettaError := newAccountIdentifier(crypto.PubkeyToAddress(refFromKey.PublicKey))
@ -517,7 +517,7 @@ func TestGetOperationComponents(t *testing.T) {
OperationIdentifier: &types.OperationIdentifier{
Index: 0,
},
Type: common.TransferOperation,
Type: common.TransferNativeOperation,
Amount: refFromAmount,
Account: refFrom,
},
@ -530,7 +530,7 @@ func TestGetOperationComponents(t *testing.T) {
Index: 0,
},
},
Type: common.TransferOperation,
Type: common.TransferNativeOperation,
Amount: refToAmount,
Account: refTo,
},
@ -551,7 +551,7 @@ func TestGetOperationComponents(t *testing.T) {
}
_, rosettaError = GetOperationComponents([]*types.Operation{
{
Type: common.CrossShardTransferOperation,
Type: common.CrossShardTransferNativeOperation,
Amount: refFromAmount,
Account: refFrom,
Metadata: refMetadataMap,

@ -0,0 +1,594 @@
package services
import (
"fmt"
"math/big"
"reflect"
"testing"
"github.com/coinbase/rosetta-sdk-go/types"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
hmytypes "github.com/harmony-one/harmony/core/types"
"github.com/harmony-one/harmony/internal/params"
"github.com/harmony-one/harmony/rosetta/common"
"github.com/harmony-one/harmony/staking"
stakingTypes "github.com/harmony-one/harmony/staking/types"
"github.com/harmony-one/harmony/test/helpers"
)
func TestGetStakingOperationsFromCreateValidator(t *testing.T) {
gasLimit := uint64(1e18)
createValidatorTxDescription := stakingTypes.Description{
Name: "SuperHero",
Identity: "YouWouldNotKnow",
Website: "Secret Website",
SecurityContact: "LicenseToKill",
Details: "blah blah blah",
}
tx, err := helpers.CreateTestStakingTransaction(func() (stakingTypes.Directive, interface{}) {
fromKey, _ := crypto.GenerateKey()
return stakingTypes.DirectiveCreateValidator, stakingTypes.CreateValidator{
Description: createValidatorTxDescription,
MinSelfDelegation: tenOnes,
MaxTotalDelegation: twelveOnes,
ValidatorAddress: crypto.PubkeyToAddress(fromKey.PublicKey),
Amount: tenOnes,
}
}, nil, 0, gasLimit, gasPrice)
if err != nil {
t.Fatal(err.Error())
}
metadata, err := helpers.GetMessageFromStakingTx(tx)
if err != nil {
t.Fatal(err.Error())
}
senderAddr, err := tx.SenderAddress()
if err != nil {
t.Fatal(err.Error())
}
senderAccID, rosettaError := newAccountIdentifier(senderAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
gasUsed := uint64(1e5)
gasFee := new(big.Int).Mul(gasPrice, big.NewInt(int64(gasUsed)))
receipt := &hmytypes.Receipt{
Status: hmytypes.ReceiptStatusSuccessful, // Failed staking transaction are never saved on-chain
GasUsed: gasUsed,
}
refOperations := newNativeOperations(gasFee, senderAccID)
refOperations = append(refOperations, &types.Operation{
OperationIdentifier: &types.OperationIdentifier{Index: 1},
RelatedOperations: []*types.OperationIdentifier{
{Index: 0},
},
Type: tx.StakingType().String(),
Status: common.SuccessOperationStatus.Status,
Account: senderAccID,
Amount: &types.Amount{
Value: negativeBigValue(tenOnes),
Currency: &common.NativeCurrency,
},
Metadata: metadata,
})
operations, rosettaError := GetOperationsFromStakingTransaction(tx, receipt)
if rosettaError != nil {
t.Fatal(rosettaError)
}
if !reflect.DeepEqual(operations, refOperations) {
t.Errorf("Expected operations to be %v not %v", refOperations, operations)
}
if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil {
t.Error(err)
}
}
func TestGetStakingOperationsFromDelegate(t *testing.T) {
gasLimit := uint64(1e18)
senderKey, err := crypto.GenerateKey()
if err != nil {
t.Fatalf(err.Error())
}
senderAddr := crypto.PubkeyToAddress(senderKey.PublicKey)
validatorKey, err := crypto.GenerateKey()
if err != nil {
t.Fatalf(err.Error())
}
validatorAddr := crypto.PubkeyToAddress(validatorKey.PublicKey)
tx, err := helpers.CreateTestStakingTransaction(func() (stakingTypes.Directive, interface{}) {
return stakingTypes.DirectiveDelegate, stakingTypes.Delegate{
DelegatorAddress: senderAddr,
ValidatorAddress: validatorAddr,
Amount: tenOnes,
}
}, senderKey, 0, gasLimit, gasPrice)
if err != nil {
t.Fatal(err.Error())
}
metadata, err := helpers.GetMessageFromStakingTx(tx)
if err != nil {
t.Fatal(err.Error())
}
senderAccID, rosettaError := newAccountIdentifier(senderAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
gasUsed := uint64(1e5)
gasFee := new(big.Int).Mul(gasPrice, big.NewInt(int64(gasUsed)))
receipt := &hmytypes.Receipt{
Status: hmytypes.ReceiptStatusSuccessful, // Failed staking transaction are never saved on-chain
GasUsed: gasUsed,
}
refOperations := newNativeOperations(gasFee, senderAccID)
refOperations = append(refOperations, &types.Operation{
OperationIdentifier: &types.OperationIdentifier{Index: 1},
RelatedOperations: []*types.OperationIdentifier{
{Index: 0},
},
Type: tx.StakingType().String(),
Status: common.SuccessOperationStatus.Status,
Account: senderAccID,
Amount: &types.Amount{
Value: negativeBigValue(tenOnes),
Currency: &common.NativeCurrency,
},
Metadata: metadata,
})
operations, rosettaError := GetOperationsFromStakingTransaction(tx, receipt)
if rosettaError != nil {
t.Fatal(rosettaError)
}
if !reflect.DeepEqual(operations, refOperations) {
t.Errorf("Expected operations to be %v not %v", refOperations, operations)
}
if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil {
t.Error(err)
}
}
func TestGetStakingOperationsFromUndelegate(t *testing.T) {
gasLimit := uint64(1e18)
senderKey, err := crypto.GenerateKey()
if err != nil {
t.Fatalf(err.Error())
}
senderAddr := crypto.PubkeyToAddress(senderKey.PublicKey)
validatorKey, err := crypto.GenerateKey()
if err != nil {
t.Fatalf(err.Error())
}
validatorAddr := crypto.PubkeyToAddress(validatorKey.PublicKey)
tx, err := helpers.CreateTestStakingTransaction(func() (stakingTypes.Directive, interface{}) {
return stakingTypes.DirectiveUndelegate, stakingTypes.Undelegate{
DelegatorAddress: senderAddr,
ValidatorAddress: validatorAddr,
Amount: tenOnes,
}
}, senderKey, 0, gasLimit, gasPrice)
if err != nil {
t.Fatal(err.Error())
}
metadata, err := helpers.GetMessageFromStakingTx(tx)
if err != nil {
t.Fatal(err.Error())
}
senderAccID, rosettaError := newAccountIdentifier(senderAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
gasUsed := uint64(1e5)
gasFee := new(big.Int).Mul(gasPrice, big.NewInt(int64(gasUsed)))
receipt := &hmytypes.Receipt{
Status: hmytypes.ReceiptStatusSuccessful, // Failed staking transaction are never saved on-chain
GasUsed: gasUsed,
}
refOperations := newNativeOperations(gasFee, senderAccID)
refOperations = append(refOperations, &types.Operation{
OperationIdentifier: &types.OperationIdentifier{Index: 1},
RelatedOperations: []*types.OperationIdentifier{
{Index: 0},
},
Type: tx.StakingType().String(),
Status: common.SuccessOperationStatus.Status,
Account: senderAccID,
Amount: &types.Amount{
Value: fmt.Sprintf("0"),
Currency: &common.NativeCurrency,
},
Metadata: metadata,
})
operations, rosettaError := GetOperationsFromStakingTransaction(tx, receipt)
if rosettaError != nil {
t.Fatal(rosettaError)
}
if !reflect.DeepEqual(operations, refOperations) {
t.Errorf("Expected operations to be %v not %v", refOperations, operations)
}
if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil {
t.Error(err)
}
}
func TestGetStakingOperationsFromCollectRewards(t *testing.T) {
gasLimit := uint64(1e18)
senderKey, err := crypto.GenerateKey()
if err != nil {
t.Fatalf(err.Error())
}
senderAddr := crypto.PubkeyToAddress(senderKey.PublicKey)
tx, err := helpers.CreateTestStakingTransaction(func() (stakingTypes.Directive, interface{}) {
return stakingTypes.DirectiveCollectRewards, stakingTypes.CollectRewards{
DelegatorAddress: senderAddr,
}
}, senderKey, 0, gasLimit, gasPrice)
if err != nil {
t.Fatal(err.Error())
}
metadata, err := helpers.GetMessageFromStakingTx(tx)
if err != nil {
t.Fatal(err.Error())
}
senderAccID, rosettaError := newAccountIdentifier(senderAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
gasUsed := uint64(1e5)
gasFee := new(big.Int).Mul(gasPrice, big.NewInt(int64(gasUsed)))
receipt := &hmytypes.Receipt{
Status: hmytypes.ReceiptStatusSuccessful, // Failed staking transaction are never saved on-chain
GasUsed: gasUsed,
Logs: []*hmytypes.Log{
{
Address: senderAddr,
Topics: []ethcommon.Hash{staking.CollectRewardsTopic},
Data: tenOnes.Bytes(),
},
},
}
refOperations := newNativeOperations(gasFee, senderAccID)
refOperations = append(refOperations, &types.Operation{
OperationIdentifier: &types.OperationIdentifier{Index: 1},
RelatedOperations: []*types.OperationIdentifier{
{Index: 0},
},
Type: tx.StakingType().String(),
Status: common.SuccessOperationStatus.Status,
Account: senderAccID,
Amount: &types.Amount{
Value: fmt.Sprintf("%v", tenOnes.Uint64()),
Currency: &common.NativeCurrency,
},
Metadata: metadata,
})
operations, rosettaError := GetOperationsFromStakingTransaction(tx, receipt)
if rosettaError != nil {
t.Fatal(rosettaError)
}
if !reflect.DeepEqual(operations, refOperations) {
t.Errorf("Expected operations to be %v not %v", refOperations, operations)
}
if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil {
t.Error(err)
}
}
func TestGetStakingOperationsFromEditValidator(t *testing.T) {
gasLimit := uint64(1e18)
senderKey, err := crypto.GenerateKey()
if err != nil {
t.Fatalf(err.Error())
}
senderAddr := crypto.PubkeyToAddress(senderKey.PublicKey)
tx, err := helpers.CreateTestStakingTransaction(func() (stakingTypes.Directive, interface{}) {
return stakingTypes.DirectiveEditValidator, stakingTypes.EditValidator{
ValidatorAddress: senderAddr,
}
}, senderKey, 0, gasLimit, gasPrice)
if err != nil {
t.Fatal(err.Error())
}
metadata, err := helpers.GetMessageFromStakingTx(tx)
if err != nil {
t.Fatal(err.Error())
}
senderAccID, rosettaError := newAccountIdentifier(senderAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
gasUsed := uint64(1e5)
gasFee := new(big.Int).Mul(gasPrice, big.NewInt(int64(gasUsed)))
receipt := &hmytypes.Receipt{
Status: hmytypes.ReceiptStatusSuccessful, // Failed staking transaction are never saved on-chain
GasUsed: gasUsed,
}
refOperations := newNativeOperations(gasFee, senderAccID)
refOperations = append(refOperations, &types.Operation{
OperationIdentifier: &types.OperationIdentifier{Index: 1},
RelatedOperations: []*types.OperationIdentifier{
{Index: 0},
},
Type: tx.StakingType().String(),
Status: common.SuccessOperationStatus.Status,
Account: senderAccID,
Amount: &types.Amount{
Value: fmt.Sprintf("0"),
Currency: &common.NativeCurrency,
},
Metadata: metadata,
})
operations, rosettaError := GetOperationsFromStakingTransaction(tx, receipt)
if rosettaError != nil {
t.Fatal(rosettaError)
}
if !reflect.DeepEqual(operations, refOperations) {
t.Errorf("Expected operations to be %v not %v", refOperations, operations)
}
if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil {
t.Error(err)
}
}
func TestNewTransferNativeOperations(t *testing.T) {
signer := hmytypes.NewEIP155Signer(params.TestChainConfig.ChainID)
tx, err := helpers.CreateTestTransaction(
signer, 0, 0, 0, 1e18, gasPrice, big.NewInt(1), []byte("test"),
)
if err != nil {
t.Fatal(err.Error())
}
senderAddr, err := tx.SenderAddress()
if err != nil {
t.Fatal(err.Error())
}
senderAccID, rosettaError := newAccountIdentifier(senderAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
receiverAccID, rosettaError := newAccountIdentifier(*tx.To())
if rosettaError != nil {
t.Fatal(rosettaError)
}
startingOpID := &types.OperationIdentifier{}
// Test failed 'contract' transaction
refOperations := []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: startingOpID.Index + 1,
},
RelatedOperations: []*types.OperationIdentifier{
{
Index: startingOpID.Index,
},
},
Type: common.TransferNativeOperation,
Status: common.ContractFailureOperationStatus.Status,
Account: senderAccID,
Amount: &types.Amount{
Value: negativeBigValue(tx.Value()),
Currency: &common.NativeCurrency,
},
},
{
OperationIdentifier: &types.OperationIdentifier{
Index: startingOpID.Index + 2,
},
RelatedOperations: []*types.OperationIdentifier{
{
Index: startingOpID.Index + 1,
},
},
Type: common.TransferNativeOperation,
Status: common.ContractFailureOperationStatus.Status,
Account: receiverAccID,
Amount: &types.Amount{
Value: fmt.Sprintf("%v", tx.Value().Uint64()),
Currency: &common.NativeCurrency,
},
},
}
receipt := &hmytypes.Receipt{
Status: hmytypes.ReceiptStatusFailed,
}
operations, rosettaError := newTransferNativeOperations(startingOpID, tx, receipt, senderAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
if !reflect.DeepEqual(operations, refOperations) {
t.Errorf("Expected operations to be %v not %v", refOperations, operations)
}
if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil {
t.Error(err)
}
// Test successful plain / contract transaction
refOperations[0].Status = common.SuccessOperationStatus.Status
refOperations[1].Status = common.SuccessOperationStatus.Status
receipt.Status = hmytypes.ReceiptStatusSuccessful
operations, rosettaError = newTransferNativeOperations(startingOpID, tx, receipt, senderAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
if !reflect.DeepEqual(operations, refOperations) {
t.Errorf("Expected operations to be %v not %v", refOperations, operations)
}
if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil {
t.Error(err)
}
}
func TestNewCrossShardSenderTransferNativeOperations(t *testing.T) {
signer := hmytypes.NewEIP155Signer(params.TestChainConfig.ChainID)
tx, err := helpers.CreateTestTransaction(
signer, 0, 1, 0, 1e18, gasPrice, big.NewInt(1), []byte("data-does-nothing"),
)
if err != nil {
t.Fatal(err.Error())
}
senderAddr, err := tx.SenderAddress()
if err != nil {
t.Fatal(err.Error())
}
senderAccID, rosettaError := newAccountIdentifier(senderAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
startingOpID := &types.OperationIdentifier{}
receiverAccID, rosettaError := newAccountIdentifier(*tx.To())
if rosettaError != nil {
t.Error(rosettaError)
}
metadata, err := types.MarshalMap(common.CrossShardTransactionOperationMetadata{
From: senderAccID,
To: receiverAccID,
})
if err != nil {
t.Fatal(err)
}
refOperations := []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: startingOpID.Index + 1,
},
RelatedOperations: []*types.OperationIdentifier{
startingOpID,
},
Type: common.CrossShardTransferNativeOperation,
Status: common.SuccessOperationStatus.Status,
Account: senderAccID,
Amount: &types.Amount{
Value: negativeBigValue(tx.Value()),
Currency: &common.NativeCurrency,
},
Metadata: metadata,
},
}
operations, rosettaError := newCrossShardSenderTransferNativeOperations(startingOpID, tx, senderAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
if !reflect.DeepEqual(operations, refOperations) {
t.Errorf("Expected operations to be %v not %v", refOperations, operations)
}
if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil {
t.Error(err)
}
}
func TestNewContractCreationNativeOperations(t *testing.T) {
dummyContractKey, err := crypto.GenerateKey()
if err != nil {
t.Fatalf(err.Error())
}
chainID := params.TestChainConfig.ChainID
signer := hmytypes.NewEIP155Signer(chainID)
tx, err := helpers.CreateTestContractCreationTransaction(
signer, 0, 0, 1e18, gasPrice, big.NewInt(0), []byte("test"),
)
if err != nil {
t.Fatal(err.Error())
}
senderAddr, err := tx.SenderAddress()
if err != nil {
t.Fatal(err.Error())
}
senderAccID, rosettaError := newAccountIdentifier(senderAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
startingOpID := &types.OperationIdentifier{}
// Test failed contract creation
contractAddr := crypto.PubkeyToAddress(dummyContractKey.PublicKey)
contractAddressID, rosettaError := newAccountIdentifier(contractAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
refOperations := []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: startingOpID.Index + 1,
},
RelatedOperations: []*types.OperationIdentifier{
startingOpID,
},
Type: common.ContractCreationOperation,
Status: common.ContractFailureOperationStatus.Status,
Account: senderAccID,
Amount: &types.Amount{
Value: negativeBigValue(tx.Value()),
Currency: &common.NativeCurrency,
},
Metadata: map[string]interface{}{
"contract_address": contractAddressID,
},
},
}
receipt := &hmytypes.Receipt{
Status: hmytypes.ReceiptStatusFailed,
ContractAddress: contractAddr,
}
operations, rosettaError := newContractCreationNativeOperations(startingOpID, tx, receipt, senderAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
if !reflect.DeepEqual(operations, refOperations) {
t.Errorf("Expected operations to be %v not %v", refOperations, operations)
}
if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil {
t.Error(err)
}
// Test successful contract creation
refOperations[0].Status = common.SuccessOperationStatus.Status
receipt.Status = hmytypes.ReceiptStatusSuccessful // Indicate successful tx
operations, rosettaError = newContractCreationNativeOperations(startingOpID, tx, receipt, senderAddr)
if rosettaError != nil {
t.Fatal(rosettaError)
}
if !reflect.DeepEqual(operations, refOperations) {
t.Errorf("Expected operations to be %v not %v", refOperations, operations)
}
if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil {
t.Error(err)
}
}
func TestNewNativeOperations(t *testing.T) {
accountID := &types.AccountIdentifier{
Address: "test-address",
}
gasFee := big.NewInt(int64(1e18))
amount := &types.Amount{
Value: negativeBigValue(gasFee),
Currency: &common.NativeCurrency,
}
ops := newNativeOperations(gasFee, accountID)
if len(ops) != 1 {
t.Fatalf("Expected new operations to be of length 1")
}
if !reflect.DeepEqual(ops[0].Account, accountID) {
t.Errorf("Expected account ID to be %v not %v", accountID, ops[0].OperationIdentifier)
}
if !reflect.DeepEqual(ops[0].Amount, amount) {
t.Errorf("Expected amount to be %v not %v", amount, ops[0].Amount)
}
if ops[0].Type != common.ExpendGasOperation {
t.Errorf("Expected operation to be %v not %v", common.ExpendGasOperation, ops[0].Type)
}
if ops[0].OperationIdentifier.Index != 0 {
t.Errorf("Expected operational ID to be of index 0")
}
if ops[0].Status != common.SuccessOperationStatus.Status {
t.Errorf("Expected operation status to be %v", common.SuccessOperationStatus.Status)
}
}

@ -0,0 +1,81 @@
package helpers
import (
"crypto/ecdsa"
"math/big"
"github.com/coinbase/rosetta-sdk-go/types"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
hmytypes "github.com/harmony-one/harmony/core/types"
rpcV2 "github.com/harmony-one/harmony/rpc/v2"
stakingTypes "github.com/harmony-one/harmony/staking/types"
)
// CreateTestStakingTransaction creates a pre-signed staking transaction
func CreateTestStakingTransaction(
payloadMaker func() (stakingTypes.Directive, interface{}), key *ecdsa.PrivateKey,
nonce, gasLimit uint64, gasPrice *big.Int,
) (*stakingTypes.StakingTransaction, error) {
tx, err := stakingTypes.NewStakingTransaction(nonce, gasLimit, gasPrice, payloadMaker)
if err != nil {
return nil, err
}
if key == nil {
key, err = crypto.GenerateKey()
if err != nil {
return nil, err
}
}
// Staking transactions are always post EIP155 epoch
return stakingTypes.Sign(tx, stakingTypes.NewEIP155Signer(tx.ChainID()), key)
}
// GetMessageFromStakingTx gets the staking message, as seen by the rpc layer
func GetMessageFromStakingTx(tx *stakingTypes.StakingTransaction) (map[string]interface{}, error) {
rpcStakingTx, err := rpcV2.NewStakingTransaction(tx, ethcommon.Hash{}, 0, 0, 0)
if err != nil {
return nil, err
}
return types.MarshalMap(rpcStakingTx.Msg)
}
// CreateTestTransaction creates a pre-signed transaction
func CreateTestTransaction(
signer hmytypes.Signer, fromShard, toShard uint32, nonce, gasLimit uint64,
gasPrice, amount *big.Int, data []byte,
) (*hmytypes.Transaction, error) {
fromKey, err := crypto.GenerateKey()
if err != nil {
return nil, err
}
toKey, err := crypto.GenerateKey()
if err != nil {
return nil, err
}
toAddr := crypto.PubkeyToAddress(toKey.PublicKey)
var tx *hmytypes.Transaction
if fromShard != toShard {
tx = hmytypes.NewCrossShardTransaction(
nonce, &toAddr, fromShard, toShard, amount, gasLimit, gasPrice, data,
)
} else {
tx = hmytypes.NewTransaction(
nonce, toAddr, fromShard, amount, gasLimit, gasPrice, data,
)
}
return hmytypes.SignTx(tx, signer, fromKey)
}
// CreateTestContractCreationTransaction creates a pre-signed contract creation transaction
func CreateTestContractCreationTransaction(
signer hmytypes.Signer, shard uint32, nonce, gasLimit uint64, gasPrice, amount *big.Int, data []byte,
) (*hmytypes.Transaction, error) {
fromKey, err := crypto.GenerateKey()
if err != nil {
return nil, err
}
tx := hmytypes.NewContractCreation(nonce, shard, amount, gasLimit, gasPrice, data)
return hmytypes.SignTx(tx, signer, fromKey)
}
Loading…
Cancel
Save