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