The core protocol of WoopChain
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
woop/rosetta/services/block.go

1295 lines
42 KiB

package services
import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"math/big"
"strings"
"github.com/coinbase/rosetta-sdk-go/server"
"github.com/coinbase/rosetta-sdk-go/types"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/harmony-one/harmony/core"
"github.com/harmony-one/harmony/core/rawdb"
hmytypes "github.com/harmony-one/harmony/core/types"
"github.com/harmony-one/harmony/crypto/bls"
"github.com/harmony-one/harmony/hmy"
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/internal/utils"
"github.com/harmony-one/harmony/rosetta/common"
"github.com/harmony-one/harmony/rpc"
rpcV2 "github.com/harmony-one/harmony/rpc/v2"
"github.com/harmony-one/harmony/shard"
"github.com/harmony-one/harmony/staking"
stakingNetwork "github.com/harmony-one/harmony/staking/network"
stakingTypes "github.com/harmony-one/harmony/staking/types"
)
// BlockAPI implements the server.BlockAPIServicer interface.
type BlockAPI struct {
hmy *hmy.Harmony
}
// NewBlockAPI creates a new instance of a BlockAPI.
func NewBlockAPI(hmy *hmy.Harmony) server.BlockAPIServicer {
return &BlockAPI{
hmy: hmy,
}
}
// Block implements the /block endpoint
func (s *BlockAPI) Block(
ctx context.Context, request *types.BlockRequest,
) (response *types.BlockResponse, rosettaError *types.Error) {
if err := assertValidNetworkIdentifier(request.NetworkIdentifier, s.hmy.ShardID); err != nil {
return nil, err
}
var blk *hmytypes.Block
var currBlockID, prevBlockID *types.BlockIdentifier
if blk, rosettaError = s.getBlock(ctx, request.BlockIdentifier); rosettaError != nil {
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(),
}
// 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
}
responseBlock := &types.Block{
BlockIdentifier: currBlockID,
ParentBlockIdentifier: prevBlockID,
Timestamp: blk.Time().Int64() * 1e3, // Timestamp must be in ms.
Transactions: transactions,
}
otherTransactions := []*types.TransactionIdentifier{}
for _, tx := range blk.Transactions() {
otherTransactions = append(otherTransactions, &types.TransactionIdentifier{
Hash: tx.Hash().String(),
})
}
for _, tx := range blk.StakingTransactions() {
otherTransactions = append(otherTransactions, &types.TransactionIdentifier{
Hash: tx.Hash().String(),
})
}
// Report cross-shard transaction payouts.
for _, cxReceipts := range blk.IncomingReceipts() {
// Report cross-shard transaction payouts.
for _, cxReceipt := range cxReceipts.Receipts {
otherTransactions = append(otherTransactions, &types.TransactionIdentifier{
Hash: cxReceipt.TxHash.String(),
})
}
}
// Report pre-staking era block rewards as transactions to fit API.
if !s.hmy.IsStakingEpoch(blk.Epoch()) {
preStakingRewardTxIDs, rosettaError := s.getAllPreStakingRewardTransactionIdentifiers(ctx, blk)
if rosettaError != nil {
return nil, rosettaError
}
otherTransactions = append(otherTransactions, preStakingRewardTxIDs...)
}
return &types.BlockResponse{
Block: responseBlock,
OtherTransactions: otherTransactions,
}, 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
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.
}
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
}
// getAllPreStakingRewardTransactionIdentifiers is only used for the /block endpoint
func (s *BlockAPI) getAllPreStakingRewardTransactionIdentifiers(
ctx context.Context, blk *hmytypes.Block,
) ([]*types.TransactionIdentifier, *types.Error) {
txIDs := []*types.TransactionIdentifier{}
blockSigInfo, rosettaError := s.getBlockSignerInfo(ctx, blk)
if rosettaError != nil {
return nil, rosettaError
}
for acc, signedBlsKeys := range blockSigInfo.signers {
if len(signedBlsKeys) > 0 {
txIDs = append(txIDs, getSpecialCaseTransactionIdentifier(blk.Hash(), acc, SpecialPreStakingRewardTxID))
}
}
return txIDs, nil
}
// isCommitteeSelectionBlock ..
func (s *BlockAPI) isCommitteeSelectionBlock(blk *hmytypes.Block) bool {
isBeaconChain := blk.ShardID() == shard.BeaconChainShardID
isNewEpoch := len(blk.Header().ShardState()) > 0
inPreStakingEra := s.hmy.IsPreStakingEpoch(blk.Epoch())
return isBeaconChain && isNewEpoch && inPreStakingEra
}
// getAllUndelegationPayoutTransactions is only used for the /block endpoint
func (s *BlockAPI) getAllUndelegationPayoutTransactions(
ctx context.Context, blk *hmytypes.Block,
) ([]*types.Transaction, *types.Error) {
if !s.isCommitteeSelectionBlock(blk) {
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: fmt.Sprintf("%v", payout),
Currency: &common.Currency,
},
},
},
})
}
return transactions, nil
}
// getBlockSignerInfo fetches the block signer information for any non-genesis block
func (s *BlockAPI) getBlockSignerInfo(
ctx context.Context, blk *hmytypes.Block,
) (*blockSignerInfo, *types.Error) {
slotList, mask, err := s.hmy.GetBlockSigners(
ctx, rpc.BlockNumber(blk.Number().Uint64()).EthBlockNumber(),
)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
totalSigners := uint(0)
sigInfos := map[ethcommon.Address][]bls.SerializedPublicKey{}
for _, slot := range slotList {
if _, ok := sigInfos[slot.EcdsaAddress]; !ok {
sigInfos[slot.EcdsaAddress] = []bls.SerializedPublicKey{}
}
if ok, err := mask.KeyEnabled(slot.BLSPublicKey); ok && err == nil {
sigInfos[slot.EcdsaAddress] = append(sigInfos[slot.EcdsaAddress], slot.BLSPublicKey)
totalSigners++
}
}
return &blockSignerInfo{
signers: sigInfos,
totalKeysSigned: totalSigners,
mask: mask,
blockHash: blk.Hash(),
}, nil
}
// BlockTransaction implements the /block/transaction endpoint
func (s *BlockAPI) BlockTransaction(
ctx context.Context, request *types.BlockTransactionRequest,
) (*types.BlockTransactionResponse, *types.Error) {
if err := assertValidNetworkIdentifier(request.NetworkIdentifier, s.hmy.ShardID); err != nil {
return nil, err
}
// Format genesis block transaction request
if request.BlockIdentifier.Index == 0 {
return s.genesisBlockTransaction(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 rosettaError2 != nil && rosettaError2.Code != common.TransactionNotFoundError.Code {
return nil, common.NewError(common.TransactionNotFoundError, map[string]interface{}{
"from_error": rosettaError2,
})
}
return response, rosettaError2
}
var transaction *types.Transaction
if txInfo.tx != nil && txInfo.receipt != nil {
transaction, rosettaError = formatTransaction(txInfo.tx, txInfo.receipt)
if rosettaError != nil {
return nil, rosettaError
}
} else if txInfo.cxReceipt != nil {
transaction, rosettaError = formatCrossShardReceiverTransaction(txInfo.cxReceipt)
if rosettaError != nil {
return nil, rosettaError
}
} else {
return nil, &common.TransactionNotFoundError
}
return &types.BlockTransactionResponse{Transaction: transaction}, nil
}
// genesisBlockTransaction is a special handler for genesis block transactions
func (s *BlockAPI) genesisBlockTransaction(
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
}
// specialBlockTransaction is a formatter for special, non-genesis, transactions
func (s *BlockAPI) specialBlockTransaction(
ctx context.Context, request *types.BlockTransactionRequest,
) (*types.BlockTransactionResponse, *types.Error) {
// If no transaction info is found, check for special case transactions.
blk, rosettaError := s.getBlock(ctx, &types.PartialBlockIdentifier{Index: &request.BlockIdentifier.Index})
if rosettaError != nil {
return nil, rosettaError
}
if s.isCommitteeSelectionBlock(blk) {
// 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) {
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(),
),
})
}
blockSignerInfo, rosettaError := s.getBlockSignerInfo(ctx, blk)
if rosettaError != nil {
return nil, rosettaError
}
transactions, rosettaError := formatPreStakingRewardTransaction(txID, blockSignerInfo, address)
if rosettaError != nil {
return nil, rosettaError
}
return &types.BlockTransactionResponse{Transaction: transactions}, nil
}
// undelegationPayoutBlockTransaction is a special handler for undelegation payout transactions
func (s *BlockAPI) undelegationPayoutBlockTransaction(
ctx context.Context, txID *types.TransactionIdentifier, blk *hmytypes.Block,
) (*types.BlockTransactionResponse, *types.Error) {
blkHash, address, rosettaError := unpackSpecialCaseTransactionIdentifier(txID, SpecialUndelegationPayoutTxID)
if rosettaError != nil {
return nil, rosettaError
}
if blkHash.String() != blk.Hash().String() {
return nil, common.NewError(common.SanityCheckError, map[string]interface{}{
"message": fmt.Sprintf(
"block hash %v != requested block hash %v in tx ID", blkHash.String(), blk.Hash().String(),
),
})
}
delegatorPayouts, err := s.hmy.GetUndelegationPayouts(ctx, blk.Epoch())
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
transactions, rosettaError := formatUndelegationPayoutTransaction(txID, delegatorPayouts, address)
if rosettaError != nil {
return nil, rosettaError
}
return &types.BlockTransactionResponse{Transaction: transactions}, nil
}
// getBlock ..
func (s *BlockAPI) getBlock(
ctx context.Context, request *types.PartialBlockIdentifier,
) (blk *hmytypes.Block, rosettaError *types.Error) {
var err error
if request.Hash != nil {
requestBlockHash := ethcommon.HexToHash(*request.Hash)
blk, err = s.hmy.GetBlock(ctx, requestBlockHash)
} else if request.Index != nil {
blk, err = s.hmy.BlockByNumber(ctx, rpc.BlockNumber(*request.Index).EthBlockNumber())
} else {
return nil, &common.BlockNotFoundError
}
if err != nil {
return nil, common.NewError(common.BlockNotFoundError, map[string]interface{}{
"message": err.Error(),
})
}
if blk == nil {
return nil, &common.BlockNotFoundError
}
return blk, nil
}
// transactionInfo stores all related information for any transaction on the Harmony chain
// Note that some elements can be nil if not applicable
type transactionInfo struct {
tx hmytypes.PoolTransaction
txIndex uint64
receipt *hmytypes.Receipt
cxReceipt *hmytypes.CXReceipt
}
// getTransactionInfo given the block hash and transaction hash
func (s *BlockAPI) getTransactionInfo(
ctx context.Context, blockHash, txHash ethcommon.Hash,
) (txInfo *transactionInfo, rosettaError *types.Error) {
// Look for all of the possible transactions
var index uint64
var plainTx *hmytypes.Transaction
var stakingTx *stakingTypes.StakingTransaction
plainTx, _, _, index = rawdb.ReadTransaction(s.hmy.ChainDb(), txHash)
if plainTx == nil {
stakingTx, _, _, index = rawdb.ReadStakingTransaction(s.hmy.ChainDb(), txHash)
}
cxReceipt, _, _, _ := rawdb.ReadCXReceipt(s.hmy.ChainDb(), txHash)
if plainTx == nil && stakingTx == nil && cxReceipt == nil {
return nil, &common.TransactionNotFoundError
}
var receipt *hmytypes.Receipt
receipts, err := s.hmy.GetReceipts(ctx, blockHash)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
if int(index) < len(receipts) {
receipt = receipts[index]
}
// Use pool transaction for concise formatting
var tx hmytypes.PoolTransaction
if stakingTx != nil {
tx = stakingTx
} else if plainTx != nil {
tx = plainTx
}
return &transactionInfo{
tx: tx,
txIndex: index,
receipt: receipt,
cxReceipt: cxReceipt,
}, nil
}
func (s *BlockAPI) getTransactionReceiptFromIndex(
ctx context.Context, blockHash ethcommon.Hash, index uint64,
) (*hmytypes.Receipt, *types.Error) {
receipts, err := s.hmy.GetReceipts(ctx, blockHash)
if err != nil || len(receipts) <= int(index) {
message := fmt.Sprintf("Transasction receipt not found")
if err != nil {
message = fmt.Sprintf("Transasction receipt not found: %v", err.Error())
}
return nil, common.NewError(common.ReceiptNotFoundError, map[string]interface{}{
"message": message,
})
}
return receipts[index], 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
}
// SpecialTransactionSuffix ..
type SpecialTransactionSuffix uint
// Special transaction suffixes that are specific to the rosetta package
const (
SpecialGenesisTxID SpecialTransactionSuffix = iota
SpecialPreStakingRewardTxID
SpecialUndelegationPayoutTxID
)
// 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(),
),
}
}
const (
blockHashStrLen = 64
b32AddrStrLen = 42
)
// 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 + b32AddrStrLen + 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
}
// blockSignerInfo contains all of the block singing information
type blockSignerInfo struct {
// signers is a map of addresses in the signers for the block to
// all of the serialized BLS keys that signed said block.
signers map[ethcommon.Address][]bls.SerializedPublicKey
// totalKeysSigned is the total number of bls keys that signed the block.
totalKeysSigned uint
// mask is the bitmap mask for the block.
mask *bls.Mask
blockHash ethcommon.Hash
}
// TransactionMetadata ..
type TransactionMetadata struct {
// CrossShardIdentifier is the transaction identifier on the from/source shard
CrossShardIdentifier *types.TransactionIdentifier `json:"cross_shard_transaction_identifier,omitempty"`
ToShardID *uint32 `json:"to_shard,omitempty"`
FromShardID *uint32 `json:"from_shard,omitempty"`
Data *string `json:"data,omitempty"`
Logs []*hmytypes.Log `json:"logs,omitempty"`
}
// UnmarshalFromInterface ..
func (t *TransactionMetadata) UnmarshalFromInterface(metaData interface{}) error {
var args TransactionMetadata
dat, err := json.Marshal(metaData)
if err != nil {
return err
}
if err := json.Unmarshal(dat, &args); err != nil {
return err
}
*t = args
return nil
}
// formatCrossShardReceiverTransaction for cross-shard payouts on destination shard
func formatCrossShardReceiverTransaction(
cxReceipt *hmytypes.CXReceipt,
) (txs *types.Transaction, rosettaError *types.Error) {
ctxID := &types.TransactionIdentifier{Hash: cxReceipt.TxHash.String()}
senderAccountID, rosettaError := newAccountIdentifier(cxReceipt.From)
if rosettaError != nil {
return nil, rosettaError
}
receiverAccountID, rosettaError := newAccountIdentifier(*cxReceipt.To)
if rosettaError != nil {
return nil, rosettaError
}
metadata, err := types.MarshalMap(TransactionMetadata{
CrossShardIdentifier: ctxID,
ToShardID: &cxReceipt.ToShardID,
FromShardID: &cxReceipt.ShardID,
})
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
opMetadata, err := types.MarshalMap(common.CrossShardTransactionOperationMetadata{
From: senderAccountID,
To: receiverAccountID,
})
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
return &types.Transaction{
TransactionIdentifier: ctxID,
Metadata: metadata,
Operations: []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: 0, // There is no gas expenditure for cross-shard transaction payout
},
Type: common.CrossShardTransferOperation,
Status: common.SuccessOperationStatus.Status,
Account: receiverAccountID,
Amount: &types.Amount{
Value: fmt.Sprintf("%v", cxReceipt.Amount),
Currency: &common.Currency,
},
Metadata: opMetadata,
},
},
}, nil
}
// formatGenesisTransaction for genesis block's initial balances
func formatGenesisTransaction(
txID *types.TransactionIdentifier, targetAddr ethcommon.Address, shardID uint32,
) (fmtTx *types.Transaction, rosettaError *types.Error) {
var b32Addr string
targetB32Addr := internalCommon.MustAddressToBech32(targetAddr)
for _, tx := range getPseudoTransactionForGenesis(getGenesisSpec(shardID)) {
if tx.To() == nil {
return nil, common.NewError(common.CatchAllError, nil)
}
b32Addr = internalCommon.MustAddressToBech32(*tx.To())
if targetB32Addr == b32Addr {
accID, rosettaError := newAccountIdentifier(*tx.To())
if rosettaError != nil {
return nil, rosettaError
}
return &types.Transaction{
TransactionIdentifier: txID,
Operations: []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: 0,
},
Type: common.GenesisFundsOperation,
Status: common.SuccessOperationStatus.Status,
Account: accID,
Amount: &types.Amount{
Value: fmt.Sprintf("%v", tx.Value()),
Currency: &common.Currency,
},
},
},
}, nil
}
}
return nil, &common.TransactionNotFoundError
}
// formatPreStakingRewardTransaction for block rewards in pre-staking era for a given Bech-32 address.
func formatPreStakingRewardTransaction(
txID *types.TransactionIdentifier, blockSigInfo *blockSignerInfo, address ethcommon.Address,
) (*types.Transaction, *types.Error) {
signatures, ok := blockSigInfo.signers[address]
if !ok || len(signatures) == 0 {
return nil, &common.TransactionNotFoundError
}
accID, rosettaError := newAccountIdentifier(address)
if rosettaError != nil {
return nil, rosettaError
}
// Calculate rewards exactly like `AccumulateRewardsAndCountSigs` but short circuit when possible.
var rewardsForThisBlock *big.Int
i := 0
last := big.NewInt(0)
count := big.NewInt(int64(blockSigInfo.totalKeysSigned))
for sigAddr, keys := range blockSigInfo.signers {
rewardsForThisAddr := big.NewInt(0)
for range keys {
cur := big.NewInt(0)
cur.Mul(stakingNetwork.BlockReward, big.NewInt(int64(i+1))).Div(cur, count)
reward := big.NewInt(0).Sub(cur, last)
rewardsForThisAddr = new(big.Int).Add(reward, rewardsForThisAddr)
last = cur
i++
}
if sigAddr == address {
rewardsForThisBlock = rewardsForThisAddr
if !(rewardsForThisAddr.Cmp(big.NewInt(0)) > 0) {
return nil, common.NewError(common.SanityCheckError, map[string]interface{}{
"message": "expected non-zero block reward in pre-staking era for block signer",
})
}
break
}
}
return &types.Transaction{
TransactionIdentifier: txID,
Operations: []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: 0,
},
Type: common.PreStakingBlockRewardOperation,
Status: common.SuccessOperationStatus.Status,
Account: accID,
Amount: &types.Amount{
Value: fmt.Sprintf("%v", rewardsForThisBlock),
Currency: &common.Currency,
},
},
},
}, 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: fmt.Sprintf("%v", payout),
Currency: &common.Currency,
},
},
},
}, nil
}
var (
// DefaultSenderAddress ..
DefaultSenderAddress = ethcommon.HexToAddress("0xEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE")
)
// formatTransaction for staking, cross-shard sender, and plain transactions
func formatTransaction(
tx hmytypes.PoolTransaction, receipt *hmytypes.Receipt,
) (fmtTx *types.Transaction, rosettaError *types.Error) {
var operations []*types.Operation
var isCrossShard, isStaking bool
var toShard uint32
switch tx.(type) {
case *stakingTypes.StakingTransaction:
isStaking = true
stakingTx := tx.(*stakingTypes.StakingTransaction)
operations, rosettaError = getStakingOperations(stakingTx, receipt)
if rosettaError != nil {
return nil, rosettaError
}
isCrossShard = false
toShard = stakingTx.ShardID()
case *hmytypes.Transaction:
isStaking = false
plainTx := tx.(*hmytypes.Transaction)
operations, rosettaError = getOperations(plainTx, receipt)
if rosettaError != nil {
return nil, rosettaError
}
isCrossShard = plainTx.ShardID() != plainTx.ToShardID()
toShard = plainTx.ToShardID()
default:
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": "unknown transaction type",
})
}
fromShard := tx.ShardID()
txID := &types.TransactionIdentifier{Hash: tx.Hash().String()}
// Set all possible metadata
var txMetadata TransactionMetadata
if isCrossShard {
txMetadata.CrossShardIdentifier = txID
txMetadata.ToShardID = &toShard
txMetadata.FromShardID = &fromShard
}
if len(tx.Data()) > 0 && !isStaking {
hexData := hex.EncodeToString(tx.Data())
txMetadata.Data = &hexData
txMetadata.Logs = receipt.Logs
}
metadata, err := types.MarshalMap(txMetadata)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
return &types.Transaction{
TransactionIdentifier: txID,
Operations: operations,
Metadata: metadata,
}, nil
}
// getOperations for one of the following transactions:
// contract creation, cross-shard sender, same-shard transfer
func getOperations(
tx *hmytypes.Transaction, receipt *hmytypes.Receipt,
) ([]*types.Operation, *types.Error) {
senderAddress, err := tx.SenderAddress()
if err != nil {
senderAddress = DefaultSenderAddress
}
accountID, rosettaError := newAccountIdentifier(senderAddress)
if rosettaError != nil {
return nil, rosettaError
}
// All operations excepts for cross-shard tx payout expend gas
gasExpended := new(big.Int).Mul(new(big.Int).SetUint64(receipt.GasUsed), tx.GasPrice())
gasOperations := newOperations(gasExpended, accountID)
// Handle different cases of plain transactions
var txOperations []*types.Operation
if tx.To() == nil {
txOperations, rosettaError = newContractCreationOperations(
gasOperations[0].OperationIdentifier, tx, receipt, senderAddress,
)
} else if tx.ShardID() != tx.ToShardID() {
txOperations, rosettaError = newCrossShardSenderTransferOperations(
gasOperations[0].OperationIdentifier, tx, senderAddress,
)
} else {
txOperations, rosettaError = newTransferOperations(
gasOperations[0].OperationIdentifier, tx, receipt, senderAddress,
)
}
if rosettaError != nil {
return nil, rosettaError
}
return append(gasOperations, txOperations...), nil
}
// getStakingOperations for all staking directives
func getStakingOperations(
tx *stakingTypes.StakingTransaction, receipt *hmytypes.Receipt,
) ([]*types.Operation, *types.Error) {
senderAddress, err := tx.SenderAddress()
if err != nil {
senderAddress = DefaultSenderAddress
}
accountID, rosettaError := newAccountIdentifier(senderAddress)
if rosettaError != nil {
return nil, rosettaError
}
// All operations excepts for cross-shard tx payout expend gas
gasExpended := new(big.Int).Mul(new(big.Int).SetUint64(receipt.GasUsed), tx.GasPrice())
gasOperations := newOperations(gasExpended, accountID)
// Format staking message for metadata using decimal numbers (hence usage of rpcV2)
rpcStakingTx, err := rpcV2.NewStakingTransaction(tx, ethcommon.Hash{}, 0, 0, 0)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
metadata, err := types.MarshalMap(rpcStakingTx.Msg)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
// Set correct amount depending on staking message directive that apply balance changes INSTANTLY
var amount *types.Amount
switch tx.StakingType() {
case stakingTypes.DirectiveCreateValidator:
if amount, rosettaError = getAmountFromCreateValidatorMessage(tx.Data()); rosettaError != nil {
return nil, rosettaError
}
case stakingTypes.DirectiveDelegate:
if amount, rosettaError = getAmountFromDelegateMessage(receipt, tx.Data()); rosettaError != nil {
return nil, rosettaError
}
case stakingTypes.DirectiveCollectRewards:
if amount, rosettaError = getAmountFromCollectRewards(receipt, senderAddress); rosettaError != nil {
return nil, rosettaError
}
default:
amount = &types.Amount{
Value: "0", // All other staking transactions do not apply balance changes instantly or at all
Currency: &common.Currency,
}
}
return append(gasOperations, &types.Operation{
OperationIdentifier: &types.OperationIdentifier{
Index: gasOperations[0].OperationIdentifier.Index + 1,
},
RelatedOperations: []*types.OperationIdentifier{
gasOperations[0].OperationIdentifier,
},
Type: tx.StakingType().String(),
Status: common.SuccessOperationStatus.Status,
Account: accountID,
Amount: amount,
Metadata: metadata,
}), nil
}
func getAmountFromCreateValidatorMessage(data []byte) (*types.Amount, *types.Error) {
msg, err := stakingTypes.RLPDecodeStakeMsg(data, stakingTypes.DirectiveCreateValidator)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
stkMsg, ok := msg.(*stakingTypes.CreateValidator)
if !ok {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": "unable to parse staking message for create validator tx",
})
}
return &types.Amount{
Value: formatNegativeValue(stkMsg.Amount),
Currency: &common.Currency,
}, nil
}
func getAmountFromDelegateMessage(receipt *hmytypes.Receipt, data []byte) (*types.Amount, *types.Error) {
msg, err := stakingTypes.RLPDecodeStakeMsg(data, stakingTypes.DirectiveDelegate)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
stkMsg, ok := msg.(*stakingTypes.Delegate)
if !ok {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": "unable to parse staking message for delegate tx",
})
}
stkAmount := stkMsg.Amount
logs := findLogsWithTopic(receipt, staking.DelegateTopic)
for _, log := range logs {
if len(log.Data) > ethcommon.AddressLength {
validatorAddress := ethcommon.BytesToAddress(log.Data[:ethcommon.AddressLength])
if log.Address == stkMsg.DelegatorAddress && stkMsg.ValidatorAddress == validatorAddress {
// Remove re-delegation amount as funds were never credited to account's balance.
stkAmount = new(big.Int).Sub(stkAmount, new(big.Int).SetBytes(log.Data[ethcommon.AddressLength:]))
break
}
}
}
return &types.Amount{
Value: formatNegativeValue(stkAmount),
Currency: &common.Currency,
}, nil
}
func getAmountFromCollectRewards(
receipt *hmytypes.Receipt, senderAddress ethcommon.Address,
) (*types.Amount, *types.Error) {
var amount *types.Amount
logs := findLogsWithTopic(receipt, staking.CollectRewardsTopic)
for _, log := range logs {
if log.Address == senderAddress {
amount = &types.Amount{
Value: fmt.Sprintf("%v", big.NewInt(0).SetBytes(log.Data)),
Currency: &common.Currency,
}
break
}
}
if amount == nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": fmt.Sprintf("collect rewards amount not found for %v", senderAddress),
})
}
return amount, nil
}
// newTransferOperations extracts & formats the operation(s) for plain transaction,
// including contract transactions.
func newTransferOperations(
startingOperationID *types.OperationIdentifier,
tx *hmytypes.Transaction, receipt *hmytypes.Receipt, senderAddress ethcommon.Address,
) ([]*types.Operation, *types.Error) {
if tx.To() == nil {
return nil, common.NewError(common.CatchAllError, nil)
}
receiverAddress := *tx.To()
// Common elements
opType := common.TransferOperation
opStatus := common.SuccessOperationStatus.Status
if receipt.Status == hmytypes.ReceiptStatusFailed {
if len(tx.Data()) > 0 {
opStatus = common.ContractFailureOperationStatus.Status
} else {
// Should never see a failed non-contract related transaction on chain
opStatus = common.FailureOperationStatus.Status
utils.Logger().Warn().Msgf("Failed transaction on chain: %v", tx.Hash().String())
}
}
// Subtraction operation elements
subOperationID := &types.OperationIdentifier{
Index: startingOperationID.Index + 1,
}
subRelatedID := []*types.OperationIdentifier{
startingOperationID,
}
subAccountID, rosettaError := newAccountIdentifier(senderAddress)
if rosettaError != nil {
return nil, rosettaError
}
subAmount := &types.Amount{
Value: formatNegativeValue(tx.Value()),
Currency: &common.Currency,
}
// Addition operation elements
addOperationID := &types.OperationIdentifier{
Index: subOperationID.Index + 1,
}
addRelatedID := []*types.OperationIdentifier{
subOperationID,
}
addAccountID, rosettaError := newAccountIdentifier(receiverAddress)
if rosettaError != nil {
return nil, rosettaError
}
addAmount := &types.Amount{
Value: fmt.Sprintf("%v", tx.Value()),
Currency: &common.Currency,
}
return []*types.Operation{
{
OperationIdentifier: subOperationID,
RelatedOperations: subRelatedID,
Type: opType,
Status: opStatus,
Account: subAccountID,
Amount: subAmount,
},
{
OperationIdentifier: addOperationID,
RelatedOperations: addRelatedID,
Type: opType,
Status: opStatus,
Account: addAccountID,
Amount: addAmount,
},
}, nil
}
// newCrossShardSenderTransferOperations extracts & formats the operation(s) for cross-shard-tx
// on the sender's shard.
func newCrossShardSenderTransferOperations(
startingOperationID *types.OperationIdentifier,
tx *hmytypes.Transaction, senderAddress ethcommon.Address,
) ([]*types.Operation, *types.Error) {
if tx.To() == nil {
return nil, common.NewError(common.CatchAllError, nil)
}
senderAccountID, rosettaError := newAccountIdentifier(senderAddress)
if rosettaError != nil {
return nil, rosettaError
}
receiverAccountID, rosettaError := newAccountIdentifier(*tx.To())
if rosettaError != nil {
return nil, rosettaError
}
metadata, err := types.MarshalMap(common.CrossShardTransactionOperationMetadata{
From: senderAccountID,
To: receiverAccountID,
})
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
return []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: startingOperationID.Index + 1,
},
RelatedOperations: []*types.OperationIdentifier{
startingOperationID,
},
Type: common.CrossShardTransferOperation,
Status: common.SuccessOperationStatus.Status,
Account: senderAccountID,
Amount: &types.Amount{
Value: formatNegativeValue(tx.Value()),
Currency: &common.Currency,
},
Metadata: metadata,
},
}, nil
}
// newContractCreationOperations extracts & formats the operation(s) for a contract creation tx
func newContractCreationOperations(
startingOperationID *types.OperationIdentifier,
tx *hmytypes.Transaction, txReceipt *hmytypes.Receipt, senderAddress ethcommon.Address,
) ([]*types.Operation, *types.Error) {
senderAccountID, rosettaError := newAccountIdentifier(senderAddress)
if rosettaError != nil {
return nil, rosettaError
}
// Set execution status as necessary
status := common.SuccessOperationStatus.Status
if txReceipt.Status == hmytypes.ReceiptStatusFailed {
status = common.ContractFailureOperationStatus.Status
}
contractAddressID, rosettaError := newAccountIdentifier(txReceipt.ContractAddress)
if rosettaError != nil {
return nil, rosettaError
}
return []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: startingOperationID.Index + 1,
},
RelatedOperations: []*types.OperationIdentifier{
startingOperationID,
},
Type: common.ContractCreationOperation,
Status: status,
Account: senderAccountID,
Amount: &types.Amount{
Value: formatNegativeValue(tx.Value()),
Currency: &common.Currency,
},
Metadata: map[string]interface{}{
"contract_address": contractAddressID,
},
},
}, nil
}
// AccountMetadata used for account identifiers
type AccountMetadata struct {
Address string `json:"hex_address"`
}
// newAccountIdentifier ..
func newAccountIdentifier(
address ethcommon.Address,
) (*types.AccountIdentifier, *types.Error) {
b32Address, err := internalCommon.AddressToBech32(address)
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
metadata, err := types.MarshalMap(AccountMetadata{Address: address.String()})
if err != nil {
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
"message": err.Error(),
})
}
return &types.AccountIdentifier{
Address: b32Address,
Metadata: metadata,
}, nil
}
// getAddress ..
func getAddress(
identifier *types.AccountIdentifier,
) (ethcommon.Address, error) {
if identifier == nil {
return ethcommon.Address{}, fmt.Errorf("identifier cannot be nil")
}
return internalCommon.Bech32ToAddress(identifier.Address)
}
// newOperations creates a new operation with the gas fee as the first operation.
// Note: the gas fee is gasPrice * gasUsed.
func newOperations(
gasFeeInATTO *big.Int, accountID *types.AccountIdentifier,
) []*types.Operation {
return []*types.Operation{
{
OperationIdentifier: &types.OperationIdentifier{
Index: 0, // gas operation is always first
},
Type: common.ExpendGasOperation,
Status: common.SuccessOperationStatus.Status,
Account: accountID,
Amount: &types.Amount{
Value: formatNegativeValue(gasFeeInATTO),
Currency: &common.Currency,
},
},
}
}
// findLogsWithTopic returns all the logs that contain the given receipt
func findLogsWithTopic(
receipt *hmytypes.Receipt, targetTopic ethcommon.Hash,
) []*hmytypes.Log {
logs := []*hmytypes.Log{}
for _, log := range receipt.Logs {
for _, topic := range log.Topics {
if topic == targetTopic {
logs = append(logs, log)
break
}
}
}
return logs
}
// formatNegativeValue ..
func formatNegativeValue(num *big.Int) string {
value := "0"
if num != nil && num.Cmp(big.NewInt(0)) == 1 {
value = fmt.Sprintf("-%v", new(big.Int).Abs(num))
}
return value
}
// 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)
}