diff --git a/db/migrations/postgres/000117_update_contractlisteners_add_filters_column.down.sql b/db/migrations/postgres/000117_update_contractlisteners_add_filters_column.down.sql new file mode 100644 index 000000000..4bc91b8f8 --- /dev/null +++ b/db/migrations/postgres/000117_update_contractlisteners_add_filters_column.down.sql @@ -0,0 +1,4 @@ +BEGIN; +ALTER TABLE contractlisteners DROP COLUMN filters; +-- no down for the VARCHAR change +COMMIT; \ No newline at end of file diff --git a/db/migrations/postgres/000117_update_contractlisteners_add_filters_column.up.sql b/db/migrations/postgres/000117_update_contractlisteners_add_filters_column.up.sql new file mode 100644 index 000000000..2cfe2ff41 --- /dev/null +++ b/db/migrations/postgres/000117_update_contractlisteners_add_filters_column.up.sql @@ -0,0 +1,5 @@ +BEGIN; +ALTER TABLE contractlisteners ADD COLUMN filters TEXT; +-- changing the length of varchar does not affect the index +ALTER TABLE contractlisteners ALTER COLUMN signature TYPE VARCHAR; +COMMIT; \ No newline at end of file diff --git a/db/migrations/sqlite/000117_update_contractlisteners_add_filters_column.down.sql b/db/migrations/sqlite/000117_update_contractlisteners_add_filters_column.down.sql new file mode 100644 index 000000000..234055a4f --- /dev/null +++ b/db/migrations/sqlite/000117_update_contractlisteners_add_filters_column.down.sql @@ -0,0 +1 @@ +ALTER TABLE contractlisteners DROP COLUMN filters; \ No newline at end of file diff --git a/db/migrations/sqlite/000117_update_contractlisteners_add_filters_column.up.sql b/db/migrations/sqlite/000117_update_contractlisteners_add_filters_column.up.sql new file mode 100644 index 000000000..0e7b992aa --- /dev/null +++ b/db/migrations/sqlite/000117_update_contractlisteners_add_filters_column.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE contractlisteners ADD COLUMN filters TEXT; +-- in SQLITE VARCHAR is equivalent to TEXT so no migration for signature length \ No newline at end of file diff --git a/doc-site/docs/reference/types/contractlistener.md b/doc-site/docs/reference/types/contractlistener.md index 9b73e1233..68c2be28d 100644 --- a/doc-site/docs/reference/types/contractlistener.md +++ b/doc-site/docs/reference/types/contractlistener.md @@ -47,16 +47,17 @@ title: ContractListener | Field Name | Description | Type | |------------|-------------|------| | `id` | The UUID of the smart contract listener | [`UUID`](simpletypes.md#uuid) | -| `interface` | A reference to an existing FFI, containing pre-registered type information for the event | [`FFIReference`](#ffireference) | +| `interface` | Deprecated: Please use 'interface' in the array of 'filters' instead | [`FFIReference`](#ffireference) | | `namespace` | The namespace of the listener, which defines the namespace of all blockchain events detected by this listener | `string` | | `name` | A descriptive name for the listener | `string` | | `backendId` | An ID assigned by the blockchain connector to this listener | `string` | -| `location` | A blockchain specific contract identifier. For example an Ethereum contract address, or a Fabric chaincode name and channel | [`JSONAny`](simpletypes.md#jsonany) | +| `location` | Deprecated: Please use 'location' in the array of 'filters' instead | [`JSONAny`](simpletypes.md#jsonany) | | `created` | The creation time of the listener | [`FFTime`](simpletypes.md#fftime) | -| `event` | The definition of the event, either provided in-line when creating the listener, or extracted from the referenced FFI | [`FFISerializedEvent`](#ffiserializedevent) | -| `signature` | The stringified signature of the event, as computed by the blockchain plugin | `string` | +| `event` | Deprecated: Please use 'event' in the array of 'filters' instead | [`FFISerializedEvent`](#ffiserializedevent) | +| `signature` | A concatenation of all the stringified signature of the event and location, as computed by the blockchain plugin | `string` | | `topic` | A topic to set on the FireFly event that is emitted each time a blockchain event is detected from the blockchain. Setting this topic on a number of listeners allows applications to easily subscribe to all events they need | `string` | | `options` | Options that control how the listener subscribes to events from the underlying blockchain | [`ContractListenerOptions`](#contractlisteneroptions) | +| `filters` | A list of filters for the contract listener. Each filter is made up of an Event and an optional Location. Events matching these filters will always be emitted in the order determined by the blockchain. | [`ListenerFilter[]`](#listenerfilter) | ## FFIReference @@ -92,3 +93,13 @@ title: ContractListener | `firstEvent` | A blockchain specific string, such as a block number, to start listening from. The special strings 'oldest' and 'newest' are supported by all blockchain connectors. Default is 'newest' | `string` | +## ListenerFilter + +| Field Name | Description | Type | +|------------|-------------|------| +| `event` | The definition of the event, either provided in-line when creating the listener, or extracted from the referenced FFI | [`FFISerializedEvent`](#ffiserializedevent) | +| `location` | A blockchain specific contract identifier. For example an Ethereum contract address, or a Fabric chaincode name and channel | [`JSONAny`](simpletypes.md#jsonany) | +| `interface` | A reference to an existing FFI, containing pre-registered type information for the event | [`FFIReference`](#ffireference) | +| `signature` | The stringified signature of the event and location, as computed by the blockchain plugin | `string` | + + diff --git a/doc-site/docs/swagger/swagger.yaml b/doc-site/docs/swagger/swagger.yaml index 45574263b..60a2e7b24 100644 --- a/doc-site/docs/swagger/swagger.yaml +++ b/doc-site/docs/swagger/swagger.yaml @@ -1297,6 +1297,11 @@ paths: name: created schema: type: string + - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' + in: query + name: filters + schema: + type: string - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' in: query name: id @@ -1387,9 +1392,8 @@ paths: format: date-time type: string event: - description: The definition of the event, either provided in-line - when creating the listener, or extracted from the referenced - FFI + description: 'Deprecated: Please use ''event'' in the array + of ''filters'' instead' properties: description: description: A description of the smart contract event @@ -1425,13 +1429,94 @@ paths: type: object type: array type: object + filters: + description: A list of filters for the contract listener. Each + filter is made up of an Event and an optional Location. Events + matching these filters will always be emitted in the order + determined by the blockchain. + items: + description: A list of filters for the contract listener. + Each filter is made up of an Event and an optional Location. + Events matching these filters will always be emitted in + the order determined by the blockchain. + properties: + event: + description: The definition of the event, either provided + in-line when creating the listener, or extracted from + the referenced FFI + properties: + description: + description: A description of the smart contract event + type: string + details: + additionalProperties: + description: Additional blockchain specific fields + about this event from the original smart contract. + Used by the blockchain plugin and for documentation + generation. + description: Additional blockchain specific fields + about this event from the original smart contract. + Used by the blockchain plugin and for documentation + generation. + type: object + name: + description: The name of the event + type: string + params: + description: An array of event parameter/argument + definitions + items: + description: An array of event parameter/argument + definitions + properties: + name: + description: The name of the parameter. Note + that parameters must be ordered correctly + on the FFI, according to the order in the + blockchain smart contract + type: string + schema: + description: FireFly uses an extended subset + of JSON Schema to describe parameters, similar + to OpenAPI/Swagger. Converters are available + for native blockchain interface definitions + / type systems - such as an Ethereum ABI. + See the documentation for more detail + type: object + type: array + type: object + interface: + description: A reference to an existing FFI, containing + pre-registered type information for the event + properties: + id: + description: The UUID of the FireFly interface + format: uuid + type: string + name: + description: The name of the FireFly interface + type: string + version: + description: The version of the FireFly interface + type: string + type: object + location: + description: A blockchain specific contract identifier. + For example an Ethereum contract address, or a Fabric + chaincode name and channel + signature: + description: The stringified signature of the event and + location, as computed by the blockchain plugin + type: string + type: object + type: array id: description: The UUID of the smart contract listener format: uuid type: string interface: - description: A reference to an existing FFI, containing pre-registered - type information for the event + description: 'Deprecated: Please use ''interface'' in the array + of ''filters'' instead' properties: id: description: The UUID of the FireFly interface @@ -1445,9 +1530,8 @@ paths: type: string type: object location: - description: A blockchain specific contract identifier. For - example an Ethereum contract address, or a Fabric chaincode - name and channel + description: 'Deprecated: Please use ''location'' in the array + of ''filters'' instead' name: description: A descriptive name for the listener type: string @@ -1467,8 +1551,8 @@ paths: type: string type: object signature: - description: The stringified signature of the event, as computed - by the blockchain plugin + description: A concatenation of all the stringified signature + of the event and location, as computed by the blockchain plugin type: string topic: description: A topic to set on the FireFly event that is emitted @@ -1513,9 +1597,47 @@ paths: application/json: schema: properties: + event: + description: 'Deprecated: Please use ''event'' in the array of ''filters'' + instead' + properties: + description: + description: A description of the smart contract event + type: string + details: + additionalProperties: + description: Additional blockchain specific fields about this + event from the original smart contract. Used by the blockchain + plugin and for documentation generation. + description: Additional blockchain specific fields about this + event from the original smart contract. Used by the blockchain + plugin and for documentation generation. + type: object + name: + description: The name of the event + type: string + params: + description: An array of event parameter/argument definitions + items: + description: An array of event parameter/argument definitions + properties: + name: + description: The name of the parameter. Note that parameters + must be ordered correctly on the FFI, according to the + order in the blockchain smart contract + type: string + schema: + description: FireFly uses an extended subset of JSON Schema + to describe parameters, similar to OpenAPI/Swagger. + Converters are available for native blockchain interface + definitions / type systems - such as an Ethereum ABI. + See the documentation for more detail + type: object + type: array + type: object location: - description: A blockchain specific contract identifier. For example - an Ethereum contract address, or a Fabric chaincode name and channel + description: 'Deprecated: Please use ''location'' in the array of + ''filters'' instead' name: description: A descriptive name for the listener type: string @@ -1552,9 +1674,8 @@ paths: format: date-time type: string event: - description: The definition of the event, either provided in-line - when creating the listener, or extracted from the referenced - FFI + description: 'Deprecated: Please use ''event'' in the array of + ''filters'' instead' properties: description: description: A description of the smart contract event @@ -1590,13 +1711,92 @@ paths: type: object type: array type: object + filters: + description: A list of filters for the contract listener. Each + filter is made up of an Event and an optional Location. Events + matching these filters will always be emitted in the order determined + by the blockchain. + items: + description: A list of filters for the contract listener. Each + filter is made up of an Event and an optional Location. Events + matching these filters will always be emitted in the order + determined by the blockchain. + properties: + event: + description: The definition of the event, either provided + in-line when creating the listener, or extracted from + the referenced FFI + properties: + description: + description: A description of the smart contract event + type: string + details: + additionalProperties: + description: Additional blockchain specific fields + about this event from the original smart contract. + Used by the blockchain plugin and for documentation + generation. + description: Additional blockchain specific fields about + this event from the original smart contract. Used + by the blockchain plugin and for documentation generation. + type: object + name: + description: The name of the event + type: string + params: + description: An array of event parameter/argument definitions + items: + description: An array of event parameter/argument + definitions + properties: + name: + description: The name of the parameter. Note that + parameters must be ordered correctly on the + FFI, according to the order in the blockchain + smart contract + type: string + schema: + description: FireFly uses an extended subset of + JSON Schema to describe parameters, similar + to OpenAPI/Swagger. Converters are available + for native blockchain interface definitions + / type systems - such as an Ethereum ABI. See + the documentation for more detail + type: object + type: array + type: object + interface: + description: A reference to an existing FFI, containing + pre-registered type information for the event + properties: + id: + description: The UUID of the FireFly interface + format: uuid + type: string + name: + description: The name of the FireFly interface + type: string + version: + description: The version of the FireFly interface + type: string + type: object + location: + description: A blockchain specific contract identifier. + For example an Ethereum contract address, or a Fabric + chaincode name and channel + signature: + description: The stringified signature of the event and + location, as computed by the blockchain plugin + type: string + type: object + type: array id: description: The UUID of the smart contract listener format: uuid type: string interface: - description: A reference to an existing FFI, containing pre-registered - type information for the event + description: 'Deprecated: Please use ''interface'' in the array + of ''filters'' instead' properties: id: description: The UUID of the FireFly interface @@ -1610,9 +1810,8 @@ paths: type: string type: object location: - description: A blockchain specific contract identifier. For example - an Ethereum contract address, or a Fabric chaincode name and - channel + description: 'Deprecated: Please use ''location'' in the array + of ''filters'' instead' name: description: A descriptive name for the listener type: string @@ -1632,8 +1831,8 @@ paths: type: string type: object signature: - description: The stringified signature of the event, as computed - by the blockchain plugin + description: A concatenation of all the stringified signature + of the event and location, as computed by the blockchain plugin type: string topic: description: A topic to set on the FireFly event that is emitted @@ -5540,6 +5739,11 @@ paths: name: created schema: type: string + - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' + in: query + name: filters + schema: + type: string - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' in: query name: id @@ -5630,9 +5834,8 @@ paths: format: date-time type: string event: - description: The definition of the event, either provided in-line - when creating the listener, or extracted from the referenced - FFI + description: 'Deprecated: Please use ''event'' in the array + of ''filters'' instead' properties: description: description: A description of the smart contract event @@ -5668,13 +5871,94 @@ paths: type: object type: array type: object + filters: + description: A list of filters for the contract listener. Each + filter is made up of an Event and an optional Location. Events + matching these filters will always be emitted in the order + determined by the blockchain. + items: + description: A list of filters for the contract listener. + Each filter is made up of an Event and an optional Location. + Events matching these filters will always be emitted in + the order determined by the blockchain. + properties: + event: + description: The definition of the event, either provided + in-line when creating the listener, or extracted from + the referenced FFI + properties: + description: + description: A description of the smart contract event + type: string + details: + additionalProperties: + description: Additional blockchain specific fields + about this event from the original smart contract. + Used by the blockchain plugin and for documentation + generation. + description: Additional blockchain specific fields + about this event from the original smart contract. + Used by the blockchain plugin and for documentation + generation. + type: object + name: + description: The name of the event + type: string + params: + description: An array of event parameter/argument + definitions + items: + description: An array of event parameter/argument + definitions + properties: + name: + description: The name of the parameter. Note + that parameters must be ordered correctly + on the FFI, according to the order in the + blockchain smart contract + type: string + schema: + description: FireFly uses an extended subset + of JSON Schema to describe parameters, similar + to OpenAPI/Swagger. Converters are available + for native blockchain interface definitions + / type systems - such as an Ethereum ABI. + See the documentation for more detail + type: object + type: array + type: object + interface: + description: A reference to an existing FFI, containing + pre-registered type information for the event + properties: + id: + description: The UUID of the FireFly interface + format: uuid + type: string + name: + description: The name of the FireFly interface + type: string + version: + description: The version of the FireFly interface + type: string + type: object + location: + description: A blockchain specific contract identifier. + For example an Ethereum contract address, or a Fabric + chaincode name and channel + signature: + description: The stringified signature of the event and + location, as computed by the blockchain plugin + type: string + type: object + type: array id: description: The UUID of the smart contract listener format: uuid type: string interface: - description: A reference to an existing FFI, containing pre-registered - type information for the event + description: 'Deprecated: Please use ''interface'' in the array + of ''filters'' instead' properties: id: description: The UUID of the FireFly interface @@ -5688,9 +5972,8 @@ paths: type: string type: object location: - description: A blockchain specific contract identifier. For - example an Ethereum contract address, or a Fabric chaincode - name and channel + description: 'Deprecated: Please use ''location'' in the array + of ''filters'' instead' name: description: A descriptive name for the listener type: string @@ -5710,8 +5993,8 @@ paths: type: string type: object signature: - description: The stringified signature of the event, as computed - by the blockchain plugin + description: A concatenation of all the stringified signature + of the event and location, as computed by the blockchain plugin type: string topic: description: A topic to set on the FireFly event that is emitted @@ -5744,8 +6027,8 @@ paths: schema: properties: event: - description: The definition of the event, either provided in-line - when creating the listener, or extracted from the referenced FFI + description: 'Deprecated: Please use ''event'' in the array of ''filters'' + instead' properties: description: description: A description of the smart contract event @@ -5782,13 +6065,90 @@ paths: type: array type: object eventPath: - description: When creating a listener from an existing FFI, this - is the pathname of the event on that FFI to be detected by this - listener + description: 'Deprecated: Please use ''eventPath'' in the array + of ''filters'' instead' type: string + filters: + description: A list of filters for the contract listener. Each filter + is made up of an Event and an optional Location. Events matching + these filters will always be emitted in the order determined by + the blockchain. + items: + description: A list of filters for the contract listener. Each + filter is made up of an Event and an optional Location. Events + matching these filters will always be emitted in the order determined + by the blockchain. + properties: + event: + description: The definition of the event, either provided + in-line when creating the listener, or extracted from the + referenced FFI + properties: + description: + description: A description of the smart contract event + type: string + details: + additionalProperties: + description: Additional blockchain specific fields about + this event from the original smart contract. Used + by the blockchain plugin and for documentation generation. + description: Additional blockchain specific fields about + this event from the original smart contract. Used by + the blockchain plugin and for documentation generation. + type: object + name: + description: The name of the event + type: string + params: + description: An array of event parameter/argument definitions + items: + description: An array of event parameter/argument definitions + properties: + name: + description: The name of the parameter. Note that + parameters must be ordered correctly on the FFI, + according to the order in the blockchain smart + contract + type: string + schema: + description: FireFly uses an extended subset of + JSON Schema to describe parameters, similar to + OpenAPI/Swagger. Converters are available for + native blockchain interface definitions / type + systems - such as an Ethereum ABI. See the documentation + for more detail + type: object + type: array + type: object + eventPath: + description: When creating a listener from an existing FFI, + this is the pathname of the event on that FFI to be detected + by this listener + type: string + interface: + description: A reference to an existing FFI, containing pre-registered + type information for the event + properties: + id: + description: The UUID of the FireFly interface + format: uuid + type: string + name: + description: The name of the FireFly interface + type: string + version: + description: The version of the FireFly interface + type: string + type: object + location: + description: A blockchain specific contract identifier. For + example an Ethereum contract address, or a Fabric chaincode + name and channel + type: object + type: array interface: - description: A reference to an existing FFI, containing pre-registered - type information for the event + description: 'Deprecated: Please use ''interface'' in the array + of ''filters'' instead' properties: id: description: The UUID of the FireFly interface @@ -5802,8 +6162,8 @@ paths: type: string type: object location: - description: A blockchain specific contract identifier. For example - an Ethereum contract address, or a Fabric chaincode name and channel + description: 'Deprecated: Please use ''location'' in the array of + ''filters'' instead' name: description: A descriptive name for the listener type: string @@ -5840,9 +6200,8 @@ paths: format: date-time type: string event: - description: The definition of the event, either provided in-line - when creating the listener, or extracted from the referenced - FFI + description: 'Deprecated: Please use ''event'' in the array of + ''filters'' instead' properties: description: description: A description of the smart contract event @@ -5878,13 +6237,92 @@ paths: type: object type: array type: object + filters: + description: A list of filters for the contract listener. Each + filter is made up of an Event and an optional Location. Events + matching these filters will always be emitted in the order determined + by the blockchain. + items: + description: A list of filters for the contract listener. Each + filter is made up of an Event and an optional Location. Events + matching these filters will always be emitted in the order + determined by the blockchain. + properties: + event: + description: The definition of the event, either provided + in-line when creating the listener, or extracted from + the referenced FFI + properties: + description: + description: A description of the smart contract event + type: string + details: + additionalProperties: + description: Additional blockchain specific fields + about this event from the original smart contract. + Used by the blockchain plugin and for documentation + generation. + description: Additional blockchain specific fields about + this event from the original smart contract. Used + by the blockchain plugin and for documentation generation. + type: object + name: + description: The name of the event + type: string + params: + description: An array of event parameter/argument definitions + items: + description: An array of event parameter/argument + definitions + properties: + name: + description: The name of the parameter. Note that + parameters must be ordered correctly on the + FFI, according to the order in the blockchain + smart contract + type: string + schema: + description: FireFly uses an extended subset of + JSON Schema to describe parameters, similar + to OpenAPI/Swagger. Converters are available + for native blockchain interface definitions + / type systems - such as an Ethereum ABI. See + the documentation for more detail + type: object + type: array + type: object + interface: + description: A reference to an existing FFI, containing + pre-registered type information for the event + properties: + id: + description: The UUID of the FireFly interface + format: uuid + type: string + name: + description: The name of the FireFly interface + type: string + version: + description: The version of the FireFly interface + type: string + type: object + location: + description: A blockchain specific contract identifier. + For example an Ethereum contract address, or a Fabric + chaincode name and channel + signature: + description: The stringified signature of the event and + location, as computed by the blockchain plugin + type: string + type: object + type: array id: description: The UUID of the smart contract listener format: uuid type: string interface: - description: A reference to an existing FFI, containing pre-registered - type information for the event + description: 'Deprecated: Please use ''interface'' in the array + of ''filters'' instead' properties: id: description: The UUID of the FireFly interface @@ -5898,9 +6336,8 @@ paths: type: string type: object location: - description: A blockchain specific contract identifier. For example - an Ethereum contract address, or a Fabric chaincode name and - channel + description: 'Deprecated: Please use ''location'' in the array + of ''filters'' instead' name: description: A descriptive name for the listener type: string @@ -5920,8 +6357,8 @@ paths: type: string type: object signature: - description: The stringified signature of the event, as computed - by the blockchain plugin + description: A concatenation of all the stringified signature + of the event and location, as computed by the blockchain plugin type: string topic: description: A topic to set on the FireFly event that is emitted @@ -6001,9 +6438,8 @@ paths: format: date-time type: string event: - description: The definition of the event, either provided in-line - when creating the listener, or extracted from the referenced - FFI + description: 'Deprecated: Please use ''event'' in the array of + ''filters'' instead' properties: description: description: A description of the smart contract event @@ -6039,13 +6475,92 @@ paths: type: object type: array type: object + filters: + description: A list of filters for the contract listener. Each + filter is made up of an Event and an optional Location. Events + matching these filters will always be emitted in the order determined + by the blockchain. + items: + description: A list of filters for the contract listener. Each + filter is made up of an Event and an optional Location. Events + matching these filters will always be emitted in the order + determined by the blockchain. + properties: + event: + description: The definition of the event, either provided + in-line when creating the listener, or extracted from + the referenced FFI + properties: + description: + description: A description of the smart contract event + type: string + details: + additionalProperties: + description: Additional blockchain specific fields + about this event from the original smart contract. + Used by the blockchain plugin and for documentation + generation. + description: Additional blockchain specific fields about + this event from the original smart contract. Used + by the blockchain plugin and for documentation generation. + type: object + name: + description: The name of the event + type: string + params: + description: An array of event parameter/argument definitions + items: + description: An array of event parameter/argument + definitions + properties: + name: + description: The name of the parameter. Note that + parameters must be ordered correctly on the + FFI, according to the order in the blockchain + smart contract + type: string + schema: + description: FireFly uses an extended subset of + JSON Schema to describe parameters, similar + to OpenAPI/Swagger. Converters are available + for native blockchain interface definitions + / type systems - such as an Ethereum ABI. See + the documentation for more detail + type: object + type: array + type: object + interface: + description: A reference to an existing FFI, containing + pre-registered type information for the event + properties: + id: + description: The UUID of the FireFly interface + format: uuid + type: string + name: + description: The name of the FireFly interface + type: string + version: + description: The version of the FireFly interface + type: string + type: object + location: + description: A blockchain specific contract identifier. + For example an Ethereum contract address, or a Fabric + chaincode name and channel + signature: + description: The stringified signature of the event and + location, as computed by the blockchain plugin + type: string + type: object + type: array id: description: The UUID of the smart contract listener format: uuid type: string interface: - description: A reference to an existing FFI, containing pre-registered - type information for the event + description: 'Deprecated: Please use ''interface'' in the array + of ''filters'' instead' properties: id: description: The UUID of the FireFly interface @@ -6059,9 +6574,8 @@ paths: type: string type: object location: - description: A blockchain specific contract identifier. For example - an Ethereum contract address, or a Fabric chaincode name and - channel + description: 'Deprecated: Please use ''location'' in the array + of ''filters'' instead' name: description: A descriptive name for the listener type: string @@ -6081,8 +6595,8 @@ paths: type: string type: object signature: - description: The stringified signature of the event, as computed - by the blockchain plugin + description: A concatenation of all the stringified signature + of the event and location, as computed by the blockchain plugin type: string topic: description: A topic to set on the FireFly event that is emitted @@ -6096,10 +6610,10 @@ paths: description: "" tags: - Default Namespace - /contracts/query: + /contracts/listeners/signature: post: - description: Queries a method on a smart contract. Performs a read-only query. - operationId: postContractQuery + description: Calculates the hash of a blockchain listener filters and events + operationId: postContractListenerSignature parameters: - description: Server-side request timeout (milliseconds, or set a custom suffix like 10s) @@ -6113,12 +6627,204 @@ paths: application/json: schema: properties: - errors: - description: An in-line FFI errors definition for the method to - invoke. Alternative to specifying FFI - items: - description: An in-line FFI errors definition for the method to - invoke. Alternative to specifying FFI + event: + description: 'Deprecated: Please use ''event'' in the array of ''filters'' + instead' + properties: + description: + description: A description of the smart contract event + type: string + details: + additionalProperties: + description: Additional blockchain specific fields about this + event from the original smart contract. Used by the blockchain + plugin and for documentation generation. + description: Additional blockchain specific fields about this + event from the original smart contract. Used by the blockchain + plugin and for documentation generation. + type: object + name: + description: The name of the event + type: string + params: + description: An array of event parameter/argument definitions + items: + description: An array of event parameter/argument definitions + properties: + name: + description: The name of the parameter. Note that parameters + must be ordered correctly on the FFI, according to the + order in the blockchain smart contract + type: string + schema: + description: FireFly uses an extended subset of JSON Schema + to describe parameters, similar to OpenAPI/Swagger. + Converters are available for native blockchain interface + definitions / type systems - such as an Ethereum ABI. + See the documentation for more detail + type: object + type: array + type: object + eventPath: + description: 'Deprecated: Please use ''eventPath'' in the array + of ''filters'' instead' + type: string + filters: + description: A list of filters for the contract listener. Each filter + is made up of an Event and an optional Location. Events matching + these filters will always be emitted in the order determined by + the blockchain. + items: + description: A list of filters for the contract listener. Each + filter is made up of an Event and an optional Location. Events + matching these filters will always be emitted in the order determined + by the blockchain. + properties: + event: + description: The definition of the event, either provided + in-line when creating the listener, or extracted from the + referenced FFI + properties: + description: + description: A description of the smart contract event + type: string + details: + additionalProperties: + description: Additional blockchain specific fields about + this event from the original smart contract. Used + by the blockchain plugin and for documentation generation. + description: Additional blockchain specific fields about + this event from the original smart contract. Used by + the blockchain plugin and for documentation generation. + type: object + name: + description: The name of the event + type: string + params: + description: An array of event parameter/argument definitions + items: + description: An array of event parameter/argument definitions + properties: + name: + description: The name of the parameter. Note that + parameters must be ordered correctly on the FFI, + according to the order in the blockchain smart + contract + type: string + schema: + description: FireFly uses an extended subset of + JSON Schema to describe parameters, similar to + OpenAPI/Swagger. Converters are available for + native blockchain interface definitions / type + systems - such as an Ethereum ABI. See the documentation + for more detail + type: object + type: array + type: object + eventPath: + description: When creating a listener from an existing FFI, + this is the pathname of the event on that FFI to be detected + by this listener + type: string + interface: + description: A reference to an existing FFI, containing pre-registered + type information for the event + properties: + id: + description: The UUID of the FireFly interface + format: uuid + type: string + name: + description: The name of the FireFly interface + type: string + version: + description: The version of the FireFly interface + type: string + type: object + location: + description: A blockchain specific contract identifier. For + example an Ethereum contract address, or a Fabric chaincode + name and channel + type: object + type: array + interface: + description: 'Deprecated: Please use ''interface'' in the array + of ''filters'' instead' + properties: + id: + description: The UUID of the FireFly interface + format: uuid + type: string + name: + description: The name of the FireFly interface + type: string + version: + description: The version of the FireFly interface + type: string + type: object + location: + description: 'Deprecated: Please use ''location'' in the array of + ''filters'' instead' + name: + description: A descriptive name for the listener + type: string + options: + description: Options that control how the listener subscribes to + events from the underlying blockchain + properties: + firstEvent: + description: A blockchain specific string, such as a block number, + to start listening from. The special strings 'oldest' and + 'newest' are supported by all blockchain connectors. Default + is 'newest' + type: string + type: object + topic: + description: A topic to set on the FireFly event that is emitted + each time a blockchain event is detected from the blockchain. + Setting this topic on a number of listeners allows applications + to easily subscribe to all events they need + type: string + type: object + responses: + "200": + content: + application/json: + schema: + properties: + signature: + description: A concatenation of all the stringified signature + of the event and location, as computed by the blockchain plugin + type: string + type: object + description: Success + default: + description: "" + tags: + - Default Namespace + /contracts/query: + post: + description: Queries a method on a smart contract. Performs a read-only query. + operationId: postContractQuery + parameters: + - description: Server-side request timeout (milliseconds, or set a custom suffix + like 10s) + in: header + name: Request-Timeout + schema: + default: 2m0s + type: string + requestBody: + content: + application/json: + schema: + properties: + errors: + description: An in-line FFI errors definition for the method to + invoke. Alternative to specifying FFI + items: + description: An in-line FFI errors definition for the method to + invoke. Alternative to specifying FFI properties: description: description: A description of the smart contract error @@ -13280,6 +13986,11 @@ paths: name: created schema: type: string + - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' + in: query + name: filters + schema: + type: string - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' in: query name: id @@ -13370,9 +14081,8 @@ paths: format: date-time type: string event: - description: The definition of the event, either provided in-line - when creating the listener, or extracted from the referenced - FFI + description: 'Deprecated: Please use ''event'' in the array + of ''filters'' instead' properties: description: description: A description of the smart contract event @@ -13408,13 +14118,94 @@ paths: type: object type: array type: object + filters: + description: A list of filters for the contract listener. Each + filter is made up of an Event and an optional Location. Events + matching these filters will always be emitted in the order + determined by the blockchain. + items: + description: A list of filters for the contract listener. + Each filter is made up of an Event and an optional Location. + Events matching these filters will always be emitted in + the order determined by the blockchain. + properties: + event: + description: The definition of the event, either provided + in-line when creating the listener, or extracted from + the referenced FFI + properties: + description: + description: A description of the smart contract event + type: string + details: + additionalProperties: + description: Additional blockchain specific fields + about this event from the original smart contract. + Used by the blockchain plugin and for documentation + generation. + description: Additional blockchain specific fields + about this event from the original smart contract. + Used by the blockchain plugin and for documentation + generation. + type: object + name: + description: The name of the event + type: string + params: + description: An array of event parameter/argument + definitions + items: + description: An array of event parameter/argument + definitions + properties: + name: + description: The name of the parameter. Note + that parameters must be ordered correctly + on the FFI, according to the order in the + blockchain smart contract + type: string + schema: + description: FireFly uses an extended subset + of JSON Schema to describe parameters, similar + to OpenAPI/Swagger. Converters are available + for native blockchain interface definitions + / type systems - such as an Ethereum ABI. + See the documentation for more detail + type: object + type: array + type: object + interface: + description: A reference to an existing FFI, containing + pre-registered type information for the event + properties: + id: + description: The UUID of the FireFly interface + format: uuid + type: string + name: + description: The name of the FireFly interface + type: string + version: + description: The version of the FireFly interface + type: string + type: object + location: + description: A blockchain specific contract identifier. + For example an Ethereum contract address, or a Fabric + chaincode name and channel + signature: + description: The stringified signature of the event and + location, as computed by the blockchain plugin + type: string + type: object + type: array id: description: The UUID of the smart contract listener format: uuid type: string interface: - description: A reference to an existing FFI, containing pre-registered - type information for the event + description: 'Deprecated: Please use ''interface'' in the array + of ''filters'' instead' properties: id: description: The UUID of the FireFly interface @@ -13428,9 +14219,8 @@ paths: type: string type: object location: - description: A blockchain specific contract identifier. For - example an Ethereum contract address, or a Fabric chaincode - name and channel + description: 'Deprecated: Please use ''location'' in the array + of ''filters'' instead' name: description: A descriptive name for the listener type: string @@ -13450,8 +14240,8 @@ paths: type: string type: object signature: - description: The stringified signature of the event, as computed - by the blockchain plugin + description: A concatenation of all the stringified signature + of the event and location, as computed by the blockchain plugin type: string topic: description: A topic to set on the FireFly event that is emitted @@ -13504,8 +14294,8 @@ paths: schema: properties: event: - description: The definition of the event, either provided in-line - when creating the listener, or extracted from the referenced FFI + description: 'Deprecated: Please use ''event'' in the array of ''filters'' + instead' properties: description: description: A description of the smart contract event @@ -13541,9 +14331,82 @@ paths: type: object type: array type: object + filters: + description: A list of filters for the contract listener. Each filter + is made up of an Event and an optional Location. Events matching + these filters will always be emitted in the order determined by + the blockchain. + items: + description: A list of filters for the contract listener. Each + filter is made up of an Event and an optional Location. Events + matching these filters will always be emitted in the order determined + by the blockchain. + properties: + event: + description: The definition of the event, either provided + in-line when creating the listener, or extracted from the + referenced FFI + properties: + description: + description: A description of the smart contract event + type: string + details: + additionalProperties: + description: Additional blockchain specific fields about + this event from the original smart contract. Used + by the blockchain plugin and for documentation generation. + description: Additional blockchain specific fields about + this event from the original smart contract. Used by + the blockchain plugin and for documentation generation. + type: object + name: + description: The name of the event + type: string + params: + description: An array of event parameter/argument definitions + items: + description: An array of event parameter/argument definitions + properties: + name: + description: The name of the parameter. Note that + parameters must be ordered correctly on the FFI, + according to the order in the blockchain smart + contract + type: string + schema: + description: FireFly uses an extended subset of + JSON Schema to describe parameters, similar to + OpenAPI/Swagger. Converters are available for + native blockchain interface definitions / type + systems - such as an Ethereum ABI. See the documentation + for more detail + type: object + type: array + type: object + interface: + description: A reference to an existing FFI, containing pre-registered + type information for the event + properties: + id: + description: The UUID of the FireFly interface + format: uuid + type: string + name: + description: The name of the FireFly interface + type: string + version: + description: The version of the FireFly interface + type: string + type: object + location: + description: A blockchain specific contract identifier. For + example an Ethereum contract address, or a Fabric chaincode + name and channel + type: object + type: array interface: - description: A reference to an existing FFI, containing pre-registered - type information for the event + description: 'Deprecated: Please use ''interface'' in the array + of ''filters'' instead' properties: id: description: The UUID of the FireFly interface @@ -13557,8 +14420,8 @@ paths: type: string type: object location: - description: A blockchain specific contract identifier. For example - an Ethereum contract address, or a Fabric chaincode name and channel + description: 'Deprecated: Please use ''location'' in the array of + ''filters'' instead' name: description: A descriptive name for the listener type: string @@ -13595,9 +14458,8 @@ paths: format: date-time type: string event: - description: The definition of the event, either provided in-line - when creating the listener, or extracted from the referenced - FFI + description: 'Deprecated: Please use ''event'' in the array of + ''filters'' instead' properties: description: description: A description of the smart contract event @@ -13633,17 +14495,96 @@ paths: type: object type: array type: object - id: - description: The UUID of the smart contract listener - format: uuid - type: string - interface: - description: A reference to an existing FFI, containing pre-registered - type information for the event - properties: - id: - description: The UUID of the FireFly interface - format: uuid + filters: + description: A list of filters for the contract listener. Each + filter is made up of an Event and an optional Location. Events + matching these filters will always be emitted in the order determined + by the blockchain. + items: + description: A list of filters for the contract listener. Each + filter is made up of an Event and an optional Location. Events + matching these filters will always be emitted in the order + determined by the blockchain. + properties: + event: + description: The definition of the event, either provided + in-line when creating the listener, or extracted from + the referenced FFI + properties: + description: + description: A description of the smart contract event + type: string + details: + additionalProperties: + description: Additional blockchain specific fields + about this event from the original smart contract. + Used by the blockchain plugin and for documentation + generation. + description: Additional blockchain specific fields about + this event from the original smart contract. Used + by the blockchain plugin and for documentation generation. + type: object + name: + description: The name of the event + type: string + params: + description: An array of event parameter/argument definitions + items: + description: An array of event parameter/argument + definitions + properties: + name: + description: The name of the parameter. Note that + parameters must be ordered correctly on the + FFI, according to the order in the blockchain + smart contract + type: string + schema: + description: FireFly uses an extended subset of + JSON Schema to describe parameters, similar + to OpenAPI/Swagger. Converters are available + for native blockchain interface definitions + / type systems - such as an Ethereum ABI. See + the documentation for more detail + type: object + type: array + type: object + interface: + description: A reference to an existing FFI, containing + pre-registered type information for the event + properties: + id: + description: The UUID of the FireFly interface + format: uuid + type: string + name: + description: The name of the FireFly interface + type: string + version: + description: The version of the FireFly interface + type: string + type: object + location: + description: A blockchain specific contract identifier. + For example an Ethereum contract address, or a Fabric + chaincode name and channel + signature: + description: The stringified signature of the event and + location, as computed by the blockchain plugin + type: string + type: object + type: array + id: + description: The UUID of the smart contract listener + format: uuid + type: string + interface: + description: 'Deprecated: Please use ''interface'' in the array + of ''filters'' instead' + properties: + id: + description: The UUID of the FireFly interface + format: uuid type: string name: description: The name of the FireFly interface @@ -13653,9 +14594,8 @@ paths: type: string type: object location: - description: A blockchain specific contract identifier. For example - an Ethereum contract address, or a Fabric chaincode name and - channel + description: 'Deprecated: Please use ''location'' in the array + of ''filters'' instead' name: description: A descriptive name for the listener type: string @@ -13675,8 +14615,8 @@ paths: type: string type: object signature: - description: The stringified signature of the event, as computed - by the blockchain plugin + description: A concatenation of all the stringified signature + of the event and location, as computed by the blockchain plugin type: string topic: description: A topic to set on the FireFly event that is emitted @@ -17955,6 +18895,11 @@ paths: name: created schema: type: string + - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' + in: query + name: filters + schema: + type: string - description: 'Data filter field. Prefixes supported: > >= < <= @ ^ ! !@ !^' in: query name: id @@ -18045,9 +18990,8 @@ paths: format: date-time type: string event: - description: The definition of the event, either provided in-line - when creating the listener, or extracted from the referenced - FFI + description: 'Deprecated: Please use ''event'' in the array + of ''filters'' instead' properties: description: description: A description of the smart contract event @@ -18083,13 +19027,94 @@ paths: type: object type: array type: object + filters: + description: A list of filters for the contract listener. Each + filter is made up of an Event and an optional Location. Events + matching these filters will always be emitted in the order + determined by the blockchain. + items: + description: A list of filters for the contract listener. + Each filter is made up of an Event and an optional Location. + Events matching these filters will always be emitted in + the order determined by the blockchain. + properties: + event: + description: The definition of the event, either provided + in-line when creating the listener, or extracted from + the referenced FFI + properties: + description: + description: A description of the smart contract event + type: string + details: + additionalProperties: + description: Additional blockchain specific fields + about this event from the original smart contract. + Used by the blockchain plugin and for documentation + generation. + description: Additional blockchain specific fields + about this event from the original smart contract. + Used by the blockchain plugin and for documentation + generation. + type: object + name: + description: The name of the event + type: string + params: + description: An array of event parameter/argument + definitions + items: + description: An array of event parameter/argument + definitions + properties: + name: + description: The name of the parameter. Note + that parameters must be ordered correctly + on the FFI, according to the order in the + blockchain smart contract + type: string + schema: + description: FireFly uses an extended subset + of JSON Schema to describe parameters, similar + to OpenAPI/Swagger. Converters are available + for native blockchain interface definitions + / type systems - such as an Ethereum ABI. + See the documentation for more detail + type: object + type: array + type: object + interface: + description: A reference to an existing FFI, containing + pre-registered type information for the event + properties: + id: + description: The UUID of the FireFly interface + format: uuid + type: string + name: + description: The name of the FireFly interface + type: string + version: + description: The version of the FireFly interface + type: string + type: object + location: + description: A blockchain specific contract identifier. + For example an Ethereum contract address, or a Fabric + chaincode name and channel + signature: + description: The stringified signature of the event and + location, as computed by the blockchain plugin + type: string + type: object + type: array id: description: The UUID of the smart contract listener format: uuid type: string interface: - description: A reference to an existing FFI, containing pre-registered - type information for the event + description: 'Deprecated: Please use ''interface'' in the array + of ''filters'' instead' properties: id: description: The UUID of the FireFly interface @@ -18103,9 +19128,8 @@ paths: type: string type: object location: - description: A blockchain specific contract identifier. For - example an Ethereum contract address, or a Fabric chaincode - name and channel + description: 'Deprecated: Please use ''location'' in the array + of ''filters'' instead' name: description: A descriptive name for the listener type: string @@ -18125,8 +19149,8 @@ paths: type: string type: object signature: - description: The stringified signature of the event, as computed - by the blockchain plugin + description: A concatenation of all the stringified signature + of the event and location, as computed by the blockchain plugin type: string topic: description: A topic to set on the FireFly event that is emitted @@ -18166,8 +19190,8 @@ paths: schema: properties: event: - description: The definition of the event, either provided in-line - when creating the listener, or extracted from the referenced FFI + description: 'Deprecated: Please use ''event'' in the array of ''filters'' + instead' properties: description: description: A description of the smart contract event @@ -18204,13 +19228,90 @@ paths: type: array type: object eventPath: - description: When creating a listener from an existing FFI, this - is the pathname of the event on that FFI to be detected by this - listener + description: 'Deprecated: Please use ''eventPath'' in the array + of ''filters'' instead' type: string + filters: + description: A list of filters for the contract listener. Each filter + is made up of an Event and an optional Location. Events matching + these filters will always be emitted in the order determined by + the blockchain. + items: + description: A list of filters for the contract listener. Each + filter is made up of an Event and an optional Location. Events + matching these filters will always be emitted in the order determined + by the blockchain. + properties: + event: + description: The definition of the event, either provided + in-line when creating the listener, or extracted from the + referenced FFI + properties: + description: + description: A description of the smart contract event + type: string + details: + additionalProperties: + description: Additional blockchain specific fields about + this event from the original smart contract. Used + by the blockchain plugin and for documentation generation. + description: Additional blockchain specific fields about + this event from the original smart contract. Used by + the blockchain plugin and for documentation generation. + type: object + name: + description: The name of the event + type: string + params: + description: An array of event parameter/argument definitions + items: + description: An array of event parameter/argument definitions + properties: + name: + description: The name of the parameter. Note that + parameters must be ordered correctly on the FFI, + according to the order in the blockchain smart + contract + type: string + schema: + description: FireFly uses an extended subset of + JSON Schema to describe parameters, similar to + OpenAPI/Swagger. Converters are available for + native blockchain interface definitions / type + systems - such as an Ethereum ABI. See the documentation + for more detail + type: object + type: array + type: object + eventPath: + description: When creating a listener from an existing FFI, + this is the pathname of the event on that FFI to be detected + by this listener + type: string + interface: + description: A reference to an existing FFI, containing pre-registered + type information for the event + properties: + id: + description: The UUID of the FireFly interface + format: uuid + type: string + name: + description: The name of the FireFly interface + type: string + version: + description: The version of the FireFly interface + type: string + type: object + location: + description: A blockchain specific contract identifier. For + example an Ethereum contract address, or a Fabric chaincode + name and channel + type: object + type: array interface: - description: A reference to an existing FFI, containing pre-registered - type information for the event + description: 'Deprecated: Please use ''interface'' in the array + of ''filters'' instead' properties: id: description: The UUID of the FireFly interface @@ -18224,8 +19325,8 @@ paths: type: string type: object location: - description: A blockchain specific contract identifier. For example - an Ethereum contract address, or a Fabric chaincode name and channel + description: 'Deprecated: Please use ''location'' in the array of + ''filters'' instead' name: description: A descriptive name for the listener type: string @@ -18262,9 +19363,8 @@ paths: format: date-time type: string event: - description: The definition of the event, either provided in-line - when creating the listener, or extracted from the referenced - FFI + description: 'Deprecated: Please use ''event'' in the array of + ''filters'' instead' properties: description: description: A description of the smart contract event @@ -18300,13 +19400,92 @@ paths: type: object type: array type: object + filters: + description: A list of filters for the contract listener. Each + filter is made up of an Event and an optional Location. Events + matching these filters will always be emitted in the order determined + by the blockchain. + items: + description: A list of filters for the contract listener. Each + filter is made up of an Event and an optional Location. Events + matching these filters will always be emitted in the order + determined by the blockchain. + properties: + event: + description: The definition of the event, either provided + in-line when creating the listener, or extracted from + the referenced FFI + properties: + description: + description: A description of the smart contract event + type: string + details: + additionalProperties: + description: Additional blockchain specific fields + about this event from the original smart contract. + Used by the blockchain plugin and for documentation + generation. + description: Additional blockchain specific fields about + this event from the original smart contract. Used + by the blockchain plugin and for documentation generation. + type: object + name: + description: The name of the event + type: string + params: + description: An array of event parameter/argument definitions + items: + description: An array of event parameter/argument + definitions + properties: + name: + description: The name of the parameter. Note that + parameters must be ordered correctly on the + FFI, according to the order in the blockchain + smart contract + type: string + schema: + description: FireFly uses an extended subset of + JSON Schema to describe parameters, similar + to OpenAPI/Swagger. Converters are available + for native blockchain interface definitions + / type systems - such as an Ethereum ABI. See + the documentation for more detail + type: object + type: array + type: object + interface: + description: A reference to an existing FFI, containing + pre-registered type information for the event + properties: + id: + description: The UUID of the FireFly interface + format: uuid + type: string + name: + description: The name of the FireFly interface + type: string + version: + description: The version of the FireFly interface + type: string + type: object + location: + description: A blockchain specific contract identifier. + For example an Ethereum contract address, or a Fabric + chaincode name and channel + signature: + description: The stringified signature of the event and + location, as computed by the blockchain plugin + type: string + type: object + type: array id: description: The UUID of the smart contract listener format: uuid type: string interface: - description: A reference to an existing FFI, containing pre-registered - type information for the event + description: 'Deprecated: Please use ''interface'' in the array + of ''filters'' instead' properties: id: description: The UUID of the FireFly interface @@ -18320,9 +19499,8 @@ paths: type: string type: object location: - description: A blockchain specific contract identifier. For example - an Ethereum contract address, or a Fabric chaincode name and - channel + description: 'Deprecated: Please use ''location'' in the array + of ''filters'' instead' name: description: A descriptive name for the listener type: string @@ -18342,8 +19520,8 @@ paths: type: string type: object signature: - description: The stringified signature of the event, as computed - by the blockchain plugin + description: A concatenation of all the stringified signature + of the event and location, as computed by the blockchain plugin type: string topic: description: A topic to set on the FireFly event that is emitted @@ -18437,9 +19615,8 @@ paths: format: date-time type: string event: - description: The definition of the event, either provided in-line - when creating the listener, or extracted from the referenced - FFI + description: 'Deprecated: Please use ''event'' in the array of + ''filters'' instead' properties: description: description: A description of the smart contract event @@ -18475,13 +19652,92 @@ paths: type: object type: array type: object + filters: + description: A list of filters for the contract listener. Each + filter is made up of an Event and an optional Location. Events + matching these filters will always be emitted in the order determined + by the blockchain. + items: + description: A list of filters for the contract listener. Each + filter is made up of an Event and an optional Location. Events + matching these filters will always be emitted in the order + determined by the blockchain. + properties: + event: + description: The definition of the event, either provided + in-line when creating the listener, or extracted from + the referenced FFI + properties: + description: + description: A description of the smart contract event + type: string + details: + additionalProperties: + description: Additional blockchain specific fields + about this event from the original smart contract. + Used by the blockchain plugin and for documentation + generation. + description: Additional blockchain specific fields about + this event from the original smart contract. Used + by the blockchain plugin and for documentation generation. + type: object + name: + description: The name of the event + type: string + params: + description: An array of event parameter/argument definitions + items: + description: An array of event parameter/argument + definitions + properties: + name: + description: The name of the parameter. Note that + parameters must be ordered correctly on the + FFI, according to the order in the blockchain + smart contract + type: string + schema: + description: FireFly uses an extended subset of + JSON Schema to describe parameters, similar + to OpenAPI/Swagger. Converters are available + for native blockchain interface definitions + / type systems - such as an Ethereum ABI. See + the documentation for more detail + type: object + type: array + type: object + interface: + description: A reference to an existing FFI, containing + pre-registered type information for the event + properties: + id: + description: The UUID of the FireFly interface + format: uuid + type: string + name: + description: The name of the FireFly interface + type: string + version: + description: The version of the FireFly interface + type: string + type: object + location: + description: A blockchain specific contract identifier. + For example an Ethereum contract address, or a Fabric + chaincode name and channel + signature: + description: The stringified signature of the event and + location, as computed by the blockchain plugin + type: string + type: object + type: array id: description: The UUID of the smart contract listener format: uuid type: string interface: - description: A reference to an existing FFI, containing pre-registered - type information for the event + description: 'Deprecated: Please use ''interface'' in the array + of ''filters'' instead' properties: id: description: The UUID of the FireFly interface @@ -18495,9 +19751,8 @@ paths: type: string type: object location: - description: A blockchain specific contract identifier. For example - an Ethereum contract address, or a Fabric chaincode name and - channel + description: 'Deprecated: Please use ''location'' in the array + of ''filters'' instead' name: description: A descriptive name for the listener type: string @@ -18517,8 +19772,8 @@ paths: type: string type: object signature: - description: The stringified signature of the event, as computed - by the blockchain plugin + description: A concatenation of all the stringified signature + of the event and location, as computed by the blockchain plugin type: string topic: description: A topic to set on the FireFly event that is emitted @@ -18532,6 +19787,205 @@ paths: description: "" tags: - Non-Default Namespace + /namespaces/{ns}/contracts/listeners/signature: + post: + description: Calculates the hash of a blockchain listener filters and events + operationId: postContractListenerSignatureNamespace + parameters: + - description: The namespace which scopes this request + in: path + name: ns + required: true + schema: + example: default + type: string + - description: Server-side request timeout (milliseconds, or set a custom suffix + like 10s) + in: header + name: Request-Timeout + schema: + default: 2m0s + type: string + requestBody: + content: + application/json: + schema: + properties: + event: + description: 'Deprecated: Please use ''event'' in the array of ''filters'' + instead' + properties: + description: + description: A description of the smart contract event + type: string + details: + additionalProperties: + description: Additional blockchain specific fields about this + event from the original smart contract. Used by the blockchain + plugin and for documentation generation. + description: Additional blockchain specific fields about this + event from the original smart contract. Used by the blockchain + plugin and for documentation generation. + type: object + name: + description: The name of the event + type: string + params: + description: An array of event parameter/argument definitions + items: + description: An array of event parameter/argument definitions + properties: + name: + description: The name of the parameter. Note that parameters + must be ordered correctly on the FFI, according to the + order in the blockchain smart contract + type: string + schema: + description: FireFly uses an extended subset of JSON Schema + to describe parameters, similar to OpenAPI/Swagger. + Converters are available for native blockchain interface + definitions / type systems - such as an Ethereum ABI. + See the documentation for more detail + type: object + type: array + type: object + eventPath: + description: 'Deprecated: Please use ''eventPath'' in the array + of ''filters'' instead' + type: string + filters: + description: A list of filters for the contract listener. Each filter + is made up of an Event and an optional Location. Events matching + these filters will always be emitted in the order determined by + the blockchain. + items: + description: A list of filters for the contract listener. Each + filter is made up of an Event and an optional Location. Events + matching these filters will always be emitted in the order determined + by the blockchain. + properties: + event: + description: The definition of the event, either provided + in-line when creating the listener, or extracted from the + referenced FFI + properties: + description: + description: A description of the smart contract event + type: string + details: + additionalProperties: + description: Additional blockchain specific fields about + this event from the original smart contract. Used + by the blockchain plugin and for documentation generation. + description: Additional blockchain specific fields about + this event from the original smart contract. Used by + the blockchain plugin and for documentation generation. + type: object + name: + description: The name of the event + type: string + params: + description: An array of event parameter/argument definitions + items: + description: An array of event parameter/argument definitions + properties: + name: + description: The name of the parameter. Note that + parameters must be ordered correctly on the FFI, + according to the order in the blockchain smart + contract + type: string + schema: + description: FireFly uses an extended subset of + JSON Schema to describe parameters, similar to + OpenAPI/Swagger. Converters are available for + native blockchain interface definitions / type + systems - such as an Ethereum ABI. See the documentation + for more detail + type: object + type: array + type: object + eventPath: + description: When creating a listener from an existing FFI, + this is the pathname of the event on that FFI to be detected + by this listener + type: string + interface: + description: A reference to an existing FFI, containing pre-registered + type information for the event + properties: + id: + description: The UUID of the FireFly interface + format: uuid + type: string + name: + description: The name of the FireFly interface + type: string + version: + description: The version of the FireFly interface + type: string + type: object + location: + description: A blockchain specific contract identifier. For + example an Ethereum contract address, or a Fabric chaincode + name and channel + type: object + type: array + interface: + description: 'Deprecated: Please use ''interface'' in the array + of ''filters'' instead' + properties: + id: + description: The UUID of the FireFly interface + format: uuid + type: string + name: + description: The name of the FireFly interface + type: string + version: + description: The version of the FireFly interface + type: string + type: object + location: + description: 'Deprecated: Please use ''location'' in the array of + ''filters'' instead' + name: + description: A descriptive name for the listener + type: string + options: + description: Options that control how the listener subscribes to + events from the underlying blockchain + properties: + firstEvent: + description: A blockchain specific string, such as a block number, + to start listening from. The special strings 'oldest' and + 'newest' are supported by all blockchain connectors. Default + is 'newest' + type: string + type: object + topic: + description: A topic to set on the FireFly event that is emitted + each time a blockchain event is detected from the blockchain. + Setting this topic on a number of listeners allows applications + to easily subscribe to all events they need + type: string + type: object + responses: + "200": + content: + application/json: + schema: + properties: + signature: + description: A concatenation of all the stringified signature + of the event and location, as computed by the blockchain plugin + type: string + type: object + description: Success + default: + description: "" + tags: + - Non-Default Namespace /namespaces/{ns}/contracts/query: post: description: Queries a method on a smart contract. Performs a read-only query. diff --git a/internal/apiserver/route_post_contract_listeners_hash.go b/internal/apiserver/route_post_contract_listeners_hash.go new file mode 100644 index 000000000..fdc6b5fb3 --- /dev/null +++ b/internal/apiserver/route_post_contract_listeners_hash.go @@ -0,0 +1,53 @@ +// Copyright © 2024 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apiserver + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly/internal/coremsgs" + "github.com/hyperledger/firefly/internal/orchestrator" + "github.com/hyperledger/firefly/pkg/core" +) + +/* +* + + This API provides the ability to retrieve the signature for the filters of a contract listener + +* +*/ +var postContractListenerSignature = &ffapi.Route{ + Name: "postContractListenerSignature", + Path: "contracts/listeners/signature", + Method: http.MethodPost, + PathParams: nil, + QueryParams: nil, + Description: coremsgs.APIEndpointsPostContractListenerHash, + JSONInputValue: func() interface{} { return &core.ContractListenerInput{} }, + JSONOutputValue: func() interface{} { return &core.ContractListenerSignatureOutput{} }, + JSONOutputCodes: []int{http.StatusOK}, + Extensions: &coreExtensions{ + EnabledIf: func(or orchestrator.Orchestrator) bool { + return or.Contracts() != nil + }, + CoreJSONHandler: func(r *ffapi.APIRequest, cr *coreRequest) (output interface{}, err error) { + return cr.or.Contracts().ConstructContractListenerSignature(cr.ctx, r.Input.(*core.ContractListenerInput)) + }, + }, +} diff --git a/internal/apiserver/route_post_contract_listeners_hash_test.go b/internal/apiserver/route_post_contract_listeners_hash_test.go new file mode 100644 index 000000000..f95cff961 --- /dev/null +++ b/internal/apiserver/route_post_contract_listeners_hash_test.go @@ -0,0 +1,48 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apiserver + +import ( + "bytes" + "encoding/json" + "net/http/httptest" + "testing" + + "github.com/hyperledger/firefly/mocks/contractmocks" + "github.com/hyperledger/firefly/pkg/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestNewContractListenerSignature(t *testing.T) { + o, r := newTestAPIServer() + o.On("Authorize", mock.Anything, mock.Anything).Return(nil) + mcm := &contractmocks.Manager{} + o.On("Contracts").Return(mcm) + input := core.ContractListenerInput{} + var buf bytes.Buffer + json.NewEncoder(&buf).Encode(&input) + req := httptest.NewRequest("POST", "/api/v1/namespaces/mynamespace/contracts/listeners/signature", &buf) + req.Header.Set("Content-Type", "application/json; charset=utf-8") + res := httptest.NewRecorder() + + mcm.On("ConstructContractListenerSignature", mock.Anything, mock.AnythingOfType("*core.ContractListenerInput")). + Return(&core.ContractListenerSignatureOutput{}, nil, nil) + r.ServeHTTP(res, req) + + assert.Equal(t, 200, res.Result().StatusCode) +} diff --git a/internal/apiserver/routes.go b/internal/apiserver/routes.go index 16b5009ed..3f146cca5 100644 --- a/internal/apiserver/routes.go +++ b/internal/apiserver/routes.go @@ -134,6 +134,7 @@ var routes = append( postContractAPIPublish, postContractAPIQuery, postContractAPIListeners, + postContractListenerSignature, postContractInterfaceGenerate, postContractInterfacePublish, postContractDeploy, diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go index da9ca7809..afa3c2c06 100644 --- a/internal/blockchain/ethereum/ethereum.go +++ b/internal/blockchain/ethereum/ethereum.go @@ -843,6 +843,26 @@ func (e *Ethereum) QueryContract(ctx context.Context, signingKey string, locatio return output, nil // note UNLIKE fabric this is just `output`, not `output.Result` - but either way the top level of what we return to the end user, is whatever the Connector sent us } +func (e *Ethereum) CheckOverlappingLocations(ctx context.Context, left *fftypes.JSONAny, right *fftypes.JSONAny) (bool, error) { + if left == nil || right == nil { + // No location on either side so overlapping + return true, nil + } + + parsedLeft, err := e.parseContractLocation(ctx, left) + if err != nil { + return false, err + } + + parsedRight, err := e.parseContractLocation(ctx, right) + if err != nil { + return false, err + } + + // For Ethereum just compared addresses + return strings.EqualFold(parsedLeft.Address, parsedRight.Address), nil +} + func (e *Ethereum) NormalizeContractLocation(ctx context.Context, ntype blockchain.NormalizeType, location *fftypes.JSONAny) (result *fftypes.JSONAny, err error) { parsed, err := e.parseContractLocation(ctx, location) if err != nil { @@ -875,17 +895,48 @@ func (e *Ethereum) encodeContractLocation(ctx context.Context, location *Locatio } func (e *Ethereum) AddContractListener(ctx context.Context, listener *core.ContractListener, lastProtocolID string) (err error) { - var location *Location namespace := listener.Namespace - if listener.Location != nil { - location, err = e.parseContractLocation(ctx, listener.Location) + filters := make([]*filter, 0) + + if len(listener.Filters) == 0 { + return i18n.NewError(ctx, coremsgs.MsgFiltersEmpty, listener.Name) + } + + // For ethconnect we need to use one event and one location as it does not support filters + // Note: the first filter event gets copied to the root of the listener for backwards + // compatibility so available here + // it will be ignored by evmconnect + var firstEventABI *abi.Entry + firstEventABI, err = ffi2abi.ConvertFFIEventDefinitionToABI(ctx, &listener.Filters[0].Event.FFIEventDefinition) + if err != nil { + return i18n.WrapError(ctx, err, coremsgs.MsgContractParamInvalid) + } + + // First filter location is copied over to the root + var location *Location + if listener.Filters[0].Location != nil { + location, err = e.parseContractLocation(ctx, listener.Filters[0].Location) if err != nil { return err } } - abi, err := ffi2abi.ConvertFFIEventDefinitionToABI(ctx, &listener.Event.FFIEventDefinition) - if err != nil { - return i18n.WrapError(ctx, err, coremsgs.MsgContractParamInvalid) + + for _, f := range listener.Filters { + abi, err := ffi2abi.ConvertFFIEventDefinitionToABI(ctx, &f.Event.FFIEventDefinition) + if err != nil { + return i18n.WrapError(ctx, err, coremsgs.MsgContractParamInvalid) + } + evmFilter := &filter{ + Event: abi, + } + if f.Location != nil { + location, err := e.parseContractLocation(ctx, f.Location) + if err != nil { + return err + } + evmFilter.Address = location.Address + } + filters = append(filters, evmFilter) } subName := fmt.Sprintf("ff-sub-%s-%s", listener.Namespace, listener.ID) @@ -893,7 +944,7 @@ func (e *Ethereum) AddContractListener(ctx context.Context, listener *core.Contr if listener.Options != nil { firstEvent = listener.Options.FirstEvent } - result, err := e.streams.createSubscription(ctx, location, e.streamID[namespace], subName, firstEvent, abi, lastProtocolID) + result, err := e.streams.createSubscription(ctx, e.streamID[namespace], subName, firstEvent, location, firstEventABI, filters, lastProtocolID) if err != nil { return err } @@ -934,12 +985,56 @@ func (e *Ethereum) GetFFIParamValidator(ctx context.Context) (fftypes.FFIParamVa return &ffi2abi.ParamValidator{}, nil } -func (e *Ethereum) GenerateEventSignature(ctx context.Context, event *fftypes.FFIEventDefinition) string { +func (e *Ethereum) GenerateEventSignature(ctx context.Context, event *fftypes.FFIEventDefinition) (string, error) { abi, err := ffi2abi.ConvertFFIEventDefinitionToABI(ctx, event) if err != nil { + return "", err + } + signature := ffi2abi.ABIMethodToSignature(abi) + indexedSignature := ABIMethodToIndexedSignature(abi) + if indexedSignature != "" { + signature = fmt.Sprintf("%s %s", signature, indexedSignature) + } + return signature, nil +} + +func (e *Ethereum) GenerateEventSignatureWithLocation(ctx context.Context, event *fftypes.FFIEventDefinition, location *fftypes.JSONAny) (string, error) { + eventSignature, err := e.GenerateEventSignature(ctx, event) + if err != nil { + // new error here needed + return "", err + } + + // No location set + if location == nil { + return fmt.Sprintf("*:%s", eventSignature), nil + } + + parsed, err := e.parseContractLocation(ctx, location) + if err != nil { + return "", err + } + + return fmt.Sprintf("%s:%s", parsed.Address, eventSignature), nil +} + +func ABIMethodToIndexedSignature(abi *abi.Entry) string { + if len(abi.Inputs) == 0 { return "" } - return ffi2abi.ABIMethodToSignature(abi) + positions := []string{} + for i, param := range abi.Inputs { + if param.Indexed { + positions = append(positions, fmt.Sprint(i)) + } + } + + // No indexed fields + if len(positions) == 0 { + return "" + } + + return "[i=" + strings.Join(positions, ",") + "]" } func (e *Ethereum) GenerateErrorSignature(ctx context.Context, errorDef *fftypes.FFIErrorDefinition) string { diff --git a/internal/blockchain/ethereum/ethereum_test.go b/internal/blockchain/ethereum/ethereum_test.go index 01a24f7d4..e9c036f3c 100644 --- a/internal/blockchain/ethereum/ethereum_test.go +++ b/internal/blockchain/ethereum/ethereum_test.go @@ -2005,18 +2005,22 @@ func TestAddSubscription(t *testing.T) { } sub := &core.ContractListener{ - Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ - "address": "0x123", - }.String()), - Event: &core.FFISerializedEvent{ - FFIEventDefinition: fftypes.FFIEventDefinition{ - Name: "Changed", - Params: fftypes.FFIParams{ - { - Name: "value", - Schema: fftypes.JSONAnyPtr(`{"type": "string", "details": {"type": "string"}}`), + Filters: []*core.ListenerFilter{ + { + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "Changed", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "string", "details": {"type": "string"}}`), + }, + }, }, }, + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "0x123", + }.String()), }, }, Options: &core.ContractListenerOptions{ @@ -2043,13 +2047,17 @@ func TestAddSubscriptionWithoutLocation(t *testing.T) { } sub := &core.ContractListener{ - Event: &core.FFISerializedEvent{ - FFIEventDefinition: fftypes.FFIEventDefinition{ - Name: "Changed", - Params: fftypes.FFIParams{ - { - Name: "value", - Schema: fftypes.JSONAnyPtr(`{"type": "string", "details": {"type": "string"}}`), + Filters: []*core.ListenerFilter{ + { + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "Changed", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "string", "details": {"type": "string"}}`), + }, + }, }, }, }, @@ -2067,6 +2075,203 @@ func TestAddSubscriptionWithoutLocation(t *testing.T) { assert.NoError(t, err) } +func TestAddSubscriptionMultipleFilters(t *testing.T) { + e, cancel := newTestEthereum() + defer cancel() + httpmock.ActivateNonDefault(e.client.GetClient()) + defer httpmock.DeactivateAndReset() + e.streamID["ns1"] = "es-1" + e.streams = &streamManager{ + client: e.client, + } + + sub := &core.ContractListener{ + Filters: core.ListenerFilters{ + { + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "Changed", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "string", "details": {"type": "string"}}`), + }, + }, + }, + }, + }, + { + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "Changed2", + Params: fftypes.FFIParams{ + { + Name: "value2", + Schema: fftypes.JSONAnyPtr(`{"type": "string", "details": {"type": "string"}}`), + }, + }, + }, + }, + Location: fftypes.JSONAnyPtr(`{"address":"0x1234"}`), + }, + }, + Options: &core.ContractListenerOptions{ + FirstEvent: string(core.SubOptsFirstEventOldest), + }, + } + + httpmock.RegisterResponder("POST", `http://localhost:12345/subscriptions`, + httpmock.NewJsonResponderOrPanic(200, &subscription{})) + + err := e.AddContractListener(context.Background(), sub, "") + + assert.NoError(t, err) +} +func TestAddSubscriptionInvalidAbi(t *testing.T) { + e, cancel := newTestEthereum() + defer cancel() + httpmock.ActivateNonDefault(e.client.GetClient()) + defer httpmock.DeactivateAndReset() + e.streamID["ns1"] = "es-1" + e.streams = &streamManager{ + client: e.client, + } + + sub := &core.ContractListener{ + Filters: core.ListenerFilters{ + { + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "Changed", + Params: fftypes.FFIParams{ + { + Name: "value2", + Schema: fftypes.JSONAnyPtr(`{"type": "string", "details": {"type": "string"}}`), + }, + }, + }, + }, + Location: fftypes.JSONAnyPtr(`{"address":"0x1234"}`), + }, + { + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "0x123", + }.String()), + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "Changed2", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`"not an abi"`), + }, + }, + }, + }, + }, + }, + } + + httpmock.RegisterResponder("POST", `http://localhost:12345/subscriptions`, + httpmock.NewJsonResponderOrPanic(200, &subscription{})) + + err := e.AddContractListener(context.Background(), sub, "") + + assert.Regexp(t, "FF10311", err) +} + +func TestAddSubscriptionMultipleFiltersInvalidAbi(t *testing.T) { + e, cancel := newTestEthereum() + defer cancel() + httpmock.ActivateNonDefault(e.client.GetClient()) + defer httpmock.DeactivateAndReset() + e.streamID["ns1"] = "es-1" + e.streams = &streamManager{ + client: e.client, + } + + sub := &core.ContractListener{ + Filters: core.ListenerFilters{ + { + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "0x123", + }.String()), + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "Changed", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`"not an abi"`), + }, + }, + }, + }, + }, + }, + } + + httpmock.RegisterResponder("POST", `http://localhost:12345/subscriptions`, + httpmock.NewJsonResponderOrPanic(200, &subscription{})) + + err := e.AddContractListener(context.Background(), sub, "") + + assert.Regexp(t, "FF10311", err) +} + +func TestAddSubscriptionMultipleFiltersBadLocation(t *testing.T) { + e, cancel := newTestEthereum() + defer cancel() + httpmock.ActivateNonDefault(e.client.GetClient()) + defer httpmock.DeactivateAndReset() + e.streamID["ns1"] = "es-1" + e.streams = &streamManager{ + client: e.client, + } + + sub := &core.ContractListener{ + Filters: core.ListenerFilters{ + { + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "Changed", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "string", "details": {"type": "string"}}`), + }, + }, + }, + }, + }, + { + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "Changed2", + Params: fftypes.FFIParams{ + { + Name: "value2", + Schema: fftypes.JSONAnyPtr(`{"type": "string", "details": {"type": "string"}}`), + }, + }, + }, + }, + Location: fftypes.JSONAnyPtr(`{""}`), + }, + }, + Options: &core.ContractListenerOptions{ + FirstEvent: string(core.SubOptsFirstEventOldest), + }, + } + + httpmock.RegisterResponder("POST", `http://localhost:12345/subscriptions`, + httpmock.NewJsonResponderOrPanic(200, &subscription{})) + + err := e.AddContractListener(context.Background(), sub, "") + assert.Error(t, err) + assert.Regexp(t, "FF10310", err) +} + func TestAddSubscriptionBadParamDetails(t *testing.T) { e, cancel := newTestEthereum() defer cancel() @@ -2078,18 +2283,22 @@ func TestAddSubscriptionBadParamDetails(t *testing.T) { } sub := &core.ContractListener{ - Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ - "address": "0x123", - }.String()), - Event: &core.FFISerializedEvent{ - FFIEventDefinition: fftypes.FFIEventDefinition{ - Name: "Changed", - Params: fftypes.FFIParams{ - { - Name: "value", - Schema: fftypes.JSONAnyPtr(`{"type": "string", "details": {"type": ""}}`), + Filters: core.ListenerFilters{ + { + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "Changed", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "string", "details": {"type": ""}}`), + }, + }, }, }, + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "0x123", + }.String()), }, }, } @@ -2114,8 +2323,12 @@ func TestAddSubscriptionBadLocation(t *testing.T) { } sub := &core.ContractListener{ - Location: fftypes.JSONAnyPtr(""), - Event: &core.FFISerializedEvent{}, + Filters: core.ListenerFilters{ + { + Location: fftypes.JSONAnyPtr(""), + Event: &core.FFISerializedEvent{}, + }, + }, } err := e.AddContractListener(context.Background(), sub, "") @@ -2123,6 +2336,20 @@ func TestAddSubscriptionBadLocation(t *testing.T) { assert.Regexp(t, "FF10310", err) } +func TestAddListenerNoFiltersFail(t *testing.T) { + e, cancel := newTestEthereum() + defer cancel() + + sub := &core.ContractListener{ + Options: &core.ContractListenerOptions{ + FirstEvent: string(core.SubOptsFirstEventNewest), + }, + } + + err := e.AddContractListener(context.Background(), sub, "") + assert.Regexp(t, "FF10475", err) +} + func TestAddSubscriptionFail(t *testing.T) { e, cancel := newTestEthereum() defer cancel() @@ -2135,10 +2362,14 @@ func TestAddSubscriptionFail(t *testing.T) { } sub := &core.ContractListener{ - Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ - "address": "0x123", - }.String()), - Event: &core.FFISerializedEvent{}, + Filters: []*core.ListenerFilter{ + { + Event: &core.FFISerializedEvent{}, + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "0x123", + }.String()), + }, + }, Options: &core.ContractListenerOptions{ FirstEvent: string(core.SubOptsFirstEventNewest), }, @@ -3465,10 +3696,72 @@ func TestGenerateEventSignature(t *testing.T) { }, } - signature := e.GenerateEventSignature(context.Background(), event) + signature, err := e.GenerateEventSignature(context.Background(), event) + assert.NoError(t, err) assert.Equal(t, "Changed(uint256,uint256,(uint256,uint256))", signature) } +func TestGenerateEventSignatureWithIndexedFields(t *testing.T) { + e, _ := newTestEthereum() + complexParam := fftypes.JSONObject{ + "type": "object", + "details": fftypes.JSONObject{ + "type": "tuple", + }, + "properties": fftypes.JSONObject{ + "prop1": fftypes.JSONObject{ + "type": "integer", + "details": fftypes.JSONObject{ + "type": "uint256", + "index": 0, + }, + }, + "prop2": fftypes.JSONObject{ + "type": "integer", + "details": fftypes.JSONObject{ + "type": "uint256", + "index": 1, + "indexed": true, + }, + }, + }, + }.String() + + event := &fftypes.FFIEventDefinition{ + Name: "Changed", + Params: []*fftypes.FFIParam{ + { + Name: "x", + Schema: fftypes.JSONAnyPtr(`{"type": "integer", "details": {"type": "uint256"}}`), + }, + { + Name: "y", + Schema: fftypes.JSONAnyPtr(`{"type": "integer", "details": {"type": "uint256", "indexed": true}}`), + }, + { + Name: "z", + Schema: fftypes.JSONAnyPtr(complexParam), + }, + }, + } + + signature, err := e.GenerateEventSignature(context.Background(), event) + assert.NoError(t, err) + assert.Equal(t, "Changed(uint256,uint256,(uint256,uint256)) [i=1]", signature) +} + +func TestGenerateEventSignatureWithEmptyDefinition(t *testing.T) { + e, _ := newTestEthereum() + + event := &fftypes.FFIEventDefinition{ + Name: "Empty", + } + + signature, err := e.GenerateEventSignature(context.Background(), event) + assert.NoError(t, err) + assert.Equal(t, "Empty()", signature) +} + func TestGenerateEventSignatureInvalid(t *testing.T) { e, _ := newTestEthereum() event := &fftypes.FFIEventDefinition{ @@ -3481,7 +3774,8 @@ func TestGenerateEventSignatureInvalid(t *testing.T) { }, } - signature := e.GenerateEventSignature(context.Background(), event) + signature, err := e.GenerateEventSignature(context.Background(), event) + assert.Error(t, err) assert.Equal(t, "", signature) } @@ -4570,3 +4864,141 @@ func TestValidateInvokeRequest(t *testing.T) { err = e.ValidateInvokeRequest(context.Background(), parsedMethod, nil, true) assert.Regexp(t, "FF10443", err) } +func TestGenerateEventSignatureWithLocation(t *testing.T) { + e, cancel := newTestEthereum() + defer cancel() + location := &Location{ + Address: "3081D84FD367044F4ED453F2024709242470388C", + } + + event := &fftypes.FFIEventDefinition{ + Name: "Changed", + Params: []*fftypes.FFIParam{ + { + Name: "x", + Schema: fftypes.JSONAnyPtr(`{"type": "integer", "details": {"type": "uint256"}}`), + }, + { + Name: "y", + Schema: fftypes.JSONAnyPtr(`{"type": "integer", "details": {"type": "uint256"}}`), + }, + }, + } + locationBytes, err := json.Marshal(location) + assert.NoError(t, err) + result, err := e.GenerateEventSignatureWithLocation(context.Background(), event, fftypes.JSONAnyPtrBytes(locationBytes)) + assert.NoError(t, err) + assert.Equal(t, "3081D84FD367044F4ED453F2024709242470388C:Changed(uint256,uint256)", result) +} + +func TestGenerateEventSignatureWithEmptyLocation(t *testing.T) { + e, cancel := newTestEthereum() + defer cancel() + + event := &fftypes.FFIEventDefinition{ + Name: "Changed", + Params: []*fftypes.FFIParam{ + { + Name: "x", + Schema: fftypes.JSONAnyPtr(`{"type": "integer", "details": {"type": "uint256"}}`), + }, + { + Name: "y", + Schema: fftypes.JSONAnyPtr(`{"type": "integer", "details": {"type": "uint256"}}`), + }, + }, + } + result, err := e.GenerateEventSignatureWithLocation(context.Background(), event, nil) + assert.NoError(t, err) + assert.Equal(t, "*:Changed(uint256,uint256)", result) +} + +func TestGenerateEventSignatureWithLocationInvalidABI(t *testing.T) { + e, cancel := newTestEthereum() + defer cancel() + + event := &fftypes.FFIEventDefinition{ + Name: "Changed", + Params: []*fftypes.FFIParam{ + { + Name: "x", + Schema: fftypes.JSONAnyPtr(`{"invalid abi"}}`), + }, + }, + } + _, err := e.GenerateEventSignatureWithLocation(context.Background(), event, nil) + assert.Error(t, err) + assert.Regexp(t, "FF22052", err.Error()) +} + +func TestGenerateEventSignatureWithLocationInvalidLocation(t *testing.T) { + e, cancel := newTestEthereum() + defer cancel() + + event := &fftypes.FFIEventDefinition{ + Name: "Changed", + Params: []*fftypes.FFIParam{ + { + Name: "x", + Schema: fftypes.JSONAnyPtr(`{"type": "integer", "details": {"type": "uint256"}}`), + }, + { + Name: "y", + Schema: fftypes.JSONAnyPtr(`{"type": "integer", "details": {"type": "uint256"}}`), + }, + }, + } + locationBytes, err := json.Marshal("{}") + assert.NoError(t, err) + _, err = e.GenerateEventSignatureWithLocation(context.Background(), event, fftypes.JSONAnyPtrBytes(locationBytes)) + assert.Error(t, err) + assert.Regexp(t, "FF10310", err.Error()) +} + +func TestCheckOverLappingLocationsEmpty(t *testing.T) { + e, cancel := newTestEthereum() + defer cancel() + result, err := e.CheckOverlappingLocations(context.Background(), nil, nil) + assert.NoError(t, err) + assert.True(t, result) +} + +func TestCheckOverLappingLocationsBadLocation(t *testing.T) { + locationBytes, err := json.Marshal("{}") + assert.NoError(t, err) + e, cancel := newTestEthereum() + defer cancel() + _, err = e.CheckOverlappingLocations(context.Background(), fftypes.JSONAnyPtrBytes(locationBytes), fftypes.JSONAnyPtrBytes(locationBytes)) + assert.Error(t, err) + assert.Regexp(t, "FF10310", err.Error()) +} + +func TestCheckOverLappingLocationsBadLocationSecond(t *testing.T) { + location := &Location{ + Address: "3081D84FD367044F4ED453F2024709242470388C", + } + goodLocationBytes, err := json.Marshal(location) + assert.NoError(t, err) + + badLocationBytes, err := json.Marshal("{}") + assert.NoError(t, err) + e, cancel := newTestEthereum() + defer cancel() + _, err = e.CheckOverlappingLocations(context.Background(), fftypes.JSONAnyPtrBytes(goodLocationBytes), fftypes.JSONAnyPtrBytes(badLocationBytes)) + assert.Error(t, err) + assert.Regexp(t, "FF10310", err.Error()) +} + +func TestCheckOverLappingLocationsSame(t *testing.T) { + location := &Location{ + Address: "3081D84FD367044F4ED453F2024709242470388C", + } + locationBytes, err := json.Marshal(location) + assert.NoError(t, err) + + e, cancel := newTestEthereum() + defer cancel() + result, err := e.CheckOverlappingLocations(context.Background(), fftypes.JSONAnyPtrBytes(locationBytes), fftypes.JSONAnyPtrBytes(locationBytes)) + assert.NoError(t, err) + assert.True(t, result) +} diff --git a/internal/blockchain/ethereum/eventstream.go b/internal/blockchain/ethereum/eventstream.go index e913139f4..078ad9510 100644 --- a/internal/blockchain/ethereum/eventstream.go +++ b/internal/blockchain/ethereum/eventstream.go @@ -26,7 +26,6 @@ import ( "github.com/go-resty/resty/v2" "github.com/hyperledger/firefly-common/pkg/ffresty" - "github.com/hyperledger/firefly-common/pkg/fftypes" "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-signer/pkg/abi" @@ -54,16 +53,21 @@ type eventStream struct { } type subscription struct { - ID string `json:"id"` - Name string `json:"name,omitempty"` - Stream string `json:"stream"` - FromBlock string `json:"fromBlock"` - EthCompatAddress string `json:"address,omitempty"` - EthCompatEvent *abi.Entry `json:"event,omitempty"` - Filters []fftypes.JSONAny `json:"filters"` + ID string `json:"id"` + Name string `json:"name,omitempty"` + Stream string `json:"stream"` + FromBlock string `json:"fromBlock"` + EthCompatAddress string `json:"address,omitempty"` + EthCompatEvent *abi.Entry `json:"event,omitempty"` + Filters []*filter `json:"filters"` subscriptionCheckpoint } +type filter struct { + Event *abi.Entry `json:"event"` + Address string `json:"address,omitempty"` +} + type subscriptionCheckpoint struct { Checkpoint ListenerCheckpoint `json:"checkpoint,omitempty"` Catchup bool `json:"catchup,omitempty"` @@ -244,7 +248,7 @@ func resolveFromBlock(ctx context.Context, firstEvent, lastProtocolID string) (s return strconv.FormatUint(blockNumber, 10), nil } -func (s *streamManager) createSubscription(ctx context.Context, location *Location, stream, subName, firstEvent string, abi *abi.Entry, lastProtocolID string) (*subscription, error) { +func (s *streamManager) createSubscription(ctx context.Context, stream, subName, firstEvent string, location *Location, abi *abi.Entry, filters []*filter, lastProtocolID string) (*subscription, error) { fromBlock, err := resolveFromBlock(ctx, firstEvent, lastProtocolID) if err != nil { return nil, err @@ -254,7 +258,8 @@ func (s *streamManager) createSubscription(ctx context.Context, location *Locati Name: subName, Stream: stream, FromBlock: fromBlock, - EthCompatEvent: abi, + EthCompatEvent: abi, // only used for ethconnect + Filters: filters, } if location != nil { @@ -327,7 +332,13 @@ func (s *streamManager) ensureFireFlySubscription(ctx context.Context, namespace name = v1Name } location := &Location{Address: instancePath} - if sub, err = s.createSubscription(ctx, location, stream, name, firstEvent, abi, lastProtocolID); err != nil { + filters := []*filter{ + { + Event: abi, + Address: location.Address, + }, + } + if sub, err = s.createSubscription(ctx, stream, name, firstEvent, location, abi, filters, lastProtocolID); err != nil { return nil, err } log.L(ctx).Infof("%s subscription: %s", abi.Name, sub.ID) diff --git a/internal/blockchain/ethereum/eventstream_test.go b/internal/blockchain/ethereum/eventstream_test.go index dfb28f872..03dbc69ed 100644 --- a/internal/blockchain/ethereum/eventstream_test.go +++ b/internal/blockchain/ethereum/eventstream_test.go @@ -27,7 +27,7 @@ func TestCreateSubscriptionBadBlock(t *testing.T) { e, cancel := newTestEthereum() defer cancel() - _, err := e.streams.createSubscription(context.Background(), nil, "", "", "wrongness", nil, "") + _, err := e.streams.createSubscription(context.Background(), "", "", "wrongness", nil, nil, []*filter{}, "") assert.Regexp(t, "FF10473", err) } diff --git a/internal/blockchain/fabric/fabric.go b/internal/blockchain/fabric/fabric.go index b0b4f1cf1..7729aaac1 100644 --- a/internal/blockchain/fabric/fabric.go +++ b/internal/blockchain/fabric/fabric.go @@ -889,6 +889,31 @@ func jsonEncodeInput(params map[string]interface{}) (output map[string]interface return } +func (f *Fabric) CheckOverlappingLocations(ctx context.Context, left *fftypes.JSONAny, right *fftypes.JSONAny) (bool, error) { + parsedLeft, err := parseContractLocation(ctx, left) + if err != nil { + return false, err + } + + parsedRight, err := parseContractLocation(ctx, right) + if err != nil { + return false, err + } + + // Different channel so not overlapping + if parsedLeft.Channel != parsedRight.Channel { + return false, nil + } + + if parsedLeft.Chaincode == "" || parsedRight.Chaincode == "" { + // Either of them location is the whole channel + return true, nil + } + + // No just compare chaincodes + return parsedLeft.Chaincode == parsedRight.Chaincode, nil +} + func (f *Fabric) NormalizeContractLocation(ctx context.Context, ntype blockchain.NormalizeType, location *fftypes.JSONAny) (result *fftypes.JSONAny, err error) { parsed, err := parseContractLocation(ctx, location) if err != nil { @@ -897,6 +922,21 @@ func (f *Fabric) NormalizeContractLocation(ctx context.Context, ntype blockchain return encodeContractLocation(ctx, ntype, parsed) } +func (f *Fabric) stringifyContractLocation(ctx context.Context, location *fftypes.JSONAny) (string, error) { + parsed, err := parseContractLocation(ctx, location) + if err != nil { + return "", err + } + + // Concatinate channel and chaincode if present + result := fmt.Sprintf("%s-*", parsed.Channel) + if parsed.Chaincode != "" { + result = fmt.Sprintf("%s-%s", parsed.Channel, parsed.Chaincode) + } + + return result, nil +} + func parseContractLocation(ctx context.Context, location *fftypes.JSONAny) (*Location, error) { if location == nil { return nil, i18n.NewError(ctx, coremsgs.MsgContractLocationInvalid, "'channel' not set") @@ -927,13 +967,24 @@ func encodeContractLocation(ctx context.Context, ntype blockchain.NormalizeType, func (f *Fabric) AddContractListener(ctx context.Context, listener *core.ContractListener, lastProtocolID string) error { namespace := listener.Namespace - location, err := parseContractLocation(ctx, listener.Location) + + if len(listener.Filters) == 0 { + return i18n.NewError(ctx, coremsgs.MsgFiltersEmpty, listener.Name) + } + + if len(listener.Filters) > 1 { + return i18n.NewError(ctx, coremsgs.MsgContractListenerBlockchainFilterLimit, listener.Name) + } + + filter := listener.Filters[0] + + location, err := parseContractLocation(ctx, filter.Location) if err != nil { return err } subName := fmt.Sprintf("ff-sub-%s-%s", listener.Namespace, listener.ID) - result, err := f.streams.createSubscription(ctx, location, f.streamID[namespace], subName, listener.Event.Name, listener.Options.FirstEvent, lastProtocolID) + result, err := f.streams.createSubscription(ctx, location, f.streamID[namespace], subName, filter.Event.Name, listener.Options.FirstEvent, lastProtocolID) if err != nil { return err } @@ -959,8 +1010,17 @@ func (f *Fabric) GenerateFFI(ctx context.Context, generationRequest *fftypes.FFI return nil, i18n.NewError(ctx, coremsgs.MsgFFIGenerationUnsupported) } -func (f *Fabric) GenerateEventSignature(ctx context.Context, event *fftypes.FFIEventDefinition) string { - return event.Name +func (f *Fabric) GenerateEventSignature(ctx context.Context, event *fftypes.FFIEventDefinition) (string, error) { + return event.Name, nil +} + +func (f *Fabric) GenerateEventSignatureWithLocation(ctx context.Context, event *fftypes.FFIEventDefinition, location *fftypes.JSONAny) (string, error) { + strLocation, err := f.stringifyContractLocation(ctx, location) + if err != nil { + return "", err + } + + return fmt.Sprintf("%s:%s", strLocation, event.Name), nil } func (f *Fabric) GenerateErrorSignature(ctx context.Context, event *fftypes.FFIErrorDefinition) string { diff --git a/internal/blockchain/fabric/fabric_test.go b/internal/blockchain/fabric/fabric_test.go index 1c5cc7550..11799546c 100644 --- a/internal/blockchain/fabric/fabric_test.go +++ b/internal/blockchain/fabric/fabric_test.go @@ -1897,11 +1897,15 @@ func TestAddSubscription(t *testing.T) { } sub := &core.ContractListener{ - Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ - "channel": "firefly", - "chaincode": "mycode", - }.String()), - Event: &core.FFISerializedEvent{}, + Filters: core.ListenerFilters{ + { + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "channel": "firefly", + "chaincode": "mycode", + }.String()), + Event: &core.FFISerializedEvent{}, + }, + }, Options: &core.ContractListenerOptions{ FirstEvent: string(core.SubOptsFirstEventOldest), }, @@ -1920,6 +1924,51 @@ func TestAddSubscription(t *testing.T) { assert.NoError(t, err) } +func TestAddSubscriptionNoFiltersFail(t *testing.T) { + e, cancel := newTestFabric() + defer cancel() + + sub := &core.ContractListener{ + Options: &core.ContractListenerOptions{ + FirstEvent: string(core.SubOptsFirstEventOldest), + }, + } + + err := e.AddContractListener(context.Background(), sub, "") + assert.Regexp(t, "FF10475", err) +} + +func TestAddSubscriptionTooManyFiltersFail(t *testing.T) { + e, cancel := newTestFabric() + defer cancel() + + sub := &core.ContractListener{ + Filters: core.ListenerFilters{ + { + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "channel": "firefly", + "chaincode": "mycode", + }.String()), + Event: &core.FFISerializedEvent{}, + }, + { + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "channel": "firefly", + "chaincode": "mycode", + }.String()), + Event: &core.FFISerializedEvent{}, + }, + }, + Options: &core.ContractListenerOptions{ + FirstEvent: string(core.SubOptsFirstEventOldest), + }, + } + + err := e.AddContractListener(context.Background(), sub, "") + + assert.Regexp(t, "FF10476", err) +} + func TestAddSubscriptionNoChannel(t *testing.T) { e, cancel := newTestFabric() defer cancel() @@ -1932,10 +1981,14 @@ func TestAddSubscriptionNoChannel(t *testing.T) { } sub := &core.ContractListener{ - Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ - "chaincode": "mycode", - }.String()), - Event: &core.FFISerializedEvent{}, + Filters: core.ListenerFilters{ + { + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "chaincode": "mycode", + }.String()), + Event: &core.FFISerializedEvent{}, + }, + }, Options: &core.ContractListenerOptions{ FirstEvent: string(core.SubOptsFirstEventOldest), }, @@ -1966,7 +2019,11 @@ func TestAddSubscriptionNoLocation(t *testing.T) { } sub := &core.ContractListener{ - Event: &core.FFISerializedEvent{}, + Filters: core.ListenerFilters{ + { + Event: &core.FFISerializedEvent{}, + }, + }, Options: &core.ContractListenerOptions{ FirstEvent: string(core.SubOptsFirstEventOldest), }, @@ -1989,8 +2046,12 @@ func TestAddSubscriptionBadLocation(t *testing.T) { } sub := &core.ContractListener{ - Location: fftypes.JSONAnyPtr(""), - Event: &core.FFISerializedEvent{}, + Filters: core.ListenerFilters{ + { + Location: fftypes.JSONAnyPtr(""), + Event: &core.FFISerializedEvent{}, + }, + }, } err := e.AddContractListener(context.Background(), sub, "") @@ -2010,11 +2071,15 @@ func TestAddSubscriptionFail(t *testing.T) { } sub := &core.ContractListener{ - Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ - "channel": "firefly", - "chaincode": "mycode", - }.String()), - Event: &core.FFISerializedEvent{}, + Filters: core.ListenerFilters{ + { + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "channel": "firefly", + "chaincode": "mycode", + }.String()), + Event: &core.FFISerializedEvent{}, + }, + }, Options: &core.ContractListenerOptions{ FirstEvent: string(core.SubOptsFirstEventNewest), }, @@ -2890,10 +2955,45 @@ func TestGenerateFFI(t *testing.T) { func TestGenerateEventSignature(t *testing.T) { e, _ := newTestFabric() - signature := e.GenerateEventSignature(context.Background(), &fftypes.FFIEventDefinition{Name: "Changed"}) + signature, err := e.GenerateEventSignature(context.Background(), &fftypes.FFIEventDefinition{Name: "Changed"}) + assert.NoError(t, err) assert.Equal(t, "Changed", signature) } +func TestStringifyContractLocationBadLocation(t *testing.T) { + e, _ := newTestFabric() + + location := fftypes.JSONAnyPtr(fftypes.JSONObject{ + "bad": "no good", + }.String()) + _, err := e.stringifyContractLocation(context.Background(), location) + assert.Error(t, err) + assert.Regexp(t, "FF10310", err.Error()) +} + +func TestGenerateEventSignatureWithBadLocation(t *testing.T) { + e, _ := newTestFabric() + + location := fftypes.JSONAnyPtr(fftypes.JSONObject{ + "bad": "no good", + }.String()) + _, err := e.GenerateEventSignatureWithLocation(context.Background(), &fftypes.FFIEventDefinition{Name: "Changed"}, location) + assert.Error(t, err) + assert.Regexp(t, "FF10310", err.Error()) +} + +func TestGenerateEventSignatureWithLocation(t *testing.T) { + e, _ := newTestFabric() + + location := fftypes.JSONAnyPtr(fftypes.JSONObject{ + "channel": "firefly", + "chaincode": "simplestorage", + }.String()) + signature, err := e.GenerateEventSignatureWithLocation(context.Background(), &fftypes.FFIEventDefinition{Name: "Changed"}, location) + assert.NoError(t, err) + assert.Equal(t, "firefly-simplestorage:Changed", signature) +} + func matchNetworkAction(action string, expectedSigningKey core.VerifierRef) interface{} { return mock.MatchedBy(func(batch []*blockchain.EventToDispatch) bool { return len(batch) == 1 && @@ -3409,3 +3509,103 @@ func TestQueryContractBadFFI(t *testing.T) { _, err := e.QueryContract(context.Background(), "", nil, nil, nil, nil) assert.Regexp(t, "FF10457", err) } + +func TestCheckOverLappingLocationsEmpty(t *testing.T) { + e, cancel := newTestFabric() + defer cancel() + location := &Location{ + Channel: "firefly", + Chaincode: "simplestorage", + } + locationBytes, err := json.Marshal(location) + assert.NoError(t, err) + result, err := e.CheckOverlappingLocations(context.Background(), fftypes.JSONAnyPtrBytes(locationBytes), nil) + assert.Error(t, err) + assert.Regexp(t, "FF10310", err.Error()) + assert.False(t, result) +} + +func TestCheckOverLappingLocationsBadLocation(t *testing.T) { + e, cancel := newTestFabric() + defer cancel() + locationBytes, err := json.Marshal("{}") + assert.NoError(t, err) + _, err = e.CheckOverlappingLocations(context.Background(), fftypes.JSONAnyPtrBytes(locationBytes), nil) + assert.Error(t, err) + assert.Regexp(t, "FF10310", err.Error()) +} + +func TestCheckOverLappingLocationsDifferentChannel(t *testing.T) { + e, cancel := newTestFabric() + defer cancel() + location := &Location{ + Channel: "firefly", + Chaincode: "simplestorage", + } + locationBytes, err := json.Marshal(location) + assert.NoError(t, err) + + location2 := &Location{ + Channel: "anotherchannel", + Chaincode: "simplestorage", + } + location2Bytes, err := json.Marshal(location2) + assert.NoError(t, err) + result, err := e.CheckOverlappingLocations(context.Background(), fftypes.JSONAnyPtrBytes(locationBytes), fftypes.JSONAnyPtrBytes(location2Bytes)) + assert.NoError(t, err) + assert.False(t, result) +} + +func TestCheckOverLappingLocationsSameChannel(t *testing.T) { + e, cancel := newTestFabric() + defer cancel() + location := &Location{ + Channel: "firefly", + Chaincode: "simplestorage", + } + locationBytes, err := json.Marshal(location) + assert.NoError(t, err) + + location2 := &Location{ + Channel: "firefly", + } + location2Bytes, err := json.Marshal(location2) + assert.NoError(t, err) + result, err := e.CheckOverlappingLocations(context.Background(), fftypes.JSONAnyPtrBytes(locationBytes), fftypes.JSONAnyPtrBytes(location2Bytes)) + assert.NoError(t, err) + assert.True(t, result) +} + +func TestCheckOverLappingLocationsSameChannelSameChaincode(t *testing.T) { + e, cancel := newTestFabric() + defer cancel() + location := &Location{ + Channel: "firefly", + Chaincode: "simplestorage", + } + locationBytes, err := json.Marshal(location) + assert.NoError(t, err) + result, err := e.CheckOverlappingLocations(context.Background(), fftypes.JSONAnyPtrBytes(locationBytes), fftypes.JSONAnyPtrBytes(locationBytes)) + assert.NoError(t, err) + assert.True(t, result) +} + +func TestCheckOverLappingLocationsSameChannelDifferentChaincode(t *testing.T) { + e, cancel := newTestFabric() + defer cancel() + location := &Location{ + Channel: "firefly", + Chaincode: "simplestorage", + } + locationBytes, err := json.Marshal(location) + assert.NoError(t, err) + + location2 := &Location{ + Channel: "firefly", + Chaincode: "anotherchaincode", + } + location2Bytes, err := json.Marshal(location2) + result, err := e.CheckOverlappingLocations(context.Background(), fftypes.JSONAnyPtrBytes(locationBytes), fftypes.JSONAnyPtrBytes(location2Bytes)) + assert.NoError(t, err) + assert.False(t, result) +} diff --git a/internal/blockchain/tezos/tezos.go b/internal/blockchain/tezos/tezos.go index 9f2225e5b..8dd1fb8d4 100644 --- a/internal/blockchain/tezos/tezos.go +++ b/internal/blockchain/tezos/tezos.go @@ -410,14 +410,54 @@ func (t *Tezos) NormalizeContractLocation(ctx context.Context, ntype blockchain. return t.encodeContractLocation(ctx, parsed) } +func (t *Tezos) CheckOverlappingLocations(ctx context.Context, left *fftypes.JSONAny, right *fftypes.JSONAny) (bool, error) { + if left == nil || right == nil { + // No location on either side so overlapping + // as means listening to everything + return true, nil + } + + parsedLeft, err := t.parseContractLocation(ctx, left) + if err != nil { + return false, err + } + + parsedRight, err := t.parseContractLocation(ctx, right) + if err != nil { + return false, err + } + + // For Ethereum just compared addresses + return parsedLeft.Address == parsedRight.Address, nil +} + +func (t *Tezos) StringifyContractLocation(ctx context.Context, location *fftypes.JSONAny) (string, error) { + parsed, err := t.parseContractLocation(ctx, location) + if err != nil { + return "", err + } + + return parsed.Address, nil +} + func (t *Tezos) AddContractListener( ctx context.Context, listener *core.ContractListener, _ string, // Tezos lexicographically sortable protocol IDs for not yet implemented for events ) (err error) { + if len(listener.Filters) == 0 { + return i18n.NewError(ctx, coremsgs.MsgFiltersEmpty, listener.Name) + } + + if len(listener.Filters) > 1 { + return i18n.NewError(ctx, coremsgs.MsgContractListenerBlockchainFilterLimit, listener.Name) + } + + filter := listener.Filters[0] + var location *Location - if listener.Location != nil { - location, err = t.parseContractLocation(ctx, listener.Location) + if filter.Location != nil { + location, err = t.parseContractLocation(ctx, filter.Location) if err != nil { return err } @@ -428,7 +468,7 @@ func (t *Tezos) AddContractListener( if listener.Options != nil { firstEvent = listener.Options.FirstEvent } - result, err := t.streams.createSubscription(ctx, location, t.streamID, subName, listener.Event.Name, firstEvent) + result, err := t.streams.createSubscription(ctx, location, t.streamID, subName, filter.Event.Name, firstEvent) if err != nil { return err } @@ -471,8 +511,24 @@ func (t *Tezos) GetFFIParamValidator(ctx context.Context) (fftypes.FFIParamValid return nil, nil } -func (t *Tezos) GenerateEventSignature(ctx context.Context, event *fftypes.FFIEventDefinition) string { - return event.Name +func (t *Tezos) GenerateEventSignature(ctx context.Context, event *fftypes.FFIEventDefinition) (string, error) { + return event.Name, nil +} + +func (t *Tezos) GenerateEventSignatureWithLocation(ctx context.Context, event *fftypes.FFIEventDefinition, location *fftypes.JSONAny) (string, error) { + eventSignature, _ := t.GenerateEventSignature(ctx, event) + + // No location set + if location == nil { + return fmt.Sprintf("*:%s", eventSignature), nil + } + + parsed, err := t.parseContractLocation(ctx, location) + if err != nil { + return "", err + } + + return fmt.Sprintf("%s:%s", parsed.Address, eventSignature), nil } func (t *Tezos) GenerateErrorSignature(ctx context.Context, event *fftypes.FFIErrorDefinition) string { diff --git a/internal/blockchain/tezos/tezos_test.go b/internal/blockchain/tezos/tezos_test.go index ee9c3dd8a..9b545a4ca 100644 --- a/internal/blockchain/tezos/tezos_test.go +++ b/internal/blockchain/tezos/tezos_test.go @@ -811,12 +811,16 @@ func TestAddSubscription(t *testing.T) { } sub := &core.ContractListener{ - Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ - "address": "KT123", - }.String()), - Event: &core.FFISerializedEvent{ - FFIEventDefinition: fftypes.FFIEventDefinition{ - Name: "Changed", + Filters: []*core.ListenerFilter{ + { + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "Changed", + }, + }, + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "KT123", + }.String()), }, }, Options: &core.ContractListenerOptions{ @@ -843,9 +847,13 @@ func TestAddSubscriptionWithoutLocation(t *testing.T) { } sub := &core.ContractListener{ - Event: &core.FFISerializedEvent{ - FFIEventDefinition: fftypes.FFIEventDefinition{ - Name: "Changed", + Filters: []*core.ListenerFilter{ + { + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "Changed", + }, + }, }, }, Options: &core.ContractListenerOptions{ @@ -873,8 +881,12 @@ func TestAddSubscriptionBadLocation(t *testing.T) { } sub := &core.ContractListener{ - Location: fftypes.JSONAnyPtr(""), - Event: &core.FFISerializedEvent{}, + Filters: core.ListenerFilters{ + { + Location: fftypes.JSONAnyPtr(""), + Event: &core.FFISerializedEvent{}, + }, + }, } err := tz.AddContractListener(context.Background(), sub, "") @@ -893,10 +905,14 @@ func TestAddSubscriptionFail(t *testing.T) { } sub := &core.ContractListener{ - Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ - "address": "KT123", - }.String()), - Event: &core.FFISerializedEvent{}, + Filters: core.ListenerFilters{ + { + Event: &core.FFISerializedEvent{}, + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "KT123", + }.String()), + }, + }, Options: &core.ContractListenerOptions{ FirstEvent: string(core.SubOptsFirstEventNewest), }, @@ -910,6 +926,60 @@ func TestAddSubscriptionFail(t *testing.T) { assert.Regexp(t, "FF10283.*pop", err) } +func TestAddSubscriptionNoFiltersFail(t *testing.T) { + tz, cancel := newTestTezos() + defer cancel() + + tz.streamID = "es-1" + tz.streams = &streamManager{ + client: tz.client, + } + + sub := &core.ContractListener{ + Options: &core.ContractListenerOptions{ + FirstEvent: string(core.SubOptsFirstEventNewest), + }, + } + + err := tz.AddContractListener(context.Background(), sub, "") + + assert.Regexp(t, "FF10475", err) +} + +func TestAddSubscriptionTwoManyFiltersFail(t *testing.T) { + tz, cancel := newTestTezos() + defer cancel() + + tz.streamID = "es-1" + tz.streams = &streamManager{ + client: tz.client, + } + + sub := &core.ContractListener{ + Filters: core.ListenerFilters{ + { + Event: &core.FFISerializedEvent{}, + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "KT123", + }.String()), + }, + { + Event: &core.FFISerializedEvent{}, + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "KT123", + }.String()), + }, + }, + Options: &core.ContractListenerOptions{ + FirstEvent: string(core.SubOptsFirstEventNewest), + }, + } + + err := tz.AddContractListener(context.Background(), sub, "") + + assert.Regexp(t, "FF10476", err) +} + func TestDeleteSubscription(t *testing.T) { tz, cancel := newTestTezos() defer cancel() @@ -1533,10 +1603,43 @@ func TestNormalizeContractLocationBlank(t *testing.T) { func TestGenerateEventSignature(t *testing.T) { tz, cancel := newTestTezos() defer cancel() - signature := tz.GenerateEventSignature(context.Background(), &fftypes.FFIEventDefinition{Name: "Changed"}) + signature, err := tz.GenerateEventSignature(context.Background(), &fftypes.FFIEventDefinition{Name: "Changed"}) + assert.NoError(t, err) assert.Equal(t, "Changed", signature) } +func TestGenerateEventSignatureWithLocationEmpty(t *testing.T) { + tz, cancel := newTestTezos() + defer cancel() + signature, err := tz.GenerateEventSignatureWithLocation(context.Background(), &fftypes.FFIEventDefinition{Name: "Changed"}, nil) + assert.NoError(t, err) + assert.Equal(t, "*:Changed", signature) +} + +func TestGenerateEventSignatureWithLocationBlank(t *testing.T) { + tz, cancel := newTestTezos() + defer cancel() + location := &Location{} + locationBytes, err := json.Marshal(location) + assert.NoError(t, err) + _, err = tz.GenerateEventSignatureWithLocation(context.Background(), &fftypes.FFIEventDefinition{Name: "Changed"}, fftypes.JSONAnyPtrBytes(locationBytes)) + assert.Error(t, err) + assert.Regexp(t, "FF10310", err) +} + +func TestGenerateEventSignatureWithLocation(t *testing.T) { + tz, cancel := newTestTezos() + defer cancel() + location := &Location{ + Address: "KT1CosvuPHD6YnY4uYNguJj6m58UuHJWyS1u", + } + locationBytes, err := json.Marshal(location) + assert.NoError(t, err) + signature, err := tz.GenerateEventSignatureWithLocation(context.Background(), &fftypes.FFIEventDefinition{Name: "Changed"}, fftypes.JSONAnyPtrBytes(locationBytes)) + assert.NoError(t, err) + assert.Equal(t, "KT1CosvuPHD6YnY4uYNguJj6m58UuHJWyS1u:Changed", signature) +} + func TestAddSubBadLocation(t *testing.T) { tz, cancel := newTestTezos() defer cancel() @@ -2019,3 +2122,79 @@ func TestStopNamespace(t *testing.T) { err := tz.StopNamespace(context.Background(), "ns1") assert.NoError(t, err) } + +func TestStringifyNormalizeContractLocation(t *testing.T) { + e, cancel := newTestTezos() + defer cancel() + location := &Location{ + Address: "3081D84FD367044F4ED453F2024709242470388C", + } + locationBytes, err := json.Marshal(location) + assert.NoError(t, err) + result, err := e.StringifyContractLocation(context.Background(), fftypes.JSONAnyPtrBytes(locationBytes)) + assert.NoError(t, err) + assert.Equal(t, "3081D84FD367044F4ED453F2024709242470388C", result) +} + +func TestStringifyNormalizeContractLocationError(t *testing.T) { + e, cancel := newTestTezos() + defer cancel() + location := &Location{} + locationBytes, err := json.Marshal(location) + assert.NoError(t, err) + _, err = e.StringifyContractLocation(context.Background(), fftypes.JSONAnyPtrBytes(locationBytes)) + assert.Error(t, err) + assert.Regexp(t, "FF10310", err) +} + +func TestCheckOverlappingLocationsEmpty(t *testing.T) { + e, cancel := newTestTezos() + defer cancel() + location := &Location{} + locationBytes, err := json.Marshal(location) + assert.NoError(t, err) + overlapping, err := e.CheckOverlappingLocations(context.Background(), nil, fftypes.JSONAnyPtrBytes(locationBytes)) + assert.NoError(t, err) + assert.True(t, overlapping) +} + +func TestCheckOverlappingLocationsBadLocation(t *testing.T) { + e, cancel := newTestTezos() + defer cancel() + location := &Location{} + locationBytes, err := json.Marshal(location) + assert.NoError(t, err) + _, err = e.CheckOverlappingLocations(context.Background(), fftypes.JSONAnyPtrBytes(locationBytes), fftypes.JSONAnyPtrBytes(locationBytes)) + assert.Error(t, err) + assert.Regexp(t, "FF10310", err.Error()) +} + +func TestCheckOverlappingLocationsOneLocation(t *testing.T) { + e, cancel := newTestTezos() + defer cancel() + location := &Location{ + Address: "3081D84FD367044F4ED453F2024709242470388C", + } + locationBytes, err := json.Marshal(location) + assert.NoError(t, err) + + location2 := &Location{} + location2Bytes, err := json.Marshal(location2) + assert.NoError(t, err) + _, err = e.CheckOverlappingLocations(context.Background(), fftypes.JSONAnyPtrBytes(locationBytes), fftypes.JSONAnyPtrBytes(location2Bytes)) + assert.Error(t, err) + assert.Regexp(t, "FF10310", err.Error()) +} + +func TestCheckOverlappingLocationsSameLocation(t *testing.T) { + e, cancel := newTestTezos() + defer cancel() + location := &Location{ + Address: "3081D84FD367044F4ED453F2024709242470388C", + } + locationBytes, err := json.Marshal(location) + assert.NoError(t, err) + result, err := e.CheckOverlappingLocations(context.Background(), fftypes.JSONAnyPtrBytes(locationBytes), fftypes.JSONAnyPtrBytes(locationBytes)) + assert.NoError(t, err) + assert.True(t, result) +} diff --git a/internal/contracts/manager.go b/internal/contracts/manager.go index 153c6a074..ab45cf18e 100644 --- a/internal/contracts/manager.go +++ b/internal/contracts/manager.go @@ -23,6 +23,7 @@ import ( "encoding/hex" "fmt" "hash" + "sort" "strings" "github.com/hyperledger/firefly-common/pkg/ffapi" @@ -71,6 +72,7 @@ type Manager interface { ResolveContractAPI(ctx context.Context, httpServerURL string, api *core.ContractAPI) error DeleteContractAPI(ctx context.Context, apiName string) error + ConstructContractListenerSignature(ctx context.Context, listener *core.ContractListenerInput) (output *core.ContractListenerSignatureOutput, err error) AddContractListener(ctx context.Context, listener *core.ContractListenerInput) (output *core.ContractListener, err error) AddContractAPIListener(ctx context.Context, apiName, eventPath string, listener *core.ContractListener) (output *core.ContractListener, err error) GetContractListenerByNameOrID(ctx context.Context, nameOrID string) (*core.ContractListener, error) @@ -206,7 +208,10 @@ func (cm *contractManager) GetFFIEvents(ctx context.Context, id *fftypes.UUID) ( events, _, err := cm.database.GetFFIEvents(ctx, cm.namespace, fb.Eq("interface", id)) if err == nil { for _, event := range events { - event.Signature = cm.blockchain.GenerateEventSignature(ctx, &event.FFIEventDefinition) + event.Signature, err = cm.blockchain.GenerateEventSignature(ctx, &event.FFIEventDefinition) + if err != nil { + return nil, err + } } } return events, err @@ -223,10 +228,6 @@ func (cm *contractManager) getFFIChildren(ctx context.Context, ffi *fftypes.FFI) return err } - for _, event := range ffi.Events { - event.Signature = cm.blockchain.GenerateEventSignature(ctx, &event.FFIEventDefinition) - } - fb := database.FFIErrorQueryFactory.NewFilter(ctx) ffi.Errors, _, err = cm.database.GetFFIErrors(ctx, cm.namespace, fb.Eq("interface", ffi.ID)) if err != nil { @@ -269,12 +270,41 @@ func (cm *contractManager) verifyListeners(ctx context.Context) error { log.L(ctx).Infof("Listener restore complete. Verified=%d", verifyCount) return nil } + + // Migrate and check if listener exists in blockchain plugin + migratedListeners := []*core.ContractListener{} for _, l := range listeners { + migrated, l, err := cm.MigrateToFiltersIfNeeded(ctx, l) + if err != nil { + return err + } + if migrated { + migratedListeners = append(migratedListeners, l) + } + if err := cm.checkContractListenerExists(ctx, l); err != nil { return err } verifyCount++ } + + // Write back the migrations + if len(migratedListeners) > 0 { + err := cm.database.RunAsGroup(ctx, func(ctx context.Context) (err error) { + for _, listener := range migratedListeners { + err := cm.database.UpsertContractListener(ctx, listener, true) + if err != nil { + return err + } + } + + return nil + }) + if err != nil { + return err + } + } + page++ } @@ -748,6 +778,7 @@ func (cm *contractManager) validateInvokeContractRequest(ctx context.Context, re return nil, i18n.NewError(ctx, coremsgs.MsgCannotSetParameterWithMessage, lastParam.Name) } } + for _, param := range req.Method.Params[:lastIndex] { schema, schemaOk := paramSchemas[param.Name] value, valueOk := req.Input[param.Name] @@ -828,7 +859,135 @@ func (cm *contractManager) checkContractListenerExists(ctx context.Context, list database.ContractListenerQueryFactory.NewUpdate(ctx).Set("backendid", listener.BackendID)) } -func (cm *contractManager) AddContractListener(ctx context.Context, listener *core.ContractListenerInput) (output *core.ContractListener, err error) { +func (cm *contractManager) parseContractListenerFilters(ctx context.Context, listener *core.ContractListenerInput) (err error) { + // Handle deprecated root event + if len(listener.Filters) == 0 { + // Copy the deprecated interface into the first element in the filters array + listener.Filters = append(listener.Filters, &core.ListenerFilterInput{ + ListenerFilter: core.ListenerFilter{ + Event: listener.Event, + Location: listener.Location, + Interface: listener.Interface, + }, + EventPath: listener.EventPath, + }) + } + + // This map check the whole signature + location + duplicateSignatureChecker := map[string]bool{} + // This will check the event signature + overlapping locations + duplicateEventSignatureChecker := map[string]*fftypes.JSONAny{} + + for _, filter := range listener.Filters { + if filter.Event == nil { + if filter.EventPath == "" || filter.Interface == nil { + return i18n.NewError(ctx, coremsgs.MsgListenerNoEvent) + } + // Copy the event definition into the filter + if filter.Event, err = cm.resolveEvent(ctx, filter.Interface, filter.EventPath); err != nil { + return err + } + } else { + filter.Interface = nil + } + + if err := cm.validateFFIEvent(ctx, &filter.Event.FFIEventDefinition); err != nil { + return err + } + + if filter.Location != nil { + if filter.Location, err = cm.blockchain.NormalizeContractLocation(ctx, blockchain.NormalizeListener, filter.Location); err != nil { + return err + } + } + + filter.Signature, err = cm.blockchain.GenerateEventSignatureWithLocation(ctx, &filter.Event.FFIEventDefinition, filter.Location) + if err != nil { + return err + } + + // Check if we have parsed a filter with the same signature including location + if duplicateSignatureChecker[filter.Signature] { + return i18n.NewError(ctx, coremsgs.MsgDuplicateContractListenerFilterLocation) + } + + eventSignature, err := cm.blockchain.GenerateEventSignature(ctx, &filter.Event.FFIEventDefinition) + if err != nil { + return err + } + + // Check if we have parsed a filter with the same signature but not location + if location, ok := duplicateEventSignatureChecker[eventSignature]; ok { + // If this duplicate filter is looking at all locations it's a superset of the previous one + // or the previous filter was also looking at all locations then we are trying to add a subset + if filter.Location == nil || location == nil { + return i18n.NewError(ctx, coremsgs.MsgDuplicateContractListenerFilterLocation) + } + + // Have to call the specific blockchain plugin to compare locations + isOverLapping, err := cm.blockchain.CheckOverlappingLocations(ctx, location, filter.Location) + if err != nil { + return err + } + + if isOverLapping { + return i18n.NewError(ctx, coremsgs.MsgDuplicateContractListenerFilterLocation) + } + } + + listener.ContractListener.Filters = append(listener.ContractListener.Filters, &core.ListenerFilter{ + Event: filter.Event, + Location: filter.Location, + Interface: filter.Interface, + Signature: filter.Signature, + }) + + duplicateSignatureChecker[filter.Signature] = true + duplicateEventSignatureChecker[eventSignature] = filter.Location + } + + // Sort to keep consistent and generate the same order of signatures + sort.Slice(listener.ContractListener.Filters, func(i, j int) bool { + return listener.ContractListener.Filters[i].Signature < listener.ContractListener.Filters[j].Signature + }) + + // Don't need to initialize it. + var sb strings.Builder + for i, filter := range listener.ContractListener.Filters { + sb.WriteString(filter.Signature) + if i+1 < len(listener.Filters) { + // Separator between filter signatures if more after this one + sb.WriteString(";") + } + } + + listener.Signature = sb.String() + + // To preserve compatibility copy the first element in the + // filters arary to the top if not present already + if listener.Event == nil && len(listener.Filters) > 0 { + listener.Event = listener.Filters[0].Event + listener.Location = listener.Filters[0].Location + listener.Interface = listener.Filters[0].Interface + } + + return nil +} + +func (cm *contractManager) ConstructContractListenerSignature(ctx context.Context, listener *core.ContractListenerInput) (output *core.ContractListenerSignatureOutput, err error) { + output = &core.ContractListenerSignatureOutput{} + + err = cm.parseContractListenerFilters(ctx, listener) + if err != nil { + return nil, err + } + + output.Signature = listener.Signature + + return output, nil +} + +func (cm *contractManager) verifyContractListener(ctx context.Context, listener *core.ContractListenerInput) (output *core.ContractListener, err error) { listener.ID = fftypes.NewUUID() listener.Namespace = cm.namespace @@ -841,6 +1000,12 @@ func (cm *contractManager) AddContractListener(ctx context.Context, listener *co return nil, err } + // Check that both the new filters and deprecated fields are not specified + if len(listener.Filters) > 0 && (listener.Event != nil || listener.EventPath != "") { + return nil, i18n.NewError(ctx, coremsgs.MsgFiltersAndRootEventError, cm.namespace, listener.Name) + } + + // This location only applies to the root event and will be ignore as part of filters if listener.Location != nil { if listener.Location, err = cm.blockchain.NormalizeContractLocation(ctx, blockchain.NormalizeListener, listener.Location); err != nil { return nil, err @@ -853,6 +1018,11 @@ func (cm *contractManager) AddContractListener(ctx context.Context, listener *co listener.Options.FirstEvent = cm.getDefaultContractListenerOptions().FirstEvent } + _, err = cm.ConstructContractListenerSignature(ctx, listener) + if err != nil { + return nil, err + } + err = cm.database.RunAsGroup(ctx, func(ctx context.Context) (err error) { // Namespace + Name must be unique if listener.Name != "" { @@ -863,20 +1033,6 @@ func (cm *contractManager) AddContractListener(ctx context.Context, listener *co } } - if listener.Event == nil { - if listener.EventPath == "" || listener.Interface == nil { - return i18n.NewError(ctx, coremsgs.MsgListenerNoEvent) - } - // Copy the event definition into the listener - if listener.Event, err = cm.resolveEvent(ctx, listener.Interface, listener.EventPath); err != nil { - return err - } - } else { - listener.Interface = nil - } - - // 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. @@ -884,6 +1040,12 @@ func (cm *contractManager) AddContractListener(ctx context.Context, listener *co if !listener.Location.IsNil() { locationLookup = listener.Location.String() } + if len(listener.Filters) == 1 && listener.Filters[0].Location != nil { + // For backwards compatibility with existing contract listeners with one filter + // We need to set the location to not find clashes + listener.Location = listener.Filters[0].Location + locationLookup = listener.Filters[0].Location.String() + } fb := database.ContractListenerQueryFactory.NewFilter(ctx) if existing, _, err := cm.database.GetContractListeners(ctx, cm.namespace, fb.And( fb.Eq("topic", listener.Topic), @@ -894,13 +1056,48 @@ func (cm *contractManager) AddContractListener(ctx context.Context, listener *co } else if len(existing) > 0 { return i18n.NewError(ctx, coremsgs.MsgContractListenerExists) } + + // Check for existense of an older listener with the old signature + // Only valid for one filter + if listener.Event != nil && len(listener.Filters) == 1 { + // Note the event signature has been extended with more information in some blockchain plugins + // That is why we do not add the signature in the query but instead iterate over the listeners + // and compare the signatures + signature, err := cm.blockchain.GenerateEventSignature(ctx, &listener.Event.FFIEventDefinition) + if err != nil { + return err + } + filter := database.ContractListenerQueryFactory.NewFilter(ctx) + if existing, _, err := cm.database.GetContractListeners(ctx, cm.namespace, filter.And( + filter.Eq("topic", listener.Topic), + filter.Eq("location", locationLookup), + )); err != nil { + return err + } else if len(existing) > 0 { + for _, listener := range existing { + // We have extended the event signature to add more information + // So we compare the start with is not guaranteed to be the same + // but it's the best comparison + if strings.HasPrefix(signature, listener.Signature) { + return i18n.NewError(ctx, coremsgs.MsgContractListenerExists) + } + + } + } + } + return nil }) if err != nil { return nil, err } - if err := cm.validateFFIEvent(ctx, &listener.Event.FFIEventDefinition); err != nil { + return &listener.ContractListener, nil +} + +func (cm *contractManager) AddContractListener(ctx context.Context, listener *core.ContractListenerInput) (output *core.ContractListener, err error) { + verifiedContractListener, err := cm.verifyContractListener(ctx, listener) + if err != nil { return nil, err } @@ -910,11 +1107,11 @@ func (cm *contractManager) AddContractListener(ctx context.Context, listener *co if listener.Name == "" { listener.Name = listener.BackendID } - if err = cm.database.InsertContractListener(ctx, &listener.ContractListener); err != nil { + if err = cm.database.InsertContractListener(ctx, verifiedContractListener); err != nil { return nil, err } - return &listener.ContractListener, err + return verifiedContractListener, err } func (cm *contractManager) AddContractAPIListener(ctx context.Context, apiName, eventPath string, listener *core.ContractListener) (output *core.ContractListener, err error) { @@ -935,6 +1132,29 @@ func (cm *contractManager) AddContractAPIListener(ctx context.Context, apiName, return cm.AddContractListener(ctx, input) } +func (cm *contractManager) MigrateToFiltersIfNeeded(ctx context.Context, listener *core.ContractListener) (bool, *core.ContractListener, error) { + migrated := false + if len(listener.Filters) == 0 && listener.Event != nil { + // Blockchain plugin has changed the signature + newSignature, err := cm.blockchain.GenerateEventSignature(ctx, &listener.Event.FFIEventDefinition) + if err != nil { + // This is safe to do because all listeners previously inserted + // verified that the event was valid before creating the signature + return false, nil, err + } + listener.Filters = append(listener.Filters, &core.ListenerFilter{ + Event: listener.Event, + Location: listener.Location, + Interface: listener.Interface, + Signature: newSignature, + }) + // Note not migrating the root signature as that would not allow rolling back + migrated = true + } + + return migrated, listener, nil +} + func (cm *contractManager) GetContractListenerByNameOrID(ctx context.Context, nameOrID string) (listener *core.ContractListener, err error) { id, err := fftypes.ParseUUID(ctx, nameOrID) if err != nil { @@ -950,6 +1170,7 @@ func (cm *contractManager) GetContractListenerByNameOrID(ctx context.Context, na if listener == nil { return nil, i18n.NewError(ctx, coremsgs.Msg404NotFound) } + return listener, nil } @@ -986,17 +1207,22 @@ func (cm *contractManager) GetContractAPIListeners(ctx context.Context, apiName, if err != nil { return nil, nil, err } - signature := cm.blockchain.GenerateEventSignature(ctx, &event.FFIEventDefinition) + signature, err := cm.blockchain.GenerateEventSignatureWithLocation(ctx, &event.FFIEventDefinition, api.Location) + if err != nil { + return nil, nil, err + } + + oldSignature, err := cm.blockchain.GenerateEventSignature(ctx, &event.FFIEventDefinition) + if err != nil { + return nil, nil, err + } fb := database.ContractListenerQueryFactory.NewFilter(ctx) f := fb.And( fb.Eq("interface", api.Interface.ID), - fb.Eq("signature", signature), + fb.Or(fb.Contains("signature", signature), fb.Eq("signature", oldSignature)), filter, ) - if !api.Location.IsNil() { - f = fb.And(f, fb.Eq("location", api.Location.Bytes())) - } return cm.database.GetContractListeners(ctx, cm.namespace, f) } diff --git a/internal/contracts/manager_test.go b/internal/contracts/manager_test.go index cefeaff56..d022d3157 100644 --- a/internal/contracts/manager_test.go +++ b/internal/contracts/manager_test.go @@ -774,7 +774,8 @@ func TestAddContractListenerInline(t *testing.T) { } mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, sub.Location).Return(sub.Location, nil) - mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed") + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", nil) + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, sub.Location).Return("0x123:changed", nil) mdi.On("GetContractListeners", context.Background(), "ns1", mock.Anything).Return(nil, nil, nil) mbi.On("AddContractListener", context.Background(), &sub.ContractListener, "").Return(nil) mdi.On("InsertContractListener", context.Background(), &sub.ContractListener).Return(nil) @@ -811,7 +812,8 @@ func TestAddContractListenerInlineNilLocation(t *testing.T) { }, } - mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed") + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", nil) + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, mock.Anything).Return("*:changed", nil) 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 @@ -851,7 +853,8 @@ func TestAddContractListenerNoLocationOK(t *testing.T) { }, } - mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed") + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", nil) + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, mock.Anything).Return("*:changed", nil) mdi.On("GetContractListeners", context.Background(), "ns1", mock.Anything).Return(nil, nil, nil) mbi.On("AddContractListener", context.Background(), &sub.ContractListener, "").Return(nil) mdi.On("InsertContractListener", context.Background(), &sub.ContractListener).Return(nil) @@ -900,7 +903,8 @@ func TestAddContractListenerByEventPath(t *testing.T) { } mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, sub.Location).Return(sub.Location, nil) - mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed") + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", nil) + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, mock.Anything).Return("0x123:changed", nil) mdi.On("GetContractListeners", context.Background(), "ns1", mock.Anything).Return(nil, nil, nil) mbi.On("AddContractListener", context.Background(), &sub.ContractListener, "").Return(nil) mdi.On("GetFFIByID", context.Background(), "ns1", interfaceID).Return(&fftypes.FFI{}, nil) @@ -910,7 +914,7 @@ func TestAddContractListenerByEventPath(t *testing.T) { result, err := cm.AddContractListener(context.Background(), sub) assert.NoError(t, err) assert.NotNil(t, result.ID) - assert.NotNil(t, result.Event) + assert.NotNil(t, result.Filters[0].Event) mbi.AssertExpectations(t) mdi.AssertExpectations(t) @@ -1232,6 +1236,157 @@ func TestAddContractListenerVerifyGetListFail(t *testing.T) { mdi.AssertExpectations(t) } +func TestAddContractListenerVerifyMigration(t *testing.T) { + cm := newTestContractManager() + + ctx := context.Background() + + mdi := cm.database.(*databasemocks.Plugin) + mdi.On("GetContractListeners", mock.Anything, "ns1", mock.MatchedBy(func(f ffapi.Filter) bool { + fi, _ := f.Finalize() + return fi.Skip == 0 && fi.Limit == 50 + })).Return([]*core.ContractListener{ + {Namespace: "ns1", ID: fftypes.NewUUID(), BackendID: "12345"}, + { + Namespace: "ns1", + ID: fftypes.NewUUID(), + BackendID: "23456", + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "changed", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "null"}`), + }, + }, + }, + }, + }, + }, nil, nil).Once() + mdi.On("GetContractListeners", mock.Anything, "ns1", mock.MatchedBy(func(f ffapi.Filter) bool { + fi, _ := f.Finalize() + return fi.Skip == 50 && fi.Limit == 50 + })).Return([]*core.ContractListener{}, nil, nil).Once() + mdi.On("UpsertContractListener", context.Background(), mock.Anything, true).Return(nil) + + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything, mock.Anything).Return("changed", nil) + mdi.On("GetBlockchainEvents", mock.Anything, "ns1", mock.Anything).Return([]*core.BlockchainEvent{}, nil, nil).Once() + mbi.On("GetContractListenerStatus", ctx, "ns1", "12345", true).Return(true, struct{}{}, core.ContractListenerStatusSynced, nil) + mbi.On("GetContractListenerStatus", ctx, "ns1", "23456", true).Return(false, nil, core.ContractListenerStatusUnknown, nil) + mbi.On("AddContractListener", ctx, mock.MatchedBy(func(l *core.ContractListener) bool { + prevBackendID := l.BackendID + l.BackendID = "34567" + return prevBackendID == "23456" && len(l.Filters) != 0 + }), "").Return(nil) + + mdi.On("UpdateContractListener", ctx, "ns1", mock.Anything, mock.MatchedBy(func(u ffapi.Update) bool { + uu, _ := u.Finalize() + return strings.Contains(uu.String(), "34567") + })).Return(nil).Once() + + err := cm.verifyListeners(ctx) + assert.NoError(t, err) + + mdi.AssertExpectations(t) + mbi.AssertExpectations(t) +} + +func TestVerifyListenersFailMigration(t *testing.T) { + cm := newTestContractManager() + + ctx := context.Background() + + mdi := cm.database.(*databasemocks.Plugin) + mdi.On("GetContractListeners", mock.Anything, "ns1", mock.MatchedBy(func(f ffapi.Filter) bool { + fi, _ := f.Finalize() + return fi.Skip == 0 && fi.Limit == 50 + })).Return([]*core.ContractListener{ + {Namespace: "ns1", ID: fftypes.NewUUID(), BackendID: "12345"}, + { + Namespace: "ns1", + ID: fftypes.NewUUID(), + BackendID: "23456", + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "changed", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "null"}`), + }, + }, + }, + }, + }, + }, nil, nil).Once() + mdi.On("UpsertContractListener", context.Background(), mock.Anything, true).Return(fmt.Errorf("pop")) + + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything, mock.Anything).Return("changed", nil) + mdi.On("GetBlockchainEvents", mock.Anything, "ns1", mock.Anything).Return([]*core.BlockchainEvent{}, nil, nil).Once() + mbi.On("GetContractListenerStatus", ctx, "ns1", "12345", true).Return(true, struct{}{}, core.ContractListenerStatusSynced, nil) + mbi.On("GetContractListenerStatus", ctx, "ns1", "23456", true).Return(false, nil, core.ContractListenerStatusUnknown, nil) + mbi.On("AddContractListener", ctx, mock.MatchedBy(func(l *core.ContractListener) bool { + prevBackendID := l.BackendID + l.BackendID = "34567" + return prevBackendID == "23456" && len(l.Filters) != 0 + }), "").Return(nil) + mdi.On("UpdateContractListener", ctx, "ns1", mock.Anything, mock.MatchedBy(func(u ffapi.Update) bool { + uu, _ := u.Finalize() + return strings.Contains(uu.String(), "34567") + })).Return(nil).Once() + + err := cm.verifyListeners(ctx) + assert.Error(t, err) + assert.Regexp(t, "pop", err.Error()) + + mdi.AssertExpectations(t) + mbi.AssertExpectations(t) +} + +func TestVerifyListenersFailUpgradeFilters(t *testing.T) { + cm := newTestContractManager() + + ctx := context.Background() + + mdi := cm.database.(*databasemocks.Plugin) + mdi.On("GetContractListeners", mock.Anything, "ns1", mock.MatchedBy(func(f ffapi.Filter) bool { + fi, _ := f.Finalize() + return fi.Skip == 0 && fi.Limit == 50 + })).Return([]*core.ContractListener{ + {Namespace: "ns1", ID: fftypes.NewUUID(), BackendID: "12345"}, + { + Namespace: "ns1", + ID: fftypes.NewUUID(), + BackendID: "23456", + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "changed", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "null"}`), + }, + }, + }, + }, + }, + }, nil, nil).Once() + + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything, mock.Anything).Return("changed", fmt.Errorf("pop")) + mbi.On("GetContractListenerStatus", ctx, "ns1", "12345", true).Return(true, struct{}{}, core.ContractListenerStatusSynced, nil) + + err := cm.verifyListeners(ctx) + assert.Error(t, err) + assert.Regexp(t, "pop", err.Error()) + + mdi.AssertExpectations(t) + mbi.AssertExpectations(t) +} + func TestAddContractListenerBadName(t *testing.T) { cm := newTestContractManager() sub := &core.ContractListenerInput{ @@ -1254,7 +1409,7 @@ func TestAddContractListenerMissingTopic(t *testing.T) { assert.Regexp(t, "FF00140.*'topic'", err) } -func TestAddContractListenerNameConflict(t *testing.T) { +func TestAddContractListenerNoInterface(t *testing.T) { cm := newTestContractManager() mbi := cm.blockchain.(*blockchainmocks.Plugin) mdi := cm.database.(*databasemocks.Plugin) @@ -1271,19 +1426,19 @@ func TestAddContractListenerNameConflict(t *testing.T) { } mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, sub.Location).Return(sub.Location, nil) - mdi.On("GetContractListener", context.Background(), "ns1", "sub1").Return(&core.ContractListener{}, nil) _, err := cm.AddContractListener(context.Background(), sub) - assert.Regexp(t, "FF10312", err) + assert.Regexp(t, "FF10317", err) mbi.AssertExpectations(t) mdi.AssertExpectations(t) } -func TestAddContractListenerNameError(t *testing.T) { +func TestAddContractListenerNameConflict(t *testing.T) { cm := newTestContractManager() mbi := cm.blockchain.(*blockchainmocks.Plugin) mdi := cm.database.(*databasemocks.Plugin) + interfaceID := fftypes.NewUUID() sub := &core.ContractListenerInput{ ContractListener: core.ContractListener{ @@ -1292,67 +1447,112 @@ func TestAddContractListenerNameError(t *testing.T) { "address": "0x123", }.String()), Topic: "test-topic", + Interface: &fftypes.FFIReference{ + ID: interfaceID, + }, }, EventPath: "changed", } + mdi.On("GetFFIByID", context.Background(), "ns1", interfaceID).Return(&fftypes.FFI{}, nil) + mdi.On("GetFFIEvent", context.Background(), "ns1", interfaceID, "changed").Return(&fftypes.FFIEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "changed", + }, + }, nil) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", nil) + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, mock.Anything).Return("0x123:changed", nil) mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, sub.Location).Return(sub.Location, nil) - mdi.On("GetContractListener", context.Background(), "ns1", "sub1").Return(nil, fmt.Errorf("pop")) + mdi.On("GetContractListener", context.Background(), "ns1", "sub1").Return(&core.ContractListener{}, nil) _, err := cm.AddContractListener(context.Background(), sub) - assert.EqualError(t, err, "pop") + assert.Regexp(t, "FF10312", err) mbi.AssertExpectations(t) mdi.AssertExpectations(t) } -func TestAddContractListenerTopicConflict(t *testing.T) { +func TestAddContractListenerNameError(t *testing.T) { cm := newTestContractManager() mbi := cm.blockchain.(*blockchainmocks.Plugin) mdi := cm.database.(*databasemocks.Plugin) + ffiID := fftypes.NewUUID() + sub := &core.ContractListenerInput{ ContractListener: core.ContractListener{ + Name: "sub1", Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ "address": "0x123", }.String()), - Event: &core.FFISerializedEvent{}, Topic: "test-topic", }, + Filters: core.ListenerFiltersInput{ + &core.ListenerFilterInput{ + ListenerFilter: core.ListenerFilter{ + Interface: &fftypes.FFIReference{ + ID: ffiID, + }, + }, + EventPath: "set", + }, + }, } + mdi.On("GetFFIByID", context.Background(), "ns1", ffiID).Return(&fftypes.FFI{}, nil) + mdi.On("GetFFIEvent", context.Background(), "ns1", ffiID, "set").Return(&fftypes.FFIEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "eventName", + }, + }, nil) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("eventSignature", nil) + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, mock.Anything).Return("0x123:eventSignature", nil) mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, sub.Location).Return(sub.Location, nil) - mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed") - mdi.On("GetContractListeners", context.Background(), "ns1", mock.Anything).Return([]*core.ContractListener{{}}, nil, nil) + mdi.On("GetContractListener", context.Background(), "ns1", "sub1").Return(nil, fmt.Errorf("pop")) _, err := cm.AddContractListener(context.Background(), sub) - assert.Regexp(t, "FF10383", err) + assert.EqualError(t, err, "pop") mbi.AssertExpectations(t) mdi.AssertExpectations(t) } -func TestAddContractListenerTopicError(t *testing.T) { +func TestAddContractListenerFiltersAndDeprecatedFail(t *testing.T) { cm := newTestContractManager() mbi := cm.blockchain.(*blockchainmocks.Plugin) mdi := cm.database.(*databasemocks.Plugin) sub := &core.ContractListenerInput{ + Filters: core.ListenerFiltersInput{ + { + ListenerFilter: core.ListenerFilter{ + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "0x123", + }.String()), + }, + }, + }, ContractListener: core.ContractListener{ Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ "address": "0x123", }.String()), - Event: &core.FFISerializedEvent{}, + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "changed", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "null"}`), + }, + }, + }, + }, Topic: "test-topic", }, } - mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, sub.Location).Return(sub.Location, nil) - mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed") - mdi.On("GetContractListeners", context.Background(), "ns1", mock.Anything).Return(nil, nil, fmt.Errorf("pop")) - _, err := cm.AddContractListener(context.Background(), sub) - assert.EqualError(t, err, "pop") + assert.Regexp(t, "FF10474", err) mbi.AssertExpectations(t) mdi.AssertExpectations(t) @@ -1384,8 +1584,6 @@ func TestAddContractListenerValidateFail(t *testing.T) { } mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, sub.Location).Return(sub.Location, nil) - mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed") - mdi.On("GetContractListeners", context.Background(), "ns1", mock.Anything).Return(nil, nil, nil) _, err := cm.AddContractListener(context.Background(), sub) assert.Regexp(t, "does not validate", err) @@ -1420,7 +1618,8 @@ func TestAddContractListenerBlockchainFail(t *testing.T) { } mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, sub.Location).Return(sub.Location, nil) - mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed") + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", nil) + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, mock.Anything).Return("0x123:changed", nil) mdi.On("GetContractListeners", context.Background(), "ns1", mock.Anything).Return(nil, nil, nil) mbi.On("AddContractListener", context.Background(), &sub.ContractListener, "").Return(fmt.Errorf("pop")) @@ -1457,7 +1656,8 @@ func TestAddContractListenerUpsertSubFail(t *testing.T) { } mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, sub.Location).Return(sub.Location, nil) - mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed") + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", nil) + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, mock.Anything).Return("0x123:changed", nil) mdi.On("GetContractListeners", context.Background(), "ns1", mock.Anything).Return(nil, nil, nil) mbi.On("AddContractListener", context.Background(), &sub.ContractListener, "").Return(nil) mdi.On("InsertContractListener", context.Background(), &sub.ContractListener).Return(fmt.Errorf("pop")) @@ -1496,13 +1696,14 @@ func TestAddContractAPIListener(t *testing.T) { mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, api.Location).Return(listener.Location, nil) mdi.On("GetFFIByID", context.Background(), "ns1", interfaceID).Return(&fftypes.FFI{}, nil) mdi.On("GetFFIEvent", context.Background(), "ns1", interfaceID, "changed").Return(event, nil) - mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed") + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", nil) + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, mock.Anything).Return("0x123:changed", nil) mdi.On("GetContractListeners", context.Background(), "ns1", mock.Anything).Return(nil, nil, nil) mbi.On("AddContractListener", context.Background(), mock.MatchedBy(func(l *core.ContractListener) bool { return *l.Interface.ID == *interfaceID && l.Topic == "test-topic" }), "").Return(nil) mdi.On("InsertContractListener", context.Background(), mock.MatchedBy(func(l *core.ContractListener) bool { - return *l.Interface.ID == *interfaceID && l.Event.Name == "changed" && l.Topic == "test-topic" + return *l.Filters[0].Interface.ID == *interfaceID && l.Filters[0].Event.Name == "changed" && l.Topic == "test-topic" })).Return(nil) _, err := cm.AddContractAPIListener(context.Background(), "simple", "changed", listener) @@ -1589,7 +1790,7 @@ func TestGetFFIWithChildren(t *testing.T) { }, nil, nil) mbi.On("GenerateEventSignature", mock.Anything, mock.MatchedBy(func(ev *fftypes.FFIEventDefinition) bool { return ev.Name == "event1" - })).Return("event1Sig") + })).Return("event1Sig", nil) mdb.On("GetFFIErrors", mock.Anything, "ns1", mock.Anything).Return([]*fftypes.FFIError{ {ID: fftypes.NewUUID(), FFIErrorDefinition: fftypes.FFIErrorDefinition{Name: "customError1"}}, }, nil, nil) @@ -1630,7 +1831,7 @@ func TestGetFFIByIDWithChildren(t *testing.T) { }, nil, nil) mbi.On("GenerateEventSignature", mock.Anything, mock.MatchedBy(func(ev *fftypes.FFIEventDefinition) bool { return ev.Name == "event1" - })).Return("event1Sig") + })).Return("event1Sig", nil) mdb.On("GetFFIErrors", mock.Anything, "ns1", mock.Anything).Return([]*fftypes.FFIError{ {ID: fftypes.NewUUID(), FFIErrorDefinition: fftypes.FFIErrorDefinition{Name: "customError1"}}, }, nil, nil) @@ -3011,6 +3212,18 @@ func TestGetContractListeners(t *testing.T) { assert.NoError(t, err) } +func TestGetContractListenersFail(t *testing.T) { + cm := newTestContractManager() + mdi := cm.database.(*databasemocks.Plugin) + + mdi.On("GetContractListeners", context.Background(), "ns1", mock.Anything).Return(nil, nil, fmt.Errorf("pop")) + + f := database.ContractListenerQueryFactory.NewFilter(context.Background()) + _, _, err := cm.GetContractListeners(context.Background(), f.And()) + assert.Error(t, err) + assert.Regexp(t, "pop", err.Error()) +} + func TestGetContractAPIListeners(t *testing.T) { cm := newTestContractManager() mbi := cm.blockchain.(*blockchainmocks.Plugin) @@ -3034,7 +3247,8 @@ func TestGetContractAPIListeners(t *testing.T) { mdi.On("GetContractAPIByName", context.Background(), "ns1", "simple").Return(api, nil) mdi.On("GetFFIByID", context.Background(), "ns1", interfaceID).Return(&fftypes.FFI{}, nil) mdi.On("GetFFIEvent", context.Background(), "ns1", interfaceID, "changed").Return(event, nil) - mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed") + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", nil) + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, mock.Anything).Return("0x123:changed", nil) mdi.On("GetContractListeners", context.Background(), "ns1", mock.Anything).Return(nil, nil, nil) f := database.ContractListenerQueryFactory.NewFilter(context.Background()) @@ -3045,34 +3259,44 @@ func TestGetContractAPIListeners(t *testing.T) { mdi.AssertExpectations(t) } -func TestGetContractAPIListenersNotFound(t *testing.T) { +func TestGetContractAPIListenersSignatureFail(t *testing.T) { cm := newTestContractManager() + mbi := cm.blockchain.(*blockchainmocks.Plugin) mdi := cm.database.(*databasemocks.Plugin) - mdi.On("GetContractAPIByName", context.Background(), "ns1", "simple").Return(nil, nil) - - f := database.ContractListenerQueryFactory.NewFilter(context.Background()) - _, _, err := cm.GetContractAPIListeners(context.Background(), "simple", "changed", f.And()) - assert.Regexp(t, "FF10109", err) - - mdi.AssertExpectations(t) -} - -func TestGetContractAPIListenersFail(t *testing.T) { - cm := newTestContractManager() - mdi := cm.database.(*databasemocks.Plugin) + interfaceID := fftypes.NewUUID() + api := &core.ContractAPI{ + Interface: &fftypes.FFIReference{ + ID: interfaceID, + }, + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "0x123", + }.String()), + } + event := &fftypes.FFIEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "changed", + }, + } - mdi.On("GetContractAPIByName", context.Background(), "ns1", "simple").Return(nil, fmt.Errorf("pop")) + mdi.On("GetContractAPIByName", context.Background(), "ns1", "simple").Return(api, nil) + mdi.On("GetFFIByID", context.Background(), "ns1", interfaceID).Return(&fftypes.FFI{}, nil) + mdi.On("GetFFIEvent", context.Background(), "ns1", interfaceID, "changed").Return(event, nil) + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, mock.Anything).Return("0x123:changed", fmt.Errorf("pop")) + mdi.On("GetContractListeners", context.Background(), "ns1", mock.Anything).Return(nil, nil, nil) f := database.ContractListenerQueryFactory.NewFilter(context.Background()) _, _, err := cm.GetContractAPIListeners(context.Background(), "simple", "changed", f.And()) - assert.EqualError(t, err, "pop") + assert.Error(t, err) + assert.Regexp(t, "pop", err.Error()) + mbi.AssertExpectations(t) mdi.AssertExpectations(t) } -func TestGetContractAPIListenersEventNotFound(t *testing.T) { +func TestGetContractAPIListenersOldSignatureFail(t *testing.T) { cm := newTestContractManager() + mbi := cm.blockchain.(*blockchainmocks.Plugin) mdi := cm.database.(*databasemocks.Plugin) interfaceID := fftypes.NewUUID() @@ -3084,32 +3308,93 @@ func TestGetContractAPIListenersEventNotFound(t *testing.T) { "address": "0x123", }.String()), } + event := &fftypes.FFIEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "changed", + }, + } mdi.On("GetContractAPIByName", context.Background(), "ns1", "simple").Return(api, nil) mdi.On("GetFFIByID", context.Background(), "ns1", interfaceID).Return(&fftypes.FFI{}, nil) - mdi.On("GetFFIEvent", context.Background(), "ns1", interfaceID, "changed").Return(nil, nil) + mdi.On("GetFFIEvent", context.Background(), "ns1", interfaceID, "changed").Return(event, nil) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", fmt.Errorf("pop")) + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, mock.Anything).Return("0x123:changed", nil) + mdi.On("GetContractListeners", context.Background(), "ns1", mock.Anything).Return(nil, nil, nil) f := database.ContractListenerQueryFactory.NewFilter(context.Background()) _, _, err := cm.GetContractAPIListeners(context.Background(), "simple", "changed", f.And()) - assert.Regexp(t, "FF10370", err) + assert.Error(t, err) + assert.Regexp(t, "pop", err.Error()) + mbi.AssertExpectations(t) mdi.AssertExpectations(t) } -func TestDeleteContractListener(t *testing.T) { +func TestGetContractAPIListenersNotFound(t *testing.T) { cm := newTestContractManager() - mbi := cm.blockchain.(*blockchainmocks.Plugin) mdi := cm.database.(*databasemocks.Plugin) - sub := &core.ContractListener{ - ID: fftypes.NewUUID(), - } - - mdi.On("GetContractListener", context.Background(), "ns1", "sub1").Return(sub, nil) - mbi.On("DeleteContractListener", context.Background(), sub, true).Return(nil) - mdi.On("DeleteContractListenerByID", context.Background(), "ns1", sub.ID).Return(nil) + mdi.On("GetContractAPIByName", context.Background(), "ns1", "simple").Return(nil, nil) - err := cm.DeleteContractListenerByNameOrID(context.Background(), "sub1") + f := database.ContractListenerQueryFactory.NewFilter(context.Background()) + _, _, err := cm.GetContractAPIListeners(context.Background(), "simple", "changed", f.And()) + assert.Regexp(t, "FF10109", err) + + mdi.AssertExpectations(t) +} + +func TestGetContractAPIListenersFail(t *testing.T) { + cm := newTestContractManager() + mdi := cm.database.(*databasemocks.Plugin) + + mdi.On("GetContractAPIByName", context.Background(), "ns1", "simple").Return(nil, fmt.Errorf("pop")) + + f := database.ContractListenerQueryFactory.NewFilter(context.Background()) + _, _, err := cm.GetContractAPIListeners(context.Background(), "simple", "changed", f.And()) + assert.EqualError(t, err, "pop") + + mdi.AssertExpectations(t) +} + +func TestGetContractAPIListenersEventNotFound(t *testing.T) { + cm := newTestContractManager() + mdi := cm.database.(*databasemocks.Plugin) + + interfaceID := fftypes.NewUUID() + api := &core.ContractAPI{ + Interface: &fftypes.FFIReference{ + ID: interfaceID, + }, + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "0x123", + }.String()), + } + + mdi.On("GetContractAPIByName", context.Background(), "ns1", "simple").Return(api, nil) + mdi.On("GetFFIByID", context.Background(), "ns1", interfaceID).Return(&fftypes.FFI{}, nil) + mdi.On("GetFFIEvent", context.Background(), "ns1", interfaceID, "changed").Return(nil, nil) + + f := database.ContractListenerQueryFactory.NewFilter(context.Background()) + _, _, err := cm.GetContractAPIListeners(context.Background(), "simple", "changed", f.And()) + assert.Regexp(t, "FF10370", err) + + mdi.AssertExpectations(t) +} + +func TestDeleteContractListener(t *testing.T) { + cm := newTestContractManager() + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mdi := cm.database.(*databasemocks.Plugin) + + sub := &core.ContractListener{ + ID: fftypes.NewUUID(), + } + + mdi.On("GetContractListener", context.Background(), "ns1", "sub1").Return(sub, nil) + mbi.On("DeleteContractListener", context.Background(), sub, true).Return(nil) + mdi.On("DeleteContractListenerByID", context.Background(), "ns1", sub.ID).Return(nil) + + err := cm.DeleteContractListenerByNameOrID(context.Background(), "sub1") assert.NoError(t, err) } @@ -3297,7 +3582,7 @@ func TestGetContractAPIInterface(t *testing.T) { }, nil, nil) mbi.On("GenerateEventSignature", mock.Anything, mock.MatchedBy(func(ev *fftypes.FFIEventDefinition) bool { return ev.Name == "event1" - })).Return("event1Sig") + })).Return("event1Sig", nil) mdb.On("GetFFIErrors", mock.Anything, "ns1", mock.Anything).Return([]*fftypes.FFIError{ {ID: fftypes.NewUUID(), FFIErrorDefinition: fftypes.FFIErrorDefinition{Name: "customError1"}}, }, nil, nil) @@ -3890,3 +4175,921 @@ func TestEnsureParamNamesIncludedInCacheKeys(t *testing.T) { assert.NotEqual(t, hex.EncodeToString(paramUniqueHash1.Sum(nil)), hex.EncodeToString(paramUniqueHash2.Sum(nil))) } + +func TestGenerateContractDeprecatedEventSignature(t *testing.T) { + cm := newTestContractManager() + + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", nil) + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, mock.Anything).Return("0x123:changed", nil) + + sub := &core.ContractListenerInput{ + ContractListener: core.ContractListener{ + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "0x123", + }.String()), + 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("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, sub.Location).Return(sub.Location, nil) + + output, err := cm.ConstructContractListenerSignature(context.Background(), sub) + assert.NoError(t, err) + assert.Equal(t, "0x123:changed", output.Signature) +} + +func TestGenerateContractFiltersCheckDuplicatesError(t *testing.T) { + cm := newTestContractManager() + event := &fftypes.FFIEvent{ + ID: fftypes.NewUUID(), + Namespace: "ns1", + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "firstEvent", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "integer"}`), + }, + }, + }, + } + + location := fftypes.JSONAnyPtr(`{"address":"0x1fa04bd8ca1b9ce9f19794faf790961134518434"}`) + + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("firstEvent", nil) + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, mock.Anything).Return("*:firstEvent", nil) + mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, location).Return(location, nil) + mdi := cm.database.(*databasemocks.Plugin) + mdi.On("GetFFIByID", context.Background(), "ns1", mock.Anything).Return(&fftypes.FFI{}, nil) + mdi.On("GetFFIEvent", context.Background(), "ns1", mock.Anything, "firstEvent").Return(event, nil) + + sub := &core.ContractListenerInput{ + Filters: []*core.ListenerFilterInput{ + { + ListenerFilter: core.ListenerFilter{ + Location: location, + Interface: &fftypes.FFIReference{ + ID: fftypes.NewUUID(), + }, + }, + EventPath: "firstEvent", + }, + { + ListenerFilter: core.ListenerFilter{ + Location: location, + Interface: &fftypes.FFIReference{ + ID: fftypes.NewUUID(), + }, + }, + EventPath: "firstEvent", + }, + }, + ContractListener: core.ContractListener{ + Options: &core.ContractListenerOptions{}, + Topic: "test-topic", + }, + } + + _, err := cm.ConstructContractListenerSignature(context.Background(), sub) + assert.Error(t, err) + assert.Regexp(t, "FF10477", err) +} + +func TestGenerateContractFiltersSignature(t *testing.T) { + cm := newTestContractManager() + event := &fftypes.FFIEvent{ + ID: fftypes.NewUUID(), + Namespace: "ns1", + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "firstEvent", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "integer"}`), + }, + }, + }, + } + + location1 := fftypes.JSONAnyPtr(`{"address":"0x1fa04bd8ca1b9ce9f19794faf790961134518434"}`) + location2 := fftypes.JSONAnyPtr(`{"address":"0x1aa04bd8ca1b9ce9f19794faf790961134518445"}`) + + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", nil) + mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, location1).Return(location1, nil).Once() + mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, location2).Return(location2, nil).Once() + mbi.On("CheckOverlappingLocations", context.Background(), location1, location2).Return(false, nil).Once() + mdi := cm.database.(*databasemocks.Plugin) + mdi.On("GetFFIByID", context.Background(), "ns1", mock.Anything).Return(&fftypes.FFI{}, nil) + mdi.On("GetFFIEvent", context.Background(), "ns1", mock.Anything, "firstEvent").Return(event, nil) + + sub := &core.ContractListenerInput{ + Filters: []*core.ListenerFilterInput{ + { + ListenerFilter: core.ListenerFilter{ + Location: location1, + Interface: &fftypes.FFIReference{ + ID: fftypes.NewUUID(), + }, + }, + EventPath: "firstEvent", + }, + { + ListenerFilter: core.ListenerFilter{ + Location: location2, + Interface: &fftypes.FFIReference{ + ID: fftypes.NewUUID(), + }, + }, + EventPath: "firstEvent", + }, + }, + ContractListener: core.ContractListener{ + Options: &core.ContractListenerOptions{}, + Topic: "test-topic", + }, + } + + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, location1).Return("0x1fa04bd8ca1b9ce9f19794faf790961134518434:firstEvent", nil).Once() + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, location2).Return("0x1aa04bd8ca1b9ce9f19794faf790961134518445:firstEvent", nil).Once() + output, err := cm.ConstructContractListenerSignature(context.Background(), sub) + assert.NoError(t, err) + assert.Equal(t, "0x1aa04bd8ca1b9ce9f19794faf790961134518445:firstEvent;0x1fa04bd8ca1b9ce9f19794faf790961134518434:firstEvent", output.Signature) +} + +func TestGenerateContractFiltersSignatureOverlappingError(t *testing.T) { + cm := newTestContractManager() + event := &fftypes.FFIEvent{ + ID: fftypes.NewUUID(), + Namespace: "ns1", + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "firstEvent", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "integer"}`), + }, + }, + }, + } + + location1 := fftypes.JSONAnyPtr(`{"address":"0x1fa04bd8ca1b9ce9f19794faf790961134518434"}`) + location2 := fftypes.JSONAnyPtr(`{"address":"0x1aa04bd8ca1b9ce9f19794faf790961134518445"}`) + + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", nil) + mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, location1).Return(location1, nil).Once() + mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, location2).Return(location2, nil).Once() + mbi.On("CheckOverlappingLocations", context.Background(), location1, location2).Return(false, fmt.Errorf("pop")).Once() + mdi := cm.database.(*databasemocks.Plugin) + mdi.On("GetFFIByID", context.Background(), "ns1", mock.Anything).Return(&fftypes.FFI{}, nil) + mdi.On("GetFFIEvent", context.Background(), "ns1", mock.Anything, "firstEvent").Return(event, nil) + + sub := &core.ContractListenerInput{ + Filters: []*core.ListenerFilterInput{ + { + ListenerFilter: core.ListenerFilter{ + Location: location1, + Interface: &fftypes.FFIReference{ + ID: fftypes.NewUUID(), + }, + }, + EventPath: "firstEvent", + }, + { + ListenerFilter: core.ListenerFilter{ + Location: location2, + Interface: &fftypes.FFIReference{ + ID: fftypes.NewUUID(), + }, + }, + EventPath: "firstEvent", + }, + }, + ContractListener: core.ContractListener{ + Options: &core.ContractListenerOptions{}, + Topic: "test-topic", + }, + } + + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, location1).Return("0x1fa04bd8ca1b9ce9f19794faf790961134518434:firstEvent", nil).Once() + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, location2).Return("0x1aa04bd8ca1b9ce9f19794faf790961134518445:firstEvent", nil).Once() + _, err := cm.ConstructContractListenerSignature(context.Background(), sub) + assert.Error(t, err) + assert.Regexp(t, "pop", err.Error()) +} + +func TestGenerateContractFiltersSignatureOverlapping(t *testing.T) { + cm := newTestContractManager() + event := &fftypes.FFIEvent{ + ID: fftypes.NewUUID(), + Namespace: "ns1", + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "firstEvent", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "integer"}`), + }, + }, + }, + } + + location1 := fftypes.JSONAnyPtr(`{"address":"0x1fa04bd8ca1b9ce9f19794faf790961134518434"}`) + location2 := fftypes.JSONAnyPtr(`{"address":"0x1aa04bd8ca1b9ce9f19794faf790961134518445"}`) + + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", nil) + mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, location1).Return(location1, nil).Once() + mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, location2).Return(location2, nil).Once() + mbi.On("CheckOverlappingLocations", context.Background(), location1, location2).Return(true, nil).Once() + mdi := cm.database.(*databasemocks.Plugin) + mdi.On("GetFFIByID", context.Background(), "ns1", mock.Anything).Return(&fftypes.FFI{}, nil) + mdi.On("GetFFIEvent", context.Background(), "ns1", mock.Anything, "firstEvent").Return(event, nil) + + sub := &core.ContractListenerInput{ + Filters: []*core.ListenerFilterInput{ + { + ListenerFilter: core.ListenerFilter{ + Location: location1, + Interface: &fftypes.FFIReference{ + ID: fftypes.NewUUID(), + }, + }, + EventPath: "firstEvent", + }, + { + ListenerFilter: core.ListenerFilter{ + Location: location2, + Interface: &fftypes.FFIReference{ + ID: fftypes.NewUUID(), + }, + }, + EventPath: "firstEvent", + }, + }, + ContractListener: core.ContractListener{ + Options: &core.ContractListenerOptions{}, + Topic: "test-topic", + }, + } + + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, location1).Return("0x1fa04bd8ca1b9ce9f19794faf790961134518434:firstEvent", nil).Once() + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, location2).Return("0x1aa04bd8ca1b9ce9f19794faf790961134518445:firstEvent", nil).Once() + _, err := cm.ConstructContractListenerSignature(context.Background(), sub) + assert.Error(t, err) + assert.Regexp(t, "FF10477", err.Error()) +} + +func TestGenerateContractFiltersSignatureSorted(t *testing.T) { + cm := newTestContractManager() + event := &fftypes.FFIEvent{ + ID: fftypes.NewUUID(), + Namespace: "ns1", + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "firstEvent", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "integer"}`), + }, + }, + }, + } + + event2 := &fftypes.FFIEvent{ + ID: fftypes.NewUUID(), + Namespace: "ns1", + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "secondEvent", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "integer"}`), + }, + }, + }, + } + + location1 := fftypes.JSONAnyPtr(`{"address":"0x1fa04bd8ca1b9ce9f19794faf790961134518434"}`) + location2 := fftypes.JSONAnyPtr(`{"address":"0x1aa04bd8ca1b9ce9f19794faf790961134518445"}`) + + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("firstEvent", nil).Once() + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("secondEvent", nil).Once() + mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, location1).Return(location1, nil).Once() + mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, location2).Return(location2, nil).Once() + mdi := cm.database.(*databasemocks.Plugin) + mdi.On("GetFFIByID", context.Background(), "ns1", mock.Anything).Return(&fftypes.FFI{}, nil) + mdi.On("GetFFIEvent", context.Background(), "ns1", mock.Anything, "firstEvent").Return(event, nil) + mdi.On("GetFFIEvent", context.Background(), "ns1", mock.Anything, "secondEvent").Return(event2, nil) + + sub := &core.ContractListenerInput{ + Filters: []*core.ListenerFilterInput{ + { + ListenerFilter: core.ListenerFilter{ + Location: location1, + Interface: &fftypes.FFIReference{ + ID: fftypes.NewUUID(), + }, + }, + EventPath: "firstEvent", + }, + { + ListenerFilter: core.ListenerFilter{ + Location: location2, + Interface: &fftypes.FFIReference{ + ID: fftypes.NewUUID(), + }, + }, + EventPath: "secondEvent", + }, + }, + ContractListener: core.ContractListener{ + Options: &core.ContractListenerOptions{}, + Topic: "test-topic", + }, + } + + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, location1).Return("0x1fa04bd8ca1b9ce9f19794faf790961134518434:firstEvent", nil).Once() + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, location2).Return("0x1aa04bd8ca1b9ce9f19794faf790961134518445:secondEvent", nil).Once() + output, err := cm.ConstructContractListenerSignature(context.Background(), sub) + assert.NoError(t, err) + assert.Equal(t, "0x1aa04bd8ca1b9ce9f19794faf790961134518445:secondEvent;0x1fa04bd8ca1b9ce9f19794faf790961134518434:firstEvent", output.Signature) +} + +func TestGenerateContractFiltersSignatureDuplicateFail(t *testing.T) { + cm := newTestContractManager() + event := &fftypes.FFIEvent{ + ID: fftypes.NewUUID(), + Namespace: "ns1", + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "firstEvent", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "integer"}`), + }, + }, + }, + } + + location1 := fftypes.JSONAnyPtr(`{"address":"0x1fa04bd8ca1b9ce9f19794faf790961134518434"}`) + location2 := fftypes.JSONAnyPtr(`{"address":"0x1fa04bd8ca1b9ce9f19794faf790961134518434"}`) + + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", nil) + mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, location1).Return(location1, nil).Once() + mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, location2).Return(location2, nil).Once() + mdi := cm.database.(*databasemocks.Plugin) + mdi.On("GetFFIByID", context.Background(), "ns1", mock.Anything).Return(&fftypes.FFI{}, nil) + mdi.On("GetFFIEvent", context.Background(), "ns1", mock.Anything, "firstEvent").Return(event, nil) + + sub := &core.ContractListenerInput{ + Filters: []*core.ListenerFilterInput{ + { + ListenerFilter: core.ListenerFilter{ + Location: location1, + Interface: &fftypes.FFIReference{ + ID: fftypes.NewUUID(), + }, + }, + EventPath: "firstEvent", + }, + { + ListenerFilter: core.ListenerFilter{ + Location: location2, + Interface: &fftypes.FFIReference{ + ID: fftypes.NewUUID(), + }, + }, + EventPath: "firstEvent", + }, + }, + ContractListener: core.ContractListener{ + Options: &core.ContractListenerOptions{}, + Topic: "test-topic", + }, + } + + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, mock.Anything).Return("0x1fa04bd8ca1b9ce9f19794faf790961134518434:firstEvent", nil) + _, err := cm.ConstructContractListenerSignature(context.Background(), sub) + assert.Error(t, err) + assert.Regexp(t, "FF10477", err.Error()) +} + +func TestGenerateContractFiltersSignatureDuplicateGenericFail(t *testing.T) { + cm := newTestContractManager() + event := &fftypes.FFIEvent{ + ID: fftypes.NewUUID(), + Namespace: "ns1", + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "firstEvent", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "integer"}`), + }, + }, + }, + } + + location2 := fftypes.JSONAnyPtr(`{"address":"0x1fa04bd8ca1b9ce9f19794faf790961134518434"}`) + + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", nil) + mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, location2).Return(location2, nil).Once() + mdi := cm.database.(*databasemocks.Plugin) + mdi.On("GetFFIByID", context.Background(), "ns1", mock.Anything).Return(&fftypes.FFI{}, nil) + mdi.On("GetFFIEvent", context.Background(), "ns1", mock.Anything, "firstEvent").Return(event, nil) + + sub := &core.ContractListenerInput{ + Filters: []*core.ListenerFilterInput{ + { + ListenerFilter: core.ListenerFilter{ + Interface: &fftypes.FFIReference{ + ID: fftypes.NewUUID(), + }, + }, + EventPath: "firstEvent", + }, + { + ListenerFilter: core.ListenerFilter{ + Location: location2, + Interface: &fftypes.FFIReference{ + ID: fftypes.NewUUID(), + }, + }, + EventPath: "firstEvent", + }, + }, + ContractListener: core.ContractListener{ + Options: &core.ContractListenerOptions{}, + Topic: "test-topic", + }, + } + + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, (*fftypes.JSONAny)(nil)).Return("*:firstEvent", nil).Once() + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, location2).Return("0x1fa04bd8ca1b9ce9f19794faf790961134518434:firstEvent", nil).Once() + _, err := cm.ConstructContractListenerSignature(context.Background(), sub) + assert.Error(t, err) + assert.Regexp(t, "FF10477", err.Error()) +} + +func TestGenerateContractFiltersSignatureNormalizeError(t *testing.T) { + cm := newTestContractManager() + event := &fftypes.FFIEvent{ + ID: fftypes.NewUUID(), + Namespace: "ns1", + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "firstEvent", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "integer"}`), + }, + }, + }, + } + location1 := fftypes.JSONAnyPtr(`{"channel":"my-channel"}`) + + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", nil) + mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, location1).Return(location1, fmt.Errorf("pop")).Once() + mdi := cm.database.(*databasemocks.Plugin) + mdi.On("GetFFIByID", context.Background(), "ns1", mock.Anything).Return(&fftypes.FFI{}, nil) + mdi.On("GetFFIEvent", context.Background(), "ns1", mock.Anything, "firstEvent").Return(event, nil) + + sub := &core.ContractListenerInput{ + Filters: []*core.ListenerFilterInput{ + { + ListenerFilter: core.ListenerFilter{ + Location: location1, + Interface: &fftypes.FFIReference{ + ID: fftypes.NewUUID(), + }, + }, + EventPath: "firstEvent", + }, + }, + ContractListener: core.ContractListener{ + Options: &core.ContractListenerOptions{}, + Topic: "test-topic", + }, + } + + _, err := cm.ConstructContractListenerSignature(context.Background(), sub) + assert.Error(t, err) + assert.Regexp(t, "pop", err.Error()) +} + +func TestMigrateFilters(t *testing.T) { + cm := newTestContractManager() + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything, mock.Anything).Return("changed", nil) + + listener := &core.ContractListener{ + Options: &core.ContractListenerOptions{}, + Topic: "test-topic", + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "changed", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "integer"}`), + }, + }, + }, + }, + } + + migrated, listener, err := cm.MigrateToFiltersIfNeeded(context.Background(), listener) + assert.NoError(t, err) + assert.True(t, migrated) + assert.NotEmpty(t, listener.Filters) +} + +func TestMigrateFiltersSignatureError(t *testing.T) { + cm := newTestContractManager() + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything, mock.Anything).Return("changed", fmt.Errorf("pop")) + + listener := &core.ContractListener{ + Options: &core.ContractListenerOptions{}, + Topic: "test-topic", + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "changed", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "integer"}`), + }, + }, + }, + }, + } + + _, listener, err := cm.MigrateToFiltersIfNeeded(context.Background(), listener) + assert.Error(t, err) + assert.Regexp(t, "pop", err.Error()) +} + +func TestAddContractListenerFail(t *testing.T) { + cm := newTestContractManager() + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mdi := cm.database.(*databasemocks.Plugin) + + sub := &core.ContractListenerInput{ + ContractListener: core.ContractListener{ + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "0x123", + }.String()), + 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("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, sub.Location).Return(sub.Location, nil) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", nil) + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, sub.Location).Return("0x123:changed", nil) + mdi.On("GetContractListeners", context.Background(), "ns1", mock.Anything).Return(nil, nil, fmt.Errorf("db error")) + + _, err := cm.AddContractListener(context.Background(), sub) + assert.Error(t, err) + assert.Regexp(t, "db error", err.Error()) + + mbi.AssertExpectations(t) + mdi.AssertExpectations(t) +} + +func TestAddContractListenerFailDuplicate(t *testing.T) { + cm := newTestContractManager() + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mdi := cm.database.(*databasemocks.Plugin) + + sub := &core.ContractListenerInput{ + ContractListener: core.ContractListener{ + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "0x123", + }.String()), + 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("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, sub.Location).Return(sub.Location, nil) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", nil) + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, sub.Location).Return("0x123:changed", nil) + mdi.On("GetContractListeners", context.Background(), "ns1", mock.Anything).Return([]*core.ContractListener{ + &sub.ContractListener, + }, nil, nil) + + _, err := cm.AddContractListener(context.Background(), sub) + assert.Error(t, err) + assert.Regexp(t, "FF10383", err.Error()) + + mbi.AssertExpectations(t) + mdi.AssertExpectations(t) +} + +func TestAddContractListenerFailDuplicateError(t *testing.T) { + cm := newTestContractManager() + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mdi := cm.database.(*databasemocks.Plugin) + + sub := &core.ContractListenerInput{ + ContractListener: core.ContractListener{ + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "0x123", + }.String()), + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "changed", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "integer"}`), + }, + }, + }, + }, + Options: &core.ContractListenerOptions{}, + Topic: "test-topic", + }, + } + + old := &core.ContractListenerInput{ + ContractListener: core.ContractListener{ + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "0x123", + }.String()), + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "changed", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "integer"}`), + }, + }, + }, + }, + Options: &core.ContractListenerOptions{}, + Topic: "test-topic", + Signature: "changed", + }, + } + + mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, sub.Location).Return(sub.Location, nil) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", nil) + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, sub.Location).Return("0x123:changed", nil) + mdi.On("GetContractListeners", context.Background(), "ns1", mock.Anything).Return([]*core.ContractListener{}, nil, nil).Once() + mdi.On("GetContractListeners", context.Background(), "ns1", mock.Anything).Return([]*core.ContractListener{ + &old.ContractListener, + }, nil, fmt.Errorf("pop")) + + _, err := cm.AddContractListener(context.Background(), sub) + assert.Error(t, err) + assert.Regexp(t, "pop", err.Error()) + + mbi.AssertExpectations(t) + mdi.AssertExpectations(t) +} + +func TestAddContractListenerFailDuplicatePrevious(t *testing.T) { + cm := newTestContractManager() + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mdi := cm.database.(*databasemocks.Plugin) + + sub := &core.ContractListenerInput{ + ContractListener: core.ContractListener{ + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "0x123", + }.String()), + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "changed", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "integer"}`), + }, + }, + }, + }, + Options: &core.ContractListenerOptions{}, + Topic: "test-topic", + }, + } + + old := &core.ContractListenerInput{ + ContractListener: core.ContractListener{ + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "0x123", + }.String()), + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "changed", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "integer"}`), + }, + }, + }, + }, + Options: &core.ContractListenerOptions{}, + Topic: "test-topic", + Signature: "changed", + }, + } + + mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, sub.Location).Return(sub.Location, nil) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", nil) + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, sub.Location).Return("0x123:changed", nil) + mdi.On("GetContractListeners", context.Background(), "ns1", mock.Anything).Return([]*core.ContractListener{}, nil, nil).Once() + mdi.On("GetContractListeners", context.Background(), "ns1", mock.Anything).Return([]*core.ContractListener{ + &old.ContractListener, + }, nil, nil) + + _, err := cm.AddContractListener(context.Background(), sub) + assert.Error(t, err) + assert.Regexp(t, "FF10383", err.Error()) + + mbi.AssertExpectations(t) + mdi.AssertExpectations(t) +} + +func TestParseContractListenerFiltersFailSignature(t *testing.T) { + cm := newTestContractManager() + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mdi := cm.database.(*databasemocks.Plugin) + + sub := &core.ContractListenerInput{ + ContractListener: core.ContractListener{ + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "0x123", + }.String()), + 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("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, sub.Location).Return(sub.Location, nil) + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, sub.Location).Return("0x123:changed", fmt.Errorf("pop")) + + err := cm.parseContractListenerFilters(context.Background(), sub) + assert.Error(t, err) + assert.Regexp(t, "pop", err.Error()) + + mbi.AssertExpectations(t) + mdi.AssertExpectations(t) +} + +func TestParseContractListenerFiltersFailEventSignature(t *testing.T) { + cm := newTestContractManager() + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mdi := cm.database.(*databasemocks.Plugin) + + sub := &core.ContractListenerInput{ + ContractListener: core.ContractListener{ + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "0x123", + }.String()), + 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("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, sub.Location).Return(sub.Location, nil) + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, sub.Location).Return("0x123:changed", nil) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", fmt.Errorf("pop")) + + err := cm.parseContractListenerFilters(context.Background(), sub) + assert.Error(t, err) + assert.Regexp(t, "pop", err.Error()) + + mbi.AssertExpectations(t) + mdi.AssertExpectations(t) +} + +func TestGetFFIEventsSignatureFail(t *testing.T) { + cm := newTestContractManager() + mdb := cm.database.(*databasemocks.Plugin) + mbi := cm.blockchain.(*blockchainmocks.Plugin) + + mdb.On("GetFFIEvents", mock.Anything, "ns1", mock.Anything).Return([]*fftypes.FFIEvent{ + {ID: fftypes.NewUUID(), FFIEventDefinition: fftypes.FFIEventDefinition{Name: "event1"}}, + }, nil, nil) + mbi.On("GenerateEventSignature", mock.Anything, mock.MatchedBy(func(ev *fftypes.FFIEventDefinition) bool { + return ev.Name == "event1" + })).Return("event1Sig", fmt.Errorf("pop")) + + _, err := cm.GetFFIEvents(context.Background(), fftypes.NewUUID()) + + assert.Error(t, err) + assert.Regexp(t, "pop", err.Error()) + + mdb.AssertExpectations(t) + mbi.AssertExpectations(t) +} + +func TestParseContractListenerFiltersFailEventSignatureDuplicateCheck(t *testing.T) { + cm := newTestContractManager() + mbi := cm.blockchain.(*blockchainmocks.Plugin) + mdi := cm.database.(*databasemocks.Plugin) + + sub := &core.ContractListenerInput{ + ContractListener: core.ContractListener{ + Location: fftypes.JSONAnyPtr(fftypes.JSONObject{ + "address": "0x123", + }.String()), + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "changed", + Params: fftypes.FFIParams{ + { + Name: "value", + Schema: fftypes.JSONAnyPtr(`{"type": "integer"}`), + }, + }, + }, + }, + Options: &core.ContractListenerOptions{}, + Topic: "test-topic", + }, + } + + mdi.On("GetContractListeners", context.Background(), "ns1", mock.Anything).Return(nil, nil, nil) + mbi.On("NormalizeContractLocation", context.Background(), blockchain.NormalizeListener, sub.Location).Return(sub.Location, nil) + mbi.On("GenerateEventSignatureWithLocation", context.Background(), mock.Anything, sub.Location).Return("0x123:changed", nil) + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", nil).Once() + mbi.On("GenerateEventSignature", context.Background(), mock.Anything).Return("changed", fmt.Errorf("pop")) + + _, err := cm.verifyContractListener(context.Background(), sub) + assert.Error(t, err) + assert.Regexp(t, "pop", err.Error()) + + mbi.AssertExpectations(t) + mdi.AssertExpectations(t) +} diff --git a/internal/coremsgs/en_api_translations.go b/internal/coremsgs/en_api_translations.go index e5d40514c..02f1a96d5 100644 --- a/internal/coremsgs/en_api_translations.go +++ b/internal/coremsgs/en_api_translations.go @@ -170,6 +170,7 @@ var ( APIEndpointsPostNewContractAPI = ffm("api.endpoints.postNewContractAPI", "Creates and broadcasts a new custom smart contract API") APIEndpointsPostNewContractInterface = ffm("api.endpoints.postNewContractInterface", "Creates and broadcasts a new custom smart contract interface") APIEndpointsPostNewContractListener = ffm("api.endpoints.postNewContractListener", "Creates a new blockchain listener for events emitted by custom smart contracts") + APIEndpointsPostContractListenerHash = ffm("api.endpoints.postContractListenerHash", "Calculates the hash of a blockchain listener filters and events") APIEndpointsPostNewDatatype = ffm("api.endpoints.postNewDatatype", "Creates and broadcasts a new datatype") APIEndpointsPostNewIdentity = ffm("api.endpoints.postNewIdentity", "Registers a new identity in the network") APIEndpointsPostNewMessageBroadcast = ffm("api.endpoints.postNewMessageBroadcast", "Broadcasts a message to all members in the network") diff --git a/internal/coremsgs/en_error_messages.go b/internal/coremsgs/en_error_messages.go index aeaea5b0d..8cccd5c34 100644 --- a/internal/coremsgs/en_error_messages.go +++ b/internal/coremsgs/en_error_messages.go @@ -27,289 +27,293 @@ var ffe = func(key, translation string, statusHint ...int) i18n.ErrorMessageKey //revive:disable var ( - MsgConfigFailed = ffe("FF10101", "Failed to read config") - MsgJSONDecodeFailed = ffe("FF10103", "Failed to decode input JSON") - MsgTLSConfigFailed = ffe("FF10105", "Failed to initialize TLS configuration") - MsgWebsocketClientError = ffe("FF10108", "Error received from WebSocket client: %s") - Msg404NotFound = ffe("FF10109", "Not found", 404) - MsgUnknownBlockchainPlugin = ffe("FF10110", "Unknown blockchain plugin: %s") - MsgEthConnectorRESTErr = ffe("FF10111", "Error from ethereum connector: %s") - MsgDBInitFailed = ffe("FF10112", "Database initialization failed") - MsgDBQueryBuildFailed = ffe("FF10113", "Database query builder failed") - MsgDBBeginFailed = ffe("FF10114", "Database begin transaction failed") - MsgDBQueryFailed = ffe("FF10115", "Database query failed") - MsgDBInsertFailed = ffe("FF10116", "Database insert failed") - MsgDBUpdateFailed = ffe("FF10117", "Database update failed") - MsgDBDeleteFailed = ffe("FF10118", "Database delete failed") - MsgDBCommitFailed = ffe("FF10119", "Database commit failed") - MsgDBMissingJoin = ffe("FF10120", "Database missing expected join entry in table '%s' for id '%s'") - MsgDBReadErr = ffe("FF10121", "Database resultset read error from table '%s'") - MsgUnknownDatabasePlugin = ffe("FF10122", "Unknown database plugin '%s'") - MsgNullDataReferenceID = ffe("FF10123", "Data id is null in message data reference %d") - MsgDupDataReferenceID = ffe("FF10124", "Duplicate data ID in message '%s'", 409) - MsgScanFailed = ffe("FF10125", "Failed to restore type '%T' into '%T'") - MsgUnregisteredBatchType = ffe("FF10126", "Unregistered batch type '%s'") - MsgBatchDispatchTimeout = ffe("FF10127", "Timed out dispatching work to batch") - MsgInitializationNilDepError = ffe("FF10128", "Initialization failed in %s due to unmet dependency") - MsgNilResponseNon204 = ffe("FF10129", "No output from API call") - MsgDataNotFound = ffe("FF10133", "Data not found for message %s", 400) - MsgUnknownSharedStoragePlugin = ffe("FF10134", "Unknown Shared Storage plugin '%s'") - MsgIPFSHashDecodeFailed = ffe("FF10135", "Failed to decode IPFS hash into 32byte value '%s'") - MsgIPFSRESTErr = ffe("FF10136", "Error from IPFS: %s") - MsgSerializationFailed = ffe("FF10137", "Serialization failed") - MsgMissingPluginConfig = ffe("FF10138", "Missing configuration '%s' for %s") - MsgMissingDataHashIndex = ffe("FF10139", "Missing data hash for index '%d' in message", 400) - MsgInvalidEthAddress = ffe("FF10141", "Supplied ethereum address is invalid", 400) - MsgInvalidTezosAddress = ffe("FF10142", "Supplied tezos address is invalid", 400) - Msg404NoResult = ffe("FF10143", "No result found", 404) - MsgUnsupportedSQLOpInFilter = ffe("FF10150", "No SQL mapping implemented for filter operator '%s'", 400) - MsgFilterSortDesc = ffe("FF10154", "Sort field. For multi-field sort use comma separated values (or multiple query values) with '-' prefix for descending") - MsgContextCanceled = ffe("FF00154", "Context cancelled") - MsgDBMigrationFailed = ffe("FF10163", "Database migration failed") - MsgHashMismatch = ffe("FF10164", "Hash mismatch") - MsgDefaultNamespaceNotFound = ffe("FF10166", "namespaces.default '%s' must be included in the namespaces.predefined configuration") - MsgEventTypesParseFail = ffe("FF10168", "Unable to parse list of event types", 400) - MsgUnknownEventType = ffe("FF10169", "Unknown event type '%s'", 400) - MsgIDMismatch = ffe("FF10170", "ID mismatch") - MsgRegexpCompileFailed = ffe("FF10171", "Unable to compile '%s' regexp '%s'") - MsgUnknownEventTransportPlugin = ffe("FF10172", "Unknown event transport plugin: %s") - MsgWSConnectionNotActive = ffe("FF10173", "Websocket connection '%s' no longer active") - MsgWSSubAlreadyInFlight = ffe("FF10174", "Websocket subscription '%s' already has a message in flight") - MsgWSMsgSubNotMatched = ffe("FF10175", "Acknowledgment does not match an inflight event + subscription") - MsgWSClientSentInvalidData = ffe("FF10176", "Invalid data") - MsgWSClientUnknownAction = ffe("FF10177", "Unknown action '%s'") - MsgWSInvalidStartAction = ffe("FF10178", "A start action must set namespace and either a name or ephemeral=true") - MsgWSAutoAckChanged = ffe("FF10179", "The autoack option must be set consistently on all start requests") - MsgWSAutoAckEnabled = ffe("FF10180", "The autoack option is enabled on this connection") - MsgConnSubscriptionNotStarted = ffe("FF10181", "Subscription %v is not started on connection") - MsgDispatcherClosing = ffe("FF10182", "Event dispatcher closing") - MsgMaxFilterSkip = ffe("FF10183", "You have reached the maximum pagination limit for this query (%d)", 400) - MsgMaxFilterLimit = ffe("FF10184", "Your query exceeds the maximum filter limit (%d)", 400) - MsgAPIServerStaticFail = ffe("FF10185", "An error occurred loading static content", 500) - MsgEventListenerClosing = ffe("FF10186", "Event listener closing") - MsgNamespaceDoesNotExist = ffe("FF10187", "Namespace does not exist", 404) - MsgInvalidSubscription = ffe("FF10189", "Invalid subscription", 400) - MsgMismatchedTransport = ffe("FF10190", "Connection ID '%s' appears not to be unique between transport '%s' and '%s'", 400) - MsgInvalidFirstEvent = ffe("FF10191", "Invalid firstEvent definition - must be 'newest','oldest' or a sequence number", 400) - MsgNumberMustBeGreaterEqual = ffe("FF10192", "Number must be greater than or equal to %d", 400) - MsgAlreadyExists = ffe("FF10193", "A %s with name '%s:%s' already exists", 409) - MsgJSONValidatorBadRef = ffe("FF10194", "Cannot use JSON validator for data with type '%s' and validator reference '%v'", 400) - MsgDatatypeNotFound = ffe("FF10195", "Datatype '%v' not found", 400) - MsgSchemaLoadFailed = ffe("FF10196", "Datatype '%s' schema invalid", 400) - MsgDataCannotBeValidated = ffe("FF10197", "Data cannot be validated", 400) - MsgJSONDataInvalidPerSchema = ffe("FF10198", "Data does not conform to the JSON schema of datatype '%s': %s", 400) - MsgDataValueIsNull = ffe("FF10199", "Data value is null", 400) - MsgDataInvalidHash = ffe("FF10201", "Invalid data: hashes do not match Hash=%s Expected=%s", 400) - MsgDataReferenceUnresolvable = ffe("FF10204", "Data reference %d cannot be resolved", 400) - MsgDataMissing = ffe("FF10205", "Data entry %d has neither 'id' to refer to existing data, or 'value' to include in-line JSON data", 400) - MsgAuthorInvalid = ffe("FF10206", "Invalid author specified", 400) - MsgMessageNotFound = ffe("FF10207", "Message '%s' not found", 404) - MsgBatchNotFound = ffe("FF10209", "Batch '%s' not found for message", 404) - MsgMessageTXNotSet = ffe("FF10210", "Message '%s' does not have an assigned transaction", 404) - MsgOwnerMissing = ffe("FF10211", "Owner missing", 400) - MsgUnknownIdentityPlugin = ffe("FF10212", "Unknown Identity plugin '%s'") - MsgUnknownDataExchangePlugin = ffe("FF10213", "Unknown Data Exchange plugin '%s'") - MsgParentIdentityNotFound = ffe("FF10214", "Identity '%s' not found in identity chain for %s '%s'") - MsgInvalidSigningIdentity = ffe("FF10215", "Invalid signing identity") - MsgNodeAndOrgIDMustBeSet = ffe("FF10216", "node.name, org.name and org.key must be configured first", 409) - MsgBlobStreamingFailed = ffe("FF10217", "Blob streaming terminated with error", 500) - MsgNodeNotFound = ffe("FF10224", "Node with name or identity '%s' not found", 400) - MsgLocalNodeNotSet = ffe("FF10225", "Unable to resolve the local node. Please ensure node.name is configured", 500) - MsgGroupNotFound = ffe("FF10226", "Group '%s' not found", 404) - MsgDXRESTErr = ffe("FF10229", "Error from data exchange: %s") - MsgInvalidHex = ffe("FF10231", "Invalid hex supplied", 400) - MsgInvalidWrongLenB32 = ffe("FF00107", "Byte length must be 32 (64 hex characters)", 400) - MsgNodeNotFoundInOrg = ffe("FF10233", "Unable to find any nodes owned by org '%s', or parent orgs", 400) - MsgDXBadResponse = ffe("FF10237", "Unexpected '%s' in data exchange response: %s") - MsgDXBadHash = ffe("FF10238", "Unexpected hash returned from data exchange upload. Hash=%s Expected=%s") - MsgBlobNotFound = ffe("FF10239", "No blob has been uploaded or confirmed received, with hash=%s", 404) - MsgDownloadBlobFailed = ffe("FF10240", "Error download blob with reference '%s' from local data exchange") - MsgDataDoesNotHaveBlob = ffe("FF10241", "Data does not have a blob attachment", 404) - MsgWebhookURLEmpty = ffe("FF10242", "Webhook subscription option 'url' cannot be empty", 400) - MsgWebhookInvalidStringMap = ffe("FF10243", "Webhook subscription option '%s' must be map of string values. %s=%T", 400) - MsgWebsocketsNoData = ffe("FF10244", "Websockets subscriptions do not support streaming the full data payload, just the references (withData must be false)", 400) - MsgWebhooksWithData = ffe("FF10245", "Webhook subscriptions require the full data payload (withData must be true)", 400) - MsgWebhooksReplyBadJSON = ffe("FF10257", "Failed to process reply from webhook as JSON") - MsgRequestTimeout = ffe("FF10260", "The request with id '%s' timed out after %.2fms", 408) - MsgRequestReplyTagRequired = ffe("FF10261", "For request messages 'header.tag' must be set on the request message to route it to a suitable responder", 400) - MsgRequestCannotHaveCID = ffe("FF10262", "For request messages 'header.cid' must be unset", 400) - MsgSystemTransportInternal = ffe("FF10266", "You cannot create subscriptions on the system events transport") - MsgFilterCountNotSupported = ffe("FF10267", "This query does not support generating a count of all results") - MsgRejected = ffe("FF10269", "Message with ID '%s' was rejected. Please check the FireFly logs for more information") - MsgRequestMustBePrivate = ffe("FF10271", "For request messages you must specify a group of private recipients", 400) - MsgUnknownTokensPlugin = ffe("FF10272", "Unknown tokens plugin '%s'", 400) - MsgMissingTokensPluginConfig = ffe("FF10273", "Invalid tokens configuration - name and plugin are required", 400) - MsgTokensRESTErr = ffe("FF10274", "Error from tokens service: %s") - MsgTokenPoolDuplicate = ffe("FF10275", "Duplicate token pool: %s", 409) - MsgTokenPoolRejected = ffe("FF10276", "Token pool with ID '%s' was rejected. Please check the FireFly logs for more information") - MsgIdentityNotFoundByString = ffe("FF10277", "Identity could not be resolved via lookup string '%s'") - MsgAuthorOrgSigningKeyMismatch = ffe("FF10279", "Author organization '%s' is not associated with signing key '%s'") - MsgCannotTransferToSelf = ffe("FF10280", "From and to addresses must be different", 400) - MsgLocalOrgNotSet = ffe("FF10281", "Unable to resolve the local root org. Please ensure org.name is configured", 500) - MsgTezosconnectRESTErr = ffe("FF10283", "Error from tezos connector: %s") - MsgFabconnectRESTErr = ffe("FF10284", "Error from fabconnect: %s") - MsgInvalidIdentity = ffe("FF10285", "Supplied Fabric signer identity is invalid", 400) - MsgFailedToDecodeCertificate = ffe("FF10286", "Failed to decode certificate: %s", 500) - MsgInvalidMessageType = ffe("FF10287", "Invalid message type - allowed types are %s", 400) - MsgWSClosed = ffe("FF10290", "Websocket closed") - MsgFieldNotSpecified = ffe("FF10292", "Field '%s' must be specified", 400) - MsgTokenPoolNotActive = ffe("FF10293", "Token pool is not yet activated") - MsgHistogramCollectionParam = ffe("FF10297", "Collection to fetch") - MsgInvalidNumberOfIntervals = ffe("FF10298", "Number of time intervals must be between %d and %d", 400) - MsgInvalidChartNumberParam = ffe("FF10299", "Invalid %s. Must be a number.", 400) - MsgHistogramInvalidTimes = ffe("FF10300", "Start time must be before end time", 400) - MsgUnsupportedCollection = ffe("FF10301", "%s collection is not supported", 400) - MsgContractInterfaceExists = ffe("FF10302", "A contract interface already exists in the namespace: '%s' with name: '%s' and version: '%s'", 409) - MsgContractInterfaceNotFound = ffe("FF10303", "Contract interface %s not found", 404) - MsgContractMissingInputArgument = ffe("FF10304", "Missing required input argument '%s'", 400) - MsgContractWrongInputType = ffe("FF10305", "Input '%v' is of type '%v' not expected type of '%v'", 400) - MsgContractMissingInputField = ffe("FF10306", "Expected object of type '%v' to contain field named '%v' but it was missing", 400) - MsgContractMapInputType = ffe("FF10307", "Unable to map input type '%v' to known FireFly type - was expecting '%v'", 400) - MsgContractByteDecode = ffe("FF10308", "Unable to decode field '%v' as bytes", 400) - MsgContractInternalType = ffe("FF10309", "Input '%v' of type '%v' is not compatible blockchain internalType of '%v'", 400) - MsgContractLocationInvalid = ffe("FF10310", "Failed to validate contract location: %v", 400) - MsgContractParamInvalid = ffe("FF10311", "Failed to validate contract param: %v", 400) - MsgContractListenerNameExists = ffe("FF10312", "A contract listener already exists in the namespace: '%s' with name: '%s'", 409) - MsgContractMethodNotSet = ffe("FF10313", "Either an interface reference and method path, or in-line method definition, must be supplied on invoke contract request", 400) - MsgContractMethodResolveError = ffe("FF10315", "Unable to resolve contract method: %s", 400) - MsgContractLocationExists = ffe("FF10316", "The contract location cannot be changed after it is created", 400) - MsgListenerNoEvent = ffe("FF10317", "Either an interface reference and event path, or in-line event definition must be supplied when creating a contract listener", 400) - MsgListenerEventNotFound = ffe("FF10318", "No event was found in namespace '%s' with id '%s'", 400) - MsgEventNameMustBeSet = ffe("FF10319", "Event name must be set", 400) - MsgMethodNameMustBeSet = ffe("FF10320", "Method name must be set", 400) - MsgContractEventResolveError = ffe("FF10321", "Unable to resolve contract event", 400) - MsgQueryOpUnsupportedMod = ffe("FF10322", "Operation '%s' on '%s' does not support modifiers", 400) - MsgDXBadSize = ffe("FF10323", "Unexpected size returned from data exchange upload. Size=%d Expected=%d") - MsgTooLargeBroadcast = ffe("FF10327", "Message size %.2fkb is too large for the max broadcast batch size of %.2fkb", 400) - MsgTooLargePrivate = ffe("FF10328", "Message size %.2fkb is too large for the max private message size of %.2fkb", 400) - MsgManifestMismatch = ffe("FF10329", "Manifest mismatch overriding '%s' status as failure: '%s'", 400) - MsgFFIValidationFail = ffe("FF10331", "Field '%s' does not validate against the provided schema", 400) - MsgFFISchemaParseFail = ffe("FF10332", "Failed to parse schema for param '%s'", 400) - MsgFFISchemaCompileFail = ffe("FF10333", "Failed compile schema for param '%s'", 400) - MsgPluginInitializationFailed = ffe("FF10334", "Plugin initialization error", 500) - MsgUnknownTransactionType = ffe("FF10336", "Unknown transaction type '%s'", 400) - MsgGoTemplateCompileFailed = ffe("FF10337", "Go template compilation for '%s' failed: %s", 500) - MsgGoTemplateExecuteFailed = ffe("FF10338", "Go template execution for '%s' failed: %s", 500) - MsgAddressResolveFailed = ffe("FF10339", "Failed to resolve signing key string '%s': %s", 500) - MsgAddressResolveBadStatus = ffe("FF10340", "Failed to resolve signing key string '%s' [%d]: %s", 500) - MsgAddressResolveBadResData = ffe("FF10341", "Failed to resolve signing key string '%s' - invalid address returned '%s': %s", 500) - MsgDXNotInitialized = ffe("FF10342", "Data exchange is initializing") - MsgDBLockFailed = ffe("FF10345", "Database lock failed") - MsgFFIGenerationFailed = ffe("FF10346", "Error generating smart contract interface: %s", 400) - MsgFFIGenerationUnsupported = ffe("FF10347", "Smart contract interface generation is not supported by this blockchain plugin", 400) - MsgBlobHashMismatch = ffe("FF10348", "Blob hash mismatch sent=%s received=%s", 400) - MsgDIDResolverUnknown = ffe("FF10349", "DID resolver unknown for DID: %s", 400) - MsgIdentityNotOrg = ffe("FF10350", "Identity '%s' with DID '%s' is not an organization", 400) - MsgIdentityNotNode = ffe("FF10351", "Identity '%s' with DID '%s' is not a node", 400) - MsgBlockchainKeyNotSet = ffe("FF10352", "No blockchain key specified", 400) - MsgNoVerifierForIdentity = ffe("FF10353", "No %s verifier registered for identity %s", 400) - MsgNodeMissingBlockchainKey = ffe("FF10354", "No signing key was specified, and no default signing key or organization signing key is configured for this namespace", 400) - MsgAuthorRegistrationMismatch = ffe("FF10355", "Verifier '%s' cannot be used for signing with author '%s'. Verifier registered to '%s'", 400) - MsgAuthorMissingForKey = ffe("FF10356", "Key '%s' has not been registered by any identity, and a separate 'author' was not supplied", 404) - MsgAuthorIncorrectForRootReg = ffe("FF10357", "Author namespace '%s' and DID '%s' combination invalid for root organization registration", 400) - MsgKeyIdentityMissing = ffe("FF10358", "Identity owner of key '%s' not found", 500) - MsgIdentityChainLoop = ffe("FF10364", "Loop detected on identity %s in chain for %s (%s)", 400) - MsgInvalidIdentityParentType = ffe("FF10365", "Parent %s (%s) of type %s is invalid for child %s (%s) of type", 400) - MsgParentIdentityMissingClaim = ffe("FF10366", "Parent %s (%s) is invalid (missing claim)", 400) - MsgDXInfoMissingID = ffe("FF10367", "Data exchange endpoint info missing 'id' field", 500) - MsgEventNotFound = ffe("FF10370", "Event with name '%s' not found", 400) - MsgOperationNotSupported = ffe("FF10371", "Operation not supported: %s", 400) - MsgFailedToRetrieve = ffe("FF10372", "Failed to retrieve %s %s", 500) - MsgBlobMissingPublic = ffe("FF10373", "Blob for data %s missing public payload reference while flushing batch", 500) - MsgDBMultiRowConfigError = ffe("FF10374", "Database invalid configuration - using multi-row insert on DB plugin that does not support query syntax for input") - MsgDBNoSequence = ffe("FF10375", "Failed to retrieve sequence for insert row %d (could mean duplicate insert)", 500) - MsgDownloadSharedFailed = ffe("FF10376", "Error downloading data with reference '%s' from shared storage") - MsgDownloadBatchMaxBytes = ffe("FF10377", "Error downloading batch with reference '%s' from shared storage - maximum size limit reached") - MsgOperationDataIncorrect = ffe("FF10378", "Operation data type incorrect: %T", 400) - MsgDataMissingBlobHash = ffe("FF10379", "Blob for data %s cannot be transferred as it is missing a hash", 500) - MsgUnexpectedDXMessageType = ffe("FF10380", "Unexpected websocket event type from DX plugin: %s", 500) - MsgContractListenerExists = ffe("FF10383", "A contract listener already exists for this combination of topic + location + event", 409) - MsgInvalidOutputOption = ffe("FF10385", "invalid output option '%s'") - MsgInvalidPluginConfiguration = ffe("FF10386", "Invalid %s plugin configuration - name and type are required") - MsgReferenceMarkdownMissing = ffe("FF10387", "Reference markdown file missing: '%s'") - MsgFFSystemReservedName = ffe("FF10388", "Invalid namespace configuration - %s is a reserved name") - MsgInvalidNamespaceMode = ffe("FF10389", "Invalid %s namespace configuration - unknown mode") - MsgNamespaceUnknownPlugin = ffe("FF10390", "Invalid %s namespace configuration - unknown plugin %s") - MsgNamespaceWrongPluginsMultiparty = ffe("FF10391", "Invalid %s namespace configuration - multiparty mode requires database, blockchain, shared storage, and data exchange plugins") - MsgNamespaceNoDatabase = ffe("FF10392", "Invalid %s namespace configuration - a database plugin is required") - MsgNamespaceMultiplePluginType = ffe("FF10394", "Invalid %s namespace configuration - multiple %s plugins provided") - MsgDuplicatePluginName = ffe("FF10395", "Invalid plugin configuration - plugin with name %s already exists", 409) - MsgInvalidFireFlyContractIndex = ffe("FF10396", "No configuration found for FireFly contract at %s") - MsgUnrecognizedNetworkAction = ffe("FF10397", "Unrecognized network action: %s", 400) - MsgOverrideExistingFieldCustomOption = ffe("FF10398", "Cannot override existing field with custom option named '%s'", 400) - MsgTerminateNotSupported = ffe("FF10399", "The 'terminate' operation to mark a switchover of smart contracts is not supported on namespace %s", 400) - MsgDefRejectedBadPayload = ffe("FF10400", "Rejected %s message '%s' - invalid payload") - MsgDefRejectedAuthorBlank = ffe("FF10401", "Rejected %s message '%s' - author is blank") - MsgDefRejectedSignatureMismatch = ffe("FF10402", "Rejected %s message '%s' - signature mismatch") - MsgDefRejectedValidateFail = ffe("FF10403", "Rejected %s '%s' - validate failed") - MsgDefRejectedIDMismatch = ffe("FF10404", "Rejected %s '%s' - ID mismatch with existing record") - MsgDefRejectedLocationMismatch = ffe("FF10405", "Rejected %s '%s' - location mismatch with existing record") - MsgDefRejectedSchemaFail = ffe("FF10406", "Rejected %s '%s' - schema check: %s") - MsgDefRejectedConflict = ffe("FF10407", "Rejected %s '%s' - conflicts with existing: %s", 409) - MsgDefRejectedIdentityNotFound = ffe("FF10408", "Rejected %s '%s' - identity not found: %s") - MsgDefRejectedWrongAuthor = ffe("FF10409", "Rejected %s '%s' - wrong author: %s") - MsgDefRejectedHashMismatch = ffe("FF10410", "Rejected %s '%s' - hash mismatch: %s != %s") - MsgInvalidNamespaceUUID = ffe("FF10411", "Expected 'namespace:' prefix on ID '%s'", 400) - MsgBadNetworkVersion = ffe("FF10412", "Bad network version: %s") - MsgDefinitionRejected = ffe("FF10413", "Definition rejected") - MsgActionNotSupported = ffe("FF10414", "This action is not supported in this namespace", 400) - MsgMessagesNotSupported = ffe("FF10415", "Messages are not supported in this namespace", 400) - MsgInvalidSubscriptionForNetwork = ffe("FF10416", "Subscription name '%s' is invalid according to multiparty network rules in effect (network version=%d)") - MsgBlockchainNotConfigured = ffe("FF10417", "No blockchain plugin configured") - MsgInvalidBatchPinEvent = ffe("FF10418", "BatchPin event is not valid - %s (%s): %s") - MsgDuplicatePluginBroadcastName = ffe("FF10419", "Invalid %s plugin broadcast name: %s - broadcast names must be unique", 409) - MsgInvalidConnectorName = ffe("FF10420", "Could not find name %s for %s connector") - MsgCannotInitLegacyNS = ffe("FF10421", "could not initialize legacy '%s' namespace - found conflicting V1 multi-party config in %s") - MsgInvalidGroupMember = ffe("FF10422", "invalid group member - node '%s' is not owned by '%s' or any of its ancestors") - MsgContractListenerStatusInvalid = ffe("FF10423", "Failed to validate contract listener status: %v", 400) - MsgCacheMissSizeLimitKeyInternal = ffe("FF10424", "could not initialize cache - size limit config key is not provided") - MsgCacheMissTTLKeyInternal = ffe("FF10425", "could not initialize cache - ttl config key is not provided") - MsgCacheConfigKeyMismatchInternal = ffe("FF10426", "could not initialize cache - '%s' and '%s' do not have identical prefix, mismatching prefixes are: '%s','%s'") - MsgCacheUnexpectedSizeKeyNameInternal = ffe("FF10427", "could not initialize cache - '%s' is not an expected size configuration key suffix. Expected values are: 'size', 'limit'") - MsgUnknownVerifierType = ffe("FF10428", "Unknown verifier type", 400) - MsgNotSupportedByBlockchainPlugin = ffe("FF10429", "Not supported by blockchain plugin", 400) - MsgIdempotencyKeyDuplicateMessage = ffe("FF10430", "Idempotency key '%s' already used for message '%s'", 409) - MsgIdempotencyKeyDuplicateTransaction = ffe("FF10431", "Idempotency key '%s' already used for transaction '%s'", 409) - MsgNonIdempotencyKeyConflictTxInsert = ffe("FF10432", "Conflict on insert of transaction '%s'. No existing transaction matching idempotency key '%s' found", 409) - MsgErrorNameMustBeSet = ffe("FF10433", "The name of the error must be set", 400) - MsgContractErrorsResolveError = ffe("FF10434", "Unable to resolve contract errors: %s", 400) - MsgUnknownInterfaceFormat = ffe("FF10435", "Unknown interface format: %s", 400) - MsgUnknownNamespace = ffe("FF10436", "Unknown namespace '%s'", 404) - MsgMissingNamespace = ffe("FF10437", "Missing namespace in request", 400) - MsgDeprecatedResetWithAutoReload = ffe("FF10438", "The deprecated reset API cannot be used when dynamic config reload is enabled", 409) - MsgConfigArrayVsRawConfigMismatch = ffe("FF10439", "Error processing configuration - mismatch between raw and processed array lengths") - MsgDefaultChannelNotConfigured = ffe("FF10440", "No default channel configured for this namespace", 400) - MsgNamespaceInitializing = ffe("FF10441", "Namespace '%s' is initializing", 412) - MsgPinsNotAssigned = ffe("FF10442", "Message cannot be sent because pins have not been assigned") - MsgMethodDoesNotSupportPinning = ffe("FF10443", "This method does not support passing a payload for pinning") - MsgOperationNotFoundInTransaction = ffe("FF10444", "No operation of type %s was found in transaction '%s'") - MsgCannotSetParameterWithMessage = ffe("FF10445", "Cannot provide a value for '%s' when pinning a message", 400) - MsgNamespaceNotStarted = ffe("FF10446", "Namespace '%s' is not started", 412) - MsgNameExists = ffe("FF10447", "Name already exists", 409) - MsgNetworkNameExists = ffe("FF10448", "Network name already exists", 409) - MsgCannotDeletePublished = ffe("FF10449", "Cannot delete an item that has been published", 409) - MsgAlreadyPublished = ffe("FF10450", "Item has already been published", 409) - MsgContractInterfaceNotPublished = ffe("FF10451", "Contract interface '%s' has not been published", 409) - MsgInvalidMessageSigner = ffe("FF10452", "Invalid message '%s'. Key '%s' does not match the signer of the pin: %s") - MsgInvalidMessageIdentity = ffe("FF10453", "Invalid message '%s'. Author '%s' does not match identity registered to %s: %s (%s)") - MsgDuplicateTLSConfig = ffe("FF10454", "Found duplicate TLS Config '%s'", 400) - MsgNotFoundTLSConfig = ffe("FF10455", "Provided TLS Config name '%s' not found for namespace '%s'", 400) - MsgSQLInsertManyOutsideTransaction = ffe("FF10456", "Attempt to perform insert many outside of a transaction", 500) - MsgUnexpectedInterfaceType = ffe("FF10457", "Unexpected interface type: %T", 500) - MsgBlockchainConnectorRESTErrConflict = ffe("FF10458", "Conflict from blockchain connector: %s", 409) - MsgTokensRESTErrConflict = ffe("FF10459", "Conflict from tokens service: %s", 409) - MsgBatchWithDataNotSupported = ffe("FF10460", "Provided subscription '%s' enables batching and withData which is not supported", 400) - MsgBatchDeliveryNotSupported = ffe("FF10461", "Batch delivery not supported by transport '%s'", 400) - MsgWSWrongNamespace = ffe("FF10462", "Websocket request received on a namespace scoped connection but the provided namespace does not match") - MsgMaxSubscriptionEventScanLimitBreached = ffe("FF10463", "Event scan limit breached with start sequence ID %d and end sequence ID %d. Please restrict your query to a narrower range", 400) - MsgSequenceIDDidNotParseToInt = ffe("FF10464", "Could not parse provided %s to an integer sequence ID", 400) - MsgInternalServerError = ffe("FF10465", "Internal server error: %s", 500) - MsgCannotCancelBatchType = ffe("FF10466", "Cannot cancel batch of type: %s", 400) - MsgErrorLoadingBatch = ffe("FF10467", "Error loading batch messages") - MsgBatchNotDispatching = ffe("FF10468", "Batch %s is not currently dispatching - current: %s", 400) - MsgNoRegistrationMessageData = ffe("FF10469", "Unable to check message registration data for org %s", 500) - MsgUnexpectedRegistrationType = ffe("FF10470", "Unexpected type checking registration status: %s", 500) - MsgUnableToParseRegistrationData = ffe("FF10471", "Unable to parse registration message data: %s", 500) - MsgInvalidLastEventProtocolID = ffe("FF10472", "Unable to parse protocol ID of previous event: %s", 500) - MsgInvalidFromBlockNumber = ffe("FF10473", "Unable to parse block number: %s", 500) + MsgConfigFailed = ffe("FF10101", "Failed to read config") + MsgJSONDecodeFailed = ffe("FF10103", "Failed to decode input JSON") + MsgTLSConfigFailed = ffe("FF10105", "Failed to initialize TLS configuration") + MsgWebsocketClientError = ffe("FF10108", "Error received from WebSocket client: %s") + Msg404NotFound = ffe("FF10109", "Not found", 404) + MsgUnknownBlockchainPlugin = ffe("FF10110", "Unknown blockchain plugin: %s") + MsgEthConnectorRESTErr = ffe("FF10111", "Error from ethereum connector: %s") + MsgDBInitFailed = ffe("FF10112", "Database initialization failed") + MsgDBQueryBuildFailed = ffe("FF10113", "Database query builder failed") + MsgDBBeginFailed = ffe("FF10114", "Database begin transaction failed") + MsgDBQueryFailed = ffe("FF10115", "Database query failed") + MsgDBInsertFailed = ffe("FF10116", "Database insert failed") + MsgDBUpdateFailed = ffe("FF10117", "Database update failed") + MsgDBDeleteFailed = ffe("FF10118", "Database delete failed") + MsgDBCommitFailed = ffe("FF10119", "Database commit failed") + MsgDBMissingJoin = ffe("FF10120", "Database missing expected join entry in table '%s' for id '%s'") + MsgDBReadErr = ffe("FF10121", "Database resultset read error from table '%s'") + MsgUnknownDatabasePlugin = ffe("FF10122", "Unknown database plugin '%s'") + MsgNullDataReferenceID = ffe("FF10123", "Data id is null in message data reference %d") + MsgDupDataReferenceID = ffe("FF10124", "Duplicate data ID in message '%s'", 409) + MsgScanFailed = ffe("FF10125", "Failed to restore type '%T' into '%T'") + MsgUnregisteredBatchType = ffe("FF10126", "Unregistered batch type '%s'") + MsgBatchDispatchTimeout = ffe("FF10127", "Timed out dispatching work to batch") + MsgInitializationNilDepError = ffe("FF10128", "Initialization failed in %s due to unmet dependency") + MsgNilResponseNon204 = ffe("FF10129", "No output from API call") + MsgDataNotFound = ffe("FF10133", "Data not found for message %s", 400) + MsgUnknownSharedStoragePlugin = ffe("FF10134", "Unknown Shared Storage plugin '%s'") + MsgIPFSHashDecodeFailed = ffe("FF10135", "Failed to decode IPFS hash into 32byte value '%s'") + MsgIPFSRESTErr = ffe("FF10136", "Error from IPFS: %s") + MsgSerializationFailed = ffe("FF10137", "Serialization failed") + MsgMissingPluginConfig = ffe("FF10138", "Missing configuration '%s' for %s") + MsgMissingDataHashIndex = ffe("FF10139", "Missing data hash for index '%d' in message", 400) + MsgInvalidEthAddress = ffe("FF10141", "Supplied ethereum address is invalid", 400) + MsgInvalidTezosAddress = ffe("FF10142", "Supplied tezos address is invalid", 400) + Msg404NoResult = ffe("FF10143", "No result found", 404) + MsgUnsupportedSQLOpInFilter = ffe("FF10150", "No SQL mapping implemented for filter operator '%s'", 400) + MsgFilterSortDesc = ffe("FF10154", "Sort field. For multi-field sort use comma separated values (or multiple query values) with '-' prefix for descending") + MsgContextCanceled = ffe("FF00154", "Context cancelled") + MsgDBMigrationFailed = ffe("FF10163", "Database migration failed") + MsgHashMismatch = ffe("FF10164", "Hash mismatch") + MsgDefaultNamespaceNotFound = ffe("FF10166", "namespaces.default '%s' must be included in the namespaces.predefined configuration") + MsgEventTypesParseFail = ffe("FF10168", "Unable to parse list of event types", 400) + MsgUnknownEventType = ffe("FF10169", "Unknown event type '%s'", 400) + MsgIDMismatch = ffe("FF10170", "ID mismatch") + MsgRegexpCompileFailed = ffe("FF10171", "Unable to compile '%s' regexp '%s'") + MsgUnknownEventTransportPlugin = ffe("FF10172", "Unknown event transport plugin: %s") + MsgWSConnectionNotActive = ffe("FF10173", "Websocket connection '%s' no longer active") + MsgWSSubAlreadyInFlight = ffe("FF10174", "Websocket subscription '%s' already has a message in flight") + MsgWSMsgSubNotMatched = ffe("FF10175", "Acknowledgment does not match an inflight event + subscription") + MsgWSClientSentInvalidData = ffe("FF10176", "Invalid data") + MsgWSClientUnknownAction = ffe("FF10177", "Unknown action '%s'") + MsgWSInvalidStartAction = ffe("FF10178", "A start action must set namespace and either a name or ephemeral=true") + MsgWSAutoAckChanged = ffe("FF10179", "The autoack option must be set consistently on all start requests") + MsgWSAutoAckEnabled = ffe("FF10180", "The autoack option is enabled on this connection") + MsgConnSubscriptionNotStarted = ffe("FF10181", "Subscription %v is not started on connection") + MsgDispatcherClosing = ffe("FF10182", "Event dispatcher closing") + MsgMaxFilterSkip = ffe("FF10183", "You have reached the maximum pagination limit for this query (%d)", 400) + MsgMaxFilterLimit = ffe("FF10184", "Your query exceeds the maximum filter limit (%d)", 400) + MsgAPIServerStaticFail = ffe("FF10185", "An error occurred loading static content", 500) + MsgEventListenerClosing = ffe("FF10186", "Event listener closing") + MsgNamespaceDoesNotExist = ffe("FF10187", "Namespace does not exist", 404) + MsgInvalidSubscription = ffe("FF10189", "Invalid subscription", 400) + MsgMismatchedTransport = ffe("FF10190", "Connection ID '%s' appears not to be unique between transport '%s' and '%s'", 400) + MsgInvalidFirstEvent = ffe("FF10191", "Invalid firstEvent definition - must be 'newest','oldest' or a sequence number", 400) + MsgNumberMustBeGreaterEqual = ffe("FF10192", "Number must be greater than or equal to %d", 400) + MsgAlreadyExists = ffe("FF10193", "A %s with name '%s:%s' already exists", 409) + MsgJSONValidatorBadRef = ffe("FF10194", "Cannot use JSON validator for data with type '%s' and validator reference '%v'", 400) + MsgDatatypeNotFound = ffe("FF10195", "Datatype '%v' not found", 400) + MsgSchemaLoadFailed = ffe("FF10196", "Datatype '%s' schema invalid", 400) + MsgDataCannotBeValidated = ffe("FF10197", "Data cannot be validated", 400) + MsgJSONDataInvalidPerSchema = ffe("FF10198", "Data does not conform to the JSON schema of datatype '%s': %s", 400) + MsgDataValueIsNull = ffe("FF10199", "Data value is null", 400) + MsgDataInvalidHash = ffe("FF10201", "Invalid data: hashes do not match Hash=%s Expected=%s", 400) + MsgDataReferenceUnresolvable = ffe("FF10204", "Data reference %d cannot be resolved", 400) + MsgDataMissing = ffe("FF10205", "Data entry %d has neither 'id' to refer to existing data, or 'value' to include in-line JSON data", 400) + MsgAuthorInvalid = ffe("FF10206", "Invalid author specified", 400) + MsgMessageNotFound = ffe("FF10207", "Message '%s' not found", 404) + MsgBatchNotFound = ffe("FF10209", "Batch '%s' not found for message", 404) + MsgMessageTXNotSet = ffe("FF10210", "Message '%s' does not have an assigned transaction", 404) + MsgOwnerMissing = ffe("FF10211", "Owner missing", 400) + MsgUnknownIdentityPlugin = ffe("FF10212", "Unknown Identity plugin '%s'") + MsgUnknownDataExchangePlugin = ffe("FF10213", "Unknown Data Exchange plugin '%s'") + MsgParentIdentityNotFound = ffe("FF10214", "Identity '%s' not found in identity chain for %s '%s'") + MsgInvalidSigningIdentity = ffe("FF10215", "Invalid signing identity") + MsgNodeAndOrgIDMustBeSet = ffe("FF10216", "node.name, org.name and org.key must be configured first", 409) + MsgBlobStreamingFailed = ffe("FF10217", "Blob streaming terminated with error", 500) + MsgNodeNotFound = ffe("FF10224", "Node with name or identity '%s' not found", 400) + MsgLocalNodeNotSet = ffe("FF10225", "Unable to resolve the local node. Please ensure node.name is configured", 500) + MsgGroupNotFound = ffe("FF10226", "Group '%s' not found", 404) + MsgDXRESTErr = ffe("FF10229", "Error from data exchange: %s") + MsgInvalidHex = ffe("FF10231", "Invalid hex supplied", 400) + MsgInvalidWrongLenB32 = ffe("FF00107", "Byte length must be 32 (64 hex characters)", 400) + MsgNodeNotFoundInOrg = ffe("FF10233", "Unable to find any nodes owned by org '%s', or parent orgs", 400) + MsgDXBadResponse = ffe("FF10237", "Unexpected '%s' in data exchange response: %s") + MsgDXBadHash = ffe("FF10238", "Unexpected hash returned from data exchange upload. Hash=%s Expected=%s") + MsgBlobNotFound = ffe("FF10239", "No blob has been uploaded or confirmed received, with hash=%s", 404) + MsgDownloadBlobFailed = ffe("FF10240", "Error download blob with reference '%s' from local data exchange") + MsgDataDoesNotHaveBlob = ffe("FF10241", "Data does not have a blob attachment", 404) + MsgWebhookURLEmpty = ffe("FF10242", "Webhook subscription option 'url' cannot be empty", 400) + MsgWebhookInvalidStringMap = ffe("FF10243", "Webhook subscription option '%s' must be map of string values. %s=%T", 400) + MsgWebsocketsNoData = ffe("FF10244", "Websockets subscriptions do not support streaming the full data payload, just the references (withData must be false)", 400) + MsgWebhooksWithData = ffe("FF10245", "Webhook subscriptions require the full data payload (withData must be true)", 400) + MsgWebhooksReplyBadJSON = ffe("FF10257", "Failed to process reply from webhook as JSON") + MsgRequestTimeout = ffe("FF10260", "The request with id '%s' timed out after %.2fms", 408) + MsgRequestReplyTagRequired = ffe("FF10261", "For request messages 'header.tag' must be set on the request message to route it to a suitable responder", 400) + MsgRequestCannotHaveCID = ffe("FF10262", "For request messages 'header.cid' must be unset", 400) + MsgSystemTransportInternal = ffe("FF10266", "You cannot create subscriptions on the system events transport") + MsgFilterCountNotSupported = ffe("FF10267", "This query does not support generating a count of all results") + MsgRejected = ffe("FF10269", "Message with ID '%s' was rejected. Please check the FireFly logs for more information") + MsgRequestMustBePrivate = ffe("FF10271", "For request messages you must specify a group of private recipients", 400) + MsgUnknownTokensPlugin = ffe("FF10272", "Unknown tokens plugin '%s'", 400) + MsgMissingTokensPluginConfig = ffe("FF10273", "Invalid tokens configuration - name and plugin are required", 400) + MsgTokensRESTErr = ffe("FF10274", "Error from tokens service: %s") + MsgTokenPoolDuplicate = ffe("FF10275", "Duplicate token pool: %s", 409) + MsgTokenPoolRejected = ffe("FF10276", "Token pool with ID '%s' was rejected. Please check the FireFly logs for more information") + MsgIdentityNotFoundByString = ffe("FF10277", "Identity could not be resolved via lookup string '%s'") + MsgAuthorOrgSigningKeyMismatch = ffe("FF10279", "Author organization '%s' is not associated with signing key '%s'") + MsgCannotTransferToSelf = ffe("FF10280", "From and to addresses must be different", 400) + MsgLocalOrgNotSet = ffe("FF10281", "Unable to resolve the local root org. Please ensure org.name is configured", 500) + MsgTezosconnectRESTErr = ffe("FF10283", "Error from tezos connector: %s") + MsgFabconnectRESTErr = ffe("FF10284", "Error from fabconnect: %s") + MsgInvalidIdentity = ffe("FF10285", "Supplied Fabric signer identity is invalid", 400) + MsgFailedToDecodeCertificate = ffe("FF10286", "Failed to decode certificate: %s", 500) + MsgInvalidMessageType = ffe("FF10287", "Invalid message type - allowed types are %s", 400) + MsgWSClosed = ffe("FF10290", "Websocket closed") + MsgFieldNotSpecified = ffe("FF10292", "Field '%s' must be specified", 400) + MsgTokenPoolNotActive = ffe("FF10293", "Token pool is not yet activated") + MsgHistogramCollectionParam = ffe("FF10297", "Collection to fetch") + MsgInvalidNumberOfIntervals = ffe("FF10298", "Number of time intervals must be between %d and %d", 400) + MsgInvalidChartNumberParam = ffe("FF10299", "Invalid %s. Must be a number.", 400) + MsgHistogramInvalidTimes = ffe("FF10300", "Start time must be before end time", 400) + MsgUnsupportedCollection = ffe("FF10301", "%s collection is not supported", 400) + MsgContractInterfaceExists = ffe("FF10302", "A contract interface already exists in the namespace: '%s' with name: '%s' and version: '%s'", 409) + MsgContractInterfaceNotFound = ffe("FF10303", "Contract interface %s not found", 404) + MsgContractMissingInputArgument = ffe("FF10304", "Missing required input argument '%s'", 400) + MsgContractWrongInputType = ffe("FF10305", "Input '%v' is of type '%v' not expected type of '%v'", 400) + MsgContractMissingInputField = ffe("FF10306", "Expected object of type '%v' to contain field named '%v' but it was missing", 400) + MsgContractMapInputType = ffe("FF10307", "Unable to map input type '%v' to known FireFly type - was expecting '%v'", 400) + MsgContractByteDecode = ffe("FF10308", "Unable to decode field '%v' as bytes", 400) + MsgContractInternalType = ffe("FF10309", "Input '%v' of type '%v' is not compatible blockchain internalType of '%v'", 400) + MsgContractLocationInvalid = ffe("FF10310", "Failed to validate contract location: %v", 400) + MsgContractParamInvalid = ffe("FF10311", "Failed to validate contract param: %v", 400) + MsgContractListenerNameExists = ffe("FF10312", "A contract listener already exists in the namespace: '%s' with name: '%s'", 409) + MsgContractMethodNotSet = ffe("FF10313", "Either an interface reference and method path, or in-line method definition, must be supplied on invoke contract request", 400) + MsgContractMethodResolveError = ffe("FF10315", "Unable to resolve contract method: %s", 400) + MsgContractLocationExists = ffe("FF10316", "The contract location cannot be changed after it is created", 400) + MsgListenerNoEvent = ffe("FF10317", "Either an interface reference and event path, or in-line event definition must be supplied when creating a contract listener", 400) + MsgListenerEventNotFound = ffe("FF10318", "No event was found in namespace '%s' with id '%s'", 400) + MsgEventNameMustBeSet = ffe("FF10319", "Event name must be set", 400) + MsgMethodNameMustBeSet = ffe("FF10320", "Method name must be set", 400) + MsgContractEventResolveError = ffe("FF10321", "Unable to resolve contract event", 400) + MsgQueryOpUnsupportedMod = ffe("FF10322", "Operation '%s' on '%s' does not support modifiers", 400) + MsgDXBadSize = ffe("FF10323", "Unexpected size returned from data exchange upload. Size=%d Expected=%d") + MsgTooLargeBroadcast = ffe("FF10327", "Message size %.2fkb is too large for the max broadcast batch size of %.2fkb", 400) + MsgTooLargePrivate = ffe("FF10328", "Message size %.2fkb is too large for the max private message size of %.2fkb", 400) + MsgManifestMismatch = ffe("FF10329", "Manifest mismatch overriding '%s' status as failure: '%s'", 400) + MsgFFIValidationFail = ffe("FF10331", "Field '%s' does not validate against the provided schema", 400) + MsgFFISchemaParseFail = ffe("FF10332", "Failed to parse schema for param '%s'", 400) + MsgFFISchemaCompileFail = ffe("FF10333", "Failed compile schema for param '%s'", 400) + MsgPluginInitializationFailed = ffe("FF10334", "Plugin initialization error", 500) + MsgUnknownTransactionType = ffe("FF10336", "Unknown transaction type '%s'", 400) + MsgGoTemplateCompileFailed = ffe("FF10337", "Go template compilation for '%s' failed: %s", 500) + MsgGoTemplateExecuteFailed = ffe("FF10338", "Go template execution for '%s' failed: %s", 500) + MsgAddressResolveFailed = ffe("FF10339", "Failed to resolve signing key string '%s': %s", 500) + MsgAddressResolveBadStatus = ffe("FF10340", "Failed to resolve signing key string '%s' [%d]: %s", 500) + MsgAddressResolveBadResData = ffe("FF10341", "Failed to resolve signing key string '%s' - invalid address returned '%s': %s", 500) + MsgDXNotInitialized = ffe("FF10342", "Data exchange is initializing") + MsgDBLockFailed = ffe("FF10345", "Database lock failed") + MsgFFIGenerationFailed = ffe("FF10346", "Error generating smart contract interface: %s", 400) + MsgFFIGenerationUnsupported = ffe("FF10347", "Smart contract interface generation is not supported by this blockchain plugin", 400) + MsgBlobHashMismatch = ffe("FF10348", "Blob hash mismatch sent=%s received=%s", 400) + MsgDIDResolverUnknown = ffe("FF10349", "DID resolver unknown for DID: %s", 400) + MsgIdentityNotOrg = ffe("FF10350", "Identity '%s' with DID '%s' is not an organization", 400) + MsgIdentityNotNode = ffe("FF10351", "Identity '%s' with DID '%s' is not a node", 400) + MsgBlockchainKeyNotSet = ffe("FF10352", "No blockchain key specified", 400) + MsgNoVerifierForIdentity = ffe("FF10353", "No %s verifier registered for identity %s", 400) + MsgNodeMissingBlockchainKey = ffe("FF10354", "No signing key was specified, and no default signing key or organization signing key is configured for this namespace", 400) + MsgAuthorRegistrationMismatch = ffe("FF10355", "Verifier '%s' cannot be used for signing with author '%s'. Verifier registered to '%s'", 400) + MsgAuthorMissingForKey = ffe("FF10356", "Key '%s' has not been registered by any identity, and a separate 'author' was not supplied", 404) + MsgAuthorIncorrectForRootReg = ffe("FF10357", "Author namespace '%s' and DID '%s' combination invalid for root organization registration", 400) + MsgKeyIdentityMissing = ffe("FF10358", "Identity owner of key '%s' not found", 500) + MsgIdentityChainLoop = ffe("FF10364", "Loop detected on identity %s in chain for %s (%s)", 400) + MsgInvalidIdentityParentType = ffe("FF10365", "Parent %s (%s) of type %s is invalid for child %s (%s) of type", 400) + MsgParentIdentityMissingClaim = ffe("FF10366", "Parent %s (%s) is invalid (missing claim)", 400) + MsgDXInfoMissingID = ffe("FF10367", "Data exchange endpoint info missing 'id' field", 500) + MsgEventNotFound = ffe("FF10370", "Event with name '%s' not found", 400) + MsgOperationNotSupported = ffe("FF10371", "Operation not supported: %s", 400) + MsgFailedToRetrieve = ffe("FF10372", "Failed to retrieve %s %s", 500) + MsgBlobMissingPublic = ffe("FF10373", "Blob for data %s missing public payload reference while flushing batch", 500) + MsgDBMultiRowConfigError = ffe("FF10374", "Database invalid configuration - using multi-row insert on DB plugin that does not support query syntax for input") + MsgDBNoSequence = ffe("FF10375", "Failed to retrieve sequence for insert row %d (could mean duplicate insert)", 500) + MsgDownloadSharedFailed = ffe("FF10376", "Error downloading data with reference '%s' from shared storage") + MsgDownloadBatchMaxBytes = ffe("FF10377", "Error downloading batch with reference '%s' from shared storage - maximum size limit reached") + MsgOperationDataIncorrect = ffe("FF10378", "Operation data type incorrect: %T", 400) + MsgDataMissingBlobHash = ffe("FF10379", "Blob for data %s cannot be transferred as it is missing a hash", 500) + MsgUnexpectedDXMessageType = ffe("FF10380", "Unexpected websocket event type from DX plugin: %s", 500) + MsgContractListenerExists = ffe("FF10383", "A contract listener already exists for this combination of topic + filters (location + event)", 409) + MsgInvalidOutputOption = ffe("FF10385", "invalid output option '%s'") + MsgInvalidPluginConfiguration = ffe("FF10386", "Invalid %s plugin configuration - name and type are required") + MsgReferenceMarkdownMissing = ffe("FF10387", "Reference markdown file missing: '%s'") + MsgFFSystemReservedName = ffe("FF10388", "Invalid namespace configuration - %s is a reserved name") + MsgInvalidNamespaceMode = ffe("FF10389", "Invalid %s namespace configuration - unknown mode") + MsgNamespaceUnknownPlugin = ffe("FF10390", "Invalid %s namespace configuration - unknown plugin %s") + MsgNamespaceWrongPluginsMultiparty = ffe("FF10391", "Invalid %s namespace configuration - multiparty mode requires database, blockchain, shared storage, and data exchange plugins") + MsgNamespaceNoDatabase = ffe("FF10392", "Invalid %s namespace configuration - a database plugin is required") + MsgNamespaceMultiplePluginType = ffe("FF10394", "Invalid %s namespace configuration - multiple %s plugins provided") + MsgDuplicatePluginName = ffe("FF10395", "Invalid plugin configuration - plugin with name %s already exists", 409) + MsgInvalidFireFlyContractIndex = ffe("FF10396", "No configuration found for FireFly contract at %s") + MsgUnrecognizedNetworkAction = ffe("FF10397", "Unrecognized network action: %s", 400) + MsgOverrideExistingFieldCustomOption = ffe("FF10398", "Cannot override existing field with custom option named '%s'", 400) + MsgTerminateNotSupported = ffe("FF10399", "The 'terminate' operation to mark a switchover of smart contracts is not supported on namespace %s", 400) + MsgDefRejectedBadPayload = ffe("FF10400", "Rejected %s message '%s' - invalid payload") + MsgDefRejectedAuthorBlank = ffe("FF10401", "Rejected %s message '%s' - author is blank") + MsgDefRejectedSignatureMismatch = ffe("FF10402", "Rejected %s message '%s' - signature mismatch") + MsgDefRejectedValidateFail = ffe("FF10403", "Rejected %s '%s' - validate failed") + MsgDefRejectedIDMismatch = ffe("FF10404", "Rejected %s '%s' - ID mismatch with existing record") + MsgDefRejectedLocationMismatch = ffe("FF10405", "Rejected %s '%s' - location mismatch with existing record") + MsgDefRejectedSchemaFail = ffe("FF10406", "Rejected %s '%s' - schema check: %s") + MsgDefRejectedConflict = ffe("FF10407", "Rejected %s '%s' - conflicts with existing: %s", 409) + MsgDefRejectedIdentityNotFound = ffe("FF10408", "Rejected %s '%s' - identity not found: %s") + MsgDefRejectedWrongAuthor = ffe("FF10409", "Rejected %s '%s' - wrong author: %s") + MsgDefRejectedHashMismatch = ffe("FF10410", "Rejected %s '%s' - hash mismatch: %s != %s") + MsgInvalidNamespaceUUID = ffe("FF10411", "Expected 'namespace:' prefix on ID '%s'", 400) + MsgBadNetworkVersion = ffe("FF10412", "Bad network version: %s") + MsgDefinitionRejected = ffe("FF10413", "Definition rejected") + MsgActionNotSupported = ffe("FF10414", "This action is not supported in this namespace", 400) + MsgMessagesNotSupported = ffe("FF10415", "Messages are not supported in this namespace", 400) + MsgInvalidSubscriptionForNetwork = ffe("FF10416", "Subscription name '%s' is invalid according to multiparty network rules in effect (network version=%d)") + MsgBlockchainNotConfigured = ffe("FF10417", "No blockchain plugin configured") + MsgInvalidBatchPinEvent = ffe("FF10418", "BatchPin event is not valid - %s (%s): %s") + MsgDuplicatePluginBroadcastName = ffe("FF10419", "Invalid %s plugin broadcast name: %s - broadcast names must be unique", 409) + MsgInvalidConnectorName = ffe("FF10420", "Could not find name %s for %s connector") + MsgCannotInitLegacyNS = ffe("FF10421", "could not initialize legacy '%s' namespace - found conflicting V1 multi-party config in %s") + MsgInvalidGroupMember = ffe("FF10422", "invalid group member - node '%s' is not owned by '%s' or any of its ancestors") + MsgContractListenerStatusInvalid = ffe("FF10423", "Failed to validate contract listener status: %v", 400) + MsgCacheMissSizeLimitKeyInternal = ffe("FF10424", "could not initialize cache - size limit config key is not provided") + MsgCacheMissTTLKeyInternal = ffe("FF10425", "could not initialize cache - ttl config key is not provided") + MsgCacheConfigKeyMismatchInternal = ffe("FF10426", "could not initialize cache - '%s' and '%s' do not have identical prefix, mismatching prefixes are: '%s','%s'") + MsgCacheUnexpectedSizeKeyNameInternal = ffe("FF10427", "could not initialize cache - '%s' is not an expected size configuration key suffix. Expected values are: 'size', 'limit'") + MsgUnknownVerifierType = ffe("FF10428", "Unknown verifier type", 400) + MsgNotSupportedByBlockchainPlugin = ffe("FF10429", "Not supported by blockchain plugin", 400) + MsgIdempotencyKeyDuplicateMessage = ffe("FF10430", "Idempotency key '%s' already used for message '%s'", 409) + MsgIdempotencyKeyDuplicateTransaction = ffe("FF10431", "Idempotency key '%s' already used for transaction '%s'", 409) + MsgNonIdempotencyKeyConflictTxInsert = ffe("FF10432", "Conflict on insert of transaction '%s'. No existing transaction matching idempotency key '%s' found", 409) + MsgErrorNameMustBeSet = ffe("FF10433", "The name of the error must be set", 400) + MsgContractErrorsResolveError = ffe("FF10434", "Unable to resolve contract errors: %s", 400) + MsgUnknownInterfaceFormat = ffe("FF10435", "Unknown interface format: %s", 400) + MsgUnknownNamespace = ffe("FF10436", "Unknown namespace '%s'", 404) + MsgMissingNamespace = ffe("FF10437", "Missing namespace in request", 400) + MsgDeprecatedResetWithAutoReload = ffe("FF10438", "The deprecated reset API cannot be used when dynamic config reload is enabled", 409) + MsgConfigArrayVsRawConfigMismatch = ffe("FF10439", "Error processing configuration - mismatch between raw and processed array lengths") + MsgDefaultChannelNotConfigured = ffe("FF10440", "No default channel configured for this namespace", 400) + MsgNamespaceInitializing = ffe("FF10441", "Namespace '%s' is initializing", 412) + MsgPinsNotAssigned = ffe("FF10442", "Message cannot be sent because pins have not been assigned") + MsgMethodDoesNotSupportPinning = ffe("FF10443", "This method does not support passing a payload for pinning") + MsgOperationNotFoundInTransaction = ffe("FF10444", "No operation of type %s was found in transaction '%s'") + MsgCannotSetParameterWithMessage = ffe("FF10445", "Cannot provide a value for '%s' when pinning a message", 400) + MsgNamespaceNotStarted = ffe("FF10446", "Namespace '%s' is not started", 412) + MsgNameExists = ffe("FF10447", "Name already exists", 409) + MsgNetworkNameExists = ffe("FF10448", "Network name already exists", 409) + MsgCannotDeletePublished = ffe("FF10449", "Cannot delete an item that has been published", 409) + MsgAlreadyPublished = ffe("FF10450", "Item has already been published", 409) + MsgContractInterfaceNotPublished = ffe("FF10451", "Contract interface '%s' has not been published", 409) + MsgInvalidMessageSigner = ffe("FF10452", "Invalid message '%s'. Key '%s' does not match the signer of the pin: %s") + MsgInvalidMessageIdentity = ffe("FF10453", "Invalid message '%s'. Author '%s' does not match identity registered to %s: %s (%s)") + MsgDuplicateTLSConfig = ffe("FF10454", "Found duplicate TLS Config '%s'", 400) + MsgNotFoundTLSConfig = ffe("FF10455", "Provided TLS Config name '%s' not found for namespace '%s'", 400) + MsgSQLInsertManyOutsideTransaction = ffe("FF10456", "Attempt to perform insert many outside of a transaction", 500) + MsgUnexpectedInterfaceType = ffe("FF10457", "Unexpected interface type: %T", 500) + MsgBlockchainConnectorRESTErrConflict = ffe("FF10458", "Conflict from blockchain connector: %s", 409) + MsgTokensRESTErrConflict = ffe("FF10459", "Conflict from tokens service: %s", 409) + MsgBatchWithDataNotSupported = ffe("FF10460", "Provided subscription '%s' enables batching and withData which is not supported", 400) + MsgBatchDeliveryNotSupported = ffe("FF10461", "Batch delivery not supported by transport '%s'", 400) + MsgWSWrongNamespace = ffe("FF10462", "Websocket request received on a namespace scoped connection but the provided namespace does not match") + MsgMaxSubscriptionEventScanLimitBreached = ffe("FF10463", "Event scan limit breached with start sequence ID %d and end sequence ID %d. Please restrict your query to a narrower range", 400) + MsgSequenceIDDidNotParseToInt = ffe("FF10464", "Could not parse provided %s to an integer sequence ID", 400) + MsgInternalServerError = ffe("FF10465", "Internal server error: %s", 500) + MsgCannotCancelBatchType = ffe("FF10466", "Cannot cancel batch of type: %s", 400) + MsgErrorLoadingBatch = ffe("FF10467", "Error loading batch messages") + MsgBatchNotDispatching = ffe("FF10468", "Batch %s is not currently dispatching - current: %s", 400) + MsgNoRegistrationMessageData = ffe("FF10469", "Unable to check message registration data for org %s", 500) + MsgUnexpectedRegistrationType = ffe("FF10470", "Unexpected type checking registration status: %s", 500) + MsgUnableToParseRegistrationData = ffe("FF10471", "Unable to parse registration message data: %s", 500) + MsgInvalidLastEventProtocolID = ffe("FF10472", "Unable to parse protocol ID of previous event: %s", 500) + MsgInvalidFromBlockNumber = ffe("FF10473", "Unable to parse block number: %s", 500) + MsgFiltersAndRootEventError = ffe("FF10474", "Cannot provide both filters and deprecated event path, please only provide one option.", 500) + MsgFiltersEmpty = ffe("FF10475", "No filters specified in contract listener: %s.", 500) + MsgContractListenerBlockchainFilterLimit = ffe("FF10476", "Blockchain plugin only supports one filter for contract listener: %s.", 500) + MsgDuplicateContractListenerFilterLocation = ffe("FF10477", "Duplicate filter provided for contract listener for location", 400) ) diff --git a/internal/coremsgs/en_struct_descriptions.go b/internal/coremsgs/en_struct_descriptions.go index f3515877e..208df0d3b 100644 --- a/internal/coremsgs/en_struct_descriptions.go +++ b/internal/coremsgs/en_struct_descriptions.go @@ -302,22 +302,29 @@ var ( // ContractListener field descriptions ContractListenerID = ffm("ContractListener.id", "The UUID of the smart contract listener") - ContractListenerInterface = ffm("ContractListener.interface", "A reference to an existing FFI, containing pre-registered type information for the event") + ContractListenerInterface = ffm("ContractListener.interface", "Deprecated: Please use 'interface' in the array of 'filters' instead") ContractListenerNamespace = ffm("ContractListener.namespace", "The namespace of the listener, which defines the namespace of all blockchain events detected by this listener") ContractListenerName = ffm("ContractListener.name", "A descriptive name for the listener") ContractListenerBackendID = ffm("ContractListener.backendId", "An ID assigned by the blockchain connector to this listener") - ContractListenerLocation = ffm("ContractListener.location", "A blockchain specific contract identifier. For example an Ethereum contract address, or a Fabric chaincode name and channel") + ContractListenerLocation = ffm("ContractListener.location", "Deprecated: Please use 'location' in the array of 'filters' instead") ContractListenerCreated = ffm("ContractListener.created", "The creation time of the listener") - ContractListenerEvent = ffm("ContractListener.event", "The definition of the event, either provided in-line when creating the listener, or extracted from the referenced FFI") + ContractListenerEvent = ffm("ContractListener.event", "Deprecated: Please use 'event' in the array of 'filters' instead") + ContractListenerFilters = ffm("ContractListener.filters", "A list of filters for the contract listener. Each filter is made up of an Event and an optional Location. Events matching these filters will always be emitted in the order determined by the blockchain.") ContractListenerTopic = ffm("ContractListener.topic", "A topic to set on the FireFly event that is emitted each time a blockchain event is detected from the blockchain. Setting this topic on a number of listeners allows applications to easily subscribe to all events they need") ContractListenerOptions = ffm("ContractListener.options", "Options that control how the listener subscribes to events from the underlying blockchain") - ContractListenerEventPath = ffm("ContractListener.eventPath", "When creating a listener from an existing FFI, this is the pathname of the event on that FFI to be detected by this listener") - ContractListenerSignature = ffm("ContractListener.signature", "The stringified signature of the event, as computed by the blockchain plugin") + ContractListenerEventPath = ffm("ContractListener.eventPath", "Deprecated: Please use 'eventPath' in the array of 'filters' instead") + ContractListenerSignature = ffm("ContractListener.signature", "A concatenation of all the stringified signature of the event and location, as computed by the blockchain plugin") ContractListenerState = ffm("ContractListener.state", "This field is provided for the event listener implementation of the blockchain provider to record state, such as checkpoint information") // ContractListenerOptions field descriptions ContractListenerOptionsFirstEvent = ffm("ContractListenerOptions.firstEvent", "A blockchain specific string, such as a block number, to start listening from. The special strings 'oldest' and 'newest' are supported by all blockchain connectors. Default is 'newest'") + ListenerFilterInterface = ffm("ListenerFilter.interface", "A reference to an existing FFI, containing pre-registered type information for the event") + ListenerFilterEvent = ffm("ListenerFilter.event", "The definition of the event, either provided in-line when creating the listener, or extracted from the referenced FFI") + ListenerFilterEventPath = ffm("ListenerFilter.eventPath", "When creating a listener from an existing FFI, this is the pathname of the event on that FFI to be detected by this listener") + ListenerFilterLocation = ffm("ListenerFilter.location", "A blockchain specific contract identifier. For example an Ethereum contract address, or a Fabric chaincode name and channel") + ListenerFilterSignature = ffm("ListenerFilter.signature", "The stringified signature of the event and location, as computed by the blockchain plugin") + // DIDDocument field descriptions DIDDocumentContext = ffm("DIDDocument.@context", "See https://www.w3.org/TR/did-core/#json-ld") DIDDocumentID = ffm("DIDDocument.id", "See https://www.w3.org/TR/did-core/#did-document-properties") diff --git a/internal/database/sqlcommon/contractlisteners_sql.go b/internal/database/sqlcommon/contractlisteners_sql.go index b1c1caaa0..87a09d654 100644 --- a/internal/database/sqlcommon/contractlisteners_sql.go +++ b/internal/database/sqlcommon/contractlisteners_sql.go @@ -44,6 +44,7 @@ var ( "topic", "options", "created", + "filters", } contractListenerFilterFieldMap = map[string]string{ "interface": "interface_id", @@ -53,6 +54,84 @@ var ( const contractlistenersTable = "contractlisteners" +func (s *SQLCommon) UpsertContractListener(ctx context.Context, listener *core.ContractListener, allowExisting bool) (err error) { + ctx, tx, autoCommit, err := s.BeginOrUseTx(ctx) + if err != nil { + return err + } + defer s.RollbackTx(ctx, tx, autoCommit) + + existing := false + if allowExisting { + // Do a select within the transaction to detemine if the UUID already exists + listenerRows, _, err := s.QueryTx(ctx, contractlistenersTable, tx, + sq.Select("id"). + From(contractlistenersTable). + Where(sq.Eq{ + "namespace": listener.Namespace, + "name": listener.Name, + }), + ) + if err != nil { + return err + } + + existing = listenerRows.Next() + if existing { + var id fftypes.UUID + _ = listenerRows.Scan(&id) + if listener.ID != nil { + if *listener.ID != id { + listenerRows.Close() + return database.IDMismatch + } + } + listener.ID = &id // Update on returned object + } + listenerRows.Close() + } + + if existing { + var interfaceID *fftypes.UUID + if listener.Interface != nil { + interfaceID = listener.Interface.ID + } + // Update the listener + if _, err = s.UpdateTx(ctx, contractlistenersTable, tx, + sq.Update(contractlistenersTable). + // Note we do not update ID + Set("backend_id", listener.BackendID). + Set("filters", listener.Filters). + Set("event", listener.Event). + Set("signature", listener.Signature). + Set("options", listener.Options). + Set("topic", listener.Topic). + Set("location", listener.Location). + Set("interface_id", interfaceID). + Where(sq.Eq{ + "namespace": listener.Namespace, + "name": listener.Name, + }), + func() { + s.callbacks.UUIDCollectionNSEvent(database.CollectionContractListeners, core.ChangeEventTypeUpdated, listener.Namespace, listener.ID) + }, + ); err != nil { + return err + } + } else { + if listener.ID == nil { + listener.ID = fftypes.NewUUID() + } + + err = s.InsertContractListener(ctx, listener) + if err != nil { + return err + } + } + + return s.CommitTx(ctx, tx, autoCommit) +} + func (s *SQLCommon) InsertContractListener(ctx context.Context, listener *core.ContractListener) (err error) { ctx, tx, autoCommit, err := s.BeginOrUseTx(ctx) if err != nil { @@ -81,6 +160,7 @@ func (s *SQLCommon) InsertContractListener(ctx context.Context, listener *core.C listener.Topic, listener.Options, listener.Created, + listener.Filters, ), func() { s.callbacks.UUIDCollectionNSEvent(database.CollectionContractListeners, core.ChangeEventTypeCreated, listener.Namespace, listener.ID) @@ -108,10 +188,14 @@ func (s *SQLCommon) contractListenerResult(ctx context.Context, row *sql.Rows) ( &listener.Topic, &listener.Options, &listener.Created, + &listener.Filters, ) if err != nil { return nil, i18n.WrapError(ctx, err, coremsgs.MsgDBReadErr, contractlistenersTable) } + + // Note: If we have a legacy "event" and "address" stored in the DB, it will be returned as before with the event at the top level + return &listener, nil } diff --git a/internal/database/sqlcommon/contractlisteners_sql_test.go b/internal/database/sqlcommon/contractlisteners_sql_test.go index b321a685a..c2b2f79b9 100644 --- a/internal/database/sqlcommon/contractlisteners_sql_test.go +++ b/internal/database/sqlcommon/contractlisteners_sql_test.go @@ -30,7 +30,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestContractListenerE2EWithDB(t *testing.T) { +func TestContractListenerLegacyE2EWithDB(t *testing.T) { s, cleanup := newSQLiteTestProvider(t) defer cleanup() ctx := context.Background() @@ -122,7 +122,7 @@ func TestContractListenerE2EWithDB(t *testing.T) { assert.Equal(t, 0, len(subs)) } -func TestUpsertContractListenerFailBegin(t *testing.T) { +func TestInsertContractListenerFailBegin(t *testing.T) { s, mock := newMockProvider().init() mock.ExpectBegin().WillReturnError(fmt.Errorf("pop")) err := s.InsertContractListener(context.Background(), &core.ContractListener{}) @@ -130,7 +130,7 @@ func TestUpsertContractListenerFailBegin(t *testing.T) { assert.NoError(t, mock.ExpectationsWereMet()) } -func TestUpsertContractListenerFailInsert(t *testing.T) { +func TestInsertContractListenerFailInsert(t *testing.T) { s, mock := newMockProvider().init() mock.ExpectBegin() mock.ExpectExec("INSERT .*").WillReturnError(fmt.Errorf("pop")) @@ -140,7 +140,7 @@ func TestUpsertContractListenerFailInsert(t *testing.T) { assert.NoError(t, mock.ExpectationsWereMet()) } -func TestUpsertContractListenerFailCommit(t *testing.T) { +func TestInsertContractListenerFailCommit(t *testing.T) { s, mock := newMockProvider().init() mock.ExpectBegin() mock.ExpectExec("INSERT .*").WillReturnResult(sqlmock.NewResult(1, 1)) @@ -211,7 +211,7 @@ func TestContractListenerDeleteFail(t *testing.T) { s, mock := newMockProvider().init() mock.ExpectBegin() mock.ExpectQuery("SELECT .*").WillReturnRows(sqlmock.NewRows(contractListenerColumns).AddRow( - fftypes.NewUUID(), nil, []byte("{}"), "ns1", "sub1", "123", "{}", "sig", "topic1", nil, fftypes.Now()), + fftypes.NewUUID(), nil, []byte("{}"), "ns1", "sub1", "123", "{}", "sig", "topic1", nil, fftypes.Now(), "[]"), ) mock.ExpectExec("DELETE .*").WillReturnError(fmt.Errorf("pop")) err := s.DeleteContractListenerByID(context.Background(), "ns", fftypes.NewUUID()) @@ -283,3 +283,197 @@ func TestUpdateContractListenerNotFount(t *testing.T) { assert.Regexp(t, "FF10143", err) assert.NoError(t, mock.ExpectationsWereMet()) } + +func TestUpsertContractListenerFailBegin(t *testing.T) { + s, mock := newMockProvider().init() + mock.ExpectBegin().WillReturnError(fmt.Errorf("pop")) + err := s.UpsertContractListener(context.Background(), &core.ContractListener{}, false) + assert.Regexp(t, "FF00175", err) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestUpsertContractListenerFailInsert(t *testing.T) { + s, mock := newMockProvider().init() + mock.ExpectBegin() + mock.ExpectExec("INSERT .*").WillReturnError(fmt.Errorf("pop")) + mock.ExpectRollback() + err := s.UpsertContractListener(context.Background(), &core.ContractListener{}, false) + assert.Regexp(t, "FF00177", err) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestUpsertContractListenerFailCommit(t *testing.T) { + s, mock := newMockProvider().init() + mock.ExpectBegin() + mock.ExpectExec("INSERT .*").WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit().WillReturnError(fmt.Errorf("pop")) + err := s.UpsertContractListener(context.Background(), &core.ContractListener{}, false) + assert.Regexp(t, "FF00180", err) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestContractListenerE2eWithDB(t *testing.T) { + s, cleanup := newSQLiteTestProvider(t) + defer cleanup() + ctx := context.Background() + + // Create a new contract listener entry + location := fftypes.JSONObject{"path": "my-api"} + locationJson, _ := json.Marshal(location) + sub := &core.ContractListener{ + ID: fftypes.NewUUID(), + Interface: &fftypes.FFIReference{ + ID: fftypes.NewUUID(), + }, + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "event1", + }, + }, + Filters: []*core.ListenerFilter{ + { + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "event1", + }, + }, + }, + }, + Namespace: "ns", + Name: "sub1", + BackendID: "sb-123", + Location: fftypes.JSONAnyPtrBytes(locationJson), + Topic: "topic1", + Options: &core.ContractListenerOptions{ + FirstEvent: "0", + }, + } + + s.callbacks.On("UUIDCollectionNSEvent", database.CollectionContractListeners, core.ChangeEventTypeCreated, "ns", sub.ID).Return() + s.callbacks.On("UUIDCollectionNSEvent", database.CollectionContractListeners, core.ChangeEventTypeUpdated, "ns", sub.ID).Return() + s.callbacks.On("UUIDCollectionNSEvent", database.CollectionContractListeners, core.ChangeEventTypeDeleted, "ns", sub.ID).Return() + + err := s.UpsertContractListener(ctx, sub, false) + assert.NotNil(t, sub.Created) + assert.NoError(t, err) + subJson, _ := json.Marshal(&sub) + + // Query back the listener (by query filter) + fb := database.ContractListenerQueryFactory.NewFilter(ctx) + filter := fb.And( + fb.Eq("backendid", sub.BackendID), + ) + subs, res, err := s.GetContractListeners(ctx, "ns", filter.Count(true)) + assert.NoError(t, err) + assert.Equal(t, 1, len(subs)) + assert.Equal(t, int64(1), *res.TotalCount) + subReadJson, _ := json.Marshal(subs[0]) + assert.Equal(t, string(subJson), string(subReadJson)) + + sub2 := &core.ContractListener{ + ID: fftypes.NewUUID(), + Interface: &fftypes.FFIReference{ + ID: fftypes.NewUUID(), + }, + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "event1", + }, + }, + Filters: []*core.ListenerFilter{ + { + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "event1", + }, + }, + }, + }, + Namespace: "ns", + Name: "sub1", + BackendID: "sb-123", + Location: fftypes.JSONAnyPtrBytes(locationJson), + Topic: "topic1", + Options: &core.ContractListenerOptions{ + FirstEvent: "0", + }, + } + + // Rejects attempt to update ID + err = s.UpsertContractListener(context.Background(), sub2, true) + assert.Equal(t, database.IDMismatch, err) + + // Update by backend ID + sub.BackendID = "sb-234" + err = s.UpsertContractListener(ctx, sub, true) + assert.NoError(t, err) + + // Query back the listener (by name) + subRead, err := s.GetContractListener(ctx, "ns", "sub1") + assert.NoError(t, err) + sub.BackendID = "sb-234" + subJson, _ = json.Marshal(&sub) + subReadJson, _ = json.Marshal(subRead) + assert.Equal(t, string(subJson), string(subReadJson)) + + // Query back the listener (by ID) + subRead, err = s.GetContractListenerByID(ctx, "ns", sub.ID) + assert.NoError(t, err) + subReadJson, _ = json.Marshal(subRead) + assert.Equal(t, string(subJson), string(subReadJson)) + + // Query back the listener (by protocol ID) + subRead, err = s.GetContractListenerByBackendID(ctx, "ns", sub.BackendID) + assert.NoError(t, err) + subReadJson, _ = json.Marshal(subRead) + assert.Equal(t, string(subJson), string(subReadJson)) + + // Query back the listener (by query filter) + filter = fb.And( + fb.Eq("backendid", sub.BackendID), + ) + subs, res, err = s.GetContractListeners(ctx, "ns", filter.Count(true)) + assert.NoError(t, err) + assert.Equal(t, 1, len(subs)) + assert.Equal(t, int64(1), *res.TotalCount) + subReadJson, _ = json.Marshal(subs[0]) + assert.Equal(t, string(subJson), string(subReadJson)) + + // Test delete, and refind no return + err = s.DeleteContractListenerByID(ctx, "ns", sub.ID) + assert.NoError(t, err) + subs, _, err = s.GetContractListeners(ctx, "ns", filter) + assert.NoError(t, err) + assert.Equal(t, 0, len(subs)) +} + +func TestUpsertContractListenerFailBeginExisting(t *testing.T) { + s, mock := newMockProvider().init() + mock.ExpectBegin().WillReturnError(fmt.Errorf("pop")) + err := s.UpsertContractListener(context.Background(), &core.ContractListener{}, true) + assert.Regexp(t, "FF00175", err) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestUpsertContractListenerFailSelect(t *testing.T) { + s, mock := newMockProvider().init() + mock.ExpectBegin() + mock.ExpectQuery("SELECT .*").WillReturnError(fmt.Errorf("pop")) + mock.ExpectRollback() + id := fftypes.NewUUID() + err := s.UpsertContractListener(context.Background(), &core.ContractListener{ID: id}, true) + assert.Regexp(t, "FF00176", err) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestUpsertContractListenerFailUpdate(t *testing.T) { + s, mock := newMockProvider().init() + id := fftypes.NewUUID() + mock.ExpectBegin() + mock.ExpectQuery("SELECT .*").WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(id.String())) + mock.ExpectExec("UPDATE .*").WillReturnError(fmt.Errorf("pop")) + mock.ExpectRollback() + err := s.UpsertContractListener(context.Background(), &core.ContractListener{ID: id}, true) + assert.Regexp(t, "pop", err) + assert.NoError(t, mock.ExpectationsWereMet()) +} diff --git a/mocks/blockchainmocks/plugin.go b/mocks/blockchainmocks/plugin.go index 108b752e9..1a8cc4728 100644 --- a/mocks/blockchainmocks/plugin.go +++ b/mocks/blockchainmocks/plugin.go @@ -90,6 +90,34 @@ func (_m *Plugin) Capabilities() *blockchain.Capabilities { return r0 } +// CheckOverlappingLocations provides a mock function with given fields: ctx, left, right +func (_m *Plugin) CheckOverlappingLocations(ctx context.Context, left *fftypes.JSONAny, right *fftypes.JSONAny) (bool, error) { + ret := _m.Called(ctx, left, right) + + if len(ret) == 0 { + panic("no return value specified for CheckOverlappingLocations") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.JSONAny, *fftypes.JSONAny) (bool, error)); ok { + return rf(ctx, left, right) + } + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.JSONAny, *fftypes.JSONAny) bool); ok { + r0 = rf(ctx, left, right) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.JSONAny, *fftypes.JSONAny) error); ok { + r1 = rf(ctx, left, right) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // DeleteContractListener provides a mock function with given fields: ctx, subscription, okNotFound func (_m *Plugin) DeleteContractListener(ctx context.Context, subscription *core.ContractListener, okNotFound bool) error { ret := _m.Called(ctx, subscription, okNotFound) @@ -155,7 +183,7 @@ func (_m *Plugin) GenerateErrorSignature(ctx context.Context, errorDef *fftypes. } // GenerateEventSignature provides a mock function with given fields: ctx, event -func (_m *Plugin) GenerateEventSignature(ctx context.Context, event *fftypes.FFIEventDefinition) string { +func (_m *Plugin) GenerateEventSignature(ctx context.Context, event *fftypes.FFIEventDefinition) (string, error) { ret := _m.Called(ctx, event) if len(ret) == 0 { @@ -163,13 +191,51 @@ func (_m *Plugin) GenerateEventSignature(ctx context.Context, event *fftypes.FFI } var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.FFIEventDefinition) (string, error)); ok { + return rf(ctx, event) + } if rf, ok := ret.Get(0).(func(context.Context, *fftypes.FFIEventDefinition) string); ok { r0 = rf(ctx, event) } else { r0 = ret.Get(0).(string) } - return r0 + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.FFIEventDefinition) error); ok { + r1 = rf(ctx, event) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GenerateEventSignatureWithLocation provides a mock function with given fields: ctx, event, location +func (_m *Plugin) GenerateEventSignatureWithLocation(ctx context.Context, event *fftypes.FFIEventDefinition, location *fftypes.JSONAny) (string, error) { + ret := _m.Called(ctx, event, location) + + if len(ret) == 0 { + panic("no return value specified for GenerateEventSignatureWithLocation") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.FFIEventDefinition, *fftypes.JSONAny) (string, error)); ok { + return rf(ctx, event, location) + } + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.FFIEventDefinition, *fftypes.JSONAny) string); ok { + r0 = rf(ctx, event, location) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.FFIEventDefinition, *fftypes.JSONAny) error); ok { + r1 = rf(ctx, event, location) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // GenerateFFI provides a mock function with given fields: ctx, generationRequest diff --git a/mocks/contractmocks/manager.go b/mocks/contractmocks/manager.go index dca991262..06c26e934 100644 --- a/mocks/contractmocks/manager.go +++ b/mocks/contractmocks/manager.go @@ -79,6 +79,36 @@ func (_m *Manager) AddContractListener(ctx context.Context, listener *core.Contr return r0, r1 } +// ConstructContractListenerSignature provides a mock function with given fields: ctx, listener +func (_m *Manager) ConstructContractListenerSignature(ctx context.Context, listener *core.ContractListenerInput) (*core.ContractListenerSignatureOutput, error) { + ret := _m.Called(ctx, listener) + + if len(ret) == 0 { + panic("no return value specified for ConstructContractListenerSignature") + } + + var r0 *core.ContractListenerSignatureOutput + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *core.ContractListenerInput) (*core.ContractListenerSignatureOutput, error)); ok { + return rf(ctx, listener) + } + if rf, ok := ret.Get(0).(func(context.Context, *core.ContractListenerInput) *core.ContractListenerSignatureOutput); ok { + r0 = rf(ctx, listener) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*core.ContractListenerSignatureOutput) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *core.ContractListenerInput) error); ok { + r1 = rf(ctx, listener) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // DeleteContractAPI provides a mock function with given fields: ctx, apiName func (_m *Manager) DeleteContractAPI(ctx context.Context, apiName string) error { ret := _m.Called(ctx, apiName) diff --git a/mocks/databasemocks/plugin.go b/mocks/databasemocks/plugin.go index 7aa75a825..75f9abae4 100644 --- a/mocks/databasemocks/plugin.go +++ b/mocks/databasemocks/plugin.go @@ -3709,6 +3709,24 @@ func (_m *Plugin) UpsertContractAPI(ctx context.Context, api *core.ContractAPI, return r0 } +// UpsertContractListener provides a mock function with given fields: ctx, sub, allowExisting +func (_m *Plugin) UpsertContractListener(ctx context.Context, sub *core.ContractListener, allowExisting bool) error { + ret := _m.Called(ctx, sub, allowExisting) + + if len(ret) == 0 { + panic("no return value specified for UpsertContractListener") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *core.ContractListener, bool) error); ok { + r0 = rf(ctx, sub, allowExisting) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // UpsertData provides a mock function with given fields: ctx, data, optimization func (_m *Plugin) UpsertData(ctx context.Context, data *core.Data, optimization database.UpsertOptimization) error { ret := _m.Called(ctx, data, optimization) diff --git a/pkg/blockchain/plugin.go b/pkg/blockchain/plugin.go index 7b022fff5..db0c3b09e 100644 --- a/pkg/blockchain/plugin.go +++ b/pkg/blockchain/plugin.go @@ -115,7 +115,13 @@ type Plugin interface { NormalizeContractLocation(ctx context.Context, ntype NormalizeType, location *fftypes.JSONAny) (*fftypes.JSONAny, error) // GenerateEventSignature generates a strigified signature for the event, incorporating any fields significant to identifying the event as unique - GenerateEventSignature(ctx context.Context, event *fftypes.FFIEventDefinition) string + GenerateEventSignature(ctx context.Context, event *fftypes.FFIEventDefinition) (string, error) + + // GenerateEventSignatureWithLocation generates a strigified signature for the event , incorporating any fields significant to identifying the event as unique and the location + GenerateEventSignatureWithLocation(ctx context.Context, event *fftypes.FFIEventDefinition, location *fftypes.JSONAny) (string, error) + + // CompareEventSignatures will compare both signatures return true if they overlap + CheckOverlappingLocations(ctx context.Context, left *fftypes.JSONAny, right *fftypes.JSONAny) (bool, error) // GenerateErrorSignature generates a strigified signature for the custom error, incorporating any fields significant to identifying the error as unique GenerateErrorSignature(ctx context.Context, errorDef *fftypes.FFIErrorDefinition) string diff --git a/pkg/core/contract_listener.go b/pkg/core/contract_listener.go index fb3fb1241..1eae67923 100644 --- a/pkg/core/contract_listener.go +++ b/pkg/core/contract_listener.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Kaleido, Inc. +// Copyright © 2024 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -33,10 +33,11 @@ type ContractListener struct { BackendID string `ffstruct:"ContractListener" json:"backendId,omitempty" ffexcludeinput:"true"` Location *fftypes.JSONAny `ffstruct:"ContractListener" json:"location,omitempty"` Created *fftypes.FFTime `ffstruct:"ContractListener" json:"created,omitempty" ffexcludeinput:"true"` - Event *FFISerializedEvent `ffstruct:"ContractListener" json:"event,omitempty" ffexcludeinput:"postContractAPIListeners"` - Signature string `ffstruct:"ContractListener" json:"signature" ffexcludeinput:"true"` + Event *FFISerializedEvent `ffstruct:"ContractListener" json:"event,omitempty"` + Signature string `ffstruct:"ContractListener" json:"signature,omitempty" ffexcludeinput:"true"` Topic string `ffstruct:"ContractListener" json:"topic,omitempty"` Options *ContractListenerOptions `ffstruct:"ContractListener" json:"options,omitempty"` + Filters ListenerFilters `ffstruct:"ContractListener" json:"filters,omitempty" ffexcludeinput:"postContractAPIListeners"` } type ContractListenerWithStatus struct { @@ -53,9 +54,29 @@ type ListenerStatusError struct { type ContractListenerInput struct { ContractListener - EventPath string `ffstruct:"ContractListener" json:"eventPath,omitempty"` + Filters ListenerFiltersInput `ffstruct:"ContractListener" json:"filters,omitempty"` + EventPath string `ffstruct:"ContractListener" json:"eventPath,omitempty"` } +type ContractListenerSignatureOutput struct { + Signature string `ffstruct:"ContractListener" json:"signature,omitempty" ffexcludeinput:"true"` +} + +type ListenerFilter struct { + Event *FFISerializedEvent `ffstruct:"ListenerFilter" json:"event,omitempty"` + Location *fftypes.JSONAny `ffstruct:"ListenerFilter" json:"location,omitempty"` + Interface *fftypes.FFIReference `ffstruct:"ListenerFilter" json:"interface,omitempty" ffexcludeinput:"postContractAPIListeners"` + Signature string `ffstruct:"ListenerFilter" json:"signature" ffexcludeinput:"true"` +} + +type ListenerFilterInput struct { + ListenerFilter + EventPath string `ffstruct:"ListenerFilter" json:"eventPath,omitempty"` +} + +type ListenerFilters []*ListenerFilter +type ListenerFiltersInput []*ListenerFilterInput + type FFISerializedEvent struct { fftypes.FFIEventDefinition } @@ -99,3 +120,23 @@ func (o ContractListenerOptions) Value() (driver.Value, error) { bytes, _ := json.Marshal(o) return bytes, nil } + +// Scan implements sql.Scanner +func (lf *ListenerFilters) Scan(src interface{}) error { + switch src := src.(type) { + case nil: + lf = nil + return nil + case string: + return json.Unmarshal([]byte(src), &lf) + case []byte: + return json.Unmarshal(src, &lf) + default: + return i18n.NewError(context.Background(), i18n.MsgTypeRestoreFailed, src, lf) + } +} + +func (lf ListenerFilters) Value() (driver.Value, error) { + bytes, _ := json.Marshal(lf) + return bytes, nil +} diff --git a/pkg/core/contract_listener_test.go b/pkg/core/contract_listener_test.go index fbbaf88ca..cb5566b25 100644 --- a/pkg/core/contract_listener_test.go +++ b/pkg/core/contract_listener_test.go @@ -96,3 +96,37 @@ func TestContractListenerOptionsValue(t *testing.T) { assert.NoError(t, err) assert.Equal(t, `{"firstEvent":"newest"}`, string(val.([]byte))) } + +func TestListenerFiltersScan(t *testing.T) { + filters := ListenerFilters{} + err := filters.Scan([]byte(`[{"event":{"name":"event1","description":"asuperevent","params":[{"name":"value","schema":{"type":"integer","details":{"type":"uint256","internalType":"uint256"}}}]},"location":{"address":"0x1234"}}]`)) + assert.NoError(t, err) +} + +func TestListenerFiltersScanNil(t *testing.T) { + params := &ListenerFilters{} + err := params.Scan(nil) + assert.Nil(t, err) +} + +func TestListenerFiltersScanString(t *testing.T) { + params := &ListenerFilters{} + err := params.Scan(`[{"event":{"name":"event1","description":"asuperevent","params":[{"name":"value","schema":{"type":"integer","details":{"type":"uint256","internalType":"uint256"}}}]},"location":{"address":"0x1234"},"signature":"changed"}]`) + assert.NoError(t, err) +} + +func TestListenerFiltersScanError(t *testing.T) { + params := &ListenerFilters{} + err := params.Scan(map[string]interface{}{"this is": "not a supported serialization of a FFISerializedEvent"}) + assert.Regexp(t, "FF00105", err) +} + +func TestListenerFiltersValue(t *testing.T) { + filtersStr := `[{"event":{"name":"event1","description":"asuperevent","params":[{"name":"value","schema":{"type":"integer","details":{"type":"uint256","internalType":"uint256"}}}]},"location":{"address":"0x1234"},"signature":"changed"}]` + filters := ListenerFilters{} + err := filters.Scan(filtersStr) + assert.NoError(t, err) + value, err := filters.Value() + assert.NoError(t, err) + assert.Equal(t, filtersStr, string(value.([]byte))) +} diff --git a/pkg/database/plugin.go b/pkg/database/plugin.go index 367d2c963..1a6ec10a2 100644 --- a/pkg/database/plugin.go +++ b/pkg/database/plugin.go @@ -521,9 +521,12 @@ type iContractAPICollection interface { } type iContractListenerCollection interface { - // InsertContractListener - upsert a listener to an external smart contract + // InsertContractListener - insert a listener to an external smart contract InsertContractListener(ctx context.Context, sub *core.ContractListener) (err error) + // UpsertContractListener - upsert a listener to an external smart contract + UpsertContractListener(ctx context.Context, sub *core.ContractListener, allowExisting bool) (err error) + // UpdateContractListener - update contract listener by id UpdateContractListener(ctx context.Context, namespace string, id *fftypes.UUID, update ffapi.Update) (err error) @@ -1060,6 +1063,7 @@ var ContractListenerQueryFactory = &ffapi.QueryFields{ "created": &ffapi.TimeField{}, "updated": &ffapi.TimeField{}, "state": &ffapi.JSONField{}, + "filters": &ffapi.JSONField{}, } // BlockchainEventQueryFactory filter fields for contract events