Rosetta API implementation (#7695)

Ref: #7492

Co-authored-by: Jonathan Gimeno <jgimeno@gmail.com>
Co-authored-by: Alessio Treglia <alessio@tendermint.com>
Co-authored-by: Frojdi Dymylja <33157909+fdymylja@users.noreply.github.com>
Co-authored-by: Robert Zaremba <robert@zaremba.ch>
Co-authored-by: Federico Kunze <31522760+fedekunze@users.noreply.github.com>
This commit is contained in:
SaReN
2021-01-21 09:33:02 +00:00
committed by GitHub
co-authored by Jonathan Gimeno Alessio Treglia Frojdi Dymylja Robert Zaremba Federico Kunze
parent d226254578
commit 57f5e96570
46 changed files with 2887 additions and 203 deletions
+40
View File
@@ -101,6 +101,29 @@ type APIConfig struct {
// Ref: https://github.com/cosmos/cosmos-sdk/issues/6420
}
// RosettaConfig defines the Rosetta API listener configuration.
type RosettaConfig struct {
// Address defines the API server to listen on
Address string `mapstructure:"address"`
// Blockchain defines the blockchain name
// defaults to DefaultBlockchain
Blockchain string `mapstructure:"blockchain"`
// Network defines the network name
Network string `mapstructure:"network"`
// Retries defines the maximum number of retries
// rosetta will do before quitting
Retries int `mapstructure:"retries"`
// Enable defines if the API server should be enabled.
Enable bool `mapstructure:"enable"`
// Offline defines if the server must be run in offline mode
Offline bool `mapstructure:"offline"`
}
// GRPCConfig defines configuration for the gRPC server.
type GRPCConfig struct {
// Enable defines if the gRPC server should be enabled.
@@ -138,6 +161,7 @@ type Config struct {
Telemetry telemetry.Config `mapstructure:"telemetry"`
API APIConfig `mapstructure:"api"`
GRPC GRPCConfig `mapstructure:"grpc"`
Rosetta RosettaConfig `mapstructure:"rosetta"`
GRPCWeb GRPCWebConfig `mapstructure:"grpc-web"`
StateSync StateSyncConfig `mapstructure:"state-sync"`
}
@@ -198,6 +222,14 @@ func DefaultConfig() *Config {
Enable: true,
Address: DefaultGRPCAddress,
},
Rosetta: RosettaConfig{
Enable: false,
Address: ":8080",
Blockchain: "app",
Network: "network",
Retries: 3,
Offline: false,
},
GRPCWeb: GRPCWebConfig{
Enable: true,
Address: DefaultGRPCWebAddress,
@@ -252,6 +284,14 @@ func GetConfig(v *viper.Viper) Config {
RPCMaxBodyBytes: v.GetUint("api.rpc-max-body-bytes"),
EnableUnsafeCORS: v.GetBool("api.enabled-unsafe-cors"),
},
Rosetta: RosettaConfig{
Enable: v.GetBool("rosetta.enable"),
Address: v.GetString("rosetta.address"),
Blockchain: v.GetString("rosetta.blockchain"),
Network: v.GetString("rosetta.network"),
Retries: v.GetInt("rosetta.retries"),
Offline: v.GetBool("rosetta.offline"),
},
GRPC: GRPCConfig{
Enable: v.GetBool("grpc.enable"),
Address: v.GetString("grpc.address"),
+24
View File
@@ -135,6 +135,30 @@ rpc-max-body-bytes = {{ .API.RPCMaxBodyBytes }}
# EnableUnsafeCORS defines if CORS should be enabled (unsafe - use it at your own risk).
enabled-unsafe-cors = {{ .API.EnableUnsafeCORS }}
###############################################################################
### Rosetta Configuration ###
###############################################################################
[rosetta]
# Enable defines if the Rosetta API server should be enabled.
enable = {{ .Rosetta.Enable }}
# Address defines the Rosetta API server to listen on.
address = "{{ .Rosetta.Address }}"
# Network defines the name of the blockchain that will be returned by Rosetta.
blockchain = "{{ .Rosetta.Blockchain }}"
# Network defines the name of the network that will be returned by Rosetta.
network = "{{ .Rosetta.Network }}"
# Retries defines the number of retries when connecting to the node before failing.
retries = {{ .Rosetta.Retries }}
# Offline defines if Rosetta server should run in offline mode.
offline = {{ .Rosetta.Offline }}
###############################################################################
### gRPC Configuration ###
###############################################################################
+42
View File
@@ -0,0 +1,42 @@
package server
import (
"fmt"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/server/rosetta"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
)
// RosettaCommand builds the rosetta root command given
// a protocol buffers serializer/deserializer
func RosettaCommand(ir codectypes.InterfaceRegistry, cdc codec.Marshaler) *cobra.Command {
cmd := &cobra.Command{
Use: "rosetta",
Short: "spin up a rosetta server",
RunE: func(cmd *cobra.Command, args []string) error {
conf, err := rosetta.FromFlags(cmd.Flags())
if err != nil {
return err
}
protoCodec, ok := cdc.(*codec.ProtoCodec)
if !ok {
return fmt.Errorf("exoected *codec.ProtoMarshaler, got: %T", cdc)
}
conf.WithCodec(ir, protoCodec)
rosettaSrv, err := rosetta.ServerFromConfig(conf)
if err != nil {
return err
}
return rosettaSrv.Start()
},
}
rosetta.SetFlags(cmd.Flags())
return cmd
}
+221
View File
@@ -0,0 +1,221 @@
package rosetta
import (
"context"
"encoding/hex"
"strings"
"github.com/btcsuite/btcd/btcec"
"github.com/coinbase/rosetta-sdk-go/types"
crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors"
"github.com/tendermint/tendermint/crypto"
"github.com/cosmos/cosmos-sdk/client/tx"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
)
func (c *Client) OperationStatuses() []*types.OperationStatus {
return []*types.OperationStatus{
{
Status: StatusSuccess,
Successful: true,
},
{
Status: StatusReverted,
Successful: false,
},
}
}
func (c *Client) Version() string {
return c.version
}
func (c *Client) SupportedOperations() []string {
var supportedOperations []string
for _, ii := range c.ir.ListImplementations("cosmos.base.v1beta1.Msg") {
resolve, err := c.ir.Resolve(ii)
if err != nil {
continue
}
if _, ok := resolve.(Msg); ok {
supportedOperations = append(supportedOperations, strings.TrimLeft(ii, "/"))
}
}
supportedOperations = append(supportedOperations, OperationFee)
return supportedOperations
}
func (c *Client) SignedTx(ctx context.Context, txBytes []byte, signatures []*types.Signature) (signedTxBytes []byte, err error) {
TxConfig := c.getTxConfig()
rawTx, err := TxConfig.TxDecoder()(txBytes)
if err != nil {
return nil, err
}
txBldr, err := TxConfig.WrapTxBuilder(rawTx)
if err != nil {
return nil, err
}
var sigs = make([]signing.SignatureV2, len(signatures))
for i, signature := range signatures {
if signature.PublicKey.CurveType != types.Secp256k1 {
return nil, crgerrs.ErrUnsupportedCurve
}
cmp, err := btcec.ParsePubKey(signature.PublicKey.Bytes, btcec.S256())
if err != nil {
return nil, err
}
compressedPublicKey := make([]byte, secp256k1.PubKeySize)
copy(compressedPublicKey, cmp.SerializeCompressed())
pubKey := &secp256k1.PubKey{Key: compressedPublicKey}
accountInfo, err := c.accountInfo(ctx, sdk.AccAddress(pubKey.Address()).String(), nil)
if err != nil {
return nil, err
}
sig := signing.SignatureV2{
PubKey: pubKey,
Data: &signing.SingleSignatureData{
SignMode: signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON,
Signature: signature.Bytes,
},
Sequence: accountInfo.GetSequence(),
}
sigs[i] = sig
}
if err = txBldr.SetSignatures(sigs...); err != nil {
return nil, err
}
txBytes, err = c.getTxConfig().TxEncoder()(txBldr.GetTx())
if err != nil {
return nil, err
}
return txBytes, nil
}
func (c *Client) ConstructionPayload(_ context.Context, request *types.ConstructionPayloadsRequest) (resp *types.ConstructionPayloadsResponse, err error) {
// check if there is at least one operation
if len(request.Operations) < 1 {
return nil, crgerrs.WrapError(crgerrs.ErrInvalidOperation, "expected at least one operation")
}
// convert rosetta operations to sdk msgs and fees (if present)
msgs, fee, err := opsToMsgsAndFees(c.ir, request.Operations)
if err != nil {
return nil, crgerrs.WrapError(crgerrs.ErrInvalidOperation, err.Error())
}
metadata, err := getMetadataFromPayloadReq(request)
if err != nil {
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, err.Error())
}
txFactory := tx.Factory{}.WithAccountNumber(metadata.AccountNumber).WithChainID(metadata.ChainID).
WithGas(metadata.Gas).WithSequence(metadata.Sequence).WithMemo(metadata.Memo).WithFees(fee.String())
TxConfig := c.getTxConfig()
txFactory = txFactory.WithTxConfig(TxConfig)
txBldr, err := tx.BuildUnsignedTx(txFactory, msgs...)
if err != nil {
return nil, err
}
// Sign_mode_legacy_amino is being used as default here, as sign_mode_direct
// needs the signer infos to be set before hand but rosetta doesn't have a way
// to do this yet. To be revisited in future versions of sdk and rosetta
if txFactory.SignMode() == signing.SignMode_SIGN_MODE_UNSPECIFIED {
txFactory = txFactory.WithSignMode(signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON)
}
signerData := authsigning.SignerData{
ChainID: txFactory.ChainID(),
AccountNumber: txFactory.AccountNumber(),
Sequence: txFactory.Sequence(),
}
signBytes, err := TxConfig.SignModeHandler().GetSignBytes(txFactory.SignMode(), signerData, txBldr.GetTx())
if err != nil {
return nil, err
}
txBytes, err := TxConfig.TxEncoder()(txBldr.GetTx())
if err != nil {
return nil, err
}
accIdentifiers := getAccountIdentifiersByMsgs(msgs)
payloads := make([]*types.SigningPayload, len(accIdentifiers))
for i, accID := range accIdentifiers {
payloads[i] = &types.SigningPayload{
AccountIdentifier: accID,
Bytes: crypto.Sha256(signBytes),
SignatureType: types.Ecdsa,
}
}
return &types.ConstructionPayloadsResponse{
UnsignedTransaction: hex.EncodeToString(txBytes),
Payloads: payloads,
}, nil
}
func getAccountIdentifiersByMsgs(msgs []sdk.Msg) []*types.AccountIdentifier {
var accIdentifiers []*types.AccountIdentifier
for _, msg := range msgs {
for _, signer := range msg.GetSigners() {
accIdentifiers = append(accIdentifiers, &types.AccountIdentifier{Address: signer.String()})
}
}
return accIdentifiers
}
func (c *Client) PreprocessOperationsToOptions(_ context.Context, req *types.ConstructionPreprocessRequest) (options map[string]interface{}, err error) {
operations := req.Operations
if len(operations) < 1 {
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "invalid number of operations")
}
msgs, err := opsToMsgs(c.ir, operations)
if err != nil {
return nil, crgerrs.WrapError(crgerrs.ErrInvalidOperation, err.Error())
}
if len(msgs) < 1 || len(msgs[0].GetSigners()) < 1 {
return nil, crgerrs.WrapError(crgerrs.ErrInvalidOperation, "operation produced no msg or signers")
}
memo, ok := req.Metadata["memo"]
if !ok {
memo = ""
}
defaultGas := float64(200000)
gas := req.SuggestedFeeMultiplier
if gas == nil {
gas = &defaultGas
}
return map[string]interface{}{
OptionAddress: msgs[0].GetSigners()[0],
OptionMemo: memo,
OptionGas: gas,
}, nil
}
+447
View File
@@ -0,0 +1,447 @@
package rosetta
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"strconv"
"time"
"github.com/cosmos/cosmos-sdk/version"
abcitypes "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/btcd/btcec"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
"github.com/coinbase/rosetta-sdk-go/types"
"google.golang.org/grpc/metadata"
"github.com/tendermint/tendermint/rpc/client/http"
tmtypes "github.com/tendermint/tendermint/rpc/core/types"
"google.golang.org/grpc"
crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors"
crgtypes "github.com/tendermint/cosmos-rosetta-gateway/types"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
grpctypes "github.com/cosmos/cosmos-sdk/types/grpc"
authclient "github.com/cosmos/cosmos-sdk/x/auth/client"
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 tmWebsocketPath = "/websocket"
const defaultNodeTimeout = 15 * time.Second
// Client implements a single network client to interact with cosmos based chains
type Client struct {
config *Config
auth auth.QueryClient
bank bank.QueryClient
ir codectypes.InterfaceRegistry
clientCtx client.Context
version string
}
func (c *Client) AccountIdentifierFromPublicKey(pubKey *types.PublicKey) (*types.AccountIdentifier, error) {
if pubKey.CurveType != "secp256k1" {
return nil, crgerrs.WrapError(crgerrs.ErrUnsupportedCurve, "only secp256k1 supported")
}
cmp, err := btcec.ParsePubKey(pubKey.Bytes, btcec.S256())
if err != nil {
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, err.Error())
}
compressedPublicKey := make([]byte, secp256k1.PubKeySize)
copy(compressedPublicKey, cmp.SerializeCompressed())
pk := secp256k1.PubKey{Key: compressedPublicKey}
return &types.AccountIdentifier{
Address: sdk.AccAddress(pk.Address()).String(),
}, nil
}
// NewClient instantiates a new online servicer
func NewClient(cfg *Config) (*Client, error) {
info := version.NewInfo()
v := info.Version
if v == "" {
v = "unknown"
}
return &Client{
config: cfg,
ir: cfg.InterfaceRegistry,
version: fmt.Sprintf("%s/%s", info.AppName, v),
}, nil
}
func (c *Client) accountInfo(ctx context.Context, addr string, height *int64) (auth.AccountI, 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)
}
var account auth.AccountI
err = c.ir.UnpackAny(accountInfo.Account, &account)
if err != nil {
return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
}
return account, nil
}
func (c *Client) Balances(ctx context.Context, addr string, height *int64) ([]*types.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 sdkCoinsToRosettaAmounts(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.clientCtx.Client.BlockByHash(ctx, bHash)
if err != nil {
return crgtypes.BlockResponse{}, err
}
return buildBlockResponse(block), nil
}
func (c *Client) BlockByHeight(ctx context.Context, height *int64) (crgtypes.BlockResponse, error) {
block, err := c.clientCtx.Client.Block(ctx, height)
if err != nil {
return crgtypes.BlockResponse{}, err
}
return buildBlockResponse(block), nil
}
func buildBlockResponse(block *tmtypes.ResultBlock) crgtypes.BlockResponse {
return crgtypes.BlockResponse{
Block: TMBlockToRosettaBlockIdentifier(block),
ParentBlock: TMBlockToRosettaParentBlockIdentifier(block),
MillisecondTimestamp: timeToMilliseconds(block.Block.Time),
TxCount: int64(len(block.Block.Txs)),
}
}
func (c *Client) BlockTransactionsByHash(ctx context.Context, hash string) (crgtypes.BlockTransactionsResponse, error) {
blockResp, err := c.BlockByHash(ctx, hash)
if err != nil {
return crgtypes.BlockTransactionsResponse{}, err
}
txs, err := c.listTransactionsInBlock(ctx, blockResp.Block.Index)
if err != nil {
return crgtypes.BlockTransactionsResponse{}, err
}
return crgtypes.BlockTransactionsResponse{
BlockResponse: blockResp,
Transactions: sdkTxsWithHashToRosettaTxs(txs),
}, nil
}
func (c *Client) BlockTransactionsByHeight(ctx context.Context, height *int64) (crgtypes.BlockTransactionsResponse, error) {
blockResp, err := c.BlockByHeight(ctx, height)
if err != nil {
return crgtypes.BlockTransactionsResponse{}, err
}
txs, err := c.listTransactionsInBlock(ctx, blockResp.Block.Index)
if err != nil {
return crgtypes.BlockTransactionsResponse{}, err
}
return crgtypes.BlockTransactionsResponse{
BlockResponse: blockResp,
Transactions: sdkTxsWithHashToRosettaTxs(txs),
}, 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
}
// listTransactionsInBlock returns the list of the transactions in a block given its height
func (c *Client) listTransactionsInBlock(ctx context.Context, height int64) ([]*sdkTxWithHash, error) {
txQuery := fmt.Sprintf(`tx.height=%d`, height)
txList, err := c.clientCtx.Client.TxSearch(ctx, txQuery, true, nil, nil, "")
if err != nil {
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
}
sdkTxs, err := tmResultTxsToSdkTxsWithHash(c.clientCtx.TxConfig.TxDecoder(), txList.Txs)
if err != nil {
return nil, err
}
return sdkTxs, nil
}
func (c *Client) TxOperationsAndSignersAccountIdentifiers(signed bool, txBytes []byte) (ops []*types.Operation, signers []*types.AccountIdentifier, err error) {
txConfig := c.getTxConfig()
rawTx, err := txConfig.TxDecoder()(txBytes)
if err != nil {
return nil, nil, err
}
txBldr, err := txConfig.WrapTxBuilder(rawTx)
if err != nil {
return nil, nil, err
}
var accountIdentifierSigners []*types.AccountIdentifier
if signed {
addrs := txBldr.GetTx().GetSigners()
for _, addr := range addrs {
signer := &types.AccountIdentifier{
Address: addr.String(),
}
accountIdentifierSigners = append(accountIdentifierSigners, signer)
}
}
return sdkTxToOperations(txBldr.GetTx(), false, false), accountIdentifierSigners, nil
}
// GetTx returns a transaction given its hash
func (c *Client) GetTx(_ context.Context, hash string) (*types.Transaction, error) {
txResp, err := authclient.QueryTx(c.clientCtx, hash)
if err != nil {
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
}
var sdkTx sdk.Tx
err = c.ir.UnpackAny(txResp.Tx, &sdkTx)
if err != nil {
return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
}
return sdkTxWithHashToOperations(&sdkTxWithHash{
HexHash: txResp.TxHash,
Code: txResp.Code,
Log: txResp.RawLog,
Tx: sdkTx,
}), nil
}
// GetUnconfirmedTx gets an unconfirmed transaction given its hash
func (c *Client) GetUnconfirmedTx(ctx context.Context, hash string) (*types.Transaction, error) {
res, err := c.clientCtx.Client.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")
}
for _, tx := range res.Txs {
if bytes.Equal(tx.Hash(), hashAsBytes) {
sdkTx, err := tmTxToSdkTx(c.clientCtx.TxConfig.TxDecoder(), tx)
if err != nil {
return nil, err
}
return &types.Transaction{
TransactionIdentifier: TmTxToRosettaTxsIdentifier(tx),
Operations: sdkTxToOperations(sdkTx, false, false),
Metadata: nil,
}, nil
}
}
return nil, crgerrs.WrapError(crgerrs.ErrNotFound, "transaction not found in mempool")
}
// Mempool returns the unconfirmed transactions in the mempool
func (c *Client) Mempool(ctx context.Context) ([]*types.TransactionIdentifier, error) {
txs, err := c.clientCtx.Client.UnconfirmedTxs(ctx, nil)
if err != nil {
return nil, err
}
return TMTxsToRosettaTxsIdentifiers(txs.Txs), nil
}
// Peers gets the number of peers
func (c *Client) Peers(ctx context.Context) ([]*types.Peer, error) {
netInfo, err := c.clientCtx.Client.NetInfo(ctx)
if err != nil {
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
}
return TmPeersToRosettaPeers(netInfo.Peers), nil
}
func (c *Client) Status(ctx context.Context) (*types.SyncStatus, error) {
status, err := c.clientCtx.Client.Status(ctx)
if err != nil {
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
}
return TMStatusToRosettaSyncStatus(status), err
}
func (c *Client) getTxConfig() client.TxConfig {
return c.clientCtx.TxConfig
}
func (c *Client) PostTx(txBytes []byte) (*types.TransactionIdentifier, map[string]interface{}, error) {
// sync ensures it will go through checkTx
res, err := c.clientCtx.BroadcastTxSync(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.RawLog))
}
return &types.TransactionIdentifier{
Hash: res.TxHash,
},
map[string]interface{}{
Log: res.RawLog,
}, nil
}
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
}
addr, ok := options[OptionAddress]
if !ok {
return nil, crgerrs.WrapError(crgerrs.ErrInvalidAddress, "no address provided")
}
addrString, ok := addr.(string)
if !ok {
return nil, crgerrs.WrapError(crgerrs.ErrInvalidAddress, "address is not a string")
}
accountInfo, err := c.accountInfo(ctx, addrString, nil)
if err != nil {
return nil, err
}
gas, ok := options[OptionGas]
if !ok {
return nil, crgerrs.WrapError(crgerrs.ErrInvalidAddress, "gas not set")
}
memo, ok := options[OptionMemo]
if !ok {
return nil, crgerrs.WrapError(crgerrs.ErrInvalidMemo, "memo not set")
}
status, err := c.clientCtx.Client.Status(ctx)
if err != nil {
return nil, err
}
return map[string]interface{}{
OptionAccountNumber: accountInfo.GetAccountNumber(),
OptionSequence: accountInfo.GetSequence(),
OptionChainID: status.NodeInfo.Network,
OptionGas: gas,
OptionMemo: memo,
}, nil
}
func (c *Client) Ready() error {
ctx, cancel := context.WithTimeout(context.Background(), defaultNodeTimeout)
defer cancel()
_, err := c.clientCtx.Client.Health(ctx)
if err != nil {
return err
}
_, err = c.bank.TotalSupply(ctx, &bank.QueryTotalSupplyRequest{})
if err != nil {
return err
}
return nil
}
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, tmWebsocketPath)
if err != nil {
return err
}
authClient := auth.NewQueryClient(grpcConn)
bankClient := bank.NewQueryClient(grpcConn)
// NodeURI and Client are set from here otherwise
// WitNodeURI will require to create a new client
// it's done here because WithNodeURI panics if
// connection to tendermint node fails
clientCtx := client.Context{
Client: tmRPC,
NodeURI: c.config.TendermintRPC,
}
clientCtx = clientCtx.
WithJSONMarshaler(c.config.Codec).
WithInterfaceRegistry(c.config.InterfaceRegistry).
WithTxConfig(authtx.NewTxConfig(c.config.Codec, authtx.DefaultSignModes)).
WithAccountRetriever(auth.AccountRetriever{}).
WithBroadcastMode(flags.BroadcastBlock)
c.auth = authClient
c.bank = bankClient
c.clientCtx = clientCtx
c.ir = c.config.InterfaceRegistry
return nil
}
+22
View File
@@ -0,0 +1,22 @@
package rosetta
import (
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
authcodec "github.com/cosmos/cosmos-sdk/x/auth/types"
bankcodec "github.com/cosmos/cosmos-sdk/x/bank/types"
)
// MakeCodec generates the codec required to interact
// with the cosmos APIs used by the rosetta gateway
func MakeCodec() (*codec.ProtoCodec, codectypes.InterfaceRegistry) {
ir := codectypes.NewInterfaceRegistry()
cdc := codec.NewProtoCodec(ir)
authcodec.RegisterInterfaces(ir)
bankcodec.RegisterInterfaces(ir)
cryptocodec.RegisterInterfaces(ir)
return cdc, ir
}
+203
View File
@@ -0,0 +1,203 @@
package rosetta
import (
"fmt"
"strings"
"time"
"github.com/coinbase/rosetta-sdk-go/types"
"github.com/spf13/pflag"
crg "github.com/tendermint/cosmos-rosetta-gateway/server"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
)
// configuration defaults constants
const (
// DefaultBlockchain defines the default blockchain identifier name
DefaultBlockchain = "app"
// DefaultAddr defines the default rosetta binding address
DefaultAddr = ":8080"
// DefaultRetries is the default number of retries
DefaultRetries = 5
// DefaultTendermintEndpoint is the default value for the tendermint endpoint
DefaultTendermintEndpoint = "localhost:26657"
// DefaultGRPCEndpoint is the default value for the gRPC endpoint
DefaultGRPCEndpoint = "localhost:9090"
// DefaultNetwork defines the default network name
DefaultNetwork = "network"
// DefaultOffline defines the default offline value
DefaultOffline = false
)
// configuration flags
const (
FlagBlockchain = "blockchain"
FlagNetwork = "network"
FlagTendermintEndpoint = "tendermint"
FlagGRPCEndpoint = "grpc"
FlagAddr = "addr"
FlagRetries = "retries"
FlagOffline = "offline"
)
// Config defines the configuration of the rosetta server
type Config struct {
// Blockchain defines the blockchain name
// defaults to DefaultBlockchain
Blockchain string
// Network defines the network name
Network string
// TendermintRPC defines the endpoint to connect to
// tendermint RPC, specifying 'tcp://' before is not
// required, usually it's at port 26657 of the
TendermintRPC string
// GRPCEndpoint defines the cosmos application gRPC endpoint
// usually it is located at 9090 port
GRPCEndpoint string
// Addr defines the default address to bind the rosetta server to
// defaults to DefaultAddr
Addr string
// Retries defines the maximum number of retries
// rosetta will do before quitting
Retries int
// Offline defines if the server must be run in offline mode
Offline bool
// Codec overrides the default data and construction api client codecs
Codec *codec.ProtoCodec
// InterfaceRegistry overrides the default data and construction api interface registry
InterfaceRegistry codectypes.InterfaceRegistry
}
// NetworkIdentifier returns the network identifier given the configuration
func (c *Config) NetworkIdentifier() *types.NetworkIdentifier {
return &types.NetworkIdentifier{
Blockchain: c.Blockchain,
Network: c.Network,
}
}
// validate validates a configuration and sets
// its defaults in case they were not provided
func (c *Config) validate() error {
if (c.Codec == nil) != (c.InterfaceRegistry == nil) {
return fmt.Errorf("codec and interface registry must be both different from nil or nil")
}
if c.Addr == "" {
c.Addr = DefaultAddr
}
if c.Blockchain == "" {
c.Blockchain = DefaultBlockchain
}
if c.Retries == 0 {
c.Retries = DefaultRetries
}
// these are must
if c.Network == "" {
return fmt.Errorf("network not provided")
}
if c.Offline {
return fmt.Errorf("offline mode is not supported for stargate implementation due to how sigv2 works")
}
// these are optional but it must be online
if c.GRPCEndpoint == "" {
return fmt.Errorf("grpc endpoint not provided")
}
if c.TendermintRPC == "" {
return fmt.Errorf("tendermint rpc not provided")
}
if !strings.HasPrefix(c.TendermintRPC, "tcp://") {
c.TendermintRPC = fmt.Sprintf("tcp://%s", c.TendermintRPC)
}
return nil
}
// WithCodec extends the configuration with a predefined Codec
func (c *Config) WithCodec(ir codectypes.InterfaceRegistry, cdc *codec.ProtoCodec) {
c.Codec = cdc
c.InterfaceRegistry = ir
}
// FromFlags gets the configuration from flags
func FromFlags(flags *pflag.FlagSet) (*Config, error) {
blockchain, err := flags.GetString(FlagBlockchain)
if err != nil {
return nil, err
}
network, err := flags.GetString(FlagNetwork)
if err != nil {
return nil, err
}
tendermintRPC, err := flags.GetString(FlagTendermintEndpoint)
if err != nil {
return nil, err
}
gRPCEndpoint, err := flags.GetString(FlagGRPCEndpoint)
if err != nil {
return nil, err
}
addr, err := flags.GetString(FlagAddr)
if err != nil {
return nil, err
}
retries, err := flags.GetInt(FlagRetries)
if err != nil {
return nil, err
}
offline, err := flags.GetBool(FlagOffline)
if err != nil {
return nil, err
}
conf := &Config{
Blockchain: blockchain,
Network: network,
TendermintRPC: tendermintRPC,
GRPCEndpoint: gRPCEndpoint,
Addr: addr,
Retries: retries,
Offline: offline,
}
err = conf.validate()
if err != nil {
return nil, err
}
return conf, nil
}
func ServerFromConfig(conf *Config) (crg.Server, error) {
err := conf.validate()
if err != nil {
return crg.Server{}, err
}
client, err := NewClient(conf)
if err != nil {
return crg.Server{}, err
}
return crg.NewServer(
crg.Settings{
Network: &types.NetworkIdentifier{
Blockchain: conf.Blockchain,
Network: conf.Network,
},
Client: client,
Listen: conf.Addr,
Offline: conf.Offline,
Retries: conf.Retries,
RetryWait: 15 * time.Second,
})
}
// SetFlags sets the configuration flags to the given flagset
func SetFlags(flags *pflag.FlagSet) {
flags.String(FlagBlockchain, DefaultBlockchain, "the blockchain type")
flags.String(FlagNetwork, DefaultNetwork, "the network name")
flags.String(FlagTendermintEndpoint, DefaultTendermintEndpoint, "the tendermint rpc endpoint, without tcp://")
flags.String(FlagGRPCEndpoint, DefaultGRPCEndpoint, "the app gRPC endpoint")
flags.String(FlagAddr, DefaultAddr, "the address rosetta will bind to")
flags.Int(FlagRetries, DefaultRetries, "the number of retries that will be done before quitting")
flags.Bool(FlagOffline, DefaultOffline, "run rosetta only with construction API")
}
+211
View File
@@ -0,0 +1,211 @@
package rosetta
import (
"fmt"
"time"
"github.com/coinbase/rosetta-sdk-go/types"
tmcoretypes "github.com/tendermint/tendermint/rpc/core/types"
tmtypes "github.com/tendermint/tendermint/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// timeToMilliseconds converts time to milliseconds timestamp
func timeToMilliseconds(t time.Time) int64 {
return t.UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond))
}
// sdkCoinsToRosettaAmounts converts []sdk.Coin to rosetta amounts
// availableCoins keeps track of current available coins vs the coins
// owned by an address. This is required to support historical balances
// as rosetta expects them to be set to 0, if an address does not own them
func sdkCoinsToRosettaAmounts(ownedCoins []sdk.Coin, availableCoins sdk.Coins) []*types.Amount {
amounts := make([]*types.Amount, len(availableCoins))
ownedCoinsMap := make(map[string]sdk.Int, len(availableCoins))
for _, ownedCoin := range ownedCoins {
ownedCoinsMap[ownedCoin.Denom] = ownedCoin.Amount
}
for i, coin := range availableCoins {
value, owned := ownedCoinsMap[coin.Denom]
if !owned {
amounts[i] = &types.Amount{
Value: sdk.NewInt(0).String(),
Currency: &types.Currency{
Symbol: coin.Denom,
},
}
continue
}
amounts[i] = &types.Amount{
Value: value.String(),
Currency: &types.Currency{
Symbol: coin.Denom,
},
}
}
return amounts
}
// sdkTxsWithHashToRosettaTxs converts sdk transactions wrapped with their hash to rosetta transactions
func sdkTxsWithHashToRosettaTxs(txs []*sdkTxWithHash) []*types.Transaction {
converted := make([]*types.Transaction, len(txs))
for i, tx := range txs {
converted[i] = sdkTxWithHashToOperations(tx)
}
return converted
}
func sdkTxWithHashToOperations(tx *sdkTxWithHash) *types.Transaction {
hasError := tx.Code != 0
return &types.Transaction{
TransactionIdentifier: &types.TransactionIdentifier{Hash: tx.HexHash},
Operations: sdkTxToOperations(tx.Tx, true, hasError),
Metadata: map[string]interface{}{
Log: tx.Log,
},
}
}
// sdkTxToOperations converts an sdk.Tx to rosetta operations
func sdkTxToOperations(tx sdk.Tx, withStatus, hasError bool) []*types.Operation {
var operations []*types.Operation
msgOps := sdkMsgsToRosettaOperations(tx.GetMsgs(), withStatus, hasError)
operations = append(operations, msgOps...)
feeTx := tx.(sdk.FeeTx)
feeOps := sdkFeeTxToOperations(feeTx, withStatus, len(msgOps))
operations = append(operations, feeOps...)
return operations
}
// sdkFeeTxToOperations converts sdk.FeeTx to rosetta operations
func sdkFeeTxToOperations(feeTx sdk.FeeTx, withStatus bool, previousOps int) []*types.Operation {
feeCoins := feeTx.GetFee()
var ops []*types.Operation
if feeCoins != nil {
var feeOps = rosettaFeeOperationsFromCoins(feeCoins, feeTx.FeePayer().String(), withStatus, previousOps)
ops = append(ops, feeOps...)
}
return ops
}
// rosettaFeeOperationsFromCoins returns the list of rosetta fee operations given sdk coins
func rosettaFeeOperationsFromCoins(coins sdk.Coins, account string, withStatus bool, previousOps int) []*types.Operation {
feeOps := make([]*types.Operation, 0)
var status string
if withStatus {
status = StatusSuccess
}
for i, coin := range coins {
op := &types.Operation{
OperationIdentifier: &types.OperationIdentifier{
Index: int64(previousOps + i),
},
Type: OperationFee,
Status: status,
Account: &types.AccountIdentifier{
Address: account,
},
Amount: &types.Amount{
Value: "-" + coin.Amount.String(),
Currency: &types.Currency{
Symbol: coin.Denom,
},
},
}
feeOps = append(feeOps, op)
}
return feeOps
}
// sdkMsgsToRosettaOperations converts sdk messages to rosetta operations
func sdkMsgsToRosettaOperations(msgs []sdk.Msg, withStatus bool, hasError bool) []*types.Operation {
var operations []*types.Operation
for _, msg := range msgs {
if rosettaMsg, ok := msg.(Msg); ok {
operations = append(operations, rosettaMsg.ToOperations(withStatus, hasError)...)
}
}
return operations
}
// TMTxsToRosettaTxsIdentifiers converts a tendermint raw transactions into an array of rosetta tx identifiers
func TMTxsToRosettaTxsIdentifiers(txs []tmtypes.Tx) []*types.TransactionIdentifier {
converted := make([]*types.TransactionIdentifier, len(txs))
for i, tx := range txs {
converted[i] = TmTxToRosettaTxsIdentifier(tx)
}
return converted
}
// TmTxToRosettaTxsIdentifier converts a tendermint raw transaction into a rosetta tx identifier
func TmTxToRosettaTxsIdentifier(tx tmtypes.Tx) *types.TransactionIdentifier {
return &types.TransactionIdentifier{Hash: fmt.Sprintf("%x", tx.Hash())}
}
// TMBlockToRosettaBlockIdentifier converts a tendermint result block to a rosetta block identifier
func TMBlockToRosettaBlockIdentifier(block *tmcoretypes.ResultBlock) *types.BlockIdentifier {
return &types.BlockIdentifier{
Index: block.Block.Height,
Hash: block.Block.Hash().String(),
}
}
// TmPeersToRosettaPeers converts tendermint peers to rosetta ones
func TmPeersToRosettaPeers(peers []tmcoretypes.Peer) []*types.Peer {
converted := make([]*types.Peer, len(peers))
for i, peer := range peers {
converted[i] = &types.Peer{
PeerID: peer.NodeInfo.Moniker,
Metadata: map[string]interface{}{
"addr": peer.NodeInfo.ListenAddr,
},
}
}
return converted
}
// TMStatusToRosettaSyncStatus converts a tendermint status to rosetta sync status
func TMStatusToRosettaSyncStatus(status *tmcoretypes.ResultStatus) *types.SyncStatus {
// determine sync status
var stage = StageSynced
if status.SyncInfo.CatchingUp {
stage = StageSyncing
}
return &types.SyncStatus{
CurrentIndex: status.SyncInfo.LatestBlockHeight,
TargetIndex: nil, // sync info does not allow us to get target height
Stage: &stage,
}
}
// TMBlockToRosettaParentBlockIdentifier returns the parent block identifier from the last block
func TMBlockToRosettaParentBlockIdentifier(block *tmcoretypes.ResultBlock) *types.BlockIdentifier {
if block.Block.Height == 1 {
return &types.BlockIdentifier{
Index: 1,
Hash: fmt.Sprintf("%X", block.BlockID.Hash.Bytes()),
}
}
return &types.BlockIdentifier{
Index: block.Block.Height - 1,
Hash: fmt.Sprintf("%X", block.Block.LastBlockID.Hash.Bytes()),
}
}
+95
View File
@@ -0,0 +1,95 @@
package rosetta
import (
"fmt"
"strconv"
"strings"
"github.com/gogo/protobuf/jsonpb"
"github.com/coinbase/rosetta-sdk-go/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// opsToMsgsAndFees converts rosetta operations to sdk.Msg and fees represented as sdk.Coins
func opsToMsgsAndFees(interfaceRegistry jsonpb.AnyResolver, ops []*types.Operation) ([]sdk.Msg, sdk.Coins, error) {
var feeAmnt []*types.Amount
var newOps []*types.Operation
var msgType string
// find the fee operation and put it aside
for _, op := range ops {
switch op.Type {
case OperationFee:
amount := op.Amount
feeAmnt = append(feeAmnt, amount)
default:
// check if operation matches the one already used
// as, at the moment, we only support operations
// that represent a single cosmos-sdk message
switch {
// if msgType was not set then set it
case msgType == "":
msgType = op.Type
// if msgType does not match op.Type then it means we're trying to send multiple messages in a single tx
case msgType != op.Type:
return nil, nil, fmt.Errorf("only single message operations are supported: %s - %s", msgType, op.Type)
}
// append operation to new ops list
newOps = append(newOps, op)
}
}
// convert all operations, except fee op to sdk.Msgs
msgs, err := opsToMsgs(interfaceRegistry, newOps)
if err != nil {
return nil, nil, err
}
return msgs, amountsToCoins(feeAmnt), nil
}
// amountsToCoins converts rosetta amounts to sdk coins
func amountsToCoins(amounts []*types.Amount) sdk.Coins {
var feeCoins sdk.Coins
for _, amount := range amounts {
absValue := strings.Trim(amount.Value, "-")
value, err := strconv.ParseInt(absValue, 10, 64)
if err != nil {
return nil
}
coin := sdk.NewCoin(amount.Currency.Symbol, sdk.NewInt(value))
feeCoins = append(feeCoins, coin)
}
return feeCoins
}
func opsToMsgs(interfaceRegistry jsonpb.AnyResolver, ops []*types.Operation) ([]sdk.Msg, error) {
var msgs []sdk.Msg
var operationsByType = make(map[string][]*types.Operation)
for _, op := range ops {
operationsByType[op.Type] = append(operationsByType[op.Type], op)
}
for opName, operations := range operationsByType {
if opName == OperationFee {
continue
}
msgType, err := interfaceRegistry.Resolve("/" + opName) // Types are registered as /proto-name in the interface registry.
if err != nil {
return nil, err
}
if rosettaMsg, ok := msgType.(Msg); ok {
m, err := rosettaMsg.FromOperations(operations)
if err != nil {
return nil, err
}
msgs = append(msgs, m)
}
}
return msgs, nil
}
+41
View File
@@ -0,0 +1,41 @@
package rosetta
import (
"github.com/coinbase/rosetta-sdk-go/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// statuses
const (
StatusSuccess = "Success"
StatusReverted = "Reverted"
StageSynced = "synced"
StageSyncing = "syncing"
)
// misc
const (
Log = "log"
)
// operations
const (
OperationFee = "fee"
)
// options
const (
OptionAccountNumber = "account_number"
OptionAddress = "address"
OptionChainID = "chain_id"
OptionSequence = "sequence"
OptionMemo = "memo"
OptionGas = "gas"
)
type Msg interface {
sdk.Msg
ToOperations(withStatus, hasError bool) []*types.Operation
FromOperations(ops []*types.Operation) (sdk.Msg, error)
}
+112
View File
@@ -0,0 +1,112 @@
package rosetta
import (
"fmt"
"github.com/coinbase/rosetta-sdk-go/types"
tmcoretypes "github.com/tendermint/tendermint/rpc/core/types"
tmtypes "github.com/tendermint/tendermint/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// tmResultTxsToSdkTxsWithHash converts tendermint result txs to cosmos sdk.Tx
func tmResultTxsToSdkTxsWithHash(decode sdk.TxDecoder, txs []*tmcoretypes.ResultTx) ([]*sdkTxWithHash, error) {
converted := make([]*sdkTxWithHash, len(txs))
for i, tx := range txs {
sdkTx, err := decode(tx.Tx)
if err != nil {
return nil, err
}
converted[i] = &sdkTxWithHash{
HexHash: fmt.Sprintf("%X", tx.Tx.Hash()),
Code: tx.TxResult.Code,
Log: tx.TxResult.Log,
Tx: sdkTx,
}
}
return converted, nil
}
func tmTxToSdkTx(decode sdk.TxDecoder, tx tmtypes.Tx) (sdk.Tx, error) {
sdkTx, err := decode(tx)
if err != nil {
return nil, err
}
return sdkTx, err
}
type sdkTxWithHash struct {
HexHash string
Code uint32
Log string
Tx sdk.Tx
}
type PayloadReqMetadata struct {
ChainID string
Sequence uint64
AccountNumber uint64
Gas uint64
Memo string
}
// getMetadataFromPayloadReq obtains the metadata from the request to /construction/payloads endpoint.
func getMetadataFromPayloadReq(req *types.ConstructionPayloadsRequest) (*PayloadReqMetadata, error) {
chainID, ok := req.Metadata[OptionChainID].(string)
if !ok {
return nil, fmt.Errorf("chain_id metadata was not provided")
}
sequence, ok := req.Metadata[OptionSequence]
if !ok {
return nil, fmt.Errorf("sequence metadata was not provided")
}
seqNum, ok := sequence.(float64)
if !ok {
return nil, fmt.Errorf("invalid sequence value")
}
accountNum, ok := req.Metadata[OptionAccountNumber]
if !ok {
return nil, fmt.Errorf("account_number metadata was not provided")
}
accNum, ok := accountNum.(float64)
if !ok {
fmt.Printf("this is type %T", accountNum)
return nil, fmt.Errorf("invalid account_number value")
}
gasNum, ok := req.Metadata[OptionGas]
if !ok {
return nil, fmt.Errorf("gas metadata was not provided")
}
gasF64, ok := gasNum.(float64)
if !ok {
return nil, fmt.Errorf("invalid gas value")
}
memo, ok := req.Metadata[OptionMemo]
if !ok {
memo = ""
}
memoStr, ok := memo.(string)
if !ok {
return nil, fmt.Errorf("invalid memo")
}
return &PayloadReqMetadata{
ChainID: chainID,
Sequence: uint64(seqNum),
AccountNumber: uint64(accNum),
Gas: uint64(gasF64),
Memo: memoStr,
}, nil
}
+43 -2
View File
@@ -9,7 +9,14 @@ import (
"runtime/pprof"
"time"
"github.com/cosmos/cosmos-sdk/server/rosetta"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/spf13/cobra"
"google.golang.org/grpc"
crgserver "github.com/tendermint/cosmos-rosetta-gateway/server"
"github.com/tendermint/tendermint/abci/server"
tcmd "github.com/tendermint/tendermint/cmd/tendermint/commands"
tmos "github.com/tendermint/tendermint/libs/os"
@@ -18,7 +25,6 @@ import (
pvm "github.com/tendermint/tendermint/privval"
"github.com/tendermint/tendermint/proxy"
"github.com/tendermint/tendermint/rpc/client/local"
"google.golang.org/grpc"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
@@ -280,7 +286,6 @@ func startInProcess(ctx *Context, clientCtx client.Context, appCreator types.App
}
var apiSrv *api.Server
if config.API.Enable {
genDoc, err := genDocProvider()
if err != nil {
@@ -326,6 +331,42 @@ func startInProcess(ctx *Context, clientCtx client.Context, appCreator types.App
}
}
var rosettaSrv crgserver.Server
if config.Rosetta.Enable {
offlineMode := config.Rosetta.Offline
if !config.GRPC.Enable { // If GRPC is not enabled rosetta cannot work in online mode, so it works in offline mode.
offlineMode = true
}
conf := &rosetta.Config{
Blockchain: config.Rosetta.Blockchain,
Network: config.Rosetta.Network,
TendermintRPC: ctx.Config.RPC.ListenAddress,
GRPCEndpoint: config.GRPC.Address,
Addr: config.Rosetta.Address,
Retries: config.Rosetta.Retries,
Offline: offlineMode,
}
conf.WithCodec(clientCtx.InterfaceRegistry, clientCtx.JSONMarshaler.(*codec.ProtoCodec))
rosettaSrv, err = rosetta.ServerFromConfig(conf)
if err != nil {
return err
}
errCh := make(chan error)
go func() {
if err := rosettaSrv.Start(); err != nil {
errCh <- err
}
}()
select {
case err := <-errCh:
return err
case <-time.After(5 * time.Second): // assume server started successfully
}
}
defer func() {
if tmNode.IsRunning() {
_ = tmNode.Stop()