diff --git a/Dockerfile_milestone1 b/Dockerfile_milestone1 deleted file mode 100644 index 0456fb148..000000000 --- a/Dockerfile_milestone1 +++ /dev/null @@ -1,7 +0,0 @@ -FROM golang:1.18 - -COPY . /go-substrate-rpc-client/events-parsing-v2/milestone-1 - -WORKDIR /go-substrate-rpc-client/events-parsing-v2/milestone-1 - -CMD go test -v ./registry/... --cover \ No newline at end of file diff --git a/Dockerfile_milestone2 b/Dockerfile_milestone2 new file mode 100644 index 000000000..0aa7f9452 --- /dev/null +++ b/Dockerfile_milestone2 @@ -0,0 +1,7 @@ +FROM golang:1.18 + +COPY . /go-substrate-rpc-client/events-parsing-v2/milestone-2 + +WORKDIR /go-substrate-rpc-client/events-parsing-v2/milestone-2 + +CMD go test -v ./registry/... --cover \ No newline at end of file diff --git a/Dockerfile_milestone2_live b/Dockerfile_milestone2_live new file mode 100644 index 000000000..0eb32a2f1 --- /dev/null +++ b/Dockerfile_milestone2_live @@ -0,0 +1,7 @@ +FROM golang:1.18 + +COPY . /go-substrate-rpc-client/events-parsing-v2/milestone-2 + +WORKDIR /go-substrate-rpc-client/events-parsing-v2/milestone-2 + +CMD go test -v -tags=live ./registry/retriever/... \ No newline at end of file diff --git a/Makefile b/Makefile index 6a92b295e..55d54c98e 100644 --- a/Makefile +++ b/Makefile @@ -46,9 +46,13 @@ test-types-decode: ## run tests for types decode generate-mocks: ## generate mocks @docker run -v `pwd`:/app -w /app --entrypoint /bin/sh vektra/mockery:v2.13.0-beta.1 -c 'go generate ./...' -test-milestone1: - @docker build -t gsrpc-m1 -f Dockerfile_milestone1 . - @docker run --rm gsrpc-m1 +test-milestone2: + @docker build -t gsrpc-m2 -f Dockerfile_milestone2 . + @docker run --rm gsrpc-m2 --name gsrpc-m2 + +test-milestone2-live: + @docker build -t gsrpc-m2-live -f Dockerfile_milestone2_live . + @docker run --rm gsrpc-m2-live --name gsrpc-m2-live help: ## shows this help @sed -ne '/@sed/!s/## //p' $(MAKEFILE_LIST) diff --git a/error/error.go b/error/error.go new file mode 100644 index 000000000..0ea4966cd --- /dev/null +++ b/error/error.go @@ -0,0 +1,26 @@ +package error + +import ( + "fmt" + "strings" +) + +type Error string + +func (e Error) Error() string { + return string(e) +} + +func (e Error) Is(err error) bool { + return strings.Contains(string(e), err.Error()) +} + +func (e Error) Wrap(err error) Error { + return Error(fmt.Errorf("%s: %w", e, err).Error()) +} + +func (e Error) WithMsg(msgFormat string, formatArgs ...any) Error { + msg := fmt.Sprintf(msgFormat, formatArgs...) + + return Error(fmt.Sprintf("%s: %s", e, msg)) +} diff --git a/error/error_test.go b/error/error_test.go new file mode 100644 index 000000000..580730757 --- /dev/null +++ b/error/error_test.go @@ -0,0 +1,36 @@ +package error + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +const ( + testErr = Error("test error") +) + +func TestError(t *testing.T) { + newStdErr := errors.New("new std error") + wrappedErr := testErr.Wrap(newStdErr) + + assert.True(t, errors.Is(wrappedErr, testErr)) + assert.True(t, errors.Is(wrappedErr, newStdErr)) + assert.Equal(t, fmt.Sprintf("%s: %s", testErr.Error(), newStdErr.Error()), wrappedErr.Error()) + + newErr := Error("new error") + newWrappedErr := newErr.Wrap(wrappedErr) + + assert.True(t, errors.Is(newWrappedErr, newErr)) + assert.True(t, errors.Is(newWrappedErr, testErr)) + assert.True(t, errors.Is(newWrappedErr, newStdErr)) + assert.Equal(t, fmt.Sprintf("%s: %s", newErr.Error(), wrappedErr.Error()), newWrappedErr.Error()) + + err := testErr.WithMsg("%d", 1) + assert.Equal(t, fmt.Sprintf("%s: 1", testErr), err.Error()) + + err = testErr.WithMsg("test msg") + assert.Equal(t, fmt.Sprintf("%s: test msg", testErr), err.Error()) +} diff --git a/registry/error.go b/registry/error.go new file mode 100644 index 000000000..b07d7406d --- /dev/null +++ b/registry/error.go @@ -0,0 +1,51 @@ +package registry + +import libErr "github.com/centrifuge/go-substrate-rpc-client/v4/error" + +const ( + ErrRecursiveDecodersResolving = libErr.Error("recursive decoders resolving") + ErrErrorsTypeNotFound = libErr.Error("errors type not found") + ErrErrorsTypeNotVariant = libErr.Error("errors type not a variant") + ErrErrorFieldsRetrieval = libErr.Error("error fields retrieval") + ErrCallsTypeNotFound = libErr.Error("calls type not found") + ErrCallsTypeNotVariant = libErr.Error("calls type not a variant") + ErrCallFieldsRetrieval = libErr.Error("call fields retrieval") + ErrEventsTypeNotFound = libErr.Error("events type not found") + ErrEventsTypeNotVariant = libErr.Error("events type not a variant") + ErrEventFieldsRetrieval = libErr.Error("event fields retrieval") + ErrFieldDecoderForRecursiveFieldNotFound = libErr.Error("field decoder for recursive field not found") + ErrRecursiveFieldResolving = libErr.Error("recursive field resolving") + ErrFieldTypeNotFound = libErr.Error("field type not found") + ErrFieldDecoderRetrieval = libErr.Error("field decoder retrieval") + ErrCompactFieldTypeNotFound = libErr.Error("compact field type not found") + ErrCompositeTypeFieldsRetrieval = libErr.Error("composite type fields retrieval") + ErrArrayFieldTypeNotFound = libErr.Error("array field type not found") + ErrVectorFieldTypeNotFound = libErr.Error("vector field type not found") + ErrFieldTypeDefinitionNotSupported = libErr.Error("field type definition not supported") + ErrVariantTypeFieldsRetrieval = libErr.Error("variant type fields decoding") + ErrCompactTupleItemTypeNotFound = libErr.Error("compact tuple item type not found") + ErrCompactTupleItemFieldDecoderRetrieval = libErr.Error("compact tuple item field decoder retrieval") + ErrCompactCompositeFieldTypeNotFound = libErr.Error("compact composite field type not found") + ErrCompactCompositeFieldDecoderRetrieval = libErr.Error("compact composite field decoder retrieval") + ErrArrayItemFieldDecoderRetrieval = libErr.Error("array item field decoder retrieval") + ErrSliceItemFieldDecoderRetrieval = libErr.Error("slice item field decoder retrieval") + ErrTupleItemTypeNotFound = libErr.Error("tuple item type not found") + ErrTupleItemFieldDecoderRetrieval = libErr.Error("tuple item field decoder retrieval") + ErrBitStoreTypeNotFound = libErr.Error("bit store type not found") + ErrBitStoreTypeNotSupported = libErr.Error("bit store type not supported") + ErrBitOrderTypeNotFound = libErr.Error("bit order type not found") + ErrBitOrderCreation = libErr.Error("bit order creation") + ErrPrimitiveTypeNotSupported = libErr.Error("primitive type not supported") + ErrTypeFieldDecoding = libErr.Error("type field decoding") + ErrVariantByteDecoding = libErr.Error("variant byte decoding") + ErrVariantFieldDecoderNotFound = libErr.Error("variant field decoder not found") + ErrArrayItemDecoderNotFound = libErr.Error("array item decoder not found") + ErrArrayItemDecoding = libErr.Error("array item decoding") + ErrSliceItemDecoderNotFound = libErr.Error("slice item decoder not found") + ErrSliceLengthDecoding = libErr.Error("slice length decoding") + ErrSliceItemDecoding = libErr.Error("slice item decoding") + ErrCompositeFieldDecoding = libErr.Error("composite field decoding") + ErrValueDecoding = libErr.Error("value decoding") + ErrRecursiveFieldDecoderNotFound = libErr.Error("recursive field decoder not found") + ErrBitVecDecoding = libErr.Error("bit vec decoding") +) diff --git a/registry/exec/exec.go b/registry/exec/exec.go new file mode 100644 index 000000000..79e08f8c8 --- /dev/null +++ b/registry/exec/exec.go @@ -0,0 +1,161 @@ +package exec + +import ( + "errors" + "fmt" + "strings" + "time" +) + +//go:generate mockery --name RetryableExecutor --structname RetryableExecutorMock --filename exec_mock.go --inpackage + +// RetryableExecutor is the interface used for executing a closure and its fallback if the initial execution fails. +// +// The interface is generic over type T which represents the return value of the closure. +type RetryableExecutor[T any] interface { + ExecWithFallback(execFn func() (T, error), fallbackFn func() error) (T, error) +} + +// retryableExecutor implements RetryableExecutor. +// +// It can be configured via the provided OptsFn(s). +type retryableExecutor[T any] struct { + opts *Opts +} + +// NewRetryableExecutor creates a new RetryableExecutor. +func NewRetryableExecutor[T any](opts ...OptsFn) RetryableExecutor[T] { + execOpts := NewDefaultExecOpts() + + for _, opt := range opts { + opt(execOpts) + } + + return &retryableExecutor[T]{ + execOpts, + } +} + +// ExecWithFallback will attempt to execute the provided execFn and, in the case of failure, it will execute +// the fallbackFn and retry execution of execFn. +func (r *retryableExecutor[T]) ExecWithFallback(execFn func() (T, error), fallbackFn func() error) (res T, err error) { + if execFn == nil { + return res, ErrMissingExecFn + } + + if fallbackFn == nil { + return res, ErrMissingFallbackFn + } + + execErr := &Error{} + + retryCount := uint(0) + + for { + res, err = execFn() + + if err == nil { + return res, nil + } + + execErr.AddErr(fmt.Errorf("exec function error: %w", err)) + + if retryCount == r.opts.maxRetryCount { + return res, execErr + } + + if err = fallbackFn(); err != nil && !r.opts.retryOnFallbackError { + execErr.AddErr(fmt.Errorf("fallback function error: %w", err)) + + return res, execErr + } + + retryCount++ + + time.Sleep(r.opts.retryTimeout) + } +} + +var ( + ErrMissingExecFn = errors.New("no exec function provided") + ErrMissingFallbackFn = errors.New("no fallback function provided") +) + +const ( + defaultMaxRetryCount = 3 + defaultErrTimeout = 0 * time.Second + defaultRetryOnFallbackError = true +) + +// Opts holds the configurable options for a RetryableExecutor. +type Opts struct { + // maxRetryCount holds maximum number of retries in the case of failure. + maxRetryCount uint + + // retryTimeout holds the timeout between retries. + retryTimeout time.Duration + + // retryOnFallbackError specifies whether a retry will be done in the case of + // failure of the fallback function. + retryOnFallbackError bool +} + +// NewDefaultExecOpts creates the default Opts. +func NewDefaultExecOpts() *Opts { + return &Opts{ + maxRetryCount: defaultMaxRetryCount, + retryTimeout: defaultErrTimeout, + retryOnFallbackError: defaultRetryOnFallbackError, + } +} + +// OptsFn is function that operate on Opts. +type OptsFn func(opts *Opts) + +// WithMaxRetryCount sets the max retry count. +// +// Note that a default value is provided if the provided count is 0. +func WithMaxRetryCount(maxRetryCount uint) OptsFn { + return func(opts *Opts) { + if maxRetryCount == 0 { + maxRetryCount = defaultMaxRetryCount + } + + opts.maxRetryCount = maxRetryCount + } +} + +// WithRetryTimeout sets the retry timeout. +func WithRetryTimeout(retryTimeout time.Duration) OptsFn { + return func(opts *Opts) { + opts.retryTimeout = retryTimeout + } +} + +// WithRetryOnFallBackError sets the retryOnFallbackError flag. +func WithRetryOnFallBackError(retryOnFallbackError bool) OptsFn { + return func(opts *Opts) { + opts.retryOnFallbackError = retryOnFallbackError + } +} + +// Error holds none or multiple errors that can happen during execution. +type Error struct { + errs []error +} + +// AddErr appends an error to the error slice of Error. +func (e *Error) AddErr(err error) { + e.errs = append(e.errs, err) +} + +// Error implements the standard error interface. +func (e *Error) Error() string { + sb := strings.Builder{} + + for i, err := range e.errs { + sb.WriteString(fmt.Sprintf("error %d: %s\n", i, err)) + } + + return sb.String() +} diff --git a/registry/exec/exec_mock.go b/registry/exec/exec_mock.go new file mode 100644 index 000000000..54c989efc --- /dev/null +++ b/registry/exec/exec_mock.go @@ -0,0 +1,46 @@ +// Code generated by mockery v2.13.0-beta.1. DO NOT EDIT. + +package exec + +import mock "github.com/stretchr/testify/mock" + +// RetryableExecutorMock is an autogenerated mock type for the RetryableExecutor type +type RetryableExecutorMock[T interface{}] struct { + mock.Mock +} + +// ExecWithFallback provides a mock function with given fields: execFn, fallbackFn +func (_m *RetryableExecutorMock[T]) ExecWithFallback(execFn func() (T, error), fallbackFn func() error) (T, error) { + ret := _m.Called(execFn, fallbackFn) + + var r0 T + if rf, ok := ret.Get(0).(func(func() (T, error), func() error) T); ok { + r0 = rf(execFn, fallbackFn) + } else { + r0 = ret.Get(0).(T) + } + + var r1 error + if rf, ok := ret.Get(1).(func(func() (T, error), func() error) error); ok { + r1 = rf(execFn, fallbackFn) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type NewRetryableExecutorMockT interface { + mock.TestingT + Cleanup(func()) +} + +// NewRetryableExecutorMock creates a new instance of RetryableExecutorMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewRetryableExecutorMock[T interface{}](t NewRetryableExecutorMockT) *RetryableExecutorMock[T] { + mock := &RetryableExecutorMock[T]{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/registry/exec/exec_test.go b/registry/exec/exec_test.go new file mode 100644 index 000000000..537cbe362 --- /dev/null +++ b/registry/exec/exec_test.go @@ -0,0 +1,153 @@ +package exec + +import ( + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestRetryableExecutor_ExecWithFallback(t *testing.T) { + exec := NewRetryableExecutor[int]() + + execFnCallCount := 0 + fallbackFnCallCount := 0 + + execFnRes := 11 + + res, err := exec.ExecWithFallback(func() (int, error) { + execFnCallCount++ + + return execFnRes, nil + }, func() error { + fallbackFnCallCount++ + + return nil + }) + + assert.Nil(t, err) + assert.Equal(t, execFnRes, res) + assert.Equal(t, 1, execFnCallCount) + assert.Equal(t, 0, fallbackFnCallCount) +} + +func TestRetryableExecutor_ExecWithFallback_RetrySuccess(t *testing.T) { + exec := NewRetryableExecutor[int]() + + execFnCallCount := 0 + fallbackFnCallCount := 0 + + execFnRes := 11 + + res, err := exec.ExecWithFallback(func() (int, error) { + execFnCallCount++ + + if execFnCallCount < 2 { + return 0, errors.New("boom") + } + + return execFnRes, nil + }, func() error { + fallbackFnCallCount++ + + return nil + }) + + assert.Nil(t, err) + assert.Equal(t, execFnRes, res) + assert.Equal(t, 2, execFnCallCount) + assert.Equal(t, 1, fallbackFnCallCount) +} + +func TestRetryableExecutor_ExecWithFallback_NilFns(t *testing.T) { + exec := NewRetryableExecutor[int]() + + res, err := exec.ExecWithFallback(nil, nil) + assert.ErrorIs(t, err, ErrMissingExecFn) + assert.Equal(t, 0, res) + + res, err = exec.ExecWithFallback(func() (int, error) { + return 1, nil + }, nil) + assert.ErrorIs(t, err, ErrMissingFallbackFn) + assert.Equal(t, 0, res) + +} + +func TestRetryableExecutor_ExecWithFallback_ExecFnError(t *testing.T) { + retryCount := uint(5) + + exec := NewRetryableExecutor[int]( + WithMaxRetryCount(retryCount), + WithRetryTimeout(100*time.Millisecond), + ) + + execFnCallCount := uint(0) + fallbackFnCallCount := uint(0) + + res, err := exec.ExecWithFallback(func() (int, error) { + execFnCallCount++ + + return 0, errors.New("boom") + }, func() error { + fallbackFnCallCount++ + + return nil + }) + assert.NotNil(t, err) + assert.Equal(t, 0, res) + assert.Equal(t, retryCount+1, execFnCallCount) + assert.Equal(t, retryCount, fallbackFnCallCount) + + execErr := err.(*Error) + assert.Len(t, execErr.errs, int(retryCount+1)) +} + +func TestRetryableExecutor_ExecWithFallback_FallBackFnError(t *testing.T) { + exec := NewRetryableExecutor[int]() + + execFnCallCount := 0 + fallbackFnCallCount := 0 + + res, err := exec.ExecWithFallback(func() (int, error) { + execFnCallCount++ + + return 0, errors.New("boom") + }, func() error { + fallbackFnCallCount++ + + return errors.New("boom") + }) + assert.NotNil(t, err) + assert.Equal(t, 0, res) + assert.Equal(t, defaultMaxRetryCount+1, execFnCallCount) + assert.Equal(t, defaultMaxRetryCount, fallbackFnCallCount) + + execErr := err.(*Error) + assert.Len(t, execErr.errs, defaultMaxRetryCount+1) +} + +func TestRetryableExecutor_ExecWithFallback_FallBackFnError_NoRetry(t *testing.T) { + exec := NewRetryableExecutor[int](WithRetryOnFallBackError(false)) + + execFnCallCount := 0 + fallbackFnCallCount := 0 + + res, err := exec.ExecWithFallback(func() (int, error) { + execFnCallCount++ + + return 0, errors.New("boom") + }, func() error { + fallbackFnCallCount++ + + return errors.New("boom") + }) + assert.NotNil(t, err) + assert.Equal(t, 0, res) + assert.Equal(t, 1, execFnCallCount) + assert.Equal(t, 1, fallbackFnCallCount) + + execErr := err.(*Error) + assert.Len(t, execErr.errs, 2) +} diff --git a/registry/factory_mock.go b/registry/factory_mock.go new file mode 100644 index 000000000..2c8b918ba --- /dev/null +++ b/registry/factory_mock.go @@ -0,0 +1,97 @@ +// Code generated by mockery v2.13.0-beta.1. DO NOT EDIT. + +package registry + +import ( + types "github.com/centrifuge/go-substrate-rpc-client/v4/types" + mock "github.com/stretchr/testify/mock" +) + +// FactoryMock is an autogenerated mock type for the Factory type +type FactoryMock struct { + mock.Mock +} + +// CreateCallRegistry provides a mock function with given fields: meta +func (_m *FactoryMock) CreateCallRegistry(meta *types.Metadata) (CallRegistry, error) { + ret := _m.Called(meta) + + var r0 CallRegistry + if rf, ok := ret.Get(0).(func(*types.Metadata) CallRegistry); ok { + r0 = rf(meta) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(CallRegistry) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*types.Metadata) error); ok { + r1 = rf(meta) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CreateErrorRegistry provides a mock function with given fields: meta +func (_m *FactoryMock) CreateErrorRegistry(meta *types.Metadata) (ErrorRegistry, error) { + ret := _m.Called(meta) + + var r0 ErrorRegistry + if rf, ok := ret.Get(0).(func(*types.Metadata) ErrorRegistry); ok { + r0 = rf(meta) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(ErrorRegistry) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*types.Metadata) error); ok { + r1 = rf(meta) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CreateEventRegistry provides a mock function with given fields: meta +func (_m *FactoryMock) CreateEventRegistry(meta *types.Metadata) (EventRegistry, error) { + ret := _m.Called(meta) + + var r0 EventRegistry + if rf, ok := ret.Get(0).(func(*types.Metadata) EventRegistry); ok { + r0 = rf(meta) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(EventRegistry) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*types.Metadata) error); ok { + r1 = rf(meta) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type NewFactoryMockT interface { + mock.TestingT + Cleanup(func()) +} + +// NewFactoryMock creates a new instance of FactoryMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewFactoryMock(t NewFactoryMockT) *FactoryMock { + mock := &FactoryMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/registry/parser/error.go b/registry/parser/error.go new file mode 100644 index 000000000..67f66b12e --- /dev/null +++ b/registry/parser/error.go @@ -0,0 +1,14 @@ +package parser + +import libErr "github.com/centrifuge/go-substrate-rpc-client/v4/error" + +const ( + ErrEventsCountDecoding = libErr.Error("events count decoding") + ErrEventPhaseDecoding = libErr.Error("event phase decoding") + ErrEventIDDecoding = libErr.Error("event ID decoding") + ErrEventDecoderNotFound = libErr.Error("event decoder not found") + ErrEventFieldsDecoding = libErr.Error("event fields decoding") + ErrEventTopicsDecoding = libErr.Error("event topics decoding") + ErrCallDecoderNotFound = libErr.Error("call decoder not found") + ErrCallFieldsDecoding = libErr.Error("call fields decoding") +) diff --git a/registry/parser/event_parser.go b/registry/parser/event_parser.go new file mode 100644 index 000000000..5e34fee81 --- /dev/null +++ b/registry/parser/event_parser.go @@ -0,0 +1,95 @@ +package parser + +import ( + "bytes" + "fmt" + + "github.com/centrifuge/go-substrate-rpc-client/v4/registry" + "github.com/centrifuge/go-substrate-rpc-client/v4/scale" + "github.com/centrifuge/go-substrate-rpc-client/v4/types" +) + +// Event holds all the information of a decoded storage event. +type Event struct { + Name string + Fields map[string]any + EventID types.EventID + Phase *types.Phase + Topics []types.Hash +} + +//go:generate mockery --name EventParser --structname EventParserMock --filename event_parser_mock.go --inpackage + +// EventParser is the interface used for parsing event storage data into []*Event. +type EventParser interface { + ParseEvents(eventRegistry registry.EventRegistry, sd *types.StorageDataRaw) ([]*Event, error) +} + +// EventParserFn implements EventParser. +type EventParserFn func(eventRegistry registry.EventRegistry, sd *types.StorageDataRaw) ([]*Event, error) + +// ParseEvents is the function required for satisfying the EventParser interface. +func (f EventParserFn) ParseEvents(eventRegistry registry.EventRegistry, sd *types.StorageDataRaw) ([]*Event, error) { + return f(eventRegistry, sd) +} + +// NewEventParser creates a new EventParser. +func NewEventParser() EventParser { + // The EventParserFn provided here is decoding the total number of events from the storage data then attempts + // to decode all the information for each event. + return EventParserFn(func(eventRegistry registry.EventRegistry, sd *types.StorageDataRaw) ([]*Event, error) { + decoder := scale.NewDecoder(bytes.NewReader(*sd)) + + eventsCount, err := decoder.DecodeUintCompact() + + if err != nil { + return nil, ErrEventsCountDecoding.Wrap(err) + } + + var events []*Event + + for i := uint64(0); i < eventsCount.Uint64(); i++ { + var phase types.Phase + + if err := decoder.Decode(&phase); err != nil { + return nil, ErrEventPhaseDecoding.Wrap(fmt.Errorf("event #%d: %w", i, err)) + } + + var eventID types.EventID + + if err := decoder.Decode(&eventID); err != nil { + return nil, ErrEventIDDecoding.Wrap(fmt.Errorf("event #%d: %w", i, err)) + } + + eventDecoder, ok := eventRegistry[eventID] + + if !ok { + return nil, ErrEventDecoderNotFound.WithMsg("event #%d with ID: %v", i, eventID) + } + + eventFields, err := eventDecoder.Decode(decoder) + + if err != nil { + return nil, ErrEventFieldsDecoding.Wrap(fmt.Errorf("event #%d: %w", i, err)) + } + + var topics []types.Hash + + if err := decoder.Decode(&topics); err != nil { + return nil, ErrEventTopicsDecoding.Wrap(fmt.Errorf("event #%d: %w", i, err)) + } + + event := &Event{ + Name: eventDecoder.Name, + Fields: eventFields, + EventID: eventID, + Phase: &phase, + Topics: topics, + } + + events = append(events, event) + } + + return events, nil + }) +} diff --git a/registry/parser/event_parser_mock.go b/registry/parser/event_parser_mock.go new file mode 100644 index 000000000..75e6395e5 --- /dev/null +++ b/registry/parser/event_parser_mock.go @@ -0,0 +1,52 @@ +// Code generated by mockery v2.13.0-beta.1. DO NOT EDIT. + +package parser + +import ( + registry "github.com/centrifuge/go-substrate-rpc-client/v4/registry" + types "github.com/centrifuge/go-substrate-rpc-client/v4/types" + mock "github.com/stretchr/testify/mock" +) + +// EventParserMock is an autogenerated mock type for the EventParser type +type EventParserMock struct { + mock.Mock +} + +// ParseEvents provides a mock function with given fields: eventRegistry, sd +func (_m *EventParserMock) ParseEvents(eventRegistry registry.EventRegistry, sd *types.StorageDataRaw) ([]*Event, error) { + ret := _m.Called(eventRegistry, sd) + + var r0 []*Event + if rf, ok := ret.Get(0).(func(registry.EventRegistry, *types.StorageDataRaw) []*Event); ok { + r0 = rf(eventRegistry, sd) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*Event) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(registry.EventRegistry, *types.StorageDataRaw) error); ok { + r1 = rf(eventRegistry, sd) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type NewEventParserMockT interface { + mock.TestingT + Cleanup(func()) +} + +// NewEventParserMock creates a new instance of EventParserMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewEventParserMock(t NewEventParserMockT) *EventParserMock { + mock := &EventParserMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/registry/parser/event_parser_test.go b/registry/parser/event_parser_test.go new file mode 100644 index 000000000..72365dccf --- /dev/null +++ b/registry/parser/event_parser_test.go @@ -0,0 +1,580 @@ +package parser + +import ( + "bytes" + "fmt" + "math/big" + "testing" + + "github.com/centrifuge/go-substrate-rpc-client/v4/registry" + "github.com/centrifuge/go-substrate-rpc-client/v4/scale" + "github.com/centrifuge/go-substrate-rpc-client/v4/types" + "github.com/stretchr/testify/assert" +) + +func TestEventParserFn_ParseEvents(t *testing.T) { + testEvents := []testEvent{ + { + Name: "test_event_1", + Phase: &types.Phase{ + IsApplyExtrinsic: true, + AsApplyExtrinsic: 1, + }, + EventID: types.EventID([2]byte{0, 1}), + EventFields: []testField{ + { + Name: "bool_value", + Value: true, + }, + { + Name: "byte_value", + Value: byte(65), + }, + { + Name: "string_value", + Value: "test", + }, + }, + Topics: []types.Hash{ + types.NewHash([]byte{0, 1, 2}), + }, + }, + { + Name: "test_event_2", + Phase: &types.Phase{ + IsFinalization: true, + }, + EventID: types.EventID([2]byte{1, 0}), + EventFields: []testField{ + { + Name: "u8_value", + Value: types.NewU8(11), + }, + { + Name: "u16_value", + Value: types.NewU16(121), + }, + { + Name: "u32_value", + Value: types.NewU32(12134), + }, + { + Name: "u64_value", + Value: types.NewU64(128678), + }, + { + Name: "u128_value", + Value: types.NewU128(*big.NewInt(56346)), + }, + { + Name: "u256_value", + Value: types.NewU256(*big.NewInt(5674)), + }, + }, + Topics: []types.Hash{ + types.NewHash([]byte{3, 4, 5}), + }, + }, + { + Name: "test_event_3", + Phase: &types.Phase{ + IsInitialization: true, + }, + EventID: types.EventID([2]byte{1, 1}), + EventFields: []testField{ + { + Name: "i8_value", + Value: types.NewI8(45), + }, + { + Name: "i16_value", + Value: types.NewI16(445), + }, + { + Name: "i32_value", + Value: types.NewI32(545), + }, + { + Name: "i64_value", + Value: types.NewI64(4789), + }, + { + Name: "i128_value", + Value: types.NewI128(*big.NewInt(56747)), + }, + { + Name: "i256_value", + Value: types.NewI256(*big.NewInt(45356747)), + }, + }, + Topics: []types.Hash{ + types.NewHash([]byte{6, 7, 8}), + }, + }, + } + + encodedEvents, reg, err := getEventParsingTestData(testEvents) + assert.NoError(t, err) + + eventParser := NewEventParser() + + res, err := eventParser.ParseEvents(reg, encodedEvents) + assert.NoError(t, err) + assert.Len(t, res, len(testEvents)) + + for i, testEvent := range testEvents { + assert.Equal(t, testEvent.Name, res[i].Name) + assert.Equal(t, testEvent.EventID, res[i].EventID) + assertEventFieldInformationIsCorrect(t, testEvent.EventFields, res[i]) + assert.Equal(t, testEvent.Phase, res[i].Phase) + assert.Equal(t, testEvent.Topics, res[i].Topics) + } +} + +func TestEventParserFn_ParseEvents_EventCountDecodeError(t *testing.T) { + testEvents := []testEvent{ + { + Name: "test_event_1", + Phase: &types.Phase{ + IsApplyExtrinsic: true, + AsApplyExtrinsic: 1, + }, + EventID: types.EventID([2]byte{0, 1}), + EventFields: []testField{ + { + Name: "bool_value", + Value: true, + }, + }, + Topics: []types.Hash{ + types.NewHash([]byte{0, 1, 2}), + }, + }, + } + + _, reg, err := getEventParsingTestData(testEvents) + assert.NoError(t, err) + + eventParser := NewEventParser() + + // No storage data + res, err := eventParser.ParseEvents(reg, &types.StorageDataRaw{}) + assert.ErrorIs(t, err, ErrEventsCountDecoding) + assert.Nil(t, res) +} + +func TestEventParserFn_ParseEvents_PhaseDecodeError(t *testing.T) { + testEvents := []testEvent{ + { + Name: "test_event_1", + Phase: &types.Phase{ + IsApplyExtrinsic: true, + AsApplyExtrinsic: 1, + }, + EventID: types.EventID([2]byte{0, 1}), + EventFields: []testField{ + { + Name: "bool_value", + Value: true, + }, + }, + Topics: []types.Hash{ + types.NewHash([]byte{0, 1, 2}), + }, + }, + } + + _, reg, err := getEventParsingTestData(testEvents) + assert.NoError(t, err) + + eventParser := NewEventParser() + + var b []byte + + buf := bytes.NewBuffer(b) + + encoder := scale.NewEncoder(buf) + + err = encoder.Encode(types.NewUCompactFromUInt(uint64(len(testEvents)))) + assert.NoError(t, err) + + // The storage data only contains the number of events, the phase decode step should fail in this case. + storageData := types.StorageDataRaw(buf.Bytes()) + + res, err := eventParser.ParseEvents(reg, &storageData) + assert.ErrorIs(t, err, ErrEventPhaseDecoding) + assert.Nil(t, res) +} + +func TestEventParserFn_ParseEvents_EventIDDecodeError(t *testing.T) { + testEvents := []testEvent{ + { + Name: "test_event_1", + Phase: &types.Phase{ + IsApplyExtrinsic: true, + AsApplyExtrinsic: 1, + }, + EventID: types.EventID([2]byte{0, 1}), + EventFields: []testField{ + { + Name: "bool_value", + Value: true, + }, + }, + Topics: []types.Hash{ + types.NewHash([]byte{0, 1, 2}), + }, + }, + } + + _, reg, err := getEventParsingTestData(testEvents) + assert.NoError(t, err) + + eventParser := NewEventParser() + + var b []byte + + buf := bytes.NewBuffer(b) + + encoder := scale.NewEncoder(buf) + + err = encoder.Encode(types.NewUCompactFromUInt(uint64(len(testEvents)))) + assert.NoError(t, err) + + err = encoder.Encode(testEvents[0].Phase) + assert.NoError(t, err) + + // The storage data only contains: + // - the number of events + // - the phase + // EventID decoding should fail in this case. + storageData := types.StorageDataRaw(buf.Bytes()) + + res, err := eventParser.ParseEvents(reg, &storageData) + assert.ErrorIs(t, err, ErrEventIDDecoding) + assert.Nil(t, res) +} + +func TestEventParserFn_ParseEvents_EventFieldsDecodeError(t *testing.T) { + testEvents := []testEvent{ + { + Name: "test_event_1", + Phase: &types.Phase{ + IsApplyExtrinsic: true, + AsApplyExtrinsic: 1, + }, + EventID: types.EventID([2]byte{0, 1}), + EventFields: []testField{ + { + Name: "bool_value", + Value: true, + }, + }, + Topics: []types.Hash{ + types.NewHash([]byte{0, 1, 2}), + }, + }, + } + + _, reg, err := getEventParsingTestData(testEvents) + assert.NoError(t, err) + + eventParser := NewEventParser() + + var b []byte + + buf := bytes.NewBuffer(b) + + encoder := scale.NewEncoder(buf) + + err = encoder.Encode(types.NewUCompactFromUInt(uint64(len(testEvents)))) + assert.NoError(t, err) + + err = encoder.Encode(testEvents[0].Phase) + assert.NoError(t, err) + + err = encoder.Encode(testEvents[0].EventID) + assert.NoError(t, err) + + // The storage data only contains: + // - the number of events + // - the phase + // - the event ID + // Event field decoding should fail in this case. + storageData := types.StorageDataRaw(buf.Bytes()) + + res, err := eventParser.ParseEvents(reg, &storageData) + assert.ErrorIs(t, err, ErrEventFieldsDecoding) + assert.Nil(t, res) +} + +func TestEventParserFn_ParseEvents_MissingEventDecoder(t *testing.T) { + testEvents := []testEvent{ + { + Name: "test_event_1", + Phase: &types.Phase{ + IsApplyExtrinsic: true, + AsApplyExtrinsic: 1, + }, + EventID: types.EventID([2]byte{0, 1}), + EventFields: []testField{ + { + Name: "bool_value", + Value: true, + }, + }, + Topics: []types.Hash{ + types.NewHash([]byte{0, 1, 2}), + }, + }, + } + + _, _, err := getEventParsingTestData(testEvents) + assert.NoError(t, err) + + eventParser := NewEventParser() + + var b []byte + + buf := bytes.NewBuffer(b) + + encoder := scale.NewEncoder(buf) + + err = encoder.Encode(types.NewUCompactFromUInt(uint64(len(testEvents)))) + assert.NoError(t, err) + + err = encoder.Encode(testEvents[0].Phase) + assert.NoError(t, err) + + err = encoder.Encode(testEvents[0].EventID) + assert.NoError(t, err) + + storageData := types.StorageDataRaw(buf.Bytes()) + + // Empty registry, decoding should fail. + res, err := eventParser.ParseEvents(registry.EventRegistry{}, &storageData) + assert.ErrorIs(t, err, ErrEventDecoderNotFound) + assert.Nil(t, res) +} + +func TestEventParserFn_ParseEvents_TopicsDecodeError(t *testing.T) { + testEvents := []testEvent{ + { + Name: "test_event_1", + Phase: &types.Phase{ + IsApplyExtrinsic: true, + AsApplyExtrinsic: 1, + }, + EventID: types.EventID([2]byte{0, 1}), + EventFields: []testField{ + { + Name: "bool_value", + Value: true, + }, + }, + Topics: []types.Hash{ + types.NewHash([]byte{0, 1, 2}), + }, + }, + } + + _, reg, err := getEventParsingTestData(testEvents) + assert.NoError(t, err) + + eventParser := NewEventParser() + + var b []byte + + buf := bytes.NewBuffer(b) + + encoder := scale.NewEncoder(buf) + + err = encoder.Encode(types.NewUCompactFromUInt(uint64(len(testEvents)))) + assert.NoError(t, err) + + err = encoder.Encode(testEvents[0].Phase) + assert.NoError(t, err) + + err = encoder.Encode(testEvents[0].EventID) + assert.NoError(t, err) + + for _, field := range testEvents[0].EventFields { + err := encoder.Encode(field.Value) + assert.NoError(t, err) + } + + // Set topics length to 1 + err = encoder.Encode(types.NewUCompactFromUInt(1)) + assert.NoError(t, err) + + // The storage data only contains: + // - the number of events + // - the phase + // - the event ID + // - the event fields + // - the topics length + // Topics decoding should fail in this case. + storageData := types.StorageDataRaw(buf.Bytes()) + + res, err := eventParser.ParseEvents(reg, &storageData) + assert.ErrorIs(t, err, ErrEventTopicsDecoding) + assert.Nil(t, res) +} + +func assertEventFieldInformationIsCorrect(t *testing.T, testFields []testField, event *Event) { + for _, testField := range testFields { + assert.Equal(t, testField.Value, event.Fields[testField.Name]) + } +} + +func getEventParsingTestData(testEvents []testEvent) (*types.StorageDataRaw, registry.EventRegistry, error) { + eventRegistry, err := getRegistryForTestEvents(testEvents) + + if err != nil { + return nil, nil, err + } + + encodedEventData, err := getEncodedEventData(testEvents) + + if err != nil { + return nil, nil, err + } + + storageData := types.StorageDataRaw(encodedEventData) + + return &storageData, eventRegistry, nil +} + +func getEncodedEventData(testEvents []testEvent) ([]byte, error) { + var encodedEventData []byte + + buf := bytes.NewBuffer(encodedEventData) + + encoder := scale.NewEncoder(buf) + + eventsLen := types.NewUCompactFromUInt(uint64(len(testEvents))) + + if err := encoder.Encode(eventsLen); err != nil { + return nil, err + } + + for _, testEvent := range testEvents { + if err := testEvent.Encode(*encoder); err != nil { + return nil, err + } + } + + return buf.Bytes(), nil +} + +func getRegistryForTestEvents(testEvents []testEvent) (registry.EventRegistry, error) { + eventRegistry := registry.EventRegistry(map[types.EventID]*registry.Type{}) + + for _, testEvent := range testEvents { + regFields, err := getTestRegistryFields(testEvent.EventFields) + + if err != nil { + return nil, err + } + + eventRegistry[testEvent.EventID] = ®istry.Type{ + Name: testEvent.Name, + Fields: regFields, + } + } + + return eventRegistry, nil +} + +func getTestRegistryFields(fields []testField) ([]*registry.Field, error) { + var regFields []*registry.Field + + for _, field := range fields { + regField, err := getTestRegistryField(field) + + if err != nil { + return nil, err + } + + regFields = append(regFields, regField) + } + + return regFields, nil +} + +func getTestRegistryField(field testField) (*registry.Field, error) { + regField := ®istry.Field{} + regField.Name = field.Name + + switch field.Value.(type) { + case bool: + regField.FieldDecoder = ®istry.ValueDecoder[bool]{} + case byte: + regField.FieldDecoder = ®istry.ValueDecoder[byte]{} + case string: + regField.FieldDecoder = ®istry.ValueDecoder[string]{} + case types.U8: + regField.FieldDecoder = ®istry.ValueDecoder[types.U8]{} + case types.U16: + regField.FieldDecoder = ®istry.ValueDecoder[types.U16]{} + case types.U32: + regField.FieldDecoder = ®istry.ValueDecoder[types.U32]{} + case types.U64: + regField.FieldDecoder = ®istry.ValueDecoder[types.U64]{} + case types.U128: + regField.FieldDecoder = ®istry.ValueDecoder[types.U128]{} + case types.U256: + regField.FieldDecoder = ®istry.ValueDecoder[types.U256]{} + case types.I8: + regField.FieldDecoder = ®istry.ValueDecoder[types.I8]{} + case types.I16: + regField.FieldDecoder = ®istry.ValueDecoder[types.I16]{} + case types.I32: + regField.FieldDecoder = ®istry.ValueDecoder[types.I32]{} + case types.I64: + regField.FieldDecoder = ®istry.ValueDecoder[types.I64]{} + case types.I128: + regField.FieldDecoder = ®istry.ValueDecoder[types.I128]{} + case types.I256: + regField.FieldDecoder = ®istry.ValueDecoder[types.I256]{} + default: + return nil, fmt.Errorf("type not supported - %v", field) + } + + return regField, nil +} + +type testField struct { + Name string + Value any +} + +type testEvent struct { + Name string + Phase *types.Phase + EventID types.EventID + EventFields []testField + Topics []types.Hash +} + +func (t testEvent) Encode(encoder scale.Encoder) error { + if err := encoder.Encode(t.Phase); err != nil { + return fmt.Errorf("couldn't encode phase: %w", err) + } + + if err := encoder.Encode(t.EventID); err != nil { + return fmt.Errorf("couldn't encode event ID: %w", err) + } + + for _, field := range t.EventFields { + if err := encoder.Encode(field.Value); err != nil { + return fmt.Errorf("couldn't encode field %v: %w", field, err) + } + } + + if err := encoder.Encode(t.Topics); err != nil { + return fmt.Errorf("couldn't encode topics: %w", err) + } + + return nil +} diff --git a/registry/parser/extrinsic_parser.go b/registry/parser/extrinsic_parser.go new file mode 100644 index 000000000..b89744356 --- /dev/null +++ b/registry/parser/extrinsic_parser.go @@ -0,0 +1,120 @@ +package parser + +import ( + "bytes" + "fmt" + + "github.com/centrifuge/go-substrate-rpc-client/v4/registry" + "github.com/centrifuge/go-substrate-rpc-client/v4/rpc/chain/generic" + "github.com/centrifuge/go-substrate-rpc-client/v4/scale" + "github.com/centrifuge/go-substrate-rpc-client/v4/types" +) + +// DefaultExtrinsic is the Extrinsic with defaults for the generic types: +// +// Address - types.MultiAddress +// Signature - types.MultiSignature +// PaymentFields - generic.DefaultPaymentFields +type DefaultExtrinsic = Extrinsic[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, +] + +// Extrinsic holds all the information of a decoded block extrinsic. +// +// This type is generic over types A, S, P, please check generic.GenericExtrinsicSignature for more +// information about these generic types. +type Extrinsic[A, S, P any] struct { + Name string + CallFields map[string]any + CallIndex types.CallIndex + Version byte + Signature generic.GenericExtrinsicSignature[A, S, P] +} + +//nolint:lll +//go:generate mockery --name ExtrinsicParser --structname ExtrinsicParserMock --filename extrinsic_parser_mock.go --inpackage + +// ExtrinsicParser is the interface used for parsing a block's extrinsics into []*Extrinsic. +// +// This interface is generic over types A, S, P, please check generic.GenericExtrinsicSignature for more +// information about these generic types. +// +//nolint:lll +type ExtrinsicParser[A, S, P any] interface { + ParseExtrinsics(callRegistry registry.CallRegistry, block generic.GenericSignedBlock[A, S, P]) ([]*Extrinsic[A, S, P], error) +} + +// ExtrinsicParserFn implements ExtrinsicParser. +// +//nolint:lll +type ExtrinsicParserFn[A, S, P any] func(callRegistry registry.CallRegistry, block generic.GenericSignedBlock[A, S, P]) ([]*Extrinsic[A, S, P], error) + +// ParseExtrinsics is the function required for satisfying the ExtrinsicParser interface. +// +//nolint:lll +func (e ExtrinsicParserFn[A, S, P]) ParseExtrinsics(callRegistry registry.CallRegistry, block generic.GenericSignedBlock[A, S, P]) ([]*Extrinsic[A, S, P], error) { + return e(callRegistry, block) +} + +// NewExtrinsicParser creates a new ExtrinsicParser. +func NewExtrinsicParser[A, S, P any]() ExtrinsicParser[A, S, P] { + // The ExtrinsicParserFn provided here is attempting to decode the types.Args of an extrinsic's method + // into a map of fields and their respective decoded values. + // + //nolint:lll + return ExtrinsicParserFn[A, S, P](func(callRegistry registry.CallRegistry, block generic.GenericSignedBlock[A, S, P]) ([]*Extrinsic[A, S, P], error) { + var extrinsics []*Extrinsic[A, S, P] + + for i, extrinsic := range block.GetGenericBlock().GetExtrinsics() { + callIndex := extrinsic.GetCall().CallIndex + + callDecoder, ok := callRegistry[callIndex] + + if !ok { + return nil, ErrCallDecoderNotFound.Wrap(fmt.Errorf("extrinsic #%d", i)) + } + + decoder := scale.NewDecoder(bytes.NewReader(extrinsic.GetCall().Args)) + + callFields, err := callDecoder.Decode(decoder) + + if err != nil { + return nil, ErrCallFieldsDecoding.Wrap(fmt.Errorf("extrinsic #%d: %w", i, err)) + } + + call := &Extrinsic[A, S, P]{ + Name: callDecoder.Name, + CallFields: callFields, + CallIndex: callIndex, + Version: extrinsic.GetVersion(), + Signature: extrinsic.GetSignature(), + } + + extrinsics = append(extrinsics, call) + } + + return extrinsics, nil + }) +} + +// DefaultExtrinsicParser is the ExtrinsicParser interface with defaults for the generic types: +// +// Address - types.MultiAddress +// Signature - types.MultiSignature +// PaymentFields - generic.DefaultPaymentFields +type DefaultExtrinsicParser = ExtrinsicParser[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, +] + +// NewDefaultExtrinsicParser returns a DefaultExtrinsicParser. +func NewDefaultExtrinsicParser() DefaultExtrinsicParser { + return NewExtrinsicParser[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]() +} diff --git a/registry/parser/extrinsic_parser_mock.go b/registry/parser/extrinsic_parser_mock.go new file mode 100644 index 000000000..bfc8908b9 --- /dev/null +++ b/registry/parser/extrinsic_parser_mock.go @@ -0,0 +1,53 @@ +// Code generated by mockery v2.13.0-beta.1. DO NOT EDIT. + +package parser + +import ( + generic "github.com/centrifuge/go-substrate-rpc-client/v4/rpc/chain/generic" + mock "github.com/stretchr/testify/mock" + + registry "github.com/centrifuge/go-substrate-rpc-client/v4/registry" +) + +// ExtrinsicParserMock is an autogenerated mock type for the ExtrinsicParser type +type ExtrinsicParserMock[A interface{}, S interface{}, P interface{}] struct { + mock.Mock +} + +// ParseExtrinsics provides a mock function with given fields: callRegistry, block +func (_m *ExtrinsicParserMock[A, S, P]) ParseExtrinsics(callRegistry registry.CallRegistry, block generic.GenericSignedBlock[A, S, P]) ([]*Extrinsic[A, S, P], error) { + ret := _m.Called(callRegistry, block) + + var r0 []*Extrinsic[A, S, P] + if rf, ok := ret.Get(0).(func(registry.CallRegistry, generic.GenericSignedBlock[A, S, P]) []*Extrinsic[A, S, P]); ok { + r0 = rf(callRegistry, block) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*Extrinsic[A, S, P]) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(registry.CallRegistry, generic.GenericSignedBlock[A, S, P]) error); ok { + r1 = rf(callRegistry, block) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type NewExtrinsicParserMockT interface { + mock.TestingT + Cleanup(func()) +} + +// NewExtrinsicParserMock creates a new instance of ExtrinsicParserMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewExtrinsicParserMock[A interface{}, S interface{}, P interface{}](t NewExtrinsicParserMockT) *ExtrinsicParserMock[A, S, P] { + mock := &ExtrinsicParserMock[A, S, P]{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/registry/parser/extrinsic_parser_test.go b/registry/parser/extrinsic_parser_test.go new file mode 100644 index 000000000..70f699301 --- /dev/null +++ b/registry/parser/extrinsic_parser_test.go @@ -0,0 +1,344 @@ +package parser + +import ( + "bytes" + "fmt" + "math/big" + "testing" + + "github.com/centrifuge/go-substrate-rpc-client/v4/rpc/chain/generic" + + "github.com/centrifuge/go-substrate-rpc-client/v4/registry" + "github.com/centrifuge/go-substrate-rpc-client/v4/scale" + "github.com/centrifuge/go-substrate-rpc-client/v4/types" + "github.com/stretchr/testify/assert" +) + +func TestExtrinsicParserFn_ParseExtrinsics(t *testing.T) { + testExtrinsics := []testExtrinsic[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]{ + { + Name: "extrinsic_1", + CallIndex: types.CallIndex{ + SectionIndex: 0, + MethodIndex: 1, + }, + CallFields: []testField{ + { + Name: "bool_field", + Value: true, + }, + { + Name: "byte_field", + Value: byte(32), + }, + { + Name: "string_field", + Value: "test", + }, + }, + Version: 1, + Signature: &generic.ExtrinsicSignature[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]{}, + }, + { + Name: "extrinsic_2", + CallIndex: types.CallIndex{ + SectionIndex: 1, + MethodIndex: 0, + }, + CallFields: []testField{ + { + Name: "u8_value", + Value: types.NewU8(11), + }, + { + Name: "u16_value", + Value: types.NewU16(121), + }, + { + Name: "u32_value", + Value: types.NewU32(12134), + }, + { + Name: "u64_value", + Value: types.NewU64(128678), + }, + { + Name: "u128_value", + Value: types.NewU128(*big.NewInt(56346)), + }, + { + Name: "u256_value", + Value: types.NewU256(*big.NewInt(5674)), + }, + }, + Version: 2, + Signature: &generic.ExtrinsicSignature[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]{}, + }, + { + Name: "extrinsic_3", + CallIndex: types.CallIndex{ + SectionIndex: 1, + MethodIndex: 1, + }, + CallFields: []testField{ + { + Name: "i8_value", + Value: types.NewI8(45), + }, + { + Name: "i16_value", + Value: types.NewI16(445), + }, + { + Name: "i32_value", + Value: types.NewI32(545), + }, + { + Name: "i64_value", + Value: types.NewI64(4789), + }, + { + Name: "i128_value", + Value: types.NewI128(*big.NewInt(56747)), + }, + { + Name: "i256_value", + Value: types.NewI256(*big.NewInt(45356747)), + }, + }, + Version: 2, + Signature: &generic.ExtrinsicSignature[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]{}, + }, + } + + block, reg, err := getExtrinsicParsingTestData[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ](testExtrinsics) + assert.NoError(t, err) + + extrinsicParser := NewExtrinsicParser[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]() + + res, err := extrinsicParser.ParseExtrinsics(reg, block) + assert.NoError(t, err) + assert.Len(t, res, len(testExtrinsics)) + + for i, testExtrinsic := range testExtrinsics { + assert.Equal(t, testExtrinsic.Name, res[i].Name) + assert.Equal(t, testExtrinsic.Version, res[i].Version) + assertExtrinsicFieldInformationIsCorrect(t, testExtrinsic.CallFields, res[i]) + assert.Equal(t, testExtrinsic.CallIndex, res[i].CallIndex) + assert.Equal(t, testExtrinsic.Signature, res[i].Signature) + } +} + +func TestExtrinsicParserFn_ParseExtrinsics_MissingCallDecoder(t *testing.T) { + testExtrinsics := []testExtrinsic[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]{ + { + Name: "extrinsic_1", + CallIndex: types.CallIndex{ + SectionIndex: 0, + MethodIndex: 1, + }, + CallFields: []testField{ + { + Name: "bool_field", + Value: true, + }, + { + Name: "byte_field", + Value: byte(32), + }, + { + Name: "string_field", + Value: "test", + }, + }, + Version: 1, + Signature: &generic.ExtrinsicSignature[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]{}, + }, + } + + block, _, err := getExtrinsicParsingTestData[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ](testExtrinsics) + assert.NoError(t, err) + + extrinsicParser := NewExtrinsicParser[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]() + + // Empty registry, decoding should fail. + res, err := extrinsicParser.ParseExtrinsics(registry.CallRegistry{}, block) + assert.ErrorIs(t, err, ErrCallDecoderNotFound) + assert.Nil(t, res) +} + +func TestExtrinsicParserFn_ParseExtrinsics_DecodeError(t *testing.T) { + testExtrinsics := []testExtrinsic[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]{ + { + Name: "extrinsic_1", + CallIndex: types.CallIndex{ + SectionIndex: 0, + MethodIndex: 1, + }, + CallFields: []testField{ + { + Name: "bool_field", + Value: true, + }, + { + Name: "byte_field", + Value: byte(32), + }, + { + Name: "string_field", + Value: "test", + }, + }, + Version: 1, + Signature: &generic.ExtrinsicSignature[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]{}, + }, + } + + block, reg, err := getExtrinsicParsingTestData[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ](testExtrinsics) + assert.NoError(t, err) + + extrinsicParser := NewExtrinsicParser[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]() + + // No args for extrinsics should trigger an error. + block.Block.Extrinsics[0].Method.Args = []byte{} + + res, err := extrinsicParser.ParseExtrinsics(reg, block) + assert.ErrorIs(t, err, ErrCallFieldsDecoding) + assert.Nil(t, res) +} + +func assertExtrinsicFieldInformationIsCorrect[A, S, P any](t *testing.T, testFields []testField, extrinsic *Extrinsic[A, S, P]) { + for _, testField := range testFields { + assert.Equal(t, testField.Value, extrinsic.CallFields[testField.Name]) + } +} + +func getExtrinsicParsingTestData[A, S, P any](testExtrinsics []testExtrinsic[A, S, P]) (*generic.SignedBlock[A, S, P], registry.CallRegistry, error) { + callRegistry, err := getRegistryForTestExtrinsic(testExtrinsics) + + if err != nil { + return nil, nil, err + } + + block := &generic.SignedBlock[A, S, P]{ + Block: &generic.Block[A, S, P]{}, + } + + for _, testExtrinsic := range testExtrinsics { + encodedExtrinsicCall, err := testExtrinsic.Encode() + + if err != nil { + return nil, nil, err + } + + block.Block.Extrinsics = append(block.Block.Extrinsics, &generic.Extrinsic[A, S, P]{ + Version: testExtrinsic.Version, + Signature: testExtrinsic.Signature, + Method: types.Call{ + CallIndex: testExtrinsic.CallIndex, + Args: encodedExtrinsicCall, + }, + }) + } + + return block, callRegistry, nil +} + +func getRegistryForTestExtrinsic[A, S, P any](testExtrinsics []testExtrinsic[A, S, P]) (registry.CallRegistry, error) { + callRegistry := registry.CallRegistry(map[types.CallIndex]*registry.Type{}) + + for _, testExtrinsic := range testExtrinsics { + regFields, err := getTestRegistryFields(testExtrinsic.CallFields) + + if err != nil { + return nil, err + } + + callRegistry[testExtrinsic.CallIndex] = ®istry.Type{ + Name: testExtrinsic.Name, + Fields: regFields, + } + } + + return callRegistry, nil +} + +type testExtrinsic[A, S, P any] struct { + Name string + CallIndex types.CallIndex + CallFields []testField + Version byte + Signature *generic.ExtrinsicSignature[A, S, P] +} + +func (t testExtrinsic[A, S, P]) Encode() ([]byte, error) { + var b []byte + + buf := bytes.NewBuffer(b) + + encoder := scale.NewEncoder(buf) + + for _, field := range t.CallFields { + if err := encoder.Encode(field.Value); err != nil { + return nil, fmt.Errorf("couldn't encode field %v: %w", field, err) + } + } + + return buf.Bytes(), nil +} diff --git a/registry/registry.go b/registry/registry.go index 3e2c16d4e..0698c2d30 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -1,14 +1,16 @@ package registry import ( - "encoding/json" "errors" "fmt" "strings" + "github.com/centrifuge/go-substrate-rpc-client/v4/scale" "github.com/centrifuge/go-substrate-rpc-client/v4/types" ) +//go:generate mockery --name Factory --structname FactoryMock --filename factory_mock.go --inpackage + // Factory is the interface responsible for generating the according registries from the metadata. type Factory interface { CreateCallRegistry(meta *types.Metadata) (CallRegistry, error) @@ -17,7 +19,7 @@ type Factory interface { } // CallRegistry maps a call name to its Type. -type CallRegistry map[string]*Type +type CallRegistry map[types.CallIndex]*Type // ErrorRegistry maps an error name to its Type. type ErrorRegistry map[string]*Type @@ -26,21 +28,20 @@ type ErrorRegistry map[string]*Type type EventRegistry map[types.EventID]*Type type factory struct { - fieldStorage map[int64]FieldType - recursiveFieldStorage map[int64]*RecursiveFieldType + fieldStorage map[int64]FieldDecoder + recursiveFieldStorage map[int64]*RecursiveDecoder } // NewFactory creates a new Factory. func NewFactory() Factory { - return &factory{ - fieldStorage: make(map[int64]FieldType), - recursiveFieldStorage: make(map[int64]*RecursiveFieldType), - } + return &factory{} } // CreateErrorRegistry creates the registry that contains the types for errors. // nolint:dupl -func (r *factory) CreateErrorRegistry(meta *types.Metadata) (ErrorRegistry, error) { +func (f *factory) CreateErrorRegistry(meta *types.Metadata) (ErrorRegistry, error) { + f.initStorages() + errorRegistry := make(map[string]*Type) for _, mod := range meta.AsMetadataV14.Pallets { @@ -51,20 +52,20 @@ func (r *factory) CreateErrorRegistry(meta *types.Metadata) (ErrorRegistry, erro errorsType, ok := meta.AsMetadataV14.EfficientLookup[mod.Errors.Type.Int64()] if !ok { - return nil, fmt.Errorf("errors type %d not found for module '%s'", mod.Errors.Type.Int64(), mod.Name) + return nil, ErrErrorsTypeNotFound.WithMsg("errors type '%d', module '%s'", mod.Errors.Type.Int64(), mod.Name) } if !errorsType.Def.IsVariant { - return nil, fmt.Errorf("errors type %d for module '%s' is not a variant", mod.Errors.Type.Int64(), mod.Name) + return nil, ErrErrorsTypeNotVariant.WithMsg("errors type '%d', module '%s'", mod.Errors.Type.Int64(), mod.Name) } for _, errorVariant := range errorsType.Def.Variant.Variants { errorName := fmt.Sprintf("%s.%s", mod.Name, errorVariant.Name) - errorFields, err := r.getTypeFields(meta, errorVariant.Fields) + errorFields, err := f.getTypeFields(meta, errorVariant.Fields) if err != nil { - return nil, fmt.Errorf("couldn't get fields for error '%s': %w", errorName, err) + return nil, ErrErrorFieldsRetrieval.WithMsg(errorName).Wrap(err) } errorRegistry[errorName] = &Type{ @@ -74,8 +75,8 @@ func (r *factory) CreateErrorRegistry(meta *types.Metadata) (ErrorRegistry, erro } } - if err := r.resolveRecursiveTypes(); err != nil { - return nil, err + if err := f.resolveRecursiveDecoders(); err != nil { + return nil, ErrRecursiveDecodersResolving.Wrap(err) } return errorRegistry, nil @@ -83,8 +84,10 @@ func (r *factory) CreateErrorRegistry(meta *types.Metadata) (ErrorRegistry, erro // CreateCallRegistry creates the registry that contains the types for calls. // nolint:dupl -func (r *factory) CreateCallRegistry(meta *types.Metadata) (CallRegistry, error) { - callRegistry := make(map[string]*Type) +func (f *factory) CreateCallRegistry(meta *types.Metadata) (CallRegistry, error) { + f.initStorages() + + callRegistry := make(map[types.CallIndex]*Type) for _, mod := range meta.AsMetadataV14.Pallets { if !mod.HasCalls { @@ -94,38 +97,45 @@ func (r *factory) CreateCallRegistry(meta *types.Metadata) (CallRegistry, error) callsType, ok := meta.AsMetadataV14.EfficientLookup[mod.Calls.Type.Int64()] if !ok { - return nil, fmt.Errorf("calls type %d not found for module '%s'", mod.Calls.Type.Int64(), mod.Name) + return nil, ErrCallsTypeNotFound.WithMsg("calls type '%d', module '%s'", mod.Calls.Type.Int64(), mod.Name) } if !callsType.Def.IsVariant { - return nil, fmt.Errorf("calls type %d for module '%s' is not a variant", mod.Calls.Type.Int64(), mod.Name) + return nil, ErrCallsTypeNotVariant.WithMsg("calls type '%d', module '%s'", mod.Calls.Type.Int64(), mod.Name) } for _, callVariant := range callsType.Def.Variant.Variants { + callIndex := types.CallIndex{ + SectionIndex: uint8(mod.Index), + MethodIndex: uint8(callVariant.Index), + } + callName := fmt.Sprintf("%s.%s", mod.Name, callVariant.Name) - callFields, err := r.getTypeFields(meta, callVariant.Fields) + callFields, err := f.getTypeFields(meta, callVariant.Fields) if err != nil { - return nil, fmt.Errorf("couldn't get fields for call '%s': %w", callName, err) + return nil, ErrCallFieldsRetrieval.WithMsg(callName).Wrap(err) } - callRegistry[callName] = &Type{ + callRegistry[callIndex] = &Type{ Name: callName, Fields: callFields, } } } - if err := r.resolveRecursiveTypes(); err != nil { - return nil, err + if err := f.resolveRecursiveDecoders(); err != nil { + return nil, ErrRecursiveDecodersResolving.Wrap(err) } return callRegistry, nil } // CreateEventRegistry creates the registry that contains the types for events. -func (r *factory) CreateEventRegistry(meta *types.Metadata) (EventRegistry, error) { +func (f *factory) CreateEventRegistry(meta *types.Metadata) (EventRegistry, error) { + f.initStorages() + eventRegistry := make(map[types.EventID]*Type) for _, mod := range meta.AsMetadataV14.Pallets { @@ -136,11 +146,11 @@ func (r *factory) CreateEventRegistry(meta *types.Metadata) (EventRegistry, erro eventsType, ok := meta.AsMetadataV14.EfficientLookup[mod.Events.Type.Int64()] if !ok { - return nil, fmt.Errorf("events type %d not found for module '%s'", mod.Events.Type.Int64(), mod.Name) + return nil, ErrEventsTypeNotFound.WithMsg("events type '%d', module '%s'", mod.Events.Type.Int64(), mod.Name) } if !eventsType.Def.IsVariant { - return nil, fmt.Errorf("events type %d for module '%s' is not a variant", mod.Events.Type.Int64(), mod.Name) + return nil, ErrEventsTypeNotVariant.WithMsg("events type '%d', module '%s'", mod.Events.Type.Int64(), mod.Name) } for _, eventVariant := range eventsType.Def.Variant.Variants { @@ -148,10 +158,10 @@ func (r *factory) CreateEventRegistry(meta *types.Metadata) (EventRegistry, erro eventName := fmt.Sprintf("%s.%s", mod.Name, eventVariant.Name) - eventFields, err := r.getTypeFields(meta, eventVariant.Fields) + eventFields, err := f.getTypeFields(meta, eventVariant.Fields) if err != nil { - return nil, fmt.Errorf("couldn't get fields for event '%s': %w", eventName, err) + return nil, ErrEventFieldsRetrieval.WithMsg(eventName).Wrap(err) } eventRegistry[eventID] = &Type{ @@ -161,158 +171,149 @@ func (r *factory) CreateEventRegistry(meta *types.Metadata) (EventRegistry, erro } } - if err := r.resolveRecursiveTypes(); err != nil { - return nil, err + if err := f.resolveRecursiveDecoders(); err != nil { + return nil, ErrRecursiveDecodersResolving.Wrap(err) } return eventRegistry, nil } -// resolveRecursiveTypes resolves all recursive types with their according field type. +// initStorages initializes the storages used when creating registries. +func (f *factory) initStorages() { + f.fieldStorage = make(map[int64]FieldDecoder) + f.recursiveFieldStorage = make(map[int64]*RecursiveDecoder) +} + +// resolveRecursiveDecoders resolves all recursive decoders with their according FieldDecoder. // nolint:lll -func (r *factory) resolveRecursiveTypes() error { - for recursiveFieldLookupIndex, recursiveFieldType := range r.recursiveFieldStorage { - fieldType, ok := r.fieldStorage[recursiveFieldLookupIndex] +func (f *factory) resolveRecursiveDecoders() error { + for recursiveFieldLookupIndex, recursiveFieldDecoder := range f.recursiveFieldStorage { + fieldDecoder, ok := f.fieldStorage[recursiveFieldLookupIndex] if !ok { - return fmt.Errorf("couldn't get field type for recursive type %d", recursiveFieldLookupIndex) + return ErrFieldDecoderForRecursiveFieldNotFound. + WithMsg( + "recursive field lookup index %d", + recursiveFieldLookupIndex, + ) } - if _, ok := fieldType.(*RecursiveFieldType); ok { - return fmt.Errorf("recursive field type %d cannot be resolved with a non-recursive field type", recursiveFieldLookupIndex) + if _, ok := fieldDecoder.(*RecursiveDecoder); ok { + return ErrRecursiveFieldResolving. + WithMsg( + "recursive field lookup index %d", + recursiveFieldLookupIndex, + ) } - recursiveFieldType.ResolvedItemType = fieldType + recursiveFieldDecoder.FieldDecoder = fieldDecoder } return nil } -// getTypeFields parses and returns all fields for a type. -func (r *factory) getTypeFields(meta *types.Metadata, fields []types.Si1Field) ([]*Field, error) { +// getTypeFields parses and returns all Field(s) for a type. +func (f *factory) getTypeFields(meta *types.Metadata, fields []types.Si1Field) ([]*Field, error) { var typeFields []*Field for _, field := range fields { fieldType, ok := meta.AsMetadataV14.EfficientLookup[field.Type.Int64()] if !ok { - return nil, fmt.Errorf("type not found for field '%s'", field.Name) + return nil, ErrFieldTypeNotFound.WithMsg(string(field.Name)) } - fieldName := getFieldName(field, fieldType) + fieldName := getFullFieldName(field, fieldType) - if storedFieldType, ok := r.getStoredFieldType(fieldName, field.Type.Int64()); ok { + if storedFieldDecoder, ok := f.getStoredFieldDecoder(field.Type.Int64()); ok { typeFields = append(typeFields, &Field{ - Name: fieldName, - FieldType: storedFieldType, - LookupIndex: field.Type.Int64(), + Name: fieldName, + FieldDecoder: storedFieldDecoder, + LookupIndex: field.Type.Int64(), }) continue } fieldTypeDef := fieldType.Def - resolvedFieldType, err := r.getFieldType(meta, fieldName, fieldTypeDef) + fieldDecoder, err := f.getFieldDecoder(meta, fieldName, fieldTypeDef) if err != nil { - return nil, fmt.Errorf("couldn't get field type for '%s': %w", fieldName, err) + return nil, ErrFieldDecoderRetrieval.WithMsg(fieldName).Wrap(err) } - r.storeFieldType(field.Type.Int64(), resolvedFieldType) + f.fieldStorage[field.Type.Int64()] = fieldDecoder typeFields = append(typeFields, &Field{ - Name: fieldName, - FieldType: resolvedFieldType, - LookupIndex: field.Type.Int64(), + Name: fieldName, + FieldDecoder: fieldDecoder, + LookupIndex: field.Type.Int64(), }) } return typeFields, nil } -// getFieldType returns the FieldType based on the provided type definition. +// getFieldDecoder returns the FieldDecoder based on the provided type definition. // nolint:funlen -func (r *factory) getFieldType(meta *types.Metadata, fieldName string, typeDef types.Si1TypeDef) (FieldType, error) { +func (f *factory) getFieldDecoder( + meta *types.Metadata, + fieldName string, + typeDef types.Si1TypeDef, +) (FieldDecoder, error) { switch { case typeDef.IsCompact: compactFieldType, ok := meta.AsMetadataV14.EfficientLookup[typeDef.Compact.Type.Int64()] if !ok { - return nil, errors.New("type not found for compact field") + return nil, ErrCompactFieldTypeNotFound.WithMsg(fieldName) } - return r.getCompactFieldType(meta, fieldName, compactFieldType.Def) + return f.getCompactFieldDecoder(meta, fieldName, compactFieldType.Def) case typeDef.IsComposite: - compositeFieldType := &CompositeFieldType{ + compositeDecoder := &CompositeDecoder{ FieldName: fieldName, } - fields, err := r.getTypeFields(meta, typeDef.Composite.Fields) + fields, err := f.getTypeFields(meta, typeDef.Composite.Fields) if err != nil { - return nil, fmt.Errorf("couldn't get composite fields: %w", err) + return nil, ErrCompositeTypeFieldsRetrieval.WithMsg(fieldName).Wrap(err) } - compositeFieldType.Fields = fields + compositeDecoder.Fields = fields - return compositeFieldType, nil + return compositeDecoder, nil case typeDef.IsVariant: - return r.getVariantFieldType(meta, typeDef) + return f.getVariantFieldDecoder(meta, typeDef) case typeDef.IsPrimitive: - return getPrimitiveType(typeDef.Primitive.Si0TypeDefPrimitive) + return getPrimitiveDecoder(typeDef.Primitive.Si0TypeDefPrimitive) case typeDef.IsArray: arrayFieldType, ok := meta.AsMetadataV14.EfficientLookup[typeDef.Array.Type.Int64()] if !ok { - return nil, fmt.Errorf("type not found for array field") + return nil, ErrArrayFieldTypeNotFound.WithMsg(fieldName) } - return r.getArrayFieldType(uint(typeDef.Array.Len), meta, fieldName, arrayFieldType.Def) + return f.getArrayFieldDecoder(uint(typeDef.Array.Len), meta, fieldName, arrayFieldType.Def) case typeDef.IsSequence: vectorFieldType, ok := meta.AsMetadataV14.EfficientLookup[typeDef.Sequence.Type.Int64()] if !ok { - return nil, errors.New("type not found for vector field") + return nil, ErrVectorFieldTypeNotFound.WithMsg(fieldName) } - return r.getSliceFieldType(meta, fieldName, vectorFieldType.Def) + return f.getSliceFieldDecoder(meta, fieldName, vectorFieldType.Def) case typeDef.IsTuple: if typeDef.Tuple == nil { - return &PrimitiveFieldType[[]any]{}, nil + return &NoopDecoder{}, nil } - return r.getTupleType(meta, fieldName, typeDef.Tuple) + return f.getTupleFieldDecoder(meta, fieldName, typeDef.Tuple) case typeDef.IsBitSequence: - bitStoreType, ok := meta.AsMetadataV14.EfficientLookup[typeDef.BitSequence.BitStoreType.Int64()] - - if !ok { - return nil, errors.New("bit store type not found") - } - - bitStoreFieldType, err := r.getFieldType(meta, "bitStoreType", bitStoreType.Def) - - if err != nil { - return nil, fmt.Errorf("couldn't get bit store field type: %w", err) - } - - bitOrderType, ok := meta.AsMetadataV14.EfficientLookup[typeDef.BitSequence.BitOrderType.Int64()] - - if !ok { - return nil, errors.New("bit order type not found") - } - - bitOrderFieldType, err := r.getFieldType(meta, "bitOrderType", bitOrderType.Def) - - if err != nil { - return nil, fmt.Errorf("couldn't get bit order field type: %w", err) - } - - return &BitSequenceType{ - BitStoreType: bitStoreFieldType, - BitOrderType: bitOrderFieldType, - }, nil + return f.getBitSequenceDecoder(meta, fieldName, typeDef.BitSequence) default: - return nil, errors.New("unsupported field type definition") + return nil, ErrFieldTypeDefinitionNotSupported.WithMsg(fieldName) } } @@ -320,56 +321,56 @@ const ( variantItemFieldNameFormat = "variant_item_%d" ) -// getVariantFieldType parses a variant type definition and returns a VariantFieldType. -func (r *factory) getVariantFieldType(meta *types.Metadata, typeDef types.Si1TypeDef) (FieldType, error) { - variantFieldType := &VariantFieldType{} +// getVariantFieldDecoder parses a variant type definition and returns a VariantDecoder. +func (f *factory) getVariantFieldDecoder(meta *types.Metadata, typeDef types.Si1TypeDef) (FieldDecoder, error) { + variantDecoder := &VariantDecoder{} - fieldTypeMap := make(map[byte]FieldType) + fieldDecoderMap := make(map[byte]FieldDecoder) for i, variant := range typeDef.Variant.Variants { if len(variant.Fields) == 0 { - fieldTypeMap[byte(variant.Index)] = &PrimitiveFieldType[byte]{} + fieldDecoderMap[byte(variant.Index)] = &NoopDecoder{} continue } variantFieldName := fmt.Sprintf(variantItemFieldNameFormat, i) - compositeFieldType := &CompositeFieldType{ + compositeDecoder := &CompositeDecoder{ FieldName: variantFieldName, } - fields, err := r.getTypeFields(meta, variant.Fields) + fields, err := f.getTypeFields(meta, variant.Fields) if err != nil { - return nil, fmt.Errorf("couldn't get field types for variant '%d': %w", variant.Index, err) + return nil, ErrVariantTypeFieldsRetrieval.WithMsg("variant '%d'", variant.Index).Wrap(err) } - compositeFieldType.Fields = fields + compositeDecoder.Fields = fields - fieldTypeMap[byte(variant.Index)] = compositeFieldType + fieldDecoderMap[byte(variant.Index)] = compositeDecoder } - variantFieldType.FieldTypeMap = fieldTypeMap + variantDecoder.FieldDecoderMap = fieldDecoderMap - return variantFieldType, nil + return variantDecoder, nil } const ( tupleItemFieldNameFormat = "tuple_item_%d" ) -// getCompactFieldType parses a compact type definition and returns the according field type. +// getCompactFieldDecoder parses a compact type definition and returns the according field decoder. // nolint:funlen,lll -func (r *factory) getCompactFieldType(meta *types.Metadata, fieldName string, typeDef types.Si1TypeDef) (FieldType, error) { +func (f *factory) getCompactFieldDecoder(meta *types.Metadata, fieldName string, typeDef types.Si1TypeDef) (FieldDecoder, error) { switch { case typeDef.IsPrimitive: - return &PrimitiveFieldType[types.UCompact]{}, nil + return &ValueDecoder[types.UCompact]{}, nil case typeDef.IsTuple: if typeDef.Tuple == nil { - return &PrimitiveFieldType[any]{}, nil + return &ValueDecoder[any]{}, nil } - compositeFieldType := &CompositeFieldType{ + compositeDecoder := &CompositeDecoder{ FieldName: fieldName, } @@ -377,29 +378,31 @@ func (r *factory) getCompactFieldType(meta *types.Metadata, fieldName string, ty itemTypeDef, ok := meta.AsMetadataV14.EfficientLookup[item.Int64()] if !ok { - return nil, fmt.Errorf("type definition for tuple item %d not found", item.Int64()) + return nil, ErrCompactTupleItemTypeNotFound.WithMsg("tuple item '%d'", item.Int64()) } fieldName := fmt.Sprintf(tupleItemFieldNameFormat, i) - itemFieldType, err := r.getCompactFieldType(meta, fieldName, itemTypeDef.Def) + itemFieldDecoder, err := f.getCompactFieldDecoder(meta, fieldName, itemTypeDef.Def) if err != nil { - return nil, fmt.Errorf("couldn't get tuple field type: %w", err) + return nil, ErrCompactTupleItemFieldDecoderRetrieval. + WithMsg("tuple item '%d'", item.Int64()). + Wrap(err) } - compositeFieldType.Fields = append(compositeFieldType.Fields, &Field{ - Name: fieldName, - FieldType: itemFieldType, - LookupIndex: item.Int64(), + compositeDecoder.Fields = append(compositeDecoder.Fields, &Field{ + Name: fieldName, + FieldDecoder: itemFieldDecoder, + LookupIndex: item.Int64(), }) } - return compositeFieldType, nil + return compositeDecoder, nil case typeDef.IsComposite: compactCompositeFields := typeDef.Composite.Fields - compositeFieldType := &CompositeFieldType{ + compositeDecoder := &CompositeDecoder{ FieldName: fieldName, } @@ -407,61 +410,64 @@ func (r *factory) getCompactFieldType(meta *types.Metadata, fieldName string, ty compactCompositeFieldType, ok := meta.AsMetadataV14.EfficientLookup[compactCompositeField.Type.Int64()] if !ok { - return nil, errors.New("compact composite field type not found") + return nil, ErrCompactCompositeFieldTypeNotFound } - compactFieldName := getFieldName(compactCompositeField, compactCompositeFieldType) + compactFieldName := getFullFieldName(compactCompositeField, compactCompositeFieldType) - compactCompositeType, err := r.getCompactFieldType(meta, compactFieldName, compactCompositeFieldType.Def) + compactCompositeDecoder, err := f.getCompactFieldDecoder(meta, compactFieldName, compactCompositeFieldType.Def) if err != nil { - return nil, fmt.Errorf("couldn't decode compact composite type: %w", err) + return nil, ErrCompactCompositeFieldDecoderRetrieval.Wrap(err) } - compositeFieldType.Fields = append(compositeFieldType.Fields, &Field{ - Name: compactFieldName, - FieldType: compactCompositeType, - LookupIndex: compactCompositeField.Type.Int64(), + compositeDecoder.Fields = append(compositeDecoder.Fields, &Field{ + Name: compactFieldName, + FieldDecoder: compactCompositeDecoder, + LookupIndex: compactCompositeField.Type.Int64(), }) } - return compositeFieldType, nil + return compositeDecoder, nil default: return nil, errors.New("unsupported compact field type") } } -// getArrayFieldType parses an array type definition and returns an ArrayFieldType. +// getArrayFieldDecoder parses an array type definition and returns an ArrayDecoder. // nolint:lll -func (r *factory) getArrayFieldType(arrayLen uint, meta *types.Metadata, fieldName string, typeDef types.Si1TypeDef) (FieldType, error) { - itemFieldType, err := r.getFieldType(meta, fieldName, typeDef) +func (f *factory) getArrayFieldDecoder(arrayLen uint, meta *types.Metadata, fieldName string, typeDef types.Si1TypeDef) (FieldDecoder, error) { + itemFieldDecoder, err := f.getFieldDecoder(meta, fieldName, typeDef) if err != nil { - return nil, fmt.Errorf("couldn't get array item field type: %w", err) + return nil, ErrArrayItemFieldDecoderRetrieval.Wrap(err) } - arrayFieldType := &ArrayFieldType{Length: arrayLen, ItemType: itemFieldType} - - return arrayFieldType, nil + return &ArrayDecoder{Length: arrayLen, ItemDecoder: itemFieldDecoder}, nil } -// getSliceFieldType parses a slice type definition and returns an SliceFieldType. -// nolint:lll -func (r *factory) getSliceFieldType(meta *types.Metadata, fieldName string, typeDef types.Si1TypeDef) (FieldType, error) { - itemFieldType, err := r.getFieldType(meta, fieldName, typeDef) +// getSliceFieldDecoder parses a slice type definition and returns an SliceDecoder. +func (f *factory) getSliceFieldDecoder( + meta *types.Metadata, + fieldName string, + typeDef types.Si1TypeDef, +) (FieldDecoder, error) { + itemFieldDecoder, err := f.getFieldDecoder(meta, fieldName, typeDef) if err != nil { - return nil, fmt.Errorf("couldn't get slice item field type: %w", err) + return nil, ErrSliceItemFieldDecoderRetrieval.Wrap(err) } - sliceFieldType := &SliceFieldType{itemFieldType} - - return sliceFieldType, nil + return &SliceDecoder{itemFieldDecoder}, nil } -// getTupleType parses a tuple type definition and returns a CompositeFieldType. -func (r *factory) getTupleType(meta *types.Metadata, fieldName string, tuple types.Si1TypeDefTuple) (FieldType, error) { - compositeFieldType := &CompositeFieldType{ +// getTupleFieldDecoder parses a tuple type definition and returns a CompositeDecoder. +func (f *factory) getTupleFieldDecoder( + meta *types.Metadata, + fieldName string, + tuple types.Si1TypeDefTuple, +) (FieldDecoder, error) { + compositeDecoder := &CompositeDecoder{ FieldName: fieldName, } @@ -469,92 +475,131 @@ func (r *factory) getTupleType(meta *types.Metadata, fieldName string, tuple typ itemTypeDef, ok := meta.AsMetadataV14.EfficientLookup[item.Int64()] if !ok { - return nil, fmt.Errorf("type definition for tuple item %d not found", i) + return nil, ErrTupleItemTypeNotFound.WithMsg("tuple item '%d'", i) } tupleFieldName := fmt.Sprintf(tupleItemFieldNameFormat, i) - itemFieldType, err := r.getFieldType(meta, tupleFieldName, itemTypeDef.Def) + itemFieldDecoder, err := f.getFieldDecoder(meta, tupleFieldName, itemTypeDef.Def) if err != nil { - return nil, fmt.Errorf("couldn't get tuple field type: %w", err) + return nil, ErrTupleItemFieldDecoderRetrieval.Wrap(err) } - compositeFieldType.Fields = append(compositeFieldType.Fields, &Field{ - Name: tupleFieldName, - FieldType: itemFieldType, - LookupIndex: item.Int64(), + compositeDecoder.Fields = append(compositeDecoder.Fields, &Field{ + Name: tupleFieldName, + FieldDecoder: itemFieldDecoder, + LookupIndex: item.Int64(), }) } - return compositeFieldType, nil + return compositeDecoder, nil } -// getPrimitiveType parses a primitive type definition and returns a PrimitiveFieldType. -func getPrimitiveType(primitiveTypeDef types.Si0TypeDefPrimitive) (FieldType, error) { +func (f *factory) getBitSequenceDecoder( + meta *types.Metadata, + fieldName string, + bitSequenceTypeDef types.Si1TypeDefBitSequence, +) (FieldDecoder, error) { + bitStoreType, ok := meta.AsMetadataV14.EfficientLookup[bitSequenceTypeDef.BitStoreType.Int64()] + + if !ok { + return nil, ErrBitStoreTypeNotFound.WithMsg(fieldName) + } + + if bitStoreType.Def.Primitive.Si0TypeDefPrimitive != types.IsU8 { + return nil, ErrBitStoreTypeNotSupported.WithMsg(fieldName) + } + + bitOrderType, ok := meta.AsMetadataV14.EfficientLookup[bitSequenceTypeDef.BitOrderType.Int64()] + + if !ok { + return nil, ErrBitOrderTypeNotFound.WithMsg(fieldName) + } + + bitOrder, err := types.NewBitOrderFromString(getBitOrderString(bitOrderType.Path)) + + if err != nil { + return nil, ErrBitOrderCreation.Wrap(err) + } + + bitSequenceDecoder := &BitSequenceDecoder{ + FieldName: fieldName, + BitOrder: bitOrder, + } + + return bitSequenceDecoder, nil +} + +func getBitOrderString(path types.Si1Path) string { + pathLen := len(path) + + if pathLen == 0 { + return "" + } + + return string(path[pathLen-1]) +} + +// getPrimitiveDecoder parses a primitive type definition and returns a ValueDecoder. +func getPrimitiveDecoder(primitiveTypeDef types.Si0TypeDefPrimitive) (FieldDecoder, error) { switch primitiveTypeDef { case types.IsBool: - return &PrimitiveFieldType[bool]{}, nil + return &ValueDecoder[bool]{}, nil case types.IsChar: - return &PrimitiveFieldType[byte]{}, nil + return &ValueDecoder[byte]{}, nil case types.IsStr: - return &PrimitiveFieldType[string]{}, nil + return &ValueDecoder[string]{}, nil case types.IsU8: - return &PrimitiveFieldType[types.U8]{}, nil + return &ValueDecoder[types.U8]{}, nil case types.IsU16: - return &PrimitiveFieldType[types.U16]{}, nil + return &ValueDecoder[types.U16]{}, nil case types.IsU32: - return &PrimitiveFieldType[types.U32]{}, nil + return &ValueDecoder[types.U32]{}, nil case types.IsU64: - return &PrimitiveFieldType[types.U64]{}, nil + return &ValueDecoder[types.U64]{}, nil case types.IsU128: - return &PrimitiveFieldType[types.U128]{}, nil + return &ValueDecoder[types.U128]{}, nil case types.IsU256: - return &PrimitiveFieldType[types.U256]{}, nil + return &ValueDecoder[types.U256]{}, nil case types.IsI8: - return &PrimitiveFieldType[types.I8]{}, nil + return &ValueDecoder[types.I8]{}, nil case types.IsI16: - return &PrimitiveFieldType[types.I16]{}, nil + return &ValueDecoder[types.I16]{}, nil case types.IsI32: - return &PrimitiveFieldType[types.I32]{}, nil + return &ValueDecoder[types.I32]{}, nil case types.IsI64: - return &PrimitiveFieldType[types.I64]{}, nil + return &ValueDecoder[types.I64]{}, nil case types.IsI128: - return &PrimitiveFieldType[types.I128]{}, nil + return &ValueDecoder[types.I128]{}, nil case types.IsI256: - return &PrimitiveFieldType[types.I256]{}, nil + return &ValueDecoder[types.I256]{}, nil default: - return nil, fmt.Errorf("unsupported primitive type %v", primitiveTypeDef) + return nil, ErrPrimitiveTypeNotSupported.WithMsg("primitive type %v", primitiveTypeDef) } } -// getStoredFieldType will attempt to return a field type from storage, and perform an extra check for recursive types. -func (r *factory) getStoredFieldType(fieldName string, fieldType int64) (FieldType, bool) { - if ft, ok := r.fieldStorage[fieldType]; ok { - if rt, ok := ft.(*RecursiveFieldType); ok { - r.recursiveFieldStorage[fieldType] = rt +// getStoredFieldDecoder will attempt to return a FieldDecoder from storage, +// and perform an extra check for recursive decoders. +func (f *factory) getStoredFieldDecoder(fieldLookupIndex int64) (FieldDecoder, bool) { + if ft, ok := f.fieldStorage[fieldLookupIndex]; ok { + if rt, ok := ft.(*RecursiveDecoder); ok { + f.recursiveFieldStorage[fieldLookupIndex] = rt } return ft, ok } // Ensure that a recursive type such as Xcm::TransferReserveAsset does not cause an infinite loop - // by adding the RecursiveFieldType the first time the field is encountered. - r.fieldStorage[fieldType] = &RecursiveFieldType{ - FieldName: fieldName, - } + // by adding the RecursiveDecoder the first time the field is encountered. + f.fieldStorage[fieldLookupIndex] = &RecursiveDecoder{} return nil, false } -func (r *factory) storeFieldType(fieldType int64, registryFieldType FieldType) { - r.fieldStorage[fieldType] = registryFieldType -} - const ( - unknownFieldName = "unknown_field_name" - - fieldPathSeparator = "::" + fieldSeparator = "." + lookupIndexFormat = "lookup_index_%d" ) func getFieldPath(fieldType *types.Si1Type) string { @@ -564,21 +609,27 @@ func getFieldPath(fieldType *types.Si1Type) string { nameParts = append(nameParts, string(pathEntry)) } - return strings.Join(nameParts, fieldPathSeparator) + return strings.Join(nameParts, fieldSeparator) } -func getFieldName(field types.Si1Field, fieldType *types.Si1Type) string { +func getFullFieldName(field types.Si1Field, fieldType *types.Si1Type) string { + fieldName := getFieldName(field) + if fieldPath := getFieldPath(fieldType); fieldPath != "" { - return fieldPath + return fmt.Sprintf("%s%s%s", fieldPath, fieldSeparator, fieldName) } + return getFieldName(field) +} + +func getFieldName(field types.Si1Field) string { switch { case field.HasName: return string(field.Name) case field.HasTypeName: return string(field.TypeName) default: - return unknownFieldName + return fmt.Sprintf(lookupIndexFormat, field.Type.Int64()) } } @@ -588,142 +639,186 @@ type Type struct { Fields []*Field } +func (t *Type) Decode(decoder *scale.Decoder) (map[string]any, error) { + fieldMap := make(map[string]any) + + for _, field := range t.Fields { + value, err := field.FieldDecoder.Decode(decoder) + + if err != nil { + return nil, ErrTypeFieldDecoding.Wrap(err) + } + + fieldMap[field.Name] = value + } + + return fieldMap, nil +} + // Field represents one field of a Type. type Field struct { - Name string - FieldType FieldType - LookupIndex int64 + Name string + FieldDecoder FieldDecoder + LookupIndex int64 } -func (f *Field) GetFieldMap() (map[string]any, error) { - fieldType, err := f.FieldType.GetFieldTypeString() +// FieldDecoder is the interface implemented by all the different types that are available. +type FieldDecoder interface { + Decode(decoder *scale.Decoder) (any, error) +} - if err != nil { - return nil, fmt.Errorf("couldn't get field type: %w", err) - } +// NoopDecoder is a FieldDecoder that does not decode anything. It comes in handy for nil tuples or variants +// with no inner types. +type NoopDecoder struct{} - fieldMap := map[string]any{ - "field_name": f.Name, - "field_type": fieldType, - "lookup_index": f.LookupIndex, - } +func (n *NoopDecoder) Decode(_ *scale.Decoder) (any, error) { + return nil, nil +} - return fieldMap, nil +// VariantDecoder holds a FieldDecoder for each variant/enum. +type VariantDecoder struct { + FieldDecoderMap map[byte]FieldDecoder } -func (f *Field) MarshalJSON() ([]byte, error) { - fieldMap, err := f.GetFieldMap() +func (v *VariantDecoder) Decode(decoder *scale.Decoder) (any, error) { + variantByte, err := decoder.ReadOneByte() if err != nil { - return nil, err + return nil, ErrVariantByteDecoding.Wrap(err) } - return json.Marshal(fieldMap) -} + variantDecoder, ok := v.FieldDecoderMap[variantByte] -// FieldType is the interface implemented by all the different types that are available. -type FieldType interface { - GetFieldTypeString() (string, error) -} + if !ok { + return nil, ErrVariantFieldDecoderNotFound.WithMsg("variant '%d'", variantByte) + } -// VariantFieldType represents an enum. -type VariantFieldType struct { - FieldTypeMap map[byte]FieldType -} + if _, ok := variantDecoder.(*NoopDecoder); ok { + return variantByte, nil + } -func (v *VariantFieldType) GetFieldTypeString() (string, error) { - return "enum", nil + return variantDecoder.Decode(decoder) } -// ArrayFieldType holds information about the length of the array and the type of its items. -type ArrayFieldType struct { - Length uint - ItemType FieldType +// ArrayDecoder holds information about the length of the array and the FieldDecoder used for its items. +type ArrayDecoder struct { + Length uint + ItemDecoder FieldDecoder } -func (a *ArrayFieldType) GetFieldTypeString() (string, error) { - arrayItemTypeString, err := a.ItemType.GetFieldTypeString() +func (a *ArrayDecoder) Decode(decoder *scale.Decoder) (any, error) { + if a.ItemDecoder == nil { + return nil, ErrArrayItemDecoderNotFound + } - if err != nil { - return "", err + slice := make([]any, 0, a.Length) + + for i := uint(0); i < a.Length; i++ { + item, err := a.ItemDecoder.Decode(decoder) + + if err != nil { + return nil, ErrArrayItemDecoding.Wrap(err) + } + + slice = append(slice, item) } - return fmt.Sprintf("[%d]%s", a.Length, arrayItemTypeString), nil + return slice, nil } -// SliceFieldType represents a vector. -type SliceFieldType struct { - ItemType FieldType +// SliceDecoder holds a FieldDecoder for the items of a vector/slice. +type SliceDecoder struct { + ItemDecoder FieldDecoder } -func (s *SliceFieldType) GetFieldTypeString() (string, error) { - sliceItemTypeString, err := s.ItemType.GetFieldTypeString() +func (s *SliceDecoder) Decode(decoder *scale.Decoder) (any, error) { + if s.ItemDecoder == nil { + return nil, ErrSliceItemDecoderNotFound + } + + sliceLen, err := decoder.DecodeUintCompact() if err != nil { - return "", err + return nil, ErrSliceLengthDecoding.Wrap(err) + } + + slice := make([]any, 0, sliceLen.Uint64()) + + for i := uint64(0); i < sliceLen.Uint64(); i++ { + item, err := s.ItemDecoder.Decode(decoder) + + if err != nil { + return nil, ErrSliceItemDecoding.Wrap(err) + } + + slice = append(slice, item) } - return fmt.Sprintf("[]%s", sliceItemTypeString), nil + return slice, nil } -// CompositeFieldType represents a struct. -type CompositeFieldType struct { +// CompositeDecoder holds all the information required to decoder a struct/composite. +type CompositeDecoder struct { FieldName string Fields []*Field } -func (c *CompositeFieldType) GetFieldTypeString() (string, error) { - return "struct", nil -} +func (e *CompositeDecoder) Decode(decoder *scale.Decoder) (any, error) { + fieldMap := make(map[string]any) -// PrimitiveFieldType holds a primitive type. -type PrimitiveFieldType[T any] struct{} + for _, field := range e.Fields { + value, err := field.FieldDecoder.Decode(decoder) -func (f *PrimitiveFieldType[T]) GetFieldTypeString() (string, error) { - var t T - return fmt.Sprintf("%T", t), nil -} + if err != nil { + return nil, ErrCompositeFieldDecoding.Wrap(err) + } -// RecursiveFieldType is a wrapper for a FieldType that is recursive. -type RecursiveFieldType struct { - depth int + fieldMap[field.Name] = value + } - FieldName string - ResolvedItemType FieldType + return fieldMap, nil } -func (r *RecursiveFieldType) GetFieldTypeString() (string, error) { - if r.ResolvedItemType == nil { - return "", fmt.Errorf("recursive type not resolved") - } +// ValueDecoder decodes a primitive type. +type ValueDecoder[T any] struct{} - if r.depth > 0 { - return "recursive", nil - } +func (v *ValueDecoder[T]) Decode(decoder *scale.Decoder) (any, error) { + var t T - r.depth++ + if err := decoder.Decode(&t); err != nil { + return nil, ErrValueDecoding.Wrap(err) + } - return r.ResolvedItemType.GetFieldTypeString() + return t, nil } -// BitSequenceType represents a bit sequence. -type BitSequenceType struct { - BitStoreType FieldType - BitOrderType FieldType +// RecursiveDecoder is a wrapper for a FieldDecoder that is recursive. +type RecursiveDecoder struct { + FieldDecoder FieldDecoder } -func (b *BitSequenceType) GetFieldTypeString() (string, error) { - bitStoreFieldType, err := b.BitStoreType.GetFieldTypeString() - - if err != nil { - return "", err +func (r *RecursiveDecoder) Decode(decoder *scale.Decoder) (any, error) { + if r.FieldDecoder == nil { + return nil, ErrRecursiveFieldDecoderNotFound } - bitOrderFieldType, err := b.BitOrderType.GetFieldTypeString() + return r.FieldDecoder.Decode(decoder) +} - if err != nil { - return "", err +// BitSequenceDecoder holds decoding information for a bit sequence. +type BitSequenceDecoder struct { + FieldName string + BitOrder types.BitOrder +} + +func (b *BitSequenceDecoder) Decode(decoder *scale.Decoder) (any, error) { + bitVec := types.NewBitVec(b.BitOrder) + + if err := bitVec.Decode(*decoder); err != nil { + return nil, ErrBitVecDecoding.Wrap(err) } - return fmt.Sprintf("%s.%s", bitStoreFieldType, bitOrderFieldType), nil + return map[string]string{ + b.FieldName: bitVec.String(), + }, nil } diff --git a/registry/registry_test.go b/registry/registry_test.go index 83363deda..4a48c74d9 100644 --- a/registry/registry_test.go +++ b/registry/registry_test.go @@ -97,7 +97,6 @@ func TestFactory_CreateErrorRegistry_NoPalletWithErrors(t *testing.T) { } func TestFactory_CreateErrorRegistry_ErrorsTypeNotFound(t *testing.T) { - testModuleName := "TestModule" errorLookupTypeID := 123 testMeta := &types.Metadata{ @@ -121,12 +120,11 @@ func TestFactory_CreateErrorRegistry_ErrorsTypeNotFound(t *testing.T) { factory := NewFactory() reg, err := factory.CreateErrorRegistry(testMeta) - assert.Equal(t, fmt.Sprintf("errors type %d not found for module '%s'", errorLookupTypeID, testModuleName), err.Error()) + assert.ErrorIs(t, err, ErrErrorsTypeNotFound) assert.Empty(t, reg) } func TestFactory_CreateErrorRegistry_ErrorsTypeNotAVariant(t *testing.T) { - testModuleName := "TestModule" errorLookupTypeID := 123 testMeta := &types.Metadata{ @@ -156,7 +154,7 @@ func TestFactory_CreateErrorRegistry_ErrorsTypeNotAVariant(t *testing.T) { factory := NewFactory() reg, err := factory.CreateErrorRegistry(testMeta) - assert.Equal(t, fmt.Sprintf("errors type %d for module '%s' is not a variant", errorLookupTypeID, testModuleName), err.Error()) + assert.ErrorIs(t, err, ErrErrorsTypeNotVariant) assert.Empty(t, reg) } @@ -207,7 +205,7 @@ func TestFactory_CreateErrorRegistry_GetTypeFieldsError(t *testing.T) { factory := NewFactory() reg, err := factory.CreateErrorRegistry(testMeta) - assert.Equal(t, "couldn't get fields for error 'TestModule.ErrorVariant1': type not found for field 'ErrorVariant1Field'", err.Error()) + assert.ErrorIs(t, err, ErrErrorFieldsRetrieval) assert.Empty(t, reg) } @@ -267,9 +265,14 @@ func TestFactory_CreateCallRegistryWithLiveMetadata(t *testing.T) { assert.True(t, callsType.Def.IsVariant, fmt.Sprintf("Calls type %d not a variant", pallet.Events.Type.Int64())) for _, callVariant := range callsType.Def.Variant.Variants { + callIndex := types.CallIndex{ + SectionIndex: uint8(pallet.Index), + MethodIndex: uint8(callVariant.Index), + } + callName := fmt.Sprintf("%s.%s", pallet.Name, callVariant.Name) - registryCallType, ok := reg[callName] + registryCallType, ok := reg[callIndex] assert.True(t, ok, fmt.Sprintf("Call '%s' not found in registry", callName)) testAsserter.assertRegistryItemContainsAllTypes(t, meta, registryCallType.Fields, callVariant.Fields) @@ -298,7 +301,6 @@ func TestFactory_CreateCallRegistry_NoPalletWithCalls(t *testing.T) { } func TestFactory_CreateCallRegistry_CallsTypeNotFound(t *testing.T) { - testModuleName := "TestModule" callLookupTypeID := 123 testMeta := &types.Metadata{ @@ -322,12 +324,11 @@ func TestFactory_CreateCallRegistry_CallsTypeNotFound(t *testing.T) { factory := NewFactory() reg, err := factory.CreateCallRegistry(testMeta) - assert.Equal(t, fmt.Sprintf("calls type %d not found for module '%s'", callLookupTypeID, testModuleName), err.Error()) + assert.ErrorIs(t, err, ErrCallsTypeNotFound) assert.Empty(t, reg) } func TestFactory_CreateCallRegistry_CallTypeNotAVariant(t *testing.T) { - testModuleName := "TestModule" callLookupTypeID := 123 testMeta := &types.Metadata{ @@ -357,7 +358,7 @@ func TestFactory_CreateCallRegistry_CallTypeNotAVariant(t *testing.T) { factory := NewFactory() reg, err := factory.CreateCallRegistry(testMeta) - assert.Equal(t, fmt.Sprintf("calls type %d for module '%s' is not a variant", callLookupTypeID, testModuleName), err.Error()) + assert.ErrorIs(t, err, ErrCallsTypeNotVariant) assert.Empty(t, reg) } @@ -408,7 +409,7 @@ func TestFactory_CreateCallRegistry_GetTypeFieldsError(t *testing.T) { factory := NewFactory() reg, err := factory.CreateCallRegistry(testMeta) - assert.Equal(t, "couldn't get fields for call 'TestModule.CallVariant1': type not found for field 'CallVariant1Field'", err.Error()) + assert.ErrorIs(t, err, ErrCallFieldsRetrieval) assert.Empty(t, reg) } @@ -499,7 +500,6 @@ func TestFactory_CreateEventRegistry_NoPalletWithEvents(t *testing.T) { } func TestFactory_CreateEventRegistry_EventsTypeNotFound(t *testing.T) { - testModuleName := "TestModule" eventLookupTypeID := 123 testMeta := &types.Metadata{ @@ -523,12 +523,11 @@ func TestFactory_CreateEventRegistry_EventsTypeNotFound(t *testing.T) { factory := NewFactory() reg, err := factory.CreateEventRegistry(testMeta) - assert.Equal(t, fmt.Sprintf("events type %d not found for module '%s'", eventLookupTypeID, testModuleName), err.Error()) + assert.ErrorIs(t, err, ErrEventsTypeNotFound) assert.Empty(t, reg) } func TestFactory_CreateEventRegistry_EventTypeNotAVariant(t *testing.T) { - testModuleName := "TestModule" callLookupTypeID := 123 testMeta := &types.Metadata{ @@ -558,7 +557,7 @@ func TestFactory_CreateEventRegistry_EventTypeNotAVariant(t *testing.T) { factory := NewFactory() reg, err := factory.CreateEventRegistry(testMeta) - assert.Equal(t, fmt.Sprintf("events type %d for module '%s' is not a variant", callLookupTypeID, testModuleName), err.Error()) + assert.ErrorIs(t, err, ErrEventsTypeNotVariant) assert.Empty(t, reg) } @@ -609,7 +608,7 @@ func TestFactory_CreateEventRegistry_GetTypeFieldError(t *testing.T) { factory := NewFactory() reg, err := factory.CreateEventRegistry(testMeta) - assert.Equal(t, "couldn't get fields for event 'TestModule.EventVariant1': type not found for field 'EventVariant1Field'", err.Error()) + assert.ErrorIs(t, err, ErrEventFieldsRetrieval) assert.Empty(t, reg) } @@ -656,17 +655,18 @@ func TestFactory_getTypeFields(t *testing.T) { } factory := NewFactory().(*factory) + factory.initStorages() res, err := factory.getTypeFields(testMeta, testFields) assert.NoError(t, err) assert.Len(t, res, 1) assert.Equal(t, testFieldName, res[0].Name) - assert.Equal(t, &PrimitiveFieldType[types.UCompact]{}, res[0].FieldType) + assert.Equal(t, &ValueDecoder[types.UCompact]{}, res[0].FieldDecoder) assert.Equal(t, int64(fieldLookUpID), res[0].LookupIndex) } -func TestFactory_getTypeFields_FieldTypeError(t *testing.T) { +func TestFactory_getTypeFields_FieldDecoderRetrievalError(t *testing.T) { fieldLookUpID := 123 testFieldName := "TestFieldName" @@ -709,9 +709,10 @@ func TestFactory_getTypeFields_FieldTypeError(t *testing.T) { } factory := NewFactory().(*factory) + factory.initStorages() res, err := factory.getTypeFields(testMeta, testFields) - assert.Equal(t, "couldn't get field type for 'TestFieldName': couldn't get composite fields: type not found for field 'CompositeField1'", err.Error()) + assert.ErrorIs(t, err, ErrFieldDecoderRetrieval) assert.Nil(t, res) } @@ -739,11 +740,11 @@ func TestFactory_getTypeFields_FieldTypeNotFoundError(t *testing.T) { factory := NewFactory().(*factory) res, err := factory.getTypeFields(testMeta, testFields) - assert.Equal(t, fmt.Sprintf("type not found for field '%s'", testFieldName), err.Error()) + assert.ErrorIs(t, err, ErrFieldTypeNotFound) assert.Nil(t, res) } -func TestFactory_getFieldType_UnsupportedTypeError(t *testing.T) { +func TestFactory_getFieldDecoder_UnsupportedTypeError(t *testing.T) { testFieldName := "TestFieldName" testFieldTypeDef := types.Si1TypeDef{ @@ -754,12 +755,12 @@ func TestFactory_getFieldType_UnsupportedTypeError(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getFieldType(testMeta, testFieldName, testFieldTypeDef) - assert.Equal(t, "unsupported field type definition", err.Error()) + res, err := factory.getFieldDecoder(testMeta, testFieldName, testFieldTypeDef) + assert.ErrorIs(t, err, ErrFieldTypeDefinitionNotSupported) assert.Nil(t, res) } -func TestFactory_getFieldType_Compact(t *testing.T) { +func TestFactory_getFieldDecoder_Compact(t *testing.T) { testFieldName := "TestFieldName" compactFieldTypeLookupID := 456 @@ -789,12 +790,12 @@ func TestFactory_getFieldType_Compact(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getFieldType(testMeta, testFieldName, testFieldTypeDef) + res, err := factory.getFieldDecoder(testMeta, testFieldName, testFieldTypeDef) assert.NoError(t, err) - assert.Equal(t, &PrimitiveFieldType[types.UCompact]{}, res) + assert.Equal(t, &ValueDecoder[types.UCompact]{}, res) } -func TestFactory_getFieldType_Compact_TypeNotFoundError(t *testing.T) { +func TestFactory_getFieldDecoder_Compact_TypeNotFoundError(t *testing.T) { testFieldName := "TestFieldName" compactFieldTypeLookupID := 456 @@ -816,12 +817,12 @@ func TestFactory_getFieldType_Compact_TypeNotFoundError(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getFieldType(testMeta, testFieldName, testFieldTypeDef) - assert.Equal(t, "type not found for compact field", err.Error()) + res, err := factory.getFieldDecoder(testMeta, testFieldName, testFieldTypeDef) + assert.ErrorIs(t, err, ErrCompactFieldTypeNotFound) assert.Nil(t, res) } -func TestFactory_getFieldType_Composite(t *testing.T) { +func TestFactory_getFieldDecoder_Composite(t *testing.T) { testFieldName := "TestFieldName" compositeFieldTypeLookupID1 := 123 @@ -881,24 +882,25 @@ func TestFactory_getFieldType_Composite(t *testing.T) { } factory := NewFactory().(*factory) + factory.initStorages() - res, err := factory.getFieldType(testMeta, testFieldName, testFieldTypeDef) + res, err := factory.getFieldDecoder(testMeta, testFieldName, testFieldTypeDef) assert.NoError(t, err) - compositeFieldType, ok := res.(*CompositeFieldType) + compositeFieldType, ok := res.(*CompositeDecoder) assert.True(t, ok) assert.Len(t, compositeFieldType.Fields, 2) - assert.Equal(t, &PrimitiveFieldType[types.U8]{}, compositeFieldType.Fields[0].FieldType) + assert.Equal(t, &ValueDecoder[types.U8]{}, compositeFieldType.Fields[0].FieldDecoder) assert.Equal(t, compositeFieldName1, compositeFieldType.Fields[0].Name) assert.Equal(t, int64(compositeFieldTypeLookupID1), compositeFieldType.Fields[0].LookupIndex) - assert.Equal(t, &PrimitiveFieldType[types.I8]{}, compositeFieldType.Fields[1].FieldType) + assert.Equal(t, &ValueDecoder[types.I8]{}, compositeFieldType.Fields[1].FieldDecoder) assert.Equal(t, compositeFieldName2, compositeFieldType.Fields[1].Name) assert.Equal(t, int64(compositeFieldTypeLookupID2), compositeFieldType.Fields[1].LookupIndex) } -func TestFactory_getFieldType_Composite_FieldError(t *testing.T) { +func TestFactory_getFieldDecoder_Composite_FieldError(t *testing.T) { testFieldName := "TestFieldName" compositeFieldTypeLookupID1 := 123 @@ -949,13 +951,14 @@ func TestFactory_getFieldType_Composite_FieldError(t *testing.T) { } factory := NewFactory().(*factory) + factory.initStorages() - res, err := factory.getFieldType(testMeta, testFieldName, testFieldTypeDef) - assert.Equal(t, fmt.Sprintf("couldn't get composite fields: type not found for field '%s'", compositeFieldName2), err.Error()) + res, err := factory.getFieldDecoder(testMeta, testFieldName, testFieldTypeDef) + assert.ErrorIs(t, err, ErrCompositeTypeFieldsRetrieval) assert.Nil(t, res) } -func TestFactory_getFieldType_Variant(t *testing.T) { +func TestFactory_getFieldDecoder_Variant(t *testing.T) { testFieldName := "TestField" variantName1 := "Variant1" @@ -1007,26 +1010,27 @@ func TestFactory_getFieldType_Variant(t *testing.T) { } factory := NewFactory().(*factory) + factory.initStorages() - res, err := factory.getFieldType(testMeta, testFieldName, testFieldTypeDef) + res, err := factory.getFieldDecoder(testMeta, testFieldName, testFieldTypeDef) assert.NoError(t, err) - variantFieldType, ok := res.(*VariantFieldType) + variantFieldType, ok := res.(*VariantDecoder) assert.True(t, ok) - assert.Len(t, variantFieldType.FieldTypeMap, 2) + assert.Len(t, variantFieldType.FieldDecoderMap, 2) - assert.Equal(t, &PrimitiveFieldType[byte]{}, variantFieldType.FieldTypeMap[0]) + assert.Equal(t, &NoopDecoder{}, variantFieldType.FieldDecoderMap[0]) - compositeVariant, ok := variantFieldType.FieldTypeMap[1].(*CompositeFieldType) + compositeVariant, ok := variantFieldType.FieldDecoderMap[1].(*CompositeDecoder) assert.True(t, ok) assert.Len(t, compositeVariant.Fields, 1) assert.Equal(t, variantFieldName, compositeVariant.Fields[0].Name) - assert.Equal(t, &PrimitiveFieldType[types.U8]{}, compositeVariant.Fields[0].FieldType) + assert.Equal(t, &ValueDecoder[types.U8]{}, compositeVariant.Fields[0].FieldDecoder) assert.Equal(t, int64(variantFieldLookupID), compositeVariant.Fields[0].LookupIndex) } -func TestFactory_getFieldType_Primitive(t *testing.T) { +func TestFactory_getFieldDecoder_Primitive(t *testing.T) { testFieldName := "TestFieldName" testFieldTypeDef := types.Si1TypeDef{ @@ -1040,12 +1044,12 @@ func TestFactory_getFieldType_Primitive(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getFieldType(testMeta, testFieldName, testFieldTypeDef) + res, err := factory.getFieldDecoder(testMeta, testFieldName, testFieldTypeDef) assert.NoError(t, err) - assert.Equal(t, &PrimitiveFieldType[types.U8]{}, res) + assert.Equal(t, &ValueDecoder[types.U8]{}, res) } -func TestFactory_getFieldType_Array(t *testing.T) { +func TestFactory_getFieldDecoder_Array(t *testing.T) { testFieldName := "TestFieldName" arrayItemTypeLookupID := 456 @@ -1080,17 +1084,17 @@ func TestFactory_getFieldType_Array(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getFieldType(testMeta, testFieldName, testFieldTypeDef) + res, err := factory.getFieldDecoder(testMeta, testFieldName, testFieldTypeDef) assert.NoError(t, err) - arrayFieldType, ok := res.(*ArrayFieldType) + arrayFieldType, ok := res.(*ArrayDecoder) assert.True(t, ok) assert.Equal(t, uint(arrayLen), arrayFieldType.Length) - assert.Equal(t, &PrimitiveFieldType[types.U8]{}, arrayFieldType.ItemType) + assert.Equal(t, &ValueDecoder[types.U8]{}, arrayFieldType.ItemDecoder) } -func TestFactory_getFieldType_Array_TypeNotFoundError(t *testing.T) { +func TestFactory_getFieldDecoder_Array_TypeNotFoundError(t *testing.T) { testFieldName := "TestFieldName" arrayItemTypeLookupID := 456 @@ -1115,12 +1119,12 @@ func TestFactory_getFieldType_Array_TypeNotFoundError(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getFieldType(testMeta, testFieldName, testFieldTypeDef) - assert.Equal(t, "type not found for array field", err.Error()) + res, err := factory.getFieldDecoder(testMeta, testFieldName, testFieldTypeDef) + assert.ErrorIs(t, err, ErrArrayFieldTypeNotFound) assert.Nil(t, res) } -func TestFactory_getFieldType_Slice(t *testing.T) { +func TestFactory_getFieldDecoder_Slice(t *testing.T) { testFieldName := "TestFieldName" sliceItemTypeLookupID := 456 @@ -1153,16 +1157,16 @@ func TestFactory_getFieldType_Slice(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getFieldType(testMeta, testFieldName, testFieldTypeDef) + res, err := factory.getFieldDecoder(testMeta, testFieldName, testFieldTypeDef) assert.NoError(t, err) - sliceFieldType, ok := res.(*SliceFieldType) + sliceFieldType, ok := res.(*SliceDecoder) assert.True(t, ok) - assert.Equal(t, &PrimitiveFieldType[types.U256]{}, sliceFieldType.ItemType) + assert.Equal(t, &ValueDecoder[types.U256]{}, sliceFieldType.ItemDecoder) } -func TestFactory_getFieldType_Slice_TypeNotFoundError(t *testing.T) { +func TestFactory_getFieldDecoder_Slice_TypeNotFoundError(t *testing.T) { testFieldName := "TestFieldName" sliceItemTypeLookupID := 456 @@ -1185,12 +1189,12 @@ func TestFactory_getFieldType_Slice_TypeNotFoundError(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getFieldType(testMeta, testFieldName, testFieldTypeDef) - assert.Equal(t, "type not found for vector field", err.Error()) + res, err := factory.getFieldDecoder(testMeta, testFieldName, testFieldTypeDef) + assert.ErrorIs(t, err, ErrVectorFieldTypeNotFound) assert.Nil(t, res) } -func TestFactory_getFieldType_Tuple(t *testing.T) { +func TestFactory_getFieldDecoder_Tuple(t *testing.T) { testFieldName := "TestFieldName" tupleItemLookupID1 := 123 @@ -1237,24 +1241,24 @@ func TestFactory_getFieldType_Tuple(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getFieldType(testMeta, testFieldName, testFieldTypeDef) + res, err := factory.getFieldDecoder(testMeta, testFieldName, testFieldTypeDef) assert.NoError(t, err) - compositeFieldType, ok := res.(*CompositeFieldType) + compositeFieldType, ok := res.(*CompositeDecoder) assert.True(t, ok) assert.Len(t, compositeFieldType.Fields, 2) assert.Equal(t, testFieldName, compositeFieldType.FieldName) assert.Equal(t, fmt.Sprintf(tupleItemFieldNameFormat, 0), compositeFieldType.Fields[0].Name) - assert.Equal(t, &PrimitiveFieldType[byte]{}, compositeFieldType.Fields[0].FieldType) + assert.Equal(t, &ValueDecoder[byte]{}, compositeFieldType.Fields[0].FieldDecoder) assert.Equal(t, int64(tupleItemLookupID1), compositeFieldType.Fields[0].LookupIndex) assert.Equal(t, fmt.Sprintf(tupleItemFieldNameFormat, 1), compositeFieldType.Fields[1].Name) - assert.Equal(t, &PrimitiveFieldType[types.I16]{}, compositeFieldType.Fields[1].FieldType) + assert.Equal(t, &ValueDecoder[types.I16]{}, compositeFieldType.Fields[1].FieldDecoder) assert.Equal(t, int64(tupleItemLookupID2), compositeFieldType.Fields[1].LookupIndex) } -func TestFactory_getFieldType_Tuple_NilTuple(t *testing.T) { +func TestFactory_getFieldDecoder_Tuple_NilTuple(t *testing.T) { testFieldName := "TestFieldName" testFieldTypeDef := types.Si1TypeDef{ @@ -1269,12 +1273,12 @@ func TestFactory_getFieldType_Tuple_NilTuple(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getFieldType(testMeta, testFieldName, testFieldTypeDef) + res, err := factory.getFieldDecoder(testMeta, testFieldName, testFieldTypeDef) assert.NoError(t, err) - assert.Equal(t, &PrimitiveFieldType[[]any]{}, res) + assert.Equal(t, &NoopDecoder{}, res) } -func TestFactory_getFieldType_BitSequence(t *testing.T) { +func TestFactory_getFieldDecoder_BitSequence(t *testing.T) { testFieldName := "TestFieldName" bitStoreLookupID := 123 @@ -1295,14 +1299,13 @@ func TestFactory_getFieldType_BitSequence(t *testing.T) { bitStoreTypeDef := types.Si1TypeDef{ IsPrimitive: true, Primitive: types.Si1TypeDefPrimitive{ - Si0TypeDefPrimitive: types.Si0TypeDefPrimitive(types.IsI64), + Si0TypeDefPrimitive: types.Si0TypeDefPrimitive(types.IsU8), }, } - bitOrderTypeDef := types.Si1TypeDef{ - IsPrimitive: true, - Primitive: types.Si1TypeDefPrimitive{ - Si0TypeDefPrimitive: types.Si0TypeDefPrimitive(types.IsI256), + bitOrderType := &types.Si1Type{ + Path: []types.Text{ + types.Text(types.BitOrderName[types.BitOrderLsb0]), }, } @@ -1312,26 +1315,24 @@ func TestFactory_getFieldType_BitSequence(t *testing.T) { int64(bitStoreLookupID): { Def: bitStoreTypeDef, }, - int64(bitOrderLookupID): { - Def: bitOrderTypeDef, - }, + int64(bitOrderLookupID): bitOrderType, }, }, } factory := NewFactory().(*factory) - res, err := factory.getFieldType(testMeta, testFieldName, testFieldTypeDef) + res, err := factory.getFieldDecoder(testMeta, testFieldName, testFieldTypeDef) assert.NoError(t, err) - bitSequenceType, ok := res.(*BitSequenceType) + bitSequenceDecoder, ok := res.(*BitSequenceDecoder) assert.True(t, ok) - assert.Equal(t, &PrimitiveFieldType[types.I64]{}, bitSequenceType.BitStoreType) - assert.Equal(t, &PrimitiveFieldType[types.I256]{}, bitSequenceType.BitOrderType) + assert.Equal(t, testFieldName, bitSequenceDecoder.FieldName) + assert.Equal(t, types.BitOrderLsb0, bitSequenceDecoder.BitOrder) } -func TestFactory_getFieldType_BitSequence_BitStoreTypeNotFound(t *testing.T) { +func TestFactory_getFieldDecoder_BitSequence_BitStoreTypeNotFound(t *testing.T) { testFieldName := "TestFieldName" bitStoreLookupID := 123 @@ -1368,12 +1369,12 @@ func TestFactory_getFieldType_BitSequence_BitStoreTypeNotFound(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getFieldType(testMeta, testFieldName, testFieldTypeDef) - assert.Equal(t, "bit store type not found", err.Error()) + res, err := factory.getFieldDecoder(testMeta, testFieldName, testFieldTypeDef) + assert.ErrorIs(t, err, ErrBitStoreTypeNotFound) assert.Nil(t, res) } -func TestFactory_getFieldType_BitSequence_BitStoreFieldTypeError(t *testing.T) { +func TestFactory_getFieldDecoder_BitSequence_BitStoreFieldTypeError(t *testing.T) { testFieldName := "TestFieldName" bitStoreLookupID := 123 @@ -1392,17 +1393,9 @@ func TestFactory_getFieldType_BitSequence_BitStoreFieldTypeError(t *testing.T) { } bitStoreTypeDef := types.Si1TypeDef{ - IsComposite: true, - Composite: types.Si1TypeDefComposite{ - Fields: []types.Si1Field{ - { - Name: "BitStoreCompositeField1", - Type: types.Si1LookupTypeID{ - // This type is not present in the efficient lookup map and should cause an error. - UCompact: types.NewUCompactFromUInt(uint64(123456)), - }, - }, - }, + IsPrimitive: true, + Primitive: types.Si1TypeDefPrimitive{ + Si0TypeDefPrimitive: types.Si0TypeDefPrimitive(types.IsU16), }, } @@ -1428,12 +1421,12 @@ func TestFactory_getFieldType_BitSequence_BitStoreFieldTypeError(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getFieldType(testMeta, testFieldName, testFieldTypeDef) - assert.Equal(t, "couldn't get bit store field type: couldn't get composite fields: type not found for field 'BitStoreCompositeField1'", err.Error()) + res, err := factory.getFieldDecoder(testMeta, testFieldName, testFieldTypeDef) + assert.ErrorIs(t, err, ErrBitStoreTypeNotSupported) assert.Nil(t, res) } -func TestFactory_getFieldType_BitSequence_BitOrderTypeNotFound(t *testing.T) { +func TestFactory_getFieldDecoder_BitSequence_BitOrderTypeNotFound(t *testing.T) { testFieldName := "TestFieldName" bitStoreLookupID := 123 @@ -1454,7 +1447,7 @@ func TestFactory_getFieldType_BitSequence_BitOrderTypeNotFound(t *testing.T) { bitStoreTypeDef := types.Si1TypeDef{ IsPrimitive: true, Primitive: types.Si1TypeDefPrimitive{ - Si0TypeDefPrimitive: types.Si0TypeDefPrimitive(types.IsI64), + Si0TypeDefPrimitive: types.Si0TypeDefPrimitive(types.IsU8), }, } @@ -1470,12 +1463,12 @@ func TestFactory_getFieldType_BitSequence_BitOrderTypeNotFound(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getFieldType(testMeta, testFieldName, testFieldTypeDef) - assert.Equal(t, "bit order type not found", err.Error()) + res, err := factory.getFieldDecoder(testMeta, testFieldName, testFieldTypeDef) + assert.ErrorIs(t, err, ErrBitOrderTypeNotFound) assert.Nil(t, res) } -func TestFactory_getFieldType_BitSequence_BitOrderFieldTypeError(t *testing.T) { +func TestFactory_getFieldDecoder_BitSequence_BitOrderCreationError(t *testing.T) { testFieldName := "TestFieldName" bitStoreLookupID := 123 @@ -1496,22 +1489,13 @@ func TestFactory_getFieldType_BitSequence_BitOrderFieldTypeError(t *testing.T) { bitStoreTypeDef := types.Si1TypeDef{ IsPrimitive: true, Primitive: types.Si1TypeDefPrimitive{ - Si0TypeDefPrimitive: types.Si0TypeDefPrimitive(types.IsI64), + Si0TypeDefPrimitive: types.Si0TypeDefPrimitive(types.IsU8), }, } - bitOrderTypeDef := types.Si1TypeDef{ - IsComposite: true, - Composite: types.Si1TypeDefComposite{ - Fields: []types.Si1Field{ - { - Name: "BitOrderCompositeField1", - Type: types.Si1LookupTypeID{ - // This type is not present in the efficient lookup map and should cause an error. - UCompact: types.NewUCompactFromUInt(uint64(123456)), - }, - }, - }, + bitOrderType := &types.Si1Type{ + Path: []types.Text{ + types.Text("unknown-order"), }, } @@ -1521,17 +1505,15 @@ func TestFactory_getFieldType_BitSequence_BitOrderFieldTypeError(t *testing.T) { int64(bitStoreLookupID): { Def: bitStoreTypeDef, }, - int64(bitOrderLookupID): { - Def: bitOrderTypeDef, - }, + int64(bitOrderLookupID): bitOrderType, }, }, } factory := NewFactory().(*factory) - res, err := factory.getFieldType(testMeta, testFieldName, testFieldTypeDef) - assert.Equal(t, "couldn't get bit order field type: couldn't get composite fields: type not found for field 'BitOrderCompositeField1'", err.Error()) + res, err := factory.getFieldDecoder(testMeta, testFieldName, testFieldTypeDef) + assert.ErrorIs(t, err, ErrBitOrderCreation) assert.Nil(t, res) } @@ -1593,9 +1575,10 @@ func TestFactory_getVariantFieldType_CompositeVariantTypeFieldError(t *testing.T } factory := NewFactory().(*factory) + factory.initStorages() - res, err := factory.getVariantFieldType(testMeta, testFieldTypeDef) - assert.Equal(t, "couldn't get field types for variant '1': couldn't get field type for 'VariantFieldName': couldn't get composite fields: type not found for field 'CompositeVariantField'", err.Error()) + res, err := factory.getVariantFieldDecoder(testMeta, testFieldTypeDef) + assert.ErrorIs(t, err, ErrVariantTypeFieldsRetrieval) assert.Nil(t, res) } @@ -1642,18 +1625,18 @@ func TestFactory_getCompactFieldType_CompactTuple(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getCompactFieldType(testMeta, testFieldName, compactFieldTypeDef) + res, err := factory.getCompactFieldDecoder(testMeta, testFieldName, compactFieldTypeDef) assert.NoError(t, err) - compositeFieldType, ok := res.(*CompositeFieldType) + compositeFieldType, ok := res.(*CompositeDecoder) assert.True(t, ok) assert.Len(t, compositeFieldType.Fields, 2) assert.Equal(t, fmt.Sprintf(tupleItemFieldNameFormat, 0), compositeFieldType.Fields[0].Name) - assert.Equal(t, &PrimitiveFieldType[types.UCompact]{}, compositeFieldType.Fields[0].FieldType) + assert.Equal(t, &ValueDecoder[types.UCompact]{}, compositeFieldType.Fields[0].FieldDecoder) assert.Equal(t, int64(tupleItemLookupID1), compositeFieldType.Fields[0].LookupIndex) assert.Equal(t, fmt.Sprintf(tupleItemFieldNameFormat, 1), compositeFieldType.Fields[1].Name) - assert.Equal(t, &PrimitiveFieldType[types.UCompact]{}, compositeFieldType.Fields[1].FieldType) + assert.Equal(t, &ValueDecoder[types.UCompact]{}, compositeFieldType.Fields[1].FieldDecoder) assert.Equal(t, int64(tupleItemLookupID2), compositeFieldType.Fields[1].LookupIndex) } @@ -1713,18 +1696,18 @@ func TestFactory_getCompactFieldType_CompactComposite(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getCompactFieldType(testMeta, testFieldName, compactFieldTypeDef) + res, err := factory.getCompactFieldDecoder(testMeta, testFieldName, compactFieldTypeDef) assert.NoError(t, err) - compositeFieldType, ok := res.(*CompositeFieldType) + compositeFieldType, ok := res.(*CompositeDecoder) assert.True(t, ok) assert.Len(t, compositeFieldType.Fields, 2) assert.Equal(t, compositeFieldName1, compositeFieldType.Fields[0].Name) - assert.Equal(t, &PrimitiveFieldType[types.UCompact]{}, compositeFieldType.Fields[0].FieldType) + assert.Equal(t, &ValueDecoder[types.UCompact]{}, compositeFieldType.Fields[0].FieldDecoder) assert.Equal(t, int64(compositeFieldLookupID1), compositeFieldType.Fields[0].LookupIndex) assert.Equal(t, compositeFieldName2, compositeFieldType.Fields[1].Name) - assert.Equal(t, &PrimitiveFieldType[types.UCompact]{}, compositeFieldType.Fields[1].FieldType) + assert.Equal(t, &ValueDecoder[types.UCompact]{}, compositeFieldType.Fields[1].FieldDecoder) assert.Equal(t, int64(compositeFieldLookupID2), compositeFieldType.Fields[1].LookupIndex) } @@ -1744,14 +1727,14 @@ func TestFactory_getArrayFieldType(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getArrayFieldType(uint(arrayLen), testMeta, testFieldName, arrayItemTypeDef) + res, err := factory.getArrayFieldDecoder(uint(arrayLen), testMeta, testFieldName, arrayItemTypeDef) assert.NoError(t, err) - arrayFieldType, ok := res.(*ArrayFieldType) + arrayFieldType, ok := res.(*ArrayDecoder) assert.True(t, ok) assert.Equal(t, uint(arrayLen), arrayFieldType.Length) - assert.Equal(t, &PrimitiveFieldType[types.U8]{}, arrayFieldType.ItemType) + assert.Equal(t, &ValueDecoder[types.U8]{}, arrayFieldType.ItemDecoder) } func TestFactory_getArrayFieldType_ItemFieldTypeError(t *testing.T) { @@ -1785,8 +1768,8 @@ func TestFactory_getArrayFieldType_ItemFieldTypeError(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getArrayFieldType(uint(arrayLen), testMeta, testFieldName, arrayItemTypeDef) - assert.Equal(t, "couldn't get array item field type: couldn't get composite fields: type not found for field 'CompositeField1'", err.Error()) + res, err := factory.getArrayFieldDecoder(uint(arrayLen), testMeta, testFieldName, arrayItemTypeDef) + assert.ErrorIs(t, err, ErrArrayItemFieldDecoderRetrieval) assert.Nil(t, res) } @@ -1804,13 +1787,13 @@ func TestFactory_getSliceFieldType(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getSliceFieldType(testMeta, testFieldName, sliceItemTypeDef) + res, err := factory.getSliceFieldDecoder(testMeta, testFieldName, sliceItemTypeDef) assert.NoError(t, err) - sliceFieldType, ok := res.(*SliceFieldType) + sliceFieldType, ok := res.(*SliceDecoder) assert.True(t, ok) - assert.Equal(t, &PrimitiveFieldType[types.U8]{}, sliceFieldType.ItemType) + assert.Equal(t, &ValueDecoder[types.U8]{}, sliceFieldType.ItemDecoder) } func TestFactory_getSliceFieldType_ItemFieldTypeError(t *testing.T) { @@ -1838,8 +1821,8 @@ func TestFactory_getSliceFieldType_ItemFieldTypeError(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getSliceFieldType(testMeta, testFieldName, sliceItemTypeDef) - assert.Equal(t, "couldn't get slice item field type: couldn't get composite fields: type not found for field 'CompositeField1'", err.Error()) + res, err := factory.getSliceFieldDecoder(testMeta, testFieldName, sliceItemTypeDef) + assert.ErrorIs(t, err, ErrSliceItemFieldDecoderRetrieval) assert.Nil(t, res) } @@ -1887,18 +1870,18 @@ func TestFactory_getTupleType(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getTupleType(testMeta, testFieldName, tupleTypeDef) + res, err := factory.getTupleFieldDecoder(testMeta, testFieldName, tupleTypeDef) assert.NoError(t, err) - compositeFieldType, ok := res.(*CompositeFieldType) + compositeFieldType, ok := res.(*CompositeDecoder) assert.True(t, ok) assert.Len(t, compositeFieldType.Fields, 2) assert.Equal(t, fmt.Sprintf(tupleItemFieldNameFormat, 0), compositeFieldType.Fields[0].Name) - assert.Equal(t, &PrimitiveFieldType[types.U8]{}, compositeFieldType.Fields[0].FieldType) + assert.Equal(t, &ValueDecoder[types.U8]{}, compositeFieldType.Fields[0].FieldDecoder) assert.Equal(t, int64(tupleItemLookupID1), compositeFieldType.Fields[0].LookupIndex) assert.Equal(t, fmt.Sprintf(tupleItemFieldNameFormat, 1), compositeFieldType.Fields[1].Name) - assert.Equal(t, &PrimitiveFieldType[types.U32]{}, compositeFieldType.Fields[1].FieldType) + assert.Equal(t, &ValueDecoder[types.U32]{}, compositeFieldType.Fields[1].FieldDecoder) assert.Equal(t, int64(tupleItemLookupID2), compositeFieldType.Fields[1].LookupIndex) } @@ -1937,12 +1920,12 @@ func TestFactory_getTupleType_TupleItemNotFound(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getTupleType(testMeta, testFieldName, tupleTypeDef) - assert.Equal(t, "type definition for tuple item 1 not found", err.Error()) + res, err := factory.getTupleFieldDecoder(testMeta, testFieldName, tupleTypeDef) + assert.ErrorIs(t, err, ErrTupleItemTypeNotFound) assert.Nil(t, res) } -func TestFactory_getTupleType_TupleItemFieldTypeError(t *testing.T) { +func TestFactory_getTupleType_TupleItemFieldDecoderError(t *testing.T) { testFieldName := "TestFieldName" tupleItemLookupID1 := 123 @@ -1995,16 +1978,16 @@ func TestFactory_getTupleType_TupleItemFieldTypeError(t *testing.T) { factory := NewFactory().(*factory) - res, err := factory.getTupleType(testMeta, testFieldName, tupleTypeDef) - assert.Equal(t, "couldn't get tuple field type: couldn't get composite fields: type not found for field 'CompositeField1'", err.Error()) + res, err := factory.getTupleFieldDecoder(testMeta, testFieldName, tupleTypeDef) + assert.ErrorIs(t, err, ErrTupleItemFieldDecoderRetrieval) assert.Nil(t, res) } func Test_getPrimitiveType_UnsupportedTypeError(t *testing.T) { primitiveTypeDef := types.Si0TypeDefPrimitive(32) - res, err := getPrimitiveType(primitiveTypeDef) - assert.NotNil(t, err) + res, err := getPrimitiveDecoder(primitiveTypeDef) + assert.ErrorIs(t, err, ErrPrimitiveTypeNotSupported) assert.Nil(t, res) } @@ -2019,7 +2002,7 @@ func newTestAsserter() *testAsserter { func (a *testAsserter) assertRegistryItemContainsAllTypes(t *testing.T, meta types.Metadata, registryItemFields []*Field, metaItemFields []types.Si1Field) { for i, metaItemField := range metaItemFields { registryItemField := registryItemFields[i] - registryItemFieldType := registryItemField.FieldType + registryItemFieldType := registryItemField.FieldDecoder metaLookupIndex := metaItemField.Type.Int64() if _, ok := a.recursiveTypeMap[metaLookupIndex]; ok { @@ -2035,21 +2018,21 @@ func (a *testAsserter) assertRegistryItemContainsAllTypes(t *testing.T, meta typ a.assertRegistryItemFieldIsCorrect(t, meta, registryItemFieldType, fieldType) - if _, ok := registryItemField.FieldType.(*RecursiveFieldType); ok { + if _, ok := registryItemField.FieldDecoder.(*RecursiveDecoder); ok { a.recursiveTypeMap[metaLookupIndex] = struct{}{} } } } -func (a *testAsserter) assertRegistryItemFieldIsCorrect(t *testing.T, meta types.Metadata, registryItemFieldType FieldType, metaFieldType *types.Si1Type) { +func (a *testAsserter) assertRegistryItemFieldIsCorrect(t *testing.T, meta types.Metadata, registryItemFieldType FieldDecoder, metaFieldType *types.Si1Type) { metaFieldTypeDef := metaFieldType.Def switch { case metaFieldTypeDef.IsComposite: - compositeRegistryFieldType, ok := registryItemFieldType.(*CompositeFieldType) + compositeRegistryFieldType, ok := registryItemFieldType.(*CompositeDecoder) if !ok { - _, isRecursive := registryItemFieldType.(*RecursiveFieldType) + _, isRecursive := registryItemFieldType.(*RecursiveDecoder) assert.True(t, isRecursive, "expected recursive field") return @@ -2057,34 +2040,34 @@ func (a *testAsserter) assertRegistryItemFieldIsCorrect(t *testing.T, meta types a.assertRegistryItemContainsAllTypes(t, meta, compositeRegistryFieldType.Fields, metaFieldTypeDef.Composite.Fields) case metaFieldTypeDef.IsVariant: - variantRegistryFieldType, ok := registryItemFieldType.(*VariantFieldType) + variantRegistryFieldType, ok := registryItemFieldType.(*VariantDecoder) if !ok { - _, isRecursive := registryItemFieldType.(*RecursiveFieldType) + _, isRecursive := registryItemFieldType.(*RecursiveDecoder) assert.True(t, isRecursive, "expected variant or recursive field") return } for _, variant := range metaFieldTypeDef.Variant.Variants { - registryVariant, ok := variantRegistryFieldType.FieldTypeMap[byte(variant.Index)] + registryVariant, ok := variantRegistryFieldType.FieldDecoderMap[byte(variant.Index)] assert.True(t, ok, "expected registry variant") if len(variant.Fields) == 0 { - _, ok = registryVariant.(*PrimitiveFieldType[byte]) - assert.True(t, ok, "expected byte field type") + _, ok = registryVariant.(*NoopDecoder) + assert.True(t, ok, "expected noop decoder") continue } - compositeRegistryField, ok := registryVariant.(*CompositeFieldType) + compositeRegistryField, ok := registryVariant.(*CompositeDecoder) assert.True(t, ok, "expected composite field type") a.assertRegistryItemContainsAllTypes(t, meta, compositeRegistryField.Fields, variant.Fields) } case metaFieldTypeDef.IsSequence: - sliceRegistryField, ok := registryItemFieldType.(*SliceFieldType) + sliceRegistryField, ok := registryItemFieldType.(*SliceDecoder) if !ok { - _, isRecursive := registryItemFieldType.(*RecursiveFieldType) + _, isRecursive := registryItemFieldType.(*RecursiveDecoder) assert.True(t, isRecursive, "expected recursive field") return @@ -2093,26 +2076,26 @@ func (a *testAsserter) assertRegistryItemFieldIsCorrect(t *testing.T, meta types sequenceFieldType, ok := meta.AsMetadataV14.EfficientLookup[metaFieldTypeDef.Sequence.Type.Int64()] assert.True(t, ok, "couldn't get sequence field type") - a.assertRegistryItemFieldIsCorrect(t, meta, sliceRegistryField.ItemType, sequenceFieldType) + a.assertRegistryItemFieldIsCorrect(t, meta, sliceRegistryField.ItemDecoder, sequenceFieldType) case metaFieldTypeDef.IsArray: - arrayRegistryField, ok := registryItemFieldType.(*ArrayFieldType) + arrayRegistryField, ok := registryItemFieldType.(*ArrayDecoder) assert.True(t, ok, "expected array field type in registry") arrayFieldType, ok := meta.AsMetadataV14.EfficientLookup[metaFieldTypeDef.Array.Type.Int64()] assert.True(t, ok, "couldn't get array field type") - a.assertRegistryItemFieldIsCorrect(t, meta, arrayRegistryField.ItemType, arrayFieldType) + a.assertRegistryItemFieldIsCorrect(t, meta, arrayRegistryField.ItemDecoder, arrayFieldType) case metaFieldTypeDef.IsTuple: if metaFieldTypeDef.Tuple == nil { - _, ok := registryItemFieldType.(*PrimitiveFieldType[[]any]) - assert.True(t, ok, "expected empty tuple field type") + _, ok := registryItemFieldType.(*NoopDecoder) + assert.True(t, ok, "expected noop decoder") return } - compositeRegistryFieldType, ok := registryItemFieldType.(*CompositeFieldType) + compositeRegistryFieldType, ok := registryItemFieldType.(*CompositeDecoder) if !ok { - _, isRecursive := registryItemFieldType.(*RecursiveFieldType) + _, isRecursive := registryItemFieldType.(*RecursiveDecoder) assert.True(t, isRecursive, "expected composite or recursive field") return } @@ -2121,12 +2104,12 @@ func (a *testAsserter) assertRegistryItemFieldIsCorrect(t *testing.T, meta types itemTypeDef, ok := meta.AsMetadataV14.EfficientLookup[item.Int64()] assert.True(t, ok, "couldn't get tuple item field type") - registryTupleItemFieldType := compositeRegistryFieldType.Fields[i].FieldType + registryTupleItemFieldType := compositeRegistryFieldType.Fields[i].FieldDecoder a.assertRegistryItemFieldIsCorrect(t, meta, registryTupleItemFieldType, itemTypeDef) } case metaFieldTypeDef.IsPrimitive: - primitiveFieldType, err := getPrimitiveType(metaFieldTypeDef.Primitive.Si0TypeDefPrimitive) + primitiveFieldType, err := getPrimitiveDecoder(metaFieldTypeDef.Primitive.Si0TypeDefPrimitive) assert.NoError(t, err, "couldn't get primitive type") assert.Equal(t, primitiveFieldType, registryItemFieldType, "primitive field types should match") @@ -2136,46 +2119,41 @@ func (a *testAsserter) assertRegistryItemFieldIsCorrect(t *testing.T, meta types switch { case compactFieldType.Def.IsPrimitive: - _, ok = registryItemFieldType.(*PrimitiveFieldType[types.UCompact]) + _, ok = registryItemFieldType.(*ValueDecoder[types.UCompact]) assert.True(t, ok, "expected compact field type in registry") case compactFieldType.Def.IsTuple: if metaFieldTypeDef.Tuple == nil { - _, ok := registryItemFieldType.(*PrimitiveFieldType[any]) + _, ok := registryItemFieldType.(*ValueDecoder[any]) assert.True(t, ok, "expected empty tuple field type") return } - compositeRegistryField, ok := registryItemFieldType.(*CompositeFieldType) + compositeRegistryField, ok := registryItemFieldType.(*CompositeDecoder) assert.True(t, ok, "expected composite field type in registry") for _, field := range compositeRegistryField.Fields { - _, ok = field.FieldType.(*PrimitiveFieldType[types.UCompact]) + _, ok = field.FieldDecoder.(*ValueDecoder[types.UCompact]) assert.True(t, ok, "expected compact field type in registry") } case compactFieldType.Def.IsComposite: - compositeRegistryField, ok := registryItemFieldType.(*CompositeFieldType) + compositeRegistryField, ok := registryItemFieldType.(*CompositeDecoder) assert.True(t, ok, "expected composite field type in registry") for _, field := range compositeRegistryField.Fields { - _, ok = field.FieldType.(*PrimitiveFieldType[types.UCompact]) + _, ok = field.FieldDecoder.(*ValueDecoder[types.UCompact]) assert.True(t, ok, "expected compact field type in registry") } default: t.Fatalf("unsupported compact field type") } case metaFieldTypeDef.IsBitSequence: - bitSequenceType, ok := registryItemFieldType.(*BitSequenceType) + bitSequenceDecoder, ok := registryItemFieldType.(*BitSequenceDecoder) assert.True(t, ok, "expected bit sequence field type in registry") - bitStoreType, ok := meta.AsMetadataV14.EfficientLookup[metaFieldTypeDef.BitSequence.BitStoreType.Int64()] - assert.True(t, ok, "couldn't get bit store field type") - - a.assertRegistryItemFieldIsCorrect(t, meta, bitSequenceType.BitStoreType, bitStoreType) - bitOrderType, ok := meta.AsMetadataV14.EfficientLookup[metaFieldTypeDef.BitSequence.BitOrderType.Int64()] - assert.True(t, ok, "couldn't get bit order field type") + assert.True(t, ok, "expected bit order type") - a.assertRegistryItemFieldIsCorrect(t, meta, bitSequenceType.BitOrderType, bitOrderType) + assert.Equal(t, types.BitOrderValue[getBitOrderString(bitOrderType.Path)], bitSequenceDecoder.BitOrder) case metaFieldTypeDef.IsHistoricMetaCompat: t.Fatalf("historic meta compat type not covered") } diff --git a/registry/retriever/error.go b/registry/retriever/error.go new file mode 100644 index 000000000..f70351ce0 --- /dev/null +++ b/registry/retriever/error.go @@ -0,0 +1,14 @@ +package retriever + +import libErr "github.com/centrifuge/go-substrate-rpc-client/v4/error" + +const ( + ErrInternalStateUpdate = libErr.Error("internal state update") + ErrBlockRetrieval = libErr.Error("block retrieval") + ErrExtrinsicParsing = libErr.Error("extrinsic parsing") + ErrMetadataRetrieval = libErr.Error("metadata retrieval") + ErrCallRegistryCreation = libErr.Error("call registry creation") + ErrStorageEventRetrieval = libErr.Error("storage event retrieval") + ErrEventParsing = libErr.Error("event parsing") + ErrEventRegistryCreation = libErr.Error("event registry creation") +) diff --git a/registry/retriever/event_retriever.go b/registry/retriever/event_retriever.go new file mode 100644 index 000000000..e4d920948 --- /dev/null +++ b/registry/retriever/event_retriever.go @@ -0,0 +1,151 @@ +package retriever + +import ( + "time" + + "github.com/centrifuge/go-substrate-rpc-client/v4/registry" + "github.com/centrifuge/go-substrate-rpc-client/v4/registry/exec" + "github.com/centrifuge/go-substrate-rpc-client/v4/registry/parser" + regState "github.com/centrifuge/go-substrate-rpc-client/v4/registry/state" + "github.com/centrifuge/go-substrate-rpc-client/v4/rpc/state" + "github.com/centrifuge/go-substrate-rpc-client/v4/types" +) + +//nolint:lll +//go:generate mockery --name EventRetriever --structname EventRetrieverMock --filename event_retriever_mock.go --inpackage + +// EventRetriever is the interface used for retrieving and decoding events. +type EventRetriever interface { + GetEvents(blockHash types.Hash) ([]*parser.Event, error) +} + +// eventRetriever implements the EventRetriever interface. +type eventRetriever struct { + eventParser parser.EventParser + + eventProvider regState.EventProvider + stateRPC state.State + + registryFactory registry.Factory + + eventStorageExecutor exec.RetryableExecutor[*types.StorageDataRaw] + eventParsingExecutor exec.RetryableExecutor[[]*parser.Event] + + eventRegistry registry.EventRegistry + meta *types.Metadata +} + +// NewEventRetriever creates a new EventRetriever. +func NewEventRetriever( + eventParser parser.EventParser, + eventProvider regState.EventProvider, + stateRPC state.State, + registryFactory registry.Factory, + eventStorageExecutor exec.RetryableExecutor[*types.StorageDataRaw], + eventParsingExecutor exec.RetryableExecutor[[]*parser.Event], +) (EventRetriever, error) { + retriever := &eventRetriever{ + eventParser: eventParser, + eventProvider: eventProvider, + stateRPC: stateRPC, + registryFactory: registryFactory, + eventStorageExecutor: eventStorageExecutor, + eventParsingExecutor: eventParsingExecutor, + } + + if err := retriever.updateInternalState(nil); err != nil { + return nil, ErrInternalStateUpdate.Wrap(err) + } + + return retriever, nil +} + +// NewDefaultEventRetriever creates a new EventRetriever using defaults for: +// +// - parser.EventParser +// - registry.Factory +// - exec.RetryableExecutor - used for retrieving event storage data. +// - exec.RetryableExecutor - used for parsing events. +func NewDefaultEventRetriever( + eventProvider regState.EventProvider, + stateRPC state.State, +) (EventRetriever, error) { + eventParser := parser.NewEventParser() + registryFactory := registry.NewFactory() + + eventStorageExecutor := exec.NewRetryableExecutor[*types.StorageDataRaw](exec.WithRetryTimeout(1 * time.Second)) + eventParsingExecutor := exec.NewRetryableExecutor[[]*parser.Event](exec.WithMaxRetryCount(1)) + + return NewEventRetriever( + eventParser, + eventProvider, + stateRPC, + registryFactory, + eventStorageExecutor, + eventParsingExecutor, + ) +} + +// GetEvents retrieves the storage data for an Event and then parses it. +// +// Both the event storage data retrieval and the event parsing are handled via the exec.RetryableExecutor +// in order to ensure retries in case of network errors or parsing errors due to an outdated event registry. +func (e *eventRetriever) GetEvents(blockHash types.Hash) ([]*parser.Event, error) { + storageEvents, err := e.eventStorageExecutor.ExecWithFallback( + func() (*types.StorageDataRaw, error) { + return e.eventProvider.GetStorageEvents(e.meta, blockHash) + }, + func() error { + return e.updateInternalState(&blockHash) + }, + ) + + if err != nil { + return nil, ErrStorageEventRetrieval.Wrap(err) + } + + events, err := e.eventParsingExecutor.ExecWithFallback( + func() ([]*parser.Event, error) { + return e.eventParser.ParseEvents(e.eventRegistry, storageEvents) + }, + func() error { + return e.updateInternalState(&blockHash) + }, + ) + + if err != nil { + return nil, ErrEventParsing.Wrap(err) + } + + return events, nil +} + +// updateInternalState will retrieve the metadata at the provided blockHash, if provided, +// create an event registry based on this metadata and store both. +func (e *eventRetriever) updateInternalState(blockHash *types.Hash) error { + var ( + meta *types.Metadata + err error + ) + + if blockHash == nil { + meta, err = e.stateRPC.GetMetadataLatest() + } else { + meta, err = e.stateRPC.GetMetadata(*blockHash) + } + + if err != nil { + return ErrMetadataRetrieval.Wrap(err) + } + + eventRegistry, err := e.registryFactory.CreateEventRegistry(meta) + + if err != nil { + return ErrEventRegistryCreation.Wrap(err) + } + + e.meta = meta + e.eventRegistry = eventRegistry + + return nil +} diff --git a/registry/retriever/event_retriever_live_test.go b/registry/retriever/event_retriever_live_test.go new file mode 100644 index 000000000..fbacdaa2c --- /dev/null +++ b/registry/retriever/event_retriever_live_test.go @@ -0,0 +1,99 @@ +//go:build live + +package retriever + +import ( + "log" + "sync" + "testing" + + gsrpc "github.com/centrifuge/go-substrate-rpc-client/v4" + "github.com/centrifuge/go-substrate-rpc-client/v4/registry/state" +) + +var ( + eventTestURLs = []string{ + "wss://fullnode.parachain.centrifuge.io", + "wss://rpc.polkadot.io", + "wss://statemint-rpc.polkadot.io", + "wss://acala-rpc-0.aca-api.network", + "wss://wss.api.moonbeam.network", + } +) + +const maxRetrievedEvents = 10000 + +func TestLive_EventRetriever_GetEvents(t *testing.T) { + t.Parallel() + + var wg sync.WaitGroup + + for _, testURL := range eventTestURLs { + testURL := testURL + + wg.Add(1) + + go func() { + defer wg.Done() + + api, err := gsrpc.NewSubstrateAPI(testURL) + + if err != nil { + log.Printf("Couldn't connect to '%s': %s\n", testURL, err) + return + } + + retriever, err := NewDefaultEventRetriever(state.NewEventProvider(api.RPC.State), api.RPC.State) + + if err != nil { + log.Printf("Couldn't create event retriever: %s", err) + return + } + + header, err := api.RPC.Chain.GetHeaderLatest() + + if err != nil { + log.Printf("Couldn't get latest header for '%s': %s\n", testURL, err) + return + } + + eventsCount := 0 + + for { + blockHash, err := api.RPC.Chain.GetBlockHash(uint64(header.Number)) + + if err != nil { + log.Printf("Couldn't retrieve blockHash for '%s', block number %d: %s\n", testURL, header.Number, err) + return + } + + events, err := retriever.GetEvents(blockHash) + + if err != nil { + log.Printf("Couldn't retrieve events for '%s', block number %d: %s\n", testURL, header.Number, err) + return + } + + log.Printf("Found %d events for '%s', at block number %d.\n", len(events), testURL, header.Number) + + eventsCount += len(events) + + if eventsCount > maxRetrievedEvents { + log.Printf("Retrieved a total of %d events for '%s', last block number %d. Stopping now.\n", eventsCount, testURL, header.Number) + + return + } + + header, err = api.RPC.Chain.GetHeader(header.ParentHash) + + if err != nil { + log.Printf("Couldn't retrieve header for block number '%d' for '%s': %s\n", header.Number, testURL, err) + + return + } + } + }() + } + + wg.Wait() +} diff --git a/registry/retriever/event_retriever_mock.go b/registry/retriever/event_retriever_mock.go new file mode 100644 index 000000000..2ee53e5f0 --- /dev/null +++ b/registry/retriever/event_retriever_mock.go @@ -0,0 +1,52 @@ +// Code generated by mockery v2.13.0-beta.1. DO NOT EDIT. + +package retriever + +import ( + parser "github.com/centrifuge/go-substrate-rpc-client/v4/registry/parser" + types "github.com/centrifuge/go-substrate-rpc-client/v4/types" + mock "github.com/stretchr/testify/mock" +) + +// EventRetrieverMock is an autogenerated mock type for the EventRetriever type +type EventRetrieverMock struct { + mock.Mock +} + +// GetEvents provides a mock function with given fields: blockHash +func (_m *EventRetrieverMock) GetEvents(blockHash types.Hash) ([]*parser.Event, error) { + ret := _m.Called(blockHash) + + var r0 []*parser.Event + if rf, ok := ret.Get(0).(func(types.Hash) []*parser.Event); ok { + r0 = rf(blockHash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*parser.Event) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(types.Hash) error); ok { + r1 = rf(blockHash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type NewEventRetrieverMockT interface { + mock.TestingT + Cleanup(func()) +} + +// NewEventRetrieverMock creates a new instance of EventRetrieverMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewEventRetrieverMock(t NewEventRetrieverMockT) *EventRetrieverMock { + mock := &EventRetrieverMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/registry/retriever/event_retriever_test.go b/registry/retriever/event_retriever_test.go new file mode 100644 index 000000000..9fb020f50 --- /dev/null +++ b/registry/retriever/event_retriever_test.go @@ -0,0 +1,469 @@ +package retriever + +import ( + "errors" + "testing" + + "github.com/centrifuge/go-substrate-rpc-client/v4/registry" + "github.com/centrifuge/go-substrate-rpc-client/v4/registry/exec" + "github.com/centrifuge/go-substrate-rpc-client/v4/registry/parser" + "github.com/centrifuge/go-substrate-rpc-client/v4/registry/state" + stateMocks "github.com/centrifuge/go-substrate-rpc-client/v4/rpc/state/mocks" + "github.com/centrifuge/go-substrate-rpc-client/v4/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestEventRetriever_New(t *testing.T) { + eventParserMock := parser.NewEventParserMock(t) + eventProviderMock := state.NewEventProviderMock(t) + stateRPCMock := stateMocks.NewState(t) + registryFactoryMock := registry.NewFactoryMock(t) + storageExecMock := exec.NewRetryableExecutorMock[*types.StorageDataRaw](t) + parsingExecMock := exec.NewRetryableExecutorMock[[]*parser.Event](t) + + latestMeta := &types.Metadata{} + + stateRPCMock.On("GetMetadataLatest"). + Return(latestMeta, nil). + Once() + + eventRegistry := registry.EventRegistry(map[types.EventID]*registry.Type{}) + + registryFactoryMock.On("CreateEventRegistry", latestMeta). + Return(eventRegistry, nil). + Once() + + res, err := NewEventRetriever( + eventParserMock, + eventProviderMock, + stateRPCMock, + registryFactoryMock, + storageExecMock, + parsingExecMock, + ) + assert.NoError(t, err) + assert.IsType(t, &eventRetriever{}, res) +} + +func TestEventRetriever_New_InternalStateUpdateError(t *testing.T) { + eventParserMock := parser.NewEventParserMock(t) + eventProviderMock := state.NewEventProviderMock(t) + stateRPCMock := stateMocks.NewState(t) + registryFactoryMock := registry.NewFactoryMock(t) + storageExecMock := exec.NewRetryableExecutorMock[*types.StorageDataRaw](t) + parsingExecMock := exec.NewRetryableExecutorMock[[]*parser.Event](t) + + metadataRetrievalError := errors.New("error") + + stateRPCMock.On("GetMetadataLatest"). + Return(nil, metadataRetrievalError). + Once() + + res, err := NewEventRetriever( + eventParserMock, + eventProviderMock, + stateRPCMock, + registryFactoryMock, + storageExecMock, + parsingExecMock, + ) + assert.ErrorIs(t, err, ErrInternalStateUpdate) + assert.Nil(t, res) + + latestMeta := &types.Metadata{} + + stateRPCMock.On("GetMetadataLatest"). + Return(latestMeta, nil). + Once() + + registryFactoryError := errors.New("error") + + registryFactoryMock.On("CreateEventRegistry", latestMeta). + Return(nil, registryFactoryError). + Once() + + res, err = NewEventRetriever( + eventParserMock, + eventProviderMock, + stateRPCMock, + registryFactoryMock, + storageExecMock, + parsingExecMock, + ) + assert.ErrorIs(t, err, ErrInternalStateUpdate) + assert.Nil(t, res) +} + +func TestEventRetriever_NewDefault(t *testing.T) { + eventProviderMock := state.NewEventProviderMock(t) + stateRPCMock := stateMocks.NewState(t) + + latestMeta := &types.Metadata{} + + stateRPCMock.On("GetMetadataLatest"). + Return(latestMeta, nil). + Once() + + res, err := NewDefaultEventRetriever(eventProviderMock, stateRPCMock) + assert.NoError(t, err) + assert.IsType(t, &eventRetriever{}, res) + + retriever := res.(*eventRetriever) + assert.IsType(t, parser.NewEventParser(), retriever.eventParser) + assert.IsType(t, registry.NewFactory(), retriever.registryFactory) + assert.IsType(t, exec.NewRetryableExecutor[*types.StorageDataRaw](), retriever.eventStorageExecutor) + assert.IsType(t, exec.NewRetryableExecutor[[]*parser.Event](), retriever.eventParsingExecutor) + assert.Equal(t, latestMeta, retriever.meta) + assert.NotNil(t, retriever.eventRegistry) +} + +func TestEventRetriever_GetEvents(t *testing.T) { + eventParserMock := parser.NewEventParserMock(t) + eventProviderMock := state.NewEventProviderMock(t) + stateRPCMock := stateMocks.NewState(t) + registryFactoryMock := registry.NewFactoryMock(t) + storageExecMock := exec.NewRetryableExecutorMock[*types.StorageDataRaw](t) + parsingExecMock := exec.NewRetryableExecutorMock[[]*parser.Event](t) + + eventRetriever := &eventRetriever{ + eventParser: eventParserMock, + eventProvider: eventProviderMock, + stateRPC: stateRPCMock, + registryFactory: registryFactoryMock, + eventStorageExecutor: storageExecMock, + eventParsingExecutor: parsingExecMock, + } + + testMeta := &types.Metadata{} + + eventRetriever.meta = testMeta + + eventRegistry := registry.EventRegistry(map[types.EventID]*registry.Type{}) + + eventRetriever.eventRegistry = eventRegistry + + blockHash := types.NewHash([]byte{0, 1, 2, 3}) + + storageEvents := &types.StorageDataRaw{} + + eventProviderMock.On("GetStorageEvents", testMeta, blockHash). + Return(storageEvents, nil). + Once() + + storageExecMock.On("ExecWithFallback", mock.Anything, mock.Anything). + Run( + func(args mock.Arguments) { + execFn, ok := args.Get(0).(func() (*types.StorageDataRaw, error)) + assert.True(t, ok) + + execFnRes, err := execFn() + assert.NoError(t, err) + assert.Equal(t, storageEvents, execFnRes) + }, + ).Return(storageEvents, nil) + + parsedEvents := []*parser.Event{} + + eventParserMock.On("ParseEvents", eventRegistry, storageEvents). + Return(parsedEvents, nil). + Once() + + parsingExecMock.On("ExecWithFallback", mock.Anything, mock.Anything). + Run( + func(args mock.Arguments) { + execFn, ok := args.Get(0).(func() ([]*parser.Event, error)) + assert.True(t, ok) + + execFnRes, err := execFn() + assert.NoError(t, err) + assert.Equal(t, parsedEvents, execFnRes) + }, + ).Return(parsedEvents, nil) + + res, err := eventRetriever.GetEvents(blockHash) + assert.NoError(t, err) + assert.Equal(t, parsedEvents, res) +} + +func TestEventRetriever_GetEvents_StorageRetrievalError(t *testing.T) { + eventParserMock := parser.NewEventParserMock(t) + eventProviderMock := state.NewEventProviderMock(t) + stateRPCMock := stateMocks.NewState(t) + registryFactoryMock := registry.NewFactoryMock(t) + storageExecMock := exec.NewRetryableExecutorMock[*types.StorageDataRaw](t) + parsingExecMock := exec.NewRetryableExecutorMock[[]*parser.Event](t) + + eventRetriever := &eventRetriever{ + eventParser: eventParserMock, + eventProvider: eventProviderMock, + stateRPC: stateRPCMock, + registryFactory: registryFactoryMock, + eventStorageExecutor: storageExecMock, + eventParsingExecutor: parsingExecMock, + } + + testMeta := &types.Metadata{} + + eventRetriever.meta = testMeta + + eventRegistry := registry.EventRegistry(map[types.EventID]*registry.Type{}) + + eventRetriever.eventRegistry = eventRegistry + + blockHash := types.NewHash([]byte{0, 1, 2, 3}) + + storageRetrievalError := errors.New("error") + + eventProviderMock.On("GetStorageEvents", testMeta, blockHash). + Return(nil, storageRetrievalError). + Once() + + stateRPCMock.On("GetMetadata", blockHash). + Return(testMeta, nil). + Once() + + registryFactoryMock.On("CreateEventRegistry", testMeta). + Return(eventRegistry, nil). + Once() + + storageExecMock.On("ExecWithFallback", mock.Anything, mock.Anything). + Run( + func(args mock.Arguments) { + execFn, ok := args.Get(0).(func() (*types.StorageDataRaw, error)) + assert.True(t, ok) + + execFnRes, err := execFn() + assert.ErrorIs(t, err, storageRetrievalError) + assert.Nil(t, execFnRes) + + fallbackFn, ok := args.Get(1).(func() error) + assert.True(t, ok) + + err = fallbackFn() + assert.NoError(t, err) + }, + ).Return(&types.StorageDataRaw{}, storageRetrievalError) + + res, err := eventRetriever.GetEvents(blockHash) + assert.ErrorIs(t, err, ErrStorageEventRetrieval) + assert.Nil(t, res) +} + +func TestEventRetriever_GetEvents_EventParsingError(t *testing.T) { + eventParserMock := parser.NewEventParserMock(t) + eventProviderMock := state.NewEventProviderMock(t) + stateRPCMock := stateMocks.NewState(t) + registryFactoryMock := registry.NewFactoryMock(t) + storageExecMock := exec.NewRetryableExecutorMock[*types.StorageDataRaw](t) + parsingExecMock := exec.NewRetryableExecutorMock[[]*parser.Event](t) + + eventRetriever := &eventRetriever{ + eventParser: eventParserMock, + eventProvider: eventProviderMock, + stateRPC: stateRPCMock, + registryFactory: registryFactoryMock, + eventStorageExecutor: storageExecMock, + eventParsingExecutor: parsingExecMock, + } + + testMeta := &types.Metadata{} + + eventRetriever.meta = testMeta + + eventRegistry := registry.EventRegistry(map[types.EventID]*registry.Type{}) + + eventRetriever.eventRegistry = eventRegistry + + blockHash := types.NewHash([]byte{0, 1, 2, 3}) + + storageEvents := &types.StorageDataRaw{} + + eventProviderMock.On("GetStorageEvents", testMeta, blockHash). + Return(storageEvents, nil). + Once() + + storageExecMock.On("ExecWithFallback", mock.Anything, mock.Anything). + Run( + func(args mock.Arguments) { + execFn, ok := args.Get(0).(func() (*types.StorageDataRaw, error)) + assert.True(t, ok) + + execFnRes, err := execFn() + assert.NoError(t, err) + assert.Equal(t, storageEvents, execFnRes) + }, + ).Return(storageEvents, nil) + + eventParsingError := errors.New("error") + + eventParserMock.On("ParseEvents", eventRegistry, storageEvents). + Return(nil, eventParsingError). + Once() + + stateRPCMock.On("GetMetadata", blockHash). + Return(testMeta, nil). + Once() + + registryFactoryMock.On("CreateEventRegistry", testMeta). + Return(eventRegistry, nil). + Once() + + parsingExecMock.On("ExecWithFallback", mock.Anything, mock.Anything). + Run( + func(args mock.Arguments) { + execFn, ok := args.Get(0).(func() ([]*parser.Event, error)) + assert.True(t, ok) + + execFnRes, err := execFn() + assert.ErrorIs(t, err, eventParsingError) + assert.Nil(t, execFnRes) + + fallbackFn, ok := args.Get(1).(func() error) + assert.True(t, ok) + + err = fallbackFn() + assert.NoError(t, err) + }, + ).Return([]*parser.Event{}, eventParsingError) + + res, err := eventRetriever.GetEvents(blockHash) + assert.ErrorIs(t, err, ErrEventParsing) + assert.Nil(t, res) +} + +func TestEventRetriever_updateInternalState(t *testing.T) { + eventParserMock := parser.NewEventParserMock(t) + eventProviderMock := state.NewEventProviderMock(t) + stateRPCMock := stateMocks.NewState(t) + registryFactoryMock := registry.NewFactoryMock(t) + storageExecMock := exec.NewRetryableExecutorMock[*types.StorageDataRaw](t) + parsingExecMock := exec.NewRetryableExecutorMock[[]*parser.Event](t) + + eventRetriever := &eventRetriever{ + eventParser: eventParserMock, + eventProvider: eventProviderMock, + stateRPC: stateRPCMock, + registryFactory: registryFactoryMock, + eventStorageExecutor: storageExecMock, + eventParsingExecutor: parsingExecMock, + } + + testMeta := &types.Metadata{} + + eventRegistry := registry.EventRegistry(map[types.EventID]*registry.Type{}) + + blockHash := types.NewHash([]byte{0, 1, 2, 3}) + + stateRPCMock.On("GetMetadata", blockHash). + Return(testMeta, nil). + Once() + + registryFactoryMock.On("CreateEventRegistry", testMeta). + Return(eventRegistry, nil). + Once() + + err := eventRetriever.updateInternalState(&blockHash) + assert.NoError(t, err) + assert.Equal(t, testMeta, eventRetriever.meta) + assert.Equal(t, eventRegistry, eventRetriever.eventRegistry) + + latestMeta := &types.Metadata{} + + stateRPCMock.On("GetMetadataLatest"). + Return(latestMeta, nil). + Once() + + registryFactoryMock.On("CreateEventRegistry", latestMeta). + Return(eventRegistry, nil). + Once() + + err = eventRetriever.updateInternalState(nil) + assert.NoError(t, err) + assert.Equal(t, latestMeta, eventRetriever.meta) + assert.Equal(t, eventRegistry, eventRetriever.eventRegistry) +} + +func TestEventRetriever_updateInternalState_MetadataRetrievalError(t *testing.T) { + eventParserMock := parser.NewEventParserMock(t) + eventProviderMock := state.NewEventProviderMock(t) + stateRPCMock := stateMocks.NewState(t) + registryFactoryMock := registry.NewFactoryMock(t) + storageExecMock := exec.NewRetryableExecutorMock[*types.StorageDataRaw](t) + parsingExecMock := exec.NewRetryableExecutorMock[[]*parser.Event](t) + + eventRetriever := &eventRetriever{ + eventParser: eventParserMock, + eventProvider: eventProviderMock, + stateRPC: stateRPCMock, + registryFactory: registryFactoryMock, + eventStorageExecutor: storageExecMock, + eventParsingExecutor: parsingExecMock, + } + + blockHash := types.NewHash([]byte{0, 1, 2, 3}) + + metadataRetrievalError := errors.New("error") + + stateRPCMock.On("GetMetadata", blockHash). + Return(nil, metadataRetrievalError). + Once() + + err := eventRetriever.updateInternalState(&blockHash) + assert.ErrorIs(t, err, ErrMetadataRetrieval) + + stateRPCMock.On("GetMetadataLatest"). + Return(nil, metadataRetrievalError). + Once() + + err = eventRetriever.updateInternalState(nil) + assert.ErrorIs(t, err, ErrMetadataRetrieval) +} + +func TestEventRetriever_updateInternalState_RegistryFactoryError(t *testing.T) { + eventParserMock := parser.NewEventParserMock(t) + eventProviderMock := state.NewEventProviderMock(t) + stateRPCMock := stateMocks.NewState(t) + registryFactoryMock := registry.NewFactoryMock(t) + storageExecMock := exec.NewRetryableExecutorMock[*types.StorageDataRaw](t) + parsingExecMock := exec.NewRetryableExecutorMock[[]*parser.Event](t) + + eventRetriever := &eventRetriever{ + eventParser: eventParserMock, + eventProvider: eventProviderMock, + stateRPC: stateRPCMock, + registryFactory: registryFactoryMock, + eventStorageExecutor: storageExecMock, + eventParsingExecutor: parsingExecMock, + } + + testMeta := &types.Metadata{} + + blockHash := types.NewHash([]byte{0, 1, 2, 3}) + + stateRPCMock.On("GetMetadata", blockHash). + Return(testMeta, nil). + Once() + + registryFactoryError := errors.New("error") + + registryFactoryMock.On("CreateEventRegistry", testMeta). + Return(nil, registryFactoryError). + Once() + + err := eventRetriever.updateInternalState(&blockHash) + assert.ErrorIs(t, err, ErrEventRegistryCreation) + + latestMeta := &types.Metadata{} + + stateRPCMock.On("GetMetadataLatest"). + Return(latestMeta, nil). + Once() + + registryFactoryMock.On("CreateEventRegistry", latestMeta). + Return(nil, registryFactoryError). + Once() + + err = eventRetriever.updateInternalState(nil) + assert.ErrorIs(t, err, ErrEventRegistryCreation) +} diff --git a/registry/retriever/extrinsic_retriever.go b/registry/retriever/extrinsic_retriever.go new file mode 100644 index 000000000..8a57419ca --- /dev/null +++ b/registry/retriever/extrinsic_retriever.go @@ -0,0 +1,174 @@ +package retriever + +import ( + "github.com/centrifuge/go-substrate-rpc-client/v4/registry" + "github.com/centrifuge/go-substrate-rpc-client/v4/registry/exec" + "github.com/centrifuge/go-substrate-rpc-client/v4/registry/parser" + "github.com/centrifuge/go-substrate-rpc-client/v4/rpc/chain/generic" + "github.com/centrifuge/go-substrate-rpc-client/v4/rpc/state" + "github.com/centrifuge/go-substrate-rpc-client/v4/types" +) + +//nolint:lll +//go:generate mockery --name ExtrinsicRetriever --structname ExtrinsicRetrieverMock --filename extrinsic_retriever_mock.go --inpackage + +// ExtrinsicRetriever is the interface used for retrieving and decoding extrinsic information. +// +// This interface is generic over types A, S, P, please check generic.GenericExtrinsicSignature for more +// information about these generic types. +type ExtrinsicRetriever[A, S, P any] interface { + GetExtrinsics(blockHash types.Hash) ([]*parser.Extrinsic[A, S, P], error) +} + +// extrinsicRetriever implements the ExtrinsicRetriever interface. +type extrinsicRetriever[ + A, S, P any, + B generic.GenericSignedBlock[A, S, P], +] struct { + extrinsicParser parser.ExtrinsicParser[A, S, P] + + genericChain generic.Chain[A, S, P, B] + stateRPC state.State + + registryFactory registry.Factory + + chainExecutor exec.RetryableExecutor[B] + extrinsicParsingExecutor exec.RetryableExecutor[[]*parser.Extrinsic[A, S, P]] + + callRegistry registry.CallRegistry + meta *types.Metadata +} + +// NewExtrinsicRetriever creates a new ExtrinsicRetriever. +func NewExtrinsicRetriever[ + A, S, P any, + B generic.GenericSignedBlock[A, S, P], +]( + extrinsicParser parser.ExtrinsicParser[A, S, P], + genericChain generic.Chain[A, S, P, B], + stateRPC state.State, + registryFactory registry.Factory, + chainExecutor exec.RetryableExecutor[B], + extrinsicParsingExecutor exec.RetryableExecutor[[]*parser.Extrinsic[A, S, P]], +) (ExtrinsicRetriever[A, S, P], error) { + retriever := &extrinsicRetriever[A, S, P, B]{ + extrinsicParser: extrinsicParser, + genericChain: genericChain, + stateRPC: stateRPC, + registryFactory: registryFactory, + chainExecutor: chainExecutor, + extrinsicParsingExecutor: extrinsicParsingExecutor, + } + + if err := retriever.updateInternalState(nil); err != nil { + return nil, ErrInternalStateUpdate.Wrap(err) + } + + return retriever, nil +} + +// DefaultExtrinsicRetriever is the ExtrinsicRetriever interface with default for the generic types: +// +// Address - types.MultiAddress +// Signature - types.MultiSignature +// PaymentFields - generic.DefaultPaymentFields +type DefaultExtrinsicRetriever = ExtrinsicRetriever[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, +] + +// NewDefaultExtrinsicRetriever returns a DefaultExtrinsicRetriever with defaults for the generic types: +// +// Address - types.MultiAddress +// Signature - types.MultiSignature +// PaymentFields - generic.DefaultPaymentFields +// Block - *generic.DefaultGenericSignedBlock +// +// Note that these generic defaults also apply to the args. +func NewDefaultExtrinsicRetriever( + extrinsicParser parser.DefaultExtrinsicParser, + genericChain generic.DefaultChain, + stateRPC state.State, + registryFactory registry.Factory, + chainExecutor exec.RetryableExecutor[*generic.DefaultGenericSignedBlock], + extrinsicParsingExecutor exec.RetryableExecutor[[]*parser.DefaultExtrinsic], +) (DefaultExtrinsicRetriever, error) { + return NewExtrinsicRetriever[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.DefaultGenericSignedBlock, + ]( + extrinsicParser, + genericChain, + stateRPC, + registryFactory, + chainExecutor, + extrinsicParsingExecutor, + ) +} + +// GetExtrinsics retrieves a generic.SignedBlock and then parses the extrinsics found in it. +// +// Both the block retrieval and the extrinsic parsing are handled via the exec.RetryableExecutor +// in order to ensure retries in case of network errors or parsing errors due to an outdated call registry. +func (e *extrinsicRetriever[A, S, P, B]) GetExtrinsics(blockHash types.Hash) ([]*parser.Extrinsic[A, S, P], error) { + block, err := e.chainExecutor.ExecWithFallback( + func() (B, error) { + return e.genericChain.GetBlock(blockHash) + }, + func() error { + return nil + }, + ) + + if err != nil { + return nil, ErrBlockRetrieval.Wrap(err) + } + + calls, err := e.extrinsicParsingExecutor.ExecWithFallback( + func() ([]*parser.Extrinsic[A, S, P], error) { + return e.extrinsicParser.ParseExtrinsics(e.callRegistry, block) + }, + func() error { + return e.updateInternalState(&blockHash) + }, + ) + + if err != nil { + return nil, ErrExtrinsicParsing.Wrap(err) + } + + return calls, nil +} + +// updateInternalState will retrieve the metadata at the provided blockHash, if provided, +// create a call registry based on this metadata and store both. +func (e *extrinsicRetriever[A, S, P, B]) updateInternalState(blockHash *types.Hash) error { + var ( + meta *types.Metadata + err error + ) + + if blockHash == nil { + meta, err = e.stateRPC.GetMetadataLatest() + } else { + meta, err = e.stateRPC.GetMetadata(*blockHash) + } + + if err != nil { + return ErrMetadataRetrieval.Wrap(err) + } + + callRegistry, err := e.registryFactory.CreateCallRegistry(meta) + + if err != nil { + return ErrCallRegistryCreation.Wrap(err) + } + + e.meta = meta + e.callRegistry = callRegistry + + return nil +} diff --git a/registry/retriever/extrinsic_retriever_live_test.go b/registry/retriever/extrinsic_retriever_live_test.go new file mode 100644 index 000000000..25877d418 --- /dev/null +++ b/registry/retriever/extrinsic_retriever_live_test.go @@ -0,0 +1,221 @@ +//go:build live + +package retriever + +import ( + "errors" + "log" + "sync" + "testing" + "time" + + gsrpc "github.com/centrifuge/go-substrate-rpc-client/v4" + "github.com/centrifuge/go-substrate-rpc-client/v4/registry" + "github.com/centrifuge/go-substrate-rpc-client/v4/registry/exec" + "github.com/centrifuge/go-substrate-rpc-client/v4/registry/parser" + "github.com/centrifuge/go-substrate-rpc-client/v4/rpc/chain/generic" + "github.com/centrifuge/go-substrate-rpc-client/v4/scale" + "github.com/centrifuge/go-substrate-rpc-client/v4/types" +) + +var ( + extTestChains = []testFnProvider{ + &testChain[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]{ + url: "wss://fullnode.parachain.centrifuge.io", + }, + &testChain[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]{ + url: "wss://rpc.polkadot.io", + }, + &testChain[ + types.MultiAddress, + types.MultiSignature, + generic.PaymentFieldsWithAssetID, + ]{ + url: "wss://statemint-rpc.polkadot.io", + }, + &testChain[ + types.MultiAddress, + AcalaMultiSignature, + generic.DefaultPaymentFields, + ]{ + url: "wss://acala-rpc-0.aca-api.network", + }, + &testChain[ + [20]byte, + [65]byte, + generic.DefaultPaymentFields, + ]{ + url: "wss://wss.api.moonbeam.network", + }, + } +) + +func TestLive_ExtrinsicRetriever_GetExtrinsics(t *testing.T) { + t.Parallel() + + var wg sync.WaitGroup + + for _, c := range extTestChains { + c := c + + wg.Add(1) + + go c.GetTestFn()(&wg) + } + + wg.Wait() +} + +type testFn func(wg *sync.WaitGroup) + +type testFnProvider interface { + GetTestFn() testFn +} + +type testChain[ + A any, + S any, + P any, +] struct { + url string +} + +const maxRetrievedExtrinsics = 1000 + +func (t *testChain[A, S, P]) GetTestFn() testFn { + return func(wg *sync.WaitGroup) { + defer wg.Done() + + testURL := t.url + + api, err := gsrpc.NewSubstrateAPI(testURL) + + if err != nil { + log.Printf("Couldn't connect to '%s': %s\n", testURL, err) + return + } + + chain := generic.NewChain[A, S, P, *generic.SignedBlock[A, S, P]](api.Client) + + extrinsicParser := parser.NewExtrinsicParser[A, S, P]() + registryFactory := registry.NewFactory() + + chainExecutor := exec.NewRetryableExecutor[*generic.SignedBlock[A, S, P]](exec.WithRetryTimeout(1 * time.Second)) + extrinsicParsingExecutor := exec.NewRetryableExecutor[[]*parser.Extrinsic[A, S, P]](exec.WithMaxRetryCount(1)) + + extrinsicRetriever, err := NewExtrinsicRetriever[A, S, P]( + extrinsicParser, + chain, + api.RPC.State, + registryFactory, + chainExecutor, + extrinsicParsingExecutor, + ) + + if err != nil { + log.Printf("Couldn't create extrinsic retriever: %s", err) + return + } + + header, err := api.RPC.Chain.GetHeaderLatest() + + if err != nil { + log.Printf("Couldn't get latest header for '%s': %s\n", testURL, err) + return + } + + extrinsicsCount := 0 + + for { + blockHash, err := api.RPC.Chain.GetBlockHash(uint64(header.Number)) + + if err != nil { + log.Printf("Couldn't retrieve blockHash for '%s', block number %d: %s\n", testURL, header.Number, err) + return + } + + extrinsics, err := extrinsicRetriever.GetExtrinsics(blockHash) + + if err != nil { + log.Printf("Couldn't retrieve extrinsics for '%s', block number %d: %s\n", testURL, header.Number, err) + } + + log.Printf("Found %d extrinsics for '%s', at block number %d.\n", len(extrinsics), testURL, header.Number) + + extrinsicsCount += len(extrinsics) + + if extrinsicsCount >= maxRetrievedExtrinsics { + log.Printf("Retrieved a total of %d extrinsics for '%s', last block number %d. Stopping now.\n", extrinsicsCount, testURL, header.Number) + + return + } + + header, err = api.RPC.Chain.GetHeader(header.ParentHash) + + if err != nil { + log.Printf("Couldn't retrieve header for block number '%d' for '%s': %s\n", header.Number, testURL, err) + + return + } + } + } +} + +type AcalaMultiSignature struct { + IsEd25519 bool + AsEd25519 types.Signature + IsSr25519 bool + AsSr25519 types.Signature + IsEcdsa bool + AsEcdsa types.EcdsaSignature + IsEthereum bool + AsEthereum [65]byte + IsEip1559 bool + AsEip1559 [65]byte + IsAcalaEip712 bool + AsAcalaEip712 [65]byte +} + +func (m *AcalaMultiSignature) Decode(decoder scale.Decoder) error { + b, err := decoder.ReadOneByte() + if err != nil { + return err + } + + switch b { + case 0: + m.IsEd25519 = true + err = decoder.Decode(&m.AsEd25519) + case 1: + m.IsSr25519 = true + err = decoder.Decode(&m.AsSr25519) + case 2: + m.IsEcdsa = true + err = decoder.Decode(&m.AsEcdsa) + case 3: + m.IsEthereum = true + err = decoder.Decode(&m.AsEthereum) + case 4: + m.IsEip1559 = true + err = decoder.Decode(&m.AsEip1559) + case 5: + m.IsAcalaEip712 = true + err = decoder.Decode(&m.AsAcalaEip712) + default: + return errors.New("signature not supported") + } + + if err != nil { + return err + } + + return nil +} diff --git a/registry/retriever/extrinsic_retriever_mock.go b/registry/retriever/extrinsic_retriever_mock.go new file mode 100644 index 000000000..e1a4a0fdf --- /dev/null +++ b/registry/retriever/extrinsic_retriever_mock.go @@ -0,0 +1,52 @@ +// Code generated by mockery v2.13.0-beta.1. DO NOT EDIT. + +package retriever + +import ( + parser "github.com/centrifuge/go-substrate-rpc-client/v4/registry/parser" + types "github.com/centrifuge/go-substrate-rpc-client/v4/types" + mock "github.com/stretchr/testify/mock" +) + +// ExtrinsicRetrieverMock is an autogenerated mock type for the ExtrinsicRetriever type +type ExtrinsicRetrieverMock[A interface{}, S interface{}, P interface{}] struct { + mock.Mock +} + +// GetExtrinsics provides a mock function with given fields: blockHash +func (_m *ExtrinsicRetrieverMock[A, S, P]) GetExtrinsics(blockHash types.Hash) ([]*parser.Extrinsic[A, S, P], error) { + ret := _m.Called(blockHash) + + var r0 []*parser.Extrinsic[A, S, P] + if rf, ok := ret.Get(0).(func(types.Hash) []*parser.Extrinsic[A, S, P]); ok { + r0 = rf(blockHash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*parser.Extrinsic[A, S, P]) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(types.Hash) error); ok { + r1 = rf(blockHash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type NewExtrinsicRetrieverMockT interface { + mock.TestingT + Cleanup(func()) +} + +// NewExtrinsicRetrieverMock creates a new instance of ExtrinsicRetrieverMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewExtrinsicRetrieverMock[A interface{}, S interface{}, P interface{}](t NewExtrinsicRetrieverMockT) *ExtrinsicRetrieverMock[A, S, P] { + mock := &ExtrinsicRetrieverMock[A, S, P]{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/registry/retriever/extrinsic_retriever_test.go b/registry/retriever/extrinsic_retriever_test.go new file mode 100644 index 000000000..6b1c50970 --- /dev/null +++ b/registry/retriever/extrinsic_retriever_test.go @@ -0,0 +1,804 @@ +package retriever + +import ( + "errors" + "testing" + + "github.com/centrifuge/go-substrate-rpc-client/v4/registry" + "github.com/centrifuge/go-substrate-rpc-client/v4/registry/exec" + "github.com/centrifuge/go-substrate-rpc-client/v4/registry/parser" + "github.com/centrifuge/go-substrate-rpc-client/v4/rpc/chain/generic" + stateMocks "github.com/centrifuge/go-substrate-rpc-client/v4/rpc/state/mocks" + "github.com/centrifuge/go-substrate-rpc-client/v4/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestExtrinsicRetriever_New(t *testing.T) { + extrinsicParserMock := parser.NewExtrinsicParserMock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ](t) + + genericChainMock := generic.NewChainMock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], + ](t) + + stateRPCMock := stateMocks.NewState(t) + registryFactoryMock := registry.NewFactoryMock(t) + chainExecMock := exec.NewRetryableExecutorMock[*generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]](t) + parsingExecMock := exec.NewRetryableExecutorMock[[]*parser.Extrinsic[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]](t) + + latestMeta := &types.Metadata{} + + stateRPCMock.On("GetMetadataLatest"). + Return(latestMeta, nil). + Once() + + callRegistry := registry.CallRegistry(map[types.CallIndex]*registry.Type{}) + + registryFactoryMock.On("CreateCallRegistry", latestMeta). + Return(callRegistry, nil). + Once() + + res, err := NewExtrinsicRetriever[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], + ]( + extrinsicParserMock, + genericChainMock, + stateRPCMock, + registryFactoryMock, + chainExecMock, + parsingExecMock, + ) + assert.NoError(t, err) + assert.IsType(t, &extrinsicRetriever[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], + ]{}, res) +} + +func TestExtrinsicRetriever_New_InternalStateUpdateError(t *testing.T) { + extrinsicParserMock := parser.NewExtrinsicParserMock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ](t) + + genericChainMock := generic.NewChainMock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], + ](t) + + stateRPCMock := stateMocks.NewState(t) + registryFactoryMock := registry.NewFactoryMock(t) + chainExecMock := exec.NewRetryableExecutorMock[*generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]](t) + parsingExecMock := exec.NewRetryableExecutorMock[[]*parser.Extrinsic[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]](t) + metadataRetrievalError := errors.New("error") + + stateRPCMock.On("GetMetadataLatest"). + Return(nil, metadataRetrievalError). + Once() + + res, err := NewExtrinsicRetriever[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], + ]( + extrinsicParserMock, + genericChainMock, + stateRPCMock, + registryFactoryMock, + chainExecMock, + parsingExecMock, + ) + assert.ErrorIs(t, err, ErrInternalStateUpdate) + assert.Nil(t, res) + + latestMeta := &types.Metadata{} + + stateRPCMock.On("GetMetadataLatest"). + Return(latestMeta, nil). + Once() + + registryFactoryError := errors.New("error") + + registryFactoryMock.On("CreateCallRegistry", latestMeta). + Return(nil, registryFactoryError). + Once() + + res, err = NewExtrinsicRetriever[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], + ]( + extrinsicParserMock, + genericChainMock, + stateRPCMock, + registryFactoryMock, + chainExecMock, + parsingExecMock, + ) + assert.ErrorIs(t, err, ErrInternalStateUpdate) + assert.Nil(t, res) +} + +func TestExtrinsicRetriever_NewDefault(t *testing.T) { + genericChainMock := generic.NewChainMock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.DefaultGenericSignedBlock, + ](t) + + stateRPCMock := stateMocks.NewState(t) + + latestMeta := &types.Metadata{} + + stateRPCMock.On("GetMetadataLatest"). + Return(latestMeta, nil). + Once() + + extrinsicParser := parser.NewDefaultExtrinsicParser() + + registryFactoryMock := registry.NewFactoryMock(t) + + callRegistry := registry.CallRegistry(map[types.CallIndex]*registry.Type{}) + + registryFactoryMock.On("CreateCallRegistry", latestMeta). + Return(callRegistry, nil). + Once() + + chainExecMock := exec.NewRetryableExecutorMock[*generic.DefaultGenericSignedBlock](t) + parsingExecMock := exec.NewRetryableExecutorMock[[]*parser.DefaultExtrinsic](t) + + res, err := NewDefaultExtrinsicRetriever( + extrinsicParser, + genericChainMock, + stateRPCMock, + registryFactoryMock, + chainExecMock, + parsingExecMock, + ) + + assert.NoError(t, err) + assert.IsType(t, &extrinsicRetriever[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.DefaultGenericSignedBlock, + ]{}, res) + + retriever := res.(*extrinsicRetriever[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.DefaultGenericSignedBlock, + ]) + assert.Equal(t, latestMeta, retriever.meta) + assert.Equal(t, callRegistry, retriever.callRegistry) +} + +func TestExtrinsicRetriever_GetExtrinsics(t *testing.T) { + extrinsicParserMock := parser.NewExtrinsicParserMock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ](t) + + genericChainMock := generic.NewChainMock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], + ](t) + + stateRPCMock := stateMocks.NewState(t) + registryFactoryMock := registry.NewFactoryMock(t) + chainExecMock := exec.NewRetryableExecutorMock[*generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]](t) + parsingExecMock := exec.NewRetryableExecutorMock[[]*parser.Extrinsic[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]](t) + + extrinsicRetriever := &extrinsicRetriever[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], + ]{ + extrinsicParser: extrinsicParserMock, + genericChain: genericChainMock, + stateRPC: stateRPCMock, + registryFactory: registryFactoryMock, + chainExecutor: chainExecMock, + extrinsicParsingExecutor: parsingExecMock, + } + + testMeta := &types.Metadata{} + + extrinsicRetriever.meta = testMeta + + callRegistry := registry.CallRegistry(map[types.CallIndex]*registry.Type{}) + + extrinsicRetriever.callRegistry = callRegistry + + blockHash := types.NewHash([]byte{0, 1, 2, 3}) + + signedBlock := &generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]{} + + genericChainMock.On("GetBlock", blockHash). + Return(signedBlock, nil). + Once() + + chainExecMock.On("ExecWithFallback", mock.Anything, mock.Anything). + Run( + func(args mock.Arguments) { + execFn, ok := args.Get(0).(func() (*generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], error)) + assert.True(t, ok) + + execFnRes, err := execFn() + assert.NoError(t, err) + assert.Equal(t, signedBlock, execFnRes) + }, + ).Return(signedBlock, nil) + + var parsedExtrinsics []*parser.Extrinsic[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ] + + extrinsicParserMock.On("ParseExtrinsics", callRegistry, signedBlock). + Return(parsedExtrinsics, nil). + Once() + + parsingExecMock.On("ExecWithFallback", mock.Anything, mock.Anything). + Run( + func(args mock.Arguments) { + execFn, ok := args.Get(0).(func() ([]*parser.Extrinsic[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], error)) + assert.True(t, ok) + + execFnRes, err := execFn() + assert.NoError(t, err) + assert.Equal(t, parsedExtrinsics, execFnRes) + }, + ).Return(parsedExtrinsics, nil) + + res, err := extrinsicRetriever.GetExtrinsics(blockHash) + assert.NoError(t, err) + assert.Equal(t, parsedExtrinsics, res) +} + +func TestExtrinsicRetriever_GetExtrinsics_BlockRetrievalError(t *testing.T) { + extrinsicParserMock := parser.NewExtrinsicParserMock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ](t) + + genericChainMock := generic.NewChainMock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], + ](t) + + stateRPCMock := stateMocks.NewState(t) + registryFactoryMock := registry.NewFactoryMock(t) + chainExecMock := exec.NewRetryableExecutorMock[*generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]](t) + parsingExecMock := exec.NewRetryableExecutorMock[[]*parser.Extrinsic[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]](t) + + extrinsicRetriever := &extrinsicRetriever[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], + ]{ + extrinsicParser: extrinsicParserMock, + genericChain: genericChainMock, + stateRPC: stateRPCMock, + registryFactory: registryFactoryMock, + chainExecutor: chainExecMock, + extrinsicParsingExecutor: parsingExecMock, + } + + testMeta := &types.Metadata{} + + extrinsicRetriever.meta = testMeta + + callRegistry := registry.CallRegistry(map[types.CallIndex]*registry.Type{}) + + extrinsicRetriever.callRegistry = callRegistry + + blockHash := types.NewHash([]byte{0, 1, 2, 3}) + + blockRetrievalError := errors.New("error") + + signedBlock := &generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]{} + + genericChainMock.On("GetBlock", blockHash). + Return(signedBlock, blockRetrievalError). + Once() + + chainExecMock.On("ExecWithFallback", mock.Anything, mock.Anything). + Run( + func(args mock.Arguments) { + execFn, ok := args.Get(0).(func() (*generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], error)) + assert.True(t, ok) + + execFnRes, err := execFn() + assert.ErrorIs(t, err, blockRetrievalError) + assert.Equal(t, signedBlock, execFnRes) + + fallbackFn, ok := args.Get(1).(func() error) + assert.True(t, ok) + + assert.NoError(t, fallbackFn()) + }, + ).Return(signedBlock, blockRetrievalError) + + res, err := extrinsicRetriever.GetExtrinsics(blockHash) + assert.ErrorIs(t, err, ErrBlockRetrieval) + assert.Nil(t, res) +} + +func TestExtrinsicRetriever_GetExtrinsics_ExtrinsicParsingError(t *testing.T) { + extrinsicParserMock := parser.NewExtrinsicParserMock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ](t) + + genericChainMock := generic.NewChainMock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], + ](t) + + stateRPCMock := stateMocks.NewState(t) + registryFactoryMock := registry.NewFactoryMock(t) + chainExecMock := exec.NewRetryableExecutorMock[*generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]](t) + parsingExecMock := exec.NewRetryableExecutorMock[[]*parser.Extrinsic[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]](t) + + extrinsicRetriever := &extrinsicRetriever[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], + ]{ + extrinsicParser: extrinsicParserMock, + genericChain: genericChainMock, + stateRPC: stateRPCMock, + registryFactory: registryFactoryMock, + chainExecutor: chainExecMock, + extrinsicParsingExecutor: parsingExecMock, + } + + testMeta := &types.Metadata{} + + extrinsicRetriever.meta = testMeta + + callRegistry := registry.CallRegistry(map[types.CallIndex]*registry.Type{}) + + extrinsicRetriever.callRegistry = callRegistry + + blockHash := types.NewHash([]byte{0, 1, 2, 3}) + + signedBlock := &generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]{} + + genericChainMock.On("GetBlock", blockHash). + Return(signedBlock, nil). + Once() + + chainExecMock.On("ExecWithFallback", mock.Anything, mock.Anything). + Run( + func(args mock.Arguments) { + execFn, ok := args.Get(0).(func() (*generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], error)) + assert.True(t, ok) + + execFnRes, err := execFn() + assert.NoError(t, err) + assert.Equal(t, signedBlock, execFnRes) + }, + ).Return(signedBlock, nil) + + extrinsicParsingError := errors.New("error") + + extrinsicParserMock.On("ParseExtrinsics", callRegistry, signedBlock). + Return(nil, extrinsicParsingError). + Once() + + stateRPCMock.On("GetMetadata", blockHash). + Return(testMeta, nil). + Once() + + registryFactoryMock.On("CreateCallRegistry", testMeta). + Return(callRegistry, nil). + Once() + + parsingExecMock.On("ExecWithFallback", mock.Anything, mock.Anything). + Run( + func(args mock.Arguments) { + execFn, ok := args.Get(0).(func() ([]*parser.Extrinsic[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], error)) + assert.True(t, ok) + + execFnRes, err := execFn() + assert.ErrorIs(t, err, extrinsicParsingError) + assert.Nil(t, execFnRes) + + fallbackFn, ok := args.Get(1).(func() error) + assert.True(t, ok) + + err = fallbackFn() + assert.NoError(t, err) + }, + ).Return([]*parser.Extrinsic[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]{}, extrinsicParsingError) + + res, err := extrinsicRetriever.GetExtrinsics(blockHash) + assert.ErrorIs(t, err, ErrExtrinsicParsing) + assert.Nil(t, res) +} + +func TestExtrinsicRetriever_updateInternalState(t *testing.T) { + extrinsicParserMock := parser.NewExtrinsicParserMock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ](t) + + genericChainMock := generic.NewChainMock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], + ](t) + + stateRPCMock := stateMocks.NewState(t) + registryFactoryMock := registry.NewFactoryMock(t) + chainExecMock := exec.NewRetryableExecutorMock[*generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]](t) + parsingExecMock := exec.NewRetryableExecutorMock[[]*parser.Extrinsic[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]](t) + + extrinsicRetriever := &extrinsicRetriever[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], + ]{ + extrinsicParser: extrinsicParserMock, + genericChain: genericChainMock, + stateRPC: stateRPCMock, + registryFactory: registryFactoryMock, + chainExecutor: chainExecMock, + extrinsicParsingExecutor: parsingExecMock, + } + + testMeta := &types.Metadata{} + + callRegistry := registry.CallRegistry(map[types.CallIndex]*registry.Type{}) + + blockHash := types.NewHash([]byte{0, 1, 2, 3}) + + stateRPCMock.On("GetMetadata", blockHash). + Return(testMeta, nil). + Once() + + registryFactoryMock.On("CreateCallRegistry", testMeta). + Return(callRegistry, nil). + Once() + + err := extrinsicRetriever.updateInternalState(&blockHash) + assert.NoError(t, err) + + latestMeta := &types.Metadata{} + + stateRPCMock.On("GetMetadataLatest"). + Return(latestMeta, nil). + Once() + + registryFactoryMock.On("CreateCallRegistry", latestMeta). + Return(callRegistry, nil). + Once() + + err = extrinsicRetriever.updateInternalState(nil) + assert.NoError(t, err) +} + +func TestExtrinsicRetriever_updateInternalState_MetadataRetrievalError(t *testing.T) { + extrinsicParserMock := parser.NewExtrinsicParserMock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ](t) + + genericChainMock := generic.NewChainMock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], + ](t) + + stateRPCMock := stateMocks.NewState(t) + registryFactoryMock := registry.NewFactoryMock(t) + chainExecMock := exec.NewRetryableExecutorMock[*generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]](t) + parsingExecMock := exec.NewRetryableExecutorMock[[]*parser.Extrinsic[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]](t) + + extrinsicRetriever := &extrinsicRetriever[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], + ]{ + extrinsicParser: extrinsicParserMock, + genericChain: genericChainMock, + stateRPC: stateRPCMock, + registryFactory: registryFactoryMock, + chainExecutor: chainExecMock, + extrinsicParsingExecutor: parsingExecMock, + } + + metadataRetrievalError := errors.New("error") + + blockHash := types.NewHash([]byte{0, 1, 2, 3}) + + stateRPCMock.On("GetMetadata", blockHash). + Return(nil, metadataRetrievalError). + Once() + + err := extrinsicRetriever.updateInternalState(&blockHash) + assert.ErrorIs(t, err, ErrMetadataRetrieval) + + stateRPCMock.On("GetMetadataLatest"). + Return(nil, metadataRetrievalError). + Once() + + err = extrinsicRetriever.updateInternalState(nil) + assert.ErrorIs(t, err, ErrMetadataRetrieval) +} + +func TestExtrinsicRetriever_updateInternalState_RegistryFactoryError(t *testing.T) { + extrinsicParserMock := parser.NewExtrinsicParserMock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ](t) + + genericChainMock := generic.NewChainMock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], + ](t) + + stateRPCMock := stateMocks.NewState(t) + registryFactoryMock := registry.NewFactoryMock(t) + chainExecMock := exec.NewRetryableExecutorMock[*generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]](t) + parsingExecMock := exec.NewRetryableExecutorMock[[]*parser.Extrinsic[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ]](t) + + extrinsicRetriever := &extrinsicRetriever[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + *generic.SignedBlock[ + types.MultiAddress, + types.MultiSignature, + generic.DefaultPaymentFields, + ], + ]{ + extrinsicParser: extrinsicParserMock, + genericChain: genericChainMock, + stateRPC: stateRPCMock, + registryFactory: registryFactoryMock, + chainExecutor: chainExecMock, + extrinsicParsingExecutor: parsingExecMock, + } + + testMeta := &types.Metadata{} + + registryFactoryError := errors.New("error") + + blockHash := types.NewHash([]byte{0, 1, 2, 3}) + + stateRPCMock.On("GetMetadata", blockHash). + Return(testMeta, nil). + Once() + + registryFactoryMock.On("CreateCallRegistry", testMeta). + Return(nil, registryFactoryError). + Once() + + err := extrinsicRetriever.updateInternalState(&blockHash) + assert.ErrorIs(t, err, ErrCallRegistryCreation) + + latestMeta := &types.Metadata{} + + stateRPCMock.On("GetMetadataLatest"). + Return(latestMeta, nil). + Once() + + registryFactoryMock.On("CreateCallRegistry", latestMeta). + Return(nil, registryFactoryError). + Once() + + err = extrinsicRetriever.updateInternalState(nil) + assert.ErrorIs(t, err, ErrCallRegistryCreation) +} diff --git a/registry/state/event_provider.go b/registry/state/event_provider.go new file mode 100644 index 000000000..226dddd81 --- /dev/null +++ b/registry/state/event_provider.go @@ -0,0 +1,51 @@ +package state + +import ( + libErr "github.com/centrifuge/go-substrate-rpc-client/v4/error" + "github.com/centrifuge/go-substrate-rpc-client/v4/rpc/state" + "github.com/centrifuge/go-substrate-rpc-client/v4/types" +) + +const ( + ErrEventStorageKeyCreation = libErr.Error("event storage key creation") + ErrEventStorageRetrieval = libErr.Error("event storage retrieval") +) + +//go:generate mockery --name EventProvider --structname EventProviderMock --filename event_provider_mock.go --inpackage + +// EventProvider is the interface used for retrieving event data from the storage. +type EventProvider interface { + GetStorageEvents(meta *types.Metadata, blockHash types.Hash) (*types.StorageDataRaw, error) +} + +// eventProvider implements the EventProvider interface. +type eventProvider struct { + stateRPC state.State +} + +// NewEventProvider creates a new EventProvider. +func NewEventProvider(stateRPC state.State) EventProvider { + return &eventProvider{stateRPC: stateRPC} +} + +const ( + storagePrefix = "System" + storageMethod = "Events" +) + +// GetStorageEvents returns the event storage data found at the provided blockHash. +func (p *eventProvider) GetStorageEvents(meta *types.Metadata, blockHash types.Hash) (*types.StorageDataRaw, error) { + key, err := types.CreateStorageKey(meta, storagePrefix, storageMethod, nil) + + if err != nil { + return nil, ErrEventStorageKeyCreation.Wrap(err) + } + + storageData, err := p.stateRPC.GetStorageRaw(key, blockHash) + + if err != nil { + return nil, ErrEventStorageRetrieval.Wrap(err) + } + + return storageData, nil +} diff --git a/registry/state/event_provider_mock.go b/registry/state/event_provider_mock.go new file mode 100644 index 000000000..f35ff131d --- /dev/null +++ b/registry/state/event_provider_mock.go @@ -0,0 +1,51 @@ +// Code generated by mockery v2.13.0-beta.1. DO NOT EDIT. + +package state + +import ( + types "github.com/centrifuge/go-substrate-rpc-client/v4/types" + mock "github.com/stretchr/testify/mock" +) + +// EventProviderMock is an autogenerated mock type for the EventProvider type +type EventProviderMock struct { + mock.Mock +} + +// GetStorageEvents provides a mock function with given fields: meta, blockHash +func (_m *EventProviderMock) GetStorageEvents(meta *types.Metadata, blockHash types.Hash) (*types.StorageDataRaw, error) { + ret := _m.Called(meta, blockHash) + + var r0 *types.StorageDataRaw + if rf, ok := ret.Get(0).(func(*types.Metadata, types.Hash) *types.StorageDataRaw); ok { + r0 = rf(meta, blockHash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.StorageDataRaw) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*types.Metadata, types.Hash) error); ok { + r1 = rf(meta, blockHash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type NewEventProviderMockT interface { + mock.TestingT + Cleanup(func()) +} + +// NewEventProviderMock creates a new instance of EventProviderMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewEventProviderMock(t NewEventProviderMockT) *EventProviderMock { + mock := &EventProviderMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/registry/state/event_provider_test.go b/registry/state/event_provider_test.go new file mode 100644 index 000000000..27cbeb483 --- /dev/null +++ b/registry/state/event_provider_test.go @@ -0,0 +1,52 @@ +package state + +import ( + "errors" + "testing" + + "github.com/centrifuge/go-substrate-rpc-client/v4/rpc/state/mocks" + "github.com/centrifuge/go-substrate-rpc-client/v4/types" + "github.com/centrifuge/go-substrate-rpc-client/v4/types/codec" + "github.com/stretchr/testify/assert" +) + +func TestProvider_GetStorageEvents(t *testing.T) { + stateRPCMock := mocks.NewState(t) + + provider := NewEventProvider(stateRPCMock) + + testHash := types.Hash{} + + var testMeta types.Metadata + + // Empty metadata should cause an error when creating the storage key. + res, err := provider.GetStorageEvents(&testMeta, testHash) + assert.ErrorIs(t, err, ErrEventStorageKeyCreation) + assert.Nil(t, res) + + err = codec.DecodeFromHex(types.MetadataV14Data, &testMeta) + assert.NoError(t, err) + + storageKey, err := types.CreateStorageKey(&testMeta, storagePrefix, storageMethod, nil) + assert.NoError(t, err) + + storageData := &types.StorageDataRaw{} + + stateRPCMock.On("GetStorageRaw", storageKey, testHash). + Return(storageData, nil). + Once() + + res, err = provider.GetStorageEvents(&testMeta, testHash) + assert.NoError(t, err) + assert.Equal(t, storageData, res) + + stateRPCError := errors.New("error") + + stateRPCMock.On("GetStorageRaw", storageKey, testHash). + Return(nil, stateRPCError). + Once() + + res, err = provider.GetStorageEvents(&testMeta, testHash) + assert.ErrorIs(t, err, ErrEventStorageRetrieval) + assert.Nil(t, res) +} diff --git a/rpc/chain/generic/block.go b/rpc/chain/generic/block.go new file mode 100644 index 000000000..38b86be7b --- /dev/null +++ b/rpc/chain/generic/block.go @@ -0,0 +1,265 @@ +package generic + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/centrifuge/go-substrate-rpc-client/v4/scale" + "github.com/centrifuge/go-substrate-rpc-client/v4/types" + "github.com/centrifuge/go-substrate-rpc-client/v4/types/codec" +) + +// DefaultGenericSignedBlock is the SignedBlock with defaults for the generic types: +// +// Address - types.MultiAddress +// Signature - types.MultiSignature +// PaymentFields - DefaultPaymentFields +type DefaultGenericSignedBlock = SignedBlock[ + types.MultiAddress, + types.MultiSignature, + DefaultPaymentFields, +] + +// GenericSignedBlock is the interface that represents the block of a particular chain. +// +// This interface is generic over types A, S, P, please check GenericExtrinsicSignature for more +// information about these generic types. +// +//nolint:revive +type GenericSignedBlock[A, S, P any] interface { + GetGenericBlock() GenericBlock[A, S, P] + GetJustification() []byte +} + +// GenericBlock is the interface that holds information about the header and extrinsics of a block. +// +// This interface is generic over types A, S, P, please check GenericExtrinsicSignature for more +// information about these generic types. +// +//nolint:revive +type GenericBlock[A, S, P any] interface { + GetHeader() types.Header + GetExtrinsics() []GenericExtrinsic[A, S, P] +} + +// GenericExtrinsic is the interface that holds the extrinsic information. +// +// This interface is generic over types A, S, P, please check GenericExtrinsicSignature for more +// information about these generic types. +// +//nolint:revive +type GenericExtrinsic[A, S, P any] interface { + GetVersion() byte + GetSignature() GenericExtrinsicSignature[A, S, P] + GetCall() types.Call +} + +// GenericExtrinsicSignature is the interface that holds the extrinsic signature information. +// +// This interface is generic over the following types: +// +// A - Signer, the default implementation for this is the types.MultiAddress type +// which can support a variable number of addresses. +// +// S - Signature, the default implementation for this is the types.MultiSignature type which can support +// multiple signature curves. +// +// P - PaymentFields (ChargeAssetTx in substrate), the default implementation for this is the DefaultPaymentFields which +// holds information about the tip sent for the extrinsic. +// +//nolint:revive +type GenericExtrinsicSignature[A, S, P any] interface { + GetSigner() A + GetSignature() S + GetEra() types.ExtrinsicEra + GetNonce() types.UCompact + GetPaymentFields() P +} + +// SignedBlock implements the GenericSignedBlock interface. +type SignedBlock[A, S, P any] struct { + Block *Block[A, S, P] `json:"block"` + Justification []byte `json:"justification"` +} + +func (s *SignedBlock[A, S, P]) GetGenericBlock() GenericBlock[A, S, P] { + return s.Block +} + +func (s *SignedBlock[A, S, P]) GetJustification() []byte { + return s.Justification +} + +// Block implements the GenericBlock interface. +type Block[A, S, P any] struct { + Header types.Header `json:"header"` + Extrinsics []*Extrinsic[A, S, P] `json:"extrinsics"` +} + +//nolint:revive +func (b *Block[A, S, P]) GetHeader() types.Header { + return b.Header +} + +//nolint:revive +func (b *Block[A, S, P]) GetExtrinsics() []GenericExtrinsic[A, S, P] { + var res []GenericExtrinsic[A, S, P] + + for _, ext := range b.Extrinsics { + res = append(res, ext) + } + return res +} + +// Extrinsic implements the GenericExtrinsic interface. +type Extrinsic[A, S, P any] struct { + // Version is the encoded version flag (which encodes the raw transaction version and signing information in one byte) + Version byte `json:"version"` + // Signature is the ExtrinsicSignature, its presence depends on the Version flag + Signature *ExtrinsicSignature[A, S, P] `json:"signature"` + // Method is the call this extrinsic wraps + Method types.Call `json:"method"` +} + +//nolint:revive +func (e *Extrinsic[A, S, P]) GetVersion() byte { + return e.Version +} + +//nolint:revive +func (e *Extrinsic[A, S, P]) GetSignature() GenericExtrinsicSignature[A, S, P] { + return e.Signature +} + +//nolint:revive +func (e *Extrinsic[A, S, P]) GetCall() types.Call { + return e.Method +} + +// UnmarshalJSON fills Extrinsic with the JSON encoded byte array given by bz +//nolint:revive +func (e *Extrinsic[A, S, P]) UnmarshalJSON(bz []byte) error { + var tmp string + if err := json.Unmarshal(bz, &tmp); err != nil { + return err + } + + var l types.UCompact + + if err := codec.DecodeFromHex(tmp, &l); err != nil { + return err + } + + prefix, err := codec.EncodeToHex(l) + if err != nil { + return err + } + + // determine whether length prefix is there + if strings.HasPrefix(tmp, prefix) { + return codec.DecodeFromHex(tmp, e) + } + + // not there, prepend with compact encoded length prefix + dec, err := codec.HexDecodeString(tmp) + if err != nil { + return err + } + length := types.NewUCompactFromUInt(uint64(len(dec))) + bprefix, err := codec.Encode(length) + if err != nil { + return err + } + bprefix = append(bprefix, dec...) + return codec.Decode(bprefix, e) +} + +// IsSigned returns true if the extrinsic is signed. +//nolint:revive +func (e *Extrinsic[A, S, P]) IsSigned() bool { + return e.Version&types.ExtrinsicBitSigned == types.ExtrinsicBitSigned +} + +// Type returns the raw transaction version. +//nolint:revive +func (e *Extrinsic[A, S, P]) Type() uint8 { + return e.Version & types.ExtrinsicUnmaskVersion +} + +// Decode decodes the extrinsic based on the data present in the decoder. +//nolint:revive +func (e *Extrinsic[A, S, P]) Decode(decoder scale.Decoder) error { + // compact length encoding (1, 2, or 4 bytes) (may not be there for Extrinsics older than Jan 11 2019) + if _, err := decoder.DecodeUintCompact(); err != nil { + return err + } + + if err := decoder.Decode(&e.Version); err != nil { + return err + } + + if e.IsSigned() { + if e.Type() != types.ExtrinsicVersion4 { + return fmt.Errorf("unsupported extrinsic version: %v (isSigned: %v, type: %v)", e.Version, e.IsSigned(), + e.Type()) + } + + e.Signature = new(ExtrinsicSignature[A, S, P]) + + if err := decoder.Decode(&e.Signature); err != nil { + return err + } + } + + if err := decoder.Decode(&e.Method); err != nil { + return err + } + + return nil +} + +// ExtrinsicSignature implements the GenericExtrinsicSignature interface. +type ExtrinsicSignature[A, S, P any] struct { + Signer A + Signature S + Era types.ExtrinsicEra + Nonce types.UCompact + PaymentFields P +} + +//nolint:revive +func (e *ExtrinsicSignature[A, S, P]) GetSigner() A { + return e.Signer +} + +//nolint:revive +func (e *ExtrinsicSignature[A, S, P]) GetSignature() S { + return e.Signature +} + +//nolint:revive +func (e *ExtrinsicSignature[A, S, P]) GetEra() types.ExtrinsicEra { + return e.Era +} + +//nolint:revive +func (e *ExtrinsicSignature[A, S, P]) GetNonce() types.UCompact { + return e.Nonce +} + +//nolint:revive +func (e *ExtrinsicSignature[A, S, P]) GetPaymentFields() P { + return e.PaymentFields +} + +// DefaultPaymentFields represents the default payment fields found in the extrinsics of most substrate chains. +type DefaultPaymentFields struct { + Tip types.UCompact +} + +// PaymentFieldsWithAssetID represents the payment fields found on chains that require an asset ID, such as statemint. +type PaymentFieldsWithAssetID struct { + Tip types.UCompact + AssetID types.Option[types.UCompact] +} diff --git a/rpc/chain/generic/chain.go b/rpc/chain/generic/chain.go new file mode 100644 index 000000000..b82b19458 --- /dev/null +++ b/rpc/chain/generic/chain.go @@ -0,0 +1,91 @@ +package generic + +import ( + "github.com/centrifuge/go-substrate-rpc-client/v4/client" + libErr "github.com/centrifuge/go-substrate-rpc-client/v4/error" + "github.com/centrifuge/go-substrate-rpc-client/v4/types" +) + +const ( + ErrGetBlockCall = libErr.Error("get block call") +) + +//go:generate mockery --name Chain --structname ChainMock --filename chain_mock.go --inpackage + +// Chain defines an interface for a client that can return a generic block B. +// +// The generic block B allows for multiple implementation of the block that can differ +// for each substrate chain implementation. +type Chain[ + A, S, P any, + B GenericSignedBlock[A, S, P], +] interface { + GetBlock(blockHash types.Hash) (B, error) + GetBlockLatest() (B, error) +} + +// genericChain implements the Chain interface. +type genericChain[ + A, S, P any, + B GenericSignedBlock[A, S, P], +] struct { + client client.Client +} + +// NewChain creates a new instance of Chain. +func NewChain[ + A, S, P any, + B GenericSignedBlock[A, S, P], +](client client.Client) Chain[A, S, P, B] { + return &genericChain[A, S, P, B]{ + client: client, + } +} + +// DefaultChain is the Chain interface with defaults for the generic types: +// +// Address - types.MultiAddress +// Signature - types.MultiSignature +// PaymentFields - DefaultPaymentFields +// Block - *DefaultGenericSignedBlock +type DefaultChain = Chain[ + types.MultiAddress, + types.MultiSignature, + DefaultPaymentFields, + *DefaultGenericSignedBlock, +] + +// NewDefaultChain creates a new DefaultChain. +func NewDefaultChain(client client.Client) DefaultChain { + return NewChain[ + types.MultiAddress, + types.MultiSignature, + DefaultPaymentFields, + *DefaultGenericSignedBlock, + ](client) +} + +// GetBlock retrieves a generic block B found at blockHash. +func (g *genericChain[A, S, P, B]) GetBlock(blockHash types.Hash) (B, error) { + return g.getBlock(&blockHash) +} + +// GetBlockLatest returns the latest generic block B. +func (g *genericChain[A, S, P, B]) GetBlockLatest() (B, error) { + return g.getBlock(nil) +} + +const ( + getBlockMethod = "chain_getBlock" +) + +// getBlock retrieves the generic block B. +func (g *genericChain[A, S, P, B]) getBlock(blockHash *types.Hash) (B, error) { + block := new(B) + + if err := client.CallWithBlockHash(g.client, block, getBlockMethod, blockHash); err != nil { + return *block, ErrGetBlockCall.Wrap(err) + } + + return *block, nil +} diff --git a/rpc/chain/generic/chain_mock.go b/rpc/chain/generic/chain_mock.go new file mode 100644 index 000000000..8b902fec8 --- /dev/null +++ b/rpc/chain/generic/chain_mock.go @@ -0,0 +1,70 @@ +// Code generated by mockery v2.13.0-beta.1. DO NOT EDIT. + +package generic + +import ( + types "github.com/centrifuge/go-substrate-rpc-client/v4/types" + mock "github.com/stretchr/testify/mock" +) + +// ChainMock is an autogenerated mock type for the Chain type +type ChainMock[A interface{}, S interface{}, P interface{}, B GenericSignedBlock[A, S, P]] struct { + mock.Mock +} + +// GetBlock provides a mock function with given fields: blockHash +func (_m *ChainMock[A, S, P, B]) GetBlock(blockHash types.Hash) (B, error) { + ret := _m.Called(blockHash) + + var r0 B + if rf, ok := ret.Get(0).(func(types.Hash) B); ok { + r0 = rf(blockHash) + } else { + r0 = ret.Get(0).(B) + } + + var r1 error + if rf, ok := ret.Get(1).(func(types.Hash) error); ok { + r1 = rf(blockHash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetBlockLatest provides a mock function with given fields: +func (_m *ChainMock[A, S, P, B]) GetBlockLatest() (B, error) { + ret := _m.Called() + + var r0 B + if rf, ok := ret.Get(0).(func() B); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(B) + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type NewChainMockT interface { + mock.TestingT + Cleanup(func()) +} + +// NewChainMock creates a new instance of ChainMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewChainMock[A interface{}, S interface{}, P interface{}, B GenericSignedBlock[A, S, P]](t NewChainMockT) *ChainMock[A, S, P, B] { + mock := &ChainMock[A, S, P, B]{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/rpc/chain/generic/chain_test.go b/rpc/chain/generic/chain_test.go new file mode 100644 index 000000000..28a73b48d --- /dev/null +++ b/rpc/chain/generic/chain_test.go @@ -0,0 +1,291 @@ +package generic + +import ( + "errors" + "fmt" + "testing" + + "github.com/centrifuge/go-substrate-rpc-client/v4/client/mocks" + "github.com/centrifuge/go-substrate-rpc-client/v4/types" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGenericDefaultChain_GetBlock(t *testing.T) { + clientMock := mocks.NewClient(t) + + genericChain := NewDefaultChain(clientMock) + + blockHash := types.Hash{1, 2, 3} + + blockJustification := []byte{4, 5, 6} + + clientMock.On("Call", mock.Anything, mock.Anything, mock.Anything). + Run( + func(args mock.Arguments) { + target, ok := args.Get(0).(**DefaultGenericSignedBlock) + assert.True(t, ok) + + *target = new(DefaultGenericSignedBlock) + (**target).Justification = blockJustification + + method, ok := args.Get(1).(string) + assert.True(t, ok) + assert.Equal(t, getBlockMethod, method) + + hashStr, ok := args.Get(2).(string) + assert.True(t, ok) + + encodedBlockHash := hexutil.Encode(blockHash[:]) + assert.Equal(t, encodedBlockHash, hashStr) + }, + ).Return(nil) + + res, err := genericChain.GetBlock(blockHash) + assert.NoError(t, err) + assert.Equal(t, blockJustification, res.GetJustification()) +} + +func TestGenericDefaultChain_GetBlock_BlockCallError(t *testing.T) { + clientMock := mocks.NewClient(t) + + genericChain := NewDefaultChain(clientMock) + + blockHash := types.Hash{1, 2, 3} + + clientMock.On("Call", mock.Anything, mock.Anything, mock.Anything). + Run( + func(args mock.Arguments) { + _, ok := args.Get(0).(**DefaultGenericSignedBlock) + assert.True(t, ok) + + method, ok := args.Get(1).(string) + assert.True(t, ok) + assert.Equal(t, getBlockMethod, method) + + hashStr, ok := args.Get(2).(string) + assert.True(t, ok) + + encodedBlockHash := hexutil.Encode(blockHash[:]) + assert.Equal(t, encodedBlockHash, hashStr) + }, + ).Return(errors.New("error")) + + res, err := genericChain.GetBlock(blockHash) + assert.ErrorIs(t, err, ErrGetBlockCall) + assert.Nil(t, res) +} + +func TestGenericDefaultChain_GetBlockLatest(t *testing.T) { + clientMock := mocks.NewClient(t) + + genericChain := NewDefaultChain(clientMock) + + blockJustification := []byte{4, 5, 6} + + clientMock.On("Call", mock.Anything, mock.Anything, mock.Anything). + Run( + func(args mock.Arguments) { + target, ok := args.Get(0).(**DefaultGenericSignedBlock) + assert.True(t, ok) + + *target = new(DefaultGenericSignedBlock) + (**target).Justification = blockJustification + + method, ok := args.Get(1).(string) + assert.True(t, ok) + assert.Equal(t, getBlockMethod, method) + }, + ).Return(nil) + + res, err := genericChain.GetBlockLatest() + assert.NoError(t, err) + assert.Equal(t, blockJustification, res.Justification) +} + +func TestGenericDefaultChain_GetBlockLatest_BlockCallError(t *testing.T) { + clientMock := mocks.NewClient(t) + + genericChain := NewDefaultChain(clientMock) + + clientMock.On("Call", mock.Anything, mock.Anything, mock.Anything). + Run( + func(args mock.Arguments) { + _, ok := args.Get(0).(**DefaultGenericSignedBlock) + assert.True(t, ok) + + method, ok := args.Get(1).(string) + assert.True(t, ok) + assert.Equal(t, getBlockMethod, method) + }, + ).Return(errors.New("error")) + + res, err := genericChain.GetBlockLatest() + assert.ErrorIs(t, err, ErrGetBlockCall) + assert.Nil(t, res) +} + +type testRunner interface { + runTest(t *testing.T) +} + +type testBlockData[ + A any, + S any, + P any, +] struct{} + +func (td *testBlockData[A, S, P]) getGenericTestTypeNames() string { + var ( + a A + s S + p P + ) + + return fmt.Sprintf("%T - %T - %T", a, s, p) +} + +func (td *testBlockData[A, S, P]) getTestName(testName string) string { + return fmt.Sprintf("%s with: %s", testName, td.getGenericTestTypeNames()) +} + +func (td *testBlockData[A, S, P]) runTest(t *testing.T) { + t.Run(td.getTestName("Get block"), func(t *testing.T) { + clientMock := mocks.NewClient(t) + + genericChain := NewChain[A, S, P, *SignedBlock[A, S, P]](clientMock) + + blockHash := types.Hash{1, 2, 3} + + blockJustification := []byte{4, 5, 6} + + clientMock.On("Call", mock.Anything, mock.Anything, mock.Anything). + Run( + func(args mock.Arguments) { + target, ok := args.Get(0).(**SignedBlock[A, S, P]) + assert.True(t, ok) + + *target = new(SignedBlock[A, S, P]) + (*target).Justification = blockJustification + + method, ok := args.Get(1).(string) + assert.True(t, ok) + assert.Equal(t, getBlockMethod, method) + + hashStr, ok := args.Get(2).(string) + assert.True(t, ok) + + encodedBlockHash := hexutil.Encode(blockHash[:]) + assert.Equal(t, encodedBlockHash, hashStr) + }, + ).Return(nil) + + res, err := genericChain.GetBlock(blockHash) + assert.NoError(t, err) + assert.Equal(t, blockJustification, res.GetJustification()) + }) + + t.Run(td.getTestName("Get block - block call error"), func(t *testing.T) { + clientMock := mocks.NewClient(t) + + genericChain := NewChain[A, S, P, *SignedBlock[A, S, P]](clientMock) + + blockHash := types.Hash{1, 2, 3} + + clientMock.On("Call", mock.Anything, mock.Anything, mock.Anything). + Run( + func(args mock.Arguments) { + _, ok := args.Get(0).(**SignedBlock[A, S, P]) + assert.True(t, ok) + + method, ok := args.Get(1).(string) + assert.True(t, ok) + assert.Equal(t, getBlockMethod, method) + + hashStr, ok := args.Get(2).(string) + assert.True(t, ok) + + encodedBlockHash := hexutil.Encode(blockHash[:]) + assert.Equal(t, encodedBlockHash, hashStr) + }, + ).Return(errors.New("error")) + + res, err := genericChain.GetBlock(blockHash) + assert.ErrorIs(t, err, ErrGetBlockCall) + assert.Nil(t, res) + }) + + t.Run(td.getTestName("Get latest block"), func(t *testing.T) { + clientMock := mocks.NewClient(t) + + genericChain := NewChain[A, S, P, *SignedBlock[A, S, P]](clientMock) + + blockJustification := []byte{4, 5, 6} + + clientMock.On("Call", mock.Anything, mock.Anything, mock.Anything). + Run( + func(args mock.Arguments) { + target, ok := args.Get(0).(**SignedBlock[A, S, P]) + assert.True(t, ok) + + *target = new(SignedBlock[A, S, P]) + (*target).Justification = blockJustification + + method, ok := args.Get(1).(string) + assert.True(t, ok) + assert.Equal(t, getBlockMethod, method) + }, + ).Return(nil) + + res, err := genericChain.GetBlockLatest() + assert.NoError(t, err) + assert.Equal(t, blockJustification, res.GetJustification()) + }) + + t.Run(td.getTestName("Get latest block - block call error"), func(t *testing.T) { + clientMock := mocks.NewClient(t) + + genericChain := NewChain[A, S, P, *SignedBlock[A, S, P]](clientMock) + + clientMock.On("Call", mock.Anything, mock.Anything, mock.Anything). + Run( + func(args mock.Arguments) { + _, ok := args.Get(0).(**SignedBlock[A, S, P]) + assert.True(t, ok) + + method, ok := args.Get(1).(string) + assert.True(t, ok) + assert.Equal(t, getBlockMethod, method) + }, + ).Return(errors.New("error")) + + res, err := genericChain.GetBlockLatest() + assert.ErrorIs(t, err, ErrGetBlockCall) + assert.Nil(t, res) + }) +} + +func TestGenericChain(t *testing.T) { + testRunners := []testRunner{ + &testBlockData[ + types.MultiAddress, + types.MultiSignature, + DefaultPaymentFields, + ]{}, + &testBlockData[ + types.MultiAddress, + types.MultiSignature, + PaymentFieldsWithAssetID, + ]{}, + &testBlockData[ + [20]byte, + [65]byte, + DefaultPaymentFields, + ]{}, + } + + for _, test := range testRunners { + test.runTest(t) + } +} diff --git a/scale/codec.go b/scale/codec.go index 76b9b286f..183249212 100644 --- a/scale/codec.go +++ b/scale/codec.go @@ -492,7 +492,12 @@ func (pd Decoder) DecodeIntoReflectValue(target reflect.Value) error { // DecodeUintCompact decodes a compact-encoded integer. See EncodeUintCompact method. func (pd Decoder) DecodeUintCompact() (*big.Int, error) { - b, _ := pd.ReadOneByte() + b, err := pd.ReadOneByte() + + if err != nil { + return nil, err + } + mode := b & 3 switch mode { case 0: diff --git a/types/bitvec.go b/types/bitvec.go new file mode 100644 index 000000000..ea1e6ac90 --- /dev/null +++ b/types/bitvec.go @@ -0,0 +1,108 @@ +package types + +import ( + "errors" + "fmt" + "math" + "math/bits" + "strings" + + "github.com/centrifuge/go-substrate-rpc-client/v4/scale" +) + +type BitOrder uint + +const ( + BitOrderLsb0 BitOrder = iota + BitOrderMsb0 +) + +var ( + BitOrderName = map[BitOrder]string{ + BitOrderLsb0: "Lsb0", + BitOrderMsb0: "Msb0", + } + + BitOrderValue = map[string]BitOrder{ + "Lsb0": BitOrderLsb0, + "Msb0": BitOrderMsb0, + } +) + +func (b *BitOrder) String() string { + return BitOrderName[*b] +} + +func NewBitOrderFromString(s string) (BitOrder, error) { + bitOrder, ok := BitOrderValue[s] + + if !ok { + return 0, fmt.Errorf("bit order '%s' not supported", s) + } + + return bitOrder, nil +} + +type BitVec struct { + BitOrder BitOrder + + ByteSlice []uint8 +} + +func NewBitVec(bitOrder BitOrder) *BitVec { + return &BitVec{ + BitOrder: bitOrder, + } +} + +func (b *BitVec) Decode(decoder scale.Decoder) error { + numBytes, err := b.GetMinimumNumberOfBytes(decoder) + + if err != nil { + return err + } + + for i := uint(0); i < numBytes; i++ { + decodedByte, err := decoder.ReadOneByte() + + if err != nil { + return err + } + + if b.BitOrder == BitOrderLsb0 { + decodedByte = bits.Reverse8(decodedByte) + } + + b.ByteSlice = append(b.ByteSlice, decodedByte) + } + + return nil +} + +func (b *BitVec) GetMinimumNumberOfBytes(decoder scale.Decoder) (uint, error) { + nb, err := decoder.DecodeUintCompact() + + if err != nil { + return 0, err + } + + numberOfBits := nb.Uint64() + + if numberOfBits == 0 { + return 0, errors.New("invalid number of bits") + } + + return uint(math.Ceil(float64(numberOfBits) / 8)), nil +} + +func (b *BitVec) String() string { + var sb strings.Builder + + sb.WriteString("0b") + + for _, byte := range b.ByteSlice { + sb.WriteString(fmt.Sprintf("%08b", byte)) + } + + return sb.String() +} diff --git a/types/bitvec_test.go b/types/bitvec_test.go new file mode 100644 index 000000000..cd11dd866 --- /dev/null +++ b/types/bitvec_test.go @@ -0,0 +1,166 @@ +package types + +import ( + "bytes" + "testing" + + "github.com/centrifuge/go-substrate-rpc-client/v4/scale" + "github.com/stretchr/testify/assert" +) + +func TestBitVec_Decode(t *testing.T) { + tests := []struct { + ScaleEncodedVal []byte + ExpectedResult string + BitOrder BitOrder + }{ + { + ScaleEncodedVal: []byte{4, 0}, + ExpectedResult: "0b00000000", + BitOrder: BitOrderLsb0, + }, + { + ScaleEncodedVal: []byte{4, 1}, + ExpectedResult: "0b10000000", + BitOrder: BitOrderLsb0, + }, + { + ScaleEncodedVal: []byte{8, 0}, + ExpectedResult: "0b00000000", + BitOrder: BitOrderLsb0, + }, + { + ScaleEncodedVal: []byte{8, 2}, + ExpectedResult: "0b01000000", + BitOrder: BitOrderLsb0, + }, + { + ScaleEncodedVal: []byte{8, 1}, + ExpectedResult: "0b10000000", + BitOrder: BitOrderLsb0, + }, + { + ScaleEncodedVal: []byte{8, 3}, + ExpectedResult: "0b11000000", + BitOrder: BitOrderLsb0, + }, + { + ScaleEncodedVal: []byte{12, 5}, + ExpectedResult: "0b10100000", + BitOrder: BitOrderLsb0, + }, + { + ScaleEncodedVal: []byte{28, 106}, + ExpectedResult: "0b01010110", + BitOrder: BitOrderLsb0, + }, + { + ScaleEncodedVal: []byte{28, 106}, + ExpectedResult: "0b01010110", + BitOrder: BitOrderLsb0, + }, + { + ScaleEncodedVal: []byte{32, 106}, + ExpectedResult: "0b01010110", + BitOrder: BitOrderLsb0, + }, + { + ScaleEncodedVal: []byte{36, 107, 1}, + ExpectedResult: "0b1101011010000000", + BitOrder: BitOrderLsb0, + }, + { + ScaleEncodedVal: []byte{60, 53, 53}, + ExpectedResult: "0b1010110010101100", + BitOrder: BitOrderLsb0, + }, + { + ScaleEncodedVal: []byte{64, 106, 106}, + ExpectedResult: "0b0101011001010110", + BitOrder: BitOrderLsb0, + }, + { + ScaleEncodedVal: []byte{68, 106, 106, 0}, + ExpectedResult: "0b010101100101011000000000", + BitOrder: BitOrderLsb0, + }, + { + ScaleEncodedVal: []byte{4, 0}, + ExpectedResult: "0b00000000", + BitOrder: BitOrderMsb0, + }, + { + ScaleEncodedVal: []byte{4, 128}, + ExpectedResult: "0b10000000", + BitOrder: BitOrderMsb0, + }, + { + ScaleEncodedVal: []byte{8, 0}, + ExpectedResult: "0b00000000", + BitOrder: BitOrderMsb0, + }, + { + ScaleEncodedVal: []byte{8, 128}, + ExpectedResult: "0b10000000", + BitOrder: BitOrderMsb0, + }, + { + ScaleEncodedVal: []byte{8, 64}, + ExpectedResult: "0b01000000", + BitOrder: BitOrderMsb0, + }, + { + ScaleEncodedVal: []byte{8, 192}, + ExpectedResult: "0b11000000", + BitOrder: BitOrderMsb0, + }, + { + ScaleEncodedVal: []byte{12, 160}, + ExpectedResult: "0b10100000", + BitOrder: BitOrderMsb0, + }, + { + ScaleEncodedVal: []byte{28, 86}, + ExpectedResult: "0b01010110", + BitOrder: BitOrderMsb0, + }, + { + ScaleEncodedVal: []byte{32, 86}, + ExpectedResult: "0b01010110", + BitOrder: BitOrderMsb0, + }, + { + ScaleEncodedVal: []byte{36, 214, 128}, + ExpectedResult: "0b1101011010000000", + BitOrder: BitOrderMsb0, + }, + { + ScaleEncodedVal: []byte{60, 172, 172}, + ExpectedResult: "0b1010110010101100", + BitOrder: BitOrderMsb0, + }, + { + ScaleEncodedVal: []byte{64, 86, 86}, + ExpectedResult: "0b0101011001010110", + BitOrder: BitOrderMsb0, + }, + { + ScaleEncodedVal: []byte{68, 86, 86, 0}, + ExpectedResult: "0b010101100101011000000000", + BitOrder: BitOrderMsb0, + }, + } + + for _, test := range tests { + b := NewBitVec(test.BitOrder) + + decoder := scale.NewDecoder(bytes.NewReader(test.ScaleEncodedVal)) + + if err := b.Decode(*decoder); err != nil { + assert.NoError(t, err) + return + } + + assert.Equal(t, test.ExpectedResult, b.String()) + } +} diff --git a/types/dispatch_result_with_post_info.go b/types/dispatch_result_with_post_info.go index 6b2aeb0d4..486d0b2bd 100644 --- a/types/dispatch_result_with_post_info.go +++ b/types/dispatch_result_with_post_info.go @@ -16,7 +16,9 @@ package types -import "github.com/centrifuge/go-substrate-rpc-client/v4/scale" +import ( + "github.com/centrifuge/go-substrate-rpc-client/v4/scale" +) // PostDispatchInfo is used in DispatchResultWithPostInfo. // Weight information that is only available post dispatch. diff --git a/types/generic_test.go b/types/generic_test.go index c0a2b4691..4b3a1dc53 100644 --- a/types/generic_test.go +++ b/types/generic_test.go @@ -4,6 +4,7 @@ import ( "testing" . "github.com/centrifuge/go-substrate-rpc-client/v4/types" + . "github.com/centrifuge/go-substrate-rpc-client/v4/types/codec" . "github.com/centrifuge/go-substrate-rpc-client/v4/types/test_utils" fuzz "github.com/google/gofuzz"