diff --git a/internal/contracts/manager.go b/internal/contracts/manager.go index b5a4e51de..85ff63eba 100644 --- a/internal/contracts/manager.go +++ b/internal/contracts/manager.go @@ -19,6 +19,7 @@ package contracts import ( "context" "crypto/sha256" + "database/sql/driver" "encoding/hex" "fmt" "hash" @@ -858,10 +859,17 @@ func (cm *contractManager) AddContractListener(ctx context.Context, listener *co // Namespace + Topic + Location + Signature must be unique listener.Signature = cm.blockchain.GenerateEventSignature(ctx, &listener.Event.FFIEventDefinition) + // Above we only call NormalizeContractLocation if the listener is non-nil, and that means + // for an unset location we will have a nil value. Using an fftypes.JSONAny in a query + // of nil does not yield the right result, so we need to do an explicit nil query. + var locationLookup driver.Value = nil + if !listener.Location.IsNil() { + locationLookup = listener.Location.String() + } fb := database.ContractListenerQueryFactory.NewFilter(ctx) if existing, _, err := cm.database.GetContractListeners(ctx, cm.namespace, fb.And( fb.Eq("topic", listener.Topic), - fb.Eq("location", listener.Location.String()), + fb.Eq("location", locationLookup), fb.Eq("signature", listener.Signature), )); err != nil { return err diff --git a/internal/contracts/manager_test.go b/internal/contracts/manager_test.go index 2663cecdd..304d05933 100644 --- a/internal/contracts/manager_test.go +++ b/internal/contracts/manager_test.go @@ -788,6 +788,46 @@ func TestAddContractListenerInline(t *testing.T) { mdi.AssertExpectations(t) } +func TestAddContractListenerInlineNilLocation(t *testing.T) { + cm := newTestContractManager() + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mdi := cm.database.(*databasemocks.Plugin) + + sub := &core.ContractListenerInput{ + ContractListener: core.ContractListener{ + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "changed", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "integer"}`), + }, + }, + }, + }, + Options: &core.ContractListenerOptions{}, + Topic: "test-topic", + }, + } + + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed") + mdi.On("GetContractListeners", context.Background(), "ns1", mock.Anything).Return(nil, nil, nil) + mbi.On("AddContractListener", context.Background(), mock.MatchedBy(func(cl *core.ContractListener) bool { + // Normalize is not called for this case + return cl.Location == nil + })).Return(nil) + mdi.On("InsertContractListener", context.Background(), &sub.ContractListener).Return(nil) + + result, err := cm.AddContractListener(context.Background(), sub) + assert.NoError(t, err) + assert.NotNil(t, result.ID) + assert.NotNil(t, result.Event) + + mbi.AssertExpectations(t) + mdi.AssertExpectations(t) +} + func TestAddContractListenerNoLocationOK(t *testing.T) { cm := newTestContractManager() mbi := cm.blockchain.(*blockchainmocks.Plugin)