Skip to content

Commit

Permalink
Implement relayer chain components constructors (#1025)
Browse files Browse the repository at this point in the history
* Implement Contract Writer init in the Relayer

* Implement Contract Reader init in the Relayer

* lint

* lint

* goimports

* Fix Solana codec encoder and decoder lenient codecs init

* lint

---------

Co-authored-by: Jonghyeon Park <[email protected]>
  • Loading branch information
ilija42 and jadepark-dev authored Jan 25, 2025
1 parent a855d4d commit 1325533
Show file tree
Hide file tree
Showing 15 changed files with 223 additions and 77 deletions.
2 changes: 1 addition & 1 deletion integration-tests/relayinterface/chain_components_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ func (h *helper) Init(t *testing.T) {
}

func (h *helper) RPCClient() *chainreader.RPCClientWrapper {
return &chainreader.RPCClientWrapper{Client: h.rpcClient}
return &chainreader.RPCClientWrapper{AccountReader: h.rpcClient}
}

func (h *helper) Context(t *testing.T) context.Context {
Expand Down
7 changes: 7 additions & 0 deletions pkg/solana/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (
"github.com/smartcontractkit/chainlink-common/pkg/utils"
mn "github.com/smartcontractkit/chainlink-framework/multinode"

"github.com/smartcontractkit/chainlink-solana/pkg/solana/fees"

"github.com/smartcontractkit/chainlink-solana/pkg/solana/client"
"github.com/smartcontractkit/chainlink-solana/pkg/solana/config"
"github.com/smartcontractkit/chainlink-solana/pkg/solana/internal"
Expand All @@ -48,6 +50,7 @@ type Chain interface {
Config() config.Config
LogPoller() LogPoller
TxManager() TxManager
FeeEstimator() fees.Estimator
// Reader returns a new Reader from the available list of nodes (if there are multiple, it will randomly select one)
Reader() (client.Reader, error)
}
Expand Down Expand Up @@ -421,6 +424,10 @@ func (c *chain) TxManager() TxManager {
return c.txm
}

func (c *chain) FeeEstimator() fees.Estimator {
return c.txm.FeeEstimator()
}

func (c *chain) Reader() (client.Reader, error) {
return c.getClient()
}
Expand Down
16 changes: 9 additions & 7 deletions pkg/solana/chainreader/client_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@ import (

"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"

"github.com/smartcontractkit/chainlink-solana/pkg/solana/client"
)

// RPCClientWrapper is a wrapper for an RPC client. This was necessary due to the solana RPC interface not
// providing directly mockable components in the GetMultipleAccounts response.
type RPCClientWrapper struct {
*rpc.Client
client.AccountReader
}

// GetMultipleAccountData is a helper function that extracts byte data from a GetMultipleAccounts rpc call.
func (w *RPCClientWrapper) GetMultipleAccountData(ctx context.Context, keys ...solana.PublicKey) ([][]byte, error) {
result, err := w.Client.GetMultipleAccountsWithOpts(ctx, keys, &rpc.GetMultipleAccountsOpts{
result, err := w.GetMultipleAccountsWithOpts(ctx, keys, &rpc.GetMultipleAccountsOpts{
Encoding: solana.EncodingBase64,
Commitment: rpc.CommitmentFinalized,
})
Expand All @@ -25,20 +27,20 @@ func (w *RPCClientWrapper) GetMultipleAccountData(ctx context.Context, keys ...s

bts := make([][]byte, len(result.Value))

for idx, result := range result.Value {
if result == nil {
for idx, res := range result.Value {
if res == nil {
return nil, rpc.ErrNotFound
}

if result.Data == nil {
if res.Data == nil {
return nil, rpc.ErrNotFound
}

if result.Data.GetBinary() == nil {
if res.Data.GetBinary() == nil {
return nil, rpc.ErrNotFound
}

bts[idx] = result.Data.GetBinary()
bts[idx] = res.Data.GetBinary()
}

return bts, nil
Expand Down
11 changes: 11 additions & 0 deletions pkg/solana/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type Reader interface {
// AccountReader is an interface that allows users to pass either the solana rpc client or the relay client
type AccountReader interface {
GetAccountInfoWithOpts(ctx context.Context, addr solana.PublicKey, opts *rpc.GetAccountInfoOpts) (*rpc.GetAccountInfoResult, error)
GetMultipleAccountsWithOpts(ctx context.Context, accounts []solana.PublicKey, opts *rpc.GetMultipleAccountsOpts) (out *rpc.GetMultipleAccountsResult, err error)
}

type Writer interface {
Expand Down Expand Up @@ -182,6 +183,16 @@ func (c *Client) GetAccountInfoWithOpts(ctx context.Context, addr solana.PublicK
return c.rpc.GetAccountInfoWithOpts(ctx, addr, opts)
}

func (c *Client) GetMultipleAccountsWithOpts(ctx context.Context, accounts []solana.PublicKey, opts *rpc.GetMultipleAccountsOpts) (out *rpc.GetMultipleAccountsResult, err error) {
done := c.latency("multiple_account_info")
defer done()

ctx, cancel := context.WithTimeout(ctx, c.contextDuration)
defer cancel()
opts.Commitment = c.commitment // overrides passed in value - use defined client commitment type
return c.rpc.GetMultipleAccountsWithOpts(ctx, accounts, opts)
}

func (c *Client) GetBlocks(ctx context.Context, startSlot uint64, endSlot *uint64) (out rpc.BlocksResult, err error) {
done := c.latency("blocks")
defer done()
Expand Down
60 changes: 60 additions & 0 deletions pkg/solana/client/mocks/reader_writer.go

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

9 changes: 9 additions & 0 deletions pkg/solana/client/multi_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ func (m *MultiClient) GetAccountInfoWithOpts(ctx context.Context, addr solana.Pu
return r.GetAccountInfoWithOpts(ctx, addr, opts)
}

func (m *MultiClient) GetMultipleAccountsWithOpts(ctx context.Context, accounts []solana.PublicKey, opts *rpc.GetMultipleAccountsOpts) (out *rpc.GetMultipleAccountsResult, err error) {
r, err := m.getClient()
if err != nil {
return nil, err
}

return r.GetMultipleAccountsWithOpts(ctx, accounts, opts)
}

func (m *MultiClient) Balance(ctx context.Context, addr solana.PublicKey) (uint64, error) {
r, err := m.getClient()
if err != nil {
Expand Down
36 changes: 24 additions & 12 deletions pkg/solana/codec/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,43 @@ import (
commontypes "github.com/smartcontractkit/chainlink-common/pkg/types"
)

type Decoder struct {
definitions map[string]Entry
lenientFromTypeCodec encodings.LenientCodecFromTypeCodec
// decoder should be initialized with newDecoder
type decoder struct {
definitions map[string]Entry
lenientCodecFromTypeCodec encodings.LenientCodecFromTypeCodec
}

var _ commontypes.Decoder = &Decoder{}
func newDecoder(definitions map[string]Entry) commontypes.Decoder {
lenientCodecFromTypeCodec := make(encodings.LenientCodecFromTypeCodec)
for k, v := range definitions {
lenientCodecFromTypeCodec[k] = v
}

return &decoder{
definitions: definitions,
lenientCodecFromTypeCodec: lenientCodecFromTypeCodec,
}
}

func (d *Decoder) Decode(ctx context.Context, raw []byte, into any, itemType string) (err error) {
func (d *decoder) Decode(ctx context.Context, raw []byte, into any, itemType string) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from: %v, while decoding %q", r, itemType)
}
}()

if d.lenientFromTypeCodec == nil {
d.lenientFromTypeCodec = make(encodings.LenientCodecFromTypeCodec)
for k, v := range d.definitions {
d.lenientFromTypeCodec[k] = v
}
if d.lenientCodecFromTypeCodec == nil {
return fmt.Errorf("decoder is not properly initialised, underlying lenientCodecFromTypeCodec is nil")
}

return d.lenientFromTypeCodec.Decode(ctx, raw, into, itemType)
return d.lenientCodecFromTypeCodec.Decode(ctx, raw, into, itemType)
}

func (d *Decoder) GetMaxDecodingSize(_ context.Context, n int, itemType string) (int, error) {
func (d *decoder) GetMaxDecodingSize(_ context.Context, n int, itemType string) (int, error) {
if d.definitions == nil {
return 0, fmt.Errorf("decoder is not properly initialised, type definitions are nil")
}

codecEntry, ok := d.definitions[itemType]
if !ok {
return 0, fmt.Errorf("%w: nil entry", commontypes.ErrInvalidType)
Expand Down
28 changes: 10 additions & 18 deletions pkg/solana/codec/decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,20 @@ func TestDecoder_Decode_Errors(t *testing.T) {
var into interface{}
someType := "some-type"
t.Run("error when item type not found", func(t *testing.T) {
d := &Decoder{definitions: map[string]Entry{}}
d.definitions[someType] = &entry{}

nonExistentType := "non-existent"
err := d.Decode(tests.Context(t), []byte{}, &into, nonExistentType)
err := newDecoder(map[string]Entry{someType: &entry{}}).
Decode(tests.Context(t), []byte{}, &into, nonExistentType)
require.ErrorIs(t, err, fmt.Errorf("%w: cannot find type %s", commontypes.ErrInvalidType, nonExistentType))
})

t.Run("error when underlying entry decode fails", func(t *testing.T) {
d := &Decoder{definitions: map[string]Entry{}}
d.definitions[someType] = &testErrDecodeEntry{}
require.Error(t, d.Decode(tests.Context(t), []byte{}, &into, someType))
require.Error(t, newDecoder(map[string]Entry{someType: &testErrDecodeEntry{}}).
Decode(tests.Context(t), []byte{}, &into, someType))
})

t.Run("remaining bytes exist after decode is ok", func(t *testing.T) {
d := &Decoder{definitions: map[string]Entry{}}
d.definitions[someType] = &testErrDecodeRemainingBytes{}
require.NoError(t, d.Decode(tests.Context(t), []byte{}, &into, someType))
require.NoError(t, newDecoder(map[string]Entry{someType: &testErrDecodeRemainingBytes{}}).
Decode(tests.Context(t), []byte{}, &into, someType))
})
}

Expand All @@ -72,19 +68,15 @@ func TestDecoder_GetMaxDecodingSize_Errors(t *testing.T) {
someType := "some-type"

t.Run("error when entry for item type is missing", func(t *testing.T) {
d := &Decoder{definitions: map[string]Entry{}}
d.definitions[someType] = &entry{}

nonExistentType := "non-existent"
_, err := d.GetMaxDecodingSize(tests.Context(t), 0, nonExistentType)
_, err := newDecoder(map[string]Entry{someType: &entry{}}).
GetMaxDecodingSize(tests.Context(t), 0, nonExistentType)
require.ErrorIs(t, err, fmt.Errorf("%w: cannot find type %s", commontypes.ErrInvalidType, nonExistentType))
})

t.Run("error when underlying entry decode fails", func(t *testing.T) {
d := &Decoder{definitions: map[string]Entry{}}
d.definitions[someType] = &testErrGetMaxDecodingSize{}

_, err := d.GetMaxDecodingSize(tests.Context(t), 0, someType)
_, err := newDecoder(map[string]Entry{someType: &testErrGetMaxDecodingSize{}}).
GetMaxDecodingSize(tests.Context(t), 0, someType)
require.Error(t, err)
})
}
28 changes: 20 additions & 8 deletions pkg/solana/codec/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,43 @@ import (
commontypes "github.com/smartcontractkit/chainlink-common/pkg/types"
)

type Encoder struct {
// encoder should be initialized with newEncoder
type encoder struct {
definitions map[string]Entry
lenientCodecFromTypeCodec encodings.LenientCodecFromTypeCodec
}

var _ commontypes.Encoder = &Encoder{}
func newEncoder(definitions map[string]Entry) commontypes.Encoder {
lenientCodecFromTypeCodec := make(encodings.LenientCodecFromTypeCodec)
for k, v := range definitions {
lenientCodecFromTypeCodec[k] = v
}

return &encoder{
lenientCodecFromTypeCodec: lenientCodecFromTypeCodec,
definitions: definitions,
}
}

func (e *Encoder) Encode(ctx context.Context, item any, itemType string) (res []byte, err error) {
func (e *encoder) Encode(ctx context.Context, item any, itemType string) (res []byte, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from: %v, while encoding %q", r, itemType)
}
}()

if e.lenientCodecFromTypeCodec == nil {
e.lenientCodecFromTypeCodec = make(encodings.LenientCodecFromTypeCodec)
for k, v := range e.definitions {
e.lenientCodecFromTypeCodec[k] = v
}
return nil, fmt.Errorf("encoder is not properly initialised, underlying lenientCodecFromTypeCodec is nil")
}

return e.lenientCodecFromTypeCodec.Encode(ctx, item, itemType)
}

func (e *Encoder) GetMaxEncodingSize(_ context.Context, n int, itemType string) (int, error) {
func (e *encoder) GetMaxEncodingSize(_ context.Context, n int, itemType string) (int, error) {
if e.definitions == nil {
return 0, fmt.Errorf("encoder is not properly initialised, type definitions are nil")
}

entry, ok := e.definitions[itemType]
if !ok {
return 0, fmt.Errorf("%w: nil entry", commontypes.ErrInvalidType)
Expand Down
Loading

0 comments on commit 1325533

Please sign in to comment.