diff --git a/rosetta/common/operations.go b/rosetta/common/operations.go index 82fbf5a44..7135ef169 100644 --- a/rosetta/common/operations.go +++ b/rosetta/common/operations.go @@ -24,15 +24,15 @@ const ( // ContractCreationOperation is an operation that only affects the native currency. ContractCreationOperation = "ContractCreation" - // GenesisFundsOperation is a special operation for genesis block only. + // GenesisFundsOperation is a side effect operation for genesis block only. // Note that no transaction can be constructed with this operation. GenesisFundsOperation = "Genesis" - // PreStakingBlockRewardOperation is a special operation for pre-staking era only. + // PreStakingBlockRewardOperation is a side effect operation for pre-staking era only. // Note that no transaction can be constructed with this operation. PreStakingBlockRewardOperation = "PreStakingBlockReward" - // UndelegationPayoutOperation is a special operation for committee election block only. + // UndelegationPayoutOperation is a side effect operation for committee election block only. // Note that no transaction can be constructed with this operation. UndelegationPayoutOperation = "UndelegationPayout" ) diff --git a/rosetta/services/block.go b/rosetta/services/block.go index 8dcb15f61..0c54f5559 100644 --- a/rosetta/services/block.go +++ b/rosetta/services/block.go @@ -48,31 +48,34 @@ func (s *BlockAPI) Block( return nil, rosettaError } - // Format genesis block if it is requested. - if blk.Number().Uint64() == 0 { - return s.genesisBlock(ctx, request, blk) - } - currBlockID = &types.BlockIdentifier{ Index: blk.Number().Int64(), Hash: blk.Hash().String(), } - prevBlock, err := s.hmy.BlockByNumber(ctx, rpc.BlockNumber(blk.Number().Int64()-1).EthBlockNumber()) - if err != nil { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "message": err.Error(), - }) - } - prevBlockID = &types.BlockIdentifier{ - Index: prevBlock.Number().Int64(), - Hash: prevBlock.Hash().String(), + + if blk.NumberU64() == 0 { + prevBlockID = currBlockID + } else { + prevBlock, err := s.hmy.BlockByNumber(ctx, rpc.BlockNumber(blk.Number().Int64()-1).EthBlockNumber()) + if err != nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": err.Error(), + }) + } + prevBlockID = &types.BlockIdentifier{ + Index: prevBlock.Number().Int64(), + Hash: prevBlock.Hash().String(), + } } - // Report undelegation payouts as transactions to fit API. - // Report all transactions here since all undelegation payout amounts are known after fetching payouts. - transactions, rosettaError := s.getAllUndelegationPayoutTransactions(ctx, blk) - if rosettaError != nil { - return nil, rosettaError + // Report any side effect transaction now as it can be computed & cached on block fetch. + transactions := []*types.Transaction{} + if s.containsSideEffectTransaction(ctx, blk) { + tx, rosettaError := s.getSideEffectTransaction(ctx, blk) + if rosettaError != nil { + return nil, rosettaError + } + transactions = append(transactions, tx) } metadata, err := types.MarshalMap(BlockMetadata{ @@ -110,14 +113,6 @@ func (s *BlockAPI) Block( }) } } - // Report pre-staking era block rewards as transactions to fit API. - if !s.hmy.IsStakingEpoch(blk.Epoch()) { - preStakingRewardTxIDs, rosettaError := s.getPreStakingRewardTransactionIdentifiers(ctx, blk) - if rosettaError != nil { - return nil, rosettaError - } - otherTransactions = append(otherTransactions, preStakingRewardTxIDs...) - } return &types.BlockResponse{ Block: responseBlock, @@ -133,17 +128,12 @@ func (s *BlockAPI) BlockTransaction( return nil, err } - // Format genesis block transaction request - if request.BlockIdentifier.Index == 0 { - return s.specialGenesisBlockTransaction(ctx, request) - } - blockHash := ethcommon.HexToHash(request.BlockIdentifier.Hash) txHash := ethcommon.HexToHash(request.TransactionIdentifier.Hash) txInfo, rosettaError := s.getTransactionInfo(ctx, blockHash, txHash) if rosettaError != nil { - // If no transaction info is found, check for special case transactions. - response, rosettaError2 := s.specialBlockTransaction(ctx, request) + // If no transaction info is found, check for side effect case transaction. + response, rosettaError2 := s.sideEffectBlockTransaction(ctx, request) if rosettaError2 != nil && rosettaError2.Code != common.TransactionNotFoundError.Code { return nil, common.NewError(common.TransactionNotFoundError, map[string]interface{}{ "from_error": rosettaError2, diff --git a/rosetta/services/block_side_effect.go b/rosetta/services/block_side_effect.go new file mode 100644 index 000000000..6c0b018eb --- /dev/null +++ b/rosetta/services/block_side_effect.go @@ -0,0 +1,168 @@ +package services + +import ( + "context" + "fmt" + "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" + 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/shard" +) + +// containsSideEffectTransaction checks if the block contains any side effect operations to report. +func (s *BlockAPI) containsSideEffectTransaction( + ctx context.Context, blk *hmytypes.Block, +) bool { + if blk == nil { + return false + } + return s.hmy.IsCommitteeSelectionBlock(blk.Header()) || !s.hmy.IsStakingEpoch(blk.Epoch()) || blk.NumberU64() == 0 +} + +const ( + // SideEffectTransactionSuffix is use in the transaction identifier for each block that contains + // side-effect operations. + SideEffectTransactionSuffix = "side_effect" + blockHashStrLen = 64 +) + +// getSideEffectTransactionIdentifier fetches 'transaction identifier' for side effect operations +// for a given block. +// Side effects are genesis funds, pre-staking era block rewards, and undelegation payouts. +// Must include block hash to guarantee uniqueness of tx identifiers. +func getSideEffectTransactionIdentifier( + blockHash ethcommon.Hash, +) *types.TransactionIdentifier { + return &types.TransactionIdentifier{ + Hash: fmt.Sprintf("%v_%v", + blockHash.String(), SideEffectTransactionSuffix, + ), + } +} + +// unpackSideEffectTransactionIdentifier returns the blockHash if the txID is formatted correctly. +func unpackSideEffectTransactionIdentifier( + txID *types.TransactionIdentifier, +) (ethcommon.Hash, *types.Error) { + hash := txID.Hash + hash = strings.TrimPrefix(hash, "0x") + hash = strings.TrimPrefix(hash, "0X") + if len(hash) < blockHashStrLen || string(hash[blockHashStrLen]) != "_" || + hash[blockHashStrLen+1:] != SideEffectTransactionSuffix { + return ethcommon.Hash{}, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": "unknown side effect transaction ID format", + }) + } + blkHash := ethcommon.HexToHash(hash[:blockHashStrLen]) + return blkHash, nil +} + +// getSideEffectTransaction returns the side effect transaction for a block if said block has one. +// Side effects to reports are: genesis funds, undelegation payouts, permissioned-phase block rewards. +func (s *BlockAPI) getSideEffectTransaction( + ctx context.Context, blk *hmytypes.Block, +) (*types.Transaction, *types.Error) { + if !s.containsSideEffectTransaction(ctx, blk) { + return nil, common.NewError(common.TransactionNotFoundError, map[string]interface{}{ + "message": "no side effect transaction found for given block", + }) + } + + var startingOpIndex *int64 + txOperations := []*types.Operation{} + updateStartingOpIndex := func(newOperations []*types.Operation) { + if len(newOperations) > 0 { + index := newOperations[len(newOperations)-1].OperationIdentifier.Index + 1 + startingOpIndex = &index + } + txOperations = append(txOperations, newOperations...) + } + + // Handle genesis funds + if blk.NumberU64() == 0 { + ops, rosettaError := GetSideEffectOperationsFromGenesisSpec(getGenesisSpec(s.hmy.ShardID), startingOpIndex) + if rosettaError != nil { + return nil, rosettaError + } + updateStartingOpIndex(ops) + } + // Handle block rewards for epoch < staking epoch (permissioned-phase block rewards) + // Note that block rewards don't start until the second block. + if !s.hmy.IsStakingEpoch(blk.Epoch()) && blk.NumberU64() > 1 { + rewards, err := s.hmy.GetPreStakingBlockRewards(ctx, blk) + if err != nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": err.Error(), + }) + } + ops, rosettaError := GetSideEffectOperationsFromPreStakingRewards(rewards, startingOpIndex) + if rosettaError != nil { + return nil, rosettaError + } + updateStartingOpIndex(ops) + } + // Handle undelegation payout + if s.hmy.IsCommitteeSelectionBlock(blk.Header()) && s.hmy.IsPreStakingEpoch(blk.Epoch()) { + payouts, err := s.hmy.GetUndelegationPayouts(ctx, blk.Epoch()) + if err != nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": err.Error(), + }) + } + ops, rosettaError := GetSideEffectOperationsFromUndelegationPayouts(payouts, startingOpIndex) + if rosettaError != nil { + return nil, rosettaError + } + updateStartingOpIndex(ops) + } + + return &types.Transaction{ + TransactionIdentifier: getSideEffectTransactionIdentifier(blk.Hash()), + Operations: txOperations, + }, nil +} + +// sideEffectBlockTransaction is a formatter for side effect transactions +func (s *BlockAPI) sideEffectBlockTransaction( + ctx context.Context, request *types.BlockTransactionRequest, +) (*types.BlockTransactionResponse, *types.Error) { + // If no transaction info is found, check for special case transactions. + blk, rosettaError := getBlock(ctx, s.hmy, &types.PartialBlockIdentifier{Index: &request.BlockIdentifier.Index}) + if rosettaError != nil { + return nil, rosettaError + } + blkHash, rosettaError := unpackSideEffectTransactionIdentifier( + request.TransactionIdentifier, + ) + if rosettaError != nil { + return nil, rosettaError + } + if blkHash.String() != blk.Hash().String() { + return nil, common.NewError(common.TransactionNotFoundError, map[string]interface{}{ + "message": fmt.Sprintf("side effect transaction is not for block: %v", blk.NumberU64()), + }) + } + tx, rosettaError := s.getSideEffectTransaction(ctx, blk) + if rosettaError != nil { + return nil, rosettaError + } + return &types.BlockTransactionResponse{Transaction: tx}, 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) +} diff --git a/rosetta/services/block_side_effect_test.go b/rosetta/services/block_side_effect_test.go new file mode 100644 index 000000000..6e02f6112 --- /dev/null +++ b/rosetta/services/block_side_effect_test.go @@ -0,0 +1,46 @@ +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/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 TestSideEffectTransactionIdentifier(t *testing.T) { + testBlkHash := ethcommon.HexToHash("0x1a06b0378d63bf589282c032f0c85b32827e3a2317c2f992f45d8f07d0caa238") + refTxID := &types.TransactionIdentifier{ + Hash: fmt.Sprintf("%v_%v", testBlkHash.String(), SideEffectTransactionSuffix), + } + specialTxID := getSideEffectTransactionIdentifier(testBlkHash) + if !reflect.DeepEqual(refTxID, specialTxID) { + t.Fatal("invalid for mate for special case TxID") + } + unpackedBlkHash, rosettaError := unpackSideEffectTransactionIdentifier(specialTxID) + if rosettaError != nil { + t.Fatal(rosettaError) + } + if unpackedBlkHash.String() != testBlkHash.String() { + t.Errorf("expected blk hash to be %v not %v", unpackedBlkHash.String(), testBlkHash.String()) + } + + _, rosettaError = unpackSideEffectTransactionIdentifier(&types.TransactionIdentifier{Hash: ""}) + if rosettaError == nil { + t.Fatal("expected rosetta error") + } + if rosettaError.Code != common.CatchAllError.Code { + t.Error("expected error code to be catch call error") + } +} diff --git a/rosetta/services/block_special.go b/rosetta/services/block_special.go deleted file mode 100644 index 4ac2e0de6..000000000 --- a/rosetta/services/block_special.go +++ /dev/null @@ -1,319 +0,0 @@ -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 -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 - } - rewards, err := s.hmy.GetPreStakingBlockRewards(ctx, currBlock) - if err != nil { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "message": err.Error(), - }) - } - txIDs := []*types.TransactionIdentifier{} - for addr := range rewards { - txIDs = append(txIDs, getSpecialCaseTransactionIdentifier( - currBlock.Hash(), addr, 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 := getBlock(ctx, s.hmy, &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 - } - 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(), - ), - }) - } - rewards, err := s.hmy.GetPreStakingBlockRewards(ctx, blk) - if err != nil { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "message": err.Error(), - }) - } - transactions, rosettaError := FormatPreStakingRewardTransaction(txID, rewards, 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) -} diff --git a/rosetta/services/block_special_test.go b/rosetta/services/block_special_test.go deleted file mode 100644 index f813a8424..000000000 --- a/rosetta/services/block_special_test.go +++ /dev/null @@ -1,77 +0,0 @@ -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") - } -} diff --git a/rosetta/services/tx_format.go b/rosetta/services/tx_format.go index 9995e346e..9f32228aa 100644 --- a/rosetta/services/tx_format.go +++ b/rosetta/services/tx_format.go @@ -9,8 +9,6 @@ import ( 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" stakingTypes "github.com/harmony-one/harmony/staking/types" ) @@ -32,7 +30,7 @@ func FormatTransaction( case *stakingTypes.StakingTransaction: isStaking = true stakingTx := tx.(*stakingTypes.StakingTransaction) - operations, rosettaError = GetOperationsFromStakingTransaction(stakingTx, receipt) + operations, rosettaError = GetNativeOperationsFromStakingTransaction(stakingTx, receipt) if rosettaError != nil { return nil, rosettaError } @@ -150,111 +148,6 @@ func FormatCrossShardReceiverTransaction( }, 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, rewards hmy.PreStakingBlockRewards, address ethcommon.Address, -) (*types.Transaction, *types.Error) { - accID, rosettaError := newAccountIdentifier(address) - if rosettaError != nil { - return nil, rosettaError - } - value, ok := rewards[address] - if !ok { - return nil, common.NewError(common.TransactionNotFoundError, map[string]interface{}{ - "message": fmt.Sprintf("%v does not have any rewards for block", - internalCommon.MustAddressToBech32(address)), - }) - } - - 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: value.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" diff --git a/rosetta/services/tx_format_test.go b/rosetta/services/tx_format_test.go index 37ccaff1b..1ef74468f 100644 --- a/rosetta/services/tx_format_test.go +++ b/rosetta/services/tx_format_test.go @@ -8,11 +8,9 @@ import ( "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" @@ -169,137 +167,6 @@ func testFormatPlainTransaction( } } -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, ) { diff --git a/rosetta/services/tx_operation.go b/rosetta/services/tx_operation.go index 8d7f4a28b..a8cdf430f 100644 --- a/rosetta/services/tx_operation.go +++ b/rosetta/services/tx_operation.go @@ -7,7 +7,9 @@ import ( "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" + "github.com/harmony-one/harmony/hmy" "github.com/harmony-one/harmony/internal/utils" "github.com/harmony-one/harmony/rosetta/common" rpcV2 "github.com/harmony-one/harmony/rpc/v2" @@ -32,7 +34,7 @@ func GetNativeOperationsFromTransaction( // 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) + gasOperations := newNativeOperationsWithGas(gasExpended, accountID) // Handle different cases of plain transactions var txOperations []*types.Operation @@ -56,9 +58,9 @@ func GetNativeOperationsFromTransaction( return append(gasOperations, txOperations...), nil } -// GetOperationsFromStakingTransaction for all staking directives +// GetNativeOperationsFromStakingTransaction for all staking directives // Note that only native operations can come from staking transactions. -func GetOperationsFromStakingTransaction( +func GetNativeOperationsFromStakingTransaction( tx *stakingTypes.StakingTransaction, receipt *hmytypes.Receipt, ) ([]*types.Operation, *types.Error) { senderAddress, err := tx.SenderAddress() @@ -72,7 +74,7 @@ func GetOperationsFromStakingTransaction( // 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) + gasOperations := newNativeOperationsWithGas(gasExpended, accountID) // Format staking message for metadata using decimal numbers (hence usage of rpcV2) rpcStakingTx, err := rpcV2.NewStakingTransaction(tx, ethcommon.Hash{}, 0, 0, 0) @@ -125,6 +127,73 @@ func GetOperationsFromStakingTransaction( }), nil } +// GetSideEffectOperationsFromUndelegationPayouts from the given payouts. +// If the startingOperationIndex is provided, all operations will be indexed starting from the given operation index. +func GetSideEffectOperationsFromUndelegationPayouts( + payouts hmy.UndelegationPayouts, startingOperationIndex *int64, +) ([]*types.Operation, *types.Error) { + return getSideEffectOperationsFromValueMap( + payouts, common.UndelegationPayoutOperation, startingOperationIndex, + ) +} + +// GetSideEffectOperationsFromPreStakingRewards from the given rewards. +// If the startingOperationIndex is provided, all operations will be indexed starting from the given operation index. +func GetSideEffectOperationsFromPreStakingRewards( + rewards hmy.PreStakingBlockRewards, startingOperationIndex *int64, +) ([]*types.Operation, *types.Error) { + return getSideEffectOperationsFromValueMap( + rewards, common.PreStakingBlockRewardOperation, startingOperationIndex, + ) +} + +// GetSideEffectOperationsFromGenesisSpec for the given spec. +// If the startingOperationIndex is provided, all operations will be indexed starting from the given operation index. +func GetSideEffectOperationsFromGenesisSpec( + spec *core.Genesis, startingOperationIndex *int64, +) ([]*types.Operation, *types.Error) { + valueMap := map[ethcommon.Address]*big.Int{} + for address, acc := range spec.Alloc { + valueMap[address] = acc.Balance + } + return getSideEffectOperationsFromValueMap( + valueMap, common.GenesisFundsOperation, startingOperationIndex, + ) +} + +// getSideEffectOperationsFromValueMap is a helper for side effect operation construction from a value map. +func getSideEffectOperationsFromValueMap( + valueMap map[ethcommon.Address]*big.Int, opType string, startingOperationIndex *int64, +) ([]*types.Operation, *types.Error) { + var opIndex int64 + operations := []*types.Operation{} + if startingOperationIndex != nil { + opIndex = *startingOperationIndex + } else { + opIndex = 0 + } + for address, value := range valueMap { + accID, rosettaError := newAccountIdentifier(address) + if rosettaError != nil { + return nil, rosettaError + } + operations = append(operations, &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: opIndex, + }, + Type: opType, + Status: common.SuccessOperationStatus.Status, + Account: accID, + Amount: &types.Amount{ + Value: value.String(), + Currency: &common.NativeCurrency, + }, + }) + opIndex++ + } + return operations, nil +} + func getAmountFromCreateValidatorMessage(data []byte) (*types.Amount, *types.Error) { msg, err := stakingTypes.RLPDecodeStakeMsg(data, stakingTypes.DirectiveCreateValidator) if err != nil { @@ -382,9 +451,9 @@ func newContractCreationNativeOperations( }, nil } -// newNativeOperations creates a new operation with the gas fee as the first operation. +// newNativeOperationsWithGas creates a new operation with the gas fee as the first operation. // Note: the gas fee is gasPrice * gasUsed. -func newNativeOperations( +func newNativeOperationsWithGas( gasFeeInATTO *big.Int, accountID *types.AccountIdentifier, ) []*types.Operation { return []*types.Operation{ diff --git a/rosetta/services/tx_operation_test.go b/rosetta/services/tx_operation_test.go index de29462f3..75351f391 100644 --- a/rosetta/services/tx_operation_test.go +++ b/rosetta/services/tx_operation_test.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" hmytypes "github.com/harmony-one/harmony/core/types" + internalCommon "github.com/harmony-one/harmony/internal/common" "github.com/harmony-one/harmony/internal/params" "github.com/harmony-one/harmony/rosetta/common" "github.com/harmony-one/harmony/staking" @@ -59,7 +60,7 @@ func TestGetStakingOperationsFromCreateValidator(t *testing.T) { Status: hmytypes.ReceiptStatusSuccessful, // Failed staking transaction are never saved on-chain GasUsed: gasUsed, } - refOperations := newNativeOperations(gasFee, senderAccID) + refOperations := newNativeOperationsWithGas(gasFee, senderAccID) refOperations = append(refOperations, &types.Operation{ OperationIdentifier: &types.OperationIdentifier{Index: 1}, RelatedOperations: []*types.OperationIdentifier{ @@ -74,7 +75,7 @@ func TestGetStakingOperationsFromCreateValidator(t *testing.T) { }, Metadata: metadata, }) - operations, rosettaError := GetOperationsFromStakingTransaction(tx, receipt) + operations, rosettaError := GetNativeOperationsFromStakingTransaction(tx, receipt) if rosettaError != nil { t.Fatal(rosettaError) } @@ -86,6 +87,74 @@ func TestGetStakingOperationsFromCreateValidator(t *testing.T) { } } +func TestGetSideEffectOperationsFromValueMap(t *testing.T) { + testAcc1 := crypto.PubkeyToAddress(internalCommon.MustGeneratePrivateKey().PublicKey) + testAcc2 := crypto.PubkeyToAddress(internalCommon.MustGeneratePrivateKey().PublicKey) + testAmount1 := big.NewInt(12000) + testAmount2 := big.NewInt(10000) + testPayouts := map[ethcommon.Address]*big.Int{ + testAcc1: testAmount1, + testAcc2: testAmount2, + } + testType := common.GenesisFundsOperation + ops, rosettaError := getSideEffectOperationsFromValueMap(testPayouts, testType, nil) + if rosettaError != nil { + t.Fatal(rosettaError) + } + for i, op := range ops { + if int64(i) != op.OperationIdentifier.Index { + t.Errorf("expected operation %v to have operation index %v", i, i) + } + address, err := getAddress(op.Account) + if err != nil { + t.Fatal(err) + } + if value, ok := testPayouts[address]; !ok { + t.Errorf("operation %v has address that is not in test map", i) + } else if value.String() != op.Amount.Value { + t.Errorf("operation %v has wrong value (%v != %v)", i, value.String(), op.Amount.Value) + } + if op.Type != testType { + t.Errorf("operation %v has wrong type", i) + } + if len(op.RelatedOperations) != 0 { + t.Errorf("operation %v has related operations", i) + } + if types.Hash(op.Amount.Currency) != common.NativeCurrencyHash { + t.Errorf("operation %v has wrong currency", i) + } + } + + testStartingIndex := int64(12) + ops, rosettaError = getSideEffectOperationsFromValueMap(testPayouts, testType, &testStartingIndex) + if rosettaError != nil { + t.Fatal(rosettaError) + } + for i, op := range ops { + if int64(i)+testStartingIndex != op.OperationIdentifier.Index { + t.Errorf("expected operation %v to have operation index %v", i, int64(i)+testStartingIndex) + } + address, err := getAddress(op.Account) + if err != nil { + t.Fatal(err) + } + if value, ok := testPayouts[address]; !ok { + t.Errorf("operation %v has address that is not in test map", i) + } else if value.String() != op.Amount.Value { + t.Errorf("operation %v has wrong value (%v != %v)", i, value.String(), op.Amount.Value) + } + if op.Type != testType { + t.Errorf("operation %v has wrong type", i) + } + if len(op.RelatedOperations) != 0 { + t.Errorf("operation %v has related operations", i) + } + if types.Hash(op.Amount.Currency) != common.NativeCurrencyHash { + t.Errorf("operation %v has wrong currency", i) + } + } +} + func TestGetStakingOperationsFromDelegate(t *testing.T) { gasLimit := uint64(1e18) senderKey, err := crypto.GenerateKey() @@ -123,7 +192,7 @@ func TestGetStakingOperationsFromDelegate(t *testing.T) { Status: hmytypes.ReceiptStatusSuccessful, // Failed staking transaction are never saved on-chain GasUsed: gasUsed, } - refOperations := newNativeOperations(gasFee, senderAccID) + refOperations := newNativeOperationsWithGas(gasFee, senderAccID) refOperations = append(refOperations, &types.Operation{ OperationIdentifier: &types.OperationIdentifier{Index: 1}, RelatedOperations: []*types.OperationIdentifier{ @@ -138,7 +207,7 @@ func TestGetStakingOperationsFromDelegate(t *testing.T) { }, Metadata: metadata, }) - operations, rosettaError := GetOperationsFromStakingTransaction(tx, receipt) + operations, rosettaError := GetNativeOperationsFromStakingTransaction(tx, receipt) if rosettaError != nil { t.Fatal(rosettaError) } @@ -187,7 +256,7 @@ func TestGetStakingOperationsFromUndelegate(t *testing.T) { Status: hmytypes.ReceiptStatusSuccessful, // Failed staking transaction are never saved on-chain GasUsed: gasUsed, } - refOperations := newNativeOperations(gasFee, senderAccID) + refOperations := newNativeOperationsWithGas(gasFee, senderAccID) refOperations = append(refOperations, &types.Operation{ OperationIdentifier: &types.OperationIdentifier{Index: 1}, RelatedOperations: []*types.OperationIdentifier{ @@ -202,7 +271,7 @@ func TestGetStakingOperationsFromUndelegate(t *testing.T) { }, Metadata: metadata, }) - operations, rosettaError := GetOperationsFromStakingTransaction(tx, receipt) + operations, rosettaError := GetNativeOperationsFromStakingTransaction(tx, receipt) if rosettaError != nil { t.Fatal(rosettaError) } @@ -251,7 +320,7 @@ func TestGetStakingOperationsFromCollectRewards(t *testing.T) { }, }, } - refOperations := newNativeOperations(gasFee, senderAccID) + refOperations := newNativeOperationsWithGas(gasFee, senderAccID) refOperations = append(refOperations, &types.Operation{ OperationIdentifier: &types.OperationIdentifier{Index: 1}, RelatedOperations: []*types.OperationIdentifier{ @@ -266,7 +335,7 @@ func TestGetStakingOperationsFromCollectRewards(t *testing.T) { }, Metadata: metadata, }) - operations, rosettaError := GetOperationsFromStakingTransaction(tx, receipt) + operations, rosettaError := GetNativeOperationsFromStakingTransaction(tx, receipt) if rosettaError != nil { t.Fatal(rosettaError) } @@ -308,7 +377,7 @@ func TestGetStakingOperationsFromEditValidator(t *testing.T) { Status: hmytypes.ReceiptStatusSuccessful, // Failed staking transaction are never saved on-chain GasUsed: gasUsed, } - refOperations := newNativeOperations(gasFee, senderAccID) + refOperations := newNativeOperationsWithGas(gasFee, senderAccID) refOperations = append(refOperations, &types.Operation{ OperationIdentifier: &types.OperationIdentifier{Index: 1}, RelatedOperations: []*types.OperationIdentifier{ @@ -323,7 +392,7 @@ func TestGetStakingOperationsFromEditValidator(t *testing.T) { }, Metadata: metadata, }) - operations, rosettaError := GetOperationsFromStakingTransaction(tx, receipt) + operations, rosettaError := GetNativeOperationsFromStakingTransaction(tx, receipt) if rosettaError != nil { t.Fatal(rosettaError) } @@ -587,7 +656,7 @@ func TestNewNativeOperations(t *testing.T) { Currency: &common.NativeCurrency, } - ops := newNativeOperations(gasFee, accountID) + ops := newNativeOperationsWithGas(gasFee, accountID) if len(ops) != 1 { t.Fatalf("Expected new operations to be of length 1") }