Skip to content

Commit

Permalink
feat(rpc): Add client wrapper (#1195)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ryan authored Nov 1, 2022
1 parent faf8b78 commit 9c996a7
Show file tree
Hide file tree
Showing 18 changed files with 922 additions and 8 deletions.
77 changes: 77 additions & 0 deletions api/rpc/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package client

import (
"context"

"github.com/filecoin-project/go-jsonrpc"

"github.com/celestiaorg/celestia-node/nodebuilder/das"
"github.com/celestiaorg/celestia-node/nodebuilder/fraud"
"github.com/celestiaorg/celestia-node/nodebuilder/header"
"github.com/celestiaorg/celestia-node/nodebuilder/share"
"github.com/celestiaorg/celestia-node/nodebuilder/state"
)

type API interface {
fraud.Module
header.Module
state.Module
share.Module
das.Module
}

type Client struct {
Fraud fraud.API
Header header.API
State state.API
Share share.API
DAS das.API

closer multiClientCloser
}

// multiClientCloser is a wrapper struct to close clients across multiple namespaces.
type multiClientCloser struct {
closers []jsonrpc.ClientCloser
}

// register adds a new closer to the multiClientCloser
func (m *multiClientCloser) register(closer jsonrpc.ClientCloser) {
m.closers = append(m.closers, closer)
}

// closeAll closes all saved clients.
func (m *multiClientCloser) closeAll() {
for _, closer := range m.closers {
closer()
}
}

// Close closes the connections to all namespaces registered on the client.
func (c *Client) Close() {
c.closer.closeAll()
}

// NewClient creates a new Client with one connection per namespace.
func NewClient(ctx context.Context, addr string) (*Client, error) {
var client Client
var multiCloser multiClientCloser

// TODO: this duplication of strings many times across the codebase can be avoided with issue #1176
var modules = map[string]interface{}{
"share": &client.Share,
"state": &client.State,
"header": &client.Header,
"fraud": &client.Fraud,
"das": &client.DAS,
}
for name, module := range modules {
closer, err := jsonrpc.NewClient(ctx, addr, name, module, nil)
if err != nil {
return nil, err
}
multiCloser.register(closer)
}

return &client, nil
}
145 changes: 145 additions & 0 deletions api/rpc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package api

import (
"context"
"encoding/json"
"reflect"
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"go.uber.org/fx"

"github.com/celestiaorg/celestia-node/api/rpc"
"github.com/celestiaorg/celestia-node/api/rpc/client"
"github.com/celestiaorg/celestia-node/nodebuilder"
dasMock "github.com/celestiaorg/celestia-node/nodebuilder/das/mocks"
fraudMock "github.com/celestiaorg/celestia-node/nodebuilder/fraud/mocks"
headerMock "github.com/celestiaorg/celestia-node/nodebuilder/header/mocks"
"github.com/celestiaorg/celestia-node/nodebuilder/node"
shareMock "github.com/celestiaorg/celestia-node/nodebuilder/share/mocks"
stateMock "github.com/celestiaorg/celestia-node/nodebuilder/state/mocks"
"github.com/celestiaorg/celestia-node/state"
)

func TestRPCCallsUnderlyingNode(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
nd, server := setupNodeWithModifiedRPC(t)
url := nd.RPCServer.ListenAddr()
client, err := client.NewClient(context.Background(), "http://"+url)
t.Cleanup(client.Close)
require.NoError(t, err)

expectedBalance := &state.Balance{
Amount: sdk.NewInt(100),
Denom: "utia",
}

server.State.EXPECT().Balance(gomock.Any()).Return(expectedBalance, nil).Times(1)

balance, err := client.State.Balance(ctx)
require.NoError(t, err)
require.Equal(t, expectedBalance, balance)
}

func TestModulesImplementFullAPI(t *testing.T) {
api := reflect.TypeOf(new(client.API)).Elem()
client := reflect.TypeOf(new(client.Client)).Elem()
for i := 0; i < client.NumField(); i++ {
module := client.Field(i)
for j := 0; j < module.Type.NumField(); j++ {
impl := module.Type.Field(j)
method, _ := api.MethodByName(impl.Name)
// closers is the only thing on the Client struct that doesn't exist in the API
if impl.Name != "closers" {
require.Equal(t, method.Type, impl.Type, "method %s does not match", impl.Name)
}
}
}
}

// TODO(@distractedm1nd): Blocked by issues #1208 and #1207
func TestAllReturnValuesAreMarshalable(t *testing.T) {
t.Skip()
ra := reflect.TypeOf(new(client.API)).Elem()
for i := 0; i < ra.NumMethod(); i++ {
m := ra.Method(i)
for j := 0; j < m.Type.NumOut(); j++ {
implementsMarshaler(t, m.Type.Out(j))
}
}
}

func implementsMarshaler(t *testing.T, typ reflect.Type) { //nolint:unused
switch typ.Kind() {
case reflect.Struct:
for i := 0; i < typ.NumField(); i++ {
implementsMarshaler(t, typ.Field(i).Type)
}
return
case reflect.Map:
implementsMarshaler(t, typ.Elem())
implementsMarshaler(t, typ.Key())
case reflect.Ptr:
fallthrough
case reflect.Array:
fallthrough
case reflect.Slice:
fallthrough
case reflect.Chan:
implementsMarshaler(t, typ.Elem())
case reflect.Interface:
if typ != reflect.TypeOf(new(interface{})).Elem() && typ != reflect.TypeOf(new(error)).Elem() {
require.True(
t,
typ.Implements(reflect.TypeOf(new(json.Marshaler)).Elem()),
"type %s does not implement json.Marshaler", typ.String(),
)
}
default:
return
}

}

func setupNodeWithModifiedRPC(t *testing.T) (*nodebuilder.Node, *mockAPI) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)

