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.
821 lines
25 KiB
821 lines
25 KiB
package services
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"math/big"
|
|
|
|
"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/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"
|
|
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 block *hmytypes.Block
|
|
var currBlockID, prevBlockID *types.BlockIdentifier
|
|
if block, rosettaError = s.getBlock(ctx, request); rosettaError != nil {
|
|
return nil, rosettaError
|
|
}
|
|
currBlockID = &types.BlockIdentifier{
|
|
Index: block.Number().Int64(),
|
|
Hash: block.Hash().String(),
|
|
}
|
|
|
|
otherTransactions := []*types.TransactionIdentifier{}
|
|
if block.Number().Int64() > 0 {
|
|
prevBlock, err := s.hmy.BlockByNumber(ctx, rpc.BlockNumber(block.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(),
|
|
}
|
|
} else {
|
|
prevBlockID = currBlockID
|
|
genesisSpec := getGenesisSpec(block.ShardID())
|
|
for _, tx := range getPseudoTransactionForGenesis(genesisSpec) {
|
|
otherTransactions = append(otherTransactions, &types.TransactionIdentifier{
|
|
Hash: tx.To().String(), // use hex address as tx identifiers for genesis block only
|
|
})
|
|
}
|
|
}
|
|
|
|
responseBlock := &types.Block{
|
|
BlockIdentifier: currBlockID,
|
|
ParentBlockIdentifier: prevBlockID,
|
|
Timestamp: block.Time().Int64() * 1e3, // Timestamp must be in ms.
|
|
Transactions: []*types.Transaction{}, // Do not return tx details as it is optional.
|
|
}
|
|
|
|
for _, tx := range block.Transactions() {
|
|
otherTransactions = append(otherTransactions, &types.TransactionIdentifier{
|
|
Hash: tx.Hash().String(),
|
|
})
|
|
}
|
|
for _, tx := range block.StakingTransactions() {
|
|
otherTransactions = append(otherTransactions, &types.TransactionIdentifier{
|
|
Hash: tx.Hash().String(),
|
|
})
|
|
}
|
|
for _, cxReceipts := range block.IncomingReceipts() {
|
|
for _, cxReceipt := range cxReceipts.Receipts {
|
|
otherTransactions = append(otherTransactions, &types.TransactionIdentifier{
|
|
Hash: cxReceipt.TxHash.String(),
|
|
})
|
|
}
|
|
}
|
|
|
|
return &types.BlockResponse{
|
|
Block: responseBlock,
|
|
OtherTransactions: otherTransactions,
|
|
}, nil
|
|
}
|
|
|
|
// BlockTransaction implements the /block/transaction endpoint
|
|
func (s *BlockAPI) BlockTransaction(
|
|
ctx context.Context, request *types.BlockTransactionRequest,
|
|
) (response *types.BlockTransactionResponse, rosettaError *types.Error) {
|
|
if err := assertValidNetworkIdentifier(request.NetworkIdentifier, s.hmy.ShardID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Special case for genesis block
|
|
if request.BlockIdentifier.Index == 0 {
|
|
txs, rosettaError := formatGenesisTransaction(request.TransactionIdentifier, s.hmy.ShardID)
|
|
if rosettaError != nil {
|
|
return nil, rosettaError
|
|
}
|
|
return &types.BlockTransactionResponse{Transaction: txs}, nil
|
|
}
|
|
|
|
blockHash := ethcommon.HexToHash(request.BlockIdentifier.Hash)
|
|
txHash := ethcommon.HexToHash(request.TransactionIdentifier.Hash)
|
|
txInfo, rosettaError := s.getTransactionInfo(ctx, blockHash, txHash)
|
|
if rosettaError != nil {
|
|
return nil, rosettaError
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (s *BlockAPI) getBlock(
|
|
ctx context.Context, request *types.BlockRequest,
|
|
) (block *hmytypes.Block, rosettaError *types.Error) {
|
|
var err error
|
|
if request.BlockIdentifier.Hash != nil {
|
|
requestBlockHash := ethcommon.HexToHash(*request.BlockIdentifier.Hash)
|
|
block, err = s.hmy.GetBlock(ctx, requestBlockHash)
|
|
} else if request.BlockIdentifier.Index != nil {
|
|
block, err = s.hmy.BlockByNumber(ctx, rpc.BlockNumber(*request.BlockIdentifier.Index).EthBlockNumber())
|
|
} else {
|
|
return nil, &common.BlockNotFoundError
|
|
}
|
|
if err != nil {
|
|
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
|
|
"message": err.Error(),
|
|
})
|
|
}
|
|
return block, 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
|
|
}
|
|
|
|
// TransactionMetadata ..
|
|
type TransactionMetadata struct {
|
|
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"`
|
|
}
|
|
|
|
// 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()}
|
|
metadata, err := rpc.NewStructuredResponse(TransactionMetadata{
|
|
CrossShardIdentifier: ctxID,
|
|
ToShardID: cxReceipt.ToShardID,
|
|
FromShardID: cxReceipt.ShardID,
|
|
})
|
|
if err != nil {
|
|
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
|
|
"message": err.Error(),
|
|
})
|
|
}
|
|
senderAccountID, rosettaError := newAccountIdentifier(cxReceipt.From)
|
|
if rosettaError != nil {
|
|
return nil, rosettaError
|
|
}
|
|
receiverAccountID, rosettaError := newAccountIdentifier(*cxReceipt.To)
|
|
if rosettaError != nil {
|
|
return nil, rosettaError
|
|
}
|
|
|
|
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.Uint64()),
|
|
Currency: &common.Currency,
|
|
},
|
|
Metadata: map[string]interface{}{"from_account": senderAccountID},
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// formatGenesisTransaction for genesis block's initial balances
|
|
func formatGenesisTransaction(
|
|
txID *types.TransactionIdentifier, shardID uint32,
|
|
) (fmtTx *types.Transaction, rosettaError *types.Error) {
|
|
genesisSpec := getGenesisSpec(shardID)
|
|
for _, tx := range getPseudoTransactionForGenesis(genesisSpec) {
|
|
if tx.To().String() == txID.Hash {
|
|
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.TransferOperation,
|
|
Status: common.SuccessOperationStatus.Status,
|
|
Account: accID,
|
|
Amount: &types.Amount{
|
|
Value: fmt.Sprintf("%v", tx.Value()),
|
|
Currency: &common.Currency,
|
|
},
|
|
Metadata: map[string]interface{}{
|
|
"type": "genesis funds",
|
|
},
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
}
|
|
return nil, &common.TransactionNotFoundError
|
|
}
|
|
|
|
// 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 bool
|
|
var toShard uint32
|
|
|
|
// Fetch correct operations depending on transaction type
|
|
stakingTx, isStaking := tx.(*stakingTypes.StakingTransaction)
|
|
if !isStaking {
|
|
plainTx, ok := tx.(*hmytypes.Transaction)
|
|
if !ok {
|
|
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
|
|
"message": "unknown transaction type",
|
|
})
|
|
}
|
|
operations, rosettaError = getOperations(plainTx, receipt)
|
|
if rosettaError != nil {
|
|
return nil, rosettaError
|
|
}
|
|
isCrossShard = plainTx.ShardID() != plainTx.ToShardID()
|
|
toShard = plainTx.ToShardID()
|
|
} else {
|
|
operations, rosettaError = getStakingOperations(stakingTx, receipt)
|
|
if rosettaError != nil {
|
|
return nil, rosettaError
|
|
}
|
|
isCrossShard = false
|
|
toShard = stakingTx.ShardID()
|
|
}
|
|
txID := &types.TransactionIdentifier{Hash: tx.Hash().String()}
|
|
|
|
// Set transaction metadata if needed
|
|
var txMetadata TransactionMetadata
|
|
var err error
|
|
if isCrossShard {
|
|
txMetadata.CrossShardIdentifier = txID
|
|
txMetadata.ToShardID = toShard
|
|
txMetadata.FromShardID = tx.ShardID()
|
|
}
|
|
if len(tx.Data()) > 0 && !isStaking {
|
|
txMetadata.Data = hex.EncodeToString(tx.Data())
|
|
}
|
|
metadata, err := rpc.NewStructuredResponse(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 correct 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 {
|
|
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
|
|
"message": err.Error(),
|
|
})
|
|
}
|
|
accountID, rosettaError := newAccountIdentifier(senderAddress)
|
|
if rosettaError != nil {
|
|
return nil, rosettaError
|
|
}
|
|
|
|
// All operations excepts for cross-shard tx payout expend gas
|
|
gasExpended := receipt.GasUsed * tx.GasPrice().Uint64()
|
|
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,
|
|
)
|
|
} else if tx.ShardID() != tx.ToShardID() {
|
|
txOperations, rosettaError = newCrossShardSenderTransferOperations(
|
|
gasOperations[0].OperationIdentifier, tx,
|
|
)
|
|
} else {
|
|
txOperations, rosettaError = newTransferOperations(
|
|
gasOperations[0].OperationIdentifier, tx, receipt,
|
|
)
|
|
}
|
|
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) {
|
|
// Sender address should have errored prior to call this function
|
|
senderAddress, _ := tx.SenderAddress()
|
|
accountID, rosettaError := newAccountIdentifier(senderAddress)
|
|
if rosettaError != nil {
|
|
return nil, rosettaError
|
|
}
|
|
|
|
// All operations excepts for cross-shard tx payout expend gas
|
|
gasExpended := receipt.GasUsed * tx.GasPrice().Uint64()
|
|
gasOperations := newOperations(gasExpended, accountID)
|
|
|
|
// Format staking message for metadata
|
|
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 := rpc.NewStructuredResponse(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
|
|
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(tx.Data()); rosettaError != nil {
|
|
return nil, rosettaError
|
|
}
|
|
case stakingTypes.DirectiveUndelegate:
|
|
if amount, rosettaError = getAmountFromUndelegateMessage(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: fmt.Sprintf("-%v", tx.Value()),
|
|
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: fmt.Sprintf("-%v", stkMsg.Amount.Uint64()),
|
|
Currency: &common.Currency,
|
|
}, nil
|
|
}
|
|
|
|
func getAmountFromDelegateMessage(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",
|
|
})
|
|
}
|
|
return &types.Amount{
|
|
Value: fmt.Sprintf("-%v", stkMsg.Amount.Uint64()),
|
|
Currency: &common.Currency,
|
|
}, nil
|
|
}
|
|
|
|
func getAmountFromUndelegateMessage(data []byte) (*types.Amount, *types.Error) {
|
|
msg, err := stakingTypes.RLPDecodeStakeMsg(data, stakingTypes.DirectiveUndelegate)
|
|
if err != nil {
|
|
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
|
|
"message": err.Error(),
|
|
})
|
|
}
|
|
stkMsg, ok := msg.(*stakingTypes.Undelegate)
|
|
if !ok {
|
|
return nil, common.NewError(common.CatchAllError, map[string]interface{}{
|
|
"message": "unable to parse staking message for undelegate tx",
|
|
})
|
|
}
|
|
return &types.Amount{
|
|
Value: fmt.Sprintf("%v", stkMsg.Amount.Uint64()),
|
|
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).Uint64()),
|
|
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,
|
|
) ([]*types.Operation, *types.Error) {
|
|
// Sender address should have errored prior to call this function
|
|
senderAddress, _ := tx.SenderAddress()
|
|
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: fmt.Sprintf("-%v", 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,
|
|
Metadata: map[string]interface{}{},
|
|
},
|
|
{
|
|
OperationIdentifier: addOperationID,
|
|
RelatedOperations: addRelatedID,
|
|
Type: opType,
|
|
Status: opStatus,
|
|
Account: addAccountID,
|
|
Amount: addAmount,
|
|
Metadata: map[string]interface{}{},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// newCrossShardSenderTransferOperations extracts & formats the operation(s) for cross-shard-tx
|
|
// on the sender's shard.
|
|
func newCrossShardSenderTransferOperations(
|
|
startingOperationID *types.OperationIdentifier,
|
|
tx *hmytypes.Transaction,
|
|
) ([]*types.Operation, *types.Error) {
|
|
// Sender address should have errored prior to call this function
|
|
senderAddress, _ := tx.SenderAddress()
|
|
receiverAddress := *tx.To()
|
|
senderAccountID, rosettaError := newAccountIdentifier(senderAddress)
|
|
if rosettaError != nil {
|
|
return nil, rosettaError
|
|
}
|
|
receiverAccountID, rosettaError := newAccountIdentifier(receiverAddress)
|
|
if rosettaError != nil {
|
|
return nil, rosettaError
|
|
}
|
|
|
|
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: fmt.Sprintf("-%v", tx.Value()),
|
|
Currency: &common.Currency,
|
|
},
|
|
Metadata: map[string]interface{}{
|
|
"to_account": receiverAccountID,
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// newContractCreationOperations extracts & formats the operation(s) for a contract creation tx
|
|
func newContractCreationOperations(
|
|
startingOperationID *types.OperationIdentifier,
|
|
tx *hmytypes.Transaction, txReceipt *hmytypes.Receipt,
|
|
) ([]*types.Operation, *types.Error) {
|
|
// Sender address should have errored prior to call this function
|
|
senderAddress, _ := tx.SenderAddress()
|
|
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
|
|
}
|
|
|
|
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: fmt.Sprintf("-%v", tx.Value()),
|
|
Currency: &common.Currency,
|
|
},
|
|
Metadata: map[string]interface{}{
|
|
"contract_address": txReceipt.ContractAddress.String(),
|
|
},
|
|
},
|
|
}, 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 := rpc.NewStructuredResponse(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
|
|
}
|
|
|
|
// newOperations creates a new operation with the gas fee as the first operation.
|
|
// Note: the gas fee is gasPrice * gasUsed.
|
|
func newOperations(
|
|
gasFeeInATTO uint64, 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: fmt.Sprintf("-%v", 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
|
|
}
|
|
|
|
// getGenesisSpec ..
|
|
func getGenesisSpec(shardID uint32) *core.Genesis {
|
|
if shard.Schedule.GetNetworkID() == shardingconfig.MainNet {
|
|
return core.NewGenesisSpec(nodeconfig.Mainnet, shardID)
|
|
}
|
|
return core.NewGenesisSpec(nodeconfig.Testnet, shardID)
|
|
}
|
|
|