From e583587d2fb149fcf0c4824a1ac778061ecb11d8 Mon Sep 17 00:00:00 2001 From: Awbrey Hughlett Date: Mon, 18 Mar 2024 15:41:19 -0500 Subject: [PATCH] BCF-3055 Add RPC Opts to Config and Pass to Binding (#627) Solana RPC options include commitment level, data encoding type, and data slice opts. Providing these options in the ChainReader config will allow more control over RPC behavior. --- .../chainreader/account_read_binding.go | 11 +++++--- .../chainreader/account_read_binding_test.go | 15 +++++------ pkg/solana/chainreader/chain_reader.go | 25 +++++++++++++++++-- pkg/solana/chainreader/chain_reader_test.go | 17 ++++++++++++- pkg/solana/config/chain_reader.go | 12 +++++++++ pkg/solana/config/chain_reader_test.go | 17 +++++++++++++ pkg/solana/config/testChainReader_valid.json | 10 +++++++- 7 files changed, 92 insertions(+), 15 deletions(-) diff --git a/pkg/solana/chainreader/account_read_binding.go b/pkg/solana/chainreader/account_read_binding.go index 107dc851b..c1635e3a9 100644 --- a/pkg/solana/chainreader/account_read_binding.go +++ b/pkg/solana/chainreader/account_read_binding.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" "github.com/smartcontractkit/chainlink-common/pkg/types" ) @@ -12,7 +13,7 @@ import ( // BinaryDataReader provides an interface for reading bytes from a source. This is likely a wrapper // for a solana client. type BinaryDataReader interface { - ReadAll(context.Context, solana.PublicKey) ([]byte, error) + ReadAll(context.Context, solana.PublicKey, *rpc.GetAccountInfoOpts) ([]byte, error) } // accountReadBinding provides decoding and reading Solana Account data using a defined codec. The @@ -22,13 +23,15 @@ type accountReadBinding struct { account solana.PublicKey codec types.RemoteCodec reader BinaryDataReader + opts *rpc.GetAccountInfoOpts } -func newAccountReadBinding(acct string, codec types.RemoteCodec, reader BinaryDataReader) *accountReadBinding { +func newAccountReadBinding(acct string, codec types.RemoteCodec, reader BinaryDataReader, opts *rpc.GetAccountInfoOpts) *accountReadBinding { return &accountReadBinding{ idlAccount: acct, codec: codec, reader: reader, + opts: opts, } } @@ -39,7 +42,7 @@ func (b *accountReadBinding) PreLoad(ctx context.Context, result *loadedResult) return } - bts, err := b.reader.ReadAll(ctx, b.account) + bts, err := b.reader.ReadAll(ctx, b.account, b.opts) if err != nil { result.err <- fmt.Errorf("%w: failed to get binary data", err) @@ -76,7 +79,7 @@ func (b *accountReadBinding) GetLatestValue(ctx context.Context, _ any, outVal a return err } } else { - if bts, err = b.reader.ReadAll(ctx, b.account); err != nil { + if bts, err = b.reader.ReadAll(ctx, b.account, b.opts); err != nil { return fmt.Errorf("%w: failed to get binary data", err) } } diff --git a/pkg/solana/chainreader/account_read_binding_test.go b/pkg/solana/chainreader/account_read_binding_test.go index a5d344f8f..3b391e104 100644 --- a/pkg/solana/chainreader/account_read_binding_test.go +++ b/pkg/solana/chainreader/account_read_binding_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -25,14 +26,14 @@ func TestPreload(t *testing.T) { t.Parallel() reader := new(mockReader) - binding := newAccountReadBinding(testCodecKey, testCodec, reader) + binding := newAccountReadBinding(testCodecKey, testCodec, reader, nil) expected := testStruct{A: true, B: 42} bts, err := testCodec.Encode(context.Background(), expected, testCodecKey) require.NoError(t, err) - reader.On("ReadAll", mock.Anything, mock.Anything).Return(bts, nil).After(time.Second) + reader.On("ReadAll", mock.Anything, mock.Anything, mock.Anything).Return(bts, nil).After(time.Second) ctx := context.Background() start := time.Now() @@ -58,12 +59,12 @@ func TestPreload(t *testing.T) { t.Parallel() reader := new(mockReader) - binding := newAccountReadBinding(testCodecKey, testCodec, reader) + binding := newAccountReadBinding(testCodecKey, testCodec, reader, nil) ctx, cancel := context.WithCancelCause(context.Background()) // make the readall pause until after the context is cancelled - reader.On("ReadAll", mock.Anything, mock.Anything). + reader.On("ReadAll", mock.Anything, mock.Anything, mock.Anything). Return([]byte{}, nil). After(600 * time.Millisecond) @@ -94,11 +95,11 @@ func TestPreload(t *testing.T) { t.Parallel() reader := new(mockReader) - binding := newAccountReadBinding(testCodecKey, testCodec, reader) + binding := newAccountReadBinding(testCodecKey, testCodec, reader, nil) ctx := context.Background() expectedErr := errors.New("test error") - reader.On("ReadAll", mock.Anything, mock.Anything). + reader.On("ReadAll", mock.Anything, mock.Anything, mock.Anything). Return([]byte{}, expectedErr) loaded := &loadedResult{ @@ -118,7 +119,7 @@ type mockReader struct { mock.Mock } -func (_m *mockReader) ReadAll(ctx context.Context, pk solana.PublicKey) ([]byte, error) { +func (_m *mockReader) ReadAll(ctx context.Context, pk solana.PublicKey, opts *rpc.GetAccountInfoOpts) ([]byte, error) { ret := _m.Called(ctx, pk) var r0 []byte diff --git a/pkg/solana/chainreader/chain_reader.go b/pkg/solana/chainreader/chain_reader.go index 3ba33508a..b52c72bb5 100644 --- a/pkg/solana/chainreader/chain_reader.go +++ b/pkg/solana/chainreader/chain_reader.go @@ -207,6 +207,7 @@ func (s *SolanaChainReaderService) init(namespaces map[string]config.ChainReader procedure.IDLAccount, codecWithModifiers, s.client, + createRPCOpts(procedure.RPCOpts), )) } } @@ -215,6 +216,26 @@ func (s *SolanaChainReaderService) init(namespaces map[string]config.ChainReader return nil } +func createRPCOpts(opts *config.RPCOpts) *rpc.GetAccountInfoOpts { + if opts == nil { + return nil + } + + result := &rpc.GetAccountInfoOpts{ + DataSlice: opts.DataSlice, + } + + if opts.Encoding != nil { + result.Encoding = *opts.Encoding + } + + if opts.Commitment != nil { + result.Commitment = *opts.Commitment + } + + return result +} + type accountDataReader struct { client *rpc.Client } @@ -223,8 +244,8 @@ func NewAccountDataReader(client *rpc.Client) *accountDataReader { return &accountDataReader{client: client} } -func (r *accountDataReader) ReadAll(ctx context.Context, pk ag_solana.PublicKey) ([]byte, error) { - result, err := r.client.GetAccountInfo(ctx, pk) +func (r *accountDataReader) ReadAll(ctx context.Context, pk ag_solana.PublicKey, opts *rpc.GetAccountInfoOpts) ([]byte, error) { + result, err := r.client.GetAccountInfoWithOpts(ctx, pk, opts) if err != nil { return nil, err } diff --git a/pkg/solana/chainreader/chain_reader_test.go b/pkg/solana/chainreader/chain_reader_test.go index 0787734c2..dd4b4ebc7 100644 --- a/pkg/solana/chainreader/chain_reader_test.go +++ b/pkg/solana/chainreader/chain_reader_test.go @@ -11,7 +11,9 @@ import ( "testing" "time" + "github.com/gagliardetto/solana-go" ag_solana "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -333,7 +335,7 @@ type mockedRPCClient struct { sequence []mockedRPCCall } -func (_m *mockedRPCClient) ReadAll(_ context.Context, pk ag_solana.PublicKey) ([]byte, error) { +func (_m *mockedRPCClient) ReadAll(_ context.Context, pk ag_solana.PublicKey, _ *rpc.GetAccountInfoOpts) ([]byte, error) { _m.mu.Lock() defer _m.mu.Unlock() @@ -414,6 +416,11 @@ func (r *chainReaderInterfaceTester) Setup(t *testing.T) { r.address[idx] = ag_solana.NewWallet().PublicKey().String() } + encodingBase64 := solana.EncodingBase64 + commitment := rpc.CommitmentConfirmed + offset := uint64(1) + length := uint64(1) + r.conf = config.ChainReader{ Namespaces: map[string]config.ChainReaderMethods{ AnyContractName: { @@ -424,6 +431,14 @@ func (r *chainReaderInterfaceTester) Setup(t *testing.T) { Procedures: []config.ChainReaderProcedure{ { IDLAccount: "TestStructB", + RPCOpts: &config.RPCOpts{ + Encoding: &encodingBase64, + Commitment: &commitment, + DataSlice: &rpc.DataSlice{ + Offset: &offset, + Length: &length, + }, + }, }, { IDLAccount: "TestStructA", diff --git a/pkg/solana/config/chain_reader.go b/pkg/solana/config/chain_reader.go index f770c0939..a1fed147d 100644 --- a/pkg/solana/config/chain_reader.go +++ b/pkg/solana/config/chain_reader.go @@ -4,6 +4,9 @@ import ( "encoding/json" "fmt" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings" "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary" @@ -66,6 +69,12 @@ func (t *EncodingType) UnmarshalJSON(data []byte) error { return nil } +type RPCOpts struct { + Encoding *solana.EncodingType `json:"encoding,omitempty"` + Commitment *rpc.CommitmentType `json:"commitment,omitempty"` + DataSlice *rpc.DataSlice `json:"dataSlice,omitempty"` +} + type ChainReaderProcedure chainDataProcedureFields type chainDataProcedureFields struct { @@ -74,6 +83,9 @@ type chainDataProcedureFields struct { // OutputModifications provides modifiers to convert chain data format to custom // output formats. OutputModifications codec.ModifiersConfig `json:"outputModifications,omitempty"` + // RPCOpts provides optional configurations for commitment, encoding, and data + // slice offsets. + RPCOpts *RPCOpts `json:"rpcOpts,omitempty"` } // BuilderForEncoding returns a builder for the encoding configuration. Defaults to little endian. diff --git a/pkg/solana/config/chain_reader_test.go b/pkg/solana/config/chain_reader_test.go index 26ac5ef91..b0ad49181 100644 --- a/pkg/solana/config/chain_reader_test.go +++ b/pkg/solana/config/chain_reader_test.go @@ -5,6 +5,8 @@ import ( "encoding/json" "testing" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -74,6 +76,13 @@ func TestBuilderForEncoding_Default(t *testing.T) { require.Equal(t, binary.LittleEndian(), builder) } +var ( + encodingBase64 = solana.EncodingBase64 + commitment = rpc.CommitmentFinalized + offset = uint64(10) + length = uint64(10) +) + var validChainReaderConfig = config.ChainReader{ Namespaces: map[string]config.ChainReaderMethods{ "Contract": { @@ -96,6 +105,14 @@ var validChainReaderConfig = config.ChainReader{ OutputModifications: codeccommon.ModifiersConfig{ &codeccommon.PropertyExtractorConfig{FieldName: "DurationVal"}, }, + RPCOpts: &config.RPCOpts{ + Encoding: &encodingBase64, + Commitment: &commitment, + DataSlice: &rpc.DataSlice{ + Offset: &offset, + Length: &length, + }, + }, }, }, }, diff --git a/pkg/solana/config/testChainReader_valid.json b/pkg/solana/config/testChainReader_valid.json index d2649739d..6dfbe0626 100644 --- a/pkg/solana/config/testChainReader_valid.json +++ b/pkg/solana/config/testChainReader_valid.json @@ -17,7 +17,15 @@ "outputModifications": [{ "Type": "extract property", "FieldName": "DurationVal" - }] + }], + "rpcOpts": { + "encoding": "base64", + "commitment": "finalized", + "dataSlice": { + "offset": 10, + "length": 10 + } + } }] } }