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.NativeTransferOperation, 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.NativeTransferOperation, 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.NativeCrossShardTransferOperation, 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, }, }, { OperationIdentifier: &types.OperationIdentifier{ Index: startingOpID.Index + 2, }, RelatedOperations: []*types.OperationIdentifier{ { Index: startingOpID.Index + 1, }, }, Type: common.ContractCreationOperation, Status: common.ContractFailureOperationStatus.Status, Account: contractAddressID, Amount: &types.Amount{ Value: tx.Value().String(), Currency: &common.NativeCurrency, }, }, } 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 refOperations[1].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) } }