|
|
|
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/hmy"
|
|
|
|
"github.com/harmony-one/harmony/internal/params"
|
|
|
|
"github.com/harmony-one/harmony/rosetta/common"
|
|
|
|
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, []byte{})
|
|
|
|
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, []byte{})
|
|
|
|
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.NativeTransferOperation {
|
|
|
|
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)
|
|
|
|
testBlkHash := ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238")
|
|
|
|
testRewards := hmy.PreStakingBlockRewards{
|
|
|
|
testAddr: big.NewInt(1),
|
|
|
|
}
|
|
|
|
refTxID := getSpecialCaseTransactionIdentifier(testBlkHash, testAddr, SpecialPreStakingRewardTxID)
|
|
|
|
tx, rosettaError := FormatPreStakingRewardTransaction(refTxID, testRewards, 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")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestFormatPreStakingRewardTransactionFail(t *testing.T) {
|
|
|
|
testKey, err := crypto.GenerateKey()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
|
|
|
|
testBlkHash := ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238")
|
|
|
|
testRewards := hmy.PreStakingBlockRewards{
|
|
|
|
FormatDefaultSenderAddress: big.NewInt(1),
|
|
|
|
}
|
|
|
|
testTxID := getSpecialCaseTransactionIdentifier(testBlkHash, testAddr, SpecialPreStakingRewardTxID)
|
|
|
|
_, rosettaError := FormatPreStakingRewardTransaction(testTxID, testRewards, testAddr)
|
|
|
|
if rosettaError == nil {
|
|
|
|
t.Fatal("expected rosetta error")
|
|
|
|
}
|
|
|
|
if common.TransactionNotFoundError.Code != rosettaError.Code {
|
|
|
|
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, []byte{})
|
|
|
|
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.NativeCrossShardTransferOperation {
|
|
|
|
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.NativeCrossShardTransferOperation,
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|