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"}} ] } }`