diff --git a/cmd/arc/services/metamorph.go b/cmd/arc/services/metamorph.go index c68c78134..f8a5c1677 100644 --- a/cmd/arc/services/metamorph.go +++ b/cmd/arc/services/metamorph.go @@ -10,6 +10,7 @@ import ( "github.com/bitcoin-sv/arc/internal/cache" "github.com/bitcoin-sv/arc/internal/tracing" + "github.com/bitcoin-sv/arc/pkg/callbacker" "github.com/libsv/go-p2p" "github.com/ordishs/go-bitcoin" @@ -57,7 +58,7 @@ func StartMetamorph(logger *slog.Logger, arcConfig *config.ArcConfig, cacheStore optsServer := make([]metamorph.ServerOption, 0) processorOpts := make([]metamorph.Option, 0) - callbackerOpts := make([]metamorph.CallbackerOption, 0) + callbackerOpts := make([]callbacker.Option, 0) if arcConfig.IsTracingEnabled() { cleanup, err := tracing.Enable(logger, "metamorph", arcConfig.Tracing) @@ -68,7 +69,7 @@ func StartMetamorph(logger *slog.Logger, arcConfig *config.ArcConfig, cacheStore } optsServer = append(optsServer, metamorph.WithTracer(arcConfig.Tracing.KeyValueAttributes...)) - callbackerOpts = append(callbackerOpts, metamorph.WithTracerCallbacker(arcConfig.Tracing.KeyValueAttributes...)) + callbackerOpts = append(callbackerOpts, callbacker.WithTracerCallbacker(arcConfig.Tracing.KeyValueAttributes...)) processorOpts = append(processorOpts, metamorph.WithTracerProcessor(arcConfig.Tracing.KeyValueAttributes...)) } @@ -125,7 +126,7 @@ func StartMetamorph(logger *slog.Logger, arcConfig *config.ArcConfig, cacheStore return nil, fmt.Errorf("failed to create callbacker client: %v", err) } - callbacker := metamorph.NewGrpcCallbacker(callbackerConn, procLogger, callbackerOpts...) + callbacker := callbacker.NewGrpcCallbacker(callbackerConn, procLogger, callbackerOpts...) processorOpts = append(processorOpts, metamorph.WithCacheExpiryTime(mtmConfig.ProcessorCacheExpiryTime), metamorph.WithProcessExpiredTxsInterval(mtmConfig.UnseenTransactionRebroadcastingInterval), diff --git a/internal/callbacker/callbacker_api/callbacker_api_client_mock.go b/internal/callbacker/callbacker_api/callbacker_api_client_mock.go new file mode 100644 index 000000000..67f4390f8 --- /dev/null +++ b/internal/callbacker/callbacker_api/callbacker_api_client_mock.go @@ -0,0 +1,145 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package callbacker_api + +import ( + context "context" + grpc "google.golang.org/grpc" + emptypb "google.golang.org/protobuf/types/known/emptypb" + sync "sync" +) + +// Ensure, that CallbackerAPIClientMock does implement CallbackerAPIClient. +// If this is not the case, regenerate this file with moq. +var _ CallbackerAPIClient = &CallbackerAPIClientMock{} + +// CallbackerAPIClientMock is a mock implementation of CallbackerAPIClient. +// +// func TestSomethingThatUsesCallbackerAPIClient(t *testing.T) { +// +// // make and configure a mocked CallbackerAPIClient +// mockedCallbackerAPIClient := &CallbackerAPIClientMock{ +// HealthFunc: func(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*HealthResponse, error) { +// panic("mock out the Health method") +// }, +// SendCallbackFunc: func(ctx context.Context, in *SendCallbackRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { +// panic("mock out the SendCallback method") +// }, +// } +// +// // use mockedCallbackerAPIClient in code that requires CallbackerAPIClient +// // and then make assertions. +// +// } +type CallbackerAPIClientMock struct { + // HealthFunc mocks the Health method. + HealthFunc func(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*HealthResponse, error) + + // SendCallbackFunc mocks the SendCallback method. + SendCallbackFunc func(ctx context.Context, in *SendCallbackRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + + // calls tracks calls to the methods. + calls struct { + // Health holds details about calls to the Health method. + Health []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // In is the in argument value. + In *emptypb.Empty + // Opts is the opts argument value. + Opts []grpc.CallOption + } + // SendCallback holds details about calls to the SendCallback method. + SendCallback []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // In is the in argument value. + In *SendCallbackRequest + // Opts is the opts argument value. + Opts []grpc.CallOption + } + } + lockHealth sync.RWMutex + lockSendCallback sync.RWMutex +} + +// Health calls HealthFunc. +func (mock *CallbackerAPIClientMock) Health(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*HealthResponse, error) { + if mock.HealthFunc == nil { + panic("CallbackerAPIClientMock.HealthFunc: method is nil but CallbackerAPIClient.Health was just called") + } + callInfo := struct { + Ctx context.Context + In *emptypb.Empty + Opts []grpc.CallOption + }{ + Ctx: ctx, + In: in, + Opts: opts, + } + mock.lockHealth.Lock() + mock.calls.Health = append(mock.calls.Health, callInfo) + mock.lockHealth.Unlock() + return mock.HealthFunc(ctx, in, opts...) +} + +// HealthCalls gets all the calls that were made to Health. +// Check the length with: +// +// len(mockedCallbackerAPIClient.HealthCalls()) +func (mock *CallbackerAPIClientMock) HealthCalls() []struct { + Ctx context.Context + In *emptypb.Empty + Opts []grpc.CallOption +} { + var calls []struct { + Ctx context.Context + In *emptypb.Empty + Opts []grpc.CallOption + } + mock.lockHealth.RLock() + calls = mock.calls.Health + mock.lockHealth.RUnlock() + return calls +} + +// SendCallback calls SendCallbackFunc. +func (mock *CallbackerAPIClientMock) SendCallback(ctx context.Context, in *SendCallbackRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + if mock.SendCallbackFunc == nil { + panic("CallbackerAPIClientMock.SendCallbackFunc: method is nil but CallbackerAPIClient.SendCallback was just called") + } + callInfo := struct { + Ctx context.Context + In *SendCallbackRequest + Opts []grpc.CallOption + }{ + Ctx: ctx, + In: in, + Opts: opts, + } + mock.lockSendCallback.Lock() + mock.calls.SendCallback = append(mock.calls.SendCallback, callInfo) + mock.lockSendCallback.Unlock() + return mock.SendCallbackFunc(ctx, in, opts...) +} + +// SendCallbackCalls gets all the calls that were made to SendCallback. +// Check the length with: +// +// len(mockedCallbackerAPIClient.SendCallbackCalls()) +func (mock *CallbackerAPIClientMock) SendCallbackCalls() []struct { + Ctx context.Context + In *SendCallbackRequest + Opts []grpc.CallOption +} { + var calls []struct { + Ctx context.Context + In *SendCallbackRequest + Opts []grpc.CallOption + } + mock.lockSendCallback.RLock() + calls = mock.calls.SendCallback + mock.lockSendCallback.RUnlock() + return calls +} diff --git a/internal/callbacker/callbacker_mocks.go b/internal/callbacker/callbacker_mocks.go index 333b73c4a..f3b2d6ce1 100644 --- a/internal/callbacker/callbacker_mocks.go +++ b/internal/callbacker/callbacker_mocks.go @@ -2,3 +2,5 @@ package callbacker // from callbacker.go //go:generate moq -out ./callbacker_mock.go ./ SenderI + +//go:generate moq -out ./callbacker_api/callbacker_api_client_mock.go ./callbacker_api/ CallbackerAPIClient diff --git a/internal/metamorph/grpc_callbacker.go b/pkg/callbacker/callbacker.go similarity index 94% rename from internal/metamorph/grpc_callbacker.go rename to pkg/callbacker/callbacker.go index e5235b2ad..3cba670d8 100644 --- a/internal/metamorph/grpc_callbacker.go +++ b/pkg/callbacker/callbacker.go @@ -1,4 +1,4 @@ -package metamorph +package callbacker import ( "context" @@ -13,6 +13,8 @@ import ( "github.com/bitcoin-sv/arc/internal/tracing" ) +var minedDoubleSpendMsg = "previously double spend attempted" + type GrpcCallbacker struct { cc callbacker_api.CallbackerAPIClient l *slog.Logger @@ -33,9 +35,9 @@ func WithTracerCallbacker(attr ...attribute.KeyValue) func(*GrpcCallbacker) { } } -type CallbackerOption func(s *GrpcCallbacker) +type Option func(s *GrpcCallbacker) -func NewGrpcCallbacker(api callbacker_api.CallbackerAPIClient, logger *slog.Logger, opts ...CallbackerOption) GrpcCallbacker { +func NewGrpcCallbacker(api callbacker_api.CallbackerAPIClient, logger *slog.Logger, opts ...Option) GrpcCallbacker { c := GrpcCallbacker{ cc: api, l: logger, diff --git a/pkg/callbacker/callbacker_test.go b/pkg/callbacker/callbacker_test.go new file mode 100644 index 000000000..eaf3f0973 --- /dev/null +++ b/pkg/callbacker/callbacker_test.go @@ -0,0 +1,75 @@ +package callbacker + +import ( + "context" + "log/slog" + "os" + "testing" + + "github.com/bitcoin-sv/arc/internal/callbacker/callbacker_api" + "github.com/bitcoin-sv/arc/internal/metamorph/metamorph_api" + "github.com/bitcoin-sv/arc/internal/metamorph/store" + "github.com/libsv/go-p2p/chaincfg/chainhash" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/emptypb" +) + +func TestSendCallback(t *testing.T) { + tt := []struct { + name string + expectedCalls int + err error + data *store.Data + }{ + { + name: "empty callbacks", + expectedCalls: 0, + data: &store.Data{ + Status: metamorph_api.Status_UNKNOWN, + Hash: &chainhash.Hash{}, + Callbacks: []store.Callback{}, + }, + }, + { + name: "empty url", + expectedCalls: 0, + + data: &store.Data{ + Status: metamorph_api.Status_UNKNOWN, + Hash: &chainhash.Hash{}, + Callbacks: []store.Callback{ + { + CallbackURL: "", + }, + }, + }, + }, + { + name: "expected call", + expectedCalls: 1, + data: &store.Data{ + Status: metamorph_api.Status_UNKNOWN, + Hash: &chainhash.Hash{}, + Callbacks: []store.Callback{ + { + CallbackURL: "http://someurl.comg", + }, + }, + }, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + apiClient := &callbacker_api.CallbackerAPIClientMock{ + SendCallbackFunc: func(_ context.Context, _ *callbacker_api.SendCallbackRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { + return nil, nil + }, + } + grpcCallbacker := NewGrpcCallbacker(apiClient, slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))) + grpcCallbacker.SendCallback(context.Background(), tc.data) + require.Equal(t, tc.expectedCalls, len(apiClient.SendCallbackCalls())) + }) + } +}