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
parent
a2266d0d7a
commit
b088d6dc77
@ -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) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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
@ -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, |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
@ -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…
Reference in new issue