fix: misc fixes for cosmos-rosetta (#13583)
### Description Closes: https://github.com/cosmos/cosmos-sdk/issues/13083 https://github.com/cosmos/cosmos-sdk/issues/11402 https://github.com/cosmos/cosmos-sdk/issues/10678 https://github.com/cosmos/cosmos-sdk/issues/12358 https://github.com/cosmos/cosmos-sdk/issues/10776 https://github.com/cosmos/cosmos-sdk/issues/12934 ### Author Checklist *All items are required. Please add a note to the item if the item is not applicable and please add links to any relevant follow up issues.* I have... - [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [x] added `!` to the type prefix if API or client breaking change - [x] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md#pr-targeting)) - [x] provided a link to the relevant issue or specification - [x] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/main/docs/building-modules) - [x] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md#testing) - [x] added a changelog entry to `CHANGELOG.md` - [x] included comments for [documenting Go code](https://blog.golang.org/godoc) - [x] updated the relevant documentation or specification - [x] reviewed "Files changed" and left comments if necessary - [ ] confirmed all CI checks have passed ### Reviewers Checklist *All items are required. Please add a note if the item is not applicable and please add your handle next to the items reviewed if you only reviewed selected items.* I have... - [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] confirmed `!` in the type prefix if API or client breaking change - [ ] confirmed all author checklist items have been addressed - [ ] reviewed state machine logic - [ ] reviewed API design and naming - [ ] reviewed documentation is accurate - [ ] reviewed tests and test coverage - [ ] manually tested (if applicable)
This commit is contained in:
@@ -1,41 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
"github.com/cosmos/cosmos-sdk/server/rosetta"
|
||||
)
|
||||
|
||||
// RosettaCommand builds the rosetta root command given
|
||||
// a protocol buffers serializer/deserializer
|
||||
func RosettaCommand(ir codectypes.InterfaceRegistry, cdc codec.Codec) *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
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
package rosetta
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/coinbase/rosetta-sdk-go/types"
|
||||
|
||||
crgerrs "github.com/cosmos/cosmos-sdk/server/rosetta/lib/errors"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// ---------- cosmos-rosetta-gateway.types.NetworkInformationProvider implementation ------------ //
|
||||
|
||||
func (c *Client) OperationStatuses() []*types.OperationStatus {
|
||||
return []*types.OperationStatus{
|
||||
{
|
||||
Status: StatusTxSuccess,
|
||||
Successful: true,
|
||||
},
|
||||
{
|
||||
Status: StatusTxReverted,
|
||||
Successful: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Version() string {
|
||||
return c.version
|
||||
}
|
||||
|
||||
func (c *Client) SupportedOperations() []string {
|
||||
return c.supportedOperations
|
||||
}
|
||||
|
||||
// ---------- cosmos-rosetta-gateway.types.OfflineClient implementation ------------ //
|
||||
|
||||
func (c *Client) SignedTx(_ context.Context, txBytes []byte, signatures []*types.Signature) (signedTxBytes []byte, err error) {
|
||||
return c.converter.ToSDK().SignedTx(txBytes, signatures)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
tx, err := c.converter.ToSDK().UnsignedTx(request.Operations)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrInvalidOperation, err.Error())
|
||||
}
|
||||
|
||||
metadata := new(ConstructionMetadata)
|
||||
if err = metadata.FromMetadata(request.Metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txBytes, payloads, err := c.converter.ToRosetta().SigningComponents(tx, metadata, request.PublicKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.ConstructionPayloadsResponse{
|
||||
UnsignedTransaction: hex.EncodeToString(txBytes),
|
||||
Payloads: payloads,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) PreprocessOperationsToOptions(_ context.Context, req *types.ConstructionPreprocessRequest) (response *types.ConstructionPreprocessResponse, err error) {
|
||||
if len(req.Operations) == 0 {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "no operations")
|
||||
}
|
||||
|
||||
// now we need to parse the operations to cosmos sdk messages
|
||||
tx, err := c.converter.ToSDK().UnsignedTx(req.Operations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get the signers
|
||||
signers := tx.GetSigners()
|
||||
signersStr := make([]string, len(signers))
|
||||
accountIdentifiers := make([]*types.AccountIdentifier, len(signers))
|
||||
|
||||
for i, sig := range signers {
|
||||
addr := sig.String()
|
||||
signersStr[i] = addr
|
||||
accountIdentifiers[i] = &types.AccountIdentifier{
|
||||
Address: addr,
|
||||
}
|
||||
}
|
||||
// get the metadata request information
|
||||
meta := new(ConstructionPreprocessMetadata)
|
||||
err = meta.FromMetadata(req.Metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if meta.GasPrice == "" {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "no gas prices")
|
||||
}
|
||||
|
||||
if meta.GasLimit == 0 {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "no gas limit")
|
||||
}
|
||||
|
||||
// prepare the options to return
|
||||
options := &PreprocessOperationsOptionsResponse{
|
||||
ExpectedSigners: signersStr,
|
||||
Memo: meta.Memo,
|
||||
GasLimit: meta.GasLimit,
|
||||
GasPrice: meta.GasPrice,
|
||||
}
|
||||
|
||||
metaOptions, err := options.ToMetadata()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &types.ConstructionPreprocessResponse{
|
||||
Options: metaOptions,
|
||||
RequiredPublicKeys: accountIdentifiers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) AccountIdentifierFromPublicKey(pubKey *types.PublicKey) (*types.AccountIdentifier, error) {
|
||||
pk, err := c.converter.ToSDK().PubKey(pubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.AccountIdentifier{
|
||||
Address: sdk.AccAddress(pk.Address()).String(),
|
||||
}, nil
|
||||
}
|
||||
@@ -1,552 +0,0 @@
|
||||
package rosetta
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/version"
|
||||
|
||||
abcitypes "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
rosettatypes "github.com/coinbase/rosetta-sdk-go/types"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"github.com/tendermint/tendermint/rpc/client/http"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
crgerrs "github.com/cosmos/cosmos-sdk/server/rosetta/lib/errors"
|
||||
crgtypes "github.com/cosmos/cosmos-sdk/server/rosetta/lib/types"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
grpctypes "github.com/cosmos/cosmos-sdk/types/grpc"
|
||||
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"
|
||||
|
||||
tmrpc "github.com/tendermint/tendermint/rpc/client"
|
||||
)
|
||||
|
||||
// interface assertion
|
||||
var _ crgtypes.Client = (*Client)(nil)
|
||||
|
||||
const (
|
||||
defaultNodeTimeout = time.Minute
|
||||
tmWebsocketPath = "/websocket"
|
||||
)
|
||||
|
||||
// Client implements a single network client to interact with cosmos based chains
|
||||
type Client struct {
|
||||
supportedOperations []string
|
||||
|
||||
config *Config
|
||||
|
||||
auth auth.QueryClient
|
||||
bank bank.QueryClient
|
||||
tmRPC tmrpc.Client
|
||||
|
||||
version string
|
||||
|
||||
converter Converter
|
||||
}
|
||||
|
||||
// NewClient instantiates a new online servicer
|
||||
func NewClient(cfg *Config) (*Client, error) {
|
||||
info := version.NewInfo()
|
||||
|
||||
v := info.Version
|
||||
if v == "" {
|
||||
v = "unknown"
|
||||
}
|
||||
|
||||
txConfig := authtx.NewTxConfig(cfg.Codec, authtx.DefaultSignModes)
|
||||
|
||||
var supportedOperations []string
|
||||
for _, ii := range cfg.InterfaceRegistry.ListImplementations(sdk.MsgInterfaceProtoName) {
|
||||
resolvedMsg, err := cfg.InterfaceRegistry.Resolve(ii)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := resolvedMsg.(sdk.Msg); ok {
|
||||
supportedOperations = append(supportedOperations, ii)
|
||||
}
|
||||
}
|
||||
|
||||
supportedOperations = append(
|
||||
supportedOperations,
|
||||
bank.EventTypeCoinSpent,
|
||||
bank.EventTypeCoinReceived,
|
||||
bank.EventTypeCoinBurn,
|
||||
)
|
||||
|
||||
return &Client{
|
||||
supportedOperations: supportedOperations,
|
||||
config: cfg,
|
||||
auth: nil,
|
||||
bank: nil,
|
||||
tmRPC: nil,
|
||||
version: fmt.Sprintf("%s/%s", info.AppName, v),
|
||||
converter: NewConverter(cfg.Codec, cfg.InterfaceRegistry, txConfig),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ---------- cosmos-rosetta-gateway.types.Client implementation ------------ //
|
||||
|
||||
// Bootstrap is gonna connect the client to the endpoints
|
||||
func (c *Client) Bootstrap() error {
|
||||
grpcConn, err := grpc.Dial(c.config.GRPCEndpoint, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
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)
|
||||
|
||||
c.auth = authClient
|
||||
c.bank = bankClient
|
||||
c.tmRPC = tmRPC
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ready performs a health check and returns an error if the client is not ready.
|
||||
func (c *Client) Ready() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultNodeTimeout)
|
||||
defer cancel()
|
||||
_, err := c.tmRPC.Health(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// to prevent timeout of reading genesis block
|
||||
var height int64 = -1
|
||||
_, err = c.BlockByHeight(ctx, &height)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.bank.TotalSupply(ctx, &bank.QueryTotalSupplyRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) accountInfo(ctx context.Context, addr string, height *int64) (*SignerData, error) {
|
||||
if height != nil {
|
||||
strHeight := strconv.FormatInt(*height, 10)
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, grpctypes.GRPCBlockHeightHeader, strHeight)
|
||||
}
|
||||
|
||||
accountInfo, err := c.auth.Account(ctx, &auth.QueryAccountRequest{
|
||||
Address: addr,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, crgerrs.FromGRPCToRosettaError(err)
|
||||
}
|
||||
|
||||
signerData, err := c.converter.ToRosetta().SignerData(accountInfo.Account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return signerData, nil
|
||||
}
|
||||
|
||||
func (c *Client) Balances(ctx context.Context, addr string, height *int64) ([]*rosettatypes.Amount, error) {
|
||||
if height != nil {
|
||||
strHeight := strconv.FormatInt(*height, 10)
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, grpctypes.GRPCBlockHeightHeader, strHeight)
|
||||
}
|
||||
|
||||
balance, err := c.bank.AllBalances(ctx, &bank.QueryAllBalancesRequest{
|
||||
Address: addr,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, crgerrs.FromGRPCToRosettaError(err)
|
||||
}
|
||||
|
||||
availableCoins, err := c.coins(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.converter.ToRosetta().Amounts(balance.Balances, availableCoins), nil
|
||||
}
|
||||
|
||||
func (c *Client) BlockByHash(ctx context.Context, hash string) (crgtypes.BlockResponse, error) {
|
||||
bHash, err := hex.DecodeString(hash)
|
||||
if err != nil {
|
||||
return crgtypes.BlockResponse{}, fmt.Errorf("invalid block hash: %s", err)
|
||||
}
|
||||
|
||||
block, err := c.tmRPC.BlockByHash(ctx, bHash)
|
||||
if err != nil {
|
||||
return crgtypes.BlockResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error())
|
||||
}
|
||||
|
||||
return c.converter.ToRosetta().BlockResponse(block), nil
|
||||
}
|
||||
|
||||
func (c *Client) BlockByHeight(ctx context.Context, height *int64) (crgtypes.BlockResponse, error) {
|
||||
height, err := c.getHeight(ctx, height)
|
||||
if err != nil {
|
||||
return crgtypes.BlockResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error())
|
||||
}
|
||||
block, err := c.tmRPC.Block(ctx, height)
|
||||
if err != nil {
|
||||
return crgtypes.BlockResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error())
|
||||
}
|
||||
|
||||
return c.converter.ToRosetta().BlockResponse(block), nil
|
||||
}
|
||||
|
||||
func (c *Client) BlockTransactionsByHash(ctx context.Context, hash string) (crgtypes.BlockTransactionsResponse, error) {
|
||||
// TODO(fdymylja): use a faster path, by searching the block by hash, instead of doing a double query operation
|
||||
blockResp, err := c.BlockByHash(ctx, hash)
|
||||
if err != nil {
|
||||
return crgtypes.BlockTransactionsResponse{}, err
|
||||
}
|
||||
|
||||
return c.blockTxs(ctx, &blockResp.Block.Index)
|
||||
}
|
||||
|
||||
func (c *Client) BlockTransactionsByHeight(ctx context.Context, height *int64) (crgtypes.BlockTransactionsResponse, error) {
|
||||
height, err := c.getHeight(ctx, height)
|
||||
if err != nil {
|
||||
return crgtypes.BlockTransactionsResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error())
|
||||
}
|
||||
blockTxResp, err := c.blockTxs(ctx, height)
|
||||
if err != nil {
|
||||
return crgtypes.BlockTransactionsResponse{}, err
|
||||
}
|
||||
return blockTxResp, nil
|
||||
}
|
||||
|
||||
// Coins fetches the existing coins in the application
|
||||
func (c *Client) coins(ctx context.Context) (sdk.Coins, error) {
|
||||
supply, err := c.bank.TotalSupply(ctx, &bank.QueryTotalSupplyRequest{})
|
||||
if err != nil {
|
||||
return nil, crgerrs.FromGRPCToRosettaError(err)
|
||||
}
|
||||
return supply.Supply, nil
|
||||
}
|
||||
|
||||
func (c *Client) TxOperationsAndSignersAccountIdentifiers(signed bool, txBytes []byte) (ops []*rosettatypes.Operation, signers []*rosettatypes.AccountIdentifier, err error) {
|
||||
switch signed {
|
||||
case false:
|
||||
rosTx, err := c.converter.ToRosetta().Tx(txBytes, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return rosTx.Operations, nil, err
|
||||
default:
|
||||
ops, signers, err = c.converter.ToRosetta().OpsAndSigners(txBytes)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// GetTx returns a transaction given its hash. For Rosetta we make a synthetic transaction for BeginBlock
|
||||
//
|
||||
// and EndBlock to adhere to balance tracking rules.
|
||||
func (c *Client) GetTx(ctx context.Context, hash string) (*rosettatypes.Transaction, error) {
|
||||
hashBytes, err := hex.DecodeString(hash)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("bad tx hash: %s", err))
|
||||
}
|
||||
|
||||
// get tx type and hash
|
||||
txType, hashBytes := c.converter.ToSDK().HashToTxType(hashBytes)
|
||||
|
||||
// construct rosetta tx
|
||||
switch txType {
|
||||
// handle begin block hash
|
||||
case BeginBlockTx:
|
||||
// get block height by hash
|
||||
block, err := c.tmRPC.BlockByHash(ctx, hashBytes)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
|
||||
}
|
||||
|
||||
// get block txs
|
||||
fullBlock, err := c.blockTxs(ctx, &block.Block.Height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fullBlock.Transactions[0], nil
|
||||
// handle deliver tx hash
|
||||
case DeliverTxTx:
|
||||
rawTx, err := c.tmRPC.Tx(ctx, hashBytes, true)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
|
||||
}
|
||||
return c.converter.ToRosetta().Tx(rawTx.Tx, &rawTx.TxResult)
|
||||
// handle end block hash
|
||||
case EndBlockTx:
|
||||
// get block height by hash
|
||||
block, err := c.tmRPC.BlockByHash(ctx, hashBytes)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
|
||||
}
|
||||
|
||||
// get block txs
|
||||
fullBlock, err := c.blockTxs(ctx, &block.Block.Height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get last tx
|
||||
return fullBlock.Transactions[len(fullBlock.Transactions)-1], nil
|
||||
// unrecognized tx
|
||||
default:
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("invalid tx hash provided: %s", hash))
|
||||
}
|
||||
}
|
||||
|
||||
// GetUnconfirmedTx gets an unconfirmed transaction given its hash
|
||||
func (c *Client) GetUnconfirmedTx(ctx context.Context, hash string) (*rosettatypes.Transaction, error) {
|
||||
res, err := c.tmRPC.UnconfirmedTxs(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrNotFound, "unconfirmed tx not found")
|
||||
}
|
||||
|
||||
hashAsBytes, err := hex.DecodeString(hash)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrInterpreting, "invalid hash")
|
||||
}
|
||||
|
||||
// assert that correct tx length is provided
|
||||
switch len(hashAsBytes) {
|
||||
default:
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("unrecognized tx size: %d", len(hashAsBytes)))
|
||||
case BeginEndBlockTxSize:
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "endblock and begin block txs cannot be unconfirmed")
|
||||
case DeliverTxSize:
|
||||
break
|
||||
}
|
||||
|
||||
// iterate over unconfirmed txs to find the one with matching hash
|
||||
for _, unconfirmedTx := range res.Txs {
|
||||
if !bytes.Equal(unconfirmedTx.Hash(), hashAsBytes) {
|
||||
continue
|
||||
}
|
||||
|
||||
return c.converter.ToRosetta().Tx(unconfirmedTx, nil)
|
||||
}
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrNotFound, "transaction not found in mempool: "+hash)
|
||||
}
|
||||
|
||||
// Mempool returns the unconfirmed transactions in the mempool
|
||||
func (c *Client) Mempool(ctx context.Context) ([]*rosettatypes.TransactionIdentifier, error) {
|
||||
txs, err := c.tmRPC.UnconfirmedTxs(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.converter.ToRosetta().TxIdentifiers(txs.Txs), nil
|
||||
}
|
||||
|
||||
// Peers gets the number of peers
|
||||
func (c *Client) Peers(ctx context.Context) ([]*rosettatypes.Peer, error) {
|
||||
netInfo, err := c.tmRPC.NetInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
|
||||
}
|
||||
return c.converter.ToRosetta().Peers(netInfo.Peers), nil
|
||||
}
|
||||
|
||||
func (c *Client) Status(ctx context.Context) (*rosettatypes.SyncStatus, error) {
|
||||
status, err := c.tmRPC.Status(ctx)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
|
||||
}
|
||||
return c.converter.ToRosetta().SyncStatus(status), err
|
||||
}
|
||||
|
||||
func (c *Client) PostTx(txBytes []byte) (*rosettatypes.TransactionIdentifier, map[string]interface{}, error) {
|
||||
// sync ensures it will go through checkTx
|
||||
res, err := c.tmRPC.BroadcastTxSync(context.Background(), txBytes)
|
||||
if err != nil {
|
||||
return nil, nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
|
||||
}
|
||||
// check if tx was broadcast successfully
|
||||
if res.Code != abcitypes.CodeTypeOK {
|
||||
return nil, nil, crgerrs.WrapError(
|
||||
crgerrs.ErrUnknown,
|
||||
fmt.Sprintf("transaction broadcast failure: (%d) %s ", res.Code, res.Log),
|
||||
)
|
||||
}
|
||||
|
||||
return &rosettatypes.TransactionIdentifier{
|
||||
Hash: fmt.Sprintf("%X", res.Hash),
|
||||
},
|
||||
map[string]interface{}{
|
||||
Log: res.Log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// construction endpoints
|
||||
|
||||
// ConstructionMetadataFromOptions builds the metadata given the options
|
||||
func (c *Client) ConstructionMetadataFromOptions(ctx context.Context, options map[string]interface{}) (meta map[string]interface{}, err error) {
|
||||
if len(options) == 0 {
|
||||
return nil, crgerrs.ErrBadArgument
|
||||
}
|
||||
|
||||
constructionOptions := new(PreprocessOperationsOptionsResponse)
|
||||
|
||||
err = constructionOptions.FromMetadata(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if default fees suggestion is enabled and gas limit or price is unset, use default
|
||||
if c.config.EnableFeeSuggestion {
|
||||
if constructionOptions.GasLimit <= 0 {
|
||||
constructionOptions.GasLimit = uint64(c.config.GasToSuggest)
|
||||
}
|
||||
if constructionOptions.GasPrice == "" {
|
||||
denom := c.config.DenomToSuggest
|
||||
constructionOptions.GasPrice = c.config.GasPrices.AmountOf(denom).String() + denom
|
||||
}
|
||||
}
|
||||
|
||||
if constructionOptions.GasLimit > 0 && constructionOptions.GasPrice != "" {
|
||||
gasPrice, err := sdk.ParseDecCoin(constructionOptions.GasPrice)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !gasPrice.IsPositive() {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "gas price must be positive")
|
||||
}
|
||||
}
|
||||
|
||||
signersData := make([]*SignerData, len(constructionOptions.ExpectedSigners))
|
||||
|
||||
for i, signer := range constructionOptions.ExpectedSigners {
|
||||
accountInfo, err := c.accountInfo(ctx, signer, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signersData[i] = accountInfo
|
||||
}
|
||||
|
||||
status, err := c.tmRPC.Status(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metadataResp := ConstructionMetadata{
|
||||
ChainID: status.NodeInfo.Network,
|
||||
SignersData: signersData,
|
||||
GasLimit: constructionOptions.GasLimit,
|
||||
GasPrice: constructionOptions.GasPrice,
|
||||
Memo: constructionOptions.Memo,
|
||||
}
|
||||
|
||||
return metadataResp.ToMetadata()
|
||||
}
|
||||
|
||||
func (c *Client) blockTxs(ctx context.Context, height *int64) (crgtypes.BlockTransactionsResponse, error) {
|
||||
// get block info
|
||||
blockInfo, err := c.tmRPC.Block(ctx, height)
|
||||
if err != nil {
|
||||
return crgtypes.BlockTransactionsResponse{}, err
|
||||
}
|
||||
// get block events
|
||||
blockResults, err := c.tmRPC.BlockResults(ctx, height)
|
||||
if err != nil {
|
||||
return crgtypes.BlockTransactionsResponse{}, err
|
||||
}
|
||||
|
||||
if len(blockResults.TxsResults) != len(blockInfo.Block.Txs) {
|
||||
// wtf?
|
||||
panic("block results transactions do now match block transactions")
|
||||
}
|
||||
// process begin and end block txs
|
||||
beginBlockTx := &rosettatypes.Transaction{
|
||||
TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: c.converter.ToRosetta().BeginBlockTxHash(blockInfo.BlockID.Hash)},
|
||||
Operations: AddOperationIndexes(
|
||||
nil,
|
||||
c.converter.ToRosetta().BalanceOps(StatusTxSuccess, blockResults.BeginBlockEvents),
|
||||
),
|
||||
}
|
||||
|
||||
endBlockTx := &rosettatypes.Transaction{
|
||||
TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: c.converter.ToRosetta().EndBlockTxHash(blockInfo.BlockID.Hash)},
|
||||
Operations: AddOperationIndexes(
|
||||
nil,
|
||||
c.converter.ToRosetta().BalanceOps(StatusTxSuccess, blockResults.EndBlockEvents),
|
||||
),
|
||||
}
|
||||
|
||||
deliverTx := make([]*rosettatypes.Transaction, len(blockInfo.Block.Txs))
|
||||
// process normal txs
|
||||
for i, tx := range blockInfo.Block.Txs {
|
||||
rosTx, err := c.converter.ToRosetta().Tx(tx, blockResults.TxsResults[i])
|
||||
if err != nil {
|
||||
return crgtypes.BlockTransactionsResponse{}, err
|
||||
}
|
||||
deliverTx[i] = rosTx
|
||||
}
|
||||
|
||||
finalTxs := make([]*rosettatypes.Transaction, 0, 2+len(deliverTx))
|
||||
finalTxs = append(finalTxs, beginBlockTx)
|
||||
finalTxs = append(finalTxs, deliverTx...)
|
||||
finalTxs = append(finalTxs, endBlockTx)
|
||||
|
||||
return crgtypes.BlockTransactionsResponse{
|
||||
BlockResponse: c.converter.ToRosetta().BlockResponse(blockInfo),
|
||||
Transactions: finalTxs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) getHeight(ctx context.Context, height *int64) (realHeight *int64, err error) {
|
||||
if height != nil && *height == -1 {
|
||||
genesisChunk, err := c.tmRPC.GenesisChunked(ctx, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
heightNum, err := extractInitialHeightFromGenesisChunk(genesisChunk.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
realHeight = &heightNum
|
||||
} else {
|
||||
realHeight = height
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var initialHeightRE = regexp.MustCompile(`"initial_height":"(\d+)"`)
|
||||
|
||||
func extractInitialHeightFromGenesisChunk(genesisChunk string) (int64, error) {
|
||||
firstChunk, err := base64.StdEncoding.DecodeString(genesisChunk)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
matches := initialHeightRE.FindStringSubmatch(string(firstChunk))
|
||||
if len(matches) != 2 {
|
||||
return 0, errors.New("failed to fetch initial_height")
|
||||
}
|
||||
|
||||
heightStr := matches[1]
|
||||
return strconv.ParseInt(heightStr, 10, 64)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package rosetta
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRegex(t *testing.T) {
|
||||
genesisChuck := base64.StdEncoding.EncodeToString([]byte(`"genesis_time":"2021-09-28T09:00:00Z","chain_id":"bombay-12","initial_height":"5900001","consensus_params":{"block":{"max_bytes":"5000000","max_gas":"1000000000","time_iota_ms":"1000"},"evidence":{"max_age_num_blocks":"100000","max_age_duration":"172800000000000","max_bytes":"50000"},"validator":{"pub_key_types":["ed25519"]},"version":{}},"validators":[{"address":"EEA4891F5F8D523A6B4B3EAC84B5C08655A00409","pub_key":{"type":"tendermint/PubKeyEd25519","value":"UX71gTBNumQq42qRd6j/K8XN/y3/HAcuAJxj97utawI="},"power":"60612","name":"BTC.Secure"},{"address":"973F589DE1CC8A54ABE2ABE0E0A4ABF13A9EBAE4","pub_key":{"type":"tendermint/PubKeyEd25519","value":"AmGQvQSAAXzSIscx/6o4rVdRMT9QvairQHaCXsWhY+c="},"power":"835","name":"MoonletWallet"},{"address":"831F402BDA0C9A3F260D4F221780BC22A4C3FB23","pub_key":{"type":"tendermint/PubKeyEd25519","value":"Tw8yKbPNEo113ZNbJJ8joeXokoMdBoazRTwb1NQ77WA="},"power":"102842","name":"BlockNgine"},{"address":"F2683F267D2B4C8714B44D68612DB37A8DD2EED7","pub_key":{"type":"tendermint/PubKeyEd25519","value":"PVE4IcWDE6QEqJSEkx55IDkg5zxBo8tVRzKFMJXYFSQ="},"power":"23200","name":"Luna Station 88"},{"address":"9D2428CBAC68C654BE11BE405344C560E6A0F626","pub_key":{"type":"tendermint/PubKeyEd25519","value":"93hzGmZjPRqOnQkb8BULjqanW3M2p1qIcLVTGkf1Zhk="},"power":"35420","name":"Terra-India"},{"address":"DC9897F22E74BF1B66E2640FA461F785F9BA7627","pub_key":{"type":"tendermint/PubKeyEd25519","value":"mlYb/Dzqwh0YJjfH59OZ4vtp+Zhdq5Oj5MNaGHq1X0E="},"power":"25163","name":"SolidStake"},{"address":"AA1A027E270A2BD7AF154999E6DE9D39C5711DE7","pub_key":{"type":"tendermint/PubKeyEd25519","value":"28z8FlpbC7sR0f1Q8OWFASDNi0FAmdldzetwQ07JJzg="},"power":"34529","name":"syncnode"},{"address":"E548735750DC5015ADDE3B0E7A1294C3B868680B","pub_key":{"type":"tendermint/PubKeyEd25519","value":"BTDtLSKp4wpQrWBwmGvp9isWC5jXaAtX1nrJtsCEWew="},"power":"36082","name":"OneStar"}`))
|
||||
height, err := extractInitialHeightFromGenesisChunk(genesisChuck)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, height, int64(5900001))
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
package rosetta
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coinbase/rosetta-sdk-go/types"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
crg "github.com/cosmos/cosmos-sdk/server/rosetta/lib/server"
|
||||
|
||||
clientflags "github.com/cosmos/cosmos-sdk/client/flags"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/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
|
||||
// DefaultEnableFeeSuggestion indicates to use fee suggestion if `construction/metadata` is called without gas limit and price
|
||||
DefaultEnableFeeSuggestion = false
|
||||
// DenomToSuggest defines the default denom for fee suggestion
|
||||
DenomToSuggest = "uatom"
|
||||
// DefaultPrices defines the default list of prices to suggest
|
||||
DefaultPrices = "1uatom,1stake"
|
||||
)
|
||||
|
||||
// configuration flags
|
||||
const (
|
||||
FlagBlockchain = "blockchain"
|
||||
FlagNetwork = "network"
|
||||
FlagTendermintEndpoint = "tendermint"
|
||||
FlagGRPCEndpoint = "grpc"
|
||||
FlagAddr = "addr"
|
||||
FlagRetries = "retries"
|
||||
FlagOffline = "offline"
|
||||
FlagEnableFeeSuggestion = "enable-fee-suggestion"
|
||||
FlagGasToSuggest = "gas-to-suggest"
|
||||
FlagDenomToSuggest = "denom-to-suggest"
|
||||
FlagPricesToSuggest = "prices-to-suggest"
|
||||
)
|
||||
|
||||
// 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
|
||||
// EnableFeeSuggestion indicates to use fee suggestion when `construction/metadata` is called without gas limit and price
|
||||
EnableFeeSuggestion bool
|
||||
// GasToSuggest defines the gas limit for fee suggestion
|
||||
GasToSuggest int
|
||||
// DenomToSuggest defines the default denom for fee suggestion
|
||||
DenomToSuggest string
|
||||
// GasPrices defines the gas prices for fee suggestion
|
||||
GasPrices sdk.DecCoins
|
||||
// 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.GasToSuggest <= 0 {
|
||||
return fmt.Errorf("gas to suggest must be positive")
|
||||
}
|
||||
if c.EnableFeeSuggestion {
|
||||
found := false
|
||||
for i := 0; i < c.GasPrices.Len(); i++ {
|
||||
if c.GasPrices.GetDenomByIndex(i) == c.DenomToSuggest {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("default suggest denom is not found in prices to suggest")
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
enableDefaultFeeSuggestion, err := flags.GetBool(FlagEnableFeeSuggestion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gasToSuggest, err := flags.GetInt(FlagGasToSuggest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
denomToSuggest, err := flags.GetString(FlagDenomToSuggest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var prices sdk.DecCoins
|
||||
if enableDefaultFeeSuggestion {
|
||||
pricesToSuggest, err := flags.GetString(FlagPricesToSuggest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prices, err = sdk.ParseDecCoins(pricesToSuggest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
conf := &Config{
|
||||
Blockchain: blockchain,
|
||||
Network: network,
|
||||
TendermintRPC: tendermintRPC,
|
||||
GRPCEndpoint: gRPCEndpoint,
|
||||
Addr: addr,
|
||||
Retries: retries,
|
||||
Offline: offline,
|
||||
EnableFeeSuggestion: enableDefaultFeeSuggestion,
|
||||
GasToSuggest: gasToSuggest,
|
||||
DenomToSuggest: denomToSuggest,
|
||||
GasPrices: prices,
|
||||
}
|
||||
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")
|
||||
flags.Bool(FlagEnableFeeSuggestion, DefaultEnableFeeSuggestion, "enable default fee suggestion")
|
||||
flags.Int(FlagGasToSuggest, clientflags.DefaultGasLimit, "default gas for fee suggestion")
|
||||
flags.String(FlagDenomToSuggest, DenomToSuggest, "default denom for fee suggestion")
|
||||
flags.String(FlagPricesToSuggest, DefaultPrices, "default prices for fee suggestion")
|
||||
}
|
||||
@@ -1,768 +0,0 @@
|
||||
package rosetta
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"cosmossdk.io/math"
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
rosettatypes "github.com/coinbase/rosetta-sdk-go/types"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
tmcoretypes "github.com/tendermint/tendermint/rpc/core/types"
|
||||
tmtypes "github.com/tendermint/tendermint/types"
|
||||
|
||||
sdkclient "github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
|
||||
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
|
||||
crgerrs "github.com/cosmos/cosmos-sdk/server/rosetta/lib/errors"
|
||||
crgtypes "github.com/cosmos/cosmos-sdk/server/rosetta/lib/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/tx/signing"
|
||||
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
|
||||
auth "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
)
|
||||
|
||||
// Converter is a utility that can be used to convert
|
||||
// back and forth from rosetta to sdk and tendermint types
|
||||
// IMPORTANT NOTES:
|
||||
// - IT SHOULD BE USED ONLY TO DEAL WITH THINGS
|
||||
// IN A STATELESS WAY! IT SHOULD NEVER INTERACT DIRECTLY
|
||||
// WITH TENDERMINT RPC AND COSMOS GRPC
|
||||
//
|
||||
// - IT SHOULD RETURN cosmos rosetta gateway error types!
|
||||
type Converter interface {
|
||||
// ToSDK exposes the methods that convert
|
||||
// rosetta types to cosmos sdk and tendermint types
|
||||
ToSDK() ToSDKConverter
|
||||
// ToRosetta exposes the methods that convert
|
||||
// sdk and tendermint types to rosetta types
|
||||
ToRosetta() ToRosettaConverter
|
||||
}
|
||||
|
||||
// ToRosettaConverter is an interface that exposes
|
||||
// all the functions used to convert sdk and
|
||||
// tendermint types to rosetta known types
|
||||
type ToRosettaConverter interface {
|
||||
// BlockResponse returns a block response given a result block
|
||||
BlockResponse(block *tmcoretypes.ResultBlock) crgtypes.BlockResponse
|
||||
// BeginBlockToTx converts the given begin block hash to rosetta transaction hash
|
||||
BeginBlockTxHash(blockHash []byte) string
|
||||
// EndBlockTxHash converts the given endblock hash to rosetta transaction hash
|
||||
EndBlockTxHash(blockHash []byte) string
|
||||
// Amounts converts sdk.Coins to rosetta.Amounts
|
||||
Amounts(ownedCoins []sdk.Coin, availableCoins sdk.Coins) []*rosettatypes.Amount
|
||||
// Ops converts an sdk.Msg to rosetta operations
|
||||
Ops(status string, msg sdk.Msg) ([]*rosettatypes.Operation, error)
|
||||
// OpsAndSigners takes raw transaction bytes and returns rosetta operations and the expected signers
|
||||
OpsAndSigners(txBytes []byte) (ops []*rosettatypes.Operation, signers []*rosettatypes.AccountIdentifier, err error)
|
||||
// Meta converts an sdk.Msg to rosetta metadata
|
||||
Meta(msg sdk.Msg) (meta map[string]interface{}, err error)
|
||||
// SignerData returns account signing data from a queried any account
|
||||
SignerData(anyAccount *codectypes.Any) (*SignerData, error)
|
||||
// SigningComponents returns rosetta's components required to build a signable transaction
|
||||
SigningComponents(tx authsigning.Tx, metadata *ConstructionMetadata, rosPubKeys []*rosettatypes.PublicKey) (txBytes []byte, payloadsToSign []*rosettatypes.SigningPayload, err error)
|
||||
// Tx converts a tendermint transaction and tx result if provided to a rosetta tx
|
||||
Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rosettatypes.Transaction, error)
|
||||
// TxIdentifiers converts a tendermint tx to transaction identifiers
|
||||
TxIdentifiers(txs []tmtypes.Tx) []*rosettatypes.TransactionIdentifier
|
||||
// BalanceOps converts events to balance operations
|
||||
BalanceOps(status string, events []abci.Event) []*rosettatypes.Operation
|
||||
// SyncStatus converts a tendermint status to sync status
|
||||
SyncStatus(status *tmcoretypes.ResultStatus) *rosettatypes.SyncStatus
|
||||
// Peers converts tendermint peers to rosetta
|
||||
Peers(peers []tmcoretypes.Peer) []*rosettatypes.Peer
|
||||
}
|
||||
|
||||
// ToSDKConverter is an interface that exposes
|
||||
// all the functions used to convert rosetta types
|
||||
// to tendermint and sdk types
|
||||
type ToSDKConverter interface {
|
||||
// UnsignedTx converts rosetta operations to an unsigned cosmos sdk transactions
|
||||
UnsignedTx(ops []*rosettatypes.Operation) (tx authsigning.Tx, err error)
|
||||
// SignedTx adds the provided signatures after decoding the unsigned transaction raw bytes
|
||||
// and returns the signed tx bytes
|
||||
SignedTx(txBytes []byte, signatures []*rosettatypes.Signature) (signedTxBytes []byte, err error)
|
||||
// Msg converts metadata to an sdk message
|
||||
Msg(meta map[string]interface{}, msg sdk.Msg) (err error)
|
||||
// HashToTxType returns the transaction type (end block, begin block or deliver tx)
|
||||
// and the real hash to query in order to get information
|
||||
HashToTxType(hashBytes []byte) (txType TransactionType, realHash []byte)
|
||||
// PubKey attempts to convert a rosetta public key to cosmos sdk one
|
||||
PubKey(pk *rosettatypes.PublicKey) (cryptotypes.PubKey, error)
|
||||
}
|
||||
|
||||
type converter struct {
|
||||
newTxBuilder func() sdkclient.TxBuilder
|
||||
txBuilderFromTx func(tx sdk.Tx) (sdkclient.TxBuilder, error)
|
||||
txDecode sdk.TxDecoder
|
||||
txEncode sdk.TxEncoder
|
||||
bytesToSign func(tx authsigning.Tx, signerData authsigning.SignerData) (b []byte, err error)
|
||||
ir codectypes.InterfaceRegistry
|
||||
cdc *codec.ProtoCodec
|
||||
}
|
||||
|
||||
func NewConverter(cdc *codec.ProtoCodec, ir codectypes.InterfaceRegistry, cfg sdkclient.TxConfig) Converter {
|
||||
return converter{
|
||||
newTxBuilder: cfg.NewTxBuilder,
|
||||
txBuilderFromTx: cfg.WrapTxBuilder,
|
||||
txDecode: cfg.TxDecoder(),
|
||||
txEncode: cfg.TxEncoder(),
|
||||
bytesToSign: func(tx authsigning.Tx, signerData authsigning.SignerData) (b []byte, err error) {
|
||||
bytesToSign, err := cfg.SignModeHandler().GetSignBytes(signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON, signerData, tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return crypto.Sha256(bytesToSign), nil
|
||||
},
|
||||
ir: ir,
|
||||
cdc: cdc,
|
||||
}
|
||||
}
|
||||
|
||||
func (c converter) ToSDK() ToSDKConverter {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c converter) ToRosetta() ToRosettaConverter {
|
||||
return c
|
||||
}
|
||||
|
||||
// OpsToUnsignedTx returns all the sdk.Msgs given the operations
|
||||
func (c converter) UnsignedTx(ops []*rosettatypes.Operation) (tx authsigning.Tx, err error) {
|
||||
builder := c.newTxBuilder()
|
||||
|
||||
var msgs []sdk.Msg
|
||||
|
||||
for i := 0; i < len(ops); i++ {
|
||||
op := ops[i]
|
||||
|
||||
protoMessage, err := c.ir.Resolve(op.Type)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "operation not found: "+op.Type)
|
||||
}
|
||||
|
||||
msg, ok := protoMessage.(sdk.Msg)
|
||||
if !ok {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "operation is not a valid supported sdk.Msg: "+op.Type)
|
||||
}
|
||||
|
||||
err = c.Msg(op.Metadata, msg)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
// verify message correctness
|
||||
if err = msg.ValidateBasic(); err != nil {
|
||||
return nil, crgerrs.WrapError(
|
||||
crgerrs.ErrBadArgument,
|
||||
fmt.Sprintf("validation of operation at index %d failed: %s", op.OperationIdentifier.Index, err),
|
||||
)
|
||||
}
|
||||
signers := msg.GetSigners()
|
||||
// check if there are enough signers
|
||||
if len(signers) == 0 {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("operation at index %d got no signers", op.OperationIdentifier.Index))
|
||||
}
|
||||
// append the msg
|
||||
msgs = append(msgs, msg)
|
||||
// if there's only one signer then simply continue
|
||||
if len(signers) == 1 {
|
||||
continue
|
||||
}
|
||||
// after we have got the msg, we need to verify if the message has multiple signers
|
||||
// if it has got multiple signers, then we need to fetch all the related operations
|
||||
// which involve the other signers of the msg, we expect to find them in order
|
||||
// so if the msg is named "v1.test.Send" and it expects 3 signers, the next 3 operations
|
||||
// must be with the same name "v1.test.Send" and contain the other signers
|
||||
// then we can just skip their processing
|
||||
for j := 0; j < len(signers)-1; j++ {
|
||||
skipOp := ops[i+j] // get the next index
|
||||
// verify that the operation is equal to the new one
|
||||
if skipOp.Type != op.Type {
|
||||
return nil, crgerrs.WrapError(
|
||||
crgerrs.ErrBadArgument,
|
||||
fmt.Sprintf("operation at index %d should have had type %s got: %s", i+j, op.Type, skipOp.Type),
|
||||
)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(op.Metadata, skipOp.Metadata) {
|
||||
return nil, crgerrs.WrapError(
|
||||
crgerrs.ErrBadArgument,
|
||||
fmt.Sprintf("operation at index %d should have had metadata equal to %#v, got: %#v", i+j, op.Metadata, skipOp.Metadata))
|
||||
}
|
||||
|
||||
i++ // increase so we skip it
|
||||
}
|
||||
}
|
||||
|
||||
if err := builder.SetMsgs(msgs...); err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, err.Error())
|
||||
}
|
||||
|
||||
return builder.GetTx(), nil
|
||||
}
|
||||
|
||||
// Msg unmarshals the rosetta metadata to the given sdk.Msg
|
||||
func (c converter) Msg(meta map[string]interface{}, msg sdk.Msg) error {
|
||||
metaBytes, err := json.Marshal(meta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.cdc.UnmarshalJSON(metaBytes, msg)
|
||||
}
|
||||
|
||||
func (c converter) Meta(msg sdk.Msg) (meta map[string]interface{}, err error) {
|
||||
b, err := c.cdc.MarshalJSON(msg)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
err = json.Unmarshal(b, &meta)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Ops will create an operation for each msg signer
|
||||
// with the message proto name as type, and the raw fields
|
||||
// as metadata
|
||||
func (c converter) Ops(status string, msg sdk.Msg) ([]*rosettatypes.Operation, error) {
|
||||
opName := sdk.MsgTypeURL(msg)
|
||||
|
||||
meta, err := c.Meta(msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ops := make([]*rosettatypes.Operation, len(msg.GetSigners()))
|
||||
for i, signer := range msg.GetSigners() {
|
||||
op := &rosettatypes.Operation{
|
||||
Type: opName,
|
||||
Status: &status,
|
||||
Account: &rosettatypes.AccountIdentifier{Address: signer.String()},
|
||||
Metadata: meta,
|
||||
}
|
||||
|
||||
ops[i] = op
|
||||
}
|
||||
|
||||
return ops, nil
|
||||
}
|
||||
|
||||
// Tx converts a tendermint raw transaction and its result (if provided) to a rosetta transaction
|
||||
func (c converter) Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rosettatypes.Transaction, error) {
|
||||
// decode tx
|
||||
tx, err := c.txDecode(rawTx)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
// get initial status, as per sdk design, if one msg fails
|
||||
// the whole TX will be considered failing, so we can't have
|
||||
// 1 msg being success and 1 msg being reverted
|
||||
status := StatusTxSuccess
|
||||
switch txResult {
|
||||
// if nil, we're probably checking an unconfirmed tx
|
||||
// or trying to build a new transaction, so status
|
||||
// is not put inside
|
||||
case nil:
|
||||
status = ""
|
||||
// set the status
|
||||
default:
|
||||
if txResult.Code != abci.CodeTypeOK {
|
||||
status = StatusTxReverted
|
||||
}
|
||||
}
|
||||
// get operations from msgs
|
||||
msgs := tx.GetMsgs()
|
||||
var rawTxOps []*rosettatypes.Operation
|
||||
|
||||
for _, msg := range msgs {
|
||||
ops, err := c.Ops(status, msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawTxOps = append(rawTxOps, ops...)
|
||||
}
|
||||
|
||||
// now get balance events from response deliver tx
|
||||
var balanceOps []*rosettatypes.Operation
|
||||
// tx result might be nil, in case we're querying an unconfirmed tx from the mempool
|
||||
if txResult != nil {
|
||||
balanceOps = c.BalanceOps(StatusTxSuccess, txResult.Events) // force set to success because no events for failed tx
|
||||
}
|
||||
|
||||
// now normalize indexes
|
||||
totalOps := AddOperationIndexes(rawTxOps, balanceOps)
|
||||
|
||||
return &rosettatypes.Transaction{
|
||||
TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: fmt.Sprintf("%X", rawTx.Hash())},
|
||||
Operations: totalOps,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c converter) BalanceOps(status string, events []abci.Event) []*rosettatypes.Operation {
|
||||
var ops []*rosettatypes.Operation
|
||||
|
||||
for _, e := range events {
|
||||
balanceOps, ok := sdkEventToBalanceOperations(status, e)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ops = append(ops, balanceOps...)
|
||||
}
|
||||
|
||||
return ops
|
||||
}
|
||||
|
||||
// sdkEventToBalanceOperations converts an event to a rosetta balance operation
|
||||
// it will panic if the event is malformed because it might mean the sdk spec
|
||||
// has changed and rosetta needs to reflect those changes too.
|
||||
// The balance operations are multiple, one for each denom.
|
||||
func sdkEventToBalanceOperations(status string, event abci.Event) (operations []*rosettatypes.Operation, isBalanceEvent bool) {
|
||||
var (
|
||||
accountIdentifier string
|
||||
coinChange sdk.Coins
|
||||
isSub bool
|
||||
)
|
||||
|
||||
switch event.Type {
|
||||
default:
|
||||
return nil, false
|
||||
case banktypes.EventTypeCoinSpent:
|
||||
spender := sdk.MustAccAddressFromBech32(event.Attributes[0].Value)
|
||||
coins, err := sdk.ParseCoinsNormalized(event.Attributes[1].Value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
isSub = true
|
||||
coinChange = coins
|
||||
accountIdentifier = spender.String()
|
||||
|
||||
case banktypes.EventTypeCoinReceived:
|
||||
receiver := sdk.MustAccAddressFromBech32(event.Attributes[0].Value)
|
||||
coins, err := sdk.ParseCoinsNormalized(event.Attributes[1].Value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
isSub = false
|
||||
coinChange = coins
|
||||
accountIdentifier = receiver.String()
|
||||
|
||||
// rosetta does not have the concept of burning coins, so we need to mock
|
||||
// the burn as a send to an address that cannot be resolved to anything
|
||||
case banktypes.EventTypeCoinBurn:
|
||||
coins, err := sdk.ParseCoinsNormalized(event.Attributes[1].Value)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
coinChange = coins
|
||||
accountIdentifier = BurnerAddressIdentifier
|
||||
}
|
||||
|
||||
operations = make([]*rosettatypes.Operation, len(coinChange))
|
||||
|
||||
for i, coin := range coinChange {
|
||||
|
||||
value := coin.Amount.String()
|
||||
// in case the event is a subtract balance one the rewrite value with
|
||||
// the negative coin identifier
|
||||
if isSub {
|
||||
value = "-" + value
|
||||
}
|
||||
|
||||
op := &rosettatypes.Operation{
|
||||
Type: event.Type,
|
||||
Status: &status,
|
||||
Account: &rosettatypes.AccountIdentifier{Address: accountIdentifier},
|
||||
Amount: &rosettatypes.Amount{
|
||||
Value: value,
|
||||
Currency: &rosettatypes.Currency{
|
||||
Symbol: coin.Denom,
|
||||
Decimals: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
operations[i] = op
|
||||
}
|
||||
return operations, true
|
||||
}
|
||||
|
||||
// Amounts converts []sdk.Coin to rosetta amounts
|
||||
func (c converter) Amounts(ownedCoins []sdk.Coin, availableCoins sdk.Coins) []*rosettatypes.Amount {
|
||||
amounts := make([]*rosettatypes.Amount, len(availableCoins))
|
||||
ownedCoinsMap := make(map[string]math.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] = &rosettatypes.Amount{
|
||||
Value: sdk.NewInt(0).String(),
|
||||
Currency: &rosettatypes.Currency{
|
||||
Symbol: coin.Denom,
|
||||
},
|
||||
}
|
||||
continue
|
||||
}
|
||||
amounts[i] = &rosettatypes.Amount{
|
||||
Value: value.String(),
|
||||
Currency: &rosettatypes.Currency{
|
||||
Symbol: coin.Denom,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return amounts
|
||||
}
|
||||
|
||||
// AddOperationIndexes adds the indexes to operations adhering to specific rules:
|
||||
// operations related to messages will be always before than the balance ones
|
||||
func AddOperationIndexes(msgOps []*rosettatypes.Operation, balanceOps []*rosettatypes.Operation) (finalOps []*rosettatypes.Operation) {
|
||||
lenMsgOps := len(msgOps)
|
||||
lenBalanceOps := len(balanceOps)
|
||||
finalOps = make([]*rosettatypes.Operation, 0, lenMsgOps+lenBalanceOps)
|
||||
|
||||
var currentIndex int64
|
||||
// add indexes to msg ops
|
||||
for _, op := range msgOps {
|
||||
op.OperationIdentifier = &rosettatypes.OperationIdentifier{
|
||||
Index: currentIndex,
|
||||
}
|
||||
|
||||
finalOps = append(finalOps, op)
|
||||
currentIndex++
|
||||
}
|
||||
|
||||
// add indexes to balance ops
|
||||
for _, op := range balanceOps {
|
||||
op.OperationIdentifier = &rosettatypes.OperationIdentifier{
|
||||
Index: currentIndex,
|
||||
}
|
||||
|
||||
finalOps = append(finalOps, op)
|
||||
currentIndex++
|
||||
}
|
||||
|
||||
return finalOps
|
||||
}
|
||||
|
||||
// EndBlockTxHash produces a mock endblock hash that rosetta can query
|
||||
// for endblock operations, it also serves the purpose of representing
|
||||
// part of the state changes happening at endblock level (balance ones)
|
||||
func (c converter) EndBlockTxHash(hash []byte) string {
|
||||
final := append([]byte{EndBlockHashStart}, hash...)
|
||||
return fmt.Sprintf("%X", final)
|
||||
}
|
||||
|
||||
// BeginBlockTxHash produces a mock beginblock hash that rosetta can query
|
||||
// for beginblock operations, it also serves the purpose of representing
|
||||
// part of the state changes happening at beginblock level (balance ones)
|
||||
func (c converter) BeginBlockTxHash(hash []byte) string {
|
||||
final := append([]byte{BeginBlockHashStart}, hash...)
|
||||
return fmt.Sprintf("%X", final)
|
||||
}
|
||||
|
||||
// HashToTxType takes the provided hash bytes from rosetta and discerns if they are
|
||||
// a deliver tx type or endblock/begin block hash, returning the real hash afterwards
|
||||
func (c converter) HashToTxType(hashBytes []byte) (txType TransactionType, realHash []byte) {
|
||||
switch len(hashBytes) {
|
||||
case DeliverTxSize:
|
||||
return DeliverTxTx, hashBytes
|
||||
|
||||
case BeginEndBlockTxSize:
|
||||
switch hashBytes[0] {
|
||||
case BeginBlockHashStart:
|
||||
return BeginBlockTx, hashBytes[1:]
|
||||
case EndBlockHashStart:
|
||||
return EndBlockTx, hashBytes[1:]
|
||||
default:
|
||||
return UnrecognizedTx, nil
|
||||
}
|
||||
|
||||
default:
|
||||
return UnrecognizedTx, nil
|
||||
}
|
||||
}
|
||||
|
||||
// StatusToSyncStatus converts a tendermint status to rosetta sync status
|
||||
func (c converter) SyncStatus(status *tmcoretypes.ResultStatus) *rosettatypes.SyncStatus {
|
||||
// determine sync status
|
||||
stage := StatusPeerSynced
|
||||
if status.SyncInfo.CatchingUp {
|
||||
stage = StatusPeerSyncing
|
||||
}
|
||||
|
||||
return &rosettatypes.SyncStatus{
|
||||
CurrentIndex: &status.SyncInfo.LatestBlockHeight,
|
||||
TargetIndex: nil, // sync info does not allow us to get target height
|
||||
Stage: &stage,
|
||||
}
|
||||
}
|
||||
|
||||
// TxIdentifiers converts a tendermint raw transactions into an array of rosetta tx identifiers
|
||||
func (c converter) TxIdentifiers(txs []tmtypes.Tx) []*rosettatypes.TransactionIdentifier {
|
||||
converted := make([]*rosettatypes.TransactionIdentifier, len(txs))
|
||||
for i, tx := range txs {
|
||||
converted[i] = &rosettatypes.TransactionIdentifier{Hash: fmt.Sprintf("%X", tx.Hash())}
|
||||
}
|
||||
|
||||
return converted
|
||||
}
|
||||
|
||||
// tmResultBlockToRosettaBlockResponse converts a tendermint result block to block response
|
||||
func (c converter) BlockResponse(block *tmcoretypes.ResultBlock) crgtypes.BlockResponse {
|
||||
var parentBlock *rosettatypes.BlockIdentifier
|
||||
|
||||
switch block.Block.Height {
|
||||
case 1:
|
||||
parentBlock = &rosettatypes.BlockIdentifier{
|
||||
Index: 1,
|
||||
Hash: fmt.Sprintf("%X", block.BlockID.Hash.Bytes()),
|
||||
}
|
||||
default:
|
||||
parentBlock = &rosettatypes.BlockIdentifier{
|
||||
Index: block.Block.Height - 1,
|
||||
Hash: fmt.Sprintf("%X", block.Block.LastBlockID.Hash.Bytes()),
|
||||
}
|
||||
}
|
||||
return crgtypes.BlockResponse{
|
||||
Block: &rosettatypes.BlockIdentifier{
|
||||
Index: block.Block.Height,
|
||||
Hash: block.Block.Hash().String(),
|
||||
},
|
||||
ParentBlock: parentBlock,
|
||||
MillisecondTimestamp: timeToMilliseconds(block.Block.Time),
|
||||
TxCount: int64(len(block.Block.Txs)),
|
||||
}
|
||||
}
|
||||
|
||||
// Peers converts tm peers to rosetta peers
|
||||
func (c converter) Peers(peers []tmcoretypes.Peer) []*rosettatypes.Peer {
|
||||
converted := make([]*rosettatypes.Peer, len(peers))
|
||||
|
||||
for i, peer := range peers {
|
||||
converted[i] = &rosettatypes.Peer{
|
||||
PeerID: peer.NodeInfo.Moniker,
|
||||
Metadata: map[string]interface{}{
|
||||
"addr": peer.NodeInfo.ListenAddr,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return converted
|
||||
}
|
||||
|
||||
// OpsAndSigners takes transactions bytes and returns the operation, is signed is true it will return
|
||||
// the account identifiers which have signed the transaction
|
||||
func (c converter) OpsAndSigners(txBytes []byte) (ops []*rosettatypes.Operation, signers []*rosettatypes.AccountIdentifier, err error) {
|
||||
rosTx, err := c.ToRosetta().Tx(txBytes, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ops = rosTx.Operations
|
||||
|
||||
// get the signers
|
||||
sdkTx, err := c.txDecode(txBytes)
|
||||
if err != nil {
|
||||
return nil, nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
txBuilder, err := c.txBuilderFromTx(sdkTx)
|
||||
if err != nil {
|
||||
return nil, nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
for _, signer := range txBuilder.GetTx().GetSigners() {
|
||||
signers = append(signers, &rosettatypes.AccountIdentifier{
|
||||
Address: signer.String(),
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c converter) SignedTx(txBytes []byte, signatures []*rosettatypes.Signature) (signedTxBytes []byte, err error) {
|
||||
rawTx, err := c.txDecode(txBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txBuilder, err := c.txBuilderFromTx(rawTx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notSignedSigs, err := txBuilder.GetTx().GetSignaturesV2() //
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
if len(notSignedSigs) != len(signatures) {
|
||||
return nil, crgerrs.WrapError(
|
||||
crgerrs.ErrInvalidTransaction,
|
||||
fmt.Sprintf("expected transaction to have signers data matching the provided signatures: %d <-> %d", len(notSignedSigs), len(signatures)))
|
||||
}
|
||||
|
||||
signedSigs := make([]signing.SignatureV2, len(notSignedSigs))
|
||||
for i, signature := range signatures {
|
||||
// TODO(fdymylja): here we should check that the public key matches...
|
||||
signedSigs[i] = signing.SignatureV2{
|
||||
PubKey: notSignedSigs[i].PubKey,
|
||||
Data: &signing.SingleSignatureData{
|
||||
SignMode: signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON,
|
||||
Signature: signature.Bytes,
|
||||
},
|
||||
Sequence: notSignedSigs[i].Sequence,
|
||||
}
|
||||
}
|
||||
|
||||
if err = txBuilder.SetSignatures(signedSigs...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txBytes, err = c.txEncode(txBuilder.GetTx())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return txBytes, nil
|
||||
}
|
||||
|
||||
func (c converter) PubKey(pubKey *rosettatypes.PublicKey) (cryptotypes.PubKey, error) {
|
||||
if pubKey.CurveType != "secp256k1" {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrUnsupportedCurve, "only secp256k1 supported")
|
||||
}
|
||||
|
||||
cmp, err := btcec.ParsePubKey(pubKey.Bytes)
|
||||
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 pk, nil
|
||||
}
|
||||
|
||||
// SigningComponents takes a sdk tx and construction metadata and returns signable components
|
||||
func (c converter) SigningComponents(tx authsigning.Tx, metadata *ConstructionMetadata, rosPubKeys []*rosettatypes.PublicKey) (txBytes []byte, payloadsToSign []*rosettatypes.SigningPayload, err error) {
|
||||
// verify metadata correctness
|
||||
feeAmount, err := sdk.ParseCoinsNormalized(metadata.GasPrice)
|
||||
if err != nil {
|
||||
return nil, nil, crgerrs.WrapError(crgerrs.ErrBadArgument, err.Error())
|
||||
}
|
||||
|
||||
signers := tx.GetSigners()
|
||||
// assert the signers data provided in options are the same as the expected signing accounts
|
||||
// and that the number of rosetta provided public keys equals the one of the signers
|
||||
if len(metadata.SignersData) != len(signers) || len(signers) != len(rosPubKeys) {
|
||||
return nil, nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "signers data and account identifiers mismatch")
|
||||
}
|
||||
|
||||
// add transaction metadata
|
||||
builder, err := c.txBuilderFromTx(tx)
|
||||
if err != nil {
|
||||
return nil, nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
builder.SetFeeAmount(feeAmount)
|
||||
builder.SetGasLimit(metadata.GasLimit)
|
||||
builder.SetMemo(metadata.Memo)
|
||||
|
||||
// build signatures
|
||||
partialSignatures := make([]signing.SignatureV2, len(signers))
|
||||
payloadsToSign = make([]*rosettatypes.SigningPayload, len(signers))
|
||||
|
||||
// pub key ordering matters, in a future release this check might be relaxed
|
||||
for i, signer := range signers {
|
||||
// assert that the provided public keys are correctly ordered
|
||||
// by checking if the signer at index i matches the pubkey at index
|
||||
pubKey, err := c.ToSDK().PubKey(rosPubKeys[0])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !bytes.Equal(pubKey.Address().Bytes(), signer.Bytes()) {
|
||||
return nil, nil, crgerrs.WrapError(
|
||||
crgerrs.ErrBadArgument,
|
||||
fmt.Sprintf("public key at index %d does not match the expected transaction signer: %X <-> %X", i, rosPubKeys[i].Bytes, signer.Bytes()),
|
||||
)
|
||||
}
|
||||
|
||||
// set the signer data
|
||||
signerData := authsigning.SignerData{
|
||||
Address: signer.String(),
|
||||
ChainID: metadata.ChainID,
|
||||
AccountNumber: metadata.SignersData[i].AccountNumber,
|
||||
Sequence: metadata.SignersData[i].Sequence,
|
||||
PubKey: pubKey,
|
||||
}
|
||||
|
||||
// get signature bytes
|
||||
signBytes, err := c.bytesToSign(tx, signerData)
|
||||
if err != nil {
|
||||
return nil, nil, crgerrs.WrapError(crgerrs.ErrUnknown, fmt.Sprintf("unable to sign tx: %s", err.Error()))
|
||||
}
|
||||
|
||||
// set payload
|
||||
payloadsToSign[i] = &rosettatypes.SigningPayload{
|
||||
AccountIdentifier: &rosettatypes.AccountIdentifier{Address: signer.String()},
|
||||
Bytes: signBytes,
|
||||
SignatureType: rosettatypes.Ecdsa,
|
||||
}
|
||||
|
||||
// set partial signature
|
||||
partialSignatures[i] = signing.SignatureV2{
|
||||
PubKey: pubKey,
|
||||
Data: &signing.SingleSignatureData{}, // needs to be set to empty otherwise the codec will cry
|
||||
Sequence: metadata.SignersData[i].Sequence,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// now we set the partial signatures in the tx
|
||||
// because we will need to decode the sequence
|
||||
// information of each account in a stateless way
|
||||
err = builder.SetSignatures(partialSignatures...)
|
||||
if err != nil {
|
||||
return nil, nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
// finally encode the tx
|
||||
txBytes, err = c.txEncode(builder.GetTx())
|
||||
if err != nil {
|
||||
return nil, nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
return txBytes, payloadsToSign, nil
|
||||
}
|
||||
|
||||
// SignerData converts the given any account to signer data
|
||||
func (c converter) SignerData(anyAccount *codectypes.Any) (*SignerData, error) {
|
||||
var acc auth.AccountI
|
||||
err := c.ir.UnpackAny(anyAccount, &acc)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
return &SignerData{
|
||||
AccountNumber: acc.GetAccountNumber(),
|
||||
Sequence: acc.GetSequence(),
|
||||
}, nil
|
||||
}
|
||||
@@ -1,343 +0,0 @@
|
||||
package rosetta_test
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
|
||||
rosettatypes "github.com/coinbase/rosetta-sdk-go/types"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/server/rosetta"
|
||||
crgerrs "github.com/cosmos/cosmos-sdk/server/rosetta/lib/errors"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
|
||||
bank "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
)
|
||||
|
||||
type ConverterTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
c rosetta.Converter
|
||||
unsignedTxBytes []byte
|
||||
unsignedTx authsigning.Tx
|
||||
|
||||
ir codectypes.InterfaceRegistry
|
||||
cdc *codec.ProtoCodec
|
||||
txConf client.TxConfig
|
||||
}
|
||||
|
||||
func (s *ConverterTestSuite) SetupTest() {
|
||||
// create an unsigned tx
|
||||
const unsignedTxHex = "0a8e010a8b010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126b0a2d636f736d6f733134376b6c68377468356a6b6a793361616a736a3272717668747668396d666465333777713567122d636f736d6f73316d6e7670386c786b616679346c787777617175356561653764787630647a36687767797436331a0b0a057374616b651202313612600a4c0a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21034c92046950c876f4a5cb6c7797d6eeb9ef80d67ced4d45fb62b1e859240ba9ad12020a0012100a0a0a057374616b651201311090a10f1a00"
|
||||
unsignedTxBytes, err := hex.DecodeString(unsignedTxHex)
|
||||
s.Require().NoError(err)
|
||||
s.unsignedTxBytes = unsignedTxBytes
|
||||
// instantiate converter
|
||||
cdc, ir := rosetta.MakeCodec()
|
||||
txConfig := authtx.NewTxConfig(cdc, authtx.DefaultSignModes)
|
||||
s.c = rosetta.NewConverter(cdc, ir, txConfig)
|
||||
// add utils
|
||||
s.ir = ir
|
||||
s.cdc = cdc
|
||||
s.txConf = txConfig
|
||||
// add authsigning tx
|
||||
sdkTx, err := txConfig.TxDecoder()(unsignedTxBytes)
|
||||
s.Require().NoError(err)
|
||||
builder, err := txConfig.WrapTxBuilder(sdkTx)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.unsignedTx = builder.GetTx()
|
||||
}
|
||||
|
||||
func (s *ConverterTestSuite) TestFromRosettaOpsToTxSuccess() {
|
||||
addr1 := sdk.AccAddress("address1").String()
|
||||
addr2 := sdk.AccAddress("address2").String()
|
||||
|
||||
msg1 := &bank.MsgSend{
|
||||
FromAddress: addr1,
|
||||
ToAddress: addr2,
|
||||
Amount: sdk.NewCoins(sdk.NewInt64Coin("test", 10)),
|
||||
}
|
||||
|
||||
msg2 := &bank.MsgSend{
|
||||
FromAddress: addr2,
|
||||
ToAddress: addr1,
|
||||
Amount: sdk.NewCoins(sdk.NewInt64Coin("utxo", 10)),
|
||||
}
|
||||
|
||||
ops, err := s.c.ToRosetta().Ops("", msg1)
|
||||
s.Require().NoError(err)
|
||||
|
||||
ops2, err := s.c.ToRosetta().Ops("", msg2)
|
||||
s.Require().NoError(err)
|
||||
|
||||
ops = append(ops, ops2...)
|
||||
|
||||
tx, err := s.c.ToSDK().UnsignedTx(ops)
|
||||
s.Require().NoError(err)
|
||||
|
||||
getMsgs := tx.GetMsgs()
|
||||
|
||||
s.Require().Equal(2, len(getMsgs))
|
||||
|
||||
s.Require().Equal(getMsgs[0], msg1)
|
||||
s.Require().Equal(getMsgs[1], msg2)
|
||||
}
|
||||
|
||||
func (s *ConverterTestSuite) TestFromRosettaOpsToTxErrors() {
|
||||
s.Run("unrecognized op", func() {
|
||||
op := &rosettatypes.Operation{
|
||||
Type: "non-existent",
|
||||
}
|
||||
|
||||
_, err := s.c.ToSDK().UnsignedTx([]*rosettatypes.Operation{op})
|
||||
|
||||
s.Require().ErrorIs(err, crgerrs.ErrBadArgument)
|
||||
})
|
||||
|
||||
s.Run("codec type but not sdk.Msg", func() {
|
||||
op := &rosettatypes.Operation{
|
||||
Type: "cosmos.crypto.ed25519.PubKey",
|
||||
}
|
||||
|
||||
_, err := s.c.ToSDK().UnsignedTx([]*rosettatypes.Operation{op})
|
||||
|
||||
s.Require().ErrorIs(err, crgerrs.ErrBadArgument)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ConverterTestSuite) TestMsgToMetaMetaToMsg() {
|
||||
msg := &bank.MsgSend{
|
||||
FromAddress: "addr1",
|
||||
ToAddress: "addr2",
|
||||
Amount: sdk.NewCoins(sdk.NewInt64Coin("test", 10)),
|
||||
}
|
||||
msg.Route()
|
||||
|
||||
meta, err := s.c.ToRosetta().Meta(msg)
|
||||
s.Require().NoError(err)
|
||||
|
||||
copyMsg := new(bank.MsgSend)
|
||||
err = s.c.ToSDK().Msg(meta, copyMsg)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(msg, copyMsg)
|
||||
}
|
||||
|
||||
func (s *ConverterTestSuite) TestSignedTx() {
|
||||
s.Run("success", func() {
|
||||
const payloadsJSON = `[{"hex_bytes":"82ccce81a3e4a7272249f0e25c3037a316ee2acce76eb0c25db00ef6634a4d57303b2420edfdb4c9a635ad8851fe5c7a9379b7bc2baadc7d74f7e76ac97459b5","signing_payload":{"address":"cosmos147klh7th5jkjy3aajsj2rqvhtvh9mfde37wq5g","hex_bytes":"ed574d84b095250280de38bf8c254e4a1f8755e5bd300b1f6ca2671688136ecc","account_identifier":{"address":"cosmos147klh7th5jkjy3aajsj2rqvhtvh9mfde37wq5g"},"signature_type":"ecdsa"},"public_key":{"hex_bytes":"034c92046950c876f4a5cb6c7797d6eeb9ef80d67ced4d45fb62b1e859240ba9ad","curve_type":"secp256k1"},"signature_type":"ecdsa"}]`
|
||||
const expectedSignedTxHex = "0a8e010a8b010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e64126b0a2d636f736d6f733134376b6c68377468356a6b6a793361616a736a3272717668747668396d666465333777713567122d636f736d6f73316d6e7670386c786b616679346c787777617175356561653764787630647a36687767797436331a0b0a057374616b651202313612620a4e0a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21034c92046950c876f4a5cb6c7797d6eeb9ef80d67ced4d45fb62b1e859240ba9ad12040a02087f12100a0a0a057374616b651201311090a10f1a4082ccce81a3e4a7272249f0e25c3037a316ee2acce76eb0c25db00ef6634a4d57303b2420edfdb4c9a635ad8851fe5c7a9379b7bc2baadc7d74f7e76ac97459b5"
|
||||
|
||||
var payloads []*rosettatypes.Signature
|
||||
s.Require().NoError(json.Unmarshal([]byte(payloadsJSON), &payloads))
|
||||
|
||||
signedTx, err := s.c.ToSDK().SignedTx(s.unsignedTxBytes, payloads)
|
||||
s.Require().NoError(err)
|
||||
|
||||
signedTxHex := hex.EncodeToString(signedTx)
|
||||
|
||||
s.Require().Equal(signedTxHex, expectedSignedTxHex)
|
||||
})
|
||||
|
||||
s.Run("signers data and signing payloads mismatch", func() {
|
||||
_, err := s.c.ToSDK().SignedTx(s.unsignedTxBytes, nil)
|
||||
s.Require().ErrorIs(err, crgerrs.ErrInvalidTransaction)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ConverterTestSuite) TestOpsAndSigners() {
|
||||
s.Run("success", func() {
|
||||
addr1 := sdk.AccAddress("address1").String()
|
||||
addr2 := sdk.AccAddress("address2").String()
|
||||
|
||||
msg := &bank.MsgSend{
|
||||
FromAddress: addr1,
|
||||
ToAddress: addr2,
|
||||
Amount: sdk.NewCoins(sdk.NewInt64Coin("test", 10)),
|
||||
}
|
||||
|
||||
builder := s.txConf.NewTxBuilder()
|
||||
s.Require().NoError(builder.SetMsgs(msg))
|
||||
|
||||
sdkTx := builder.GetTx()
|
||||
txBytes, err := s.txConf.TxEncoder()(sdkTx)
|
||||
s.Require().NoError(err)
|
||||
|
||||
ops, signers, err := s.c.ToRosetta().OpsAndSigners(txBytes)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Require().Equal(len(ops), len(sdkTx.GetMsgs())*len(sdkTx.GetSigners()), "operation number mismatch")
|
||||
|
||||
s.Require().Equal(len(signers), len(sdkTx.GetSigners()), "signers number mismatch")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ConverterTestSuite) TestBeginEndBlockAndHashToTxType() {
|
||||
const deliverTxHex = "5229A67AA008B5C5F1A0AEA77D4DEBE146297A30AAEF01777AF10FAD62DD36AB"
|
||||
|
||||
deliverTxBytes, err := hex.DecodeString(deliverTxHex)
|
||||
s.Require().NoError(err)
|
||||
|
||||
endBlockTxHex := s.c.ToRosetta().EndBlockTxHash(deliverTxBytes)
|
||||
beginBlockTxHex := s.c.ToRosetta().BeginBlockTxHash(deliverTxBytes)
|
||||
|
||||
txType, hash := s.c.ToSDK().HashToTxType(deliverTxBytes)
|
||||
|
||||
s.Require().Equal(rosetta.DeliverTxTx, txType)
|
||||
s.Require().Equal(deliverTxBytes, hash, "deliver tx hash should not change")
|
||||
|
||||
endBlockTxBytes, err := hex.DecodeString(endBlockTxHex)
|
||||
s.Require().NoError(err)
|
||||
|
||||
txType, hash = s.c.ToSDK().HashToTxType(endBlockTxBytes)
|
||||
|
||||
s.Require().Equal(rosetta.EndBlockTx, txType)
|
||||
s.Require().Equal(deliverTxBytes, hash, "end block tx hash should be equal to a block hash")
|
||||
|
||||
beginBlockTxBytes, err := hex.DecodeString(beginBlockTxHex)
|
||||
s.Require().NoError(err)
|
||||
|
||||
txType, hash = s.c.ToSDK().HashToTxType(beginBlockTxBytes)
|
||||
|
||||
s.Require().Equal(rosetta.BeginBlockTx, txType)
|
||||
s.Require().Equal(deliverTxBytes, hash, "begin block tx hash should be equal to a block hash")
|
||||
|
||||
txType, hash = s.c.ToSDK().HashToTxType([]byte("invalid"))
|
||||
|
||||
s.Require().Equal(rosetta.UnrecognizedTx, txType)
|
||||
s.Require().Nil(hash)
|
||||
|
||||
txType, hash = s.c.ToSDK().HashToTxType(append([]byte{0x3}, deliverTxBytes...))
|
||||
s.Require().Equal(rosetta.UnrecognizedTx, txType)
|
||||
s.Require().Nil(hash)
|
||||
}
|
||||
|
||||
func (s *ConverterTestSuite) TestSigningComponents() {
|
||||
s.Run("invalid metadata coins", func() {
|
||||
_, _, err := s.c.ToRosetta().SigningComponents(nil, &rosetta.ConstructionMetadata{GasPrice: "invalid"}, nil)
|
||||
s.Require().ErrorIs(err, crgerrs.ErrBadArgument)
|
||||
})
|
||||
|
||||
s.Run("length signers data does not match signers", func() {
|
||||
_, _, err := s.c.ToRosetta().SigningComponents(s.unsignedTx, &rosetta.ConstructionMetadata{GasPrice: "10stake"}, nil)
|
||||
s.Require().ErrorIs(err, crgerrs.ErrBadArgument)
|
||||
})
|
||||
|
||||
s.Run("length pub keys does not match signers", func() {
|
||||
_, _, err := s.c.ToRosetta().SigningComponents(
|
||||
s.unsignedTx,
|
||||
&rosetta.ConstructionMetadata{GasPrice: "10stake", SignersData: []*rosetta.SignerData{
|
||||
{
|
||||
AccountNumber: 0,
|
||||
Sequence: 0,
|
||||
},
|
||||
}},
|
||||
nil)
|
||||
s.Require().ErrorIs(err, crgerrs.ErrBadArgument)
|
||||
})
|
||||
|
||||
s.Run("ros pub key is valid but not the one we expect", func() {
|
||||
validButUnexpected, err := hex.DecodeString("030da9096a40eb1d6c25f1e26e9cbf8941fc84b8f4dc509c8df5e62a29ab8f2415")
|
||||
s.Require().NoError(err)
|
||||
|
||||
_, _, err = s.c.ToRosetta().SigningComponents(
|
||||
s.unsignedTx,
|
||||
&rosetta.ConstructionMetadata{GasPrice: "10stake", SignersData: []*rosetta.SignerData{
|
||||
{
|
||||
AccountNumber: 0,
|
||||
Sequence: 0,
|
||||
},
|
||||
}},
|
||||
[]*rosettatypes.PublicKey{
|
||||
{
|
||||
Bytes: validButUnexpected,
|
||||
CurveType: rosettatypes.Secp256k1,
|
||||
},
|
||||
})
|
||||
s.Require().ErrorIs(err, crgerrs.ErrBadArgument)
|
||||
})
|
||||
|
||||
s.Run("success", func() {
|
||||
expectedPubKey, err := hex.DecodeString("034c92046950c876f4a5cb6c7797d6eeb9ef80d67ced4d45fb62b1e859240ba9ad")
|
||||
s.Require().NoError(err)
|
||||
|
||||
_, _, err = s.c.ToRosetta().SigningComponents(
|
||||
s.unsignedTx,
|
||||
&rosetta.ConstructionMetadata{GasPrice: "10stake", SignersData: []*rosetta.SignerData{
|
||||
{
|
||||
AccountNumber: 0,
|
||||
Sequence: 0,
|
||||
},
|
||||
}},
|
||||
[]*rosettatypes.PublicKey{
|
||||
{
|
||||
Bytes: expectedPubKey,
|
||||
CurveType: rosettatypes.Secp256k1,
|
||||
},
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ConverterTestSuite) TestBalanceOps() {
|
||||
s.Run("not a balance op", func() {
|
||||
notBalanceOp := abci.Event{
|
||||
Type: "not-a-balance-op",
|
||||
}
|
||||
|
||||
ops := s.c.ToRosetta().BalanceOps("", []abci.Event{notBalanceOp})
|
||||
s.Len(ops, 0, "expected no balance ops")
|
||||
})
|
||||
|
||||
s.Run("multiple balance ops from 2 multicoins event", func() {
|
||||
subBalanceOp := bank.NewCoinSpentEvent(
|
||||
sdk.AccAddress("test"),
|
||||
sdk.NewCoins(sdk.NewInt64Coin("test", 10), sdk.NewInt64Coin("utxo", 10)),
|
||||
)
|
||||
|
||||
addBalanceOp := bank.NewCoinReceivedEvent(
|
||||
sdk.AccAddress("test"),
|
||||
sdk.NewCoins(sdk.NewInt64Coin("test", 10), sdk.NewInt64Coin("utxo", 10)),
|
||||
)
|
||||
|
||||
ops := s.c.ToRosetta().BalanceOps("", []abci.Event{(abci.Event)(subBalanceOp), (abci.Event)(addBalanceOp)})
|
||||
s.Len(ops, 4)
|
||||
})
|
||||
|
||||
s.Run("spec broken", func() {
|
||||
s.Require().Panics(func() {
|
||||
specBrokenSub := abci.Event{
|
||||
Type: bank.EventTypeCoinSpent,
|
||||
}
|
||||
_ = s.c.ToRosetta().BalanceOps("", []abci.Event{specBrokenSub})
|
||||
})
|
||||
|
||||
s.Require().Panics(func() {
|
||||
specBrokenSub := abci.Event{
|
||||
Type: bank.EventTypeCoinBurn,
|
||||
}
|
||||
_ = s.c.ToRosetta().BalanceOps("", []abci.Event{specBrokenSub})
|
||||
})
|
||||
|
||||
s.Require().Panics(func() {
|
||||
specBrokenSub := abci.Event{
|
||||
Type: bank.EventTypeCoinReceived,
|
||||
}
|
||||
_ = s.c.ToRosetta().BalanceOps("", []abci.Event{specBrokenSub})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestConverterTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(ConverterTestSuite))
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
package errors
|
||||
|
||||
// errors.go contains all the errors returned by the adapter implementation
|
||||
// plus some extra utilities to parse those errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
grpccodes "google.golang.org/grpc/codes"
|
||||
grpcstatus "google.golang.org/grpc/status"
|
||||
|
||||
"github.com/coinbase/rosetta-sdk-go/types"
|
||||
)
|
||||
|
||||
// ListErrors lists all the registered errors
|
||||
func ListErrors() []*types.Error {
|
||||
return registry.list()
|
||||
}
|
||||
|
||||
// SealAndListErrors seals the registry and lists its errors
|
||||
func SealAndListErrors() []*types.Error {
|
||||
registry.seal()
|
||||
return registry.list()
|
||||
}
|
||||
|
||||
// Error defines an error that can be converted to a Rosetta API error.
|
||||
type Error struct {
|
||||
rosErr *types.Error
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
if e.rosErr == nil {
|
||||
return ErrUnknown.Error()
|
||||
}
|
||||
return fmt.Sprintf("rosetta: (%d) %s", e.rosErr.Code, e.rosErr.Message)
|
||||
}
|
||||
|
||||
// Is implements errors.Is for *Error, two errors are considered equal
|
||||
// if their error codes are identical
|
||||
func (e *Error) Is(err error) bool {
|
||||
// assert it can be casted
|
||||
rosErr, ok := err.(*Error)
|
||||
if rosErr == nil || !ok {
|
||||
return false
|
||||
}
|
||||
// check that both *Error's are correctly initialized to avoid dereference panics
|
||||
if rosErr.rosErr == nil || e.rosErr == nil {
|
||||
return false
|
||||
}
|
||||
// messages are equal if their error codes match
|
||||
return rosErr.rosErr.Code == e.rosErr.Code
|
||||
}
|
||||
|
||||
// WrapError wraps the rosetta error with additional context
|
||||
func WrapError(err *Error, msg string) *Error {
|
||||
return &Error{rosErr: &types.Error{
|
||||
Code: err.rosErr.Code,
|
||||
Message: err.rosErr.Message,
|
||||
Description: err.rosErr.Description,
|
||||
Retriable: err.rosErr.Retriable,
|
||||
Details: map[string]interface{}{
|
||||
"info": msg,
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
// ToRosetta attempts to converting an error into a rosetta
|
||||
// error, if the error cannot be converted it will be parsed as unknown
|
||||
func ToRosetta(err error) *types.Error {
|
||||
// if it's null or not known
|
||||
rosErr, ok := err.(*Error)
|
||||
if rosErr == nil || !ok {
|
||||
return ToRosetta(WrapError(ErrUnknown, ErrUnknown.Error()))
|
||||
}
|
||||
return rosErr.rosErr
|
||||
}
|
||||
|
||||
// FromGRPCToRosettaError converts a gRPC error to rosetta error
|
||||
func FromGRPCToRosettaError(err error) *Error {
|
||||
status, ok := grpcstatus.FromError(err)
|
||||
if !ok {
|
||||
return WrapError(ErrUnknown, err.Error())
|
||||
}
|
||||
switch status.Code() {
|
||||
case grpccodes.NotFound:
|
||||
return WrapError(ErrNotFound, status.Message())
|
||||
case grpccodes.FailedPrecondition:
|
||||
return WrapError(ErrBadArgument, status.Message())
|
||||
case grpccodes.InvalidArgument:
|
||||
return WrapError(ErrBadArgument, status.Message())
|
||||
case grpccodes.Internal:
|
||||
return WrapError(ErrInternal, status.Message())
|
||||
default:
|
||||
return WrapError(ErrUnknown, status.Message())
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterError(code int32, message string, retryable bool, description string) *Error {
|
||||
e := &Error{rosErr: &types.Error{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Description: &description,
|
||||
Retriable: retryable,
|
||||
Details: nil,
|
||||
}}
|
||||
registry.add(e)
|
||||
return e
|
||||
}
|
||||
|
||||
// Default error list
|
||||
var (
|
||||
// ErrUnknown defines an unknown error, if this is returned it means
|
||||
// the library is ignoring an error
|
||||
ErrUnknown = RegisterError(0, "unknown", false, "unknown error")
|
||||
// ErrOffline is returned when there is an attempt to query an endpoint in offline mode
|
||||
ErrOffline = RegisterError(1, "cannot query endpoint in offline mode", false, "returned when querying an online endpoint in offline mode")
|
||||
// ErrNetworkNotSupported is returned when there is an attempt to query a network which is not supported
|
||||
ErrNetworkNotSupported = RegisterError(2, "network is not supported", false, "returned when querying a non supported network")
|
||||
// ErrCodec is returned when there's an error while marshalling or unmarshalling data
|
||||
ErrCodec = RegisterError(3, "encode/decode error", true, "returned when there are errors encoding or decoding information to and from the node")
|
||||
// ErrInvalidOperation is returned when the operation supplied to rosetta is not a valid one
|
||||
ErrInvalidOperation = RegisterError(4, "invalid operation", false, "returned when the operation is not valid")
|
||||
// ErrInvalidTransaction is returned when the provided hex bytes of a TX are not valid
|
||||
ErrInvalidTransaction = RegisterError(5, "invalid transaction", false, "returned when the transaction is invalid")
|
||||
// ErrInvalidAddress is returned when the byte of the address are bad
|
||||
ErrInvalidAddress = RegisterError(7, "invalid address", false, "returned when the address is malformed")
|
||||
// ErrInvalidPubkey is returned when the public key is invalid
|
||||
ErrInvalidPubkey = RegisterError(8, "invalid pubkey", false, "returned when the public key is invalid")
|
||||
// ErrInterpreting is returned when there are errors interpreting the data from the node, most likely related to breaking changes, version incompatibilities
|
||||
ErrInterpreting = RegisterError(9, "error interpreting data from node", false, "returned when there are issues interpreting requests or response from node")
|
||||
ErrInvalidMemo = RegisterError(11, "invalid memo", false, "returned when the memo is invalid")
|
||||
// ErrBadArgument is returned when the request is malformed
|
||||
ErrBadArgument = RegisterError(400, "bad argument", false, "request is malformed")
|
||||
// ErrNotFound is returned when the required object was not found
|
||||
// retry is set to true because something that is not found now
|
||||
// might be found later, example: a TX
|
||||
ErrNotFound = RegisterError(404, "not found", true, "returned when the node does not find what the client is asking for")
|
||||
// ErrInternal is returned when the node is experiencing internal errors
|
||||
ErrInternal = RegisterError(500, "internal error", false, "returned when the node experiences internal errors")
|
||||
// ErrBadGateway is returned when there are problems interacting with the nodes
|
||||
ErrBadGateway = RegisterError(502, "bad gateway", true, "return when the node is unreachable")
|
||||
// ErrNotImplemented is returned when a method is not implemented yet
|
||||
ErrNotImplemented = RegisterError(14, "not implemented", false, "returned when querying an endpoint which is not implemented")
|
||||
// ErrUnsupportedCurve is returned when the curve specified is not supported
|
||||
ErrUnsupportedCurve = RegisterError(15, "unsupported curve, expected secp256k1", false, "returned when using an unsupported crypto curve")
|
||||
)
|
||||
@@ -1,68 +0,0 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRegisterError(t *testing.T) {
|
||||
var error *Error
|
||||
// this is the number of errors registered by default in errors.go
|
||||
registeredErrorsCount := 16
|
||||
assert.Equal(t, len(registry.list()), registeredErrorsCount)
|
||||
assert.ElementsMatch(t, registry.list(), ListErrors())
|
||||
// add a new Error
|
||||
error = RegisterError(69, "nice!", false, "nice!")
|
||||
assert.NotNil(t, error)
|
||||
// now we have a new error
|
||||
registeredErrorsCount++
|
||||
assert.Equal(t, len(ListErrors()), registeredErrorsCount)
|
||||
// re-register an error should not change anything
|
||||
error = RegisterError(69, "nice!", false, "nice!")
|
||||
assert.Equal(t, len(ListErrors()), registeredErrorsCount)
|
||||
|
||||
// test sealing
|
||||
assert.Equal(t, registry.sealed, false)
|
||||
errors := SealAndListErrors()
|
||||
assert.Equal(t, registry.sealed, true)
|
||||
assert.Equal(t, len(errors), registeredErrorsCount)
|
||||
// add a new error on a sealed registry
|
||||
error = RegisterError(1024, "bytes", false, "bytes")
|
||||
assert.NotNil(t, error)
|
||||
}
|
||||
|
||||
func TestError_Error(t *testing.T) {
|
||||
var error *Error
|
||||
// nil cases
|
||||
assert.False(t, ErrOffline.Is(error))
|
||||
error = &Error{}
|
||||
assert.False(t, ErrOffline.Is(error))
|
||||
// wrong type
|
||||
assert.False(t, ErrOffline.Is(&MyError{}))
|
||||
// test with wrapping an error
|
||||
error = WrapError(ErrOffline, "offline")
|
||||
assert.True(t, ErrOffline.Is(error))
|
||||
|
||||
// test equality
|
||||
assert.False(t, ErrOffline.Is(ErrBadGateway))
|
||||
assert.True(t, ErrBadGateway.Is(ErrBadGateway))
|
||||
}
|
||||
|
||||
func TestToRosetta(t *testing.T) {
|
||||
var error *Error
|
||||
// nil case
|
||||
assert.NotNil(t, ToRosetta(error))
|
||||
// wrong type
|
||||
assert.NotNil(t, ToRosetta(&MyError{}))
|
||||
}
|
||||
|
||||
type MyError struct{}
|
||||
|
||||
func (e *MyError) Error() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (e *MyError) Is(err error) bool {
|
||||
return true
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/coinbase/rosetta-sdk-go/types"
|
||||
)
|
||||
|
||||
type errorRegistry struct {
|
||||
mu *sync.RWMutex
|
||||
sealed bool
|
||||
errors map[int32]*types.Error
|
||||
}
|
||||
|
||||
func (r *errorRegistry) add(err *Error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.sealed {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "[ROSETTA] WARNING: attempts to register errors after seal will be ignored")
|
||||
}
|
||||
if _, ok := r.errors[err.rosErr.Code]; ok {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "[ROSETTA] WARNING: attempts to register an already registered error will be ignored, code: ", err.rosErr.Code)
|
||||
}
|
||||
r.errors[err.rosErr.Code] = err.rosErr
|
||||
}
|
||||
|
||||
func (r errorRegistry) list() []*types.Error {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
rosErrs := make([]*types.Error, 0, len(registry.errors))
|
||||
for _, v := range r.errors {
|
||||
rosErrs = append(rosErrs, v)
|
||||
}
|
||||
return rosErrs
|
||||
}
|
||||
|
||||
func (r *errorRegistry) seal() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.sealed = true
|
||||
}
|
||||
|
||||
var registry = errorRegistry{
|
||||
mu: new(sync.RWMutex),
|
||||
errors: make(map[int32]*types.Error),
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/coinbase/rosetta-sdk-go/types"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/server/rosetta/lib/errors"
|
||||
)
|
||||
|
||||
// ConstructionCombine Combine creates a network-specific transaction from an unsigned transaction
|
||||
// and an array of provided signatures. The signed transaction returned from this method will be
|
||||
// sent to the /construction/submit endpoint by the caller.
|
||||
func (on OnlineNetwork) ConstructionCombine(ctx context.Context, request *types.ConstructionCombineRequest) (*types.ConstructionCombineResponse, *types.Error) {
|
||||
txBytes, err := hex.DecodeString(request.UnsignedTransaction)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
|
||||
signedTx, err := on.client.SignedTx(ctx, txBytes, request.Signatures)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
|
||||
return &types.ConstructionCombineResponse{
|
||||
SignedTransaction: hex.EncodeToString(signedTx),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ConstructionDerive Derive returns the AccountIdentifier associated with a public key.
|
||||
func (on OnlineNetwork) ConstructionDerive(_ context.Context, request *types.ConstructionDeriveRequest) (*types.ConstructionDeriveResponse, *types.Error) {
|
||||
account, err := on.client.AccountIdentifierFromPublicKey(request.PublicKey)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
return &types.ConstructionDeriveResponse{
|
||||
AccountIdentifier: account,
|
||||
Metadata: nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ConstructionHash TransactionHash returns the network-specific transaction hash for a signed
|
||||
// transaction.
|
||||
func (on OnlineNetwork) ConstructionHash(ctx context.Context, request *types.ConstructionHashRequest) (*types.TransactionIdentifierResponse, *types.Error) {
|
||||
bz, err := hex.DecodeString(request.SignedTransaction)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(errors.WrapError(errors.ErrInvalidTransaction, "error decoding tx"))
|
||||
}
|
||||
|
||||
hash := sha256.Sum256(bz)
|
||||
bzHash := hash[:]
|
||||
hashString := hex.EncodeToString(bzHash)
|
||||
|
||||
return &types.TransactionIdentifierResponse{
|
||||
TransactionIdentifier: &types.TransactionIdentifier{
|
||||
Hash: strings.ToUpper(hashString),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ConstructionMetadata Get any information required to construct a transaction for a specific
|
||||
// network (i.e. ChainID, Gas, Memo, ...).
|
||||
func (on OnlineNetwork) ConstructionMetadata(ctx context.Context, request *types.ConstructionMetadataRequest) (*types.ConstructionMetadataResponse, *types.Error) {
|
||||
metadata, err := on.client.ConstructionMetadataFromOptions(ctx, request.Options)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
|
||||
response := &types.ConstructionMetadataResponse{
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
if metadata["gas_price"] != nil && metadata["gas_limit"] != nil {
|
||||
gasPrice, ok := metadata["gas_price"].(string)
|
||||
if !ok {
|
||||
return nil, errors.ToRosetta(errors.WrapError(errors.ErrBadArgument, "invalid gas_price"))
|
||||
}
|
||||
if gasPrice == "" { // gas_price is unset. skip fee suggestion
|
||||
return response, nil
|
||||
}
|
||||
price, err := sdk.ParseDecCoin(gasPrice)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
|
||||
gasLimit, ok := metadata["gas_limit"].(float64)
|
||||
if !ok {
|
||||
return nil, errors.ToRosetta(errors.WrapError(errors.ErrBadArgument, "invalid gas_limit"))
|
||||
}
|
||||
if gasLimit == 0 { // gas_limit is unset. skip fee suggestion
|
||||
return response, nil
|
||||
}
|
||||
gas := sdk.NewIntFromUint64(uint64(gasLimit))
|
||||
|
||||
suggestedFee := types.Amount{
|
||||
Value: strconv.FormatInt(price.Amount.MulInt64(gas.Int64()).Ceil().TruncateInt64(), 10),
|
||||
Currency: &(types.Currency{
|
||||
Symbol: price.Denom,
|
||||
Decimals: 0,
|
||||
}),
|
||||
}
|
||||
response.SuggestedFee = []*types.Amount{&suggestedFee}
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ConstructionParse Parse is called on both unsigned and signed transactions to understand the
|
||||
// intent of the formulated transaction. This is run as a sanity check before signing (after
|
||||
// /construction/payloads) and before broadcast (after /construction/combine).
|
||||
func (on OnlineNetwork) ConstructionParse(ctx context.Context, request *types.ConstructionParseRequest) (*types.ConstructionParseResponse, *types.Error) {
|
||||
txBytes, err := hex.DecodeString(request.Transaction)
|
||||
if err != nil {
|
||||
err := errors.WrapError(errors.ErrInvalidTransaction, err.Error())
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
ops, signers, err := on.client.TxOperationsAndSignersAccountIdentifiers(request.Signed, txBytes)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
return &types.ConstructionParseResponse{
|
||||
Operations: ops,
|
||||
AccountIdentifierSigners: signers,
|
||||
Metadata: nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ConstructionPayloads Payloads is called with an array of operations and the response from
|
||||
// /construction/metadata. It returns an unsigned transaction blob and a collection of payloads that
|
||||
// must be signed by particular AccountIdentifiers using a certain SignatureType.
|
||||
func (on OnlineNetwork) ConstructionPayloads(ctx context.Context, request *types.ConstructionPayloadsRequest) (*types.ConstructionPayloadsResponse, *types.Error) {
|
||||
payload, err := on.client.ConstructionPayload(ctx, request)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// ConstructionPreprocess Preprocess is called prior to /construction/payloads to construct a
|
||||
// request for any metadata that is needed for transaction construction given (i.e. account nonce).
|
||||
func (on OnlineNetwork) ConstructionPreprocess(ctx context.Context, request *types.ConstructionPreprocessRequest) (*types.ConstructionPreprocessResponse, *types.Error) {
|
||||
options, err := on.client.PreprocessOperationsToOptions(ctx, request)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
|
||||
return options, nil
|
||||
}
|
||||
|
||||
// ConstructionSubmit Submit a pre-signed transaction to the node. This call does not block on the
|
||||
// transaction being included in a block. Rather, it returns immediately with an indication of
|
||||
// whether or not the transaction was included in the mempool.
|
||||
func (on OnlineNetwork) ConstructionSubmit(ctx context.Context, request *types.ConstructionSubmitRequest) (*types.TransactionIdentifierResponse, *types.Error) {
|
||||
txBytes, err := hex.DecodeString(request.SignedTransaction)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
|
||||
res, meta, err := on.client.PostTx(txBytes)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
|
||||
return &types.TransactionIdentifierResponse{
|
||||
TransactionIdentifier: res,
|
||||
Metadata: meta,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coinbase/rosetta-sdk-go/types"
|
||||
"github.com/cosmos/cosmos-sdk/server/rosetta/lib/errors"
|
||||
crgtypes "github.com/cosmos/cosmos-sdk/server/rosetta/lib/types"
|
||||
)
|
||||
|
||||
// AccountBalance retrieves the account balance of an address
|
||||
// rosetta requires us to fetch the block information too
|
||||
func (on OnlineNetwork) AccountBalance(ctx context.Context, request *types.AccountBalanceRequest) (*types.AccountBalanceResponse, *types.Error) {
|
||||
var (
|
||||
height int64
|
||||
block crgtypes.BlockResponse
|
||||
err error
|
||||
)
|
||||
|
||||
switch {
|
||||
case request.BlockIdentifier == nil:
|
||||
block, err = on.client.BlockByHeight(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
case request.BlockIdentifier.Hash != nil:
|
||||
block, err = on.client.BlockByHash(ctx, *request.BlockIdentifier.Hash)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
height = block.Block.Index
|
||||
case request.BlockIdentifier.Index != nil:
|
||||
height = *request.BlockIdentifier.Index
|
||||
block, err = on.client.BlockByHeight(ctx, &height)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
}
|
||||
|
||||
accountCoins, err := on.client.Balances(ctx, request.AccountIdentifier.Address, &height)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
|
||||
return &types.AccountBalanceResponse{
|
||||
BlockIdentifier: block.Block,
|
||||
Balances: accountCoins,
|
||||
Metadata: nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Block gets the transactions in the given block
|
||||
func (on OnlineNetwork) Block(ctx context.Context, request *types.BlockRequest) (*types.BlockResponse, *types.Error) {
|
||||
var (
|
||||
blockResponse crgtypes.BlockTransactionsResponse
|
||||
err error
|
||||
)
|
||||
|
||||
// When fetching data by BlockIdentifier, it may be possible to only specify the index or hash.
|
||||
// If neither property is specified, it is assumed that the client is making a request at the current block.
|
||||
switch {
|
||||
case request.BlockIdentifier == nil: // unlike AccountBalance(), BlockIdentifer is mandatory by spec 1.4.10.
|
||||
err := errors.WrapError(errors.ErrBadArgument, "block identifier needs to be specified")
|
||||
return nil, errors.ToRosetta(err)
|
||||
|
||||
case request.BlockIdentifier.Hash != nil:
|
||||
blockResponse, err = on.client.BlockTransactionsByHash(ctx, *request.BlockIdentifier.Hash)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
case request.BlockIdentifier.Index != nil:
|
||||
blockResponse, err = on.client.BlockTransactionsByHeight(ctx, request.BlockIdentifier.Index)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
|
||||
default:
|
||||
// both empty
|
||||
blockResponse, err = on.client.BlockTransactionsByHeight(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Both of index and hash can be specified in reuqest, so make sure they are not mismatching.
|
||||
if request.BlockIdentifier.Index != nil && *request.BlockIdentifier.Index != blockResponse.Block.Index {
|
||||
err := errors.WrapError(errors.ErrBadArgument, "mismatching index")
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
|
||||
if request.BlockIdentifier.Hash != nil && *request.BlockIdentifier.Hash != blockResponse.Block.Hash {
|
||||
err := errors.WrapError(errors.ErrBadArgument, "mismatching hash")
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
|
||||
return &types.BlockResponse{
|
||||
Block: &types.Block{
|
||||
BlockIdentifier: blockResponse.Block,
|
||||
ParentBlockIdentifier: blockResponse.ParentBlock,
|
||||
Timestamp: blockResponse.MillisecondTimestamp,
|
||||
Transactions: blockResponse.Transactions,
|
||||
Metadata: nil,
|
||||
},
|
||||
OtherTransactions: nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BlockTransaction gets the given transaction in the specified block, we do not need to check the block itself too
|
||||
// due to the fact that tendermint achieves instant finality
|
||||
func (on OnlineNetwork) BlockTransaction(ctx context.Context, request *types.BlockTransactionRequest) (*types.BlockTransactionResponse, *types.Error) {
|
||||
tx, err := on.client.GetTx(ctx, request.TransactionIdentifier.Hash)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
|
||||
return &types.BlockTransactionResponse{
|
||||
Transaction: tx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Mempool fetches the transactions contained in the mempool
|
||||
func (on OnlineNetwork) Mempool(ctx context.Context, _ *types.NetworkRequest) (*types.MempoolResponse, *types.Error) {
|
||||
txs, err := on.client.Mempool(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
|
||||
return &types.MempoolResponse{
|
||||
TransactionIdentifiers: txs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MempoolTransaction fetches a single transaction in the mempool
|
||||
// NOTE: it is not implemented yet
|
||||
func (on OnlineNetwork) MempoolTransaction(ctx context.Context, request *types.MempoolTransactionRequest) (*types.MempoolTransactionResponse, *types.Error) {
|
||||
tx, err := on.client.GetUnconfirmedTx(ctx, request.TransactionIdentifier.Hash)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
|
||||
return &types.MempoolTransactionResponse{
|
||||
Transaction: tx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (on OnlineNetwork) NetworkList(_ context.Context, _ *types.MetadataRequest) (*types.NetworkListResponse, *types.Error) {
|
||||
return &types.NetworkListResponse{NetworkIdentifiers: []*types.NetworkIdentifier{on.network}}, nil
|
||||
}
|
||||
|
||||
func (on OnlineNetwork) NetworkOptions(_ context.Context, _ *types.NetworkRequest) (*types.NetworkOptionsResponse, *types.Error) {
|
||||
return on.networkOptions, nil
|
||||
}
|
||||
|
||||
func (on OnlineNetwork) NetworkStatus(ctx context.Context, _ *types.NetworkRequest) (*types.NetworkStatusResponse, *types.Error) {
|
||||
block, err := on.client.BlockByHeight(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
|
||||
peers, err := on.client.Peers(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
|
||||
syncStatus, err := on.client.Status(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.ToRosetta(err)
|
||||
}
|
||||
|
||||
return &types.NetworkStatusResponse{
|
||||
CurrentBlockIdentifier: block.Block,
|
||||
CurrentBlockTimestamp: block.MillisecondTimestamp,
|
||||
GenesisBlockIdentifier: on.genesisBlockIdentifier,
|
||||
OldestBlockIdentifier: nil,
|
||||
SyncStatus: syncStatus,
|
||||
Peers: peers,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coinbase/rosetta-sdk-go/types"
|
||||
|
||||
crgerrs "github.com/cosmos/cosmos-sdk/server/rosetta/lib/errors"
|
||||
crgtypes "github.com/cosmos/cosmos-sdk/server/rosetta/lib/types"
|
||||
)
|
||||
|
||||
// NewOffline instantiates the instance of an offline network
|
||||
// whilst the offline network does not support the DataAPI,
|
||||
// it supports a subset of the construction API.
|
||||
func NewOffline(network *types.NetworkIdentifier, client crgtypes.Client) (crgtypes.API, error) {
|
||||
return OfflineNetwork{
|
||||
OnlineNetwork{
|
||||
client: client,
|
||||
network: network,
|
||||
networkOptions: networkOptionsFromClient(client, nil),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// OfflineNetwork implements an offline data API
|
||||
// which is basically a data API that constantly
|
||||
// returns errors, because it cannot be used if offline
|
||||
type OfflineNetwork struct {
|
||||
OnlineNetwork
|
||||
}
|
||||
|
||||
// Implement DataAPI in offline mode, which means no method is available
|
||||
func (o OfflineNetwork) AccountBalance(_ context.Context, _ *types.AccountBalanceRequest) (*types.AccountBalanceResponse, *types.Error) {
|
||||
return nil, crgerrs.ToRosetta(crgerrs.ErrOffline)
|
||||
}
|
||||
|
||||
func (o OfflineNetwork) Block(_ context.Context, _ *types.BlockRequest) (*types.BlockResponse, *types.Error) {
|
||||
return nil, crgerrs.ToRosetta(crgerrs.ErrOffline)
|
||||
}
|
||||
|
||||
func (o OfflineNetwork) BlockTransaction(_ context.Context, _ *types.BlockTransactionRequest) (*types.BlockTransactionResponse, *types.Error) {
|
||||
return nil, crgerrs.ToRosetta(crgerrs.ErrOffline)
|
||||
}
|
||||
|
||||
func (o OfflineNetwork) Mempool(_ context.Context, _ *types.NetworkRequest) (*types.MempoolResponse, *types.Error) {
|
||||
return nil, crgerrs.ToRosetta(crgerrs.ErrOffline)
|
||||
}
|
||||
|
||||
func (o OfflineNetwork) MempoolTransaction(_ context.Context, _ *types.MempoolTransactionRequest) (*types.MempoolTransactionResponse, *types.Error) {
|
||||
return nil, crgerrs.ToRosetta(crgerrs.ErrOffline)
|
||||
}
|
||||
|
||||
func (o OfflineNetwork) NetworkStatus(_ context.Context, _ *types.NetworkRequest) (*types.NetworkStatusResponse, *types.Error) {
|
||||
return nil, crgerrs.ToRosetta(crgerrs.ErrOffline)
|
||||
}
|
||||
|
||||
func (o OfflineNetwork) ConstructionSubmit(_ context.Context, _ *types.ConstructionSubmitRequest) (*types.TransactionIdentifierResponse, *types.Error) {
|
||||
return nil, crgerrs.ToRosetta(crgerrs.ErrOffline)
|
||||
}
|
||||
|
||||
func (o OfflineNetwork) ConstructionMetadata(_ context.Context, _ *types.ConstructionMetadataRequest) (*types.ConstructionMetadataResponse, *types.Error) {
|
||||
return nil, crgerrs.ToRosetta(crgerrs.ErrOffline)
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/coinbase/rosetta-sdk-go/types"
|
||||
|
||||
crgerrs "github.com/cosmos/cosmos-sdk/server/rosetta/lib/errors"
|
||||
crgtypes "github.com/cosmos/cosmos-sdk/server/rosetta/lib/types"
|
||||
)
|
||||
|
||||
// genesisBlockFetchTimeout defines a timeout to fetch the genesis block
|
||||
const genesisBlockFetchTimeout = 15 * time.Second
|
||||
|
||||
// NewOnlineNetwork builds a single network adapter.
|
||||
// It will get the Genesis block on the beginning to avoid calling it everytime.
|
||||
func NewOnlineNetwork(network *types.NetworkIdentifier, client crgtypes.Client) (crgtypes.API, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), genesisBlockFetchTimeout)
|
||||
defer cancel()
|
||||
|
||||
var genesisHeight int64 = -1 // to use initial_height in genesis.json
|
||||
block, err := client.BlockByHeight(ctx, &genesisHeight)
|
||||
if err != nil {
|
||||
return OnlineNetwork{}, err
|
||||
}
|
||||
|
||||
return OnlineNetwork{
|
||||
client: client,
|
||||
network: network,
|
||||
networkOptions: networkOptionsFromClient(client, block.Block),
|
||||
genesisBlockIdentifier: block.Block,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// OnlineNetwork groups together all the components required for the full rosetta implementation
|
||||
type OnlineNetwork struct {
|
||||
client crgtypes.Client // used to query cosmos app + tendermint
|
||||
|
||||
network *types.NetworkIdentifier // identifies the network, it's static
|
||||
networkOptions *types.NetworkOptionsResponse // identifies the network options, it's static
|
||||
|
||||
genesisBlockIdentifier *types.BlockIdentifier // identifies genesis block, it's static
|
||||
}
|
||||
|
||||
// AccountsCoins - relevant only for UTXO based chain
|
||||
// see https://www.rosetta-api.org/docs/AccountApi.html#accountcoins
|
||||
func (o OnlineNetwork) AccountCoins(_ context.Context, _ *types.AccountCoinsRequest) (*types.AccountCoinsResponse, *types.Error) {
|
||||
return nil, crgerrs.ToRosetta(crgerrs.ErrOffline)
|
||||
}
|
||||
|
||||
// networkOptionsFromClient builds network options given the client
|
||||
func networkOptionsFromClient(client crgtypes.Client, genesisBlock *types.BlockIdentifier) *types.NetworkOptionsResponse {
|
||||
var tsi *int64
|
||||
if genesisBlock != nil {
|
||||
tsi = &(genesisBlock.Index)
|
||||
}
|
||||
return &types.NetworkOptionsResponse{
|
||||
Version: &types.Version{
|
||||
RosettaVersion: crgtypes.SpecVersion,
|
||||
NodeVersion: client.Version(),
|
||||
},
|
||||
Allow: &types.Allow{
|
||||
OperationStatuses: client.OperationStatuses(),
|
||||
OperationTypes: client.SupportedOperations(),
|
||||
Errors: crgerrs.SealAndListErrors(),
|
||||
HistoricalBalanceLookup: true,
|
||||
TimestampStartIndex: tsi,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
assert "github.com/coinbase/rosetta-sdk-go/asserter"
|
||||
"github.com/coinbase/rosetta-sdk-go/server"
|
||||
"github.com/coinbase/rosetta-sdk-go/types"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/server/rosetta/lib/internal/service"
|
||||
crgtypes "github.com/cosmos/cosmos-sdk/server/rosetta/lib/types"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultRetries = 5
|
||||
DefaultRetryWait = 5 * time.Second
|
||||
)
|
||||
|
||||
// Settings define the rosetta server settings
|
||||
type Settings struct {
|
||||
// Network contains the information regarding the network
|
||||
Network *types.NetworkIdentifier
|
||||
// Client is the online API handler
|
||||
Client crgtypes.Client
|
||||
// Listen is the address the handler will listen at
|
||||
Listen string
|
||||
// Offline defines if the rosetta service should be exposed in offline mode
|
||||
Offline bool
|
||||
// Retries is the number of readiness checks that will be attempted when instantiating the handler
|
||||
// valid only for online API
|
||||
Retries int
|
||||
// RetryWait is the time that will be waited between retries
|
||||
RetryWait time.Duration
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
h http.Handler
|
||||
addr string
|
||||
}
|
||||
|
||||
func (h Server) Start() error {
|
||||
return http.ListenAndServe(h.addr, h.h) //nolint:gosec
|
||||
}
|
||||
|
||||
func NewServer(settings Settings) (Server, error) {
|
||||
asserter, err := assert.NewServer(
|
||||
settings.Client.SupportedOperations(),
|
||||
true,
|
||||
[]*types.NetworkIdentifier{settings.Network},
|
||||
nil,
|
||||
false,
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
return Server{}, fmt.Errorf("cannot build asserter: %w", err)
|
||||
}
|
||||
|
||||
var adapter crgtypes.API
|
||||
switch settings.Offline {
|
||||
case true:
|
||||
adapter, err = newOfflineAdapter(settings)
|
||||
case false:
|
||||
adapter, err = newOnlineAdapter(settings)
|
||||
}
|
||||
if err != nil {
|
||||
return Server{}, err
|
||||
}
|
||||
h := server.NewRouter(
|
||||
server.NewAccountAPIController(adapter, asserter),
|
||||
server.NewBlockAPIController(adapter, asserter),
|
||||
server.NewNetworkAPIController(adapter, asserter),
|
||||
server.NewMempoolAPIController(adapter, asserter),
|
||||
server.NewConstructionAPIController(adapter, asserter),
|
||||
)
|
||||
|
||||
return Server{
|
||||
h: h,
|
||||
addr: settings.Listen,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newOfflineAdapter(settings Settings) (crgtypes.API, error) {
|
||||
if settings.Client == nil {
|
||||
return nil, fmt.Errorf("client is nil")
|
||||
}
|
||||
return service.NewOffline(settings.Network, settings.Client)
|
||||
}
|
||||
|
||||
func newOnlineAdapter(settings Settings) (crgtypes.API, error) {
|
||||
if settings.Client == nil {
|
||||
return nil, fmt.Errorf("client is nil")
|
||||
}
|
||||
if settings.Retries <= 0 {
|
||||
settings.Retries = DefaultRetries
|
||||
}
|
||||
if settings.RetryWait == 0 {
|
||||
settings.RetryWait = DefaultRetryWait
|
||||
}
|
||||
|
||||
var err error
|
||||
err = settings.Client.Bootstrap()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := 0; i < settings.Retries; i++ {
|
||||
err = settings.Client.Ready()
|
||||
if err != nil {
|
||||
time.Sleep(settings.RetryWait)
|
||||
continue
|
||||
}
|
||||
return service.NewOnlineNetwork(settings.Network, settings.Client)
|
||||
}
|
||||
return nil, fmt.Errorf("maximum number of retries exceeded, last error: %w", err)
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/coinbase/rosetta-sdk-go/server"
|
||||
"github.com/coinbase/rosetta-sdk-go/types"
|
||||
)
|
||||
|
||||
// SpecVersion defines the specification of rosetta
|
||||
const SpecVersion = ""
|
||||
|
||||
// NetworkInformationProvider defines the interface used to provide information regarding
|
||||
// the network and the version of the cosmos sdk used
|
||||
type NetworkInformationProvider interface {
|
||||
// SupportedOperations lists the operations supported by the implementation
|
||||
SupportedOperations() []string
|
||||
// OperationStatuses returns the list of statuses supported by the implementation
|
||||
OperationStatuses() []*types.OperationStatus
|
||||
// Version returns the version of the node
|
||||
Version() string
|
||||
}
|
||||
|
||||
// Client defines the API the client implementation should provide.
|
||||
type Client interface {
|
||||
// Bootstrap Needed if the client needs to perform some action before connecting.
|
||||
Bootstrap() error
|
||||
// Ready checks if the servicer constraints for queries are satisfied
|
||||
// for example the node might still not be ready, it's useful in process
|
||||
// when the rosetta instance might come up before the node itself
|
||||
// the servicer must return nil if the node is ready
|
||||
Ready() error
|
||||
|
||||
// Data API
|
||||
|
||||
// Balances fetches the balance of the given address
|
||||
// if height is not nil, then the balance will be displayed
|
||||
// at the provided height, otherwise last block balance will be returned
|
||||
Balances(ctx context.Context, addr string, height *int64) ([]*types.Amount, error)
|
||||
// BlockByHash gets a block and its transaction at the provided height
|
||||
BlockByHash(ctx context.Context, hash string) (BlockResponse, error)
|
||||
// BlockByHeight gets a block given its height, if height is nil then last block is returned
|
||||
BlockByHeight(ctx context.Context, height *int64) (BlockResponse, error)
|
||||
// BlockTransactionsByHash gets the block, parent block and transactions
|
||||
// given the block hash.
|
||||
BlockTransactionsByHash(ctx context.Context, hash string) (BlockTransactionsResponse, error)
|
||||
// BlockTransactionsByHeight gets the block, parent block and transactions
|
||||
// given the block hash.
|
||||
BlockTransactionsByHeight(ctx context.Context, height *int64) (BlockTransactionsResponse, error)
|
||||
// GetTx gets a transaction given its hash
|
||||
GetTx(ctx context.Context, hash string) (*types.Transaction, error)
|
||||
// GetUnconfirmedTx gets an unconfirmed Tx given its hash
|
||||
// NOTE(fdymylja): NOT IMPLEMENTED YET!
|
||||
GetUnconfirmedTx(ctx context.Context, hash string) (*types.Transaction, error)
|
||||
// Mempool returns the list of the current non confirmed transactions
|
||||
Mempool(ctx context.Context) ([]*types.TransactionIdentifier, error)
|
||||
// Peers gets the peers currently connected to the node
|
||||
Peers(ctx context.Context) ([]*types.Peer, error)
|
||||
// Status returns the node status, such as sync data, version etc
|
||||
Status(ctx context.Context) (*types.SyncStatus, error)
|
||||
|
||||
// Construction API
|
||||
|
||||
// PostTx posts txBytes to the node and returns the transaction identifier plus metadata related
|
||||
// to the transaction itself.
|
||||
PostTx(txBytes []byte) (res *types.TransactionIdentifier, meta map[string]interface{}, err error)
|
||||
// ConstructionMetadataFromOptions builds metadata map from an option map
|
||||
ConstructionMetadataFromOptions(ctx context.Context, options map[string]interface{}) (meta map[string]interface{}, err error)
|
||||
OfflineClient
|
||||
}
|
||||
|
||||
// OfflineClient defines the functionalities supported without having access to the node
|
||||
type OfflineClient interface {
|
||||
NetworkInformationProvider
|
||||
// SignedTx returns the signed transaction given the tx bytes (msgs) plus the signatures
|
||||
SignedTx(ctx context.Context, txBytes []byte, sigs []*types.Signature) (signedTxBytes []byte, err error)
|
||||
// TxOperationsAndSignersAccountIdentifiers returns the operations related to a transaction and the account
|
||||
// identifiers if the transaction is signed
|
||||
TxOperationsAndSignersAccountIdentifiers(signed bool, hexBytes []byte) (ops []*types.Operation, signers []*types.AccountIdentifier, err error)
|
||||
// ConstructionPayload returns the construction payload given the request
|
||||
ConstructionPayload(ctx context.Context, req *types.ConstructionPayloadsRequest) (resp *types.ConstructionPayloadsResponse, err error)
|
||||
// PreprocessOperationsToOptions returns the options given the preprocess operations
|
||||
PreprocessOperationsToOptions(ctx context.Context, req *types.ConstructionPreprocessRequest) (resp *types.ConstructionPreprocessResponse, err error)
|
||||
// AccountIdentifierFromPublicKey returns the account identifier given the public key
|
||||
AccountIdentifierFromPublicKey(pubKey *types.PublicKey) (*types.AccountIdentifier, error)
|
||||
}
|
||||
|
||||
type BlockTransactionsResponse struct {
|
||||
BlockResponse
|
||||
Transactions []*types.Transaction
|
||||
}
|
||||
|
||||
type BlockResponse struct {
|
||||
Block *types.BlockIdentifier
|
||||
ParentBlock *types.BlockIdentifier
|
||||
MillisecondTimestamp int64
|
||||
TxCount int64
|
||||
}
|
||||
|
||||
// API defines the exposed APIs
|
||||
// if the service is online
|
||||
type API interface {
|
||||
DataAPI
|
||||
ConstructionAPI
|
||||
}
|
||||
|
||||
// DataAPI defines the full data API implementation
|
||||
type DataAPI interface {
|
||||
server.NetworkAPIServicer
|
||||
server.AccountAPIServicer
|
||||
server.BlockAPIServicer
|
||||
server.MempoolAPIServicer
|
||||
}
|
||||
|
||||
var _ server.ConstructionAPIServicer = ConstructionAPI(nil)
|
||||
|
||||
// ConstructionAPI defines the full construction API with
|
||||
// the online and offline endpoints
|
||||
type ConstructionAPI interface {
|
||||
ConstructionOnlineAPI
|
||||
ConstructionOfflineAPI
|
||||
}
|
||||
|
||||
// ConstructionOnlineAPI defines the construction methods
|
||||
// allowed in an online implementation
|
||||
type ConstructionOnlineAPI interface {
|
||||
ConstructionMetadata(
|
||||
context.Context,
|
||||
*types.ConstructionMetadataRequest,
|
||||
) (*types.ConstructionMetadataResponse, *types.Error)
|
||||
ConstructionSubmit(
|
||||
context.Context,
|
||||
*types.ConstructionSubmitRequest,
|
||||
) (*types.TransactionIdentifierResponse, *types.Error)
|
||||
}
|
||||
|
||||
// ConstructionOfflineAPI defines the construction methods
|
||||
// allowed
|
||||
type ConstructionOfflineAPI interface {
|
||||
ConstructionCombine(
|
||||
context.Context,
|
||||
*types.ConstructionCombineRequest,
|
||||
) (*types.ConstructionCombineResponse, *types.Error)
|
||||
ConstructionDerive(
|
||||
context.Context,
|
||||
*types.ConstructionDeriveRequest,
|
||||
) (*types.ConstructionDeriveResponse, *types.Error)
|
||||
ConstructionHash(
|
||||
context.Context,
|
||||
*types.ConstructionHashRequest,
|
||||
) (*types.TransactionIdentifierResponse, *types.Error)
|
||||
ConstructionParse(
|
||||
context.Context,
|
||||
*types.ConstructionParseRequest,
|
||||
) (*types.ConstructionParseResponse, *types.Error)
|
||||
ConstructionPayloads(
|
||||
context.Context,
|
||||
*types.ConstructionPayloadsRequest,
|
||||
) (*types.ConstructionPayloadsResponse, *types.Error)
|
||||
ConstructionPreprocess(
|
||||
context.Context,
|
||||
*types.ConstructionPreprocessRequest,
|
||||
) (*types.ConstructionPreprocessResponse, *types.Error)
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package rosetta
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
)
|
||||
|
||||
// statuses
|
||||
const (
|
||||
StatusTxSuccess = "Success"
|
||||
StatusTxReverted = "Reverted"
|
||||
StatusPeerSynced = "synced"
|
||||
StatusPeerSyncing = "syncing"
|
||||
)
|
||||
|
||||
// In rosetta all state transitions must be represented as transactions
|
||||
// since in tendermint begin block and end block are state transitions
|
||||
// which are not represented as transactions we mock only the balance changes
|
||||
// happening at those levels as transactions. (check BeginBlockTxHash for more info)
|
||||
const (
|
||||
DeliverTxSize = sha256.Size
|
||||
BeginEndBlockTxSize = DeliverTxSize + 1
|
||||
EndBlockHashStart = 0x0
|
||||
BeginBlockHashStart = 0x1
|
||||
)
|
||||
|
||||
const (
|
||||
// BurnerAddressIdentifier mocks the account identifier of a burner address
|
||||
// all coins burned in the sdk will be sent to this identifier, which per sdk.AccAddress
|
||||
// design we will never be able to query (as of now).
|
||||
// Rosetta does not understand supply contraction.
|
||||
BurnerAddressIdentifier = "burner"
|
||||
)
|
||||
|
||||
// TransactionType is used to distinguish if a rosetta provided hash
|
||||
// represents endblock, beginblock or deliver tx
|
||||
type TransactionType int
|
||||
|
||||
const (
|
||||
UnrecognizedTx TransactionType = iota
|
||||
BeginBlockTx
|
||||
EndBlockTx
|
||||
DeliverTxTx
|
||||
)
|
||||
|
||||
// metadata options
|
||||
|
||||
// misc
|
||||
const (
|
||||
Log = "log"
|
||||
)
|
||||
|
||||
// ConstructionPreprocessMetadata is used to represent
|
||||
// the metadata rosetta can provide during preprocess options
|
||||
type ConstructionPreprocessMetadata struct {
|
||||
Memo string `json:"memo"`
|
||||
GasLimit uint64 `json:"gas_limit"`
|
||||
GasPrice string `json:"gas_price"`
|
||||
}
|
||||
|
||||
func (c *ConstructionPreprocessMetadata) FromMetadata(meta map[string]interface{}) error {
|
||||
return unmarshalMetadata(meta, c)
|
||||
}
|
||||
|
||||
// PreprocessOperationsOptionsResponse is the structured metadata options returned by the preprocess operations endpoint
|
||||
type PreprocessOperationsOptionsResponse struct {
|
||||
ExpectedSigners []string `json:"expected_signers"`
|
||||
Memo string `json:"memo"`
|
||||
GasLimit uint64 `json:"gas_limit"`
|
||||
GasPrice string `json:"gas_price"`
|
||||
}
|
||||
|
||||
func (c PreprocessOperationsOptionsResponse) ToMetadata() (map[string]interface{}, error) {
|
||||
return marshalMetadata(c)
|
||||
}
|
||||
|
||||
func (c *PreprocessOperationsOptionsResponse) FromMetadata(meta map[string]interface{}) error {
|
||||
return unmarshalMetadata(meta, c)
|
||||
}
|
||||
|
||||
// SignerData contains information on the signers when the request
|
||||
// is being created, used to populate the account information
|
||||
type SignerData struct {
|
||||
AccountNumber uint64 `json:"account_number"`
|
||||
Sequence uint64 `json:"sequence"`
|
||||
}
|
||||
|
||||
// ConstructionMetadata are the metadata options used to
|
||||
// construct a transaction. It is returned by ConstructionMetadataFromOptions
|
||||
// and fed to ConstructionPayload to process the bytes to sign.
|
||||
type ConstructionMetadata struct {
|
||||
ChainID string `json:"chain_id"`
|
||||
SignersData []*SignerData `json:"signer_data"`
|
||||
GasLimit uint64 `json:"gas_limit"`
|
||||
GasPrice string `json:"gas_price"`
|
||||
Memo string `json:"memo"`
|
||||
}
|
||||
|
||||
func (c ConstructionMetadata) ToMetadata() (map[string]interface{}, error) {
|
||||
return marshalMetadata(c)
|
||||
}
|
||||
|
||||
func (c *ConstructionMetadata) FromMetadata(meta map[string]interface{}) error {
|
||||
return unmarshalMetadata(meta, c)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package rosetta
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
crgerrs "github.com/cosmos/cosmos-sdk/server/rosetta/lib/errors"
|
||||
)
|
||||
|
||||
// timeToMilliseconds converts time to milliseconds timestamp
|
||||
func timeToMilliseconds(t time.Time) int64 {
|
||||
return t.UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond))
|
||||
}
|
||||
|
||||
// unmarshalMetadata unmarshals the given meta to the target
|
||||
func unmarshalMetadata(meta map[string]interface{}, target interface{}) error {
|
||||
b, err := json.Marshal(meta)
|
||||
if err != nil {
|
||||
return crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
err = json.Unmarshal(b, target)
|
||||
if err != nil {
|
||||
return crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// marshalMetadata marshals the given interface to map[string]interface{}
|
||||
func marshalMetadata(o interface{}) (meta map[string]interface{}, err error) {
|
||||
b, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
return nil, crgerrs.WrapError(crgerrs.ErrCodec, err.Error())
|
||||
}
|
||||
meta = make(map[string]interface{})
|
||||
err = json.Unmarshal(b, &meta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
+6
-6
@@ -3,6 +3,7 @@ package server
|
||||
// DONTCOVER
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -21,14 +22,14 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
"cosmossdk.io/tools/rosetta"
|
||||
crgserver "cosmossdk.io/tools/rosetta/lib/server"
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/client/flags"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
"github.com/cosmos/cosmos-sdk/server/api"
|
||||
serverconfig "github.com/cosmos/cosmos-sdk/server/config"
|
||||
servergrpc "github.com/cosmos/cosmos-sdk/server/grpc"
|
||||
"github.com/cosmos/cosmos-sdk/server/rosetta"
|
||||
crgserver "github.com/cosmos/cosmos-sdk/server/rosetta/lib/server"
|
||||
"github.com/cosmos/cosmos-sdk/server/types"
|
||||
pruningtypes "github.com/cosmos/cosmos-sdk/store/pruning/types"
|
||||
"github.com/cosmos/cosmos-sdk/telemetry"
|
||||
@@ -463,10 +464,9 @@ func startInProcess(ctx *Context, clientCtx client.Context, appCreator types.App
|
||||
if config.Rosetta.Enable {
|
||||
offlineMode := config.Rosetta.Offline
|
||||
|
||||
// If GRPC is not enabled rosetta cannot work in online mode, so it works in
|
||||
// offline mode.
|
||||
if !config.GRPC.Enable {
|
||||
offlineMode = true
|
||||
// If GRPC is not enabled rosetta cannot work in online mode, so we throw an error.
|
||||
if !config.GRPC.Enable && !offlineMode {
|
||||
return errors.New("'grpc' must be enable in online mode for Rosetta to work")
|
||||
}
|
||||
|
||||
minGasPrices, err := sdktypes.ParseDecCoins(config.MinGasPrices)
|
||||
|
||||
Reference in New Issue
Block a user