Rosetta API implementation (#7695)
Ref: #7492 Co-authored-by: Jonathan Gimeno <jgimeno@gmail.com> Co-authored-by: Alessio Treglia <alessio@tendermint.com> Co-authored-by: Frojdi Dymylja <33157909+fdymylja@users.noreply.github.com> Co-authored-by: Robert Zaremba <robert@zaremba.ch> Co-authored-by: Federico Kunze <31522760+fedekunze@users.noreply.github.com>
This commit is contained in:
co-authored by
Jonathan Gimeno
Alessio Treglia
Frojdi Dymylja
Robert Zaremba
Federico Kunze
parent
d226254578
commit
57f5e96570
@@ -5,6 +5,7 @@
|
||||
- Jonathan Gimeno (@jgimeno)
|
||||
- David Grierson (@senormonito)
|
||||
- Alessio Treglia (@alessio)
|
||||
- Frojdy Dymylja (@fdymylja)
|
||||
|
||||
## Context
|
||||
|
||||
@@ -35,9 +36,11 @@ We will achieve these delivering on these principles by the following:
|
||||
|
||||
1. There will be an external repo called [cosmos-rosetta-gateway](https://github.com/tendermint/cosmos-rosetta-gateway)
|
||||
for the implementation of the core Rosetta API features, particularly:
|
||||
a. The types and interfaces. This separates design from implementation detail.
|
||||
b. Some core implementations: specifically, the `Service` functionality as this is independent of the Cosmos SDK version.
|
||||
2. Due to differences between the Cosmos release series, each series will have its own specific API implementations of `Network` struct and `Adapter` interface.
|
||||
a. The types and interfaces (`Client`, `OfflineClient`...), this separates design from implementation detail.
|
||||
b. The `Server` functionality as this is independent of the Cosmos SDK version.
|
||||
c. The `Online/OfflineNetwork`, which is not exported, and implements the rosetta API using the `Client` interface to query the node, build tx and so on.
|
||||
d. The `errors` package to extend rosetta errors.
|
||||
2. Due to differences between the Cosmos release series, each series will have its own specific implementation of `Client` interface.
|
||||
3. There will be two options for starting an API service in applications:
|
||||
a. API shares the application process
|
||||
b. API-specific process.
|
||||
@@ -49,143 +52,130 @@ We will achieve these delivering on these principles by the following:
|
||||
|
||||
As section will describe the proposed external library, including the service implementation, plus the defined types and interfaces.
|
||||
|
||||
#### Service
|
||||
#### Server
|
||||
|
||||
`Service` is a simple `struct` that is started and listens to the port specified in the options. This is meant to be used across all the Cosmos SDK versions that are actively supported.
|
||||
`Server` is a simple `struct` that is started and listens to the port specified in the settings. This is meant to be used across all the Cosmos SDK versions that are actively supported.
|
||||
|
||||
The constructor follows:
|
||||
|
||||
`func New(options Options, network Network) (*Service, error)`
|
||||
`func NewServer(settings Settings) (Server, error)`
|
||||
|
||||
`Settings`, which are used to construct a new server, are the following:
|
||||
```go
|
||||
// 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
|
||||
}
|
||||
```
|
||||
|
||||
#### Types
|
||||
|
||||
`Service` accepts an `Options` `struct` that holds service configuration values, such as the port the service would be listening to:
|
||||
Package types uses a mixture of rosetta types and custom defined type wrappers, that the client must parse and return while executing operations.
|
||||
|
||||
```golang
|
||||
type Options struct {
|
||||
ListenAddress string
|
||||
|
||||
##### Interfaces
|
||||
|
||||
Every SDK version uses a different format to connect (rpc, gRPC, etc), query and build transactions, we have abstracted this in what is the `Client` interface.
|
||||
The client uses rosetta types, whilst the `Online/OfflineNetwork` takes care of returning correctly parsed rosetta responses and errors.
|
||||
|
||||
Each Cosmos SDK release series will have their own `Client` implementations.
|
||||
Developers can implement their own custom `Client`s as required.
|
||||
|
||||
```go
|
||||
// Client defines the API the client implementation should provide.
|
||||
type Client interface {
|
||||
// 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)
|
||||
// BlockByHashAlt gets a block and its transaction at the provided height
|
||||
BlockByHash(ctx context.Context, hash string) (BlockResponse, error)
|
||||
// BlockByHeightAlt 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)
|
||||
// BlockTransactionsByHash 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
|
||||
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) (options map[string]interface{}, err error)
|
||||
// AccountIdentifierFromPublicKey returns the account identifier given the public key
|
||||
AccountIdentifierFromPublicKey(pubKey *types.PublicKey) (*types.AccountIdentifier, error)
|
||||
}
|
||||
```
|
||||
|
||||
The `Network` type holds network-specific properties (i.e. configuration values) and adapters. Pre-configured concrete types will be available for each Cosmos SDK release. Applications can also create their own custom types.
|
||||
|
||||
```golang
|
||||
type Network struct {
|
||||
Properties rosetta.NetworkProperties
|
||||
Adapter rosetta.Adapter
|
||||
}
|
||||
```
|
||||
|
||||
A `NetworkProperties` `struct` comprises basic values that are required by a Rosetta API `Service`:
|
||||
|
||||
```golang
|
||||
type NetworkProperties struct {
|
||||
// Mandatory properties
|
||||
Blockchain string
|
||||
Network string
|
||||
SupportedOperations []string
|
||||
}
|
||||
```
|
||||
|
||||
Rosetta API services use `Blockchain` and `Network` as identifiers, e.g. the developers of _gaia_, the application that powers the Cosmos Hub, may want to set those to `Cosmos Hub` and `cosmos-hub-3` respectively.
|
||||
|
||||
`SupportedOperations` contains the transaction types that are supported by the library. At the present time,
|
||||
only `cosmos-sdk/MsgSend` is supported in Launchpad. Additional operations will be added in due time.
|
||||
|
||||
For Launchpad we will map the amino type name to the operation supported, in Stargate we will use the protoc one.
|
||||
|
||||
#### Interfaces
|
||||
|
||||
Every SDK version uses a different format to connect (rpc, gRpc, etc), we have abstracted this in what is called the
|
||||
Adapter. This is an interface that defines the methods an adapter implementation must provide in order to be used
|
||||
in the `Network` interface.
|
||||
|
||||
Each Cosmos SDK release series will have their own Adapter implementations.
|
||||
Developers can implement their own custom adapters as required.
|
||||
|
||||
```golang
|
||||
type Adapter interface {
|
||||
DataAPI
|
||||
ConstructionAPI
|
||||
}
|
||||
|
||||
type DataAPI interface {
|
||||
server.NetworkAPIServicer
|
||||
server.AccountAPIServicer
|
||||
server.MempoolAPIServicer
|
||||
server.BlockAPIServicer
|
||||
server.ConstructionAPIServicer
|
||||
}
|
||||
|
||||
type ConstructionAPI interface {
|
||||
server.ConstructionAPIServicer
|
||||
}
|
||||
```
|
||||
|
||||
Example in pseudo-code of an Adapter interface:
|
||||
|
||||
```golang
|
||||
type SomeAdapter struct {
|
||||
cosmosClient client
|
||||
tendermintClient client
|
||||
}
|
||||
|
||||
func NewSomeAdapter(cosmosClient client, tendermintClient client) rosetta.Adapter {
|
||||
return &SomeAdapter{cosmosClient: cosmosClient, tendermintClient: tendermintClient}
|
||||
}
|
||||
|
||||
func (s SomeAdapter) NetworkStatus(ctx context.Context, request *types.NetworkRequest) (*types.NetworkStatusResponse, *types.Error) {
|
||||
resp := s.tendermintClient.CallStatus()
|
||||
// ... Parse status Response
|
||||
// build NetworkStatusResponse
|
||||
return networkStatusResp, nil
|
||||
}
|
||||
|
||||
func (s SomeAdapter) AccountBalance(ctx context.Context, request *types.AccountBalanceRequest) (*types.AccountBalanceResponse, *types.Error) {
|
||||
resp := s.cosmosClient.Account()
|
||||
// ... Parse cosmos specific account response
|
||||
// build AccountBalanceResponse
|
||||
return AccountBalanceResponse, nil
|
||||
}
|
||||
|
||||
// And we repeat for all the methods defined in the interface.
|
||||
```
|
||||
|
||||
For further information about the `Servicer` interfaces, please refer to the [Coinbase's rosetta-sdk-go's documentation](https://pkg.go.dev/github.com/coinbase/rosetta-sdk-go@v0.5.9/server).
|
||||
|
||||
### 2. Cosmos SDK Implementation
|
||||
|
||||
As described, each Cosmos SDK release series will have version specific implementations of `Network` and `Adapter`, as
|
||||
well as a `NewNetwork` constructor.
|
||||
The cosmos sdk implementation, based on version, takes care of satisfying the `Client` interface.
|
||||
In Stargate, Launchpad and 0.37, we have introduced the concept of rosetta.Msg, this message is not in the shared repository as the sdk.Msg type differs between cosmos-sdk versions.
|
||||
|
||||
Due to separation of interface and implementation, application developers have the option to override as needed,
|
||||
using this code as reference.
|
||||
The rosetta.Msg interface follows:
|
||||
|
||||
```golang
|
||||
// NewNetwork returns the default application configuration.
|
||||
func NewNetwork(options Options) service.Network {
|
||||
cosmosClient := cosmos.NewClient(fmt.Sprintf("http://%s", options.CosmosEndpoint))
|
||||
tendermintClient := tendermint.NewClient(fmt.Sprintf("http://%s", options.TendermintEndpoint))
|
||||
|
||||
return service.Network{
|
||||
Properties: rosetta.NetworkProperties{
|
||||
Blockchain: options.Blockchain,
|
||||
Network: options.Network,
|
||||
SupportedOperations: []string{OperationTransfer},
|
||||
},
|
||||
Adapter: newAdapter(
|
||||
cosmosClient,
|
||||
tendermintClient,
|
||||
properties{
|
||||
Blockchain: options.Blockchain,
|
||||
Network: options.Network,
|
||||
OfflineMode: options.OfflineMode,
|
||||
},
|
||||
),
|
||||
}
|
||||
```go
|
||||
// Msg represents a cosmos-sdk message that can be converted from and to a rosetta operation.
|
||||
type Msg interface {
|
||||
sdk.Msg
|
||||
ToOperations(withStatus, hasError bool) []*types.Operation
|
||||
FromOperations(ops []*types.Operation) (sdk.Msg, error)
|
||||
}
|
||||
```
|
||||
|
||||
Hence developers who want to extend the rosetta set of supported operations just need to extend their module's sdk.Msgs with the `ToOperations` and `FromOperations` methods.
|
||||
### 3. API service invocation
|
||||
|
||||
As stated at the start, application developers will have two methods for invocation of the Rosetta API service:
|
||||
@@ -195,67 +185,13 @@ As stated at the start, application developers will have two methods for invocat
|
||||
|
||||
#### Shared Process (Only Stargate)
|
||||
|
||||
Rosetta API service could run within the same execution process as the application. New configuration option and
|
||||
command line flags would be provided to support this:
|
||||
Rosetta API service could run within the same execution process as the application. This would be enabled via app.toml settings, and if gRPC is not enabled the rosetta instance would be spinned in offline mode (tx building capabilities only).
|
||||
|
||||
```golang
|
||||
if config.Rosetta.Enable {
|
||||
....
|
||||
get contecxt, flags, etc
|
||||
...
|
||||
|
||||
h, err := service.New(
|
||||
service.Options{ListenAddress: config.Rosetta.ListenAddress},
|
||||
rosetta.NewNetwork(cdc, options),
|
||||
)
|
||||
if err != nil {
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
go func() {
|
||||
if err := h.Start(config); err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
#### Separate API service
|
||||
|
||||
Client application developers can write a new command to launch a Rosetta API server as a separate process too:
|
||||
Client application developers can write a new command to launch a Rosetta API server as a separate process too, using the rosetta command contained in the `/server/rosetta` package. Construction of the command depends on cosmos sdk version. Examples can be found inside `simd` for stargate, and `contrib/rosetta/simapp` for other release series.
|
||||
|
||||
```golang
|
||||
func RosettaCommand(cdc *codec.Codec) *cobra.Command {
|
||||
|
||||
...
|
||||
cmd := &cobra.Command{
|
||||
Use: "rosetta",
|
||||
....
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
....
|
||||
get contecxt, flags, etc
|
||||
...
|
||||
|
||||
h, err := service.New(
|
||||
service.Options{Endpoint: endpoint},
|
||||
rosetta.NewNetwork(cdc, options),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
h.Start()
|
||||
}
|
||||
}
|
||||
...
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
## Status
|
||||
|
||||
|
||||
@@ -13,3 +13,4 @@ This folder contains documentation on how to run a node and interact with it.
|
||||
1. [Interacting with a Node](./interact-node.md)
|
||||
1. [Generating, Signing and Broadcasting Transactions](./txs.md)
|
||||
1. [Cosmos Upgrade Manager](./cosmovisor.md)
|
||||
1. [Rosetta API](./rosetta.md)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Rosetta
|
||||
|
||||
Package rosetta implements the rosetta API for the current cosmos sdk release series.
|
||||
|
||||
The client satisfies [cosmos-rosetta-gateway](https://github.com/tendermint/cosmos-rosetta-gateway) `Client` interface implementation.
|
||||
|
||||
## Extension
|
||||
|
||||
There are two ways in which you can customize and extend the implementation with your custom settings.
|
||||
|
||||
### Message extension
|
||||
|
||||
In order to make an `sdk.Msg` understandable by rosetta the only thing which is required is adding the methods to your message that satisfy the `rosetta.Msg` interface.
|
||||
Examples on how to do so can be found in the staking types such as `MsgDelegate`, or in bank types such as `MsgSend`.
|
||||
|
||||
### Client interface override
|
||||
|
||||
In case more customization is required, it's possible to embed the Client type and override the methods which require customizations.
|
||||
|
||||
Example:
|
||||
```go
|
||||
package custom_client
|
||||
import (
|
||||
|
||||
"context"
|
||||
"github.com/coinbase/rosetta-sdk-go/types"
|
||||
"github.com/cosmos/cosmos-sdk/server/rosetta"
|
||||
)
|
||||
|
||||
// CustomClient embeds the standard cosmos client
|
||||
// which means that it implements the cosmos-rosetta-gateway Client
|
||||
// interface while at the same time allowing to customize certain methods
|
||||
type CustomClient struct {
|
||||
*rosetta.Client
|
||||
}
|
||||
|
||||
func (c *CustomClient) ConstructionPayload(_ context.Context, request *types.ConstructionPayloadsRequest) (resp *types.ConstructionPayloadsResponse, err error) {
|
||||
// provide custom signature bytes
|
||||
panic("implement me")
|
||||
}
|
||||
```
|
||||
|
||||
### Error extension
|
||||
|
||||
Since rosetta requires to provide 'returned' errors to network options. In order to declare a new rosetta error, we use the `errors` package in cosmos-rosetta-gateway.
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
package custom_errors
|
||||
import crgerrs "github.com/tendermint/cosmos-rosetta-gateway/errors"
|
||||
|
||||
var customErrRetriable = true
|
||||
var CustomError = crgerrs.RegisterError(100, "custom message", customErrRetriable, "description")
|
||||
```
|
||||
|
||||
Note: errors must be registered before cosmos-rosetta-gateway's `Server`.`Start` method is called. Otherwise the registration will be ignored. Errors with same code will be ignored too.
|
||||
|
||||
## Integration in app.go
|
||||
|
||||
To integrate rosetta as a command in your application, in app.go, in your root command simply use the `server.RosettaCommand` method.
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
package app
|
||||
import (
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/server"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func buildAppCommand(rootCmd *cobra.Command) {
|
||||
// more app.go init stuff
|
||||
// ...
|
||||
// add rosetta command
|
||||
rootCmd.AddCommand(server.RosettaCommand(encodingConfig.InterfaceRegistry, encodingConfig.Marshaler))
|
||||
}
|
||||
```
|
||||
|
||||
A full implementation example can be found in `simapp` package.
|
||||
|
||||
NOTE: when using a customized client, the command cannot be used as the constructors required **may** differ, so it's required to create a new one. We intend to provide a way to init a customized client without writing extra code in the future.
|
||||
Reference in New Issue
Block a user