From 5b3b9fb73825cd2c8b2a69a9274d30fe053fd57e Mon Sep 17 00:00:00 2001 From: Awbrey Hughlett Date: Fri, 15 Mar 2024 17:57:17 -0500 Subject: [PATCH] BCF-3016 Convert Multiple Binding Calls to Parallel (#617) * Convert Multiple Binding Calls to Parallel The binding calls are expected to be run in sequence as they map over the returnVal, but the associated RPC calls can be run in parallel. This commit preloads all data over RPC in parallel before mapping over the returnVal in sequence. * remove unnecessary constant --- .../chainreader/account_read_binding.go | 53 ++++- .../chainreader/account_read_binding_test.go | 159 +++++++++++++ pkg/solana/chainreader/bindings.go | 11 +- pkg/solana/chainreader/bindings_test.go | 4 +- pkg/solana/chainreader/chain_reader.go | 81 ++++++- pkg/solana/chainreader/chain_reader_test.go | 209 +++++++++++++++--- 6 files changed, 473 insertions(+), 44 deletions(-) create mode 100644 pkg/solana/chainreader/account_read_binding_test.go diff --git a/pkg/solana/chainreader/account_read_binding.go b/pkg/solana/chainreader/account_read_binding.go index 6d44d1c6f..107dc851b 100644 --- a/pkg/solana/chainreader/account_read_binding.go +++ b/pkg/solana/chainreader/account_read_binding.go @@ -24,12 +24,61 @@ type accountReadBinding struct { reader BinaryDataReader } +func newAccountReadBinding(acct string, codec types.RemoteCodec, reader BinaryDataReader) *accountReadBinding { + return &accountReadBinding{ + idlAccount: acct, + codec: codec, + reader: reader, + } +} + var _ readBinding = &accountReadBinding{} -func (b *accountReadBinding) GetLatestValue(ctx context.Context, _ any, outVal any) error { +func (b *accountReadBinding) PreLoad(ctx context.Context, result *loadedResult) { + if result == nil { + return + } + bts, err := b.reader.ReadAll(ctx, b.account) if err != nil { - return fmt.Errorf("%w: failed to get binary data", err) + result.err <- fmt.Errorf("%w: failed to get binary data", err) + + return + } + + select { + case <-ctx.Done(): + result.err <- ctx.Err() + default: + result.value <- bts + } +} + +func (b *accountReadBinding) GetLatestValue(ctx context.Context, _ any, outVal any, result *loadedResult) error { + var ( + bts []byte + err error + ) + + if result != nil { + // when preloading, the process will wait for one of three conditions: + // 1. the context ends and returns an error + // 2. bytes were loaded in the bytes channel + // 3. an error was loaded in the err channel + select { + case <-ctx.Done(): + err = ctx.Err() + case bts = <-result.value: + case err = <-result.err: + } + + if err != nil { + return err + } + } else { + if bts, err = b.reader.ReadAll(ctx, b.account); err != nil { + return fmt.Errorf("%w: failed to get binary data", err) + } } return b.codec.Decode(ctx, bts, outVal, b.idlAccount) diff --git a/pkg/solana/chainreader/account_read_binding_test.go b/pkg/solana/chainreader/account_read_binding_test.go new file mode 100644 index 000000000..a5d344f8f --- /dev/null +++ b/pkg/solana/chainreader/account_read_binding_test.go @@ -0,0 +1,159 @@ +package chainreader + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings" + "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary" + "github.com/smartcontractkit/chainlink-common/pkg/types" +) + +func TestPreload(t *testing.T) { + t.Parallel() + + testCodec := makeTestCodec(t) + + t.Run("get latest value waits for preload", func(t *testing.T) { + t.Parallel() + + reader := new(mockReader) + binding := newAccountReadBinding(testCodecKey, testCodec, reader) + + 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) + + ctx := context.Background() + start := time.Now() + loaded := &loadedResult{ + value: make(chan []byte, 1), + err: make(chan error, 1), + } + + binding.PreLoad(ctx, loaded) + + var result testStruct + + err = binding.GetLatestValue(ctx, nil, &result, loaded) + elapsed := time.Since(start) + + require.NoError(t, err) + assert.GreaterOrEqual(t, elapsed, time.Second) + assert.Less(t, elapsed, 1100*time.Millisecond) + assert.Equal(t, expected, result) + }) + + t.Run("cancelled context exits preload and returns error on get latest value", func(t *testing.T) { + t.Parallel() + + reader := new(mockReader) + binding := newAccountReadBinding(testCodecKey, testCodec, reader) + + ctx, cancel := context.WithCancelCause(context.Background()) + + // make the readall pause until after the context is cancelled + reader.On("ReadAll", mock.Anything, mock.Anything). + Return([]byte{}, nil). + After(600 * time.Millisecond) + + expectedErr := errors.New("test error") + go func() { + time.Sleep(500 * time.Millisecond) + cancel(expectedErr) + }() + + loaded := &loadedResult{ + value: make(chan []byte, 1), + err: make(chan error, 1), + } + start := time.Now() + binding.PreLoad(ctx, loaded) + + var result testStruct + err := binding.GetLatestValue(ctx, nil, &result, loaded) + elapsed := time.Since(start) + + assert.ErrorIs(t, err, ctx.Err()) + assert.ErrorIs(t, context.Cause(ctx), expectedErr) + assert.GreaterOrEqual(t, elapsed, 600*time.Millisecond) + assert.Less(t, elapsed, 700*time.Millisecond) + }) + + t.Run("error from preload is returned in get latest value", func(t *testing.T) { + t.Parallel() + + reader := new(mockReader) + binding := newAccountReadBinding(testCodecKey, testCodec, reader) + ctx := context.Background() + expectedErr := errors.New("test error") + + reader.On("ReadAll", mock.Anything, mock.Anything). + Return([]byte{}, expectedErr) + + loaded := &loadedResult{ + value: make(chan []byte, 1), + err: make(chan error, 1), + } + binding.PreLoad(ctx, loaded) + + var result testStruct + err := binding.GetLatestValue(ctx, nil, &result, loaded) + + assert.ErrorIs(t, err, expectedErr) + }) +} + +type mockReader struct { + mock.Mock +} + +func (_m *mockReader) ReadAll(ctx context.Context, pk solana.PublicKey) ([]byte, error) { + ret := _m.Called(ctx, pk) + + var r0 []byte + if val, ok := ret.Get(0).([]byte); ok { + r0 = val + } + + var r1 error + if fn, ok := ret.Get(1).(func() error); ok { + r1 = fn() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type testStruct struct { + A bool + B int64 +} + +const testCodecKey = "TEST" + +func makeTestCodec(t *testing.T) types.RemoteCodec { + t.Helper() + + builder := binary.LittleEndian() + + structCodec, err := encodings.NewStructCodec([]encodings.NamedTypeCodec{ + {Name: "A", Codec: builder.Bool()}, + {Name: "B", Codec: builder.Int64()}, + }) + + require.NoError(t, err) + + return encodings.CodecFromTypeCodec(map[string]encodings.TypeCodec{testCodecKey: structCodec}) +} diff --git a/pkg/solana/chainreader/bindings.go b/pkg/solana/chainreader/bindings.go index 2125684b2..3e96f3be9 100644 --- a/pkg/solana/chainreader/bindings.go +++ b/pkg/solana/chainreader/bindings.go @@ -11,7 +11,8 @@ import ( ) type readBinding interface { - GetLatestValue(ctx context.Context, params, returnVal any) error + PreLoad(context.Context, *loadedResult) + GetLatestValue(ctx context.Context, params, returnVal any, preload *loadedResult) error Bind(types.BoundContract) error CreateType(bool) (any, error) } @@ -77,6 +78,9 @@ func (b namespaceBindings) CreateType(namespace, methodName string, forEncoding } tBinding := reflect.TypeOf(bindingType) + if tBinding.Kind() == reflect.Pointer { + tBinding = tBinding.Elem() + } // all bindings must be structs to allow multiple bindings if tBinding.Kind() != reflect.Struct { @@ -140,3 +144,8 @@ func (b namespaceBindings) Bind(boundContracts []types.BoundContract) error { return nil } + +type loadedResult struct { + value chan []byte + err chan error +} diff --git a/pkg/solana/chainreader/bindings_test.go b/pkg/solana/chainreader/bindings_test.go index 77befa06b..94b3632a5 100644 --- a/pkg/solana/chainreader/bindings_test.go +++ b/pkg/solana/chainreader/bindings_test.go @@ -105,7 +105,9 @@ type mockBinding struct { mock.Mock } -func (_m *mockBinding) GetLatestValue(ctx context.Context, params, returnVal any) error { +func (_m *mockBinding) PreLoad(context.Context, *loadedResult) {} + +func (_m *mockBinding) GetLatestValue(ctx context.Context, params, returnVal any, _ *loadedResult) error { return nil } diff --git a/pkg/solana/chainreader/chain_reader.go b/pkg/solana/chainreader/chain_reader.go index 97be00f50..3ba33508a 100644 --- a/pkg/solana/chainreader/chain_reader.go +++ b/pkg/solana/chainreader/chain_reader.go @@ -3,6 +3,9 @@ package chainreader import ( "context" "encoding/json" + "fmt" + "reflect" + "sync" ag_solana "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" @@ -26,6 +29,7 @@ type SolanaChainReaderService struct { bindings namespaceBindings // service state management + wg sync.WaitGroup services.StateMachine } @@ -67,6 +71,8 @@ func (s *SolanaChainReaderService) Start(_ context.Context) error { // up used resources. Subsequent calls to Close return an error. func (s *SolanaChainReaderService) Close() error { return s.StopOnce(ServiceName, func() error { + s.wg.Wait() + return nil }) } @@ -86,17 +92,78 @@ func (s *SolanaChainReaderService) HealthReport() map[string]error { // GetLatestValue implements the types.ChainReader interface and requests and parses on-chain // data named by the provided contract, method, and params. func (s *SolanaChainReaderService) GetLatestValue(ctx context.Context, contractName, method string, params any, returnVal any) error { + if err := s.Ready(); err != nil { + return err + } + + s.wg.Add(1) + defer s.wg.Done() + bindings, err := s.bindings.GetReadBindings(contractName, method) if err != nil { return err } - for _, binding := range bindings { - if err := binding.GetLatestValue(ctx, params, returnVal); err != nil { + localCtx, localCancel := context.WithCancel(ctx) + + // the wait group ensures GetLatestValue returns only after all go-routines have completed + var wg sync.WaitGroup + + results := make(map[int]*loadedResult) + + if len(bindings) > 1 { + // might go for some guardrails when dealing with multiple bindings + // the returnVal should be compatible with multiple passes by the codec decoder + // this should only apply to types struct{} and map[any]any + tReturnVal := reflect.TypeOf(returnVal) + if tReturnVal.Kind() == reflect.Pointer { + tReturnVal = reflect.Indirect(reflect.ValueOf(returnVal)).Type() + } + + switch tReturnVal.Kind() { + case reflect.Struct, reflect.Map: + default: + localCancel() + + wg.Wait() + + return fmt.Errorf("%w: multiple bindings is only supported for struct and map", types.ErrInvalidType) + } + + // for multiple bindings, preload the remote data in parallel + for idx, binding := range bindings { + results[idx] = &loadedResult{ + value: make(chan []byte, 1), + err: make(chan error, 1), + } + + wg.Add(1) + go func(ctx context.Context, rb readBinding, res *loadedResult) { + defer wg.Done() + + rb.PreLoad(ctx, res) + }(localCtx, binding, results[idx]) + } + } + + // in the case of parallel preloading, GetLatestValue will still run in + // sequence because the function will block until the data is loaded. + // in the case of no preloading, GetLatestValue will load and decode in + // sequence. + for idx, binding := range bindings { + if err := binding.GetLatestValue(ctx, params, returnVal, results[idx]); err != nil { + localCancel() + + wg.Wait() + return err } } + localCancel() + + wg.Wait() + return nil } @@ -136,11 +203,11 @@ func (s *SolanaChainReaderService) init(namespaces map[string]config.ChainReader return err } - s.bindings.AddReadBinding(namespace, methodName, &accountReadBinding{ - idlAccount: procedure.IDLAccount, - codec: codecWithModifiers, - reader: s.client, - }) + s.bindings.AddReadBinding(namespace, methodName, newAccountReadBinding( + procedure.IDLAccount, + codecWithModifiers, + s.client, + )) } } } diff --git a/pkg/solana/chainreader/chain_reader_test.go b/pkg/solana/chainreader/chain_reader_test.go index 11caaaa0e..0787734c2 100644 --- a/pkg/solana/chainreader/chain_reader_test.go +++ b/pkg/solana/chainreader/chain_reader_test.go @@ -7,12 +7,12 @@ import ( "os" "strconv" "strings" + "sync" "testing" "time" ag_solana "github.com/gagliardetto/solana-go" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" codeccommon "github.com/smartcontractkit/chainlink-common/pkg/codec" @@ -89,8 +89,13 @@ func TestSolanaChainReaderService_GetLatestValue(t *testing.T) { require.NoError(t, err) require.NotNil(t, svc) + require.NoError(t, svc.Start(ctx)) - client.On("ReadAll", mock.Anything, mock.Anything).Return(encoded, nil) + t.Cleanup(func() { + require.NoError(t, svc.Close()) + }) + + client.SetNext(encoded, nil, 0) var result modifiedStructWithNestedStruct @@ -112,8 +117,13 @@ func TestSolanaChainReaderService_GetLatestValue(t *testing.T) { require.NoError(t, err) require.NotNil(t, svc) + require.NoError(t, svc.Start(ctx)) - client.On("ReadAll", mock.Anything, mock.Anything).Return(nil, expectedErr) + t.Cleanup(func() { + require.NoError(t, svc.Close()) + }) + + client.SetNext(nil, expectedErr, 0) var result modifiedStructWithNestedStruct @@ -130,6 +140,11 @@ func TestSolanaChainReaderService_GetLatestValue(t *testing.T) { require.NoError(t, err) require.NotNil(t, svc) + require.NoError(t, svc.Start(ctx)) + + t.Cleanup(func() { + require.NoError(t, svc.Close()) + }) var result modifiedStructWithNestedStruct @@ -146,6 +161,11 @@ func TestSolanaChainReaderService_GetLatestValue(t *testing.T) { require.NoError(t, err) require.NotNil(t, svc) + require.NoError(t, svc.Start(ctx)) + + t.Cleanup(func() { + require.NoError(t, svc.Close()) + }) var result modifiedStructWithNestedStruct @@ -162,6 +182,11 @@ func TestSolanaChainReaderService_GetLatestValue(t *testing.T) { require.NoError(t, err) require.NotNil(t, svc) + require.NoError(t, svc.Start(ctx)) + + t.Cleanup(func() { + require.NoError(t, svc.Close()) + }) pk := ag_solana.NewWallet().PublicKey() err = svc.Bind(ctx, []types.BoundContract{ @@ -184,6 +209,11 @@ func TestSolanaChainReaderService_GetLatestValue(t *testing.T) { require.NoError(t, err) require.NotNil(t, svc) + require.NoError(t, svc.Start(ctx)) + + t.Cleanup(func() { + require.NoError(t, svc.Close()) + }) pk := ag_solana.NewWallet().PublicKey() @@ -291,21 +321,74 @@ type modifiedStructWithNestedStruct struct { EnumVal uint8 } -// TODO: BCF-3060 convert mock client to be instance of solana chain. +type mockedRPCCall struct { + bts []byte + err error + delay time.Duration +} + type mockedRPCClient struct { - mock.Mock + mu sync.Mutex + responseByAddress map[string]mockedRPCCall + sequence []mockedRPCCall +} + +func (_m *mockedRPCClient) ReadAll(_ context.Context, pk ag_solana.PublicKey) ([]byte, error) { + _m.mu.Lock() + defer _m.mu.Unlock() + + if _m.responseByAddress == nil { + _m.responseByAddress = make(map[string]mockedRPCCall) + } + + if resp, ok := _m.responseByAddress[pk.String()]; ok { + if resp.delay > 0 { + time.Sleep(resp.delay) + } + + delete(_m.responseByAddress, pk.String()) + + return resp.bts, resp.err + } + + if len(_m.sequence) == 0 { + panic("no values to return") + } + + next := _m.sequence[0] + _m.sequence = _m.sequence[1:len(_m.sequence)] + + if next.delay > 0 { + time.Sleep(next.delay) + } + + return next.bts, next.err } -func (_m *mockedRPCClient) ReadAll(ctx context.Context, pk ag_solana.PublicKey) ([]byte, error) { - ret := _m.Called(ctx, pk) +func (_m *mockedRPCClient) SetNext(bts []byte, err error, delay time.Duration) { + _m.mu.Lock() + defer _m.mu.Unlock() + + _m.sequence = append(_m.sequence, mockedRPCCall{ + bts: bts, + err: err, + delay: delay, + }) +} - var r0 []byte +func (_m *mockedRPCClient) SetForAddress(pk ag_solana.PublicKey, bts []byte, err error, delay time.Duration) { + _m.mu.Lock() + defer _m.mu.Unlock() - if val, ok := ret.Get(0).([]byte); ok { - r0 = val + if _m.responseByAddress == nil { + _m.responseByAddress = make(map[string]mockedRPCCall) } - return r0, ret.Error(1) + _m.responseByAddress[pk.String()] = mockedRPCCall{ + bts: bts, + err: err, + delay: delay, + } } type chainReaderInterfaceTester struct { @@ -326,7 +409,7 @@ func (r *chainReaderInterfaceTester) Name() string { } func (r *chainReaderInterfaceTester) Setup(t *testing.T) { - r.address = make([]string, 6) + r.address = make([]string, 8) for idx := range r.address { r.address[idx] = ag_solana.NewWallet().PublicKey().String() } @@ -336,11 +419,14 @@ func (r *chainReaderInterfaceTester) Setup(t *testing.T) { AnyContractName: { Methods: map[string]config.ChainDataReader{ MethodTakingLatestParamsReturningTestStruct: { - AnchorIDL: fmt.Sprintf(baseIDL, testStructIDL, strings.Join([]string{midLevelStructIDL, innerStructIDL}, ",")), + AnchorIDL: fullStructIDL(t), Encoding: config.EncodingTypeBorsh, Procedures: []config.ChainReaderProcedure{ { - IDLAccount: "TestStruct", + IDLAccount: "TestStructB", + }, + { + IDLAccount: "TestStructA", }, }, }, @@ -381,14 +467,16 @@ func (r *chainReaderInterfaceTester) Setup(t *testing.T) { }, }, MethodReturningSeenStruct: { - AnchorIDL: fmt.Sprintf(baseIDL, testStructIDL, strings.Join([]string{midLevelStructIDL, innerStructIDL}, ",")), + AnchorIDL: fullStructIDL(t), Encoding: config.EncodingTypeBorsh, Procedures: []config.ChainReaderProcedure{ { - IDLAccount: "TestStruct", + IDLAccount: "TestStructB", + }, + { + IDLAccount: "TestStructA", OutputModifications: codeccommon.ModifiersConfig{ &codeccommon.HardCodeModifierConfig{OffChainValues: map[string]any{"ExtraField": AnyExtraValue}}, - // &codeccommon.RenameModifierConfig{Fields: map[string]string{"NestedStruct.Inner.IntVal": "I"}}, }, }, }, @@ -423,6 +511,11 @@ func (r *chainReaderInterfaceTester) GetChainReader(t *testing.T) types.ChainRea t.FailNow() } + require.NoError(t, svc.Start(context.Background())) + t.Cleanup(func() { + require.NoError(t, svc.Close()) + }) + if r.reader == nil { r.reader = &wrappedTestChainReader{ test: t, @@ -445,6 +538,10 @@ type wrappedTestChainReader struct { } func (r *wrappedTestChainReader) GetLatestValue(ctx context.Context, contractName string, method string, params, returnVal any) error { + var ( + a ag_solana.PublicKey + b ag_solana.PublicKey + ) switch contractName + method { case AnyContractName + EventName: // t.Skip won't skip the test here @@ -464,7 +561,7 @@ func (r *wrappedTestChainReader) GetLatestValue(ctx context.Context, contractNam r.test.FailNow() } - r.client.On("ReadAll", mock.Anything, mock.Anything).Return(bts, nil).Once() + r.client.SetNext(bts, nil, 0) case AnyContractName + MethodReturningUint64Slice: cdc := makeTestCodec(r.test, fmt.Sprintf(baseIDL, uint64SliceBaseTypeIDL, ""), config.EncodingTypeBincode) onChainStruct := struct { @@ -478,7 +575,7 @@ func (r *wrappedTestChainReader) GetLatestValue(ctx context.Context, contractNam r.test.FailNow() } - r.client.On("ReadAll", mock.Anything, mock.Anything).Return(bts, nil).Once() + r.client.SetNext(bts, nil, 0) case AnySecondContractName + MethodReturningUint64, AnyContractName + DifferentMethodReturningUint64: cdc := makeTestCodec(r.test, fmt.Sprintf(baseIDL, uint64BaseTypeIDL, ""), config.EncodingTypeBorsh) onChainStruct := struct { @@ -492,32 +589,57 @@ func (r *wrappedTestChainReader) GetLatestValue(ctx context.Context, contractNam r.test.FailNow() } - r.client.On("ReadAll", mock.Anything, mock.Anything).Return(bts, nil).Once() + r.client.SetNext(bts, nil, 0) case AnyContractName + MethodReturningSeenStruct: nextStruct := CreateTestStruct(0, r.tester) r.testStructQueue = append(r.testStructQueue, &nextStruct) + a, b = getAddresses(r.test, r.tester, 5, 6) + fallthrough default: if r.testStructQueue == nil || len(r.testStructQueue) == 0 { r.test.FailNow() } + if contractName+method != AnyContractName+MethodReturningSeenStruct { + a, b = getAddresses(r.test, r.tester, 0, 1) + } + nextTestStruct := r.testStructQueue[0] r.testStructQueue = r.testStructQueue[1:len(r.testStructQueue)] - cdc := makeTestCodec(r.test, fmt.Sprintf(baseIDL, testStructIDL, strings.Join([]string{midLevelStructIDL, innerStructIDL}, ",")), config.EncodingTypeBorsh) - bts, err := cdc.Encode(ctx, nextTestStruct, "TestStruct") + // split into two encoded parts to test the preloading function + cdc := makeTestCodec(r.test, fullStructIDL(r.test), config.EncodingTypeBorsh) + + bts, err := cdc.Encode(ctx, nextTestStruct, "TestStructB") if err != nil { r.test.FailNow() } - r.client.On("ReadAll", mock.Anything, mock.Anything).Return(bts, nil).Once() + // make part A return slower than part B + r.client.SetForAddress(a, bts, nil, 300*time.Millisecond) + + bts, err = cdc.Encode(ctx, nextTestStruct, "TestStructA") + if err != nil { + r.test.FailNow() + } + + r.client.SetForAddress(b, bts, nil, 50*time.Millisecond) } return r.service.GetLatestValue(ctx, contractName, method, params, returnVal) } +func getAddresses(t *testing.T, tester ChainReaderInterfaceTester, a, b int) (ag_solana.PublicKey, ag_solana.PublicKey) { + t.Helper() + + bindings := tester.GetBindings(t) + fn := ag_solana.MustPublicKeyFromBase58 + + return fn(bindings[a].Address), fn(bindings[b].Address) +} + func (r *wrappedTestChainReader) Bind(ctx context.Context, bindings []types.BoundContract) error { return r.service.Bind(ctx, bindings) } @@ -552,11 +674,13 @@ func (r *chainReaderInterfaceTester) TriggerEvent(t *testing.T, testStruct *Test func (r *chainReaderInterfaceTester) GetBindings(t *testing.T) []types.BoundContract { return []types.BoundContract{ {Name: strings.Join([]string{AnyContractName, MethodTakingLatestParamsReturningTestStruct, "0"}, "."), Address: r.address[0], Pending: true}, - {Name: strings.Join([]string{AnyContractName, MethodReturningUint64, "0"}, "."), Address: r.address[1], Pending: true}, - {Name: strings.Join([]string{AnyContractName, DifferentMethodReturningUint64, "0"}, "."), Address: r.address[2], Pending: true}, - {Name: strings.Join([]string{AnyContractName, MethodReturningUint64Slice, "0"}, "."), Address: r.address[3], Pending: true}, - {Name: strings.Join([]string{AnyContractName, MethodReturningSeenStruct, "0"}, "."), Address: r.address[4], Pending: true}, - {Name: strings.Join([]string{AnySecondContractName, MethodReturningUint64, "0"}, "."), Address: r.address[5], Pending: true}, + {Name: strings.Join([]string{AnyContractName, MethodTakingLatestParamsReturningTestStruct, "1"}, "."), Address: r.address[1], Pending: true}, + {Name: strings.Join([]string{AnyContractName, MethodReturningUint64, "0"}, "."), Address: r.address[2], Pending: true}, + {Name: strings.Join([]string{AnyContractName, DifferentMethodReturningUint64, "0"}, "."), Address: r.address[3], Pending: true}, + {Name: strings.Join([]string{AnyContractName, MethodReturningUint64Slice, "0"}, "."), Address: r.address[4], Pending: true}, + {Name: strings.Join([]string{AnyContractName, MethodReturningSeenStruct, "0"}, "."), Address: r.address[5], Pending: true}, + {Name: strings.Join([]string{AnyContractName, MethodReturningSeenStruct, "1"}, "."), Address: r.address[6], Pending: true}, + {Name: strings.Join([]string{AnySecondContractName, MethodReturningUint64, "0"}, "."), Address: r.address[7], Pending: true}, } } @@ -593,6 +717,16 @@ func makeTestCodec(t *testing.T, rawIDL string, encoding config.EncodingType) en return testCodec } +func fullStructIDL(t *testing.T) string { + t.Helper() + + return fmt.Sprintf( + baseIDL, + strings.Join([]string{testStructAIDL, testStructBIDL}, ","), + strings.Join([]string{midLevelStructIDL, innerStructIDL}, ","), + ) +} + const ( baseIDL = `{ "version": "0.1.0", @@ -601,19 +735,28 @@ const ( "types": [%s] }` - testStructIDL = `{ - "name": "TestStruct", + testStructAIDL = `{ + "name": "TestStructA", "type": { "kind": "struct", "fields": [ {"name": "field","type": {"option": "i32"}}, {"name": "differentField","type": "string"}, + {"name": "bigField","type": "i128"}, + {"name": "nestedStruct","type": {"defined": "MidLevelStruct"}} + ] + } + }` + + testStructBIDL = `{ + "name": "TestStructB", + "type": { + "kind": "struct", + "fields": [ {"name": "oracleID","type": "u8"}, {"name": "oracleIDs","type": {"array": ["u8",32]}}, {"name": "account","type": "bytes"}, - {"name": "accounts","type": {"vec": "bytes"}}, - {"name": "bigField","type": "i128"}, - {"name": "nestedStruct","type": {"defined": "MidLevelStruct"}} + {"name": "accounts","type": {"vec": "bytes"}} ] } }`