Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement relayer chain components constructors #1025

Merged
merged 9 commits into from
Jan 25, 2025
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")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does prometheus need to be configured for this to work?

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 initalised, 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 initalised, 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 initalised, 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 initalised, type definitions are nil")
}

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