diff --git a/doc-site/docs/reference/events.md b/doc-site/docs/reference/events.md index faf51cc39..ee3ba1f71 100644 --- a/doc-site/docs/reference/events.md +++ b/doc-site/docs/reference/events.md @@ -187,6 +187,9 @@ Once you have configured the blockchain event listener, every event detected from the blockchain will result in a FireFly event delivered to your application of type `blockchain_event_received`. +As of 1.3.1 a group of event filters can be established under a single topic when supported by the connector, which has benefits for ordering. +See [Contract Listeners](../reference/types/contractlistener.md) for more detail + Check out the [Custom Contracts Tutorial](../tutorials/custom_contracts/index.md) for a walk-through of how to set up listeners for the events from your smart contracts. diff --git a/doc-site/docs/reference/types/_includes/contractlistener_description.md b/doc-site/docs/reference/types/_includes/contractlistener_description.md index e6c1721b8..5027b71ad 100644 --- a/doc-site/docs/reference/types/_includes/contractlistener_description.md +++ b/doc-site/docs/reference/types/_includes/contractlistener_description.md @@ -4,3 +4,144 @@ of the interface for that event. Check out the [Custom Contracts Tutorial](../../tutorials/custom_contracts/index.md) for a walk-through of how to set up listeners for the events from your smart contracts. + +See below for a deep dive into the format of contract listeners and important concepts to understand when managing them. + +### Event filters + +#### Multiple filters + +From v1.3.1 onwards, a contract listener can be created with multiple filters under a single topic, when supported by the connector. Each filter contains: + +- a reference to a specific blockchain event to listen for +- (optional) a specific location/address to listen from +- a connector-specific signature (generated from the event and the location) + +In addition to this list of multiple filters, the listener specifies a single `topic` to identify the stream of events. + +Creating a single listener that listens for multiple events will allow for the easiest management of listeners, and for strong ordering of the events that they process. + +#### Single filter + +Before v1.3.1, each contract listener would only support listening to one specific event from a contract interface. Each listener would be comprised of: + +- a reference to a specific blockchain event to listen for +- (optional) a specific location/address to listen from +- a connector-specific signature (generated from the event), which allows you to easily identify and search for the contact listener for an event +- a `topic` which determines the ordered stream that these events are part of + +For backwards compatibility, this format is still supported by the API. + +### Signature strings + +#### String format + +Each filter is identified by a generated `signature` that matches a single event, and each contract listener is identified by a `signature` computed from its filters. + +Ethereum provides a string standard for event signatures, of the form `EventName(uint256,bytes)`. Prior to v1.3.1, the signature of each Ethereum contract listener would exactly follow this Ethereum format. + +As of v1.3.1, Ethereum signature strings have been changed, because this format does not fully describe the event - particularly because each top-level parameter can in the ABI definition be marked as `indexed`. For example, while the following two Solidity events have the same signature, they are serialized differently due to the different placement of `indexed` parameters, and thus a listener must define both individually to be able to process them: + +- ERC-20 `Transfer` + + ```solidity + event Transfer(address indexed _from, address indexed _to, uint256 _value) + ``` + +- ERC-721 `Transfer` + + ```solidity + event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); + ``` + +The two above are now expressed in the following manner by the Ethereum blockchain connector: + +```solidity +Transfer(address,address,uint256) [i=0,1] +Transfer(address,address,uint256) [i=0,1,2] +``` + +The `[i=]` listing at the end of the signature indicates the position of all parameters that are marked as `indexed`. + +Building on the blockchain-specific signature format for each event, FireFly will then compute the final signature for each filter and each contract listener as follows: + +- Each filter signature is a combination of the location and the specific connector event signature, such as `0xa5ea5d0a6b2eaf194716f0cc73981939dca26da1:Changed(address,uint256) [i=0]` +- Each contract listener signature is a concatenation of all the filter signatures, separated by `;` + +#### Duplicate filters + +FireFly restricts the creation of a contract listener containing duplicate filters. + +This includes the special case where one filter is a superset of another filter, due to a wildcard location. + +For example, if two filters are listening to the same event, but one has specified a location and the other hasn't, then the latter will be a superset, and already be listening to all the events matching the first filter. Creation of duplicate or superset filters within a single listener will be blocked. + +#### Duplicate listeners + +As noted above, each listener has a generated signature. This signature - containing all the locations and event signatures combined with the listener topic - will guarantee uniqueness of the contract listener. If you tried to create the same listener again, you would receive HTTP 409. This combination can allow a developer to assert that their listener exists, without the risk of creating duplicates. + +**Note:** Prior to v1.3.1, FireFly would detect duplicates simply by requiring a unique combination of signature + topic + location for each listener. The updated behavior for the listener signature is intended to preserve similar functionality, even when dealing with listeners that contain many event filters. + +### Backwards compatibility + +As noted throughout this document, the behavior of listeners is changed in v1.3.1. However, the following behaviors are retained for backwards-compatibility, to ensure that code written prior to v1.3.1 should continue to function. + +- The response from all query APIs of `listeners` will continue to populate top-level `event` and `location` fields + - The first entry from the `filters` array is duplicated to these fields +- On input to create a new `listener`, the `event` and `location` fields are still supported + - They function identically to supplying a `filters` array with a single entry +- The `signature` field is preserved at the listener level + - The format has been changed as described above + +### Input formats + +The two input formats supported when creating a contract listener are shown below. + +**Muliple Filters** + +```json +{ + "filters": [ + { + "interface": { + "id": "8bdd27a5-67c1-4960-8d1e-7aa31b9084d3" + }, + "location": { + "address": "0xa5ea5d0a6b2eaf194716f0cc73981939dca26da1" + }, + "eventPath": "Changed" + }, + { + "interface": { + "id": "8bdd27a5-67c1-4960-8d1e-7aa31b9084d3" + }, + "location": { + "address": "0xa4ea5d0b6b2eaf194716f0cc73981939dca27da1" + }, + "eventPath": "AnotherEvent" + } + ], + "options": { + "firstEvent": "newest" + }, + "topic": "simple-storage" +} +``` + +**One filter (old format)** + +```json +{ + "interface": { + "id": "8bdd27a5-67c1-4960-8d1e-7aa31b9084d3" + }, + "location": { + "address": "0xa5ea5d0a6b2eaf194716f0cc73981939dca26da1" + }, + "eventPath": "Changed", + "options": { + "firstEvent": "newest" + }, + "topic": "simple-storage" +} +``` diff --git a/doc-site/docs/reference/types/contractlistener.md b/doc-site/docs/reference/types/contractlistener.md index 68c2be28d..9b5b13415 100644 --- a/doc-site/docs/reference/types/contractlistener.md +++ b/doc-site/docs/reference/types/contractlistener.md @@ -34,11 +34,35 @@ title: ContractListener } ] }, - "signature": "Changed(uint256)", + "signature": "0x596003a91a97757ef1916c8d6c0d42592630d2cf:Changed(uint256)", "topic": "app1_topic", "options": { "firstEvent": "newest" - } + }, + "filters": [ + { + "event": { + "name": "Changed", + "description": "", + "params": [ + { + "name": "x", + "schema": { + "type": "integer", + "details": { + "type": "uint256", + "internalType": "uint256" + } + } + } + ] + }, + "location": { + "address": "0x596003a91a97757ef1916c8d6c0d42592630d2cf" + }, + "signature": "0x596003a91a97757ef1916c8d6c0d42592630d2cf:Changed(uint256)" + } + ] } ``` diff --git a/doc-site/docs/releasenotes/index.md b/doc-site/docs/releasenotes/index.md index 01abc7415..40b349b7a 100644 --- a/doc-site/docs/releasenotes/index.md +++ b/doc-site/docs/releasenotes/index.md @@ -4,6 +4,14 @@ title: Release Notes [Full release notes](https://github.com/hyperledger/firefly/releases) +## [v1.3.1 - Aug 5, 2024](https://github.com/hyperledger/firefly/releases/tag/v1.3.1) + +What's New: + +- Enable contract listeners with multiple filters + See [Contract Listeners](../reference/types/contractlistener.md) for details +- New multiparty status API at `/status/multiparty` + ## [v1.3.0 - April 25, 2024](https://github.com/hyperledger/firefly/releases/tag/v1.1.0) [Migration guide](1.3_migration_guide.md) diff --git a/doc-site/docs/tutorials/chains/index.md b/doc-site/docs/tutorials/chains/index.md deleted file mode 100644 index 792e6a71e..000000000 --- a/doc-site/docs/tutorials/chains/index.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Connecting to other blockchains ---- - -Write interesting stuff here diff --git a/doc-site/docs/tutorials/custom_contracts/ethereum.md b/doc-site/docs/tutorials/custom_contracts/ethereum.md index 52e91f936..26de3d47c 100644 --- a/doc-site/docs/tutorials/custom_contracts/ethereum.md +++ b/doc-site/docs/tutorials/custom_contracts/ethereum.md @@ -648,7 +648,7 @@ Here is an example of sending 100 wei with a transaction: Now that we've seen how to submit transactions and preform read-only queries to the blockchain, let's look at how to receive blockchain events so we know when things are happening in realtime. -If you look at the source code for the smart contract we're working with above, you'll notice that it emits an event when the stored value of the integer is set. In order to receive these events, we first need to instruct FireFly to listen for this specific type of blockchain event. To do this, we create an **Event Listener**. The `/contracts/listeners` endpoint is RESTful so there are `POST`, `GET`, and `DELETE` methods available on it. To create a new listener, we will make a `POST` request. We are going to tell FireFly to listen to events with name `"Changed"` from the FireFly Interface we defined earlier, referenced by its ID. We will also tell FireFly which contract address we expect to emit these events, and the topic to assign these events to. Topics are a way for applications to subscribe to events they are interested in. +If you look at the source code for the smart contract we're working with above, you'll notice that it emits an event when the stored value of the integer is set. In order to receive these events, we first need to instruct FireFly to listen for this specific type of blockchain event. To do this, we create an **Event Listener**. The `/contracts/listeners` endpoint is RESTful so there are `POST`, `GET`, and `DELETE` methods available on it. To create a new listener, we will make a `POST` request. We are going to tell FireFly to listen to events with name `"Changed"` from the FireFly Interface we defined earlier, referenced by its ID. We will also tell FireFly which contract address we expect to emit these events, and the topic to assign these events to. You can specify multiple filters for a listener, in this case we only specify one for our event. Topics are a way for applications to subscribe to events they are interested in. ### Request @@ -656,13 +656,17 @@ If you look at the source code for the smart contract we're working with above, ```json { - "interface": { - "id": "8bdd27a5-67c1-4960-8d1e-7aa31b9084d3" - }, - "location": { - "address": "0xa5ea5d0a6b2eaf194716f0cc73981939dca26da1" - }, - "eventPath": "Changed", + "filters": [ + { + "interface": { + "id": "8bdd27a5-67c1-4960-8d1e-7aa31b9084d3" + }, + "location": { + "address": "0xa5ea5d0a6b2eaf194716f0cc73981939dca26da1" + }, + "eventPath": "Changed" + } + ], "options": { "firstEvent": "newest" }, @@ -674,17 +678,17 @@ If you look at the source code for the smart contract we're working with above, ```json { - "id": "1bfa3b0f-3d90-403e-94a4-af978d8c5b14", + "id": "e7c8457f-4ffd-42eb-ac11-4ad8aed30de1", "interface": { - "id": "8bdd27a5-67c1-4960-8d1e-7aa31b9084d3" + "id": "55fdb62a-fefc-4313-99e4-e3f95fcca5f0" }, "namespace": "default", - "name": "sb-66209ffc-d355-4ac0-7151-bc82490ca9df", - "protocolId": "sb-66209ffc-d355-4ac0-7151-bc82490ca9df", + "name": "019104d7-bb0a-c008-76a9-8cb923d91b37", + "backendId": "019104d7-bb0a-c008-76a9-8cb923d91b37", "location": { "address": "0xa5ea5d0a6b2eaf194716f0cc73981939dca26da1" }, - "created": "2022-02-17T22:02:36.34549538Z", + "created": "2024-07-30T18:12:12.704964Z", "event": { "name": "Changed", "description": "", @@ -712,9 +716,49 @@ If you look at the source code for the smart contract we're working with above, } ] }, + "signature": "0xa5ea5d0a6b2eaf194716f0cc73981939dca26da1:Changed(address,uint256) [i=0]", + "topic": "simple-storage", "options": { - "firstEvent": "oldest" - } + "firstEvent": "newest" + }, + "filters": [ + { + "event": { + "name": "Changed", + "description": "", + "params": [ + { + "name": "from", + "schema": { + "type": "string", + "details": { + "type": "address", + "internalType": "address", + "indexed": true + } + } + }, + { + "name": "value", + "schema": { + "type": "integer", + "details": { + "type": "uint256", + "internalType": "uint256" + } + } + } + ] + }, + "location": { + "address": "0xa5ea5d0a6b2eaf194716f0cc73981939dca26da1" + }, + "interface": { + "id": "55fdb62a-fefc-4313-99e4-e3f95fcca5f0" + }, + "signature": "0xa5ea5d0a6b2eaf194716f0cc73981939dca26da1:Changed(address,uint256) [i=0]" + } + ] } ``` diff --git a/doc-site/docs/tutorials/custom_contracts/fabric.md b/doc-site/docs/tutorials/custom_contracts/fabric.md index 2b4723ca1..037a3a19e 100644 --- a/doc-site/docs/tutorials/custom_contracts/fabric.md +++ b/doc-site/docs/tutorials/custom_contracts/fabric.md @@ -576,16 +576,20 @@ The `/contracts/listeners` endpoint is RESTful so there are `POST`, `GET`, and ` ```json { - "interface": { - "id": "f1e5522c-59a5-4787-bbfd-89975e5b0954" - }, - "location": { - "channel": "firefly", - "chaincode": "asset_transfer" - }, - "event": { - "name": "AssetCreated" - }, + "filters": [ + { + "interface": { + "id": "f1e5522c-59a5-4787-bbfd-89975e5b0954" + }, + "location": { + "channel": "firefly", + "chaincode": "asset_transfer" + }, + "event": { + "name": "AssetCreated" + } + } + ], "options": { "firstEvent": "oldest" }, @@ -597,25 +601,39 @@ The `/contracts/listeners` endpoint is RESTful so there are `POST`, `GET`, and ` ```json { - "id": "6e7f5dd8-5a57-4163-a1d2-5654e784dc31", + "id": "d6b5e774-c9e5-474c-9495-ec07fa47a907", "namespace": "default", - "name": "sb-2cac2bfa-38af-4408-4ff3-973421410e5d", - "backendId": "sb-2cac2bfa-38af-4408-4ff3-973421410e5d", + "name": "sb-44aa348a-bafb-4243-594e-dcad689f1032", + "backendId": "sb-44aa348a-bafb-4243-594e-dcad689f1032", "location": { "channel": "firefly", "chaincode": "asset_transfer" }, - "created": "2022-05-02T17:19:13.144561086Z", + "created": "2024-07-22T15:36:58.514085959Z", "event": { "name": "AssetCreated", "description": "", "params": null }, - "signature": "AssetCreated", + "signature": "firefly-asset_transfer:AssetCreated", "topic": "assets", "options": { "firstEvent": "oldest" - } + }, + "filters": [ + { + "event": { + "name": "AssetCreated", + "description": "", + "params": null + }, + "location": { + "channel": "firefly", + "chaincode": "asset_transfer" + }, + "signature": "firefly-asset_transfer:AssetCreated" + } + ] } ``` diff --git a/internal/reference/reference.go b/internal/reference/reference.go index 28fc1346d..699c6aab1 100644 --- a/internal/reference/reference.go +++ b/internal/reference/reference.go @@ -389,11 +389,36 @@ func GenerateObjectsReferenceMarkdown(ctx context.Context) (map[string][]byte, e }, }, }, - Signature: "Changed(uint256)", + Signature: "0x596003a91a97757ef1916c8d6c0d42592630d2cf:Changed(uint256)", Topic: "app1_topic", Options: &core.ContractListenerOptions{ FirstEvent: "newest", }, + Filters: core.ListenerFilters{ + { + Event: &core.FFISerializedEvent{ + FFIEventDefinition: fftypes.FFIEventDefinition{ + Name: "Changed", + Params: fftypes.FFIParams{ + { + Name: "x", + Schema: fftypes.JSONAnyPtr(`{ + "type": "integer", + "details": { + "type": "uint256", + "internalType": "uint256" + } + }`), + }, + }, + }, + }, + Signature: "0x596003a91a97757ef1916c8d6c0d42592630d2cf:Changed(uint256)", + Location: fftypes.JSONAnyPtr(`{ + "address": "0x596003a91a97757ef1916c8d6c0d42592630d2cf" + }`), + }, + }, }, &core.TokenPool{