github.com/Finschia/finschia-sdk@v0.48.1/server/rosetta/client_online.go (about)

     1  package rosetta
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/base64"
     7  	"encoding/hex"
     8  	"errors"
     9  	"fmt"
    10  	"regexp"
    11  	"strconv"
    12  	"time"
    13  
    14  	rosettatypes "github.com/coinbase/rosetta-sdk-go/types"
    15  	"google.golang.org/grpc"
    16  	"google.golang.org/grpc/metadata"
    17  
    18  	ocrpc "github.com/Finschia/ostracon/rpc/client"
    19  	"github.com/Finschia/ostracon/rpc/client/http"
    20  	abci "github.com/tendermint/tendermint/abci/types"
    21  
    22  	crgerrs "github.com/Finschia/finschia-sdk/server/rosetta/lib/errors"
    23  	crgtypes "github.com/Finschia/finschia-sdk/server/rosetta/lib/types"
    24  	sdk "github.com/Finschia/finschia-sdk/types"
    25  	grpctypes "github.com/Finschia/finschia-sdk/types/grpc"
    26  	"github.com/Finschia/finschia-sdk/version"
    27  	authtx "github.com/Finschia/finschia-sdk/x/auth/tx"
    28  	auth "github.com/Finschia/finschia-sdk/x/auth/types"
    29  	bank "github.com/Finschia/finschia-sdk/x/bank/types"
    30  )
    31  
    32  // interface assertion
    33  var _ crgtypes.Client = (*Client)(nil)
    34  
    35  const (
    36  	tmWebsocketPath    = "/websocket"
    37  	defaultNodeTimeout = 15 * time.Second
    38  )
    39  
    40  // Client implements a single network client to interact with cosmos based chains
    41  type Client struct {
    42  	supportedOperations []string
    43  
    44  	config *Config
    45  
    46  	auth  auth.QueryClient
    47  	bank  bank.QueryClient
    48  	tmRPC ocrpc.Client
    49  
    50  	version string
    51  
    52  	converter Converter
    53  }
    54  
    55  // NewClient instantiates a new online servicer
    56  func NewClient(cfg *Config) (*Client, error) {
    57  	info := version.NewInfo()
    58  
    59  	v := info.Version
    60  	if v == "" {
    61  		v = "unknown"
    62  	}
    63  
    64  	txConfig := authtx.NewTxConfig(cfg.Codec, authtx.DefaultSignModes)
    65  
    66  	var supportedOperations []string
    67  	for _, ii := range cfg.InterfaceRegistry.ListImplementations(sdk.MsgInterfaceProtoName) {
    68  		resolvedMsg, err := cfg.InterfaceRegistry.Resolve(ii)
    69  		if err != nil {
    70  			continue
    71  		}
    72  
    73  		if _, ok := resolvedMsg.(sdk.Msg); ok {
    74  			supportedOperations = append(supportedOperations, ii)
    75  		}
    76  	}
    77  
    78  	supportedOperations = append(
    79  		supportedOperations,
    80  		bank.EventTypeCoinSpent,
    81  		bank.EventTypeCoinReceived,
    82  		bank.EventTypeCoinBurn,
    83  	)
    84  
    85  	return &Client{
    86  		supportedOperations: supportedOperations,
    87  		config:              cfg,
    88  		auth:                nil,
    89  		bank:                nil,
    90  		tmRPC:               nil,
    91  		version:             fmt.Sprintf("%s/%s", info.AppName, v),
    92  		converter:           NewConverter(cfg.Codec, cfg.InterfaceRegistry, txConfig),
    93  	}, nil
    94  }
    95  
    96  // ---------- cosmos-rosetta-gateway.types.Client implementation ------------ //
    97  
    98  // Bootstrap is gonna connect the client to the endpoints
    99  func (c *Client) Bootstrap() error {
   100  	grpcConn, err := grpc.Dial(c.config.GRPCEndpoint, grpc.WithInsecure())
   101  	if err != nil {
   102  		return err
   103  	}
   104  
   105  	tmRPC, err := http.New(c.config.TendermintRPC, tmWebsocketPath)
   106  	if err != nil {
   107  		return err
   108  	}
   109  
   110  	authClient := auth.NewQueryClient(grpcConn)
   111  	bankClient := bank.NewQueryClient(grpcConn)
   112  
   113  	c.auth = authClient
   114  	c.bank = bankClient
   115  	c.tmRPC = tmRPC
   116  
   117  	return nil
   118  }
   119  
   120  // Ready performs a health check and returns an error if the client is not ready.
   121  func (c *Client) Ready() error {
   122  	ctx, cancel := context.WithTimeout(context.Background(), defaultNodeTimeout)
   123  	defer cancel()
   124  	_, err := c.tmRPC.Health(ctx)
   125  	if err != nil {
   126  		return err
   127  	}
   128  	_, err = c.bank.TotalSupply(ctx, &bank.QueryTotalSupplyRequest{})
   129  	if err != nil {
   130  		return err
   131  	}
   132  	return nil
   133  }
   134  
   135  func (c *Client) accountInfo(ctx context.Context, addr string, height *int64) (*SignerData, error) {
   136  	if height != nil {
   137  		strHeight := strconv.FormatInt(*height, 10)
   138  		ctx = metadata.AppendToOutgoingContext(ctx, grpctypes.GRPCBlockHeightHeader, strHeight)
   139  	}
   140  
   141  	accountInfo, err := c.auth.Account(ctx, &auth.QueryAccountRequest{
   142  		Address: addr,
   143  	})
   144  	if err != nil {
   145  		return nil, crgerrs.FromGRPCToRosettaError(err)
   146  	}
   147  
   148  	signerData, err := c.converter.ToRosetta().SignerData(accountInfo.Account)
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  	return signerData, nil
   153  }
   154  
   155  func (c *Client) Balances(ctx context.Context, addr string, height *int64) ([]*rosettatypes.Amount, error) {
   156  	if height != nil {
   157  		strHeight := strconv.FormatInt(*height, 10)
   158  		ctx = metadata.AppendToOutgoingContext(ctx, grpctypes.GRPCBlockHeightHeader, strHeight)
   159  	}
   160  
   161  	balance, err := c.bank.AllBalances(ctx, &bank.QueryAllBalancesRequest{
   162  		Address: addr,
   163  	})
   164  	if err != nil {
   165  		return nil, crgerrs.FromGRPCToRosettaError(err)
   166  	}
   167  
   168  	availableCoins, err := c.coins(ctx)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  
   173  	return c.converter.ToRosetta().Amounts(balance.Balances, availableCoins), nil
   174  }
   175  
   176  func (c *Client) BlockByHash(ctx context.Context, hash string) (crgtypes.BlockResponse, error) {
   177  	bHash, err := hex.DecodeString(hash)
   178  	if err != nil {
   179  		return crgtypes.BlockResponse{}, fmt.Errorf("invalid block hash: %s", err)
   180  	}
   181  
   182  	block, err := c.tmRPC.BlockByHash(ctx, bHash)
   183  	if err != nil {
   184  		return crgtypes.BlockResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error())
   185  	}
   186  
   187  	return c.converter.ToRosetta().BlockResponse(block), nil
   188  }
   189  
   190  func (c *Client) BlockByHeight(ctx context.Context, height *int64) (crgtypes.BlockResponse, error) {
   191  	height, err := c.getHeight(ctx, height)
   192  	if err != nil {
   193  		return crgtypes.BlockResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error())
   194  	}
   195  	block, err := c.tmRPC.Block(ctx, height)
   196  	if err != nil {
   197  		return crgtypes.BlockResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error())
   198  	}
   199  
   200  	return c.converter.ToRosetta().BlockResponse(block), nil
   201  }
   202  
   203  func (c *Client) BlockTransactionsByHash(ctx context.Context, hash string) (crgtypes.BlockTransactionsResponse, error) {
   204  	// TODO(fdymylja): use a faster path, by searching the block by hash, instead of doing a double query operation
   205  	blockResp, err := c.BlockByHash(ctx, hash)
   206  	if err != nil {
   207  		return crgtypes.BlockTransactionsResponse{}, err
   208  	}
   209  
   210  	return c.blockTxs(ctx, &blockResp.Block.Index)
   211  }
   212  
   213  func (c *Client) BlockTransactionsByHeight(ctx context.Context, height *int64) (crgtypes.BlockTransactionsResponse, error) {
   214  	height, err := c.getHeight(ctx, height)
   215  	if err != nil {
   216  		return crgtypes.BlockTransactionsResponse{}, crgerrs.WrapError(crgerrs.ErrBadGateway, err.Error())
   217  	}
   218  	blockTxResp, err := c.blockTxs(ctx, height)
   219  	if err != nil {
   220  		return crgtypes.BlockTransactionsResponse{}, err
   221  	}
   222  	return blockTxResp, nil
   223  }
   224  
   225  // Coins fetches the existing coins in the application
   226  func (c *Client) coins(ctx context.Context) (sdk.Coins, error) {
   227  	supply, err := c.bank.TotalSupply(ctx, &bank.QueryTotalSupplyRequest{})
   228  	if err != nil {
   229  		return nil, crgerrs.FromGRPCToRosettaError(err)
   230  	}
   231  	return supply.Supply, nil
   232  }
   233  
   234  func (c *Client) TxOperationsAndSignersAccountIdentifiers(signed bool, txBytes []byte) (ops []*rosettatypes.Operation, signers []*rosettatypes.AccountIdentifier, err error) {
   235  	switch signed {
   236  	case false:
   237  		rosTx, err := c.converter.ToRosetta().Tx(txBytes, nil)
   238  		if err != nil {
   239  			return nil, nil, err
   240  		}
   241  		return rosTx.Operations, nil, err
   242  	default:
   243  		ops, signers, err = c.converter.ToRosetta().OpsAndSigners(txBytes)
   244  		return
   245  	}
   246  }
   247  
   248  // GetTx returns a transaction given its hash. For Rosetta we  make a synthetic transaction for BeginBlock
   249  // and EndBlock to adhere to balance tracking rules.
   250  func (c *Client) GetTx(ctx context.Context, hash string) (*rosettatypes.Transaction, error) {
   251  	hashBytes, err := hex.DecodeString(hash)
   252  	if err != nil {
   253  		return nil, crgerrs.WrapError(crgerrs.ErrCodec, fmt.Sprintf("bad tx hash: %s", err))
   254  	}
   255  
   256  	// get tx type and hash
   257  	txType, hashBytes := c.converter.ToSDK().HashToTxType(hashBytes)
   258  
   259  	// construct rosetta tx
   260  	switch txType {
   261  	// handle begin block hash
   262  	case BeginBlockTx:
   263  		// get block height by hash
   264  		block, err := c.tmRPC.BlockByHash(ctx, hashBytes)
   265  		if err != nil {
   266  			return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
   267  		}
   268  
   269  		// get block txs
   270  		fullBlock, err := c.blockTxs(ctx, &block.Block.Height)
   271  		if err != nil {
   272  			return nil, err
   273  		}
   274  
   275  		return fullBlock.Transactions[0], nil
   276  	// handle deliver tx hash
   277  	case DeliverTxTx:
   278  		rawTx, err := c.tmRPC.Tx(ctx, hashBytes, true)
   279  		if err != nil {
   280  			return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
   281  		}
   282  		return c.converter.ToRosetta().Tx(rawTx.Tx, &rawTx.TxResult)
   283  	// handle end block hash
   284  	case EndBlockTx:
   285  		// get block height by hash
   286  		block, err := c.tmRPC.BlockByHash(ctx, hashBytes)
   287  		if err != nil {
   288  			return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
   289  		}
   290  
   291  		// get block txs
   292  		fullBlock, err := c.blockTxs(ctx, &block.Block.Height)
   293  		if err != nil {
   294  			return nil, err
   295  		}
   296  
   297  		// get last tx
   298  		return fullBlock.Transactions[len(fullBlock.Transactions)-1], nil
   299  	// unrecognized tx
   300  	default:
   301  		return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("invalid tx hash provided: %s", hash))
   302  	}
   303  }
   304  
   305  // GetUnconfirmedTx gets an unconfirmed transaction given its hash
   306  func (c *Client) GetUnconfirmedTx(ctx context.Context, hash string) (*rosettatypes.Transaction, error) {
   307  	res, err := c.tmRPC.UnconfirmedTxs(ctx, nil)
   308  	if err != nil {
   309  		return nil, crgerrs.WrapError(crgerrs.ErrNotFound, "unconfirmed tx not found")
   310  	}
   311  
   312  	hashAsBytes, err := hex.DecodeString(hash)
   313  	if err != nil {
   314  		return nil, crgerrs.WrapError(crgerrs.ErrInterpreting, "invalid hash")
   315  	}
   316  
   317  	// assert that correct tx length is provided
   318  	switch len(hashAsBytes) {
   319  	default:
   320  		return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, fmt.Sprintf("unrecognized tx size: %d", len(hashAsBytes)))
   321  	case BeginEndBlockTxSize:
   322  		return nil, crgerrs.WrapError(crgerrs.ErrBadArgument, "endblock and begin block txs cannot be unconfirmed")
   323  	case DeliverTxSize:
   324  		break
   325  	}
   326  
   327  	// iterate over unconfirmed txs to find the one with matching hash
   328  	for _, unconfirmedTx := range res.Txs {
   329  		if !bytes.Equal(unconfirmedTx.Hash(), hashAsBytes) {
   330  			continue
   331  		}
   332  
   333  		return c.converter.ToRosetta().Tx(unconfirmedTx, nil)
   334  	}
   335  	return nil, crgerrs.WrapError(crgerrs.ErrNotFound, "transaction not found in mempool: "+hash)
   336  }
   337  
   338  // Mempool returns the unconfirmed transactions in the mempool
   339  func (c *Client) Mempool(ctx context.Context) ([]*rosettatypes.TransactionIdentifier, error) {
   340  	txs, err := c.tmRPC.UnconfirmedTxs(ctx, nil)
   341  	if err != nil {
   342  		return nil, err
   343  	}
   344  
   345  	return c.converter.ToRosetta().TxIdentifiers(txs.Txs), nil
   346  }
   347  
   348  // Peers gets the number of peers
   349  func (c *Client) Peers(ctx context.Context) ([]*rosettatypes.Peer, error) {
   350  	netInfo, err := c.tmRPC.NetInfo(ctx)
   351  	if err != nil {
   352  		return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
   353  	}
   354  	return c.converter.ToRosetta().Peers(netInfo.Peers), nil
   355  }
   356  
   357  func (c *Client) Status(ctx context.Context) (*rosettatypes.SyncStatus, error) {
   358  	status, err := c.tmRPC.Status(ctx)
   359  	if err != nil {
   360  		return nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
   361  	}
   362  	return c.converter.ToRosetta().SyncStatus(status), err
   363  }
   364  
   365  func (c *Client) PostTx(txBytes []byte) (*rosettatypes.TransactionIdentifier, map[string]interface{}, error) {
   366  	// sync ensures it will go through checkTx
   367  	res, err := c.tmRPC.BroadcastTxSync(context.Background(), txBytes)
   368  	if err != nil {
   369  		return nil, nil, crgerrs.WrapError(crgerrs.ErrUnknown, err.Error())
   370  	}
   371  	// check if tx was broadcast successfully
   372  	if res.Code != abci.CodeTypeOK {
   373  		return nil, nil, crgerrs.WrapError(
   374  			crgerrs.ErrUnknown,
   375  			fmt.Sprintf("transaction broadcast failure: (%d) %s ", res.Code, res.Log),
   376  		)
   377  	}
   378  
   379  	return &rosettatypes.TransactionIdentifier{
   380  			Hash: fmt.Sprintf("%X", res.Hash),
   381  		},
   382  		map[string]interface{}{
   383  			Log: res.Log,
   384  		}, nil
   385  }
   386  
   387  // construction endpoints
   388  
   389  // ConstructionMetadataFromOptions builds the metadata given the options
   390  func (c *Client) ConstructionMetadataFromOptions(ctx context.Context, options map[string]interface{}) (meta map[string]interface{}, err error) {
   391  	if len(options) == 0 {
   392  		return nil, crgerrs.ErrBadArgument
   393  	}
   394  
   395  	constructionOptions := new(PreprocessOperationsOptionsResponse)
   396  
   397  	err = constructionOptions.FromMetadata(options)
   398  	if err != nil {
   399  		return nil, err
   400  	}
   401  
   402  	signersData := make([]*SignerData, len(constructionOptions.ExpectedSigners))
   403  
   404  	for i, signer := range constructionOptions.ExpectedSigners {
   405  		accountInfo, err := c.accountInfo(ctx, signer, nil)
   406  		if err != nil {
   407  			return nil, err
   408  		}
   409  
   410  		signersData[i] = accountInfo
   411  	}
   412  
   413  	status, err := c.tmRPC.Status(ctx)
   414  	if err != nil {
   415  		return nil, err
   416  	}
   417  
   418  	metadataResp := ConstructionMetadata{
   419  		ChainID:     status.NodeInfo.Network,
   420  		SignersData: signersData,
   421  		GasLimit:    constructionOptions.GasLimit,
   422  		GasPrice:    constructionOptions.GasPrice,
   423  		Memo:        constructionOptions.Memo,
   424  	}
   425  
   426  	return metadataResp.ToMetadata()
   427  }
   428  
   429  func (c *Client) blockTxs(ctx context.Context, height *int64) (crgtypes.BlockTransactionsResponse, error) {
   430  	// get block info
   431  	blockInfo, err := c.tmRPC.Block(ctx, height)
   432  	if err != nil {
   433  		return crgtypes.BlockTransactionsResponse{}, err
   434  	}
   435  	// get block events
   436  	blockResults, err := c.tmRPC.BlockResults(ctx, height)
   437  	if err != nil {
   438  		return crgtypes.BlockTransactionsResponse{}, err
   439  	}
   440  
   441  	if len(blockResults.TxsResults) != len(blockInfo.Block.Txs) {
   442  		// wtf?
   443  		panic("block results transactions do now match block transactions")
   444  	}
   445  	// process begin and end block txs
   446  	beginBlockTx := &rosettatypes.Transaction{
   447  		TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: c.converter.ToRosetta().BeginBlockTxHash(blockInfo.BlockID.Hash)},
   448  		Operations: AddOperationIndexes(
   449  			nil,
   450  			c.converter.ToRosetta().BalanceOps(StatusTxSuccess, blockResults.BeginBlockEvents),
   451  		),
   452  	}
   453  
   454  	endBlockTx := &rosettatypes.Transaction{
   455  		TransactionIdentifier: &rosettatypes.TransactionIdentifier{Hash: c.converter.ToRosetta().EndBlockTxHash(blockInfo.BlockID.Hash)},
   456  		Operations: AddOperationIndexes(
   457  			nil,
   458  			c.converter.ToRosetta().BalanceOps(StatusTxSuccess, blockResults.EndBlockEvents),
   459  		),
   460  	}
   461  
   462  	deliverTx := make([]*rosettatypes.Transaction, len(blockInfo.Block.Txs))
   463  	// process normal txs
   464  	for i, tx := range blockInfo.Block.Txs {
   465  		rosTx, err := c.converter.ToRosetta().Tx(tx, blockResults.TxsResults[i])
   466  		if err != nil {
   467  			return crgtypes.BlockTransactionsResponse{}, err
   468  		}
   469  		deliverTx[i] = rosTx
   470  	}
   471  
   472  	finalTxs := make([]*rosettatypes.Transaction, 0, 2+len(deliverTx))
   473  	finalTxs = append(finalTxs, beginBlockTx)
   474  	finalTxs = append(finalTxs, deliverTx...)
   475  	finalTxs = append(finalTxs, endBlockTx)
   476  
   477  	return crgtypes.BlockTransactionsResponse{
   478  		BlockResponse: c.converter.ToRosetta().BlockResponse(blockInfo),
   479  		Transactions:  finalTxs,
   480  	}, nil
   481  }
   482  
   483  func (c *Client) getHeight(ctx context.Context, height *int64) (realHeight *int64, err error) {
   484  	if height != nil && *height == -1 {
   485  		genesisChunk, err := c.tmRPC.GenesisChunked(ctx, 0)
   486  		if err != nil {
   487  			return nil, err
   488  		}
   489  
   490  		heightNum, err := extractInitialHeightFromGenesisChunk(genesisChunk.Data)
   491  		if err != nil {
   492  			return nil, err
   493  		}
   494  
   495  		realHeight = &heightNum
   496  	} else {
   497  		realHeight = height
   498  	}
   499  	return
   500  }
   501  
   502  func extractInitialHeightFromGenesisChunk(genesisChunk string) (int64, error) {
   503  	firstChunk, err := base64.StdEncoding.DecodeString(genesisChunk)
   504  	if err != nil {
   505  		return 0, err
   506  	}
   507  
   508  	re, err := regexp.Compile("\"initial_height\":\"(\\d+)\"") //nolint:gocritic
   509  	if err != nil {
   510  		return 0, err
   511  	}
   512  
   513  	matches := re.FindStringSubmatch(string(firstChunk))
   514  	if len(matches) != 2 {
   515  		return 0, errors.New("failed to fetch initial_height")
   516  	}
   517  
   518  	heightStr := matches[1]
   519  	return strconv.ParseInt(heightStr, 10, 64)
   520  }