514 lines
15 KiB
Go
514 lines
15 KiB
Go
package rosetta
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"time"
|
|
|
|
rosettatypes "github.com/coinbase/rosetta-sdk-go/types"
|
|
abcitypes "github.com/tendermint/tendermint/abci/types"
|
|
tmrpc "github.com/tendermint/tendermint/rpc/client"
|
|
"github.com/tendermint/tendermint/rpc/client/http"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/metadata"
|
|
|
|
crgerrs "github.com/cosmos/cosmos-sdk/server/rosetta/lib/errors"
|
|
crgtypes "github.com/cosmos/cosmos-sdk/server/rosetta/lib/types"
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
grpctypes "github.com/cosmos/cosmos-sdk/types/grpc"
|
|
"github.com/cosmos/cosmos-sdk/version"
|
|
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
|
|
auth "github.com/cosmos/cosmos-sdk/x/auth/types"
|
|
bank "github.com/cosmos/cosmos-sdk/x/bank/types"
|
|
)
|
|
|
|
// interface assertion
|
|
var _ crgtypes.Client = (*Client)(nil)
|
|
|
|
const defaultNodeTimeout = 15 * time.Second
|
|
|
|
// Client implements a single network client to interact with cosmos based chains
|
|
type Client struct {
|
|
supportedOperations []string
|
|
|
|
config *Config
|
|
|
|
auth auth.QueryClient
|
|
bank bank.QueryClient
|
|
tmRPC tmrpc.Client
|
|
|
|
version string
|
|
|
|
converter Converter
|
|
}
|
|
|
|
// NewClient instantiates a new online servicer
|
|
func NewClient(cfg *Config) (*Client, error) {
|
|
info := version.NewInfo()
|
|
|
|
v := info.Version
|
|
if v == "" {
|
|
v = "unknown"
|
|
}
|
|
|
|
txConfig := authtx.NewTxConfig(cfg.Codec, authtx.DefaultSignModes)
|
|
|
|
var supportedOperations []string
|
|
for _, ii := range cfg.InterfaceRegistry.ListImplementations(sdk.MsgInterfaceProtoName) {
|
|
resolvedMsg, err := cfg.InterfaceRegistry.Resolve(ii)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if _, ok := resolvedMsg.(sdk.Msg); ok {
|
|
supportedOperations = append(supportedOperations, ii)
|
|
}
|
|
}
|
|
|
|
supportedOperations = append(
|
|
supportedOperations,
|
|
bank.EventTypeCoinSpent,
|
|
bank.EventTypeCoinReceived,
|
|
bank.EventTypeCoinBurn,
|
|
)
|
|
|
|
return &Client{
|
|
supportedOperations: supportedOperations,
|
|
config: cfg,
|
|
auth: nil,
|
|
bank: nil,
|
|
tmRPC: nil,
|
|
version: fmt.Sprintf("%s/%s", info.AppName, v),
|
|
converter: NewConverter(cfg.Codec, cfg.InterfaceRegistry, txConfig),
|
|
}, nil
|
|
}
|
|
|
|
// ---------- cosmos-rosetta-gateway.types.Client implementation ------------ //
|
|
|
|
// Bootstrap is gonna connect the client to the endpoints
|
|
func (c *Client) Bootstrap() error {
|
|
grpcConn, err := grpc.Dial(c.config.GRPCEndpoint, grpc.WithInsecure())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tmRPC, err := http.New(c.config.TendermintRPC)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
authClient := auth.NewQueryClient(grpcConn)
|
|
bankClient := bank.NewQueryClient(grpcConn)
|
|
|
|
c.auth = authClient
|
|
c.bank = bankClient
|
|
c.tmRPC = tmRPC
|
|
|
|
return nil
|
|
}
|
|
|
|
// Ready performs a health check and returns an error if the client is not ready.
|
|
func (c *Client) Ready() error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), defaultNodeTimeout)
|
|
defer cancel()
|
|
_, err := c.tmRPC.Health(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = c.bank.TotalSupply(ctx, &bank.QueryTotalSupplyRequest{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) accountInfo(ctx context.Context, addr string, height *int64) (*SignerData, error) {
|
|
if height != nil {
|
|
strHeight := strconv.FormatInt(*height, 10)
|
|
ctx = metadata.AppendToOutgoingContext(ctx, grpctypes.GRPCBlockHeightHeader, strHeight)
|
|
}
|
|
|
|
accountInfo, err := c.auth.Account(ctx, &auth.QueryAccountRequest{
|
|
Address: addr,
|
|
})
|
|
if err != nil {
|
|
return nil, crgerrs.FromGRPCToRosettaError(err)
|
|
}
|
|
|
|
signerData, err := c.converter.ToRosetta().SignerData(accountInfo.Account)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return signerData, nil
|
|
}
|
|
|
|
func (c *Client) Balances(ctx context.Context, addr string, height *int64) ([]*rosettatypes.Amount, error) {
|
|
if height != nil {
|
|
strHeight := strconv.FormatInt(*height, 10)
|
|
ctx = metadata.AppendToOutgoingContext(ctx, grpctypes.GRPCBlockHeightHeader, strHeight)
|
|
}
|
|
|
|
balance, err := c.bank.AllBalances(ctx, &bank.QueryAllBalancesRequest{
|
|
Address: addr,
|
|
})
|
|
if err != nil {
|
|
return nil, crgerrs.FromGRPCToRosettaError(err)
|
|
}
|
|
|
|
availableCoins, err := c.coins(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return c.converter.ToRosetta().Amounts(balance.Balances, availableCoins), nil
|
|
}
|
|
|
|
func (c *Client) BlockByHash(ctx context.Context, hash string) (crgtypes.BlockResponse, error) {
|
|
bHash, err := hex.DecodeString(hash)
|
|
if err != nil {
|
|
return crgtypes.BlockResponse{}, fmt.Errorf("invalid block hash: %s", err)
|
|
}
|
|
|
|
block, err := c.tmRPC.BlockByHash(ctx, bHash)
|
|
if err != nil {
|
|
return crgtypes.BlockResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error())
|
|
}
|
|
|
|
return c.converter.ToRosetta().BlockResponse(block), nil
|
|
}
|
|
|
|
func (c *Client) BlockByHeight(ctx context.Context, height *int64) (crgtypes.BlockResponse, error) {
|
|
height, err := c.getHeight(ctx, height)
|
|
if err != nil {
|
|
return crgtypes.BlockResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error())
|
|
}
|
|
block, err := c.tmRPC.Block(ctx, height)
|
|
if err != nil {
|
|
return crgtypes.BlockResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error())
|
|
}
|
|
|
|
return c.converter.ToRosetta().BlockResponse(block), nil
|
|
}
|
|
|
|
func (c *Client) BlockTransactionsByHash(ctx context.Context, hash string) (crgtypes.BlockTransactionsResponse, error) {
|
|
// TODO(fdymylja): use a faster path, by searching the block by hash, instead of doing a double query operation
|
|
blockResp, err := c.BlockByHash(ctx, hash)
|
|
if err != nil {
|
|
return crgtypes.BlockTransactionsResponse{}, err
|
|
}
|
|
|
|
return c.blockTxs(ctx, &blockResp.Block.Index)
|
|
}
|
|
|
|
func (c *Client) BlockTransactionsByHeight(ctx context.Context, height *int64) (crgtypes.BlockTransactionsResponse, error) {
|
|
height, err := c.getHeight(ctx, height)
|
|
if err != nil {
|
|
return crgtypes.BlockTransactionsResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error())
|
|
}
|
|
blockTxResp, err := c.blockTxs(ctx, height)
|
|
if err != nil {
|
|
return crgtypes.BlockTransactionsResponse{}, err
|
|
}
|
|
return blockTxResp, nil
|
|
}
|
|
|
|
// Coins fetches the existing coins in the application
|
|
func (c *Client) coins(ctx context.Context) (sdk.Coins, error) {
|
|
supply, err := c.bank.TotalSupply(ctx, &bank.QueryTotalSupplyRequest{})
|
|
if err != nil {
|
|
return nil, crgerrs.FromGRPCToRosettaError(err)
|
|
}
|
|
return supply.Supply, nil
|
|
}
|
|
|
|
func (c *Client) TxOperationsAndSignersAccountIdentifiers(signed bool, txBytes []byte) (ops []*rosettatypes.Operation, signers []*rosettatypes.AccountIdentifier, err error) {
|
|
switch signed {
|
|
case false:
|
|
rosTx, err := c.converter.ToRosetta().Tx(txBytes, nil)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return rosTx.Operations, nil, err
|
|
default:
|
|
ops, signers, err = c.converter.ToRosetta().OpsAndSigners(txBytes)
|
|
return
|
|
}
|
|
}
|
|
|
|
// GetTx returns a transaction given its hash. For Rosetta we make a synthetic transaction for BeginBlock
|
|
// and EndBlock to adhere to balance tracking rules.
|
|
func (c *Client) GetTx(ctx context.Context, hash string) (*rosettatypes.Transaction, error) {
|
|
hashBytes, err := hex.DecodeString(hash)
|
|
if err != nil {
|
|
return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("bad tx hash: %s", err))
|
|
}
|
|
|
|
// get tx type and hash
|
|
txType, hashBytes := c.converter.ToSDK().HashToTxType(hashBytes)
|
|
|
|
// construct rosetta tx
|
|
switch txType {
|
|
// handle begin block hash
|
|
case BeginBlockTx:
|
|
// get block height by hash
|
|
block, err := c.tmRPC.BlockByHash(ctx, hashBytes)
|
|
if err != nil {
|
|
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
|
|
}
|
|
|
|
// get block txs
|
|
fullBlock, err := c.blockTxs(ctx, &block.Block.Height)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return fullBlock.Transactions[0], nil
|
|
// handle deliver tx hash
|
|
case DeliverTxTx:
|
|
rawTx, err := c.tmRPC.Tx(ctx, hashBytes, true)
|
|
if err != nil {
|
|
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
|
|
}
|
|
return c.converter.ToRosetta().Tx(rawTx.Tx, &rawTx.TxResult)
|
|
// handle end block hash
|
|
case EndBlockTx:
|
|
// get block height by hash
|
|
block, err := c.tmRPC.BlockByHash(ctx, hashBytes)
|
|
if err != nil {
|
|
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
|
|
}
|
|
|
|
// get block txs
|
|
fullBlock, err := c.blockTxs(ctx, &block.Block.Height)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// get last tx
|
|
return fullBlock.Transactions[len(fullBlock.Transactions)-1], nil
|
|
// unrecognized tx
|
|
default:
|
|
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("invalid tx hash provided: %s", hash))
|
|
}
|
|
}
|
|
|
|
// GetUnconfirmedTx gets an unconfirmed transaction given its hash
|
|
func (c *Client) GetUnconfirmedTx(ctx context.Context, hash string) (*rosettatypes.Transaction, error) {
|
|
res, err := c.tmRPC.UnconfirmedTxs(ctx, nil)
|
|
if err != nil {
|
|
return nil, crgerrs.WrapError(crgerrs.ErrNotFound, "unconfirmed tx not found")
|
|
}
|
|
|
|
hashAsBytes, err := hex.DecodeString(hash)
|
|
if err != nil {
|
|
return nil, crgerrs.WrapError(crgerrs.ErrInterpreting, "invalid hash")
|
|
}
|
|
|
|
// assert that correct tx length is provided
|
|
switch len(hashAsBytes) {
|
|
default:
|
|
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("unrecognized tx size: %d", len(hashAsBytes)))
|
|
case BeginEndBlockTxSize:
|
|
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "endblock and begin block txs cannot be unconfirmed")
|
|
case DeliverTxSize:
|
|
break
|
|
}
|
|
|
|
// iterate over unconfirmed txs to find the one with matching hash
|
|
for _, unconfirmedTx := range res.Txs {
|
|
if !bytes.Equal(unconfirmedTx.Hash(), hashAsBytes) {
|
|
continue
|
|
}
|
|
|
|
return c.converter.ToRosetta().Tx(unconfirmedTx, nil)
|
|
}
|
|
return nil, crgerrs.WrapError(crgerrs.ErrNotFound, "transaction not found in mempool: "+hash)
|
|
}
|
|
|
|
// Mempool returns the unconfirmed transactions in the mempool
|
|
func (c *Client) Mempool(ctx context.Context) ([]*rosettatypes.TransactionIdentifier, error) {
|
|
txs, err := c.tmRPC.UnconfirmedTxs(ctx, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return c.converter.ToRosetta().TxIdentifiers(txs.Txs), nil
|
|
}
|
|
|
|
// Peers gets the number of peers
|
|
func (c *Client) Peers(ctx context.Context) ([]*rosettatypes.Peer, error) {
|
|
netInfo, err := c.tmRPC.NetInfo(ctx)
|
|
if err != nil {
|
|
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
|
|
}
|
|
return c.converter.ToRosetta().Peers(netInfo.Peers), nil
|
|
}
|
|
|
|
func (c *Client) Status(ctx context.Context) (*rosettatypes.SyncStatus, error) {
|
|
status, err := c.tmRPC.Status(ctx)
|
|
if err != nil {
|
|
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
|
|
}
|
|
return c.converter.ToRosetta().SyncStatus(status), err
|
|
}
|
|
|
|
func (c *Client) PostTx(txBytes []byte) (*rosettatypes.TransactionIdentifier, map[string]interface{}, error) {
|
|
// sync ensures it will go through checkTx
|
|
res, err := c.tmRPC.BroadcastTxSync(context.Background(), txBytes)
|
|
if err != nil {
|
|
return nil, nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
|
|
}
|
|
// check if tx was broadcast successfully
|
|
if res.Code != abcitypes.CodeTypeOK {
|
|
return nil, nil, crgerrs.WrapError(
|
|
crgerrs.ErrUnknown,
|
|
fmt.Sprintf("transaction broadcast failure: (%d) %s ", res.Code, res.Log),
|
|
)
|
|
}
|
|
|
|
return &rosettatypes.TransactionIdentifier{
|
|
Hash: fmt.Sprintf("%X", res.Hash),
|
|
},
|
|
map[string]interface{}{
|
|
Log: res.Log,
|
|
}, nil
|
|
}
|
|
|
|
// construction endpoints
|
|
|
|
// ConstructionMetadataFromOptions builds the metadata given the options
|
|
func (c *Client) ConstructionMetadataFromOptions(ctx context.Context, options map[string]interface{}) (meta map[string]interface{}, err error) {
|
|
if len(options) == 0 {
|
|
return nil, crgerrs.ErrBadArgument
|
|
}
|
|
|
|
constructionOptions := new(PreprocessOperationsOptionsResponse)
|
|
|
|
err = constructionOptions.FromMetadata(options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
signersData := make([]*SignerData, len(constructionOptions.ExpectedSigners))
|
|
|
|
for i, signer := range constructionOptions.ExpectedSigners {
|
|
accountInfo, err := c.accountInfo(ctx, signer, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
signersData[i] = accountInfo
|
|
}
|
|
|
|
status, err := c.tmRPC.Status(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
metadataResp := ConstructionMetadata{
|
|
ChainID: status.NodeInfo.Network,
|
|
SignersData: signersData,
|
|
GasLimit: constructionOptions.GasLimit,
|
|
GasPrice: constructionOptions.GasPrice,
|
|
Memo: constructionOptions.Memo,
|
|
}
|
|
|
|
return metadataResp.ToMetadata()
|
|
}
|
|
|
|
func (c *Client) blockTxs(ctx context.Context, height *int64) (crgtypes.BlockTransactionsResponse, error) {
|
|
// get block info
|
|
blockInfo, err := c.tmRPC.Block(ctx, height)
|
|
if err != nil {
|
|
return crgtypes.BlockTransactionsResponse{}, err
|
|
}
|
|
// get block events
|
|
blockResults, err := c.tmRPC.BlockResults(ctx, height)
|
|
if err != nil {
|
|
return crgtypes.BlockTransactionsResponse{}, err
|
|
}
|
|
|
|
if len(blockResults.TxsResults) != len(blockInfo.Block.Txs) {
|
|
// wtf?
|
|
panic("block results transactions do now match block transactions")
|
|
}
|
|
// process begin and end block txs
|
|
beginBlockTx := &rosettatypes.Transaction{
|
|
TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: c.converter.ToRosetta().BeginBlockTxHash(blockInfo.BlockID.Hash)},
|
|
Operations: AddOperationIndexes(
|
|
nil,
|
|
c.converter.ToRosetta().BalanceOps(StatusTxSuccess, blockResults.BeginBlockEvents),
|
|
),
|
|
}
|
|
|
|
endBlockTx := &rosettatypes.Transaction{
|
|
TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: c.converter.ToRosetta().EndBlockTxHash(blockInfo.BlockID.Hash)},
|
|
Operations: AddOperationIndexes(
|
|
nil,
|
|
c.converter.ToRosetta().BalanceOps(StatusTxSuccess, blockResults.EndBlockEvents),
|
|
),
|
|
}
|
|
|
|
deliverTx := make([]*rosettatypes.Transaction, len(blockInfo.Block.Txs))
|
|
// process normal txs
|
|
for i, tx := range blockInfo.Block.Txs {
|
|
rosTx, err := c.converter.ToRosetta().Tx(tx, blockResults.TxsResults[i])
|
|
if err != nil {
|
|
return crgtypes.BlockTransactionsResponse{}, err
|
|
}
|
|
deliverTx[i] = rosTx
|
|
}
|
|
|
|
finalTxs := make([]*rosettatypes.Transaction, 0, 2+len(deliverTx))
|
|
finalTxs = append(finalTxs, beginBlockTx)
|
|
finalTxs = append(finalTxs, deliverTx...)
|
|
finalTxs = append(finalTxs, endBlockTx)
|
|
|
|
return crgtypes.BlockTransactionsResponse{
|
|
BlockResponse: c.converter.ToRosetta().BlockResponse(blockInfo),
|
|
Transactions: finalTxs,
|
|
}, nil
|
|
}
|
|
|
|
func (c *Client) getHeight(ctx context.Context, height *int64) (realHeight *int64, err error) {
|
|
if height != nil && *height == -1 {
|
|
genesisChunk, err := c.tmRPC.GenesisChunked(ctx, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
heightNum, err := extractInitialHeightFromGenesisChunk(genesisChunk.Data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
realHeight = &heightNum
|
|
} else {
|
|
realHeight = height
|
|
}
|
|
return
|
|
}
|
|
|
|
var initialHeightRE = regexp.MustCompile(`"initial_height":"(\d+)"`)
|
|
|
|
func extractInitialHeightFromGenesisChunk(genesisChunk string) (int64, error) {
|
|
firstChunk, err := base64.StdEncoding.DecodeString(genesisChunk)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
matches := initialHeightRE.FindStringSubmatch(string(firstChunk))
|
|
if len(matches) != 2 {
|
|
return 0, errors.New("failed to fetch initial_height")
|
|
}
|
|
|
|
heightStr := matches[1]
|
|
return strconv.ParseInt(heightStr, 10, 64)
|
|
}
|