From ff363a46624d23cfa1ce0b63a2f860c31a2c793d Mon Sep 17 00:00:00 2001 From: Andrea Giacobino Date: Tue, 11 May 2021 15:32:59 +0200 Subject: [PATCH] feat: embed rosetta library (as is) taken from v1.0.0 release branch of the library: https://github.com/tendermint/cosmos-rosetta-gateway/tree/release/v1.0.0 see: https://github.com/cosmos/cosmos-sdk/issues/9300 --- server/rosetta/lib/errors/errors.go | 152 ++++++++++++++++ server/rosetta/lib/errors/registry.go | 48 +++++ .../lib/internal/service/construction.go | 118 +++++++++++++ server/rosetta/lib/internal/service/data.go | 158 +++++++++++++++++ .../rosetta/lib/internal/service/offline.go | 62 +++++++ server/rosetta/lib/internal/service/online.go | 66 +++++++ server/rosetta/lib/server/server.go | 115 ++++++++++++ server/rosetta/lib/types/types.go | 164 ++++++++++++++++++ 8 files changed, 883 insertions(+) create mode 100644 server/rosetta/lib/errors/errors.go create mode 100644 server/rosetta/lib/errors/registry.go create mode 100644 server/rosetta/lib/internal/service/construction.go create mode 100644 server/rosetta/lib/internal/service/data.go create mode 100644 server/rosetta/lib/internal/service/offline.go create mode 100644 server/rosetta/lib/internal/service/online.go create mode 100644 server/rosetta/lib/server/server.go create mode 100644 server/rosetta/lib/types/types.go diff --git a/server/rosetta/lib/errors/errors.go b/server/rosetta/lib/errors/errors.go new file mode 100644 index 000000000000..392aab5748d6 --- /dev/null +++ b/server/rosetta/lib/errors/errors.go @@ -0,0 +1,152 @@ +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 { + // check if one is nil and the other isn't + if (e == nil && err != nil) || (err == nil && e != nil) { + return false + } + // assert it can be casted + rosErr, ok := err.(*Error) + if !ok { + return false + } + // check that both *Error's are correctly initialized to avoid dereference panics + if (rosErr.rosErr == nil && e.rosErr != nil) || (e.rosErr == nil && rosErr.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 err == nil { + return nil + } + rosErr, ok := err.(*Error) + if !ok { + return ToRosetta(WrapError(ErrUnknown, err.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") +) diff --git a/server/rosetta/lib/errors/registry.go b/server/rosetta/lib/errors/registry.go new file mode 100644 index 000000000000..a1bdb898c061 --- /dev/null +++ b/server/rosetta/lib/errors/registry.go @@ -0,0 +1,48 @@ +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), +} diff --git a/server/rosetta/lib/internal/service/construction.go b/server/rosetta/lib/internal/service/construction.go new file mode 100644 index 000000000000..5510c9d813e6 --- /dev/null +++ b/server/rosetta/lib/internal/service/construction.go @@ -0,0 +1,118 @@ +package service + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "strings" + + "github.com/coinbase/rosetta-sdk-go/types" + "github.com/cosmos/cosmos-sdk/server/rosetta/lib/errors" +) + +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 +} + +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 +} + +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 +} + +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) + } + + return &types.ConstructionMetadataResponse{ + Metadata: metadata, + }, nil +} + +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 + +} + +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 +} + +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 +} + +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 +} diff --git a/server/rosetta/lib/internal/service/data.go b/server/rosetta/lib/internal/service/data.go new file mode 100644 index 000000000000..38b8617d4511 --- /dev/null +++ b/server/rosetta/lib/internal/service/data.go @@ -0,0 +1,158 @@ +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 + ) + // block identifier is assumed not to be nil as rosetta will do this check for us + // check if we have to query via hash or block number + switch { + 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: + err := errors.WrapError(errors.ErrBadArgument, "at least one of hash or index needs to be specified") + 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 +} diff --git a/server/rosetta/lib/internal/service/offline.go b/server/rosetta/lib/internal/service/offline.go new file mode 100644 index 000000000000..40eda47365e0 --- /dev/null +++ b/server/rosetta/lib/internal/service/offline.go @@ -0,0 +1,62 @@ +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 +} + +// 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) +} diff --git a/server/rosetta/lib/internal/service/online.go b/server/rosetta/lib/internal/service/online.go new file mode 100644 index 000000000000..eea263e09839 --- /dev/null +++ b/server/rosetta/lib/internal/service/online.go @@ -0,0 +1,66 @@ +package service + +import ( + "context" + "time" + + "github.com/coinbase/rosetta-sdk-go/types" + "github.com/cosmos/cosmos-sdk/server/rosetta/lib/errors" + 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 + block, err := client.BlockByHeight(ctx, &genesisHeight) + if err != nil { + return OnlineNetwork{}, err + } + + return OnlineNetwork{ + client: client, + network: network, + networkOptions: networkOptionsFromClient(client), + 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) *types.NetworkOptionsResponse { + return &types.NetworkOptionsResponse{ + Version: &types.Version{ + RosettaVersion: crgtypes.SpecVersion, + NodeVersion: client.Version(), + }, + Allow: &types.Allow{ + OperationStatuses: client.OperationStatuses(), + OperationTypes: client.SupportedOperations(), + Errors: errors.SealAndListErrors(), + HistoricalBalanceLookup: true, + }, + } +} diff --git a/server/rosetta/lib/server/server.go b/server/rosetta/lib/server/server.go new file mode 100644 index 000000000000..a9104af3c3ee --- /dev/null +++ b/server/rosetta/lib/server/server.go @@ -0,0 +1,115 @@ +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 +const 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) +} + +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) +} diff --git a/server/rosetta/lib/types/types.go b/server/rosetta/lib/types/types.go new file mode 100644 index 000000000000..bb1dd3db1cab --- /dev/null +++ b/server/rosetta/lib/types/types.go @@ -0,0 +1,164 @@ +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 + // OperationsStatuses 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 { + // 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) (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) +}