package services import ( "crypto/ecdsa" "fmt" "math/big" "reflect" "testing" "github.com/coinbase/rosetta-sdk-go/types" "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" 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 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) } }