ctrl := gomock.NewController(t)

mockAPI := &mockAPI{
stateMock.NewMockModule(ctrl),
shareMock.NewMockModule(ctrl),
fraudMock.NewMockModule(ctrl),
headerMock.NewMockModule(ctrl),
dasMock.NewMockModule(ctrl),
}

overrideRPCHandler := fx.Invoke(func(srv *rpc.Server) {
srv.RegisterService("state", mockAPI.State)
srv.RegisterService("share", mockAPI.Share)
srv.RegisterService("fraud", mockAPI.Fraud)
srv.RegisterService("header", mockAPI.Header)
srv.RegisterService("das", mockAPI.Das)
})
nd := nodebuilder.TestNode(t, node.Full, overrideRPCHandler)
// start node
err := nd.Start(ctx)
require.NoError(t, err)
t.Cleanup(func() {
err = nd.Stop(ctx)
require.NoError(t, err)
})
return nd, mockAPI
}

type mockAPI struct {
State *stateMock.MockModule
Share *shareMock.MockModule
Fraud *fraudMock.MockModule
Header *headerMock.MockModule
Das *dasMock.MockModule
}
1 change: 1 addition & 0 deletions das/daser.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ func (d *DASer) sample(ctx context.Context, h *header.ExtendedHeader) error {
return nil
}

// SamplingStats returns the current statistics over the DA sampling process.
func (d *DASer) SamplingStats(ctx context.Context) (SamplingStats, error) {
return d.sampler.stats(ctx)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ require (
github.com/filecoin-project/go-jsonrpc v0.1.8
github.com/gammazero/workerpool v1.1.3
github.com/gogo/protobuf v1.3.3
github.com/golang/mock v1.6.0
github.com/gorilla/mux v1.8.0
github.com/hashicorp/go-retryablehttp v0.7.1-0.20211018174820-ff6d014e72d9
github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d
Expand Down
51 changes: 51 additions & 0 deletions nodebuilder/das/mocks/api.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions nodebuilder/das/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ func ConstructModule(tp node.Type, cfg *Config) fx.Option {
return das.Stop(ctx)
}),
)),
// Module is needed for the RPC handler
fx.Provide(func(das *das.DASer) Module {
return das
}),
)
case node.Bridge:
return fx.Options()
Expand Down
20 changes: 20 additions & 0 deletions nodebuilder/das/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package das

import (
"context"

"github.com/celestiaorg/celestia-node/das"
)

type Module interface {
// SamplingStats returns the current statistics over the DA sampling process.
SamplingStats(ctx context.Context) (das.SamplingStats, error)
}

// API is a wrapper around Module for the RPC.
// TODO(@distractedm1nd): These structs need to be autogenerated.
//
//go:generate go run github.com/golang/mock/mockgen -destination=mocks/api.go -package=mocks . Module
type API struct {
SamplingStats func(ctx context.Context) (das.SamplingStats, error)
}
Loading

0 comments on commit 9c996a7

Please sign in to comment.