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:
co-authored by
Jonathan Gimeno
Alessio Treglia
Frojdi Dymylja
Robert Zaremba
Federico Kunze
parent
d226254578
commit
57f5e96570
@@ -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"),
|
||||
|
||||
@@ -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 ###
|
||||
###############################################################################
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user