diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
new file mode 100644
index 0000000000..6387af06aa
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug-report.yml
@@ -0,0 +1,62 @@
+name: ๐ Bug
+description: File a bug/issue
+title: "bug: "
+labels: [bug]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ _The more information you share, the faster we can help you._
+ Prior to opening the issue, please make sure that you:
+ - Use English (EN/US) to communicate.
+ - Search the [open issues](https://github.com/hyperledger/firefly/issues) to avoid duplicating the issue.
+
+ - type: textarea
+ id: problem
+ attributes:
+ label: What happened?
+ description: |
+ Please provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner.
+ If this matter is security related, please disclose it privately via security@lfdecentralizedtrust.org
+ validations:
+ required: true
+
+ - type: textarea
+ id: expected
+ attributes:
+ label: What did you expect to happen?
+ validations:
+ required: true
+
+ - type: textarea
+ id: repro
+ attributes:
+ label: How can we reproduce it (as minimally and precisely as possible)?
+ validations:
+ required: true
+
+ - type: textarea
+ id: additional
+ attributes:
+ label: Anything else we need to know?
+
+ - type: textarea
+ id: osVersion
+ attributes:
+ label: OS version
+ value: |
+
+
+ ```console
+ # On Linux:
+ $ cat /etc/os-release
+ # paste output here
+ $ uname -a
+ # paste output here
+
+ # On Windows:
+ C:\> wmic os get Caption, Version, BuildNumber, OSArchitecture
+ # paste output here
+ ```
+
+
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000..51cdb2da77
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Support Request
+ url: https://discord.gg/hyperledger
+ about: Support request or question relating to Hyperledger Firefly
diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml
new file mode 100644
index 0000000000..7ba36d42b2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/enhancement.yml
@@ -0,0 +1,27 @@
+name: Enhancement Tracking Issue
+description: Provide supporting details for an enhancement
+labels: [enhancement]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ _The more information you share, the faster we can help you._
+ Prior to opening the issue, please make sure that you:
+ - Use English (EN/US) to communicate.
+ - Search the [open issues](https://github.com/hyperledger/firefly/issues) to avoid duplicating the issue.
+
+ - type: textarea
+ id: enhancement
+ attributes:
+ label: What would you like to be added?
+ description: |
+ A clear and concise description of what you want to happen.
+ validations:
+ required: true
+
+ - type: textarea
+ id: rationale
+ attributes:
+ label: Why is this needed?
+ validations:
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/improve_docs.yml b/.github/ISSUE_TEMPLATE/improve_docs.yml
new file mode 100644
index 0000000000..13220bcdc2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/improve_docs.yml
@@ -0,0 +1,31 @@
+name: "Documentation Issue"
+description: Issues related to documentation.
+title: "docs: "
+labels: [documentation]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ _The more information you share, the faster we can help you._
+ Prior to opening the issue, please make sure that you:
+ - Use English (EN/US) to communicate.
+ - Search the [open issues](https://github.com/hyperledger/firefly/issues) to avoid duplicating the issue.
+
+ - type: textarea
+ id: current-state
+ attributes:
+ label: Current State
+ description: Describe the current state of the documentation.
+ placeholder: |
+ The documentation for the API in this page (url) is missing ...
+ validations:
+ required: true
+ - type: textarea
+ id: desired-state
+ attributes:
+ label: Desired State
+ description: Describe the desired state the documentation should be in.
+ placeholder: |
+ Add here ...
+ validations:
+ required: true
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000000..e883a79c06
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,46 @@
+
+
+## Proposed changes
+
+Please include a summary of the changes here and why we need those changes. And also let us know which issue is fixed.
+
+Fixes #
+
+
+
+## Types of changes
+
+
+
+
+
+- [ ] Bug fix
+- [ ] New feature added
+- [ ] Documentation Update
+
+
+
+## Please make sure to follow these points
+
+
+
+
+
+- [ ] I have read the contributing guidelines.
+- [ ] I have performed a self-review of my own code or work.
+- [ ] I have commented my code, particularly in hard-to-understand areas.
+- [ ] My changes generates no new warnings.
+- [ ] My Pull Request title is in format < issue name >
eg Added links in the documentation
.
+- [ ] I have added tests that prove my fix is effective or that my feature works.
+- [ ] My changes have sufficient code coverage (unit, integration, e2e tests).
+
+
+
+## Screenshots (If Applicable)
+
+
+
+
+## Other Information
+
+Any message for the reviewer or kick off the discussion by explaining why you considered this particular solution, any alternatives etc.
diff --git a/.github/workflows/docker_main.yml b/.github/workflows/docker_main.yml
index 6b360ed85a..94eadaf729 100644
--- a/.github/workflows/docker_main.yml
+++ b/.github/workflows/docker_main.yml
@@ -4,10 +4,16 @@ on:
push:
branches:
- main
+ paths-ignore:
+ - '.github/**' # exclude .github directory
+ - '**.md' # exclude all markdown files
jobs:
docker:
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
steps:
- uses: actions/checkout@v3
with:
diff --git a/.github/workflows/docker_release.yml b/.github/workflows/docker_release.yml
index b58c9de961..11a5c311a0 100644
--- a/.github/workflows/docker_release.yml
+++ b/.github/workflows/docker_release.yml
@@ -5,13 +5,25 @@ on:
types: [released, prereleased]
jobs:
+
docker:
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
with:
fetch-depth: 0
+ - name: Get the latest tag
+ id: get_latest_tag
+ run: |
+ git fetch --tags
+ latest_tag=$(git tag -l | sort -V | tail -n 1)
+ echo "latest tag: $latest_tag"
+ echo "LATEST_TAG=$latest_tag" >> $GITHUB_ENV
+
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
@@ -25,9 +37,9 @@ jobs:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
-
+
- name: Set latest tag
- if: github.event.action == 'released'
+ if: github.event.action == 'released' && github.ref_name == env.LATEST_TAG
run: |
echo "DOCKER_TAGS=${{ env.DOCKER_TAGS }},ghcr.io/${{ github.repository }}:latest" >> $GITHUB_ENV
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 83dbdae578..b37a48995e 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -23,32 +23,24 @@ jobs:
git config --global user.name "GitHub Actions"
git config --global user.email "noreply@github.com"
- - name: Check if this is the latest release
+ - name: Get the latest tag
run: |
- LATEST_TAG=$(
- curl -L \
- -H "Accept: application/vnd.github+json" \
- -H "Authorization: Bearer ${{ github.token }}" \
- -H "X-GitHub-Api-Version: 2022-11-28" \
- https://api.github.com/repos/${{ github.repository }}/releases/latest \
- | jq -r '.tag_name'
- )
- IS_LATEST=${{ env.LATEST_TAG == github.event.release.tag_name }}
- echo This release is: "${{ github.event.release.tag_name }}"
- echo The latest release is: "$LATEST_TAG"
- echo "IS_LATEST_RELEASE=$IS_LATEST" >> "$GITHUB_ENV"
+ git fetch --tags
+ latest_tag=$(git tag -l | sort -V | grep -v "rc" | tail -n 1)
+ echo "latest tag: $latest_tag"
+ echo "LATEST_TAG=$latest_tag" >> $GITHUB_ENV
- name: Install docs dependencies
working-directory: doc-site
run: pip install -r requirements.txt
- name: Update doc site for release
- if: ${{ github.event_name == 'release' && env.IS_LATEST_RELEASE != 'true' }}
+ if: github.event.action == 'released' && github.ref_name != env.LATEST_TAG
working-directory: doc-site
run: mike deploy ${{ github.event.release.tag_name }} --push
- name: Update doc site for latest release
- if: ${{ github.event_name == 'release' && env.IS_LATEST_RELEASE == 'true' }}
+ if: github.event.action == 'released' && github.ref_name == env.LATEST_TAG
working-directory: doc-site
run: mike deploy ${{ github.event.release.tag_name }} latest -u --push
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 139064f6a6..b3a754fea4 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -3,8 +3,19 @@ name: Go
on:
push:
branches: [main]
+ paths:
+ - '**' # include all files
+ - '!.github/**' # exclude .github directory
+ - '!**.md' # exclude all markdown files
+ - 'doc-site/docs/reference/**.md' # include markdown files that are auto generated and need to be tested
+
pull_request:
- branches: [main]
+ paths:
+ - '**' # include all files
+ - '!.github/**' # exclude .github directory
+ - '!**.md' # exclude all markdown files
+ - 'doc-site/docs/reference/**.md' # include markdown files that are auto generated and need to be tested
+
workflow_dispatch:
jobs:
@@ -20,13 +31,15 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
- go-version: 1.21
+ go-version: 1.22
- name: Build and Test
run: make
- - name: Upload coverage
- run: bash <(curl -s https://codecov.io/bash)
+ - uses: codecov/codecov-action@v4
+ with:
+ codecov_yml_path: ./codecov.yml
+ token: ${{ secrets.CODECOV_TOKEN }}
docker:
runs-on: ubuntu-latest
@@ -38,7 +51,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
- go-version: 1.21
+ go-version: 1.22
- name: Build Docker image
run: make docker
@@ -47,7 +60,7 @@ jobs:
run: docker save --output firefly.tar.gz hyperledger/firefly
- name: Upload Docker image
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: firefly-docker
path: firefly.tar.gz
@@ -130,10 +143,10 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
- go-version: 1.21
+ go-version: 1.22
- name: Download Docker image
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@v4
with:
name: firefly-docker
@@ -152,7 +165,7 @@ jobs:
run: ./test/e2e/run.sh
- name: Archive container logs
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: always()
with:
name: container-logs-${{ matrix.test-suite }}-${{ matrix.blockchain-provider }}-${{ matrix.blockchain-connector }}-${{ matrix.database-type }}-${{ matrix.token-provider }}
diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 8d3252bde8..cba43d3dfe 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -54,7 +54,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
- go-version: 1.21
+ go-version: 1.22
- name: Update manifest to latest commit for every service
run: ./manifestgen.sh head
@@ -75,7 +75,7 @@ jobs:
run: ./test/e2e/run.sh
- name: Archive container logs
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: always()
with:
name: container-logs-${{ matrix.test-suite }}-${{ matrix.blockchain-node }}-${{ matrix.database-type }}
@@ -91,7 +91,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
- go-version: 1.21
+ go-version: 1.22
- name: Update manifest to latest commit for every service
run: ./manifestgen.sh head
@@ -111,7 +111,7 @@ jobs:
run: ./test/e2e/run.sh
- name: Archive container logs
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
if: always()
with:
name: container-logs-TestEthereumV1MigrationE2ESuite-geth-postgres
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
new file mode 100644
index 0000000000..d8649cc3f7
--- /dev/null
+++ b/.github/workflows/scorecard.yml
@@ -0,0 +1,73 @@
+# This workflow uses actions that are not certified by GitHub. They are provided
+# by a third-party and are governed by separate terms of service, privacy
+# policy, and support documentation.
+
+name: Scorecard supply-chain security
+on:
+ # For Branch-Protection check. Only the default branch is supported. See
+ # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
+ branch_protection_rule:
+ # To guarantee Maintained check is occasionally updated. See
+ # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
+ schedule:
+ - cron: '24 7 * * 4'
+ push:
+ branches: [ "main" ]
+
+# Declare default permissions as read only.
+permissions: read-all
+
+jobs:
+ analysis:
+ name: Scorecard analysis
+ runs-on: ubuntu-latest
+ permissions:
+ # Needed to upload the results to code-scanning dashboard.
+ security-events: write
+ # Needed to publish results and get a badge (see publish_results below).
+ id-token: write
+ # Uncomment the permissions below if installing in a private repository.
+ # contents: read
+ # actions: read
+
+ steps:
+ - name: "Checkout code"
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ with:
+ persist-credentials: false
+
+ - name: "Run analysis"
+ uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
+ with:
+ results_file: results.sarif
+ results_format: sarif
+ # (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
+ # - you want to enable the Branch-Protection check on a *public* repository, or
+ # - you are installing Scorecard on a *private* repository
+ # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional.
+ repo_token: ${{ secrets.SCORECARD_TOKEN }}
+
+ # Public repositories:
+ # - Publish results to OpenSSF REST API for easy access by consumers
+ # - Allows the repository to include the Scorecard badge.
+ # - See https://github.com/ossf/scorecard-action#publishing-results.
+ # For private repositories:
+ # - `publish_results` will always be set to `false`, regardless
+ # of the value entered here.
+ publish_results: true
+
+ # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
+ # format to the repository Actions tab.
+ - name: "Upload artifact"
+ uses: actions/upload-artifact@v4
+ with:
+ name: SARIF file
+ path: results.sarif
+ retention-days: 5
+
+ # Upload the results to GitHub's code scanning dashboard (optional).
+ # Commenting out will disable upload of results to your repo's Code Scanning dashboard
+ - name: "Upload to code-scanning"
+ uses: github/codeql-action/upload-sarif@v3
+ with:
+ sarif_file: results.sarif
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f2b63ed3d4..6591fd2c19 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -4,7 +4,7 @@ We welcome contributions to the FireFly Project in many forms, and
there's always plenty to do!
Please visit the
-[contributors guide](https://hyperledger.github.io/firefly//contributors/index.html) in the
+[contributors guide](https://hyperledger.github.io/firefly/latest/contributors/index.html) in the
docs to learn how to make contributions to this exciting project.
This work is licensed under a Creative Commons Attribution 4.0 International License.
diff --git a/Dockerfile b/Dockerfile
index 57c298b54b..4c857ea475 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,19 +1,24 @@
+# ARG Definitions
+# Consider adding default values for the ARGs based on this warning:
+# https://github.com/hyperledger/firefly/actions/runs/10795366695/job/29941873807#step:4:171
ARG FIREFLY_BUILDER_TAG
ARG FABRIC_BUILDER_TAG
ARG FABRIC_BUILDER_PLATFORM
ARG SOLIDITY_BUILDER_TAG
ARG BASE_TAG
+
ARG BUILD_VERSION
ARG GIT_REF
+# Firefly Builder
FROM $FIREFLY_BUILDER_TAG AS firefly-builder
ARG BUILD_VERSION
ARG GIT_REF
RUN apk add make=4.4.1-r2 \
gcc=13.2.1_git20231014-r0 \
build-base=0.5-r3 \
- curl=8.5.0-r0 \
- git=2.43.4-r0
+ curl=8.11.1-r0 \
+ git=2.43.6-r0
WORKDIR /firefly
RUN chgrp -R 0 /firefly \
&& chmod -R g+rwX /firefly \
@@ -26,6 +31,7 @@ RUN go mod download
ADD --chown=1001:0 . .
RUN make build
+# Fabric Builder
FROM --platform=$FABRIC_BUILDER_PLATFORM $FABRIC_BUILDER_TAG AS fabric-builder
WORKDIR /firefly/smart_contracts/fabric/firefly-go
RUN chgrp -R 0 /firefly \
@@ -39,13 +45,13 @@ RUN GO111MODULE=on go mod vendor
WORKDIR /tmp/fabric
RUN curl https://github.com/hyperledger/fabric/releases/download/v2.3.2/hyperledger-fabric-linux-amd64-2.3.2.tar.gz -L --output hyperledger-fabric-linux-amd64-2.3.2.tar.gz
RUN tar -zxf hyperledger-fabric-linux-amd64-2.3.2.tar.gz
-ENV FABRIC_CFG_PATH /tmp/fabric/config/
+ENV FABRIC_CFG_PATH=/tmp/fabric/config/
RUN ./bin/peer lifecycle chaincode package /firefly/smart_contracts/fabric/firefly-go/firefly_fabric.tar.gz --path /firefly/smart_contracts/fabric/firefly-go --lang golang --label firefly_1.0
+# Solidity Builder
FROM $SOLIDITY_BUILDER_TAG AS solidity-builder
WORKDIR /firefly/solidity_firefly
-RUN chgrp -R 0 /firefly \
- && chmod -R g+rwX /firefly
+RUN chgrp -R 0 /firefly && chmod -R g+rwX /firefly
ADD --chown=1001:0 smart_contracts/ethereum/solidity_firefly/ .
USER 1001
RUN mkdir -p build/contracts \
@@ -54,21 +60,23 @@ RUN mkdir -p build/contracts \
&& cd ../build/contracts \
&& mv combined.json Firefly.json
-FROM alpine:3.19 AS SBOM
+# SBOM
+FROM alpine:3.19 AS sbom
WORKDIR /
ADD . /SBOM
-RUN apk add --no-cache curl
+RUN apk add --no-cache curl
RUN curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.48.3
RUN trivy fs --format spdx-json --output /sbom.spdx.json /SBOM
-RUN trivy sbom /sbom.spdx.json --severity UNKNOWN,HIGH,CRITICAL --exit-code 1
+RUN trivy sbom /sbom.spdx.json --severity UNKNOWN,HIGH,CRITICAL --db-repository public.ecr.aws/aquasecurity/trivy-db --exit-code 1
+# Final executable build
FROM $BASE_TAG
ARG UI_TAG
ARG UI_RELEASE
RUN apk add --update --no-cache \
sqlite=3.44.2-r0 \
- postgresql16-client=16.3-r0 \
- curl=8.5.0-r0 \
+ postgresql16-client=16.6-r0 \
+ curl=8.11.1-r0 \
jq=1.7.1-r0
WORKDIR /firefly
RUN chgrp -R 0 /firefly \
@@ -83,10 +91,10 @@ COPY --from=firefly-builder --chown=1001:0 /firefly/firefly ./firefly
COPY --from=firefly-builder --chown=1001:0 /firefly/db ./db
COPY --from=solidity-builder --chown=1001:0 /firefly/solidity_firefly/build/contracts ./contracts
COPY --from=fabric-builder --chown=1001:0 /firefly/smart_contracts/fabric/firefly-go/firefly_fabric.tar.gz ./contracts/firefly_fabric.tar.gz
-ENV UI_RELEASE https://github.com/hyperledger/firefly-ui/releases/download/$UI_TAG/$UI_RELEASE.tgz
+ENV UI_RELEASE=https://github.com/hyperledger/firefly-ui/releases/download/$UI_TAG/$UI_RELEASE.tgz
RUN mkdir /firefly/frontend \
&& curl -sLo - $UI_RELEASE | tar -C /firefly/frontend -zxvf -
-COPY --from=SBOM /sbom.spdx.json /sbom.spdx.json
+COPY --from=sbom /sbom.spdx.json /sbom.spdx.json
RUN ln -s /firefly/firefly /usr/bin/firefly
USER 1001
ENTRYPOINT [ "firefly" ]
diff --git a/MAINTAINERS.md b/MAINTAINERS.md
index aa40d41329..1ed30e66b4 100644
--- a/MAINTAINERS.md
+++ b/MAINTAINERS.md
@@ -5,7 +5,7 @@ The following is the list of current maintainers this repo:
| Name | GitHub | Email | LFID |
| ----------------- | --------------- | ---------------------------- | ----------------- |
| Peter Broadhurst | peterbroadhurst | peter.broadhurst@kaleido.io | peterbroadhurst |
-| Nicko Guyer | nguyer | nicko.guyer@kaleido.io | nguyer |
+| Enrique Lacal | enriquel8 | enrique.lacal@kaleido.io | enriquelacal |
| Andrew Richardson | awrichar | andrew.richardson@kaleido.io | Andrew.Richardson |
| Alex Shorsher | shorsher | alex.shorsher@kaleido.io | shorsher |
diff --git a/README.md b/README.md
index 6ebccdc7d8..6b260378a0 100644
--- a/README.md
+++ b/README.md
@@ -12,18 +12,21 @@ Hyperledger FireFly is the first open source Supernode: a complete stack for ent
The FireFly API for digital assets, data flows, and blockchain transactions makes it radically faster to build production-ready apps on popular chains and protocols.
+
+[ENGLISH](./README.md) | [็ฎไฝไธญๆ](./README_zh_CN.md)
+
## Start using Hyperledger FireFly
The best place to learn about FireFly is in the [documentation](https://hyperledger.github.io/firefly).
-There you will find our [Getting Started Guide](https://hyperledger.github.io/firefly/gettingstarted/),
+There you will find our [Getting Started Guide](https://hyperledger.github.io/firefly/latest/gettingstarted/),
which will get you a running FireFly network of Supernodes on your local machine in a few minutes.
Your development environment will come with:
FireFly CLI | FireFly Explorer UI | FireFly Sandbox |
:----------------------------:|:-----------------------------------:|:----------------:|
-![](./images/firefly_cli.png) | ![](./images/firefly_explorer.png) | ![](./images/firefly_sandbox.png) |
+[![CLI](./images/firefly_cli.png)](https://hyperledger.github.io/firefly/latest/gettingstarted/firefly_cli/#install-the-firefly-cli) | [![UI](./images/firefly_explorer.png)](https://github.com/hyperledger/firefly-ui) | [![Sandbox](./images/firefly_sandbox.png)](https://hyperledger.github.io/firefly/latest/gettingstarted/sandbox/#use-the-sandbox) |
## Engage with the community
@@ -40,11 +43,11 @@ all the plumbing for your blockchain application from scratch.
[![Hyperledger FireFly Architecture Overview](./doc-site/docs/images/firefly_architecture_overview.jpg)](https://raw.githubusercontent.com/kaleido-io/firefly/main/doc-site/docs/images/firefly_architecture_overview.jpg)
-## Start contributing to Hyperledger FireFy
+## Start contributing to Hyperledger FireFly
There are lots of places you can contribute, regardless of whether your skills are front-end, backend-end, or full-stack.
-Check out our [Contributor Guide](https://hyperledger.github.io/firefly/contributors/), and **welcome!**.
+Check out our [Contributor Guide](https://hyperledger.github.io/firefly/latest/contributors/), and **welcome!**.
## Other repos
@@ -297,7 +300,7 @@ Plugins: Each plugin comprises a Go shim, plus a remote agent microservice runti
โ
โ โโโโโโโโโโโโโโโโโ - Database Interactions
โโโโโโโโโโโโโค database [Di]โ * Create, Read, Update, Delete (CRUD) actions
- โ โ interace โ * Filtering and update definition interace
+ โ โ interface โ * Filtering and update definition interface
โ โโโโโโโฌโโโโโโโโโโ * Migrations and Indexes
โ โ
โ โโโโโโโโโโ ... extensible to NoSQL (CouchDB / MongoDB etc.)
diff --git a/README_zh_CN.md b/README_zh_CN.md
new file mode 100644
index 0000000000..6315982e77
--- /dev/null
+++ b/README_zh_CN.md
@@ -0,0 +1,346 @@
+# Hyperledger FireFly
+
+[![codecov](https://codecov.io/gh/hyperledger/firefly/branch/main/graph/badge.svg?token=QdEnpMqB1G)](https://codecov.io/gh/hyperledger/firefly)
+[![Go Report Card](https://goreportcard.com/badge/github.com/hyperledger/firefly)](https://goreportcard.com/report/github.com/hyperledger/firefly)
+[![FireFy Documentation](https://img.shields.io/static/v1?label=FireFly&message=documentation&color=informational)](https://hyperledger.github.io/firefly//)
+![build](https://github.com/hyperledger/firefly/actions/workflows/docker_main.yml/badge.svg?branch=main)
+[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/7826/badge)](https://www.bestpractices.dev/projects/7826)
+
+![Hyperledger FireFly](./images/hyperledger_firefly_logo.png)
+
+Hyperledger FireFly ๆฏ้ฆๆฌพๅผๆบ็่ถ
็บง่็น๏ผไธไธชๅฎๅ
จ็ไผไธ็บง็ๆๅปบๅๆๅฑWeb3ๅบ็จ็ๅ
จๆ ๅผ่งฃๅณๆนๆกใ
+
+FireFly ๆไพ็ๆฐๅญ่ตไบงใๆฐๆฎๆตๅๅบๅ้พไบคๆAPI๏ผไฝฟไผไธ่ฝๅคๅฟซ้ๅจๆต่ก็ๅบๅ้พๆๆฏๅๅ่ฎฎไธๆๅปบ็ไบงๅฐฑ็ปช็ๅบ็จ็จๅบใ
+
+[ENGLISH](./README.md) | [็ฎไฝไธญๆ](./README_zh_CN.md)
+
+## ๅผๅงไฝฟ็จ Hyperledger FireFly
+
+ไบ่งฃFireFly็ๆไฝณๆนๅผ่ฏทๅ็
ง[ๆๆกฃ](https://hyperledger.github.io/firefly)ใ
+
+ๆจๅฏไปฅๅจ่ฟ้ๆพๅฐๆไปฌ็[ๅ
ฅ้จๆๅ](https://hyperledger.github.io/firefly/latest/gettingstarted/),
+้่ฟ่ฏฅๆๅๅฐๅธฎๅฉๆจๅจๅ ๅ้ๅ
ๅจๆฌๅฐๆบๅจไธ่ฟ่ก่ตทๆฅไธไธชFireFly่ถ
็บง่็น็ฝ็ป็ๅผๅ็ฏๅขใ
+
+ๆจ็ๅผๅ็ฏๅขๅฐๅ
ๆฌ:
+
+FireFly CLI | FireFly Explorer UI | FireFly Sandbox |
+:----------------------------:|:-----------------------------------:|:----------------:|
+[![CLI](./images/firefly_cli.png)](https://hyperledger.github.io/firefly/latest/gettingstarted/firefly_cli/#install-the-firefly-cli) | [![UI](./images/firefly_explorer.png)](https://github.com/hyperledger/firefly-ui) | [![Sandbox](./images/firefly_sandbox.png)](https://hyperledger.github.io/firefly/latest/gettingstarted/sandbox/#use-the-sandbox) |
+
+## ๅ ๅ
ฅ็คพๅบ
+
+- [ๅ ๅ
ฅๆไปฌ็ Discord](https://discord.gg/hyperledger)
+
+## ๆๆฏๆถๆ
+
+Hyperledger FireFly ๆฅๆๅฏๆๆ็ๅพฎๆๅกๆถๆใๆ ่ฎบๆฏๅบๅ้พๅ่ฎฎใERCไปฃๅธๆ ๅใ่ชๅฎไนๆบ่ฝๅ็บฆ๏ผ่ฟๆฏไบไปถๅๅๅฑไปฅๅ็งๆๆฐๆฎๅบ๏ผไธๅ้ฝๅฏไปฅๆไปถๅใ
+
+ๅ ๆญค๏ผๅณไฝฟๆจๆ้่ฆ็ๅบๅ้พๆๆฏ็ฎๅ่ฟๆฒกๆ็ธๅฏนๅบ็ๆฏๆ๏ผๆจไนไธๅฟ
ๆ
ๅฟใๆไปถๅ็่ฎพ่ฎกๅคงๅคง้ไฝไบๆทปๅ ๆดๅค็ๅบๅ้พๆๆฏ็้พๅบฆ๏ผ้ฟๅ
ไฝ ่ฑ่ดนๅคง้ๆถ้ดๅป้ๆฐๆๅปบไธๅๅบๅ้พๆๆฏไน้ดๅฏไปฅๅค็จ็ๅบ็ก่ฎพๆฝใ
+
+[![Hyperledger FireFly ๆๆฏๆถๆๅพ](./doc-site/docs/images/firefly_architecture_overview.jpg)](https://raw.githubusercontent.com/kaleido-io/firefly/main/doc-site/docs/images/firefly_architecture_overview.jpg)
+
+## ๅผๅงไธบ Hyperledger FireFly ๅ่ดก็ฎ
+
+ๆ ่ฎบๆจๆฏๅ็ซฏใๅ็ซฏ๏ผ่ฟๆฏๅ
จๆ ๅผๅ่
๏ผ่ฟ้้ฝๆ้ๅๆจ็่ดก็ฎๆบไผใ
+
+่ฏทๆฅ็ๆไปฌ็[่ดก็ฎ่
ๆๅ](https://hyperledger.github.io/firefly/latest/contributors/)๏ผ**ๆฌข่ฟๅ ๅ
ฅ**๏ผ
+
+## ๅ
ถไปๅญๅจๅบ
+
+ๆจๅฝๅๆๅจ็ๆฏโๆ ธๅฟโๅญๅจๅบ๏ผ็จGo่ฏญ่จ็ผๅ๏ผๅ
ๅซไบAPIๆๅกๅจๅไธญๅคฎ็ผๆๅผๆใๆฌๅบ่ฟๆไพๅค็งๆไปถๆฅๅฃ๏ผ็จไบๆฏๆไฝฟ็จ TypeScript ๅ Java ็ญ่ฏญ่จ็ผๅ็ๅพฎๆๅก่ฟๆฅๅจไปฅๅๅ
ถไปๅ
ณ้ฎ่ฟ่ก็ปไปถใ
+
+ไปฅไธๆฏๆจๅฏ่ฝๆๅ
ด่ถฃ็ๅไธชๅพฎๆๅก็ปไปถใ็จๆท็ป้ชใCLI ๅๅบ็จ็คบไพ็ๅญๅจๅบใ
+
+> ๆณจๆ๏ผไปฅไธไป
ๅๅบไบๅผๆบๅญๅจๅบๅๆไปถ
+
+### ๅบๅ้พ่ฟๆฅ
+
+- Transaction Manager๏ผๅบๅ้พไบคๆ็ฎก็็ปไปถ๏ผ- https://github.com/hyperledger/firefly-transaction-manager
+- RLP & ABI ็ผ็ , Keystore V3ๅฎ็จๅทฅๅ
ท ๅ secp256k1 ็ญพๅ่ฟ่กๆถ - https://github.com/hyperledger/firefly-signer
+- ้็จๅไปฅๅคชๅๅบๅ้พ็ๅ่่ฟๆฅๅจ - https://github.com/hyperledger/firefly-evmconnect
+ - EVMๅ
ผๅฎนๅ
ฌ้พ: ่ฏทๅ่ง[ๆๆกฃ](https://hyperledger.github.io/firefly)
+- ้ๅฏน่ฎธๅฏๅถไปฅๅคชๅๅบๅ้พ็่ฟๆฅๅจ - https://github.com/hyperledger/firefly-ethconnect
+ - ็งๆ/่ฎธๅฏๅถๅบๅ้พ: Hyperledger Besu / Quorum
+- Hyperledger Fabric่ฟๆฅๅจ - https://github.com/hyperledger/firefly-fabconnect
+- Tezos่ฟๆฅๅจ - https://github.com/hyperledger/firefly-tezosconnect
+- Corda่ฟๆฅๅจ็คบไพ: https://github.com/hyperledger/firefly-cordaconnect
+ - ไฝฟ็จ่ฏฅ่ฟๆฅๅจ๏ผ้่ฆๅฏน CorDapp ่ฟ่กๅฎๅถๅๅผๅ
+
+### ไปฃๅธๆ ๅ
+
+- ERC20/ERC721 ไปฃๅธ - https://github.com/hyperledger/firefly-tokens-erc20-erc721
+- ERC1155 ไปฃๅธ - https://github.com/hyperledger/firefly-tokens-erc1155
+
+### ็งๆๆฐๆฎๆป็บฟ่ฟๆฅ
+
+- HTTPS ๆฐๆฎไบคๆข็ปไปถ - https://github.com/hyperledger/firefly-dataexchange-https
+
+### ๅผๅ่
็ๆ็ณป็ป
+
+- ๅฝไปค่ก็้ข (CLI) - https://github.com/hyperledger/firefly-cli
+- ๅพๅฝข็จๆท็้ข - https://github.com/hyperledger/firefly-ui
+- Node.js SDK - https://github.com/hyperledger/firefly-sdk-nodejs
+- ๆฒ็/ๆต่ฏๅทฅๅ
ท - https://github.com/hyperledger/firefly-sandbox
+- ็คบไพ - https://github.com/hyperledger/firefly-samples
+- FireFly ๆง่ฝๆต่ฏ CLI: https://github.com/hyperledger/firefly-perf-cli
+- ้จ็ฝฒๅฐKubernetes็็คบไพHelm Charts: https://github.com/hyperledger/firefly-helm-charts
+
+## FireFly ๆ ธๅฟไปฃ็ ๅฑ็บง็ปๆ
+
+```
+โโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ
+โ cmd โโโโค firefly [Ff]โ - CLIๅ
ฅๅฃ
+โโโโโโโโโโโโ โ โ - ็ถ็บงไธไธๆๅๅปบ
+ โ โ - ไฟกๅทๅค็
+ โโโโโโโฌโโโโโโโโโโ
+ โ
+โโโโโโโโโโโโ โโโโโโโดโโโโโโโโโโ - HTTP ็ๅฌๅจ (Gorilla mux)
+โ internal โโโโค api [As]โ * TLS (SSL), CORS ้
็ฝฎ็ญ.
+โโโโโโโโโโโโ โ server โ * ๅไธ็ซฏๅฃไธ็ WS ๅ็บง
+ โ โ - REST ่ทฏ็ฑๅฎไน
+ โโโโโโโฌโโโโโโโโโโ * ๅช่ด่ดฃ็ฎๅ็่ทฏ็ฑ้ป่พ๏ผๆๆๅค็้ป่พไบค็ฑorchestrator
+ โ
+ โโโโโโโดโโโโโโโโโโ - REST ่ทฏ็ฑๅฎไนๆกๆถ
+ โ openapi [Oa]โ * ๆ ๅๅ Body๏ผPath๏ผQuery๏ผ Filter ่ฏญไน
+ โ spec | - ็ๆ OpenAPI 3.0 (Swagger) ๆๆกฃ
+ โโโโโโโฌโโโโโโโโโโ * ๅ
ๆฌ Swagger. UI
+ โ
+ โโโโโโโดโโโโโโโโโโ - WebSocket ๆๅกๅจ
+ โ [Ws]โ * ไธบไธๅกๅบ็จๅผๅๆไพๅผๅ่
ๅๅฅฝ็ JSON ๅ่ฎฎ
+ โ websockets โ * ๅฏ้ ็ๆๅบไบไปถไผ ้
+ โโโโโโโฌโโโโโโโโโโ * _Event interface [Ei] ๆฏๆ้ๆๅ
ถไป่ฎก็ฎๆกๆถ/ไผ ่พๆนๅผ_
+ โ
+ โโโโโโโดโโโโโโโโโโ - ็ๅฌๆฐๆฎๅบไบไปถๆนๅ็ๆๅฑๆฅๅฃ
+ โ admin [Ae]โ * ็จไบๆๅปบๅค้จ่ฟ่ก็ๅพฎๆๅกๆฉๅฑๆ ธๅฟๅ่ฝ
+ โ events | * ่ขซ Transaction Manager ็ปไปถไฝฟ็จ
+ โโโโโโโฌโโโโโโโโโโ * ๆฏๆ็นๅฎๅฏน่ฑก็ฑปๅ่ฟๆปค
+ โ
+ โโโโโโโดโโโโโโโโโโ - ๆ ธๅฟๆฐๆฎ็ฑปๅ
+ โ fftypes [Ft]โ * ็จไบ API ๅๅบๅๅ
+ โ โ * API ๅฏ้่ฟ่ทฏ็ฑๅฎไนๅฏน่พๅ
ฅๅญๆฎต่ฟ่กๅฑ่ฝ
+ โโโโโโโฌโโโโโโโโโโ
+ โ
+ โโโโโโโดโโโโโโโโโโ - ๆ ธๅฟ่ฟ่กๆถ็ฏๅขๆๅกๅจ๏ผๅๅงๅๅๆฅๆไปฅไธ็ๅฎไพ:
+ โ [Or]โ * Components๏ผๅ่ฝ็ปไปถ
+ โโโโโโโโโฌโโโโค orchestrator โ * Plugins๏ผๅฏๆๆ็ๅบ็ก่ฎพๆฝๆๅก
+ โ โ โ โ - ๅ่ทฏ็ฑๅฑๆไพๅค็้ป่พ
+ โ โ โโโโโโโโโโโโโโโโโ * ๆๆ็API่ฐ็จๅฐไผๅจ่ฟ้ๅผๅงๅค็
+ โ โ
+ โ Components: ๅ
ๅซไธป่ฆๅ่ฝ็ๅค็้ป่พ
+ โ โ
+ โ โ โโโโโโโโโโโโโโโโโ - ่ทจ้พๆๆฏๆบ่ฝๅ็บฆ้ป่พ็ๆดๅ
+ โ โโโโโค contract [Cm]โ * ไธบๆบ่ฝๅ็บฆ็ๆ OpenAPI 3 / Swagger ๅฎไน๏ผๅนถไธไผ ๆญๅฐ็ฝ็ปไธญ
+ โ โ โ manager โ * ็ฎก็ๅ็ๅบๅ้พไบไปถ็ๅฌ๏ผๅนถๅฐๅ
ถ่ทฏ็ฑๅฐๅบ็จไบไปถ
+ โ โ โโโโโโโโโโโโโโโโโ * ่ด่ดฃๅจๅ็ๅบๅ้พๆฅๅฃ (ABI ็ญ) ๅ FireFly Interface [FFI] ๆ ผๅผไน้ด็่ฝฌๆข
+ โ โ
+ โ โ โโโโโโโโโโโโโโโโโ - ็ปดๆคๆดไธช็ฝ็ป็่งๅพ
+ โ โโโโโค network [Nm]โ * ไธ network permissioning [NP] ๆไปถ็้ๆ
+ โ โ โ map โ * ไธๅนฟๆญๆไปถ็้ๆ
+ โ โ โโโโโโโโโโโโโโโโโ * ๅค็ๆๅ่บซไปฝใ่็น่บซไปฝๅ็ญพๅ่บซไปฝ็ๅฑๆฌก็ปๆ
+ โ โ
+ โ โ โโโโโโโโโโโโโโโโโ - ๅนฟๆญๆฐๆฎ็ปๆๆ็ฝ็ปไธญ็ๆๅ
+ โ โโโโโค broadcast [Bm]โ * ๅฎ็ฐๆน้็ปไปถ็ๅๅ
+ โ โ โ manager | * ไธ shared storage [Ss] ๆไปถ็้ๆ
+ โ โ โโโโโโโโโโโโโโโโโ * ไธblockchain interface [Bi] ๆไปถ็้ๆ
+ โ โ
+ โ โ โโโโโโโโโโโโโโโโโ - ๅ้็งๆๆฐๆฎ็ป็ฝ็ปไธญ็ๆๅ
+ โ โโโโโค private [Pm]โ * ๅฎ็ฐๆนๅค็็ปไปถ็่ฐๅบฆๅจ
+ โ โ โ messaging | * ไธ data exchange [Dx] ๆไปถ็้ๆ
+ โ โ โโโโโโโโฌโโโโโโโโโ * ๆถๆฏๅฏไปฅ้่ฟๅบๅ้พๅบๅฎๆ่
ๆๅบ๏ผๆ่
ไป
ๅ้
+ โ โ โ
+ โ โ โโโโโโโโดโโโโโโโโโ - ๆ็้็ฆป็ๆฐๆฎๅๅบๅ้พ็ๅๆน็พค็ป
+ โ โ โ group [Gm]โ * ไธ data exchange [Dx] ๆไปถ็้ๆ
+ โ โ โ manager โ * ไธ blockchain interface [Bi] ๆไปถ็้ๆ
+ โ โ โโโโโโโโโโโโโโโโโ
+ โ โ
+ โ โ โโโโโโโโโโโโโโโโโ - ็งๆๆฐๆฎ็ฎก็ๅ้ช่ฏ
+ โ โโโโโค data [Dm]โ * ๅฎ็ฐๆน้็ปไปถ็่ฐๅบฆๅจ
+ โ โ โ manager โ * ไธ data exchange [Dx] ๆไปถ็้ๆ
+ โ โ โโโโโโโโฌโโโโโโโโโ * ไธ blockchain interface [Bi] ๆไปถ็้ๆ
+ โ โ โ
+ โ โ โโโโโโโโดโโโโโโโโโ - JSON ๆฐๆฎๆจกๅผ็ฎก็ไธ้ช่ฏ๏ผๆถๆๅฏๆฉๅฑๅฐ XML ็ญ๏ผ
+ โ โ โ json [Jv]โ * ็ซๅ
ๅ็ซๅคๆถๆฏ็JSON่ฏญๆณ็ฎก็ๅ้ช่ฏ
+ โ โ โ validator โ * ๆจกๅผไผ ๆญ
+ โ โ โโโโโโโโฌโโโโโโโโโ * ไธๅนฟๆญๆไปถ้ๆ
+ โ โ โ
+ โ โ โโโโโโโโดโโโโโโโโโ - ๅฏ้่ฟIDๆ่
hashๅฏปๅ็ไบ่ฟๅถๆฐๆฎๅญๅจ
+ โ โ โ blobstore [Bs]โ * ไธ data exchange [Dx] ๆไปถ็้ๆ
+ โ โ โ โ * ๅฏนๆฐๆฎ่ฟ่กhashๅๅค็, ๅนถไธๅจblobๅญๅจไธญ็ปดๆค่ด่ฝฝๅผ็จ็ๆ ๅฐ
+ โ โ โโโโโโโโโโโโโโโโโ * ไธ blockchain interface [Bi] ๆไปถ็้ๆ
+ โ โ
+ โ โ โโโโโโโโโโโโโโโโโ - ่ด่ดฃๅ
ฑไบซๅญๅจๅ
ๅฎน็ไธ่ฝฝ
+ โ โโโโโค shared [Sd]โ * ๅนถ่กๅผๆญฅไธ่ฝฝ
+ โ โ โ download โ * ๅฏ้ ็้่ฏๅๅฎๆบๆขๅค
+ โ โ โโโโโโโโโโโโโโโโโ * ๅฎๆๅ้็ฅไบไปถaggregator
+ โ โ
+ โ โ โโโโโโโโโโโโโโโโโ
+ โ โโโโโค identity [Im] โ - ่ทจ็ปไปถไธญๅฟๅ่บซไปฝ็ฎก็ๆๅก
+ โ โ โ manager โ * ่งฃๆAPI่พๅ
ฅ็่บซไปฝๅๅฏ้ฅ็ปๅ๏ผไพๅฆ็ญๅ็งฐใๆ ผๅผๅ็ญ๏ผ
+ โ โ โ โ * ๅฐๆณจๅ็้พไธ็ญพๅๅฏ้ฅๆ ๅฐๅ่บซไปฝ
+ โ โ โโโโโโโโโโโโโโโโโ * ้ๆๅบๅ้พๆฅๅฃๅๅฏๆๆ่บซไปฝๆฅๅฃ๏ผๅพ
ๅฎ๏ผ
+ โ โ
+ โ โ โโโโโโโโโโโโโโโโโ - ่ฎฐๅฝ้่ฟๆไปถๅฏนๅค้จ็ปไปถๆง่ก็ๆๆๆไฝ
+ โ โโโโโค operation [Om]โ * ๆดๆฐๆฐๆฎๅบไธญ็่พๅ
ฅ/่พๅบ
+ โ โ โ manager โ * ไธบๆไปถๆไพไธ่ด็้่ฏ่ฏญไน
+ โ โ โโโโโโโโโโโโโโโโโ
+ โ โ
+ โ โ โโโโโโโโโโโโโโโโโ - ็งๆๆฐๆฎ็ฎก็ๅ้ช่ฏ
+ โ โโโโโค event [Em]โ * ๅฎ็ฐๆน้็ปไปถ็่ฐๅบฆๅจ
+ โ โ โ manager โ * ไธ data exchange [Dx] ๆไปถ็้ๆ
+ โ โ โโโโโโโโฌโโโโโโโโโ * ไธ blockchain interface [Bi] ๆไปถ็้ๆ
+ โ โ โ
+ โ โ โโโโโโโโดโโโโโโโโโ - ๅค็ไผ ๅ
ฅ็ๅค้จๆฐๆฎ
+ โ โ โ [Ag]โ * ไธ data exchange [Dx] ๆไปถ็้ๆ
+ โ โ โ aggregator โ * ไธ shared storage [Ss] ๆไปถ็้ๆ
+ โ โ โ โ * ไธ blockchain interface [Bi] ๆไปถ็้ๆ
+ โ โ โ โ - ็กฎไฟๅชๆๆๆๆฐๆฎ้ฝๅฐฑ็ปชๆถๆๆไบไปถๆไผ่ขซๅๅ
+ โ โ โโโโโโโโฌโโโโโโโโโ * ไธไธๆๆ็ฅ๏ผ้ฟๅ
โๅ
จๅฑ้ปๅกโๅบๆฏ็ๅบ็ฐ
+ โ โ โ
+ โ โ โโโโโโโโดโโโโโโโโโ - ่ฎข้
็ฎก็
+ โ โ โ [Sm]โ * ๅๅปบๅ็ฎก็่ฎข้
+ โ โ โ subscription โ * ๅๅปบๅ็ฎก็่ฎข้
+ โ โ โ manager โ * ๅๆถๆฏๅฐไบไปถๅน้
้ป่พ
+ โ โ โโโโโโโโฌโโโโโโโโโ
+ โ โ โ
+ โ โ โโโโโโโโดโโโโโโโโโ - ็ฎก็ไบไปถๅฐๅทฒ่ฟๆฅๅบ็จ็ไผ ้
+ โ โ โ event [Ed]โ * ไธ data exchange [Dx] ๆไปถ็้ๆ
+ โ โ โ dispatcher โ * ไธ blockchain interface [Bi] ๆไปถ็้ๆ
+ โ โ โโโโโโโโโโโโโโโโโ
+ โ โ
+ โ โ โโโโโโโโโโโโโโโโโ - Token ๅๅปบใไผ ่พ็ๅๅงๅใ็ดขๅผๅๅๅ
+ โ โโโโโค asset [Am]โ * ๆฏๆๅ่ดจๅไปฃๅธ๏ผๅฆๆฐๅญ่ดงๅธ๏ผ
+ โ โ โ manager โ * ๆฏๆ้ๅ่ดจๅไปฃๅธ๏ผ NFTs / globally uniqueness / digital twins
+ โ โ โโโโโโโโโโโโโโโโโ * ไบคๆๅๅฒ็ๅ
จ็ดขๅผ
+ โ โ [REST/WebSockets]
+ โ โ โโโโโโโดโโโโโโโโโโโโโโ โโโโโโโโโโโโ โโ
+ โ โ โ ERC-20 / ERC-721 โโโโโค ERC-1155 โโโโโค ๅๅปบ token ่ฟๆฅๅจ็็ฎๅๆกๆถ
+ โ โ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโ โโ
+ โ โ
+ โ โ โโโโโโโโโโโโโโโโโ
+ โ โโโโโค sync / [Sa] โ - ๅๆญฅใๅผๆญฅๆกฅๆฅ
+ โ โ โ async bridge โ * ๆไพๅๆญฅ่ฏทๆฑใๅบ็ญAPIs
+ โ โ โ โ * ่ฝฌๆขไธบๅบๅฑไบไปถ้ฉฑๅจAPI
+ โ โ โโโโโโโโโโโโโโโโโ
+ โ โ
+ โ โ โโโโโโโโโโโโโโโโโ - ่ๅๆฅๆๅๆฐๆฎ๏ผๅนถ็ๆๅๅธ็จไบไธ้พ
+ โ โโโโโค batch [Ba]โ * ๅฏๆๆ่ฐๅบฆๅจ
+ โ โ โ manager โ - ๆฐๆฎๅบๅไธป็บฟAPIๅค็่งฃ่ฆ
+ โ โ โ โ * ๆๅ
ณactive/activeๆๅบ็ๆดๅคไฟกๆฏ๏ผ่ฏทๅ่ๆถๆๅพ
+ โ โ โโโโโโโโฌโโโโโโโโโ - ็ฎก็ๆนๅค็ๅจๅฎไพ็ๅๅปบ
+ โ โ โ
+ โ โ โโโโโโโโดโโโโโโโโโ - ๆ นๆฎ้ๆฑๅฏๅจ็็ญ็ๅฝๅจๆไปฃ็
+ โ โ โ batch [Bp]โ * ไธไฝ่
+ๆถๆฏ็ฑปๅ่ฆๅ
+ โ โ โ processor โ - ๆน้ๆๅปบ100ๅคๆกไฟกๆฏไปฅไผๅไธ้พๆง่ฝ
+ โ โ โ โ * ่ๅๆถๆฏๅๆฐๆฎ๏ผๅนถ็ๆๅๅธ็จไบไธ้พ
+ โ โ โโโโโโโโโโโโโโโโโ - ๅจ้
็ฝฎ็็ฉบ้ฒๆถ้ดๅ่ชๅจๅ
ณ้ญ
+ โ ... ๆดๅคๅพ
ๅฎ
+ โ
+Plugins: ๆฏไธชๆไปถ้ฝๅ
ๅซไธไธชGo shim๏ผไปฅๅไธไธช่ฟ็จไปฃ็ๅพฎๆๅก่ฟ่ก็ฏๅข(ๅฆๆ้่ฆ)
+ โ
+ โ โโโโโโโโโโโโโโโโโ - ๅบๅ้พๆฅๅฃ
+ โโโโโโโโโโโโโค [Bi]โ * ไบคๆๆไบค - ๅ
ๆฌ็ญพๅๅฏ้ฅ็ฎก็
+ โ โ blockchain โ * ไบไปถ็ๅฌ
+ โ โ interface โ * ๆ ๅๅๆไฝ๏ผๅ่ชๅฎไน้พไธ่ฆๅ
+ โ โโโโโโโฌโโโโโโโโโโ
+ โ โ
+ โ โโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโฌ-โโโโโโโโโโโโโโโโโโโโ
+ โ โโโโโโโดโโโโโโโโโโ โโโโโโโโโดโโโโโโโโ โโโโโโโโโดโโโโโโโโโ โโโโโโโโโดโโโโโโโโโ
+ โ โ ethereum โ โ fabric โ โ corda/cordapps โ โ tezos โ
+ โ โโโโโโโฌโโโโโโโโโโ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ
+ โ [REST/WebSockets]
+ โ โโโโโโโดโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโ โโ
+ โ โ transaction manager [Tm] โโโโโค Connector API [ffcapi] โโโโโค ๆๅปบๅบๅ้พ่ฟๆฅๅจ็็ฎๅๆกๆถ
+ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโ โโ
+ โ
+ โ โโโโโโโโโโโโโโโโโ - ไปฃๅธๆฅๅฃ
+ โโโโโโโโโโโโโค tokens [Ti]โ * ๆ ๅๅ็ๆ ธๅฟๆฆๅฟต๏ผtoken pools๏ผtransfers๏ผapprovals
+ โ โ interface โ * ๅฏๆๆ็่ทจไปฃๅธๆ ๅ
+ โ โโโโโโโโโโโโโโโโโ * ้่ฟๅพฎๆๅก่ฟๆฅๅจ้ๅฏน่ชๅฎไนไปฃๅธๆ ๅ็็ฎๅๅฎ็ฐๆนๅผ็ๆฏๆ
+ โ [REST/WebSockets]
+ โ โโโโโโโดโโโโโโโโโโโโโโ โโโโโโโโโโโโ โโ
+ โ โ ERC-20 / ERC-721 โโโโโค ERC-1155 โโโโโค ๆๅปบไปฃๅธ่ฟๆฅๅจ็็ฎๅๆกๆถ
+ โ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโ โโ
+ โ
+ โ โโโโโโโโโโโโโโโโโ - P2P ๅ
ๅฎนๅฏปๅๆไปถ็ณป็ป
+ โโโโโโโโโโโโโค shared [Si]โ * ่ด่ฝฝ ไธไผ / ไธ่ฝฝ
+ โ โ storage โ * ๆๆ็่ด่ฝฝๅ่็ฎก็
+ โ โ interface โ
+ โ โโโโโโโฌโโโโโโโโโโ
+ โ โ
+ โ โโโโโโโโโโ ... ๅฏๆๅฑ่ณไปปๆๅ
ฑไบซๅญๅจ็ณป็ป๏ผๅฏไพๆๆๆๅ่ฎฟ้ฎ
+ โ โโโโโโโดโโโโโโโโโโ
+ โ โ ipfs โ
+ โ โโโโโโโโโโโโโโโโโ
+ โ
+ โ โโโโโโโโโโโโโโโโโ - ็งๆๆฐๆฎไบคๆข
+ โโโโโโโโโโโโโค data [Dx]โ * Blob ๅญๅจ
+ โ โ exchange โ * ็งๆๅฎๅ
จๆถๆฏไผ ้
+ โ โโโโโโโฌโโโโโโโโโโ * ๅฎๅ
จ็ๆไปถไผ ่พ
+ โ โ
+ โ โโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโ ... ๅฏๆๅฑ่ณไปปๆ็งๆๆฐๆฎไบคๆขๆๆฏ
+ โ โโโโโโโดโโโโโโโโโโ โโโโโโโโโดโโโโโโโโ
+ โ โ https / MTLS โ โ Kaleido โ
+ โ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ
+ โ
+ โ โโโโโโโโโโโโโโโโโ - API ่ฎค่ฏๅ่ฎค่ฏๆฅๅฃ
+ โโโโโโโโโโโโโค api auth [Aa]โ * ้ช่ฏๅฎๅ
จๅญๆฎ (OpenID Connect id token JWTs etc.)
+ โ โ โ * ๆๅ API/user ่บซไปฝ (็จไบ่บซไปฝๆฅๅฃๆ ๅฐ)
+ โ โโโโโโโฌโโโโโโโโโโ * ็ป็ฒๅบฆ API ่ฎฟ้ฎๆงๅถ็ๆง่ก็น
+ โ โ
+ โ โโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโ ... ๅฏๆๅฑๅฐไปปๆๅ็น็ปๅฝๆๆฏ
+ โ โโโโโโโดโโโโโโโโโโ โโโโโโโโโดโโโโโโโโ
+ โ โ apikey โ โ jwt โ
+ โ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ
+ โ
+ โ โโโโโโโโโโโโโโโโโ - ๆฐๆฎๅบ้ๆ
+ โโโโโโโโโโโโโค database [Di]โ * ๅๅปบ, ่ฏปๅ, ๆดๆฐ, ๅ ้ค (CRUD) ๆไฝ
+ โ โ interface โ * ่ฟๆปคๅๆดๆฐๅฎไนๆฅๅฃ
+ โ โโโโโโโฌโโโโโโโโโโ * ่ฟ็งปๅ็ดขๅผ
+ โ โ
+ โ โโโโโโโโโโ ... ๅฏๆๅฑ่ณไปปๆNoSql (CouchDB / MongoDB etc.)
+ โ โโโโโโโดโโโโโโโโโโ
+ โ โ sqlcommon โ
+ โ โโโโโโโฌโโโโโโโโโโ
+ โ โโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโ ... ๅฏๆๅฑ่ณๅ
ถไปSQLๆฐๆฎๅบ
+ โ โโโโโโโดโโโโโโโโโโ โโโโโโโโโดโโโโโโโโโ
+ โ โ postgres โ โ sqlite3 โ
+ โ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ
+ โ
+ โ โโโโโโโโโโโโโโโโโ - ๅฐๆ ธๅฟไบไปถๅผๆ่ฟๆฅ่ณๅค้จๆกๆถๅๅบ็จ
+ โโโโโโโโโโโโโค event [Ei]โ * ๆฏๆ้ฟๅจๆ (ๅฏๆ็ปญ็) ๅ็ญๆ็ไบไปถ่ฎข้
+ โ โ interface โ * ๆน้ใ่ฟๆปค๏ผๆๆ็ไผ ่พ้ฝไผๅจๆ ธๅฟๅบๅ่ฟ่กๅค็
+ โ โโโโโโโฌโโโโโโโโโโ * ๆฅๅฃๆฏๆ่ฟๆฅ่พๅ
ฅ (websocket) ๅ่ฟๆฅ่พๅบ (ไปฃ็่ฟ่ก็ฏๅขๆไปถ) ๆไปถ
+ โ โ
+ โ โโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโ ... ๅฏๆๅฑ่ณๅ
ถไป็ไบไปถๆป็บฟ (Kafka, NATS, AMQP etc.)
+ โ โโโโโโโดโโโโโโโโโโ โโโโโโโโโดโโโโโโโโโ
+ โ โ websockets โ โ webhooks โ
+ โ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ
+ โ ... ๆดๅคๅพ
ๅฎ
+
+ ้ขๅค็ๅทฅๅ
ท็ฑปๆกๆถ
+ โโโโโโโโโโโโโโโโโ - REST API ๅฎขๆท็ซฏ
+ โ rest [Re]โ * ๆไพไพฟๅฉๆงๅๆฅๅฟ
+ โ client โ * ๆ ๅ็่ฎค่ฏ, ้
็ฝฎๅ้่ฏ้ป่พ
+ โโโโโโโโโโโโโโโโโ * ๆๅปบๅ้่ฏ
+
+ โโโโโโโโโโโโโโโโโ - WebSocket ๅฎขๆท็ซฏ
+ โ wsclient [Wc]โ * ๆไพไพฟๅฉๆงๅๆฅๅฟ
+ โ โ * ๆ ๅๅ่ฎค่ฏ๏ผ้
็ฝฎๅ้่ฟ้ป่พ
+ โโโโโโโโโโโโโโโโโ * ๅบไบ Gorilla WebSockets
+
+ โโโโโโโโโโโโโโโโโ - ็ฟป่ฏๆกๆถ
+ โ i18n [In]โ * ๆๆ็ฟป่ฏๅ
ๅฎนๅฟ
้กป่ฆๆทปๅ ๅฐ `en_translations.json` - ็จ `FF10101` ไฝไธบ key
+ โ โ * ้่ฏฏไผ่ขซๆๅ
, `error` ๅ
ๅ
ๆไพไบ้ขๅค็็นๆง (ๅ ๆ ็ญ.)
+ โโโโโโโโโโโโโโโโโ * ไนๆฏๆๆ่ฟฐ็ฟป่ฏ, ไพๅฆ OpenAPI ๆ่ฟฐ
+
+ โโโโโโโโโโโโโโโโโ - ๆฅๅฟๆกๆถ
+ โ log [Lo]โ * ๆฅๅฟๆกๆถ (logrus) ้ๆไบไธไธๆๆ ็ญพ
+ โ โ * ไธไธๆ่ดฏ็ฉฟๆดไธชไปฃ็ ๏ผ็จไบไผ ้API่ฐ็จไธไธๆไปฅๅๆฅๅฟไธไธๆ
+ โโโโโโโโโโโโโโโโโ * ๆ ทไพ: ๆๆ็API่ฐ็จ้ฝๆ็ๅฏ่ฟฝๆบฏID, ไปฅๅๆถ้ฟ
+
+ โโโโโโโโโโโโโโโโโ - ้
็ฝฎ
+ โ config [Co]โ * ๅบไบๆฅๅฟๆกๆถ็ๆไปถๅ็ฏๅขๅ้ (viper)
+ โ โ * ไธป่ฆ้
็ฝฎkeyๅ
จ้จ้ไธญๅฎไน
+ โโโโโโโโโโโโโโโโโ * ๆไปถ้่ฟ่ฟๅๅ
ถ้
็ฝฎ็ปๆ่ฟ่ก้ๆ (JSON ๆ ็ญพ)
+
+```
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 0000000000..4bc91b8f88
--- /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 0000000000..2cfe2ff41a
--- /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 0000000000..234055a4f0
--- /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 0000000000..0e7b992aa8
--- /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/.github/workflows/ci.yml b/doc-site/.github/workflows/ci.yml
index ddff3e16ed..e24cd9fa52 100644
--- a/doc-site/.github/workflows/ci.yml
+++ b/doc-site/.github/workflows/ci.yml
@@ -23,7 +23,7 @@ jobs:
- uses: actions/setup-python@v4
with:
python-version: 3.x
- - uses: actions/cache@v2
+ - uses: actions/cache@v4
with:
key: ${{ github.ref }}
path: .cache
diff --git a/doc-site/docs/SUMMARY.md b/doc-site/docs/SUMMARY.md
index cb5d3eb3eb..e93f3798f1 100644
--- a/doc-site/docs/SUMMARY.md
+++ b/doc-site/docs/SUMMARY.md
@@ -28,5 +28,7 @@
* contributors/*
* [API Spec](swagger/index.md)
* [FAQs](faqs/index.md)
+* [Troubleshooting](troubleshooting/index.md)
+ * troubleshooting/*
* [Release Notes](releasenotes/index.md)
* releasenotes/*
\ No newline at end of file
diff --git a/doc-site/docs/contributors/dev_environment_setup.md b/doc-site/docs/contributors/dev_environment_setup.md
index c4385d81e8..850d91c0e5 100644
--- a/doc-site/docs/contributors/dev_environment_setup.md
+++ b/doc-site/docs/contributors/dev_environment_setup.md
@@ -12,7 +12,7 @@ This guide will walk you through setting up your machine for contributing to Fir
You will need a few prerequisites set up on your machine before you can build FireFly from source. We recommend doing development on macOS, Linux, or WSL 2.0.
-- [Go 1.21](https://golang.org/dl/)
+- [Go 1.22](https://golang.org/dl/)
- make
- GCC
- openssl
diff --git a/doc-site/docs/gettingstarted/firefly_cli.md b/doc-site/docs/gettingstarted/firefly_cli.md
index 9d00732537..bb26fd574c 100644
--- a/doc-site/docs/gettingstarted/firefly_cli.md
+++ b/doc-site/docs/gettingstarted/firefly_cli.md
@@ -24,24 +24,34 @@ In order to run the FireFly CLI, you will need a few things installed on your de
There are several ways to install the FireFly CLI. The easiest way to get up and running with the FireFly CLI is to download a pre-compiled binary of the latest release.
-### Download the package for your OS
+### Install via Binary Package Download
-Go to the [latest release page](https://github.com/hyperledger/firefly-cli/releases/latest) and download the package for your OS and CPU architecture.
+Download the package for your OS by navigating to the [latest release page](https://github.com/hyperledger/firefly-cli/releases/latest) and downloading the appropriate package for your OS and architecture.
-### Extract the binary and move it to `/usr/bin/local`
+#### Unpack and Install the Binary
+
+
+Assuming you downloaded the package from GitHub into your `Downloads` directory, run the following command to extract the binary and move it to your system path:
-Assuming you downloaded the package from GitHub into your `Downloads` directory, run the following command:
```
sudo tar -zxf ~/Downloads/firefly-cli_*.tar.gz -C /usr/local/bin ff && rm ~/Downloads/firefly-cli_*.tar.gz
```
-If you downloaded the package from GitHub into a different directory, you will need to change the `tar` command above to wherever the `firefly-cli_*.tar.gz` file is located.
+If you downloaded the package into a different directory, adjust the command to point to the correct location of the `firefly-cli_*.tar.gz` file.
-### macOSUsers
+#### macOSUsers
> **NOTE**: On recent versions of macOS, default security settings will prevent the FireFly CLI binary from running, because it was downloaded from the internet. You will need to [allow the FireFly CLI in System Preferences](https://github.com/hyperledger/firefly-cli/blob/main/docs/mac_help.md), before it will run.
+### Install via Homebrew (macOS)
+
+You can also install the FireFly CLI using Homebrew:
+
+```
+brew install firefly
+```
+
### Alternative installation method: Install via Go
If you have a local Go development environment, and you have included `${GOPATH}/bin` in your path, you could also use Go to install the FireFly CLI by running:
diff --git a/doc-site/docs/images/firefly_message_types.png b/doc-site/docs/images/firefly_message_types.png
new file mode 100644
index 0000000000..0da0133777
Binary files /dev/null and b/doc-site/docs/images/firefly_message_types.png differ
diff --git a/doc-site/docs/reference/config.md b/doc-site/docs/reference/config.md
index 72353252db..89c794f420 100644
--- a/doc-site/docs/reference/config.md
+++ b/doc-site/docs/reference/config.md
@@ -264,6 +264,7 @@ title: Configuration Reference
|idleTimeout|The max duration to hold a HTTP keepalive connection between calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`475ms`
|maxConnsPerHost|The max number of connections, per unique hostname. Zero means no limit|`int`|`0`
|maxIdleConns|The max number of idle connections to hold pooled|`int`|`100`
+|maxIdleConnsPerHost|The max number of idle connections, per unique hostname. Zero means net/http uses the default of only 2.|`int`|`100`
|passthroughHeadersEnabled|Enable passing through the set of allowed HTTP request headers|`boolean`|`false`
|requestTimeout|The maximum amount of time that a request is allowed to remain open|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
|tlsHandshakeTimeout|The maximum amount of time to wait for a successful TLS handshake|[`time.Duration`](https://pkg.go.dev/time#Duration)|`10s`
@@ -291,15 +292,25 @@ title: Configuration Reference
|initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms`
|maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
+## events.webhooks.throttle
+
+|Key|Description|Type|Default Value|
+|---|-----------|----|-------------|
+|burst|The maximum number of requests that can be made in a short period of time before the throttling kicks in.|`int`|``
+|requestsPerSecond|The average rate at which requests are allowed to pass through over time.|`int`|``
+
## events.webhooks.tls
|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
+|ca|The TLS certificate authority in PEM format (this option is ignored if caFile is also set)|`string`|``
|caFile|The path to the CA file for TLS on this API|`string`|``
+|cert|The TLS certificate in PEM format (this option is ignored if certFile is also set)|`string`|``
|certFile|The path to the certificate file for TLS on this API|`string`|``
|clientAuth|Enables or disables client auth for TLS on this API|`string`|``
|enabled|Enables or disables TLS on this API|`boolean`|`false`
|insecureSkipHostVerify|When to true in unit test development environments to disable TLS verification. Use with extreme caution|`boolean`|``
+|key|The TLS certificate key in PEM format (this option is ignored if keyFile is also set)|`string`|``
|keyFile|The path to the private key file for TLS on this API|`string`|``
|requiredDNAttributes|A set of required subject DN attributes. Each entry is a regular expression, and the subject certificate must have a matching attribute of the specified type (CN, C, O, OU, ST, L, STREET, POSTALCODE, SERIALNUMBER are valid attributes)|`map[string]string`|``
@@ -343,11 +354,14 @@ title: Configuration Reference
|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
+|ca|The TLS certificate authority in PEM format (this option is ignored if caFile is also set)|`string`|``
|caFile|The path to the CA file for TLS on this API|`string`|``
+|cert|The TLS certificate in PEM format (this option is ignored if certFile is also set)|`string`|``
|certFile|The path to the certificate file for TLS on this API|`string`|``
|clientAuth|Enables or disables client auth for TLS on this API|`string`|``
|enabled|Enables or disables TLS on this API|`boolean`|`false`
|insecureSkipHostVerify|When to true in unit test development environments to disable TLS verification. Use with extreme caution|`boolean`|``
+|key|The TLS certificate key in PEM format (this option is ignored if keyFile is also set)|`string`|``
|keyFile|The path to the private key file for TLS on this API|`string`|``
|requiredDNAttributes|A set of required subject DN attributes. Each entry is a regular expression, and the subject certificate must have a matching attribute of the specified type (CN, C, O, OU, ST, L, STREET, POSTALCODE, SERIALNUMBER are valid attributes)|`map[string]string`|``
@@ -420,11 +434,14 @@ title: Configuration Reference
|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
+|ca|The TLS certificate authority in PEM format (this option is ignored if caFile is also set)|`string`|``
|caFile|The path to the CA file for TLS on this API|`string`|``
+|cert|The TLS certificate in PEM format (this option is ignored if certFile is also set)|`string`|``
|certFile|The path to the certificate file for TLS on this API|`string`|``
|clientAuth|Enables or disables client auth for TLS on this API|`string`|``
|enabled|Enables or disables TLS on this API|`boolean`|`false`
|insecureSkipHostVerify|When to true in unit test development environments to disable TLS verification. Use with extreme caution|`boolean`|``
+|key|The TLS certificate key in PEM format (this option is ignored if keyFile is also set)|`string`|``
|keyFile|The path to the private key file for TLS on this API|`string`|``
|requiredDNAttributes|A set of required subject DN attributes. Each entry is a regular expression, and the subject certificate must have a matching attribute of the specified type (CN, C, O, OU, ST, L, STREET, POSTALCODE, SERIALNUMBER are valid attributes)|`map[string]string`|``
@@ -490,11 +507,14 @@ title: Configuration Reference
|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
+|ca|The TLS certificate authority in PEM format (this option is ignored if caFile is also set)|`string`|``
|caFile|The path to the CA file for TLS on this API|`string`|``
+|cert|The TLS certificate in PEM format (this option is ignored if certFile is also set)|`string`|``
|certFile|The path to the certificate file for TLS on this API|`string`|``
|clientAuth|Enables or disables client auth for TLS on this API|`string`|``
|enabled|Enables or disables TLS on this API|`boolean`|`false`
|insecureSkipHostVerify|When to true in unit test development environments to disable TLS verification. Use with extreme caution|`boolean`|``
+|key|The TLS certificate key in PEM format (this option is ignored if keyFile is also set)|`string`|``
|keyFile|The path to the private key file for TLS on this API|`string`|``
|requiredDNAttributes|A set of required subject DN attributes. Each entry is a regular expression, and the subject certificate must have a matching attribute of the specified type (CN, C, O, OU, ST, L, STREET, POSTALCODE, SERIALNUMBER are valid attributes)|`map[string]string`|``
@@ -588,6 +608,7 @@ title: Configuration Reference
|idleTimeout|The max duration to hold a HTTP keepalive connection between calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`475ms`
|maxConnsPerHost|The max number of connections, per unique hostname. Zero means no limit|`int`|`0`
|maxIdleConns|The max number of idle connections to hold pooled|`int`|`100`
+|maxIdleConnsPerHost|The max number of idle connections, per unique hostname. Zero means net/http uses the default of only 2.|`int`|`100`
|method|The HTTP method to use when making requests to the Address Resolver|`string`|`GET`
|passthroughHeadersEnabled|Enable passing through the set of allowed HTTP request headers|`boolean`|`false`
|requestTimeout|The maximum amount of time that a request is allowed to remain open|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
@@ -620,15 +641,25 @@ title: Configuration Reference
|initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms`
|maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
+## plugins.blockchain[].ethereum.addressResolver.throttle
+
+|Key|Description|Type|Default Value|
+|---|-----------|----|-------------|
+|burst|The maximum number of requests that can be made in a short period of time before the throttling kicks in.|`int`|``
+|requestsPerSecond|The average rate at which requests are allowed to pass through over time.|`int`|``
+
## plugins.blockchain[].ethereum.addressResolver.tls
|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
+|ca|The TLS certificate authority in PEM format (this option is ignored if caFile is also set)|`string`|``
|caFile|The path to the CA file for TLS on this API|`string`|``
+|cert|The TLS certificate in PEM format (this option is ignored if certFile is also set)|`string`|``
|certFile|The path to the certificate file for TLS on this API|`string`|``
|clientAuth|Enables or disables client auth for TLS on this API|`string`|``
|enabled|Enables or disables TLS on this API|`boolean`|`false`
|insecureSkipHostVerify|When to true in unit test development environments to disable TLS verification. Use with extreme caution|`boolean`|``
+|key|The TLS certificate key in PEM format (this option is ignored if keyFile is also set)|`string`|``
|keyFile|The path to the private key file for TLS on this API|`string`|``
|requiredDNAttributes|A set of required subject DN attributes. Each entry is a regular expression, and the subject certificate must have a matching attribute of the specified type (CN, C, O, OU, ST, L, STREET, POSTALCODE, SERIALNUMBER are valid attributes)|`map[string]string`|``
@@ -646,6 +677,7 @@ title: Configuration Reference
|instance|The Ethereum address of the FireFly BatchPin smart contract that has been deployed to the blockchain|Address `string`|``
|maxConnsPerHost|The max number of connections, per unique hostname. Zero means no limit|`int`|`0`
|maxIdleConns|The max number of idle connections to hold pooled|`int`|`100`
+|maxIdleConnsPerHost|The max number of idle connections, per unique hostname. Zero means net/http uses the default of only 2.|`int`|`100`
|passthroughHeadersEnabled|Enable passing through the set of allowed HTTP request headers|`boolean`|`false`
|prefixLong|The prefix that will be used for Ethconnect specific HTTP headers when FireFly makes requests to Ethconnect|`string`|`firefly`
|prefixShort|The prefix that will be used for Ethconnect specific query parameters when FireFly makes requests to Ethconnect|`string`|`fly`
@@ -686,15 +718,25 @@ title: Configuration Reference
|initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms`
|maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
+## plugins.blockchain[].ethereum.ethconnect.throttle
+
+|Key|Description|Type|Default Value|
+|---|-----------|----|-------------|
+|burst|The maximum number of requests that can be made in a short period of time before the throttling kicks in.|`int`|``
+|requestsPerSecond|The average rate at which requests are allowed to pass through over time.|`int`|``
+
## plugins.blockchain[].ethereum.ethconnect.tls
|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
+|ca|The TLS certificate authority in PEM format (this option is ignored if caFile is also set)|`string`|``
|caFile|The path to the CA file for TLS on this API|`string`|``
+|cert|The TLS certificate in PEM format (this option is ignored if certFile is also set)|`string`|``
|certFile|The path to the certificate file for TLS on this API|`string`|``
|clientAuth|Enables or disables client auth for TLS on this API|`string`|``
|enabled|Enables or disables TLS on this API|`boolean`|`false`
|insecureSkipHostVerify|When to true in unit test development environments to disable TLS verification. Use with extreme caution|`boolean`|``
+|key|The TLS certificate key in PEM format (this option is ignored if keyFile is also set)|`string`|``
|keyFile|The path to the private key file for TLS on this API|`string`|``
|requiredDNAttributes|A set of required subject DN attributes. Each entry is a regular expression, and the subject certificate must have a matching attribute of the specified type (CN, C, O, OU, ST, L, STREET, POSTALCODE, SERIALNUMBER are valid attributes)|`map[string]string`|``
@@ -720,6 +762,7 @@ title: Configuration Reference
|idleTimeout|The max duration to hold a HTTP keepalive connection between calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`475ms`
|maxConnsPerHost|The max number of connections, per unique hostname. Zero means no limit|`int`|`0`
|maxIdleConns|The max number of idle connections to hold pooled|`int`|`100`
+|maxIdleConnsPerHost|The max number of idle connections, per unique hostname. Zero means net/http uses the default of only 2.|`int`|`100`
|passthroughHeadersEnabled|Enable passing through the set of allowed HTTP request headers|`boolean`|`false`
|requestTimeout|The maximum amount of time that a request is allowed to remain open|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
|tlsHandshakeTimeout|The maximum amount of time to wait for a successful TLS handshake|[`time.Duration`](https://pkg.go.dev/time#Duration)|`10s`
@@ -748,15 +791,25 @@ title: Configuration Reference
|initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms`
|maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
+## plugins.blockchain[].ethereum.fftm.throttle
+
+|Key|Description|Type|Default Value|
+|---|-----------|----|-------------|
+|burst|The maximum number of requests that can be made in a short period of time before the throttling kicks in.|`int`|``
+|requestsPerSecond|The average rate at which requests are allowed to pass through over time.|`int`|``
+
## plugins.blockchain[].ethereum.fftm.tls
|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
+|ca|The TLS certificate authority in PEM format (this option is ignored if caFile is also set)|`string`|``
|caFile|The path to the CA file for TLS on this API|`string`|``
+|cert|The TLS certificate in PEM format (this option is ignored if certFile is also set)|`string`|``
|certFile|The path to the certificate file for TLS on this API|`string`|``
|clientAuth|Enables or disables client auth for TLS on this API|`string`|``
|enabled|Enables or disables TLS on this API|`boolean`|`false`
|insecureSkipHostVerify|When to true in unit test development environments to disable TLS verification. Use with extreme caution|`boolean`|``
+|key|The TLS certificate key in PEM format (this option is ignored if keyFile is also set)|`string`|``
|keyFile|The path to the private key file for TLS on this API|`string`|``
|requiredDNAttributes|A set of required subject DN attributes. Each entry is a regular expression, and the subject certificate must have a matching attribute of the specified type (CN, C, O, OU, ST, L, STREET, POSTALCODE, SERIALNUMBER are valid attributes)|`map[string]string`|``
@@ -774,6 +827,7 @@ title: Configuration Reference
|idleTimeout|The max duration to hold a HTTP keepalive connection between calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`475ms`
|maxConnsPerHost|The max number of connections, per unique hostname. Zero means no limit|`int`|`0`
|maxIdleConns|The max number of idle connections to hold pooled|`int`|`100`
+|maxIdleConnsPerHost|The max number of idle connections, per unique hostname. Zero means net/http uses the default of only 2.|`int`|`100`
|passthroughHeadersEnabled|Enable passing through the set of allowed HTTP request headers|`boolean`|`false`
|prefixLong|The prefix that will be used for Fabconnect specific HTTP headers when FireFly makes requests to Fabconnect|`string`|`firefly`
|prefixShort|The prefix that will be used for Fabconnect specific query parameters when FireFly makes requests to Fabconnect|`string`|`fly`
@@ -815,15 +869,25 @@ title: Configuration Reference
|initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms`
|maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
+## plugins.blockchain[].fabric.fabconnect.throttle
+
+|Key|Description|Type|Default Value|
+|---|-----------|----|-------------|
+|burst|The maximum number of requests that can be made in a short period of time before the throttling kicks in.|`int`|``
+|requestsPerSecond|The average rate at which requests are allowed to pass through over time.|`int`|``
+
## plugins.blockchain[].fabric.fabconnect.tls
|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
+|ca|The TLS certificate authority in PEM format (this option is ignored if caFile is also set)|`string`|``
|caFile|The path to the CA file for TLS on this API|`string`|``
+|cert|The TLS certificate in PEM format (this option is ignored if certFile is also set)|`string`|``
|certFile|The path to the certificate file for TLS on this API|`string`|``
|clientAuth|Enables or disables client auth for TLS on this API|`string`|``
|enabled|Enables or disables TLS on this API|`boolean`|`false`
|insecureSkipHostVerify|When to true in unit test development environments to disable TLS verification. Use with extreme caution|`boolean`|``
+|key|The TLS certificate key in PEM format (this option is ignored if keyFile is also set)|`string`|``
|keyFile|The path to the private key file for TLS on this API|`string`|``
|requiredDNAttributes|A set of required subject DN attributes. Each entry is a regular expression, and the subject certificate must have a matching attribute of the specified type (CN, C, O, OU, ST, L, STREET, POSTALCODE, SERIALNUMBER are valid attributes)|`map[string]string`|``
@@ -851,6 +915,7 @@ title: Configuration Reference
|idleTimeout|The max duration to hold a HTTP keepalive connection between calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`475ms`
|maxConnsPerHost|The max number of connections, per unique hostname. Zero means no limit|`int`|`0`
|maxIdleConns|The max number of idle connections to hold pooled|`int`|`100`
+|maxIdleConnsPerHost|The max number of idle connections, per unique hostname. Zero means net/http uses the default of only 2.|`int`|`100`
|method|The HTTP method to use when making requests to the Address Resolver|`string`|`GET`
|passthroughHeadersEnabled|Enable passing through the set of allowed HTTP request headers|`boolean`|`false`
|requestTimeout|The maximum amount of time that a request is allowed to remain open|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
@@ -883,15 +948,25 @@ title: Configuration Reference
|initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms`
|maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
+## plugins.blockchain[].tezos.addressResolver.throttle
+
+|Key|Description|Type|Default Value|
+|---|-----------|----|-------------|
+|burst|The maximum number of requests that can be made in a short period of time before the throttling kicks in.|`int`|``
+|requestsPerSecond|The average rate at which requests are allowed to pass through over time.|`int`|``
+
## plugins.blockchain[].tezos.addressResolver.tls
|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
+|ca|The TLS certificate authority in PEM format (this option is ignored if caFile is also set)|`string`|``
|caFile|The path to the CA file for TLS on this API|`string`|``
+|cert|The TLS certificate in PEM format (this option is ignored if certFile is also set)|`string`|``
|certFile|The path to the certificate file for TLS on this API|`string`|``
|clientAuth|Enables or disables client auth for TLS on this API|`string`|``
|enabled|Enables or disables TLS on this API|`boolean`|`false`
|insecureSkipHostVerify|When to true in unit test development environments to disable TLS verification. Use with extreme caution|`boolean`|``
+|key|The TLS certificate key in PEM format (this option is ignored if keyFile is also set)|`string`|``
|keyFile|The path to the private key file for TLS on this API|`string`|``
|requiredDNAttributes|A set of required subject DN attributes. Each entry is a regular expression, and the subject certificate must have a matching attribute of the specified type (CN, C, O, OU, ST, L, STREET, POSTALCODE, SERIALNUMBER are valid attributes)|`map[string]string`|``
@@ -907,6 +982,7 @@ title: Configuration Reference
|idleTimeout|The max duration to hold a HTTP keepalive connection between calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`475ms`
|maxConnsPerHost|The max number of connections, per unique hostname. Zero means no limit|`int`|`0`
|maxIdleConns|The max number of idle connections to hold pooled|`int`|`100`
+|maxIdleConnsPerHost|The max number of idle connections, per unique hostname. Zero means net/http uses the default of only 2.|`int`|`100`
|passthroughHeadersEnabled|Enable passing through the set of allowed HTTP request headers|`boolean`|`false`
|prefixLong|The prefix that will be used for Tezosconnect specific HTTP headers when FireFly makes requests to Tezosconnect|`string`|`firefly`
|prefixShort|The prefix that will be used for Tezosconnect specific query parameters when FireFly makes requests to Tezosconnect|`string`|`fly`
@@ -947,15 +1023,25 @@ title: Configuration Reference
|initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms`
|maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
+## plugins.blockchain[].tezos.tezosconnect.throttle
+
+|Key|Description|Type|Default Value|
+|---|-----------|----|-------------|
+|burst|The maximum number of requests that can be made in a short period of time before the throttling kicks in.|`int`|``
+|requestsPerSecond|The average rate at which requests are allowed to pass through over time.|`int`|``
+
## plugins.blockchain[].tezos.tezosconnect.tls
|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
+|ca|The TLS certificate authority in PEM format (this option is ignored if caFile is also set)|`string`|``
|caFile|The path to the CA file for TLS on this API|`string`|``
+|cert|The TLS certificate in PEM format (this option is ignored if certFile is also set)|`string`|``
|certFile|The path to the certificate file for TLS on this API|`string`|``
|clientAuth|Enables or disables client auth for TLS on this API|`string`|``
|enabled|Enables or disables TLS on this API|`boolean`|`false`
|insecureSkipHostVerify|When to true in unit test development environments to disable TLS verification. Use with extreme caution|`boolean`|``
+|key|The TLS certificate key in PEM format (this option is ignored if keyFile is also set)|`string`|``
|keyFile|The path to the private key file for TLS on this API|`string`|``
|requiredDNAttributes|A set of required subject DN attributes. Each entry is a regular expression, and the subject certificate must have a matching attribute of the specified type (CN, C, O, OU, ST, L, STREET, POSTALCODE, SERIALNUMBER are valid attributes)|`map[string]string`|``
@@ -1031,6 +1117,7 @@ title: Configuration Reference
|manifestEnabled|Determines whether to require+validate a manifest from other DX instances in the network. Must be supported by the connector|`string`|`false`
|maxConnsPerHost|The max number of connections, per unique hostname. Zero means no limit|`int`|`0`
|maxIdleConns|The max number of idle connections to hold pooled|`int`|`100`
+|maxIdleConnsPerHost|The max number of idle connections, per unique hostname. Zero means net/http uses the default of only 2.|`int`|`100`
|passthroughHeadersEnabled|Enable passing through the set of allowed HTTP request headers|`boolean`|`false`
|requestTimeout|The maximum amount of time that a request is allowed to remain open|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
|tlsHandshakeTimeout|The maximum amount of time to wait for a successful TLS handshake|[`time.Duration`](https://pkg.go.dev/time#Duration)|`10s`
@@ -1076,15 +1163,25 @@ title: Configuration Reference
|initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms`
|maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
+## plugins.dataexchange[].ffdx.throttle
+
+|Key|Description|Type|Default Value|
+|---|-----------|----|-------------|
+|burst|The maximum number of requests that can be made in a short period of time before the throttling kicks in.|`int`|``
+|requestsPerSecond|The average rate at which requests are allowed to pass through over time.|`int`|``
+
## plugins.dataexchange[].ffdx.tls
|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
+|ca|The TLS certificate authority in PEM format (this option is ignored if caFile is also set)|`string`|``
|caFile|The path to the CA file for TLS on this API|`string`|``
+|cert|The TLS certificate in PEM format (this option is ignored if certFile is also set)|`string`|``
|certFile|The path to the certificate file for TLS on this API|`string`|``
|clientAuth|Enables or disables client auth for TLS on this API|`string`|``
|enabled|Enables or disables TLS on this API|`boolean`|`false`
|insecureSkipHostVerify|When to true in unit test development environments to disable TLS verification. Use with extreme caution|`boolean`|``
+|key|The TLS certificate key in PEM format (this option is ignored if keyFile is also set)|`string`|``
|keyFile|The path to the private key file for TLS on this API|`string`|``
|requiredDNAttributes|A set of required subject DN attributes. Each entry is a regular expression, and the subject certificate must have a matching attribute of the specified type (CN, C, O, OU, ST, L, STREET, POSTALCODE, SERIALNUMBER are valid attributes)|`map[string]string`|``
@@ -1124,6 +1221,7 @@ title: Configuration Reference
|idleTimeout|The max duration to hold a HTTP keepalive connection between calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`475ms`
|maxConnsPerHost|The max number of connections, per unique hostname. Zero means no limit|`int`|`0`
|maxIdleConns|The max number of idle connections to hold pooled|`int`|`100`
+|maxIdleConnsPerHost|The max number of idle connections, per unique hostname. Zero means net/http uses the default of only 2.|`int`|`100`
|passthroughHeadersEnabled|Enable passing through the set of allowed HTTP request headers|`boolean`|`false`
|requestTimeout|The maximum amount of time that a request is allowed to remain open|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
|tlsHandshakeTimeout|The maximum amount of time to wait for a successful TLS handshake|[`time.Duration`](https://pkg.go.dev/time#Duration)|`10s`
@@ -1152,15 +1250,25 @@ title: Configuration Reference
|initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms`
|maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
+## plugins.sharedstorage[].ipfs.api.throttle
+
+|Key|Description|Type|Default Value|
+|---|-----------|----|-------------|
+|burst|The maximum number of requests that can be made in a short period of time before the throttling kicks in.|`int`|``
+|requestsPerSecond|The average rate at which requests are allowed to pass through over time.|`int`|``
+
## plugins.sharedstorage[].ipfs.api.tls
|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
+|ca|The TLS certificate authority in PEM format (this option is ignored if caFile is also set)|`string`|``
|caFile|The path to the CA file for TLS on this API|`string`|``
+|cert|The TLS certificate in PEM format (this option is ignored if certFile is also set)|`string`|``
|certFile|The path to the certificate file for TLS on this API|`string`|``
|clientAuth|Enables or disables client auth for TLS on this API|`string`|``
|enabled|Enables or disables TLS on this API|`boolean`|`false`
|insecureSkipHostVerify|When to true in unit test development environments to disable TLS verification. Use with extreme caution|`boolean`|``
+|key|The TLS certificate key in PEM format (this option is ignored if keyFile is also set)|`string`|``
|keyFile|The path to the private key file for TLS on this API|`string`|``
|requiredDNAttributes|A set of required subject DN attributes. Each entry is a regular expression, and the subject certificate must have a matching attribute of the specified type (CN, C, O, OU, ST, L, STREET, POSTALCODE, SERIALNUMBER are valid attributes)|`map[string]string`|``
@@ -1174,6 +1282,7 @@ title: Configuration Reference
|idleTimeout|The max duration to hold a HTTP keepalive connection between calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`475ms`
|maxConnsPerHost|The max number of connections, per unique hostname. Zero means no limit|`int`|`0`
|maxIdleConns|The max number of idle connections to hold pooled|`int`|`100`
+|maxIdleConnsPerHost|The max number of idle connections, per unique hostname. Zero means net/http uses the default of only 2.|`int`|`100`
|passthroughHeadersEnabled|Enable passing through the set of allowed HTTP request headers|`boolean`|`false`
|requestTimeout|The maximum amount of time that a request is allowed to remain open|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
|tlsHandshakeTimeout|The maximum amount of time to wait for a successful TLS handshake|[`time.Duration`](https://pkg.go.dev/time#Duration)|`10s`
@@ -1202,15 +1311,25 @@ title: Configuration Reference
|initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms`
|maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
+## plugins.sharedstorage[].ipfs.gateway.throttle
+
+|Key|Description|Type|Default Value|
+|---|-----------|----|-------------|
+|burst|The maximum number of requests that can be made in a short period of time before the throttling kicks in.|`int`|``
+|requestsPerSecond|The average rate at which requests are allowed to pass through over time.|`int`|``
+
## plugins.sharedstorage[].ipfs.gateway.tls
|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
+|ca|The TLS certificate authority in PEM format (this option is ignored if caFile is also set)|`string`|``
|caFile|The path to the CA file for TLS on this API|`string`|``
+|cert|The TLS certificate in PEM format (this option is ignored if certFile is also set)|`string`|``
|certFile|The path to the certificate file for TLS on this API|`string`|``
|clientAuth|Enables or disables client auth for TLS on this API|`string`|``
|enabled|Enables or disables TLS on this API|`boolean`|`false`
|insecureSkipHostVerify|When to true in unit test development environments to disable TLS verification. Use with extreme caution|`boolean`|``
+|key|The TLS certificate key in PEM format (this option is ignored if keyFile is also set)|`string`|``
|keyFile|The path to the private key file for TLS on this API|`string`|``
|requiredDNAttributes|A set of required subject DN attributes. Each entry is a regular expression, and the subject certificate must have a matching attribute of the specified type (CN, C, O, OU, ST, L, STREET, POSTALCODE, SERIALNUMBER are valid attributes)|`map[string]string`|``
@@ -1232,6 +1351,7 @@ title: Configuration Reference
|idleTimeout|The max duration to hold a HTTP keepalive connection between calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`475ms`
|maxConnsPerHost|The max number of connections, per unique hostname. Zero means no limit|`int`|`0`
|maxIdleConns|The max number of idle connections to hold pooled|`int`|`100`
+|maxIdleConnsPerHost|The max number of idle connections, per unique hostname. Zero means net/http uses the default of only 2.|`int`|`100`
|passthroughHeadersEnabled|Enable passing through the set of allowed HTTP request headers|`boolean`|`false`
|requestTimeout|The maximum amount of time that a request is allowed to remain open|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
|tlsHandshakeTimeout|The maximum amount of time to wait for a successful TLS handshake|[`time.Duration`](https://pkg.go.dev/time#Duration)|`10s`
@@ -1277,15 +1397,25 @@ title: Configuration Reference
|initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms`
|maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s`
+## plugins.tokens[].fftokens.throttle
+
+|Key|Description|Type|Default Value|
+|---|-----------|----|-------------|
+|burst|The maximum number of requests that can be made in a short period of time before the throttling kicks in.|`int`|``
+|requestsPerSecond|The average rate at which requests are allowed to pass through over time.|`int`|``
+
## plugins.tokens[].fftokens.tls
|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
+|ca|The TLS certificate authority in PEM format (this option is ignored if caFile is also set)|`string`|``
|caFile|The path to the CA file for TLS on this API|`string`|``
+|cert|The TLS certificate in PEM format (this option is ignored if certFile is also set)|`string`|``
|certFile|The path to the certificate file for TLS on this API|`string`|``
|clientAuth|Enables or disables client auth for TLS on this API|`string`|``
|enabled|Enables or disables TLS on this API|`boolean`|`false`
|insecureSkipHostVerify|When to true in unit test development environments to disable TLS verification. Use with extreme caution|`boolean`|``
+|key|The TLS certificate key in PEM format (this option is ignored if keyFile is also set)|`string`|``
|keyFile|The path to the private key file for TLS on this API|`string`|``
|requiredDNAttributes|A set of required subject DN attributes. Each entry is a regular expression, and the subject certificate must have a matching attribute of the specified type (CN, C, O, OU, ST, L, STREET, POSTALCODE, SERIALNUMBER are valid attributes)|`map[string]string`|``
@@ -1346,11 +1476,14 @@ title: Configuration Reference
|Key|Description|Type|Default Value|
|---|-----------|----|-------------|
+|ca|The TLS certificate authority in PEM format (this option is ignored if caFile is also set)|`string`|``
|caFile|The path to the CA file for TLS on this API|`string`|``
+|cert|The TLS certificate in PEM format (this option is ignored if certFile is also set)|`string`|``
|certFile|The path to the certificate file for TLS on this API|`string`|``
|clientAuth|Enables or disables client auth for TLS on this API|`string`|``
|enabled|Enables or disables TLS on this API|`boolean`|`false`
|insecureSkipHostVerify|When to true in unit test development environments to disable TLS verification. Use with extreme caution|`boolean`|``
+|key|The TLS certificate key in PEM format (this option is ignored if keyFile is also set)|`string`|``
|keyFile|The path to the private key file for TLS on this API|`string`|``
|requiredDNAttributes|A set of required subject DN attributes. Each entry is a regular expression, and the subject certificate must have a matching attribute of the specified type (CN, C, O, OU, ST, L, STREET, POSTALCODE, SERIALNUMBER are valid attributes)|`map[string]string`|``
diff --git a/doc-site/docs/reference/events.md b/doc-site/docs/reference/events.md
index faf51cc390..ee3ba1f71b 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/firefly_interface_format.md b/doc-site/docs/reference/firefly_interface_format.md
index e9d1238564..3a52a2ce99 100644
--- a/doc-site/docs/reference/firefly_interface_format.md
+++ b/doc-site/docs/reference/firefly_interface_format.md
@@ -98,7 +98,7 @@ For example, the Ethereum plugin always needs to know what Solidity type the fie
## Automated generation of FireFly Interfaces
-A convenience endpoint exists on the API to facilitate converting from native blockchain interface formats such as an Ethereum ABI to the FireFly Interface format. For details, please see the API documentation for the contract interface generation endpoint.
+A convenience endpoint exists on the API to facilitate converting from native blockchain interface formats such as an Ethereum ABI to the FireFly Interface format. For details, please see the API documentation for the contract interface generation endpoint.
For an example of using this endpoint with a specific Ethereum contract, please see the [Tutorial to Work with custom smart contracts](../tutorials/custom_contracts/index.md).
diff --git a/doc-site/docs/reference/types/_includes/blockchainevent_description.md b/doc-site/docs/reference/types/_includes/blockchainevent_description.md
index 67d37f978e..85546259ea 100644
--- a/doc-site/docs/reference/types/_includes/blockchainevent_description.md
+++ b/doc-site/docs/reference/types/_includes/blockchainevent_description.md
@@ -25,8 +25,8 @@ the blockchain plugins to try and create some consistency.
An example `protocolId` string is: `000000000041/000020/000003`
- `000000000041` - this is the block number
-- `000020` - this is the transaction index within that block
-- `000003` - this is the event (/log) index within that transaction
+- `000020` - this is the **transaction** index within that **block**
+- `000003` - this is the **event (/log)** index within that **block**
The string is alphanumerically sortable as a plain string;
diff --git a/doc-site/docs/reference/types/_includes/contractlistener_description.md b/doc-site/docs/reference/types/_includes/contractlistener_description.md
index e6c1721b8f..5027b71ade 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/_includes/ffbigint_description.md b/doc-site/docs/reference/types/_includes/ffbigint_description.md
index 497b545b16..e9b6762446 100644
--- a/doc-site/docs/reference/types/_includes/ffbigint_description.md
+++ b/doc-site/docs/reference/types/_includes/ffbigint_description.md
@@ -7,5 +7,49 @@ strings (with base 10).
On input you can provide JSON string (string with an `0x` prefix are
parsed at base 16), or a JSON number.
-Be careful when using JSON numbers, that the largest
-number that is safe to transfer using a JSON number is 2^53 - 1.
\ No newline at end of file
+## Maximum size of numbers in versions of FireFly up to `v1.3.1`
+
+In versions of FireFly up to and including `v1.3.1`, be careful when using large JSON numbers. The largest number that is safe to transfer using a JSON number is 2^53 - 1 and it is
+possible to receive errors from the transaction manager, or for precision to be silently lost when passing numeric parameters larger than that. It is recommended to pass large numbers as strings to avoid loss of precision.
+
+## Maximum size of numbers in versions of FireFly `v1.3.2` and higher
+
+In FireFly `v1.3.2` support was added for 256-bit precision JSON numbers. Some application frameworks automatically serialize large JSON numbers to a string which FireFly already supports, but there is no upper limit
+to the size of a number that can be represented in JSON. FireFly now supports much larger JSON numbers, up to 256-bit precision. For example the following input parameter to a contract constructor is now supported:
+
+```
+ ...
+ "definition": [{
+ "inputs": [
+ {
+ "internalType":" uint256",
+ "name": "x",
+ "type": "uint256"
+ }
+ ],
+ "outputs":[],
+ "type":"constructor"
+ }],
+ "params": [ 10000000000000000000000000 ]
+ ...
+```
+
+Some application frameworks seralize large numbers in scientific notation e.g. `1e+25`. FireFly `v1.3.2` added supported for handling scientific numbers in parameters. This removes the need to change an application
+that uses this number format. For example the following input parameter to a contract constructor is now supported:
+
+```
+ ...
+ "definition": [{
+ "inputs": [
+ {
+ "internalType":" uint256",
+ "name": "x",
+ "type": "uint256"
+ }
+ ],
+ "outputs":[],
+ "type":"constructor"
+ }],
+ "params": [ 1e+25 ]
+ ...
+```
diff --git a/doc-site/docs/reference/types/contractlistener.md b/doc-site/docs/reference/types/contractlistener.md
index 9b73e12332..9b5b13415a 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)"
+ }
+ ]
}
```
@@ -47,16 +71,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 +117,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/releasenotes/index.md b/doc-site/docs/releasenotes/index.md
index 01abc7415f..9e8eddb499 100644
--- a/doc-site/docs/releasenotes/index.md
+++ b/doc-site/docs/releasenotes/index.md
@@ -4,6 +4,26 @@ title: Release Notes
[Full release notes](https://github.com/hyperledger/firefly/releases)
+## [v1.3.2 - Oct 3, 2024](https://github.com/hyperledger/firefly/releases/tag/v1.3.2)
+
+What's New:
+
+- Support for JSON numbers larger than `2^53-1`
+ - See [FFBigInt](../reference/types/simpletypes.md#ffbigint) for detailed explanation
+ - Support added to FireFly core, including the UI, FireFly Transaction Manager, and FireFly EVMConnect
+- Ability to install FireFly CLI with Brew for MacOS users
+ See [Brew](../gettingstarted/firefly_cli.md#install-via-homebrew-macOS)
+- Miscellaneous bug fixes and minor improvements
+- FireFly has been upgraded to use Go 1.22
+
+## [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/swagger/swagger.yaml b/doc-site/docs/swagger/swagger.yaml
index 60d2995996..60a2e7b24d 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
@@ -5069,7 +5268,8 @@ paths:
description: Invokes a method on a smart contract. Performs a blockchain transaction.
operationId: postContractInvoke
parameters:
- - description: When true the HTTP request blocks until the message is confirmed
+ - description: When true the HTTP request blocks until the blockchain transaction
+ is confirmed
in: query
name: confirm
schema:
@@ -5539,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
@@ -5629,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
@@ -5667,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
@@ -5687,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
@@ -5709,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
@@ -5743,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
@@ -5781,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
@@ -5801,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
@@ -5839,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
@@ -5877,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
@@ -5897,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
@@ -5919,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
@@ -6000,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
@@ -6038,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
@@ -6058,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
@@ -6080,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
@@ -6095,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)
@@ -6112,11 +6627,203 @@ 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
+ 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:
@@ -13279,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
@@ -13369,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
@@ -13407,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
@@ -13427,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
@@ -13449,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
@@ -13503,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
@@ -13540,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
@@ -13556,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
@@ -13594,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
@@ -13632,18 +14495,97 @@ 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
- 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
+ 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
type: string
@@ -13652,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
@@ -13674,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
@@ -17476,7 +18417,8 @@ paths:
schema:
example: default
type: string
- - description: When true the HTTP request blocks until the message is confirmed
+ - description: When true the HTTP request blocks until the blockchain transaction
+ is confirmed
in: query
name: confirm
schema:
@@ -17953,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
@@ -18043,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
@@ -18081,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
@@ -18101,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
@@ -18123,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
@@ -18164,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
@@ -18202,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
@@ -18222,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
@@ -18260,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
@@ -18298,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
@@ -18318,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
@@ -18340,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
@@ -18435,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
@@ -18473,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
@@ -18493,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
@@ -18515,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
@@ -18530,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/doc-site/docs/troubleshooting/index.md b/doc-site/docs/troubleshooting/index.md
new file mode 100644
index 0000000000..7b7722d95d
--- /dev/null
+++ b/doc-site/docs/troubleshooting/index.md
@@ -0,0 +1,5 @@
+---
+title: Troubleshooting
+---
+
+This section includes troubleshooting tips for identifying issues with a running FireFly node, and for gathering useful data before opening an issue.
diff --git a/doc-site/docs/troubleshooting/undelivered_messages.md b/doc-site/docs/troubleshooting/undelivered_messages.md
new file mode 100644
index 0000000000..3abfe1a71a
--- /dev/null
+++ b/doc-site/docs/troubleshooting/undelivered_messages.md
@@ -0,0 +1,95 @@
+---
+title: Undelivered messages
+---
+
+When using FireFly in multiparty mode to deliver broadcast or private messages, one potential problem is that of
+undelivered messages. In general FireFly's message delivery service should be extremely reliable, but understanding
+when something has gone wrong (and how to recover) can be important for maintaining system health.
+
+## Background
+
+This guide assumes some familiarity with how
+[multiparty event sequencing](../architecture/multiparty_event_sequencing.md) works.
+In general, FireFly messages come in three varieties:
+
+![FireFly Message Types](../images/firefly_message_types.png "FireFly Message Types")
+
+1. **Unpinned private messages:** private messages delivered directly via data exchange
+2. **Pinned private messages:** private messages delivered via data exchange, with a hash of the message recorded on the blockchain ledger
+3. **Pinned broadcast messages:** messages stored in IPFS, with a hash and reference to the message shared
+
+All messages are batched for efficiency, but in cases of low throughput, you may frequently see batches
+containing exactly one message.
+
+"Pinned" messages are those that use the blockchain ledger for reliable timestamping and ordering. These messages have
+two pieces which must be received before the message can be processed: the **batch** is the actual contents of
+the message(s), and the **pin** is the lightweight blockchain transaction that records the existence and ordering of
+that batch. We frequently refer to this combination as a **batch-pin**.
+
+> Note: there is a fourth type of message denoted with the type "definition", used for things such as identitity claims
+> and advertisement of contract APIs. For most troubleshooting purposes these can be treated the same as pinned
+> broadcast messages, as they follow the same pattern (with only a few additional processings steps inside FireFly).
+
+## Symptoms
+
+When some part of the multiparty messaging infrastructure requires troubleshooting, common symptoms include:
+
+- a message was sent, but is not present on some other node where it should have been received
+- a message is stuck indefinitely in "sent" or "pending" state
+
+## Troubleshooting steps
+
+When troubleshooting one of the symptoms above, the main goal is to identify the specific piece of the infrastructure that is
+experiencing an issue. This can lead you to diagnose specific issues such as misconfiguration, network problems, database
+integrity problems, or potential code bugs.
+
+In all cases, the **batch ID** is the most critical piece of data for determining the nature of the issue. You can usually
+retrieve the batch for a particular message by querying `/messages/` and looking for the `batch` field in the returned
+response. In rare cases, if this is not populated, you can also retrieve the message transaction via `/messages//transaction`,
+and then you can use the transaction ID to query `/batches?tx.id=`.
+
+The batch ID will be the same on all nodes involved in the messaging flow. Therefore, the following two steps can be
+easily performed to check for the existence of the expected items:
+
+- query `/batches/` on each node that should have the message
+- query `/pins?batch=` on each node that should have the message (for pinned messages only)
+
+Then choose one of these scenarios to focus in on an area of interest:
+
+#### 1) Is the batch missing on a node that should have received it?
+
+For private messages, this indicates a potential problem with **data exchange**. Check the sending node to see if the FireFly
+operations succeeded when sending the batch via data exchange, and check the data exchange logs for any issues processing it
+(the FireFly operation ID can be used to trace the operation through data exchange as well).
+If an operation failed on the sending node, you may need to retry it with `/operations//retry`.
+
+For broadcast messages, this indicates a potential problem with **IPFS**. Check the sending node to see if the FireFly
+operations succeeded when uploading the batch to IPFS, and the receiving node to see if the operations succeeded when
+downloading the batch from IPFS. If an operation failed, you may need to retry it with `/operations//retry`.
+
+#### 2) Is the batch present, but the pin is missing?
+
+This indicates a potential problem with the **blockchain connector**. Check if the underlying blockchain node is
+healthy and mining blocks. Check the sending FireFly node to see if the operation succeeded when pinning the batch via the
+blockchain. Check the blockchain connector logs (such as evmconnect or fabconnect) to see if it is
+successfully processing events from the blockchain, or if it is encountering any errors before forwarding those events
+on to FireFly.
+
+#### 3) Are the batch and pin both present, but the messages from the batch are still stuck in "sent" or "pending"?
+
+Check the pin details to see if it contains a field `"dispatched": true`. If this field is false or missing, it means
+that the pin was received but couldn't be matched successfully with the off-chain batch contents. Check the FireFly
+logs and search for the batch ID - likely this issue is in FireFly and it will have logged some problem while
+aggregating the batch-pin. In some cases, the FireFly logs may indicate that the pin could not be dispatched because
+it was "stuck" behind another pin on the same context - so you may need to follow the trail to a batch-pin for a
+different batch and determine why that earlier one was not processed (by starting over on this rubric
+and troubleshooting that batch).
+
+## Opening an issue
+
+It's possible that the above steps may lead to an obvious solution (such as recovering a crashed service or retrying a
+failed operation). If they do not, you can open an issue. The more detail you can include from the troubleshooting above
+(including the type of message, the nodes involved, and the details on the batch and pin found when examining each node),
+the more likely it is that someone can help to suggest additional troubleshooting. Full logs from FireFly, and (as
+deemed relevant from the troubleshooting above) full logs from the data exchange or blockchain connector runtimes, will
+also make it easier to offer additional insight.
diff --git a/doc-site/docs/tutorials/broadcast_data.md b/doc-site/docs/tutorials/broadcast_data.md
index 5ea6d7aed1..86ca25a20f 100644
--- a/doc-site/docs/tutorials/broadcast_data.md
+++ b/doc-site/docs/tutorials/broadcast_data.md
@@ -20,7 +20,7 @@ title: Broadcast data
## Additional info
- Key Concepts: [Broadcast / shared data](../overview/multiparty/broadcast.md)
-- Swagger Reference: POST /api/v1/namespaces/{ns}/messages/broadcast
+- Swagger Reference: POST /api/v1/namespaces/{ns}/messages/broadcast
## Example 1: Inline string data
diff --git a/doc-site/docs/tutorials/chains/avalanche.md b/doc-site/docs/tutorials/chains/avalanche.md
index 01f0f10cf0..8255dc6ba6 100644
--- a/doc-site/docs/tutorials/chains/avalanche.md
+++ b/doc-site/docs/tutorials/chains/avalanche.md
@@ -1,5 +1,5 @@
---
-title: Aavalanche
+title: Avalanche
---
Starting with FireFly v1.1, it's easy to connect to public Ethereum chains. This guide will walk you through the steps to create a local FireFly development environment and connect it to the Avalanche C-Chain Fuji testnet.
diff --git a/doc-site/docs/tutorials/chains/index.md b/doc-site/docs/tutorials/chains/index.md
deleted file mode 100644
index 792e6a71e9..0000000000
--- 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/create_custom_identity.md b/doc-site/docs/tutorials/create_custom_identity.md
index 6524ef93bb..52624f24cd 100644
--- a/doc-site/docs/tutorials/create_custom_identity.md
+++ b/doc-site/docs/tutorials/create_custom_identity.md
@@ -11,7 +11,7 @@ Out of the box, a FireFly Supernode contains both an `org` and a `node` identity
## Additional info
- Reference: [Identities](../reference/identities.md)
-- Swagger: POST /api/v1/identities
+- Swagger: POST /api/v1/identities
## Previous steps: Start your environment
diff --git a/doc-site/docs/tutorials/custom_contracts/ethereum.md b/doc-site/docs/tutorials/custom_contracts/ethereum.md
index 52e91f936e..26de3d47c4 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 2b4723ca1a..037a3a19e5 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/doc-site/docs/tutorials/custom_contracts/index.md b/doc-site/docs/tutorials/custom_contracts/index.md
index 449099a1e0..70a4467cd0 100644
--- a/doc-site/docs/tutorials/custom_contracts/index.md
+++ b/doc-site/docs/tutorials/custom_contracts/index.md
@@ -12,23 +12,32 @@ FireFly's unified API creates a consistent application experience regardless of
FireFly defines the following constructs to support custom smart contracts:
-- **Contract Interface**: FireFly defines a common, blockchain agnostic way to describe smart contracts. This is referred to as a Contract Interface. A contract interface is written in the FireFly Interface (FFI) format. It is a simple JSON document that has a name, a namespace, a version, a list of methods, and a list of events.
+### Contract Interface
+
+FireFly defines a common, blockchain agnostic way to describe smart contracts. This is referred to as a Contract Interface. A contract interface is written in the FireFly Interface (FFI) format. It is a simple JSON document that has a name, a namespace, a version, a list of methods, and a list of events.
For more details, you can also have a look at the [Reference page for the FireFly Interface Format](../../reference/firefly_interface_format.md).
-For blockchains that offer a DSL describing the smart contract interface, such as Ethereum's ABI (Application Binary Interface), FireFly offers a convenience tool to convert the DSL into the FFI format.
+For blockchains that offer a DSL describing the smart contract interface, such as Ethereum's ABI (Application Binary Interface), FireFly offers an API to [convert the DSL into the FFI format](../custom_contracts/ethereum.md#the-firefly-interface-format).
+
> **NOTE**: Contract interfaces are scoped to a namespace. Within a namespace each contract interface must have a unique name and version combination. The same name and version combination can exist in _different_ namespaces simultaneously.
-- **HTTP API**: Based on a Contract Interface, FireFly further defines an HTTP API for the smart contract, which is complete with an OpenAPI Specification and the Swagger UI. An HTTP API defines an `/invoke` root path to submit transactions, and a `/query` root path to send query requests to read the state back out.
+### HTTP API
+
+Based on a Contract Interface, FireFly further defines an HTTP API for the smart contract, which is complete with an OpenAPI Specification and the Swagger UI. An HTTP API defines an `/invoke` root path to submit transactions, and a `/query` root path to send query requests to read the state back out.
-How the invoke vs. query requests get interpreted into the native blockchain requests are specific to the blockchain's connector. For instance, the Ethereum connector translates `/invoke` calls to `eth_sendTransaction` JSON-RPC requests, while `/query` calls are translated into `eth_call` JSON-RPC requests. One the other hand, the Fabric connector translates `/invoke` calls to the multiple requests required to submit a transaction to a Fabric channel (which first collects endorsements from peer nodes, and then sends the assembled transaction payload to an orderer, for details please refer to the Fabric documentation).
+How the invoke vs. query requests get interpreted into the native blockchain requests are specific to the blockchain's connector. For instance, the Ethereum connector translates `/invoke` calls to `eth_sendTransaction` JSON-RPC requests, while `/query` calls are translated into `eth_call` JSON-RPC requests. On the other hand, the Fabric connector translates `/invoke` calls to the multiple requests required to submit a transaction to a Fabric channel (which first collects endorsements from peer nodes, and then sends the assembled transaction payload to an orderer, for details please refer to the Fabric documentation).
-- **Blockchain Event Listener**: Regardless of a blockchain's specific design, transaction processing are always asynchronous. This means a transaction is submitted to the network, at which point the submitting client gets an acknowledgement that it has been accepted for further processing. The client then listens for notifications by the blockchain when the transaction gets committed to the blockchain's ledger.
+### Blockchain Event Listener
+
+Regardless of a blockchain's specific design, transaction processing are always asynchronous. This means a transaction is submitted to the network, at which point the submitting client gets an acknowledgement that it has been accepted for further processing. The client then listens for notifications by the blockchain when the transaction gets committed to the blockchain's ledger.
FireFly defines event listeners to allow the client application to specify the relevant blockchain events to keep track of. A client application can then receive the notifications from FireFly via an event subscription.
-- **Event Subscription**: While an event listener tells FireFly to keep track of certain events emitted by the blockchain, an event subscription tells FireFly to relay those events to the client application. Each subscriptions represents a stream of events that can be delivered to a listening client with various modes of delivery with at-least-once delivery guarantee.
+### Event Subscription
+
+An event listener in FireFly tracks specific blockchain events, while an event subscription directs FireFly to send those events to the client application. Each subscription creates a stream of events that can be delivered to the client with various delivery options, ensuring an at-least-once delivery guarantee.
This is exactly the same as listening for any other events from FireFly. For more details on how Subscriptions work in FireFly you can read the [Getting Started guide to Listen for events](../events.md).
diff --git a/doc-site/docs/tutorials/define_datatype.md b/doc-site/docs/tutorials/define_datatype.md
index fd24901e77..17fa24de45 100644
--- a/doc-site/docs/tutorials/define_datatype.md
+++ b/doc-site/docs/tutorials/define_datatype.md
@@ -20,7 +20,7 @@ of datatypes, as is used to broadcast the data itself.
## Additional info
- Key Concepts: [Broadcast / shared data](../overview/multiparty/broadcast.md)
-- Swagger: POST /api/v1/namespaces/{ns}/datatypes
+- Swagger: POST /api/v1/namespaces/{ns}/datatypes
### Example 1: Broadcast new datatype
diff --git a/doc-site/docs/tutorials/private_send.md b/doc-site/docs/tutorials/private_send.md
index 1d40c1bf0f..63d71f1324 100644
--- a/doc-site/docs/tutorials/private_send.md
+++ b/doc-site/docs/tutorials/private_send.md
@@ -37,7 +37,7 @@ title: Privately send data
## Additional info
- Key Concepts: [Private data exchange](../overview/multiparty/data_exchange.md)
-- Swagger: POST /api/v1/namespaces/{ns}/messages/private
+- Swagger: POST /api/v1/namespaces/{ns}/messages/private
## Example 1: Pinned private send of in-line string data
diff --git a/doc-site/docs/tutorials/query_messages.md b/doc-site/docs/tutorials/query_messages.md
index a221ddd466..6bdf2ee328 100644
--- a/doc-site/docs/tutorials/query_messages.md
+++ b/doc-site/docs/tutorials/query_messages.md
@@ -17,7 +17,7 @@ This builds on the APIs to query and filter messages, described below
## Additional info
- Reference: [API Query Syntax](../reference/api_query_syntax.md)
-- Swagger: GET /api/v1/namespaces/{ns}/messages
+- Swagger: GET /api/v1/namespaces/{ns}/messages
### Example 1: Query confirmed messages
diff --git a/ffconfig/main_test.go b/ffconfig/main_test.go
new file mode 100644
index 0000000000..f7b1365613
--- /dev/null
+++ b/ffconfig/main_test.go
@@ -0,0 +1,106 @@
+// 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 main
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+var configPath string = "../test/data/config/firefly.core.yaml"
+
+func TestMainFail(t *testing.T) {
+ // Run the crashing code when FLAG is set
+ if os.Getenv("FLAG") == "1" {
+ main()
+ return
+ }
+ // Run the test in a subprocess
+ cmd := exec.Command(os.Args[0], "-test.run=TestMainFail")
+ cmd.Env = append(os.Environ(), "FLAG=1")
+ err := cmd.Run()
+
+ // Cast the error as *exec.ExitError and compare the result
+ e, ok := err.(*exec.ExitError)
+ expectedErrorString := "exit status 1"
+ assert.Equal(t, true, ok)
+ assert.Equal(t, expectedErrorString, e.Error())
+}
+
+func TestConfigMigrateRootCmdErrorNoArgs(t *testing.T) {
+ rootCmd.SetArgs([]string{})
+ defer rootCmd.SetArgs([]string{})
+ err := rootCmd.Execute()
+ assert.Error(t, err)
+ assert.Regexp(t, "a command is required", err)
+}
+
+func TestConfigMigrateCmdMissingConfig(t *testing.T) {
+ rootCmd.SetArgs([]string{"migrate"})
+ defer rootCmd.SetArgs([]string{})
+ err := rootCmd.Execute()
+ assert.Error(t, err)
+ assert.Regexp(t, "no such file or directory", err)
+}
+
+func TestConfigMigrateCmd(t *testing.T) {
+ rootCmd.SetArgs([]string{"migrate", "-f", configPath})
+ defer rootCmd.SetArgs([]string{})
+ err := rootCmd.Execute()
+ assert.NoError(t, err)
+}
+
+func TestMain(t *testing.T) {
+ // Run the exiting code when FLAG is set
+ if os.Getenv("FLAG") == "0" {
+ rootCmd.SetArgs([]string{"migrate", "-f", configPath})
+ main()
+ return
+ }
+
+ // Run the test in a subprocess
+ cmd := exec.Command(os.Args[0], "-test.run=TestMain")
+ cmd.Env = append(os.Environ(), "FLAG=0")
+ err := cmd.Run()
+
+ // Cast the error as *exec.ExitError and compare the result
+ _, ok := err.(*exec.ExitError)
+ assert.Equal(t, false, ok)
+}
+
+func TestConfigMigrateCmdWriteOutput(t *testing.T) {
+ tmpDir, err := os.MkdirTemp(os.TempDir(), "out")
+ assert.NoError(t, err)
+ defer os.RemoveAll(tmpDir)
+
+ rootCmd.SetArgs([]string{"migrate", "-f", configPath, "-o", fmt.Sprintf(tmpDir, "out.config")})
+ defer rootCmd.SetArgs([]string{})
+ err = rootCmd.Execute()
+ assert.NoError(t, err)
+}
+
+func TestConfigMigrateCmdBadVersion(t *testing.T) {
+ rootCmd.SetArgs([]string{"migrate", "-f", configPath, "--from", "badversion"})
+ defer rootCmd.SetArgs([]string{})
+ err := rootCmd.Execute()
+ assert.Error(t, err)
+ assert.Regexp(t, "bad 'from' version", err)
+}
diff --git a/go.mod b/go.mod
index 2d6c72a2a3..e3e45000a3 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,8 @@
module github.com/hyperledger/firefly
-go 1.21
+go 1.22
+
+toolchain go1.22.7
require (
blockwatch.cc/tzgo v1.17.1
@@ -15,8 +17,8 @@ require (
github.com/golang-migrate/migrate/v4 v4.17.0
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.1
- github.com/hyperledger/firefly-common v1.4.6
- github.com/hyperledger/firefly-signer v1.1.12
+ github.com/hyperledger/firefly-common v1.4.14
+ github.com/hyperledger/firefly-signer v1.1.19
github.com/jarcoal/httpmock v1.2.0
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.19
@@ -28,8 +30,8 @@ require (
github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.8.4
gitlab.com/hfuss/mux-prometheus v0.0.5
- golang.org/x/net v0.20.0
- golang.org/x/text v0.14.0
+ golang.org/x/net v0.33.0
+ golang.org/x/text v0.21.0
gopkg.in/yaml.v2 v2.4.0
)
@@ -70,7 +72,7 @@ require (
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
- github.com/rs/cors v1.10.1 // indirect
+ github.com/rs/cors v1.11.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
@@ -83,10 +85,11 @@ require (
github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
- golang.org/x/crypto v0.18.0 // indirect
+ golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e // indirect
- golang.org/x/sys v0.16.0 // indirect
- golang.org/x/term v0.16.0 // indirect
+ golang.org/x/sys v0.28.0 // indirect
+ golang.org/x/term v0.27.0 // indirect
+ golang.org/x/time v0.5.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
diff --git a/go.sum b/go.sum
index cf382953b7..80e992c5bf 100644
--- a/go.sum
+++ b/go.sum
@@ -77,10 +77,10 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
-github.com/hyperledger/firefly-common v1.4.6 h1:qqXoSaRml3WjUnWcWxrrXs5AIOWa+UcMXLCF8yEa4Pk=
-github.com/hyperledger/firefly-common v1.4.6/go.mod h1:jkErZdQmC9fsAJZQO427tURdwB9iiW+NMUZSqS3eBIE=
-github.com/hyperledger/firefly-signer v1.1.12 h1:wv1cq4HV60G2MQdmIEkYkywoxUSkaH0ss95Nn3ohdEk=
-github.com/hyperledger/firefly-signer v1.1.12/go.mod h1:4MW7bcTqPsS7SKwANJZRL030cJRsHcpB/a+06wUROvc=
+github.com/hyperledger/firefly-common v1.4.14 h1:G1x7jKBM2MmbGAo+Hwu/9w3F4cyGuWvYViEZGPLWlic=
+github.com/hyperledger/firefly-common v1.4.14/go.mod h1:tYTzTbVODv/gx0TJ3TkEb+gUieQiAbqLfj/yFNrlDV4=
+github.com/hyperledger/firefly-signer v1.1.19 h1:Gq5HqUp9/7egLrahJY9WMk4Y9dZVPIl99aSIged93HM=
+github.com/hyperledger/firefly-signer v1.1.19/go.mod h1:XTwaPRkAfVxk2G3PQOYHLbuvMOiBs0px/4vwXTsUtsA=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY=
@@ -163,8 +163,8 @@ github.com/qeesung/image2ascii v1.0.1 h1:Fe5zTnX/v/qNC3OC4P/cfASOXS501Xyw2UUcgrL
github.com/qeesung/image2ascii v1.0.1/go.mod h1:kZKhyX0h2g/YXa/zdJR3JnLnJ8avHjZ3LrvEKSYyAyU=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
-github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo=
-github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
+github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
+github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
@@ -218,25 +218,27 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
-golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
-golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
+golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e h1:723BNChdd0c2Wk6WOE320qGBiPtYx0F0Bbm1kriShfE=
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
-golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
-golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
-golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
+golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -249,23 +251,23 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
-golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
-golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
-golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
+golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
+golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
@@ -273,8 +275,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM=
-golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
diff --git a/go.work b/go.work
index 28b0fad5d2..80c90d39e5 100644
--- a/go.work
+++ b/go.work
@@ -1,4 +1,6 @@
-go 1.21
+go 1.22
+
+toolchain go1.22.7
use (
.
diff --git a/go.work.sum b/go.work.sum
index ee356555a1..6ce247df52 100644
--- a/go.work.sum
+++ b/go.work.sum
@@ -253,6 +253,10 @@ github.com/hashicorp/go-memdb v1.3.3/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYi
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
+github.com/hyperledger/firefly-common v1.4.14 h1:G1x7jKBM2MmbGAo+Hwu/9w3F4cyGuWvYViEZGPLWlic=
+github.com/hyperledger/firefly-common v1.4.14/go.mod h1:tYTzTbVODv/gx0TJ3TkEb+gUieQiAbqLfj/yFNrlDV4=
+github.com/hyperledger/firefly-signer v1.1.19 h1:Gq5HqUp9/7egLrahJY9WMk4Y9dZVPIl99aSIged93HM=
+github.com/hyperledger/firefly-signer v1.1.19/go.mod h1:XTwaPRkAfVxk2G3PQOYHLbuvMOiBs0px/4vwXTsUtsA=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E=
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
@@ -317,8 +321,14 @@ go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7
go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
google.golang.org/api v0.153.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
diff --git a/internal/apiserver/ffi2swagger.go b/internal/apiserver/ffi2swagger.go
index c6f63d1c23..b5699b50d5 100644
--- a/internal/apiserver/ffi2swagger.go
+++ b/internal/apiserver/ffi2swagger.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2023 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -89,6 +89,9 @@ func addFFIMethod(ctx context.Context, routes []*ffapi.Route, method *fftypes.FF
Name: fmt.Sprintf("invoke_%s", method.Pathname),
Path: fmt.Sprintf("invoke/%s", method.Pathname), // must match a route defined in apiserver routes!
Method: http.MethodPost,
+ QueryParams: []*ffapi.QueryParam{
+ {Name: "confirm", Description: coremsgs.APIConfirmInvokeQueryParam, IsBool: true, Example: "true"},
+ },
JSONInputSchema: func(ctx context.Context, schemaGen ffapi.SchemaGenerator) (*openapi3.SchemaRef, error) {
return contractRequestJSONSchema(ctx, &method.Params, hasLocation)
},
diff --git a/internal/apiserver/route_patch_update_identity.go b/internal/apiserver/route_patch_update_identity.go
index c70b877df4..46ec71e84e 100644
--- a/internal/apiserver/route_patch_update_identity.go
+++ b/internal/apiserver/route_patch_update_identity.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2022 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -33,7 +33,7 @@ var patchUpdateIdentity = &ffapi.Route{
{Name: "iid", Description: coremsgs.APIParamsIdentityID},
},
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true},
+ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true},
},
Description: coremsgs.APIEndpointsPatchUpdateIdentity,
JSONInputValue: func() interface{} { return &core.IdentityUpdateDTO{} },
diff --git a/internal/apiserver/route_post_contract_api_invoke.go b/internal/apiserver/route_post_contract_api_invoke.go
index 2b0bf89441..3befc3d513 100644
--- a/internal/apiserver/route_post_contract_api_invoke.go
+++ b/internal/apiserver/route_post_contract_api_invoke.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2022 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -35,7 +35,7 @@ var postContractAPIInvoke = &ffapi.Route{
{Name: "methodPath", Description: coremsgs.APIParamsMethodPath},
},
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true, Example: "true"},
+ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true, Example: "true"},
},
Description: coremsgs.APIEndpointsPostContractAPIInvoke,
JSONInputValue: func() interface{} { return &core.ContractCallRequest{} },
diff --git a/internal/apiserver/route_post_contract_api_publish.go b/internal/apiserver/route_post_contract_api_publish.go
index 068c707169..f9c4602a8b 100644
--- a/internal/apiserver/route_post_contract_api_publish.go
+++ b/internal/apiserver/route_post_contract_api_publish.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2023 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -34,7 +34,7 @@ var postContractAPIPublish = &ffapi.Route{
{Name: "apiName", Description: coremsgs.APIParamsContractAPIName},
},
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true},
+ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true},
},
Description: coremsgs.APIEndpointsPostContractAPIPublish,
JSONInputValue: func() interface{} { return &core.DefinitionPublish{} },
diff --git a/internal/apiserver/route_post_contract_deploy.go b/internal/apiserver/route_post_contract_deploy.go
index d2d1013455..c233754c5d 100644
--- a/internal/apiserver/route_post_contract_deploy.go
+++ b/internal/apiserver/route_post_contract_deploy.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2022 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -32,7 +32,7 @@ var postContractDeploy = &ffapi.Route{
Method: http.MethodPost,
PathParams: nil,
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true, Example: "true"},
+ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true, Example: "true"},
},
Description: coremsgs.APIEndpointsPostContractDeploy,
JSONInputValue: func() interface{} { return &core.ContractDeployRequest{} },
diff --git a/internal/apiserver/route_post_contract_interface_publish.go b/internal/apiserver/route_post_contract_interface_publish.go
index 1f161f8833..56790402a4 100644
--- a/internal/apiserver/route_post_contract_interface_publish.go
+++ b/internal/apiserver/route_post_contract_interface_publish.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2023 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -35,7 +35,7 @@ var postContractInterfacePublish = &ffapi.Route{
{Name: "version", Description: coremsgs.APIParamsContractInterfaceVersion},
},
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true},
+ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true},
},
Description: coremsgs.APIEndpointsPostContractInterfacePublish,
JSONInputValue: func() interface{} { return &core.DefinitionPublish{} },
diff --git a/internal/apiserver/route_post_contract_invoke.go b/internal/apiserver/route_post_contract_invoke.go
index 69bd84db93..afe695c4cb 100644
--- a/internal/apiserver/route_post_contract_invoke.go
+++ b/internal/apiserver/route_post_contract_invoke.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2022 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -32,7 +32,7 @@ var postContractInvoke = &ffapi.Route{
Method: http.MethodPost,
PathParams: nil,
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true, Example: "true"},
+ {Name: "confirm", Description: coremsgs.APIConfirmInvokeQueryParam, IsBool: true, Example: "true"},
},
Description: coremsgs.APIEndpointsPostContractInvoke,
JSONInputValue: func() interface{} { return &core.ContractCallRequest{} },
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 0000000000..fdc6b5fb3f
--- /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 0000000000..f95cff9610
--- /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/route_post_new_contract_api.go b/internal/apiserver/route_post_new_contract_api.go
index e3f076d133..e9bfe34879 100644
--- a/internal/apiserver/route_post_new_contract_api.go
+++ b/internal/apiserver/route_post_new_contract_api.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2023 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -32,7 +32,7 @@ var postNewContractAPI = &ffapi.Route{
Method: http.MethodPost,
PathParams: nil,
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true, Example: "true"},
+ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true, Example: "true"},
{Name: "publish", Description: coremsgs.APIPublishQueryParam, IsBool: true},
},
Description: coremsgs.APIEndpointsPostNewContractAPI,
diff --git a/internal/apiserver/route_post_new_contract_interface.go b/internal/apiserver/route_post_new_contract_interface.go
index 5a02ad8473..abfd08acea 100644
--- a/internal/apiserver/route_post_new_contract_interface.go
+++ b/internal/apiserver/route_post_new_contract_interface.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2023 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -32,7 +32,7 @@ var postNewContractInterface = &ffapi.Route{
Method: http.MethodPost,
PathParams: nil,
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true, Example: "true"},
+ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true, Example: "true"},
{Name: "publish", Description: coremsgs.APIPublishQueryParam, IsBool: true},
},
Description: coremsgs.APIEndpointsPostNewContractInterface,
diff --git a/internal/apiserver/route_post_new_datatype.go b/internal/apiserver/route_post_new_datatype.go
index 7f8e358d40..5cc7af89ff 100644
--- a/internal/apiserver/route_post_new_datatype.go
+++ b/internal/apiserver/route_post_new_datatype.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2022 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -31,7 +31,7 @@ var postNewDatatype = &ffapi.Route{
Method: http.MethodPost,
PathParams: nil,
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true, Example: "true"},
+ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true, Example: "true"},
},
Description: coremsgs.APIEndpointsPostNewDatatype,
JSONInputValue: func() interface{} { return &core.Datatype{} },
diff --git a/internal/apiserver/route_post_new_identity.go b/internal/apiserver/route_post_new_identity.go
index 2625057cae..0b9ab9967b 100644
--- a/internal/apiserver/route_post_new_identity.go
+++ b/internal/apiserver/route_post_new_identity.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2022 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -31,7 +31,7 @@ var postNewIdentity = &ffapi.Route{
Method: http.MethodPost,
PathParams: nil,
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true},
+ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true},
},
Description: coremsgs.APIEndpointsPostNewIdentity,
JSONInputValue: func() interface{} { return &core.IdentityCreateDTO{} },
diff --git a/internal/apiserver/route_post_new_message_broadcast.go b/internal/apiserver/route_post_new_message_broadcast.go
index a1513d0b43..fe0ce52593 100644
--- a/internal/apiserver/route_post_new_message_broadcast.go
+++ b/internal/apiserver/route_post_new_message_broadcast.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2022 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -32,7 +32,7 @@ var postNewMessageBroadcast = &ffapi.Route{
Method: http.MethodPost,
PathParams: nil,
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true},
+ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true},
},
Description: coremsgs.APIEndpointsPostNewMessageBroadcast,
JSONInputValue: func() interface{} { return &core.MessageInOut{} },
diff --git a/internal/apiserver/route_post_new_message_private.go b/internal/apiserver/route_post_new_message_private.go
index cd68fe6239..0411b0cda0 100644
--- a/internal/apiserver/route_post_new_message_private.go
+++ b/internal/apiserver/route_post_new_message_private.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2022 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -32,7 +32,7 @@ var postNewMessagePrivate = &ffapi.Route{
Method: http.MethodPost,
PathParams: nil,
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true},
+ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true},
},
Description: coremsgs.APIEndpointsPostNewMessagePrivate,
JSONInputValue: func() interface{} { return &core.MessageInOut{} },
diff --git a/internal/apiserver/route_post_new_node_self.go b/internal/apiserver/route_post_new_node_self.go
index ef25eb4991..c606157b86 100644
--- a/internal/apiserver/route_post_new_node_self.go
+++ b/internal/apiserver/route_post_new_node_self.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2022 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -32,7 +32,7 @@ var postNodesSelf = &ffapi.Route{
Method: http.MethodPost,
PathParams: nil,
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true, Example: "true"},
+ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true, Example: "true"},
},
Description: coremsgs.APIEndpointsPostNodesSelf,
JSONInputValue: func() interface{} { return &core.EmptyInput{} },
diff --git a/internal/apiserver/route_post_new_organization.go b/internal/apiserver/route_post_new_organization.go
index 487b656312..1c26d1673e 100644
--- a/internal/apiserver/route_post_new_organization.go
+++ b/internal/apiserver/route_post_new_organization.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2023 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -32,7 +32,7 @@ var postNewOrganization = &ffapi.Route{
Method: http.MethodPost,
PathParams: nil,
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true, Example: "true"},
+ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true, Example: "true"},
},
Description: coremsgs.APIEndpointsPostNewOrganization,
JSONInputValue: func() interface{} { return &core.IdentityCreateDTO{} },
diff --git a/internal/apiserver/route_post_new_organization_self.go b/internal/apiserver/route_post_new_organization_self.go
index 03ecd7e3b2..53651a3e0d 100644
--- a/internal/apiserver/route_post_new_organization_self.go
+++ b/internal/apiserver/route_post_new_organization_self.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2022 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -32,7 +32,7 @@ var postNewOrganizationSelf = &ffapi.Route{
Method: http.MethodPost,
PathParams: nil,
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true, Example: "true"},
+ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true, Example: "true"},
},
Description: coremsgs.APIEndpointsPostNewOrganizationSelf,
JSONInputValue: func() interface{} { return &core.EmptyInput{} },
diff --git a/internal/apiserver/route_post_token_approval.go b/internal/apiserver/route_post_token_approval.go
index ea25ad4dea..a7350195a6 100644
--- a/internal/apiserver/route_post_token_approval.go
+++ b/internal/apiserver/route_post_token_approval.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2022 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -31,7 +31,7 @@ var postTokenApproval = &ffapi.Route{
Method: http.MethodPost,
PathParams: nil,
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true},
+ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true},
},
Description: coremsgs.APIEndpointsPostTokenApproval,
JSONInputValue: func() interface{} {
diff --git a/internal/apiserver/route_post_token_burn.go b/internal/apiserver/route_post_token_burn.go
index 18edf618c9..6b2e19a3dc 100644
--- a/internal/apiserver/route_post_token_burn.go
+++ b/internal/apiserver/route_post_token_burn.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2022 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -31,7 +31,7 @@ var postTokenBurn = &ffapi.Route{
Method: http.MethodPost,
PathParams: nil,
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true},
+ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true},
},
Description: coremsgs.APIEndpointsPostTokenBurn,
JSONInputValue: func() interface{} { return &core.TokenTransferInput{} },
diff --git a/internal/apiserver/route_post_token_mint.go b/internal/apiserver/route_post_token_mint.go
index d8567c2724..7cc52894c5 100644
--- a/internal/apiserver/route_post_token_mint.go
+++ b/internal/apiserver/route_post_token_mint.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2022 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -31,7 +31,7 @@ var postTokenMint = &ffapi.Route{
Method: http.MethodPost,
PathParams: nil,
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true},
+ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true},
},
Description: coremsgs.APIEndpointsPostTokenMint,
JSONInputValue: func() interface{} { return &core.TokenTransferInput{} },
diff --git a/internal/apiserver/route_post_token_pool.go b/internal/apiserver/route_post_token_pool.go
index e5e20fa15b..84dc6b546a 100644
--- a/internal/apiserver/route_post_token_pool.go
+++ b/internal/apiserver/route_post_token_pool.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2023 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -31,7 +31,7 @@ var postTokenPool = &ffapi.Route{
Method: http.MethodPost,
PathParams: nil,
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true},
+ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true},
{Name: "publish", Description: coremsgs.APIPublishQueryParam, IsBool: true},
},
Description: coremsgs.APIEndpointsPostTokenPool,
diff --git a/internal/apiserver/route_post_token_pool_publish.go b/internal/apiserver/route_post_token_pool_publish.go
index 5abd25927e..6a953ee264 100644
--- a/internal/apiserver/route_post_token_pool_publish.go
+++ b/internal/apiserver/route_post_token_pool_publish.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2023 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -33,7 +33,7 @@ var postTokenPoolPublish = &ffapi.Route{
{Name: "nameOrId", Description: coremsgs.APIParamsTokenPoolNameOrID},
},
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true},
+ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true},
},
Description: coremsgs.APIEndpointsPostTokenPoolPublish,
JSONInputValue: func() interface{} { return &core.DefinitionPublish{} },
diff --git a/internal/apiserver/route_post_token_transfer.go b/internal/apiserver/route_post_token_transfer.go
index d4899ffafd..3dd2cb1bc3 100644
--- a/internal/apiserver/route_post_token_transfer.go
+++ b/internal/apiserver/route_post_token_transfer.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2022 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -31,7 +31,7 @@ var postTokenTransfer = &ffapi.Route{
Method: http.MethodPost,
PathParams: nil,
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true},
+ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true},
},
Description: coremsgs.APIEndpointsPostTokenTransfer,
JSONInputValue: func() interface{} { return &core.TokenTransferInput{} },
diff --git a/internal/apiserver/route_put_contract_api.go b/internal/apiserver/route_put_contract_api.go
index 28521ea315..818c5e0fe3 100644
--- a/internal/apiserver/route_put_contract_api.go
+++ b/internal/apiserver/route_put_contract_api.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2022 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -35,7 +35,7 @@ var putContractAPI = &ffapi.Route{
{Name: "id", Example: "id", Description: coremsgs.APIParamsContractAPIName},
},
QueryParams: []*ffapi.QueryParam{
- {Name: "confirm", Description: coremsgs.APIConfirmQueryParam, IsBool: true, Example: "true"},
+ {Name: "confirm", Description: coremsgs.APIConfirmMsgQueryParam, IsBool: true, Example: "true"},
},
Description: coremsgs.APIParamsContractAPIID,
JSONInputValue: func() interface{} { return &core.ContractAPI{} },
diff --git a/internal/apiserver/routes.go b/internal/apiserver/routes.go
index 16b5009ed9..3f146cca50 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/apiserver/server.go b/internal/apiserver/server.go
index fb61b64b81..ca1671a051 100644
--- a/internal/apiserver/server.go
+++ b/internal/apiserver/server.go
@@ -61,6 +61,7 @@ type apiServer struct {
ffiSwaggerGen FFISwaggerGen
apiPublicURL string
dynamicPublicURLHeader string
+ defaultNamespace string
}
func InitConfig() {
@@ -76,6 +77,7 @@ func NewAPIServer() Server {
apiTimeout: config.GetDuration(coreconfig.APIRequestTimeout),
apiMaxTimeout: config.GetDuration(coreconfig.APIRequestMaxTimeout),
dynamicPublicURLHeader: config.GetString(coreconfig.APIDynamicPublicURLHeader),
+ defaultNamespace: config.GetString(coreconfig.NamespacesDefault),
metricsEnabled: config.GetBool(coreconfig.MetricsEnabled),
ffiSwaggerGen: &ffiSwaggerGen{},
}
@@ -188,7 +190,11 @@ func (as *apiServer) getBaseURL(req *http.Request) string {
vars := mux.Vars(req)
if ns, ok := vars["ns"]; ok && ns != "" {
baseURL += `/namespaces/` + ns
+ } else {
+ // Use the default namespace
+ baseURL += `/namespaces/` + as.defaultNamespace
}
+
return baseURL
}
diff --git a/internal/apiserver/server_test.go b/internal/apiserver/server_test.go
index 8d61e03c07..7855a033ef 100644
--- a/internal/apiserver/server_test.go
+++ b/internal/apiserver/server_test.go
@@ -549,3 +549,29 @@ func TestGetNamespacedWebSocketHandlerUnknownNamespace(t *testing.T) {
assert.Error(t, err)
assert.Equal(t, 404, status)
}
+
+func TestContractAPIDefaultNS(t *testing.T) {
+ mgr, o, as := newTestServer()
+ r := as.createMuxRouter(context.Background(), mgr)
+ mcm := &contractmocks.Manager{}
+ o.On("Contracts").Return(mcm)
+ mffi := apiservermocks.NewFFISwaggerGen(t)
+ as.ffiSwaggerGen = mffi
+ s := httptest.NewServer(r)
+ defer s.Close()
+
+ o.On("Authorize", mock.Anything, mock.Anything).Return(nil)
+
+ api := &core.ContractAPI{
+ Interface: &fftypes.FFIReference{
+ ID: fftypes.NewUUID(),
+ },
+ }
+
+ mcm.On("GetContractAPIs", mock.Anything, "http://127.0.0.1:5000/api/v1/namespaces/default", mock.Anything).Return([]*core.ContractAPI{api}, nil, nil)
+
+ res, err := resty.New().R().
+ Get(fmt.Sprintf("http://%s/api/v1/apis", s.Listener.Addr()))
+ assert.NoError(t, err)
+ assert.Equal(t, 200, res.StatusCode())
+}
diff --git a/internal/batch/batch_processor.go b/internal/batch/batch_processor.go
index c4496f0ac7..a708c6d559 100644
--- a/internal/batch/batch_processor.go
+++ b/internal/batch/batch_processor.go
@@ -633,6 +633,12 @@ func (bp *batchProcessor) dispatchBatch(payload *DispatchPayload) error {
}
}
}
+ conflictErr, conflictTestOk := err.(operations.ConflictError)
+ if conflictTestOk && conflictErr.IsConflictError() {
+ // We know that the connector has received our batch, so we shouldn't need to retry
+ payload.addMessageUpdate(payload.Messages, core.MessageStateReady, core.MessageStateSent)
+ return true, nil
+ }
} else {
if core.IsPinned(payload.Batch.TX.Type) {
payload.addMessageUpdate(payload.Messages, core.MessageStateReady, core.MessageStateSent)
diff --git a/internal/batch/batch_processor_test.go b/internal/batch/batch_processor_test.go
index fde0426605..f5704b60e0 100644
--- a/internal/batch/batch_processor_test.go
+++ b/internal/batch/batch_processor_test.go
@@ -75,6 +75,18 @@ func mockRunAsGroupPassthrough(mdi *databasemocks.Plugin) {
}
}
+type testConflictError struct {
+ err error
+}
+
+func (tce *testConflictError) Error() string {
+ return tce.err.Error()
+}
+
+func (tce *testConflictError) IsConflictError() bool {
+ return true
+}
+
func TestUnfilledBatch(t *testing.T) {
log.SetLevel("debug")
coreconfig.Reset()
@@ -129,6 +141,17 @@ func TestUnfilledBatch(t *testing.T) {
mim.AssertExpectations(t)
}
+func TestHandleDispatchConflictError(t *testing.T) {
+ cancel, _, bp := newTestBatchProcessor(t, func(c context.Context, state *DispatchPayload) error {
+ conflictErr := testConflictError{err: fmt.Errorf("pop")}
+ return &conflictErr
+ })
+ defer cancel()
+ bp.dispatchBatch(&DispatchPayload{})
+ bp.cancelCtx()
+ <-bp.done
+}
+
func TestBatchSizeOverflow(t *testing.T) {
log.SetLevel("debug")
coreconfig.Reset()
diff --git a/internal/blockchain/bifactory/factory_test.go b/internal/blockchain/bifactory/factory_test.go
new file mode 100644
index 0000000000..a2b493c796
--- /dev/null
+++ b/internal/blockchain/bifactory/factory_test.go
@@ -0,0 +1,60 @@
+// Copyright ยฉ 2023 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 bifactory
+
+import (
+ "context"
+ "testing"
+
+ "github.com/hyperledger/firefly-common/pkg/config"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetPluginUnknown(t *testing.T) {
+ ctx := context.Background()
+ _, err := GetPlugin(ctx, "foo")
+ assert.Error(t, err)
+ assert.Regexp(t, "FF10110", err)
+}
+
+func TestGetPluginEthereum(t *testing.T) {
+ ctx := context.Background()
+ plugin, err := GetPlugin(ctx, "ethereum")
+ assert.NoError(t, err)
+ assert.NotNil(t, plugin)
+}
+
+func TestGetPluginFabric(t *testing.T) {
+ ctx := context.Background()
+ plugin, err := GetPlugin(ctx, "fabric")
+ assert.NoError(t, err)
+ assert.NotNil(t, plugin)
+}
+
+func TestGetPluginTezos(t *testing.T) {
+ ctx := context.Background()
+ plugin, err := GetPlugin(ctx, "tezos")
+ assert.NoError(t, err)
+ assert.NotNil(t, plugin)
+}
+
+var root = config.RootSection("di")
+
+func TestInitConfig(t *testing.T) {
+ conf := root.SubArray("plugins")
+ InitConfig(conf)
+}
diff --git a/internal/blockchain/common/common.go b/internal/blockchain/common/common.go
index d15e0b8c4a..564b9da0f8 100644
--- a/internal/blockchain/common/common.go
+++ b/internal/blockchain/common/common.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2023 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -392,9 +392,17 @@ func (s *subscriptions) GetSubscription(subID string) *SubscriptionInfo {
}
// Common function for handling receipts from blockchain connectors.
-func HandleReceipt(ctx context.Context, plugin core.Named, reply *BlockchainReceiptNotification, callbacks BlockchainCallbacks) error {
+func HandleReceipt(ctx context.Context, namespace string, plugin core.Named, reply *BlockchainReceiptNotification, callbacks BlockchainCallbacks) error {
l := log.L(ctx)
+ if namespace != "" {
+ opNamespace, _, _ := core.ParseNamespacedOpID(ctx, reply.Headers.ReceiptID)
+ if opNamespace != namespace {
+ l.Debugf("Ignoring operation update from other namespace: request=%s tx=%s message=%s", reply.Headers.ReceiptID, reply.TxHash, reply.Message)
+ return nil
+ }
+ }
+
if reply.Headers.ReceiptID == "" || reply.Headers.ReplyType == "" {
return fmt.Errorf("reply cannot be processed - missing fields: %+v", reply)
}
@@ -409,7 +417,7 @@ func HandleReceipt(ctx context.Context, plugin core.Named, reply *BlockchainRece
updateType = core.OpStatusFailed
}
- // Slightly upgly conversion from ReceiptFromBlockchain -> JSONObject which the generic OperationUpdate() function requires
+ // Slightly ugly conversion from ReceiptFromBlockchain -> JSONObject which the generic OperationUpdate() function requires
var output fftypes.JSONObject
obj, err := json.Marshal(reply)
if err != nil {
diff --git a/internal/blockchain/common/common_test.go b/internal/blockchain/common/common_test.go
index 402a40339d..f20c5f1a4c 100644
--- a/internal/blockchain/common/common_test.go
+++ b/internal/blockchain/common/common_test.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2022 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -342,15 +342,15 @@ func TestGoodSuccessReceipt(t *testing.T) {
cb.SetHandler("ns1", mcb)
mcb.On("OperationUpdate", "ns1", mock.Anything).Return()
- err := HandleReceipt(context.Background(), nil, &reply, cb)
+ err := HandleReceipt(context.Background(), "", nil, &reply, cb)
assert.NoError(t, err)
reply.Headers.ReplyType = "TransactionUpdate"
- err = HandleReceipt(context.Background(), nil, &reply, cb)
+ err = HandleReceipt(context.Background(), "", nil, &reply, cb)
assert.NoError(t, err)
reply.Headers.ReplyType = "TransactionFailed"
- err = HandleReceipt(context.Background(), nil, &reply, cb)
+ err = HandleReceipt(context.Background(), "", nil, &reply, cb)
assert.NoError(t, err)
}
@@ -365,7 +365,7 @@ func TestReceiptMarshallingError(t *testing.T) {
cb.SetHandler("ns1", mcb)
mcb.On("OperationUpdate", "ns1", mock.Anything).Return()
- err := HandleReceipt(context.Background(), nil, &reply, cb)
+ err := HandleReceipt(context.Background(), "", nil, &reply, cb)
assert.Error(t, err)
assert.Regexp(t, ".*[^n]marshalling error.*", err)
}
@@ -384,10 +384,19 @@ func TestBadReceipt(t *testing.T) {
data := fftypes.JSONAnyPtr(`{}`)
err := json.Unmarshal(data.Bytes(), &reply)
assert.NoError(t, err)
- err = HandleReceipt(context.Background(), nil, &reply, nil)
+ err = HandleReceipt(context.Background(), "", nil, &reply, nil)
assert.Error(t, err)
}
+func TestWrongNamespaceReceipt(t *testing.T) {
+ var reply BlockchainReceiptNotification
+ data := fftypes.JSONAnyPtr(`{}`)
+ err := json.Unmarshal(data.Bytes(), &reply)
+ assert.NoError(t, err)
+ err = HandleReceipt(context.Background(), "wrong", nil, &reply, nil)
+ assert.NoError(t, err)
+}
+
func TestErrorWrappingConflict(t *testing.T) {
ctx := context.Background()
res := &resty.Response{
diff --git a/internal/blockchain/ethereum/ethereum.go b/internal/blockchain/ethereum/ethereum.go
index 7e9dd73d26..6ecfa283c8 100644
--- a/internal/blockchain/ethereum/ethereum.go
+++ b/internal/blockchain/ethereum/ethereum.go
@@ -271,7 +271,7 @@ func (e *Ethereum) Capabilities() *blockchain.Capabilities {
return e.capabilities
}
-func (e *Ethereum) AddFireflySubscription(ctx context.Context, namespace *core.Namespace, contract *blockchain.MultipartyContract) (string, error) {
+func (e *Ethereum) AddFireflySubscription(ctx context.Context, namespace *core.Namespace, contract *blockchain.MultipartyContract, lastProtocolID string) (string, error) {
ethLocation, err := e.parseContractLocation(ctx, contract.Location)
if err != nil {
return "", err
@@ -286,7 +286,7 @@ func (e *Ethereum) AddFireflySubscription(ctx context.Context, namespace *core.N
if !ok {
return "", i18n.NewError(ctx, coremsgs.MsgInternalServerError, "eventstream ID not found")
}
- sub, err := e.streams.ensureFireFlySubscription(ctx, namespace.Name, version, ethLocation.Address, contract.FirstEvent, streamID, batchPinEventABI)
+ sub, err := e.streams.ensureFireFlySubscription(ctx, namespace.Name, version, ethLocation.Address, contract.FirstEvent, streamID, batchPinEventABI, lastProtocolID)
if err != nil {
return "", err
@@ -513,7 +513,7 @@ func (e *Ethereum) eventLoop(namespace string, wsconn wsclient.WSClient, closed
if !isBatch {
var receipt common.BlockchainReceiptNotification
_ = json.Unmarshal(msgBytes, &receipt)
- err := common.HandleReceipt(ctx, e, &receipt, e.callbacks)
+ err := common.HandleReceipt(ctx, namespace, e, &receipt, e.callbacks)
if err != nil {
l.Errorf("Failed to process receipt: %+v", msgTyped)
}
@@ -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 {
@@ -874,18 +894,49 @@ func (e *Ethereum) encodeContractLocation(ctx context.Context, location *Locatio
return result, err
}
-func (e *Ethereum) AddContractListener(ctx context.Context, listener *core.ContractListener) (err error) {
- var location *Location
+func (e *Ethereum) AddContractListener(ctx context.Context, listener *core.ContractListener, lastProtocolID string) (err error) {
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)
+ 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 {
@@ -1128,7 +1223,7 @@ func (e *Ethereum) GetTransactionStatus(ctx context.Context, operation *core.Ope
TxHash: statusResponse.GetString("transactionHash"),
Message: statusResponse.GetString("errorMessage"),
ProtocolID: receiptInfo.GetString("protocolId")}
- err := common.HandleReceipt(ctx, e, receipt, e.callbacks)
+ err := common.HandleReceipt(ctx, operation.Namespace, e, receipt, e.callbacks)
if err != nil {
log.L(ctx).Warnf("Failed to handle receipt")
}
diff --git a/internal/blockchain/ethereum/ethereum_test.go b/internal/blockchain/ethereum/ethereum_test.go
index 83a624056c..739a3e2989 100644
--- a/internal/blockchain/ethereum/ethereum_test.go
+++ b/internal/blockchain/ethereum/ethereum_test.go
@@ -906,7 +906,7 @@ func TestInitAllExistingStreams(t *testing.T) {
<-toServer
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
- _, err = e.AddFireflySubscription(e.ctx, ns, contract)
+ _, err = e.AddFireflySubscription(e.ctx, ns, contract, "")
assert.NoError(t, err)
assert.Equal(t, 4, httpmock.GetTotalCallCount())
@@ -964,7 +964,7 @@ func TestInitAllExistingStreamsV1(t *testing.T) {
<-toServer
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
- _, err = e.AddFireflySubscription(e.ctx, ns, contract)
+ _, err = e.AddFireflySubscription(e.ctx, ns, contract, "")
assert.NoError(t, err)
assert.Equal(t, 4, httpmock.GetTotalCallCount())
@@ -1022,7 +1022,7 @@ func TestInitAllExistingStreamsOld(t *testing.T) {
<-toServer
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
- _, err = e.AddFireflySubscription(e.ctx, ns, contract)
+ _, err = e.AddFireflySubscription(e.ctx, ns, contract, "")
assert.NoError(t, err)
assert.Equal(t, 4, httpmock.GetTotalCallCount())
@@ -1080,7 +1080,7 @@ func TestInitAllExistingStreamsInvalidName(t *testing.T) {
<-toServer
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
- _, err = e.AddFireflySubscription(e.ctx, ns, contract)
+ _, err = e.AddFireflySubscription(e.ctx, ns, contract, "")
assert.Regexp(t, "FF10416", err)
}
@@ -1835,7 +1835,7 @@ func TestHandleReceiptTXSuccess(t *testing.T) {
err := json.Unmarshal(data.Bytes(), &reply)
assert.NoError(t, err)
- common.HandleReceipt(context.Background(), e, &reply, e.callbacks)
+ common.HandleReceipt(context.Background(), "ns1", e, &reply, e.callbacks)
em.AssertExpectations(t)
}
@@ -1922,7 +1922,7 @@ func TestHandleReceiptTXUpdateEVMConnect(t *testing.T) {
assert.NoError(t, err)
expectedReceiptId := "ns1:" + operationID.String()
assert.Equal(t, reply.Headers.ReceiptID, expectedReceiptId)
- common.HandleReceipt(context.Background(), e, &reply, e.callbacks)
+ common.HandleReceipt(context.Background(), "", e, &reply, e.callbacks)
em.AssertExpectations(t)
}
@@ -1987,7 +1987,7 @@ func TestHandleMsgBatchBadData(t *testing.T) {
data := fftypes.JSONAnyPtr(`{}`)
err := json.Unmarshal(data.Bytes(), &reply)
assert.NoError(t, err)
- common.HandleReceipt(context.Background(), e, &reply, e.callbacks)
+ common.HandleReceipt(context.Background(), "", e, &reply, e.callbacks)
}
func TestFormatNil(t *testing.T) {
@@ -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{
@@ -2027,7 +2031,7 @@ func TestAddSubscription(t *testing.T) {
httpmock.RegisterResponder("POST", `http://localhost:12345/subscriptions`,
httpmock.NewJsonResponderOrPanic(200, &subscription{}))
- err := e.AddContractListener(context.Background(), sub)
+ err := e.AddContractListener(context.Background(), sub, "")
assert.NoError(t, err)
}
@@ -2043,16 +2047,73 @@ 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"}}`),
+ },
+ },
+ },
+ },
+ },
+ },
+ 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 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),
@@ -2062,10 +2123,154 @@ func TestAddSubscriptionWithoutLocation(t *testing.T) {
httpmock.RegisterResponder("POST", `http://localhost:12345/subscriptions`,
httpmock.NewJsonResponderOrPanic(200, &subscription{}))
- err := e.AddContractListener(context.Background(), sub)
+ 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()
@@ -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()),
},
},
}
@@ -2097,7 +2306,7 @@ func TestAddSubscriptionBadParamDetails(t *testing.T) {
httpmock.RegisterResponder("POST", `http://localhost:12345/subscriptions`,
httpmock.NewJsonResponderOrPanic(200, &subscription{}))
- err := e.AddContractListener(context.Background(), sub)
+ err := e.AddContractListener(context.Background(), sub, "")
assert.Regexp(t, "FF10311", err)
}
@@ -2114,15 +2323,33 @@ 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)
+ err := e.AddContractListener(context.Background(), sub, "")
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),
},
@@ -2147,7 +2378,7 @@ func TestAddSubscriptionFail(t *testing.T) {
httpmock.RegisterResponder("POST", `http://localhost:12345/subscriptions`,
httpmock.NewStringResponder(500, "pop"))
- err := e.AddContractListener(context.Background(), sub)
+ err := e.AddContractListener(context.Background(), sub, "")
assert.Regexp(t, "FF10111", err)
assert.Regexp(t, "pop", err)
@@ -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)
}
@@ -4005,7 +4299,7 @@ func TestAddSubBadLocation(t *testing.T) {
}
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
- _, err := e.AddFireflySubscription(e.ctx, ns, contract)
+ _, err := e.AddFireflySubscription(e.ctx, ns, contract, "")
assert.Regexp(t, "FF10310", err)
}
@@ -4025,9 +4319,15 @@ func TestAddAndRemoveFireflySubscription(t *testing.T) {
httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions",
httpmock.NewJsonResponderOrPanic(200, []subscription{}))
httpmock.RegisterResponder("POST", "http://localhost:12345/subscriptions",
- httpmock.NewJsonResponderOrPanic(200, subscription{
- ID: "sub1",
- }))
+ func(r *http.Request) (*http.Response, error) {
+ var s subscription
+ err := json.NewDecoder(r.Body).Decode(&s)
+ assert.NoError(t, err)
+ assert.Equal(t, "19", s.FromBlock)
+ return httpmock.NewJsonResponderOrPanic(200, subscription{
+ ID: "sub1",
+ })(r)
+ })
httpmock.RegisterResponder("POST", "http://localhost:12345/", mockNetworkVersion(2))
utEthconnectConf.Set(ffresty.HTTPConfigURL, "http://localhost:12345")
@@ -4055,7 +4355,7 @@ func TestAddAndRemoveFireflySubscription(t *testing.T) {
e.streamID["ns1"] = "es12345"
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
- subID, err := e.AddFireflySubscription(e.ctx, ns, contract)
+ subID, err := e.AddFireflySubscription(e.ctx, ns, contract, "000000000020/000000/000000")
assert.NoError(t, err)
assert.NotNil(t, e.subs.GetSubscription("sub1"))
@@ -4103,7 +4403,7 @@ func TestAddFireflySubscriptionV1(t *testing.T) {
e.streamID["ns1"] = "es12345"
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
- _, err = e.AddFireflySubscription(e.ctx, ns, contract)
+ _, err = e.AddFireflySubscription(e.ctx, ns, contract, "")
assert.NoError(t, err)
assert.NotNil(t, e.subs.GetSubscription("sub1"))
}
@@ -4147,7 +4447,7 @@ func TestAddFireflySubscriptionEventstreamFail(t *testing.T) {
}
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
- _, err = e.AddFireflySubscription(e.ctx, ns, contract)
+ _, err = e.AddFireflySubscription(e.ctx, ns, contract, "")
assert.Regexp(t, "FF10465", err)
}
@@ -4189,7 +4489,7 @@ func TestAddFireflySubscriptionQuerySubsFail(t *testing.T) {
e.streamID["ns1"] = "es12345"
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
- _, err = e.AddFireflySubscription(e.ctx, ns, contract)
+ _, err = e.AddFireflySubscription(e.ctx, ns, contract, "")
assert.Regexp(t, "FF10111", err)
}
@@ -4231,7 +4531,7 @@ func TestAddFireflySubscriptionCreateError(t *testing.T) {
}
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
- _, err = e.AddFireflySubscription(e.ctx, ns, contract)
+ _, err = e.AddFireflySubscription(e.ctx, ns, contract, "")
assert.Regexp(t, "FF10111", err)
}
@@ -4273,7 +4573,7 @@ func TestAddFireflySubscriptionGetVersionError(t *testing.T) {
e.streamID["ns1"] = "es12345"
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
- _, err = e.AddFireflySubscription(e.ctx, ns, contract)
+ _, err = e.AddFireflySubscription(e.ctx, ns, contract, "")
assert.Regexp(t, "FF10111", err)
}
@@ -4564,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 498f3117d0..078ad95107 100644
--- a/internal/blockchain/ethereum/eventstream.go
+++ b/internal/blockchain/ethereum/eventstream.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2023 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -21,10 +21,11 @@ import (
"crypto/sha256"
"encoding/hex"
"fmt"
+ "strconv"
+ "strings"
"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"
@@ -52,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"`
@@ -201,19 +207,59 @@ func (s *streamManager) getSubscriptionName(ctx context.Context, subID string) (
return sub.Name, nil
}
-func (s *streamManager) createSubscription(ctx context.Context, location *Location, stream, subName, firstEvent string, abi *abi.Entry) (*subscription, error) {
- // Map FireFly "firstEvent" values to Ethereum "fromBlock" values
- switch firstEvent {
- case string(core.SubOptsFirstEventOldest):
+func resolveFromBlock(ctx context.Context, firstEvent, lastProtocolID string) (string, error) {
+ // Parse the lastProtocolID if supplied
+ var blockBeforeNewestEvent *uint64
+ if len(lastProtocolID) > 0 {
+ blockStr := strings.Split(lastProtocolID, "/")[0]
+ parsedUint, err := strconv.ParseUint(blockStr, 10, 64)
+ if err != nil {
+ return "", i18n.NewError(ctx, coremsgs.MsgInvalidLastEventProtocolID, lastProtocolID)
+ }
+ if parsedUint > 0 {
+ // We jump back on block from the last event, to minimize re-delivery while ensuring
+ // we get all events since the last delivered (including subsequent events in the same block)
+ parsedUint--
+ blockBeforeNewestEvent = &parsedUint
+ }
+ }
+
+ // If the user requested newest, then we use the last block number if we have one,
+ // or we pass the request for newest down to the connector
+ if firstEvent == "" || firstEvent == string(core.SubOptsFirstEventNewest) || firstEvent == "latest" {
+ if blockBeforeNewestEvent != nil {
+ return strconv.FormatUint(*blockBeforeNewestEvent, 10), nil
+ }
+ return "latest", nil
+ }
+
+ // Otherwise we expect to be able to parse the block, with "oldest" being the same as "0"
+ if firstEvent == string(core.SubOptsFirstEventOldest) {
firstEvent = "0"
- case string(core.SubOptsFirstEventNewest):
- firstEvent = "latest"
}
+ blockNumber, err := strconv.ParseUint(firstEvent, 10, 64)
+ if err != nil {
+ return "", i18n.NewError(ctx, coremsgs.MsgInvalidFromBlockNumber, firstEvent)
+ }
+ // If the last event is already dispatched after this block, recreate the listener from that block
+ if blockBeforeNewestEvent != nil && *blockBeforeNewestEvent > blockNumber {
+ blockNumber = *blockBeforeNewestEvent
+ }
+ return strconv.FormatUint(blockNumber, 10), nil
+}
+
+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
+ }
+
sub := subscription{
Name: subName,
Stream: stream,
- FromBlock: firstEvent,
- EthCompatEvent: abi,
+ FromBlock: fromBlock,
+ EthCompatEvent: abi, // only used for ethconnect
+ Filters: filters,
}
if location != nil {
@@ -244,7 +290,7 @@ func (s *streamManager) deleteSubscription(ctx context.Context, subID string, ok
return nil
}
-func (s *streamManager) ensureFireFlySubscription(ctx context.Context, namespace string, version int, instancePath, firstEvent, stream string, abi *abi.Entry) (sub *subscription, err error) {
+func (s *streamManager) ensureFireFlySubscription(ctx context.Context, namespace string, version int, instancePath, firstEvent, stream string, abi *abi.Entry, lastProtocolID string) (sub *subscription, err error) {
// Include a hash of the instance path in the subscription, so if we ever point at a different
// contract configuration, we re-subscribe from block 0.
// We don't need full strength hashing, so just use the first 16 chars for readability.
@@ -286,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); 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
new file mode 100644
index 0000000000..03dbc69ed2
--- /dev/null
+++ b/internal/blockchain/ethereum/eventstream_test.go
@@ -0,0 +1,69 @@
+// 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 ethereum
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCreateSubscriptionBadBlock(t *testing.T) {
+ e, cancel := newTestEthereum()
+ defer cancel()
+
+ _, err := e.streams.createSubscription(context.Background(), "", "", "wrongness", nil, nil, []*filter{}, "")
+ assert.Regexp(t, "FF10473", err)
+}
+
+func TestResolveFromBlockCombinations(t *testing.T) {
+
+ ctx := context.Background()
+
+ fromBlock, err := resolveFromBlock(ctx, "", "")
+ assert.Equal(t, "latest", fromBlock)
+ assert.NoError(t, err)
+
+ fromBlock, err = resolveFromBlock(ctx, "latest", "")
+ assert.Equal(t, "latest", fromBlock)
+ assert.NoError(t, err)
+
+ fromBlock, err = resolveFromBlock(ctx, "newest", "")
+ assert.Equal(t, "latest", fromBlock)
+ assert.NoError(t, err)
+
+ fromBlock, err = resolveFromBlock(ctx, "0", "")
+ assert.Equal(t, "0", fromBlock)
+ assert.NoError(t, err)
+
+ fromBlock, err = resolveFromBlock(ctx, "0", "000000000010/000000/000050")
+ assert.Equal(t, "9", fromBlock)
+ assert.NoError(t, err)
+
+ fromBlock, err = resolveFromBlock(ctx, "20", "000000000010/000000/000050")
+ assert.Equal(t, "20", fromBlock)
+ assert.NoError(t, err)
+
+ fromBlock, err = resolveFromBlock(ctx, "", "000000000010/000000/000050")
+ assert.Equal(t, "9", fromBlock)
+ assert.NoError(t, err)
+
+ _, err = resolveFromBlock(ctx, "", "wrong")
+ assert.Regexp(t, "FF10472", err)
+
+}
diff --git a/internal/blockchain/fabric/eventstream.go b/internal/blockchain/fabric/eventstream.go
index 49c2c2ec5c..48d1b72712 100644
--- a/internal/blockchain/fabric/eventstream.go
+++ b/internal/blockchain/fabric/eventstream.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2023 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -19,6 +19,8 @@ package fabric
import (
"context"
"fmt"
+ "strconv"
+ "strings"
"github.com/go-resty/resty/v2"
"github.com/hyperledger/firefly-common/pkg/ffresty"
@@ -154,22 +156,25 @@ func (s *streamManager) getSubscriptions(ctx context.Context) (subs []*subscript
return subs, nil
}
-func (s *streamManager) getSubscription(ctx context.Context, subID string) (sub *subscription, err error) {
+func (s *streamManager) getSubscription(ctx context.Context, subID string, okNotFound bool) (sub *subscription, err error) {
res, err := s.client.R().
SetContext(ctx).
SetResult(&sub).
Get(fmt.Sprintf("/subscriptions/%s", subID))
if err != nil || !res.IsSuccess() {
+ if okNotFound && res.StatusCode() == 404 {
+ return nil, nil
+ }
return nil, ffresty.WrapRestErr(ctx, res, err, coremsgs.MsgFabconnectRESTErr)
}
return sub, nil
}
-func (s *streamManager) getSubscriptionName(ctx context.Context, subID string) (string, error) {
+func (s *streamManager) getSubscriptionName(ctx context.Context, subID string, okNotFound bool) (string, error) {
if cachedValue := s.cache.GetString("sub:" + subID); cachedValue != "" {
return cachedValue, nil
}
- sub, err := s.getSubscription(ctx, subID)
+ sub, err := s.getSubscription(ctx, subID, okNotFound)
if err != nil {
return "", err
}
@@ -177,11 +182,54 @@ func (s *streamManager) getSubscriptionName(ctx context.Context, subID string) (
return sub.Name, nil
}
-func (s *streamManager) createSubscription(ctx context.Context, location *Location, stream, name, event, firstEvent string) (*subscription, error) {
- // Map FireFly "firstEvent" values to Fabric "fromBlock" values
+func resolveFromBlock(ctx context.Context, firstEvent, lastProtocolID string) (string, error) {
+ // Parse the lastProtocolID if supplied
+ var blockBeforeNewestEvent *uint64
+ if len(lastProtocolID) > 0 {
+ blockStr := strings.Split(lastProtocolID, "/")[0]
+ parsedUint, err := strconv.ParseUint(blockStr, 10, 64)
+ if err != nil {
+ return "", i18n.NewError(ctx, coremsgs.MsgInvalidLastEventProtocolID, lastProtocolID)
+ }
+ if parsedUint > 0 {
+ // We jump back on block from the last event, to minimize re-delivery while ensuring
+ // we get all events since the last delivered (including subsequent events in the same block)
+ parsedUint--
+ blockBeforeNewestEvent = &parsedUint
+ }
+ }
+
+ // If the user requested newest, then we use the last block number if we have one,
+ // or we pass the request for newest down to the connector
+ if firstEvent == "" || firstEvent == string(core.SubOptsFirstEventNewest) || firstEvent == "latest" {
+ if blockBeforeNewestEvent != nil {
+ return strconv.FormatUint(*blockBeforeNewestEvent, 10), nil
+ }
+ return "newest", nil
+ }
+
+ // Otherwise we expect to be able to parse the block, with "oldest" being the same as "0"
if firstEvent == string(core.SubOptsFirstEventOldest) {
firstEvent = "0"
}
+ blockNumber, err := strconv.ParseUint(firstEvent, 10, 64)
+ if err != nil {
+ return "", i18n.NewError(ctx, coremsgs.MsgInvalidFromBlockNumber, firstEvent)
+ }
+ // If the last event is already dispatched after this block, recreate the listener from that block
+ if blockBeforeNewestEvent != nil && *blockBeforeNewestEvent > blockNumber {
+ blockNumber = *blockBeforeNewestEvent
+ }
+ return strconv.FormatUint(blockNumber, 10), nil
+}
+
+func (s *streamManager) createSubscription(ctx context.Context, location *Location, stream, name, event, firstEvent, lastProtocolID string) (*subscription, error) {
+
+ fromBlock, err := resolveFromBlock(ctx, firstEvent, lastProtocolID)
+ if err != nil {
+ return nil, err
+ }
+
sub := subscription{
Name: name,
Channel: location.Channel,
@@ -190,7 +238,7 @@ func (s *streamManager) createSubscription(ctx context.Context, location *Locati
Filter: eventFilter{
EventFilter: event,
},
- FromBlock: firstEvent,
+ FromBlock: fromBlock,
}
if location.Chaincode != "" {
@@ -221,7 +269,7 @@ func (s *streamManager) deleteSubscription(ctx context.Context, subID string, ok
return nil
}
-func (s *streamManager) ensureFireFlySubscription(ctx context.Context, namespace string, version int, location *Location, firstEvent, stream, event string) (sub *subscription, err error) {
+func (s *streamManager) ensureFireFlySubscription(ctx context.Context, namespace string, version int, location *Location, firstEvent, stream, event, lastProtocolID string) (sub *subscription, err error) {
existingSubs, err := s.getSubscriptions(ctx)
if err != nil {
return nil, err
@@ -250,7 +298,7 @@ func (s *streamManager) ensureFireFlySubscription(ctx context.Context, namespace
if version == 1 {
name = v1Name
}
- if sub, err = s.createSubscription(ctx, location, stream, name, event, firstEvent); err != nil {
+ if sub, err = s.createSubscription(ctx, location, stream, name, event, firstEvent, lastProtocolID); err != nil {
return nil, err
}
log.L(ctx).Infof("%s subscription: %s", event, sub.ID)
diff --git a/internal/blockchain/fabric/eventstream_test.go b/internal/blockchain/fabric/eventstream_test.go
new file mode 100644
index 0000000000..b5a15eda42
--- /dev/null
+++ b/internal/blockchain/fabric/eventstream_test.go
@@ -0,0 +1,69 @@
+// 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 fabric
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCreateSubscriptionBadBlock(t *testing.T) {
+ e, cancel := newTestFabric()
+ defer cancel()
+
+ _, err := e.streams.createSubscription(context.Background(), nil, "", "", "", "wrongness", "")
+ assert.Regexp(t, "FF10473", err)
+}
+
+func TestResolveFromBlockCombinations(t *testing.T) {
+
+ ctx := context.Background()
+
+ fromBlock, err := resolveFromBlock(ctx, "", "")
+ assert.Equal(t, "newest", fromBlock)
+ assert.NoError(t, err)
+
+ fromBlock, err = resolveFromBlock(ctx, "latest", "")
+ assert.Equal(t, "newest", fromBlock)
+ assert.NoError(t, err)
+
+ fromBlock, err = resolveFromBlock(ctx, "newest", "")
+ assert.Equal(t, "newest", fromBlock)
+ assert.NoError(t, err)
+
+ fromBlock, err = resolveFromBlock(ctx, "0", "")
+ assert.Equal(t, "0", fromBlock)
+ assert.NoError(t, err)
+
+ fromBlock, err = resolveFromBlock(ctx, "0", "000000000010/4763a0c50e3bba7cef1a7ba35dd3f9f3426bb04d0156f326e84ec99387c4746d")
+ assert.Equal(t, "9", fromBlock)
+ assert.NoError(t, err)
+
+ fromBlock, err = resolveFromBlock(ctx, "20", "000000000010/4763a0c50e3bba7cef1a7ba35dd3f9f3426bb04d0156f326e84ec99387c4746d")
+ assert.Equal(t, "20", fromBlock)
+ assert.NoError(t, err)
+
+ fromBlock, err = resolveFromBlock(ctx, "", "000000000010/4763a0c50e3bba7cef1a7ba35dd3f9f3426bb04d0156f326e84ec99387c4746d")
+ assert.Equal(t, "9", fromBlock)
+ assert.NoError(t, err)
+
+ _, err = resolveFromBlock(ctx, "", "wrong")
+ assert.Regexp(t, "FF10472", err)
+
+}
diff --git a/internal/blockchain/fabric/fabric.go b/internal/blockchain/fabric/fabric.go
index 54cdf8b295..a854aceee7 100644
--- a/internal/blockchain/fabric/fabric.go
+++ b/internal/blockchain/fabric/fabric.go
@@ -396,7 +396,7 @@ func (f *Fabric) buildEventLocationString(chaincode string) string {
func (f *Fabric) processContractEvent(ctx context.Context, events common.EventsToDispatch, msgJSON fftypes.JSONObject) (err error) {
subID := msgJSON.GetString("subId")
- subName, err := f.streams.getSubscriptionName(ctx, subID)
+ subName, err := f.streams.getSubscriptionName(ctx, subID, false)
if err != nil {
return err // this is a problem - we should be able to find the listener that dispatched this to us
}
@@ -411,7 +411,7 @@ func (f *Fabric) processContractEvent(ctx context.Context, events common.EventsT
return nil
}
-func (f *Fabric) AddFireflySubscription(ctx context.Context, namespace *core.Namespace, contract *blockchain.MultipartyContract) (string, error) {
+func (f *Fabric) AddFireflySubscription(ctx context.Context, namespace *core.Namespace, contract *blockchain.MultipartyContract, lastProtocolID string) (string, error) {
fabricOnChainLocation, err := parseContractLocation(ctx, contract.Location)
if err != nil {
return "", err
@@ -437,7 +437,7 @@ func (f *Fabric) AddFireflySubscription(ctx context.Context, namespace *core.Nam
if !ok {
return "", i18n.NewError(ctx, coremsgs.MsgInternalServerError, "eventstream ID not found")
}
- sub, err := f.streams.ensureFireFlySubscription(ctx, namespace.Name, version, fabricOnChainLocation, contract.FirstEvent, streamID, batchPinEvent)
+ sub, err := f.streams.ensureFireFlySubscription(ctx, namespace.Name, version, fabricOnChainLocation, contract.FirstEvent, streamID, batchPinEvent, lastProtocolID)
if err != nil {
return "", err
}
@@ -540,7 +540,7 @@ func (f *Fabric) eventLoop(namespace string, wsconn wsclient.WSClient, closed ch
var receipt common.BlockchainReceiptNotification
_ = json.Unmarshal(msgBytes, &receipt)
- err := common.HandleReceipt(ctx, f, &receipt, f.callbacks)
+ err := common.HandleReceipt(ctx, namespace, f, &receipt, f.callbacks)
if err != nil {
l.Errorf("Failed to process receipt: %+v", msgTyped)
}
@@ -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")
@@ -925,15 +965,26 @@ func encodeContractLocation(ctx context.Context, ntype blockchain.NormalizeType,
return result, err
}
-func (f *Fabric) AddContractListener(ctx context.Context, listener *core.ContractListener) error {
+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)
+ result, err := f.streams.createSubscription(ctx, location, f.streamID[namespace], subName, filter.Event.Name, listener.Options.FirstEvent, lastProtocolID)
if err != nil {
return err
}
@@ -947,7 +998,13 @@ func (f *Fabric) DeleteContractListener(ctx context.Context, subscription *core.
func (f *Fabric) GetContractListenerStatus(ctx context.Context, namespace, subID string, okNotFound bool) (bool, interface{}, core.ContractListenerStatus, error) {
// Fabconnect does not currently provide any additional status info for listener subscriptions.
- return true, nil, core.ContractListenerStatusUnknown, nil
+ // But we check for existence of the subscription
+ sub, err := f.streams.getSubscription(ctx, subID, okNotFound)
+ if err != nil || sub == nil {
+ return false, nil, core.ContractListenerStatusUnknown, err
+ }
+
+ return true, nil, core.ContractListenerStatusUnknown, err
}
func (f *Fabric) GetFFIParamValidator(ctx context.Context) (fftypes.FFIParamValidator, error) {
@@ -959,8 +1016,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 1b315e4535..2453ba5d46 100644
--- a/internal/blockchain/fabric/fabric_test.go
+++ b/internal/blockchain/fabric/fabric_test.go
@@ -442,7 +442,7 @@ func TestInitAllExistingStreams(t *testing.T) {
<-toServer
- _, err = e.AddFireflySubscription(e.ctx, ns, contract)
+ _, err = e.AddFireflySubscription(e.ctx, ns, contract, "")
assert.NoError(t, err)
assert.Equal(t, 3, httpmock.GetTotalCallCount())
@@ -500,7 +500,7 @@ func TestInitAllExistingStreamsV1(t *testing.T) {
<-toServer
- _, err = e.AddFireflySubscription(e.ctx, ns, contract)
+ _, err = e.AddFireflySubscription(e.ctx, ns, contract, "")
assert.NoError(t, err)
assert.Equal(t, 3, httpmock.GetTotalCallCount())
@@ -525,6 +525,7 @@ func TestAddFireflySubscriptionGlobal(t *testing.T) {
json.NewDecoder(req.Body).Decode(&body)
assert.Equal(t, "firefly", body["channel"])
assert.Equal(t, nil, body["chaincode"])
+ assert.Equal(t, "9", body["fromBlock"])
return httpmock.NewJsonResponderOrPanic(200, body)(req)
})
@@ -554,7 +555,7 @@ func TestAddFireflySubscriptionGlobal(t *testing.T) {
assert.NoError(t, err)
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
e.streamID["ns1"] = "es12345"
- _, err = e.AddFireflySubscription(e.ctx, ns, contract)
+ _, err = e.AddFireflySubscription(e.ctx, ns, contract, "000000000010/4763a0c50e3bba7cef1a7ba35dd3f9f3426bb04d0156f326e84ec99387c4746d")
assert.NoError(t, err)
}
@@ -604,7 +605,7 @@ func TestAddFireflySubscriptionEventstreamFail(t *testing.T) {
err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi)
assert.NoError(t, err)
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
- _, err = e.AddFireflySubscription(e.ctx, ns, contract)
+ _, err = e.AddFireflySubscription(e.ctx, ns, contract, "")
assert.Regexp(t, "FF10465", err)
}
@@ -646,7 +647,7 @@ func TestAddFireflySubscriptionBadOptions(t *testing.T) {
assert.NoError(t, err)
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
e.streamID["ns1"] = "es12345"
- _, err = e.AddFireflySubscription(e.ctx, ns, contract)
+ _, err = e.AddFireflySubscription(e.ctx, ns, contract, "")
assert.Regexp(t, "pop", err)
}
@@ -687,7 +688,7 @@ func TestAddFireflySubscriptionQuerySubsFail(t *testing.T) {
assert.NoError(t, err)
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
e.streamID["ns1"] = "es12345"
- _, err = e.AddFireflySubscription(e.ctx, ns, contract)
+ _, err = e.AddFireflySubscription(e.ctx, ns, contract, "")
assert.Regexp(t, "pop", err)
}
@@ -729,7 +730,7 @@ func TestAddFireflySubscriptionGetVersionError(t *testing.T) {
err := e.Init(e.ctx, e.cancelCtx, utConfig, &metricsmocks.Manager{}, cmi)
assert.NoError(t, err)
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
- _, err = e.AddFireflySubscription(e.ctx, ns, contract)
+ _, err = e.AddFireflySubscription(e.ctx, ns, contract, "")
assert.Regexp(t, "pop", err)
}
@@ -784,7 +785,7 @@ func TestAddAndRemoveFireflySubscriptionDeprecatedSubName(t *testing.T) {
<-toServer
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
- subID, err := e.AddFireflySubscription(e.ctx, ns, contract)
+ subID, err := e.AddFireflySubscription(e.ctx, ns, contract, "")
assert.NoError(t, err)
assert.Equal(t, 3, httpmock.GetTotalCallCount())
@@ -846,7 +847,7 @@ func TestAddFireflySubscriptionInvalidSubName(t *testing.T) {
<-toServer
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
- _, err = e.AddFireflySubscription(e.ctx, ns, contract)
+ _, err = e.AddFireflySubscription(e.ctx, ns, contract, "")
assert.Regexp(t, "FF10416", err)
}
@@ -860,7 +861,7 @@ func TestAddFFSubscriptionBadLocation(t *testing.T) {
FirstEvent: "oldest",
}
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
- _, err := e.AddFireflySubscription(e.ctx, ns, contract)
+ _, err := e.AddFireflySubscription(e.ctx, ns, contract, "")
assert.Regexp(t, "F10310", err)
}
@@ -1091,7 +1092,7 @@ func TestSubQueryCreateError(t *testing.T) {
assert.NoError(t, err)
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
e.streamID["ns1"] = "es12345"
- _, err = e.AddFireflySubscription(e.ctx, ns, contract)
+ _, err = e.AddFireflySubscription(e.ctx, ns, contract, "")
assert.Regexp(t, "FF10284.*pop", err)
}
@@ -1139,7 +1140,7 @@ func TestSubQueryCreate(t *testing.T) {
assert.NoError(t, err)
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
e.streamID["ns1"] = "es12345"
- _, err = e.AddFireflySubscription(e.ctx, ns, contract)
+ _, err = e.AddFireflySubscription(e.ctx, ns, contract, "")
assert.NoError(t, err)
}
@@ -1814,7 +1815,7 @@ func TestHandleReceiptTXSuccess(t *testing.T) {
err := json.Unmarshal(data, &reply)
assert.NoError(t, err)
- common.HandleReceipt(context.Background(), e, &reply, e.callbacks)
+ common.HandleReceipt(context.Background(), "ns1", e, &reply, e.callbacks)
em.AssertExpectations(t)
}
@@ -1835,7 +1836,7 @@ func TestHandleReceiptNoRequestID(t *testing.T) {
data := []byte(`{}`)
err := json.Unmarshal(data, &reply)
assert.NoError(t, err)
- common.HandleReceipt(context.Background(), e, &reply, e.callbacks)
+ common.HandleReceipt(context.Background(), "", e, &reply, e.callbacks)
}
func TestHandleReceiptFailedTx(t *testing.T) {
@@ -1875,7 +1876,7 @@ func TestHandleReceiptFailedTx(t *testing.T) {
err := json.Unmarshal(data, &reply)
assert.NoError(t, err)
- common.HandleReceipt(context.Background(), e, &reply, e.callbacks)
+ common.HandleReceipt(context.Background(), "", e, &reply, e.callbacks)
em.AssertExpectations(t)
}
@@ -1896,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),
},
@@ -1914,11 +1919,56 @@ func TestAddSubscription(t *testing.T) {
return httpmock.NewJsonResponderOrPanic(200, &subscription{})(req)
})
- err := e.AddContractListener(context.Background(), sub)
+ err := e.AddContractListener(context.Background(), sub, "")
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()
@@ -1931,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),
},
@@ -1948,7 +2002,7 @@ func TestAddSubscriptionNoChannel(t *testing.T) {
return httpmock.NewJsonResponderOrPanic(200, &subscription{})(req)
})
- err := e.AddContractListener(context.Background(), sub)
+ err := e.AddContractListener(context.Background(), sub, "")
assert.Regexp(t, "FF10310.*channel", err)
}
@@ -1965,13 +2019,17 @@ func TestAddSubscriptionNoLocation(t *testing.T) {
}
sub := &core.ContractListener{
- Event: &core.FFISerializedEvent{},
+ Filters: core.ListenerFilters{
+ {
+ Event: &core.FFISerializedEvent{},
+ },
+ },
Options: &core.ContractListenerOptions{
FirstEvent: string(core.SubOptsFirstEventOldest),
},
}
- err := e.AddContractListener(context.Background(), sub)
+ err := e.AddContractListener(context.Background(), sub, "")
assert.Regexp(t, "FF10310.*channel", err)
}
@@ -1988,11 +2046,15 @@ 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)
+ err := e.AddContractListener(context.Background(), sub, "")
assert.Regexp(t, "FF10310", err)
}
@@ -2009,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),
},
@@ -2022,7 +2088,7 @@ func TestAddSubscriptionFail(t *testing.T) {
httpmock.RegisterResponder("POST", `http://localhost:12345/subscriptions`,
httpmock.NewStringResponder(500, "pop"))
- err := e.AddContractListener(context.Background(), sub)
+ err := e.AddContractListener(context.Background(), sub, "")
assert.Regexp(t, "FF10284.*pop", err)
}
@@ -2889,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 &&
@@ -3259,12 +3360,62 @@ func TestGetContractListenerStatus(t *testing.T) {
httpmock.ActivateNonDefault(e.client.GetClient())
defer httpmock.DeactivateAndReset()
- _, detail, status, err := e.GetContractListenerStatus(context.Background(), "ns1", "id", true)
+ e.streams = &streamManager{
+ client: e.client,
+ }
+
+ httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions/id",
+ httpmock.NewJsonResponderOrPanic(200, subscription{
+ ID: "sb-cb37cc07-e873-4f58-44ab-55add6bba320", Stream: "es12345", Name: "ff-sub-ns1-11232312312",
+ }))
+
+ found, detail, status, err := e.GetContractListenerStatus(context.Background(), "ns1", "id", true)
+ assert.True(t, found)
assert.Nil(t, detail)
assert.Equal(t, core.ContractListenerStatusUnknown, status)
assert.NoError(t, err)
}
+func TestGetContractListenerStatusNotFound(t *testing.T) {
+ e, cancel := newTestFabric()
+ defer cancel()
+ httpmock.ActivateNonDefault(e.client.GetClient())
+ defer httpmock.DeactivateAndReset()
+
+ e.streams = &streamManager{
+ client: e.client,
+ }
+
+ httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions/id",
+ httpmock.NewJsonResponderOrPanic(404, nil))
+
+ found, detail, status, err := e.GetContractListenerStatus(context.Background(), "ns1", "id", true)
+ assert.False(t, found)
+ assert.Nil(t, detail)
+ assert.Equal(t, core.ContractListenerStatusUnknown, status)
+ assert.NoError(t, err)
+}
+
+func TestGetContractListenerStatusError(t *testing.T) {
+ e, cancel := newTestFabric()
+ defer cancel()
+ httpmock.ActivateNonDefault(e.client.GetClient())
+ defer httpmock.DeactivateAndReset()
+
+ e.streams = &streamManager{
+ client: e.client,
+ }
+
+ httpmock.RegisterResponder("GET", "http://localhost:12345/subscriptions/id",
+ httpmock.NewJsonResponderOrPanic(500, nil))
+
+ found, detail, status, err := e.GetContractListenerStatus(context.Background(), "ns1", "id", true)
+ assert.False(t, found)
+ assert.Nil(t, detail)
+ assert.Equal(t, core.ContractListenerStatusUnknown, status)
+ assert.Error(t, err)
+}
+
func TestGetTransactionStatus(t *testing.T) {
e, cancel := newTestFabric()
defer cancel()
@@ -3408,3 +3559,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 e9b1a2383e..a2103ca4ca 100644
--- a/internal/blockchain/tezos/tezos.go
+++ b/internal/blockchain/tezos/tezos.go
@@ -261,7 +261,11 @@ func (t *Tezos) Capabilities() *blockchain.Capabilities {
return t.capabilities
}
-func (t *Tezos) AddFireflySubscription(ctx context.Context, namespace *core.Namespace, contract *blockchain.MultipartyContract) (string, error) {
+func (t *Tezos) AddFireflySubscription(ctx context.Context,
+ namespace *core.Namespace,
+ contract *blockchain.MultipartyContract,
+ _ string, // Tezos lexicographically sortable protocol IDs for not yet implemented for events
+) (string, error) {
tezosLocation, err := t.parseContractLocation(ctx, contract.Location)
if err != nil {
return "", err
@@ -406,10 +410,54 @@ func (t *Tezos) NormalizeContractLocation(ctx context.Context, ntype blockchain.
return t.encodeContractLocation(ctx, parsed)
}
-func (t *Tezos) AddContractListener(ctx context.Context, listener *core.ContractListener) (err error) {
+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
}
@@ -420,7 +468,7 @@ func (t *Tezos) AddContractListener(ctx context.Context, listener *core.Contract
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
}
@@ -463,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 {
@@ -527,7 +591,7 @@ func (t *Tezos) GetTransactionStatus(ctx context.Context, operation *core.Operat
TxHash: statusResponse.GetString("transactionHash"),
Message: statusResponse.GetString("errorMessage"),
ProtocolID: receiptInfo.GetString("protocolId")}
- err := common.HandleReceipt(ctx, t, receipt, t.callbacks)
+ err := common.HandleReceipt(ctx, operation.Namespace, t, receipt, t.callbacks)
if err != nil {
log.L(ctx).Warnf("Failed to handle receipt")
}
@@ -758,7 +822,7 @@ func (t *Tezos) eventLoop() {
var receipt common.BlockchainReceiptNotification
_ = json.Unmarshal(msgBytes, &receipt)
- err := common.HandleReceipt(ctx, t, &receipt, t.callbacks)
+ err := common.HandleReceipt(ctx, "", t, &receipt, t.callbacks) // TODO: should be specific to a namespace
if err != nil {
l.Errorf("Failed to process receipt: %+v", msgTyped)
}
diff --git a/internal/blockchain/tezos/tezos_test.go b/internal/blockchain/tezos/tezos_test.go
index 2a55d109fe..6ed39ae656 100644
--- a/internal/blockchain/tezos/tezos_test.go
+++ b/internal/blockchain/tezos/tezos_test.go
@@ -548,7 +548,7 @@ func TestInitAllExistingStreams(t *testing.T) {
assert.NoError(t, err)
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
- _, err = tz.AddFireflySubscription(tz.ctx, ns, contract)
+ _, err = tz.AddFireflySubscription(tz.ctx, ns, contract, "")
assert.NoError(t, err)
assert.Equal(t, 3, httpmock.GetTotalCallCount())
@@ -695,7 +695,7 @@ func TestHandleReceiptTXSuccess(t *testing.T) {
err := json.Unmarshal(data.Bytes(), &reply)
assert.NoError(t, err)
- common.HandleReceipt(context.Background(), tz, &reply, tz.callbacks)
+ common.HandleReceipt(context.Background(), "", tz, &reply, tz.callbacks)
tm.AssertExpectations(t)
}
@@ -780,7 +780,7 @@ func TestHandleReceiptTXUpdateTezosConnect(t *testing.T) {
assert.NoError(t, err)
expectedReceiptId := "ns1:" + operationID.String()
assert.Equal(t, reply.Headers.ReceiptID, expectedReceiptId)
- common.HandleReceipt(context.Background(), tz, &reply, tz.callbacks)
+ common.HandleReceipt(context.Background(), "", tz, &reply, tz.callbacks)
tm.AssertExpectations(t)
}
@@ -797,7 +797,7 @@ func TestHandleMsgBatchBadData(t *testing.T) {
data := fftypes.JSONAnyPtr(`{}`)
err := json.Unmarshal(data.Bytes(), &reply)
assert.NoError(t, err)
- common.HandleReceipt(context.Background(), tz, &reply, tz.callbacks)
+ common.HandleReceipt(context.Background(), "", tz, &reply, tz.callbacks)
}
func TestAddSubscription(t *testing.T) {
@@ -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{
@@ -827,7 +831,7 @@ func TestAddSubscription(t *testing.T) {
httpmock.RegisterResponder("POST", `http://localhost:12345/subscriptions`,
httpmock.NewJsonResponderOrPanic(200, &subscription{}))
- err := tz.AddContractListener(context.Background(), sub)
+ err := tz.AddContractListener(context.Background(), sub, "")
assert.NoError(t, err)
}
@@ -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{
@@ -856,7 +864,7 @@ func TestAddSubscriptionWithoutLocation(t *testing.T) {
httpmock.RegisterResponder("POST", `http://localhost:12345/subscriptions`,
httpmock.NewJsonResponderOrPanic(200, &subscription{}))
- err := tz.AddContractListener(context.Background(), sub)
+ err := tz.AddContractListener(context.Background(), sub, "")
assert.NoError(t, err)
}
@@ -873,11 +881,15 @@ 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)
+ err := tz.AddContractListener(context.Background(), sub, "")
assert.Regexp(t, "FF10310", err)
}
@@ -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),
},
@@ -905,11 +921,65 @@ func TestAddSubscriptionFail(t *testing.T) {
httpmock.RegisterResponder("POST", `http://localhost:12345/subscriptions`,
httpmock.NewStringResponder(500, "pop"))
- err := tz.AddContractListener(context.Background(), sub)
+ err := tz.AddContractListener(context.Background(), sub, "")
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()
@@ -1549,7 +1652,7 @@ func TestAddSubBadLocation(t *testing.T) {
}
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
- _, err := tz.AddFireflySubscription(tz.ctx, ns, contract)
+ _, err := tz.AddFireflySubscription(tz.ctx, ns, contract, "")
assert.Regexp(t, "FF10310", err)
}
@@ -1597,7 +1700,7 @@ func TestAddAndRemoveFireflySubscription(t *testing.T) {
}
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
- subID, err := tz.AddFireflySubscription(tz.ctx, ns, contract)
+ subID, err := tz.AddFireflySubscription(tz.ctx, ns, contract, "")
assert.NoError(t, err)
assert.NotNil(t, tz.subs.GetSubscription("sub1"))
@@ -1641,7 +1744,7 @@ func TestAddFireflySubscriptionQuerySubsFail(t *testing.T) {
}
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
- _, err = tz.AddFireflySubscription(tz.ctx, ns, contract)
+ _, err = tz.AddFireflySubscription(tz.ctx, ns, contract, "")
assert.Regexp(t, "FF10283", err)
}
@@ -1681,7 +1784,7 @@ func TestAddFireflySubscriptionCreateError(t *testing.T) {
}
ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
- _, err = tz.AddFireflySubscription(tz.ctx, ns, contract)
+ _, err = tz.AddFireflySubscription(tz.ctx, ns, contract, "")
assert.Regexp(t, "FF10283", err)
}
@@ -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 628f09450a..753ab7e54f 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)
@@ -158,7 +160,12 @@ func NewContractManager(ctx context.Context, ns string, di database.Plugin, bi b
// cause recreation of all the listeners (noting that listeners that were specified to start
// from latest, will start from the new latest rather than replaying from the block they
// started from before they were deleted).
- return cm, cm.verifyListeners(ctx)
+ err = cm.verifyListeners(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ return cm, nil
}
func (cm *contractManager) Name() string {
@@ -206,7 +213,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 +233,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 +275,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 +783,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]
@@ -804,14 +840,159 @@ func (cm *contractManager) checkContractListenerExists(ctx context.Context, list
log.L(ctx).Debugf("Validated listener %s:%s (BackendID=%s)", listener.Signature, listener.ID, listener.BackendID)
return nil
}
- if err = cm.blockchain.AddContractListener(ctx, listener); err != nil {
+
+ // For the case that we're establishing a listener from "latest" we obtain the protocol ID
+ // of the latest event confirmed from the blockchain for a given subscription.
+ // This protocolID should be parsed and used by the blockchain plugin if SubOptsFirstEventNewest
+ // is passed through, and the listener does not exist.
+ fb := database.BlockchainEventQueryFactory.NewFilter(ctx).Sort("-protocolid").Limit(1)
+ latestEvents, _, err := cm.database.GetBlockchainEvents(ctx, cm.namespace, fb.Eq(
+ "listener", listener.ID,
+ ))
+ if err != nil {
+ return err
+ }
+ lastProtocolID := ""
+ if len(latestEvents) > 0 {
+ lastProtocolID = latestEvents[0].ProtocolID
+ }
+
+ if err = cm.blockchain.AddContractListener(ctx, listener, lastProtocolID); err != nil {
return err
}
return cm.database.UpdateContractListener(ctx, cm.namespace, listener.ID,
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
@@ -824,6 +1005,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
@@ -836,6 +1023,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 != "" {
@@ -846,20 +1038,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.
@@ -867,6 +1045,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),
@@ -877,26 +1061,62 @@ 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
}
- if err = cm.blockchain.AddContractListener(ctx, &listener.ContractListener); err != nil {
+
+ if err = cm.blockchain.AddContractListener(ctx, &listener.ContractListener, ""); err != nil {
return nil, err
}
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) {
@@ -917,6 +1137,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 {
@@ -932,6 +1175,7 @@ func (cm *contractManager) GetContractListenerByNameOrID(ctx context.Context, na
if listener == nil {
return nil, i18n.NewError(ctx, coremsgs.Msg404NotFound)
}
+
return listener, nil
}
@@ -968,17 +1212,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 deb02a3523..e936fb28d2 100644
--- a/internal/contracts/manager_test.go
+++ b/internal/contracts/manager_test.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2023 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -98,6 +98,33 @@ func TestName(t *testing.T) {
assert.Equal(t, "ContractManager", cm.Name())
}
+func TestNewContractManagerVerifyListenersFails(t *testing.T) {
+ mdi := &databasemocks.Plugin{}
+ mdm := &datamocks.Manager{}
+ mbm := &broadcastmocks.Manager{}
+ mpm := &privatemessagingmocks.Manager{}
+ mbp := &batchmocks.Manager{}
+ mim := &identitymanagermocks.Manager{}
+ mbi := &blockchainmocks.Plugin{}
+ mom := &operationmocks.Manager{}
+ txw := &txwritermocks.Writer{}
+ cmi := &cachemocks.Manager{}
+ msa := &syncasyncmocks.Bridge{}
+
+ ctx := context.Background()
+
+ cmi.On("GetCache", mock.Anything).Return(cache.NewUmanagedCache(ctx, 100, 5*time.Minute), nil)
+ txHelper, _ := txcommon.NewTransactionHelper(ctx, "ns1", mdi, mdm, cmi)
+ mbi.On("GetFFIParamValidator", mock.Anything).Return(nil, nil)
+ mom.On("RegisterHandler", mock.Anything, mock.Anything, mock.Anything)
+ mbi.On("Name").Return("mockblockchain").Maybe()
+ mdi.On("GetContractListeners", mock.Anything, "ns1", mock.Anything).Return(nil, nil, fmt.Errorf("KABOOM!")).Once()
+
+ cm, err := NewContractManager(context.Background(), "ns1", mdi, mbi, mdm, mbm, mpm, mbp, mim, mom, txHelper, txw, msa, cmi)
+ assert.Nil(t, cm)
+ assert.NotNil(t, err)
+}
+
func TestNewContractManagerFFISchemaLoaderFail(t *testing.T) {
mdi := &databasemocks.Plugin{}
mdm := &datamocks.Manager{}
@@ -774,9 +801,10 @@ 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)
+ mbi.On("AddContractListener", context.Background(), &sub.ContractListener, "").Return(nil)
mdi.On("InsertContractListener", context.Background(), &sub.ContractListener).Return(nil)
result, err := cm.AddContractListener(context.Background(), sub)
@@ -811,12 +839,13 @@ 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
return cl.Location == nil
- })).Return(nil)
+ }), "").Return(nil)
mdi.On("InsertContractListener", context.Background(), &sub.ContractListener).Return(nil)
result, err := cm.AddContractListener(context.Background(), sub)
@@ -851,9 +880,10 @@ 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)
+ mbi.On("AddContractListener", context.Background(), &sub.ContractListener, "").Return(nil)
mdi.On("InsertContractListener", context.Background(), &sub.ContractListener).Return(nil)
result, err := cm.AddContractListener(context.Background(), sub)
@@ -900,9 +930,10 @@ 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)
+ mbi.On("AddContractListener", context.Background(), &sub.ContractListener, "").Return(nil)
mdi.On("GetFFIByID", context.Background(), "ns1", interfaceID).Return(&fftypes.FFI{}, nil)
mdi.On("GetFFIEvent", context.Background(), "ns1", interfaceID, sub.EventPath).Return(event, nil)
mdi.On("InsertContractListener", context.Background(), &sub.ContractListener).Return(nil)
@@ -910,7 +941,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)
@@ -1071,6 +1102,13 @@ func TestAddContractListenerVerifyOk(t *testing.T) {
fi, _ := f.Finalize()
return fi.Skip == 50 && fi.Limit == 50
})).Return([]*core.ContractListener{}, nil, nil).Once()
+ mdi.On("GetBlockchainEvents", mock.Anything, "ns1", mock.MatchedBy(func(f ffapi.Filter) bool {
+ fi, err := f.Finalize()
+ assert.NoError(t, err)
+ return fi.Limit == 1 && strings.Contains(fi.String(), "listener")
+ })).Return([]*core.BlockchainEvent{
+ {Namespace: "ns1", ID: fftypes.NewUUID(), ProtocolID: "001/002/003"},
+ }, nil, nil).Once()
mbi := cm.blockchain.(*blockchainmocks.Plugin)
mbi.On("GetContractListenerStatus", ctx, "ns1", "12345", true).Return(true, struct{}{}, core.ContractListenerStatusSynced, nil)
@@ -1079,7 +1117,7 @@ func TestAddContractListenerVerifyOk(t *testing.T) {
prevBackendID := l.BackendID
l.BackendID = "34567"
return prevBackendID == "23456"
- })).Return(nil)
+ }), "001/002/003").Return(nil)
mdi.On("UpdateContractListener", ctx, "ns1", mock.Anything, mock.MatchedBy(func(u ffapi.Update) bool {
uu, _ := u.Finalize()
@@ -1106,6 +1144,7 @@ func TestAddContractListenerVerifyUpdateFail(t *testing.T) {
{Namespace: "ns1", ID: fftypes.NewUUID(), BackendID: "12345"},
{Namespace: "ns1", ID: fftypes.NewUUID(), BackendID: "23456"},
}, nil, nil).Once()
+ mdi.On("GetBlockchainEvents", mock.Anything, "ns1", mock.Anything).Return([]*core.BlockchainEvent{}, nil, nil).Once()
mbi := cm.blockchain.(*blockchainmocks.Plugin)
mbi.On("GetContractListenerStatus", ctx, "ns1", "12345", true).Return(true, struct{}{}, core.ContractListenerStatusSynced, nil)
@@ -1114,7 +1153,7 @@ func TestAddContractListenerVerifyUpdateFail(t *testing.T) {
prevBackendID := l.BackendID
l.BackendID = "34567"
return prevBackendID == "23456"
- })).Return(nil)
+ }), "").Return(nil)
mdi.On("UpdateContractListener", ctx, "ns1", mock.Anything, mock.MatchedBy(func(u ffapi.Update) bool {
uu, _ := u.Finalize()
@@ -1141,6 +1180,7 @@ func TestAddContractListenerVerifyAddFail(t *testing.T) {
{Namespace: "ns1", ID: fftypes.NewUUID(), BackendID: "12345"},
{Namespace: "ns1", ID: fftypes.NewUUID(), BackendID: "23456"},
}, nil, nil).Once()
+ mdi.On("GetBlockchainEvents", mock.Anything, "ns1", mock.Anything).Return([]*core.BlockchainEvent{}, nil, nil).Once()
mbi := cm.blockchain.(*blockchainmocks.Plugin)
mbi.On("GetContractListenerStatus", ctx, "ns1", "12345", true).Return(true, struct{}{}, core.ContractListenerStatusSynced, nil)
@@ -1149,7 +1189,34 @@ func TestAddContractListenerVerifyAddFail(t *testing.T) {
prevBackendID := l.BackendID
l.BackendID = "34567"
return prevBackendID == "23456"
- })).Return(fmt.Errorf("pop"))
+ }), "").Return(fmt.Errorf("pop"))
+
+ err := cm.verifyListeners(ctx)
+ assert.Regexp(t, "pop", err)
+
+ mdi.AssertExpectations(t)
+ mbi.AssertExpectations(t)
+}
+
+func TestAddContractListenerGetEventsFail(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"},
+ }, nil, nil).Once()
+ mdi.On("GetBlockchainEvents", mock.Anything, "ns1", mock.Anything).
+ Return(nil, nil, fmt.Errorf("pop")).Once()
+
+ mbi := cm.blockchain.(*blockchainmocks.Plugin)
+ 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)
err := cm.verifyListeners(ctx)
assert.Regexp(t, "pop", err)
@@ -1196,6 +1263,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{
@@ -1218,7 +1436,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)
@@ -1235,19 +1453,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{
@@ -1256,67 +1474,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)
@@ -1348,8 +1611,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)
@@ -1384,9 +1645,10 @@ 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"))
+ mbi.On("AddContractListener", context.Background(), &sub.ContractListener, "").Return(fmt.Errorf("pop"))
_, err := cm.AddContractListener(context.Background(), sub)
assert.EqualError(t, err, "pop")
@@ -1421,9 +1683,10 @@ 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)
+ mbi.On("AddContractListener", context.Background(), &sub.ContractListener, "").Return(nil)
mdi.On("InsertContractListener", context.Background(), &sub.ContractListener).Return(fmt.Errorf("pop"))
_, err := cm.AddContractListener(context.Background(), sub)
@@ -1460,13 +1723,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)
+ }), "").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)
@@ -1553,7 +1817,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)
@@ -1594,7 +1858,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)
@@ -2975,6 +3239,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)
@@ -2998,7 +3274,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())
@@ -3009,22 +3286,93 @@ 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(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.Error(t, err)
+ assert.Regexp(t, "pop", err.Error())
+
+ mbi.AssertExpectations(t)
+ mdi.AssertExpectations(t)
+}
+
+func TestGetContractAPIListenersOldSignatureFail(t *testing.T) {
+ cm := newTestContractManager()
+ mbi := cm.blockchain.(*blockchainmocks.Plugin)
+ 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(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", 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.Error(t, err)
+ assert.Regexp(t, "pop", err.Error())
+
+ mbi.AssertExpectations(t)
+ mdi.AssertExpectations(t)
+}
+
+func TestGetContractAPIListenersNotFound(t *testing.T) {
+ cm := newTestContractManager()
+ 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)
mdi.On("GetContractAPIByName", context.Background(), "ns1", "simple").Return(nil, fmt.Errorf("pop"))
@@ -3261,7 +3609,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)
@@ -3854,3 +4202,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/contracts/operations.go b/internal/contracts/operations.go
index c308aa51ad..cbb9c63e3f 100644
--- a/internal/contracts/operations.go
+++ b/internal/contracts/operations.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2023 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -17,6 +17,7 @@
package contracts
import (
+ "bytes"
"context"
"encoding/json"
@@ -38,7 +39,9 @@ type blockchainContractDeployData struct {
func addBlockchainReqInputs(op *core.Operation, req interface{}) (err error) {
var reqJSON []byte
if reqJSON, err = json.Marshal(req); err == nil {
- err = json.Unmarshal(reqJSON, &op.Input)
+ d := json.NewDecoder(bytes.NewReader(reqJSON))
+ d.UseNumber()
+ err = d.Decode(&op.Input)
}
return err
}
diff --git a/internal/contracts/operations_test.go b/internal/contracts/operations_test.go
index bd425dabc1..b3dcb9b4ff 100644
--- a/internal/contracts/operations_test.go
+++ b/internal/contracts/operations_test.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2023 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -16,7 +16,9 @@
package contracts
import (
+ "bytes"
"context"
+ "encoding/json"
"fmt"
"testing"
@@ -35,6 +37,21 @@ import (
"github.com/stretchr/testify/mock"
)
+const sampleRequestLargeNumberInput = `{
+ "location": {
+ "address": "0x1111"
+ },
+ "key": "0x123",
+ "method": {
+ "name": "set",
+ "params": null,
+ "returns": null
+ },
+ "input": {
+ "value": 10000000000000000000000000001
+ }
+}`
+
func reqWithMessage(msgType core.MessageType) *core.ContractCallRequest {
return &core.ContractCallRequest{
Key: "0x123",
@@ -60,6 +77,7 @@ func reqWithMessage(msgType core.MessageType) *core.ContractCallRequest {
}
func TestPrepareAndRunBlockchainInvoke(t *testing.T) {
+
cm := newTestContractManager()
op := &core.Operation{
@@ -101,6 +119,46 @@ func TestPrepareAndRunBlockchainInvoke(t *testing.T) {
mbi.AssertExpectations(t)
}
+func TestPrepareAndRunBlockchainInvokeLargeNumberInput(t *testing.T) {
+
+ cm := newTestContractManager()
+
+ var req core.ContractCallRequest
+ d := json.NewDecoder(bytes.NewReader([]byte(sampleRequestLargeNumberInput)))
+ d.UseNumber()
+ err := d.Decode(&req)
+ assert.NoError(t, err)
+
+ op := &core.Operation{
+ Type: core.OpTypeBlockchainInvoke,
+ ID: fftypes.NewUUID(),
+ Namespace: "ns1",
+ }
+
+ err = addBlockchainReqInputs(op, req)
+ assert.NoError(t, err)
+
+ mbi := cm.blockchain.(*blockchainmocks.Plugin)
+ opaqueData := "anything"
+ mbi.On("ParseInterface", context.Background(), mock.MatchedBy(func(method *fftypes.FFIMethod) bool {
+ return method.Name == req.Method.Name
+ }), req.Errors).Return(opaqueData, nil)
+ mbi.On("InvokeContract", context.Background(), "ns1:"+op.ID.String(), "0x123", mock.MatchedBy(func(loc *fftypes.JSONAny) bool {
+ return loc.String() == req.Location.String()
+ }), opaqueData, req.Input, req.Options, (*blockchain.BatchPin)(nil)).Return(false, nil)
+
+ po, err := cm.PrepareOperation(context.Background(), op)
+ assert.NoError(t, err)
+ assert.Equal(t, &req, po.Data.(txcommon.BlockchainInvokeData).Request)
+
+ _, phase, err := cm.RunOperation(context.Background(), po)
+
+ assert.Equal(t, core.OpPhasePending, phase)
+ assert.NoError(t, err)
+
+ mbi.AssertExpectations(t)
+}
+
func TestPrepareAndRunBlockchainInvokeRejected(t *testing.T) {
cm := newTestContractManager()
diff --git a/internal/coremsgs/en_api_translations.go b/internal/coremsgs/en_api_translations.go
index bb9b36e066..02f1a96d5c 100644
--- a/internal/coremsgs/en_api_translations.go
+++ b/internal/coremsgs/en_api_translations.go
@@ -20,8 +20,8 @@ package coremsgs
var (
CoreSystemNSDescription = ffm("core.systemNSDescription", "FireFly system namespace (legacy - no longer used by newer versions)")
- APIParamsConfigRecordKeyUpdate = ffm("api.params.configRecordKey.update", "The configuration key to update. This should use dot notation to reference a key documented in https://hyperledger.github.io/firefly/reference/config.html")
- APIParamsConfigRecordKeyGet = ffm("api.params.configRecordKey.get", "The configuration key to get. This should use dot notation to reference a key documented in https://hyperledger.github.io/firefly/reference/config.html")
+ APIParamsConfigRecordKeyUpdate = ffm("api.params.configRecordKey.update", "The configuration key to update. This should use dot notation to reference a key documented in https://hyperledger.github.io/firefly/latest/reference/config/")
+ APIParamsConfigRecordKeyGet = ffm("api.params.configRecordKey.get", "The configuration key to get. This should use dot notation to reference a key documented in https://hyperledger.github.io/firefly/latest/reference/config/")
APIParamsOperationIDGet = ffm("api.params.operationID.get", "The operation ID key to get")
APIParamsOperationNamespacedID = ffm("api.params.spiOperationID", "The operation ID as passed to the connector when the operation was performed, including the 'namespace:' prefix")
APIParamsNamespace = ffm("api.params.namespace", "The namespace which scopes this request")
@@ -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")
@@ -202,7 +203,8 @@ var (
APIFilterLimitDesc = ffm("api.filterLimit", "The maximum number of records to return (max: %d)")
APIFilterCountDesc = ffm("api.filterCount", "Return a total count as well as items (adds extra database processing)")
APIFetchDataDesc = ffm("api.fetchData", "Fetch the data and include it in the messages returned")
- APIConfirmQueryParam = ffm("api.confirmQueryParam", "When true the HTTP request blocks until the message is confirmed")
+ APIConfirmMsgQueryParam = ffm("api.confirmMsgQueryParam", "When true the HTTP request blocks until the message is confirmed")
+ APIConfirmInvokeQueryParam = ffm("api.confirmInvokeQueryParam", "When true the HTTP request blocks until the blockchain transaction is confirmed")
APIPublishQueryParam = ffm("api.publishQueryParam", "When true the definition will be published to all other members of the multiparty network")
APIHistogramStartTimeParam = ffm("api.histogramStartTime", "Start time of the data to be fetched")
APIHistogramEndTimeParam = ffm("api.histogramEndTime", "End time of the data to be fetched")
diff --git a/internal/coremsgs/en_error_messages.go b/internal/coremsgs/en_error_messages.go
index efb2e9db2e..8cccd5c344 100644
--- a/internal/coremsgs/en_error_messages.go
+++ b/internal/coremsgs/en_error_messages.go
@@ -27,287 +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)
+ 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 f3515877ee..208df0d3b5 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/coremsgs/es/es_struct_descriptions.go b/internal/coremsgs/es/es_struct_descriptions.go
deleted file mode 100644
index cc585b6669..0000000000
--- a/internal/coremsgs/es/es_struct_descriptions.go
+++ /dev/null
@@ -1,52 +0,0 @@
-// 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 es
-
-import (
- "github.com/hyperledger/firefly-common/pkg/i18n"
- "golang.org/x/text/language"
-)
-
-//revive:disable
-
-/*
-This file contains the field level descriptions that are used in
-OpenAPI Spec generation. Each struct field that wants to use one of these
-needs to have an ffstruct tag on it, indicating the name of the struct.
-That will be combined with the JSON field name (note, it is not the GO
-field name, but the JSON serialized name), separated by a "." This is the
-key used to lookup the translation below. If it is not found, the description
-is left blank in the OpenAPI spec
-
-Example:
-// message.go
-type Message struct {
- Header MessageHeader `ffstruct:"Message" json:"header"`
-
-// en_translations_descriptions.go
-MessageHeader = ffm("Message.header", "The message header")
-
-*/
-
-var ffm = func(key, translation string) i18n.MessageKey {
- return i18n.FFM(language.Spanish, key, translation)
-}
-
-var (
- // MessageHeader field descriptions
- MessageHeaderID = ffm("MessageHeader.id", "El UUID del mensaje. รnico para cada mensaje")
-)
diff --git a/internal/database/difactory/factory_test.go b/internal/database/difactory/factory_test.go
new file mode 100644
index 0000000000..f334df0875
--- /dev/null
+++ b/internal/database/difactory/factory_test.go
@@ -0,0 +1,53 @@
+// Copyright ยฉ 2023 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 difactory
+
+import (
+ "context"
+ "testing"
+
+ "github.com/hyperledger/firefly-common/pkg/config"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetPluginUnknown(t *testing.T) {
+ ctx := context.Background()
+ _, err := GetPlugin(ctx, "foo")
+ assert.Error(t, err)
+ assert.Regexp(t, "FF10122", err)
+}
+
+func TestGetPluginPostgres(t *testing.T) {
+ ctx := context.Background()
+ plugin, err := GetPlugin(ctx, "postgres")
+ assert.NoError(t, err)
+ assert.NotNil(t, plugin)
+}
+
+func TestGetPluginSQLite(t *testing.T) {
+ ctx := context.Background()
+ plugin, err := GetPlugin(ctx, "sqlite3")
+ assert.NoError(t, err)
+ assert.NotNil(t, plugin)
+}
+
+var root = config.RootSection("di")
+
+func TestInitConfig(t *testing.T) {
+ conf := root.SubArray("plugins")
+ InitConfig(conf)
+}
diff --git a/internal/database/sqlcommon/contractlisteners_sql.go b/internal/database/sqlcommon/contractlisteners_sql.go
index b1c1caaa0e..87a09d6541 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 b321a685a7..c2b2f79b92 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/internal/dataexchange/dxfactory/factory_test.go b/internal/dataexchange/dxfactory/factory_test.go
new file mode 100644
index 0000000000..989570a105
--- /dev/null
+++ b/internal/dataexchange/dxfactory/factory_test.go
@@ -0,0 +1,46 @@
+// Copyright ยฉ 2023 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 dxfactory
+
+import (
+ "context"
+ "testing"
+
+ "github.com/hyperledger/firefly-common/pkg/config"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetPluginUnknown(t *testing.T) {
+ ctx := context.Background()
+ _, err := GetPlugin(ctx, "foo")
+ assert.Error(t, err)
+ assert.Regexp(t, "FF10213", err)
+}
+
+func TestGetPlugin(t *testing.T) {
+ ctx := context.Background()
+ plugin, err := GetPlugin(ctx, "ffdx")
+ assert.NoError(t, err)
+ assert.NotNil(t, plugin)
+}
+
+var root = config.RootSection("di")
+
+func TestInitConfig(t *testing.T) {
+ conf := root.SubArray("plugins")
+ InitConfig(conf)
+}
diff --git a/internal/definitions/sender.go b/internal/definitions/sender.go
index 3d998c3a86..70f3664095 100644
--- a/internal/definitions/sender.go
+++ b/internal/definitions/sender.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2023 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -93,8 +93,12 @@ func NewDefinitionSender(ctx context.Context, ns *core.Namespace, multiparty boo
tokenBroadcastNames: tokenBroadcastNames,
}
dh, err := newDefinitionHandler(ctx, ns, multiparty, di, bi, dx, dm, im, am, cm, reverseMap(tokenBroadcastNames))
+ if err != nil {
+ return nil, nil, err
+ }
+
ds.handler = dh
- return ds, dh, err
+ return ds, dh, nil
}
// reverseMap reverses the key/values of a given map
diff --git a/internal/definitions/sender_test.go b/internal/definitions/sender_test.go
index b7a8f41499..261c5c3579 100644
--- a/internal/definitions/sender_test.go
+++ b/internal/definitions/sender_test.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2021 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -105,6 +105,26 @@ func TestInitSenderFail(t *testing.T) {
assert.Regexp(t, "FF10128", err)
}
+func TestNewDefinitionSenderHandlerThrows(t *testing.T) {
+ mdi := &databasemocks.Plugin{}
+ mbi := &blockchainmocks.Plugin{}
+ mdx := &dataexchangemocks.Plugin{}
+ mbm := &broadcastmocks.Manager{}
+ mim := &identitymanagermocks.Manager{}
+ mdm := &datamocks.Manager{}
+ mcm := &contractmocks.Manager{}
+
+ tokenBroadcastNames := make(map[string]string)
+ tokenBroadcastNames["connector1"] = "remote1"
+
+ ctx := context.Background()
+ ns := &core.Namespace{Name: "ns1", NetworkName: "ns1"}
+ ds, dh, err := NewDefinitionSender(ctx, ns, false, mdi, mbi, mdx, mbm, mim, mdm, nil, mcm, tokenBroadcastNames)
+ assert.Nil(t, ds)
+ assert.Nil(t, dh)
+ assert.NotNil(t, err)
+}
+
func TestName(t *testing.T) {
ds := newTestDefinitionSender(t)
defer ds.cleanup(t)
diff --git a/internal/events/blockchain_event.go b/internal/events/blockchain_event.go
index 8708c9cd31..337cadcff2 100644
--- a/internal/events/blockchain_event.go
+++ b/internal/events/blockchain_event.go
@@ -71,20 +71,21 @@ func (em *eventManager) getChainListenerByProtocolIDCached(ctx context.Context,
return l, nil
}
-func (em *eventManager) maybePersistBlockchainEvent(ctx context.Context, chainEvent *core.BlockchainEvent, listener *core.ContractListener) error {
+// handleBlockchainBatchPinEvent handles a blockchain event, returning true if the event was created, false if it was a duplicate along with an error if any failures occur
+func (em *eventManager) maybePersistBlockchainEvent(ctx context.Context, chainEvent *core.BlockchainEvent, listener *core.ContractListener) (bool, error) {
existing, err := em.txHelper.InsertOrGetBlockchainEvent(ctx, chainEvent)
if err != nil {
- return err
+ return false, err
}
if existing != nil {
log.L(ctx).Debugf("Ignoring duplicate blockchain event %s", chainEvent.ProtocolID)
// Return the ID of the existing event
chainEvent.ID = existing.ID
- return nil
+ return false, nil
}
topic := em.getTopicForChainListener(listener)
ffEvent := core.NewEvent(core.EventTypeBlockchainEventReceived, chainEvent.Namespace, chainEvent.ID, chainEvent.TX.ID, topic)
- return em.database.InsertEvent(ctx, ffEvent)
+ return true, em.database.InsertEvent(ctx, ffEvent)
}
func (em *eventManager) getChainListenerCached(cacheKey string, getter func() (*core.ContractListener, error)) (*core.ContractListener, error) {
diff --git a/internal/events/blockchain_event_test.go b/internal/events/blockchain_event_test.go
index 67c293a5cd..4823eff538 100644
--- a/internal/events/blockchain_event_test.go
+++ b/internal/events/blockchain_event_test.go
@@ -151,6 +151,7 @@ func TestContractEventWrongNS(t *testing.T) {
}
+// TODO: Add test case for event not existing
func TestPersistBlockchainEventDuplicate(t *testing.T) {
em := newTestEventManager(t)
defer em.cleanup(t)
@@ -173,9 +174,10 @@ func TestPersistBlockchainEventDuplicate(t *testing.T) {
em.mth.On("InsertOrGetBlockchainEvent", mock.Anything, ev).
Return(&core.BlockchainEvent{ID: existingID}, nil)
- err := em.maybePersistBlockchainEvent(em.ctx, ev, nil)
+ created, err := em.maybePersistBlockchainEvent(em.ctx, ev, nil)
assert.NoError(t, err)
assert.Equal(t, existingID, ev.ID)
+ assert.False(t, created)
}
diff --git a/internal/events/eifactory/factory_test.go b/internal/events/eifactory/factory_test.go
new file mode 100644
index 0000000000..fb964793e0
--- /dev/null
+++ b/internal/events/eifactory/factory_test.go
@@ -0,0 +1,59 @@
+// Copyright ยฉ 2023 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 eifactory
+
+import (
+ "context"
+ "testing"
+
+ "github.com/hyperledger/firefly-common/pkg/config"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetPluginUnknown(t *testing.T) {
+ ctx := context.Background()
+ _, err := GetPlugin(ctx, "foo")
+ assert.Error(t, err)
+ assert.Regexp(t, "FF10172", err)
+}
+
+func TestGetPluginWebSockets(t *testing.T) {
+ ctx := context.Background()
+ plugin, err := GetPlugin(ctx, "websockets")
+ assert.NoError(t, err)
+ assert.NotNil(t, plugin)
+}
+
+func TestGetPluginWebHooks(t *testing.T) {
+ ctx := context.Background()
+ plugin, err := GetPlugin(ctx, "webhooks")
+ assert.NoError(t, err)
+ assert.NotNil(t, plugin)
+}
+
+func TestGetPluginEvents(t *testing.T) {
+ ctx := context.Background()
+ plugin, err := GetPlugin(ctx, "system")
+ assert.NoError(t, err)
+ assert.NotNil(t, plugin)
+}
+
+var root = config.RootSection("di")
+
+func TestInitConfig(t *testing.T) {
+ InitConfig(root)
+}
diff --git a/internal/events/event_manager_test.go b/internal/events/event_manager_test.go
index f634108c42..ac06c88c08 100644
--- a/internal/events/event_manager_test.go
+++ b/internal/events/event_manager_test.go
@@ -681,7 +681,7 @@ func TestEventFilterOnSubscriptionMatchesEventType(t *testing.T) {
filteredEvents, _ = em.FilterHistoricalEventsOnSubscription(context.Background(), events, subscription)
assert.NotNil(t, filteredEvents)
assert.Equal(t, 1, len(filteredEvents))
-
+
listenerUuid := fftypes.NewUUID()
events[0].Event.Topic = ""
diff --git a/internal/events/token_pool_created.go b/internal/events/token_pool_created.go
index dec69f4468..2fb4c1ea2c 100644
--- a/internal/events/token_pool_created.go
+++ b/internal/events/token_pool_created.go
@@ -62,10 +62,13 @@ func (em *eventManager) confirmPool(ctx context.Context, pool *core.TokenPool, e
Type: pool.TX.Type,
BlockchainID: blockchainID,
})
- if err := em.maybePersistBlockchainEvent(ctx, chainEvent, nil); err != nil {
+ created, err := em.maybePersistBlockchainEvent(ctx, chainEvent, nil)
+ if err != nil {
return err
}
- em.emitBlockchainEventMetric(ev)
+ if created {
+ em.emitBlockchainEventMetric(ev)
+ }
}
if _, err := em.txHelper.PersistTransaction(ctx, pool.TX.ID, pool.TX.Type, blockchainID); err != nil {
return err
diff --git a/internal/events/tokens_approved.go b/internal/events/tokens_approved.go
index f93b719681..272641f546 100644
--- a/internal/events/tokens_approved.go
+++ b/internal/events/tokens_approved.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2023 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -97,10 +97,13 @@ func (em *eventManager) persistTokenApproval(ctx context.Context, approval *toke
Type: approval.TX.Type,
BlockchainID: approval.Event.BlockchainTXID,
})
- if err := em.maybePersistBlockchainEvent(ctx, chainEvent, nil); err != nil {
+ created, err := em.maybePersistBlockchainEvent(ctx, chainEvent, nil)
+ if err != nil {
return false, err
}
- em.emitBlockchainEventMetric(approval.Event)
+ if created {
+ em.emitBlockchainEventMetric(approval.Event)
+ }
approval.BlockchainEvent = chainEvent.ID
fb := database.TokenApprovalQueryFactory.NewFilter(ctx)
diff --git a/internal/events/tokens_transferred.go b/internal/events/tokens_transferred.go
index 74cccc89de..c4048611f3 100644
--- a/internal/events/tokens_transferred.go
+++ b/internal/events/tokens_transferred.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2023 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -89,10 +89,13 @@ func (em *eventManager) persistTokenTransfer(ctx context.Context, transfer *toke
Type: transfer.TX.Type,
BlockchainID: transfer.Event.BlockchainTXID,
})
- if err := em.maybePersistBlockchainEvent(ctx, chainEvent, nil); err != nil {
+ created, err := em.maybePersistBlockchainEvent(ctx, chainEvent, nil)
+ if err != nil {
return false, err
}
- em.emitBlockchainEventMetric(transfer.Event)
+ if created {
+ em.emitBlockchainEventMetric(transfer.Event)
+ }
transfer.BlockchainEvent = chainEvent.ID
// This is a no-op if we've already persisted this token transfer
diff --git a/internal/identity/iifactory/factory_test.go b/internal/identity/iifactory/factory_test.go
new file mode 100644
index 0000000000..3110dbb65b
--- /dev/null
+++ b/internal/identity/iifactory/factory_test.go
@@ -0,0 +1,46 @@
+// Copyright ยฉ 2023 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 iifactory
+
+import (
+ "context"
+ "testing"
+
+ "github.com/hyperledger/firefly-common/pkg/config"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetPluginUnknown(t *testing.T) {
+ ctx := context.Background()
+ _, err := GetPlugin(ctx, "foo")
+ assert.Error(t, err)
+ assert.Regexp(t, "FF10212", err)
+}
+
+func TestGetPlugin(t *testing.T) {
+ ctx := context.Background()
+ plugin, err := GetPlugin(ctx, "onchain")
+ assert.NoError(t, err)
+ assert.NotNil(t, plugin)
+}
+
+var root = config.RootSection("di")
+
+func TestInitConfig(t *testing.T) {
+ conf := root.SubArray("plugins")
+ InitConfig(conf)
+}
diff --git a/internal/multiparty/manager.go b/internal/multiparty/manager.go
index b0c7e1a615..5ac4ac35b0 100644
--- a/internal/multiparty/manager.go
+++ b/internal/multiparty/manager.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2023 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -155,7 +155,23 @@ func (mm *multipartyManager) configureContractCommon(ctx context.Context, migrat
}
}
- subID, err := mm.blockchain.AddFireflySubscription(ctx, mm.namespace, current)
+ // For the case that we're establishing a listener from "latest" we obtain the protocol ID
+ // of the latest event confirmed from the blockchain for a given subscription.
+ // This protocolID should be parsed and used by the blockchain plugin if SubOptsFirstEventNewest
+ // is passed through, and the listener does not exist.
+ fb := database.BlockchainEventQueryFactory.NewFilter(ctx).Sort("-protocolid").Limit(1)
+ latestEvents, _, err := mm.database.GetBlockchainEvents(ctx, mm.namespace.Name, fb.Eq(
+ "listener", nil,
+ ))
+ if err != nil {
+ return err
+ }
+ lastProtocolID := ""
+ if len(latestEvents) > 0 {
+ lastProtocolID = latestEvents[0].ProtocolID
+ }
+
+ subID, err := mm.blockchain.AddFireflySubscription(ctx, mm.namespace, current, lastProtocolID)
if err == nil {
active.Location = current.Location
active.FirstEvent = current.FirstEvent
diff --git a/internal/multiparty/manager_test.go b/internal/multiparty/manager_test.go
index 8dfc1218ce..dc9392c8c5 100644
--- a/internal/multiparty/manager_test.go
+++ b/internal/multiparty/manager_test.go
@@ -19,8 +19,10 @@ package multiparty
import (
"context"
"fmt"
+ "strings"
"testing"
+ "github.com/hyperledger/firefly-common/pkg/ffapi"
"github.com/hyperledger/firefly-common/pkg/fftypes"
"github.com/hyperledger/firefly/internal/txcommon"
"github.com/hyperledger/firefly/mocks/blockchainmocks"
@@ -115,8 +117,15 @@ func TestConfigureContract(t *testing.T) {
defer mp.cleanup(t)
mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(1, nil)
- mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
+ mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, "001/002/003").Return("test", nil)
mp.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
+ mp.mdi.On("GetBlockchainEvents", mock.Anything, "ns1", mock.MatchedBy(func(f ffapi.Filter) bool {
+ fi, err := f.Finalize()
+ assert.NoError(t, err)
+ return fi.Limit == 1 && strings.Contains(fi.String(), "listener")
+ })).Return([]*core.BlockchainEvent{
+ {Namespace: "ns1", ID: fftypes.NewUUID(), ProtocolID: "001/002/003"},
+ }, nil, nil).Once()
mp.multipartyManager.config.Contracts = []blockchain.MultipartyContract{{
FirstEvent: "0",
@@ -141,6 +150,7 @@ func TestConfigureContractLocationChanged(t *testing.T) {
mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(1, nil)
mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
mp.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
+ mp.mdi.On("GetBlockchainEvents", mock.Anything, "ns1", mock.Anything).Return(nil, nil, nil).Once()
mp.multipartyManager.namespace.Contracts = &core.MultipartyContracts{
Active: &core.MultipartyContract{
@@ -168,6 +178,7 @@ func TestConfigureContractDeprecatedConfig(t *testing.T) {
mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(1, nil)
mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
mp.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
+ mp.mdi.On("GetBlockchainEvents", mock.Anything, "ns1", mock.Anything).Return(nil, nil, nil).Once()
err := mp.ConfigureContract(context.Background())
@@ -263,6 +274,7 @@ func TestSubmitNetworkAction(t *testing.T) {
mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(1, nil)
mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
mp.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
+ mp.mdi.On("GetBlockchainEvents", mock.Anything, "ns1", mock.Anything).Return(nil, nil, nil).Once()
mp.mth.On("SubmitNewTransaction", mock.Anything, core.TransactionTypeNetworkAction, core.IdempotencyKey("")).Return(txid, nil)
mp.mbi.On("Name").Return("ut")
mp.mom.On("AddOrReuseOperation", context.Background(), mock.MatchedBy(func(op *core.Operation) bool {
@@ -306,6 +318,7 @@ func TestSubmitNetworkActionTXFail(t *testing.T) {
mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(1, nil)
mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
mp.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
+ mp.mdi.On("GetBlockchainEvents", mock.Anything, "ns1", mock.Anything).Return(nil, nil, nil).Once()
mp.mth.On("SubmitNewTransaction", mock.Anything, core.TransactionTypeNetworkAction, core.IdempotencyKey("")).Return(nil, fmt.Errorf("pop"))
err := mp.ConfigureContract(context.Background())
@@ -317,6 +330,32 @@ func TestSubmitNetworkActionTXFail(t *testing.T) {
mp.mth.AssertExpectations(t)
}
+func TestConfigureContractLookupBlockchainEventFail(t *testing.T) {
+ location := fftypes.JSONAnyPtr(fftypes.JSONObject{
+ "address": "0x123",
+ }.String())
+
+ mp := newTestMultipartyManager()
+ defer mp.cleanup(t)
+
+ mp.multipartyManager.namespace.Contracts = &core.MultipartyContracts{
+ Active: &core.MultipartyContract{Index: 0},
+ }
+ mp.multipartyManager.config.Contracts = []blockchain.MultipartyContract{{
+ FirstEvent: "0",
+ Location: location,
+ }}
+
+ mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(1, nil)
+ mp.mdi.On("GetBlockchainEvents", mock.Anything, "ns1", mock.Anything).Return(nil, nil, fmt.Errorf("pop")).Once()
+
+ err := mp.ConfigureContract(context.Background())
+ assert.EqualError(t, err, "pop")
+
+ mp.mbi.AssertExpectations(t)
+ mp.mth.AssertExpectations(t)
+}
+
func TestSubmitNetworkActionOpFail(t *testing.T) {
location := fftypes.JSONAnyPtr(fftypes.JSONObject{
"address": "0x123",
@@ -337,6 +376,7 @@ func TestSubmitNetworkActionOpFail(t *testing.T) {
mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(1, nil)
mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
mp.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
+ mp.mdi.On("GetBlockchainEvents", mock.Anything, "ns1", mock.Anything).Return(nil, nil, nil).Once()
mp.mth.On("SubmitNewTransaction", mock.Anything, core.TransactionTypeNetworkAction, core.IdempotencyKey("")).Return(txid, nil)
mp.mbi.On("Name").Return("ut")
mp.mom.On("AddOrReuseOperation", context.Background(), mock.Anything).Return(fmt.Errorf("pop"))
@@ -362,6 +402,7 @@ func TestSubmitNetworkActionBadType(t *testing.T) {
mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(1, nil)
mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
mp.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
+ mp.mdi.On("GetBlockchainEvents", mock.Anything, "ns1", mock.Anything).Return(nil, nil, nil).Once()
mp.multipartyManager.namespace.Contracts = &core.MultipartyContracts{
Active: &core.MultipartyContract{Index: 0},
@@ -623,6 +664,7 @@ func TestGetNetworkVersion(t *testing.T) {
mp.mbi.On("GetNetworkVersion", mock.Anything, mock.Anything).Return(1, nil)
mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
mp.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
+ mp.mdi.On("GetBlockchainEvents", mock.Anything, "ns1", mock.Anything).Return(nil, nil, nil).Once()
mp.multipartyManager.namespace.Contracts = &core.MultipartyContracts{
Active: &core.MultipartyContract{Index: 0},
@@ -651,6 +693,7 @@ func TestConfgureAndTerminateContract(t *testing.T) {
mp.mbi.On("AddFireflySubscription", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("test", nil)
mp.mdi.On("UpsertNamespace", mock.Anything, mock.AnythingOfType("*core.Namespace"), true).Return(nil)
mp.mbi.On("RemoveFireflySubscription", mock.Anything, mock.Anything).Return(nil)
+ mp.mdi.On("GetBlockchainEvents", mock.Anything, "ns1", mock.Anything).Return(nil, nil, nil)
mp.multipartyManager.namespace.Contracts = &core.MultipartyContracts{
Active: &core.MultipartyContract{Index: 0},
diff --git a/internal/namespace/manager_test.go b/internal/namespace/manager_test.go
index 9dce67d2b6..9d21f65d15 100644
--- a/internal/namespace/manager_test.go
+++ b/internal/namespace/manager_test.go
@@ -1448,7 +1448,9 @@ namespaces:
assert.NoError(t, err)
assert.NotNil(t, tlsConfigs["myconfig"])
assert.True(t, tlsConfigs["myconfig"].RootCAs.Equal(expectedTLSConfig.RootCAs))
- assert.Equal(t, tlsConfigs["myconfig"].Certificates, expectedTLSConfig.Certificates)
+ certificate, err := tlsConfigs["myconfig"].GetCertificate(nil)
+ assert.NoError(t, err)
+ assert.Equal(t, *certificate, cert)
}
func TestLoadTLSConfigsDuplicateConfigs(t *testing.T) {
diff --git a/internal/operations/manager.go b/internal/operations/manager.go
index 65c3d0f8ef..27e67c5dc1 100644
--- a/internal/operations/manager.go
+++ b/internal/operations/manager.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2023 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -234,10 +234,12 @@ func (om *operationsManager) RetryOperation(ctx context.Context, opID *fftypes.U
var po *core.PreparedOperation
var idempotencyKey core.IdempotencyKey
err = om.database.RunAsGroup(ctx, func(ctx context.Context) error {
- op, err = om.findLatestRetry(ctx, opID)
+ parent, err := om.findLatestRetry(ctx, opID)
if err != nil {
return err
}
+ // Deep copy the operation so the parent ID will not get overwritten
+ op = parent.DeepCopy()
tx, err := om.updater.txHelper.GetTransactionByIDCached(ctx, op.Transaction)
if err != nil {
diff --git a/internal/operations/manager_test.go b/internal/operations/manager_test.go
index 4e5448c01e..626dc7292f 100644
--- a/internal/operations/manager_test.go
+++ b/internal/operations/manager_test.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2022 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -346,9 +346,12 @@ func TestRetryOperationSuccess(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, 1, len(info.SetOperations))
assert.Equal(t, "retry", info.SetOperations[0].Field)
- val, err := info.SetOperations[0].Value.Value()
+ retryVal, err := info.SetOperations[0].Value.Value()
assert.NoError(t, err)
- assert.Equal(t, op.ID.String(), val)
+ // The retry value of the parent operation should be the new operation ID
+ assert.Equal(t, op.Retry.String(), retryVal.(string))
+ // The parent ID should not change
+ assert.Equal(t, op.ID.String(), opID.String())
return true
})).Return(true, nil)
mdi.On("GetTransactionByID", mock.Anything, "ns1", txID).Return(&core.Transaction{
diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go
index 0b6746d324..5909aeaa87 100644
--- a/internal/orchestrator/orchestrator.go
+++ b/internal/orchestrator/orchestrator.go
@@ -191,33 +191,34 @@ type Config struct {
}
type orchestrator struct {
- ctx context.Context
- cancelCtx context.CancelFunc
- started bool
- startedLock sync.Mutex
- namespace *core.Namespace
- config Config
- plugins *Plugins
- multiparty multiparty.Manager // only for multiparty
- batch batch.Manager // only for multiparty
- broadcast broadcast.Manager // only for multiparty
- messaging privatemessaging.Manager // only for multiparty
- sharedDownload shareddownload.Manager // only for multiparty
- identity identity.Manager
- events events.EventManager
- networkmap networkmap.Manager
- defhandler definitions.Handler
- defsender definitions.Sender
- data data.Manager
- syncasync syncasync.Bridge
- assets assets.Manager
- bc boundCallbacks
- contracts contracts.Manager
- metrics metrics.Manager
- cacheManager cache.Manager
- operations operations.Manager
- txHelper txcommon.Helper
- txWriter txwriter.Writer
+ ctx context.Context
+ cancelCtx context.CancelFunc
+ started bool
+ startedBlockchainPlugin bool
+ startedLock sync.Mutex
+ namespace *core.Namespace
+ config Config
+ plugins *Plugins
+ multiparty multiparty.Manager // only for multiparty
+ batch batch.Manager // only for multiparty
+ broadcast broadcast.Manager // only for multiparty
+ messaging privatemessaging.Manager // only for multiparty
+ sharedDownload shareddownload.Manager // only for multiparty
+ identity identity.Manager
+ events events.EventManager
+ networkmap networkmap.Manager
+ defhandler definitions.Handler
+ defsender definitions.Sender
+ data data.Manager
+ syncasync syncasync.Bridge
+ assets assets.Manager
+ bc boundCallbacks
+ contracts contracts.Manager
+ metrics metrics.Manager
+ cacheManager cache.Manager
+ operations operations.Manager
+ txHelper txcommon.Helper
+ txWriter txwriter.Writer
}
func NewOrchestrator(ns *core.Namespace, config Config, plugins *Plugins, metrics metrics.Manager, cacheManager cache.Manager) Orchestrator {
@@ -568,11 +569,16 @@ func (or *orchestrator) initManagers(ctx context.Context) (err error) {
}
func (or *orchestrator) initComponents(ctx context.Context) (err error) {
- if or.blockchain() != nil {
+ // The blockchain plugin doesn't return a manager or struct that we can check for
+ // nil like all the other mangagers to see if it has been initialised before!
+ // So we have a boolean to check so that when the retry wrapper initialises these components
+ // again we do have multiple ones running
+ if or.blockchain() != nil && !or.startedBlockchainPlugin {
err = or.blockchain().StartNamespace(ctx, or.namespace.Name)
if err != nil {
return err
}
+ or.startedBlockchainPlugin = true
}
if or.data == nil {
diff --git a/internal/reference/reference.go b/internal/reference/reference.go
index 28fc1346df..699c6aab1b 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{
diff --git a/internal/sharedstorage/ssfactory/factory_test.go b/internal/sharedstorage/ssfactory/factory_test.go
new file mode 100644
index 0000000000..5013d4cf45
--- /dev/null
+++ b/internal/sharedstorage/ssfactory/factory_test.go
@@ -0,0 +1,46 @@
+// Copyright ยฉ 2023 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 ssfactory
+
+import (
+ "context"
+ "testing"
+
+ "github.com/hyperledger/firefly-common/pkg/config"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetPluginUnknown(t *testing.T) {
+ ctx := context.Background()
+ _, err := GetPlugin(ctx, "foo")
+ assert.Error(t, err)
+ assert.Regexp(t, "FF10134", err)
+}
+
+func TestGetPlugin(t *testing.T) {
+ ctx := context.Background()
+ plugin, err := GetPlugin(ctx, "ipfs")
+ assert.NoError(t, err)
+ assert.NotNil(t, plugin)
+}
+
+var root = config.RootSection("di")
+
+func TestInitConfig(t *testing.T) {
+ conf := root.SubArray("plugins")
+ InitConfig(conf)
+}
diff --git a/internal/tokens/tifactory/factory_test.go b/internal/tokens/tifactory/factory_test.go
new file mode 100644
index 0000000000..f1d1229321
--- /dev/null
+++ b/internal/tokens/tifactory/factory_test.go
@@ -0,0 +1,46 @@
+// Copyright ยฉ 2023 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 tifactory
+
+import (
+ "context"
+ "testing"
+
+ "github.com/hyperledger/firefly-common/pkg/config"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetPluginUnknown(t *testing.T) {
+ ctx := context.Background()
+ _, err := GetPlugin(ctx, "foo")
+ assert.Error(t, err)
+ assert.Regexp(t, "FF10272", err)
+}
+
+func TestGetPlugin(t *testing.T) {
+ ctx := context.Background()
+ plugin, err := GetPlugin(ctx, "fftokens")
+ assert.NoError(t, err)
+ assert.NotNil(t, plugin)
+}
+
+var root = config.RootSection("tokens")
+
+func TestInitConfig(t *testing.T) {
+ conf := root.SubArray("plugins")
+ InitConfig(conf)
+}
diff --git a/internal/txcommon/contract_inputs.go b/internal/txcommon/contract_inputs.go
index 942ca66258..9b900f023c 100644
--- a/internal/txcommon/contract_inputs.go
+++ b/internal/txcommon/contract_inputs.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2023 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -17,6 +17,7 @@
package txcommon
import (
+ "bytes"
"context"
"encoding/json"
@@ -39,7 +40,9 @@ type BlockchainInvokeData struct {
func RetrieveBlockchainInvokeInputs(ctx context.Context, op *core.Operation) (*core.ContractCallRequest, error) {
var req core.ContractCallRequest
s := op.Input.String()
- if err := json.Unmarshal([]byte(s), &req); err != nil {
+ d := json.NewDecoder(bytes.NewReader([]byte(s)))
+ d.UseNumber()
+ if err := d.Decode(&req); err != nil {
return nil, i18n.WrapError(ctx, err, i18n.MsgJSONObjectParseFailed, s)
}
return &req, nil
diff --git a/manifest.json b/manifest.json
index 9a813cc2d6..a6285d4da1 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,50 +1,50 @@
{
"ethconnect": {
"image": "ghcr.io/hyperledger/firefly-ethconnect",
- "tag": "v3.3.1",
- "sha": "4b78a0f88a06d2de1e6868b0cbeef44b9be622b7ef46b5a8d6ca381a36c1c6a5"
+ "tag": "v3.3.2",
+ "sha": "bdbfa05a0bc1610813ba0ef65b2c8f9f7df4a5ead5643a580e3ba5927747c2cf"
},
"evmconnect": {
"image": "ghcr.io/hyperledger/firefly-evmconnect",
- "tag": "v1.3.8",
- "sha": "b4afb5bd4032a87f7f4145d49480f204a873302f21b476f85c39e93a776e3a1c"
+ "tag": "v1.3.18",
+ "sha": "11a782c778227d198c0bff2fc94ad7e52ceea5a9994728ba05e781541680c32d"
},
"fabconnect": {
"image": "ghcr.io/hyperledger/firefly-fabconnect",
- "tag": "v0.9.19",
- "sha": "9479213b0114619c7690fa924ebbe7f837cf851c4ea9bd901798c7783edfc7e7"
+ "tag": "v0.9.21",
+ "sha": "394262c5888fb6647a7b8b1f3f8d652e1db941b67e3c68bccfbedfa53c4d874d"
},
"tezosconnect": {
"image": "ghcr.io/hyperledger/firefly-tezosconnect",
- "tag": "v0.2.2",
- "sha": "238cef711f2ee8f841ad767be12ad33710136d6f5ab160de74715c0225188b94"
+ "tag": "v0.2.7",
+ "sha": "cee6ecda8208e24989a6db2c1a2dd1b0defb89363e7728571b8858e794a295be"
},
"dataexchange-https": {
"image": "ghcr.io/hyperledger/firefly-dataexchange-https",
- "tag": "v1.2.0",
- "sha": "4ac765f7a07b9d17ab5648b3c789875791db364659c975a23626ec5921f11ce4"
+ "tag": "v1.3.0",
+ "sha": "1e01b0d5fc3cfd95de150d47c09ba419b8ec98345d105a6d67d7bc7101aeea89"
},
"tokens-erc1155": {
"image": "ghcr.io/hyperledger/firefly-tokens-erc1155",
- "tag": "v1.3.2",
- "sha": "7bb27808ab2b4582775ca1a18bdc40f7db3a2de56c69c7778224a36105da111a"
+ "tag": "v1.3.3",
+ "sha": "5efbe76ce2c484d86e9e3cc2cfe3cee85d91ee8712b31dfc592a2de771749e78"
},
"tokens-erc20-erc721": {
"image": "ghcr.io/hyperledger/firefly-tokens-erc20-erc721",
- "tag": "v1.3.2",
- "sha": "c75699b05cb41c8950dcb1b1eed49f7beee910433ff78830df6db61dfc325812"
+ "tag": "v1.3.3",
+ "sha": "5cfa10432c1cd885f3cbe5934799d692ba7186013e03a6b92bd6aa53ddf4f414"
},
"signer": {
"image": "ghcr.io/hyperledger/firefly-signer",
- "tag": "v1.1.13",
- "sha": "9f4c29ea05eb111d958d8210601cdf876b4f70fbe443c080dbc7c0c16c7921e9"
+ "tag": "v1.1.17",
+ "sha": "4391674f66dadb42ef29f3ea0ac62b130bad041675ff25ad3f8c7c0c8eda3854"
},
"build": {
"firefly-builder": {
- "image": "golang:1.21-alpine3.19"
+ "image": "golang:1.22-alpine3.19"
},
"fabric-builder": {
- "image": "golang:1.21",
+ "image": "golang:1.22",
"platform": "linux/x86_64"
},
"solidity-builder": {
@@ -55,10 +55,10 @@
}
},
"ui": {
- "tag": "v1.3.0",
- "release": "v1.3.0"
+ "tag": "v1.3.1",
+ "release": "v1.3.1"
},
"cli": {
- "tag": "v1.3.0"
+ "tag": "v1.3.2"
}
}
diff --git a/mocks/apiservermocks/ffi_swagger_gen.go b/mocks/apiservermocks/ffi_swagger_gen.go
index 0751f3f27f..08a9311f59 100644
--- a/mocks/apiservermocks/ffi_swagger_gen.go
+++ b/mocks/apiservermocks/ffi_swagger_gen.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package apiservermocks
diff --git a/mocks/apiservermocks/server.go b/mocks/apiservermocks/server.go
index 97593c40dc..71247dfb95 100644
--- a/mocks/apiservermocks/server.go
+++ b/mocks/apiservermocks/server.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package apiservermocks
diff --git a/mocks/assetmocks/manager.go b/mocks/assetmocks/manager.go
index 8b95588cc4..3f2788b224 100644
--- a/mocks/assetmocks/manager.go
+++ b/mocks/assetmocks/manager.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package assetmocks
diff --git a/mocks/batchmocks/manager.go b/mocks/batchmocks/manager.go
index 9054cb6613..a8ceb379e8 100644
--- a/mocks/batchmocks/manager.go
+++ b/mocks/batchmocks/manager.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package batchmocks
diff --git a/mocks/blockchaincommonmocks/firefly_subscriptions.go b/mocks/blockchaincommonmocks/firefly_subscriptions.go
index a41334e630..835d36edfc 100644
--- a/mocks/blockchaincommonmocks/firefly_subscriptions.go
+++ b/mocks/blockchaincommonmocks/firefly_subscriptions.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package blockchaincommonmocks
diff --git a/mocks/blockchainmocks/callbacks.go b/mocks/blockchainmocks/callbacks.go
index 0ee5cf3877..e4cd093a95 100644
--- a/mocks/blockchainmocks/callbacks.go
+++ b/mocks/blockchainmocks/callbacks.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package blockchainmocks
diff --git a/mocks/blockchainmocks/plugin.go b/mocks/blockchainmocks/plugin.go
index 34a5408749..1a8cc47286 100644
--- a/mocks/blockchainmocks/plugin.go
+++ b/mocks/blockchainmocks/plugin.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package blockchainmocks
@@ -24,17 +24,17 @@ type Plugin struct {
mock.Mock
}
-// AddContractListener provides a mock function with given fields: ctx, subscription
-func (_m *Plugin) AddContractListener(ctx context.Context, subscription *core.ContractListener) error {
- ret := _m.Called(ctx, subscription)
+// AddContractListener provides a mock function with given fields: ctx, subscription, lastProtocolID
+func (_m *Plugin) AddContractListener(ctx context.Context, subscription *core.ContractListener, lastProtocolID string) error {
+ ret := _m.Called(ctx, subscription, lastProtocolID)
if len(ret) == 0 {
panic("no return value specified for AddContractListener")
}
var r0 error
- if rf, ok := ret.Get(0).(func(context.Context, *core.ContractListener) error); ok {
- r0 = rf(ctx, subscription)
+ if rf, ok := ret.Get(0).(func(context.Context, *core.ContractListener, string) error); ok {
+ r0 = rf(ctx, subscription, lastProtocolID)
} else {
r0 = ret.Error(0)
}
@@ -42,9 +42,9 @@ func (_m *Plugin) AddContractListener(ctx context.Context, subscription *core.Co
return r0
}
-// AddFireflySubscription provides a mock function with given fields: ctx, namespace, contract
-func (_m *Plugin) AddFireflySubscription(ctx context.Context, namespace *core.Namespace, contract *blockchain.MultipartyContract) (string, error) {
- ret := _m.Called(ctx, namespace, contract)
+// AddFireflySubscription provides a mock function with given fields: ctx, namespace, contract, lastProtocolID
+func (_m *Plugin) AddFireflySubscription(ctx context.Context, namespace *core.Namespace, contract *blockchain.MultipartyContract, lastProtocolID string) (string, error) {
+ ret := _m.Called(ctx, namespace, contract, lastProtocolID)
if len(ret) == 0 {
panic("no return value specified for AddFireflySubscription")
@@ -52,17 +52,17 @@ func (_m *Plugin) AddFireflySubscription(ctx context.Context, namespace *core.Na
var r0 string
var r1 error
- if rf, ok := ret.Get(0).(func(context.Context, *core.Namespace, *blockchain.MultipartyContract) (string, error)); ok {
- return rf(ctx, namespace, contract)
+ if rf, ok := ret.Get(0).(func(context.Context, *core.Namespace, *blockchain.MultipartyContract, string) (string, error)); ok {
+ return rf(ctx, namespace, contract, lastProtocolID)
}
- if rf, ok := ret.Get(0).(func(context.Context, *core.Namespace, *blockchain.MultipartyContract) string); ok {
- r0 = rf(ctx, namespace, contract)
+ if rf, ok := ret.Get(0).(func(context.Context, *core.Namespace, *blockchain.MultipartyContract, string) string); ok {
+ r0 = rf(ctx, namespace, contract, lastProtocolID)
} else {
r0 = ret.Get(0).(string)
}
- if rf, ok := ret.Get(1).(func(context.Context, *core.Namespace, *blockchain.MultipartyContract) error); ok {
- r1 = rf(ctx, namespace, contract)
+ if rf, ok := ret.Get(1).(func(context.Context, *core.Namespace, *blockchain.MultipartyContract, string) error); ok {
+ r1 = rf(ctx, namespace, contract, lastProtocolID)
} else {
r1 = ret.Error(1)
}
@@ -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/broadcastmocks/manager.go b/mocks/broadcastmocks/manager.go
index 4d71587477..957f64e75d 100644
--- a/mocks/broadcastmocks/manager.go
+++ b/mocks/broadcastmocks/manager.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package broadcastmocks
diff --git a/mocks/cachemocks/manager.go b/mocks/cachemocks/manager.go
index dee91e14f7..ed923f1558 100644
--- a/mocks/cachemocks/manager.go
+++ b/mocks/cachemocks/manager.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package cachemocks
diff --git a/mocks/contractmocks/manager.go b/mocks/contractmocks/manager.go
index 3d53ea10e7..06c26e9343 100644
--- a/mocks/contractmocks/manager.go
+++ b/mocks/contractmocks/manager.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package contractmocks
@@ -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/coremocks/operation_callbacks.go b/mocks/coremocks/operation_callbacks.go
index f404e05065..0e6acabc5d 100644
--- a/mocks/coremocks/operation_callbacks.go
+++ b/mocks/coremocks/operation_callbacks.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package coremocks
diff --git a/mocks/databasemocks/callbacks.go b/mocks/databasemocks/callbacks.go
index 5b936ba7ca..4bb148b577 100644
--- a/mocks/databasemocks/callbacks.go
+++ b/mocks/databasemocks/callbacks.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package databasemocks
diff --git a/mocks/databasemocks/plugin.go b/mocks/databasemocks/plugin.go
index 6cb9f1b0fa..75f9abae40 100644
--- a/mocks/databasemocks/plugin.go
+++ b/mocks/databasemocks/plugin.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package databasemocks
@@ -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/mocks/dataexchangemocks/callbacks.go b/mocks/dataexchangemocks/callbacks.go
index bdfa4370c7..e39b409a0d 100644
--- a/mocks/dataexchangemocks/callbacks.go
+++ b/mocks/dataexchangemocks/callbacks.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package dataexchangemocks
diff --git a/mocks/dataexchangemocks/dx_event.go b/mocks/dataexchangemocks/dx_event.go
index c0cdcd3f78..2ee5e469f6 100644
--- a/mocks/dataexchangemocks/dx_event.go
+++ b/mocks/dataexchangemocks/dx_event.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package dataexchangemocks
diff --git a/mocks/dataexchangemocks/plugin.go b/mocks/dataexchangemocks/plugin.go
index c1671088ca..ecd545651e 100644
--- a/mocks/dataexchangemocks/plugin.go
+++ b/mocks/dataexchangemocks/plugin.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package dataexchangemocks
diff --git a/mocks/datamocks/manager.go b/mocks/datamocks/manager.go
index e4395a3b7d..ef595ebd69 100644
--- a/mocks/datamocks/manager.go
+++ b/mocks/datamocks/manager.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package datamocks
diff --git a/mocks/definitionsmocks/handler.go b/mocks/definitionsmocks/handler.go
index 3c226e1863..c4d2ba66f4 100644
--- a/mocks/definitionsmocks/handler.go
+++ b/mocks/definitionsmocks/handler.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package definitionsmocks
diff --git a/mocks/definitionsmocks/sender.go b/mocks/definitionsmocks/sender.go
index cbf22ec0c3..23e487e476 100644
--- a/mocks/definitionsmocks/sender.go
+++ b/mocks/definitionsmocks/sender.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package definitionsmocks
diff --git a/mocks/eventmocks/event_manager.go b/mocks/eventmocks/event_manager.go
index 13afc96fbe..a5fad4a769 100644
--- a/mocks/eventmocks/event_manager.go
+++ b/mocks/eventmocks/event_manager.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package eventmocks
diff --git a/mocks/eventsmocks/callbacks.go b/mocks/eventsmocks/callbacks.go
index 676e061018..f4ec135b9c 100644
--- a/mocks/eventsmocks/callbacks.go
+++ b/mocks/eventsmocks/callbacks.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package eventsmocks
diff --git a/mocks/eventsmocks/plugin.go b/mocks/eventsmocks/plugin.go
index 453592403a..2efc0a2425 100644
--- a/mocks/eventsmocks/plugin.go
+++ b/mocks/eventsmocks/plugin.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package eventsmocks
diff --git a/mocks/identitymanagermocks/manager.go b/mocks/identitymanagermocks/manager.go
index 2eb6b8e614..51b648ea92 100644
--- a/mocks/identitymanagermocks/manager.go
+++ b/mocks/identitymanagermocks/manager.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package identitymanagermocks
diff --git a/mocks/identitymocks/callbacks.go b/mocks/identitymocks/callbacks.go
index 2f90520c7d..f3f9946f8f 100644
--- a/mocks/identitymocks/callbacks.go
+++ b/mocks/identitymocks/callbacks.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package identitymocks
diff --git a/mocks/identitymocks/plugin.go b/mocks/identitymocks/plugin.go
index 62ec273bad..bdb0e52b8d 100644
--- a/mocks/identitymocks/plugin.go
+++ b/mocks/identitymocks/plugin.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package identitymocks
diff --git a/mocks/metricsmocks/manager.go b/mocks/metricsmocks/manager.go
index f1c968f470..33a5e7bd78 100644
--- a/mocks/metricsmocks/manager.go
+++ b/mocks/metricsmocks/manager.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package metricsmocks
diff --git a/mocks/multipartymocks/manager.go b/mocks/multipartymocks/manager.go
index 74885e6f62..65cf1ea118 100644
--- a/mocks/multipartymocks/manager.go
+++ b/mocks/multipartymocks/manager.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package multipartymocks
diff --git a/mocks/namespacemocks/manager.go b/mocks/namespacemocks/manager.go
index 5819566927..fa6e9c5b39 100644
--- a/mocks/namespacemocks/manager.go
+++ b/mocks/namespacemocks/manager.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package namespacemocks
diff --git a/mocks/networkmapmocks/manager.go b/mocks/networkmapmocks/manager.go
index 015a8895a7..2751287852 100644
--- a/mocks/networkmapmocks/manager.go
+++ b/mocks/networkmapmocks/manager.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package networkmapmocks
diff --git a/mocks/operationmocks/manager.go b/mocks/operationmocks/manager.go
index 4c5e99c4de..98a2a22fed 100644
--- a/mocks/operationmocks/manager.go
+++ b/mocks/operationmocks/manager.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package operationmocks
diff --git a/mocks/orchestratormocks/orchestrator.go b/mocks/orchestratormocks/orchestrator.go
index 7fd34c8ade..6f94f78900 100644
--- a/mocks/orchestratormocks/orchestrator.go
+++ b/mocks/orchestratormocks/orchestrator.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package orchestratormocks
diff --git a/mocks/privatemessagingmocks/manager.go b/mocks/privatemessagingmocks/manager.go
index 71859e10d4..8988ca5859 100644
--- a/mocks/privatemessagingmocks/manager.go
+++ b/mocks/privatemessagingmocks/manager.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package privatemessagingmocks
diff --git a/mocks/shareddownloadmocks/callbacks.go b/mocks/shareddownloadmocks/callbacks.go
index 5a1d256329..26bcd708f3 100644
--- a/mocks/shareddownloadmocks/callbacks.go
+++ b/mocks/shareddownloadmocks/callbacks.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package shareddownloadmocks
diff --git a/mocks/shareddownloadmocks/manager.go b/mocks/shareddownloadmocks/manager.go
index e24548ac39..33226238f6 100644
--- a/mocks/shareddownloadmocks/manager.go
+++ b/mocks/shareddownloadmocks/manager.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package shareddownloadmocks
diff --git a/mocks/sharedstoragemocks/callbacks.go b/mocks/sharedstoragemocks/callbacks.go
index 3a89381555..6296a2b7c8 100644
--- a/mocks/sharedstoragemocks/callbacks.go
+++ b/mocks/sharedstoragemocks/callbacks.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package sharedstoragemocks
diff --git a/mocks/sharedstoragemocks/plugin.go b/mocks/sharedstoragemocks/plugin.go
index e1d8e4cb7c..a48e274235 100644
--- a/mocks/sharedstoragemocks/plugin.go
+++ b/mocks/sharedstoragemocks/plugin.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package sharedstoragemocks
diff --git a/mocks/spieventsmocks/manager.go b/mocks/spieventsmocks/manager.go
index 405c4a27ed..a61707c9aa 100644
--- a/mocks/spieventsmocks/manager.go
+++ b/mocks/spieventsmocks/manager.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package spieventsmocks
diff --git a/mocks/syncasyncmocks/bridge.go b/mocks/syncasyncmocks/bridge.go
index 2e67785cd7..bce0a36631 100644
--- a/mocks/syncasyncmocks/bridge.go
+++ b/mocks/syncasyncmocks/bridge.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package syncasyncmocks
diff --git a/mocks/syncasyncmocks/sender.go b/mocks/syncasyncmocks/sender.go
index 879ccba37d..fd4bafa34f 100644
--- a/mocks/syncasyncmocks/sender.go
+++ b/mocks/syncasyncmocks/sender.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package syncasyncmocks
diff --git a/mocks/systemeventmocks/event_interface.go b/mocks/systemeventmocks/event_interface.go
index dd04220c49..dd0d4398ef 100644
--- a/mocks/systemeventmocks/event_interface.go
+++ b/mocks/systemeventmocks/event_interface.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package systemeventmocks
diff --git a/mocks/tokenmocks/callbacks.go b/mocks/tokenmocks/callbacks.go
index 5929ef0ede..36a362a344 100644
--- a/mocks/tokenmocks/callbacks.go
+++ b/mocks/tokenmocks/callbacks.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package tokenmocks
diff --git a/mocks/tokenmocks/plugin.go b/mocks/tokenmocks/plugin.go
index 79e6415373..b3292e799c 100644
--- a/mocks/tokenmocks/plugin.go
+++ b/mocks/tokenmocks/plugin.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package tokenmocks
diff --git a/mocks/txcommonmocks/helper.go b/mocks/txcommonmocks/helper.go
index 44af5281b3..4fbbc91931 100644
--- a/mocks/txcommonmocks/helper.go
+++ b/mocks/txcommonmocks/helper.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package txcommonmocks
diff --git a/mocks/txwritermocks/writer.go b/mocks/txwritermocks/writer.go
index 186bf8e4f7..5358086e05 100644
--- a/mocks/txwritermocks/writer.go
+++ b/mocks/txwritermocks/writer.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package txwritermocks
diff --git a/mocks/websocketsmocks/web_sockets_namespaced.go b/mocks/websocketsmocks/web_sockets_namespaced.go
index ee93753cc9..2a036a3b1a 100644
--- a/mocks/websocketsmocks/web_sockets_namespaced.go
+++ b/mocks/websocketsmocks/web_sockets_namespaced.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package websocketsmocks
diff --git a/mocks/wsmocks/ws_client.go b/mocks/wsmocks/ws_client.go
index 77712254ff..fccb7288b7 100644
--- a/mocks/wsmocks/ws_client.go
+++ b/mocks/wsmocks/ws_client.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.1. DO NOT EDIT.
+// Code generated by mockery v2.40.2. DO NOT EDIT.
package wsmocks
diff --git a/pkg/blockchain/plugin.go b/pkg/blockchain/plugin.go
index ea80a32f59..db0c3b09e8 100644
--- a/pkg/blockchain/plugin.go
+++ b/pkg/blockchain/plugin.go
@@ -97,7 +97,7 @@ type Plugin interface {
QueryContract(ctx context.Context, signingKey string, location *fftypes.JSONAny, parsedMethod interface{}, input map[string]interface{}, options map[string]interface{}) (interface{}, error)
// AddContractListener adds a new subscription to a user-specified contract and event
- AddContractListener(ctx context.Context, subscription *core.ContractListener) error
+ AddContractListener(ctx context.Context, subscription *core.ContractListener, lastProtocolID string) error
// DeleteContractListener deletes a previously-created subscription
DeleteContractListener(ctx context.Context, subscription *core.ContractListener, okNotFound bool) error
@@ -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
@@ -127,7 +133,7 @@ type Plugin interface {
GetAndConvertDeprecatedContractConfig(ctx context.Context) (location *fftypes.JSONAny, fromBlock string, err error)
// AddFireflySubscription creates a FireFly BatchPin subscription for the provided location
- AddFireflySubscription(ctx context.Context, namespace *core.Namespace, contract *MultipartyContract) (subID string, err error)
+ AddFireflySubscription(ctx context.Context, namespace *core.Namespace, contract *MultipartyContract, lastProtocolID string) (subID string, err error)
// RemoveFireFlySubscription removes the provided FireFly subscription
RemoveFireflySubscription(ctx context.Context, subID string)
diff --git a/pkg/core/contract_listener.go b/pkg/core/contract_listener.go
index fb3fb1241b..1eae679236 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 fbbaf88ca7..cb5566b25e 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/core/operation.go b/pkg/core/operation.go
index 5d8d098b8a..ff7a39607f 100644
--- a/pkg/core/operation.go
+++ b/pkg/core/operation.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2023 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -74,6 +74,79 @@ func (op *Operation) IsTokenOperation() bool {
return op.Type == OpTypeTokenActivatePool || op.Type == OpTypeTokenApproval || op.Type == OpTypeTokenCreatePool || op.Type == OpTypeTokenTransfer
}
+func (op *Operation) DeepCopy() *Operation {
+ cop := &Operation{
+ Namespace: op.Namespace,
+ Type: op.Type,
+ Status: op.Status,
+ Plugin: op.Plugin,
+ Error: op.Error,
+ }
+ if op.ID != nil {
+ idCopy := *op.ID
+ cop.ID = &idCopy
+ }
+ if op.Transaction != nil {
+ txCopy := *op.Transaction
+ cop.Transaction = &txCopy
+ }
+ if op.Created != nil {
+ createdCopy := *op.Created
+ cop.Created = &createdCopy
+ }
+ if op.Updated != nil {
+ updatedCopy := *op.Updated
+ cop.Updated = &updatedCopy
+ }
+ if op.Retry != nil {
+ retryCopy := *op.Retry
+ cop.Retry = &retryCopy
+ }
+ if op.Input != nil {
+ cop.Input = deepCopyMap(op.Input)
+ }
+ if op.Output != nil {
+ cop.Output = deepCopyMap(op.Output)
+ }
+ return cop
+}
+
+func deepCopyMap(original map[string]interface{}) map[string]interface{} {
+ if original == nil {
+ return nil
+ }
+ copy := make(map[string]interface{}, len(original))
+ for key, value := range original {
+ switch v := value.(type) {
+ case map[string]interface{}:
+ copy[key] = deepCopyMap(v)
+ case []interface{}:
+ copy[key] = deepCopySlice(v)
+ default:
+ copy[key] = v
+ }
+ }
+ return copy
+}
+
+func deepCopySlice(original []interface{}) []interface{} {
+ if original == nil {
+ return nil
+ }
+ copy := make([]interface{}, len(original))
+ for i, value := range original {
+ switch v := value.(type) {
+ case map[string]interface{}:
+ copy[i] = deepCopyMap(v)
+ case []interface{}:
+ copy[i] = deepCopySlice(v)
+ default:
+ copy[i] = v
+ }
+ }
+ return copy
+}
+
// OpStatus is the current status of an operation
type OpStatus string
diff --git a/pkg/core/operation_test.go b/pkg/core/operation_test.go
index 0ac132814c..6f9f4b44ef 100644
--- a/pkg/core/operation_test.go
+++ b/pkg/core/operation_test.go
@@ -1,4 +1,4 @@
-// Copyright ยฉ 2021 Kaleido, Inc.
+// Copyright ยฉ 2024 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
@@ -18,6 +18,7 @@ package core
import (
"context"
+ "reflect"
"testing"
"github.com/hyperledger/firefly-common/pkg/fftypes"
@@ -97,6 +98,63 @@ func TestOperationTypes(t *testing.T) {
assert.False(t, op.IsBlockchainOperation())
}
+func TestOperationDeepCopy(t *testing.T) {
+ op := &Operation{
+ ID: fftypes.NewUUID(),
+ Namespace: "ns1",
+ Transaction: fftypes.NewUUID(),
+ Type: OpTypeBlockchainInvoke,
+ Status: OpStatusInitialized,
+ Plugin: "fake",
+ Input: fftypes.JSONObject{"key": "value"},
+ Output: fftypes.JSONObject{"result": "success"},
+ Error: "error message",
+ Created: fftypes.Now(),
+ Updated: fftypes.Now(),
+ Retry: fftypes.NewUUID(),
+ }
+
+ copyOp := op.DeepCopy()
+ shallowCopy := op // Shallow copy for showcasing that DeepCopy is a deep copy
+
+ // Ensure the data was copied correctly
+ assert.Equal(t, op.ID, copyOp.ID)
+ assert.Equal(t, op.Namespace, copyOp.Namespace)
+ assert.Equal(t, op.Transaction, copyOp.Transaction)
+ assert.Equal(t, op.Type, copyOp.Type)
+ assert.Equal(t, op.Status, copyOp.Status)
+ assert.Equal(t, op.Plugin, copyOp.Plugin)
+ assert.Equal(t, op.Input, copyOp.Input)
+ assert.Equal(t, op.Output, copyOp.Output)
+ assert.Equal(t, op.Error, copyOp.Error)
+ assert.Equal(t, op.Created, copyOp.Created)
+ assert.Equal(t, op.Updated, copyOp.Updated)
+ assert.Equal(t, op.Retry, copyOp.Retry)
+
+ // Modify the original and ensure the copy is not modified
+ *op.ID = *fftypes.NewUUID()
+ assert.NotEqual(t, copyOp.ID, op.ID)
+
+ *op.Created = *fftypes.Now()
+ assert.NotEqual(t, copyOp.Created, op.Created)
+
+ // Ensure the copy is a deep copy by comparing the pointers of the fields
+ assert.NotSame(t, copyOp.ID, op.ID)
+ assert.NotSame(t, copyOp.Created, op.Created)
+ assert.NotSame(t, copyOp.Updated, op.Updated)
+ assert.NotSame(t, copyOp.Transaction, op.Transaction)
+ assert.NotSame(t, copyOp.Retry, op.Retry)
+ assert.NotSame(t, copyOp.Input, op.Input)
+ assert.NotSame(t, copyOp.Output, op.Output)
+
+ // showcasing that the shallow copy is a shallow copy and the copied object value changed as well the pointer has the same address as the original
+ assert.Equal(t, shallowCopy.ID, op.ID)
+ assert.Same(t, shallowCopy.ID, op.ID)
+
+ // Ensure no new fields are added to the Operation struct
+ // If a new field is added, this test will fail and the DeepCopy function should be updated
+ assert.Equal(t, 12, reflect.TypeOf(Operation{}).NumField())
+}
func TestParseNamespacedOpID(t *testing.T) {
ctx := context.Background()
@@ -124,3 +182,113 @@ func TestParseNamespacedOpID(t *testing.T) {
assert.Equal(t, "ns1", ns)
}
+
+func TestDeepCopyMapNil(t *testing.T) {
+ original := map[string]interface{}(nil)
+ copy := deepCopyMap(original)
+ assert.Nil(t, copy)
+}
+
+func TestDeepCopyMapEmpty(t *testing.T) {
+ original := map[string]interface{}{}
+ copy := deepCopyMap(original)
+ assert.NotNil(t, copy)
+ assert.Empty(t, copy)
+}
+
+func TestDeepCopyMapSimple(t *testing.T) {
+ original := map[string]interface{}{
+ "key1": "value1",
+ "key2": 42,
+ }
+ copy := deepCopyMap(original)
+ assert.Equal(t, original, copy)
+}
+
+func TestDeepCopyMapNestedMap(t *testing.T) {
+ original := map[string]interface{}{
+ "key1": map[string]interface{}{
+ "nestedKey1": "nestedValue1",
+ },
+ }
+ copy := deepCopyMap(original)
+ assert.Equal(t, original, copy)
+ assert.NotSame(t, original["key1"], copy["key1"])
+}
+
+func TestDeepCopyMapNestedSlice(t *testing.T) {
+ original := map[string]interface{}{
+ "key1": []interface{}{"value1", 42},
+ }
+ copy := deepCopyMap(original)
+ assert.Equal(t, original, copy)
+ assert.NotSame(t, original["key1"], copy["key1"])
+}
+
+func TestDeepCopyMapMixed(t *testing.T) {
+ original := map[string]interface{}{
+ "key1": "value1",
+ "key2": map[string]interface{}{
+ "nestedKey1": "nestedValue1",
+ },
+ "key3": []interface{}{"value1", 42},
+ }
+ copy := deepCopyMap(original)
+ assert.Equal(t, original, copy)
+ assert.NotSame(t, original["key2"], copy["key2"])
+ assert.NotSame(t, original["key3"], copy["key3"])
+}
+
+func TestDeepCopySliceNil(t *testing.T) {
+ original := []interface{}(nil)
+ copy := deepCopySlice(original)
+ assert.Nil(t, copy)
+}
+
+func TestDeepCopySliceEmpty(t *testing.T) {
+ original := []interface{}{}
+ copy := deepCopySlice(original)
+ assert.NotNil(t, copy)
+ assert.Empty(t, copy)
+}
+
+func TestDeepCopySliceSimple(t *testing.T) {
+ original := []interface{}{"value1", 42}
+ copy := deepCopySlice(original)
+ assert.Equal(t, original, copy)
+}
+
+func TestDeepCopySliceNestedMap(t *testing.T) {
+ original := []interface{}{
+ map[string]interface{}{
+ "nestedKey1": "nestedValue1",
+ },
+ }
+ copy := deepCopySlice(original)
+ assert.Equal(t, original, copy)
+ assert.NotSame(t, original[0], copy[0])
+}
+
+func TestDeepCopySliceNestedSlice(t *testing.T) {
+ original := []interface{}{
+ []interface{}{"value1", 42},
+ }
+ copy := deepCopySlice(original)
+ assert.Equal(t, original, copy)
+ assert.NotSame(t, original[0], copy[0])
+}
+
+func TestDeepCopySliceMixed(t *testing.T) {
+ original := []interface{}{
+ "value1",
+ 42,
+ map[string]interface{}{
+ "nestedKey1": "nestedValue1",
+ },
+ []interface{}{"value2", 43},
+ }
+ copy := deepCopySlice(original)
+ assert.Equal(t, original, copy)
+ assert.NotSame(t, original[2], copy[2])
+ assert.NotSame(t, original[3], copy[3])
+}
diff --git a/pkg/database/plugin.go b/pkg/database/plugin.go
index 367d2c963d..1a6ec10a26 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
diff --git a/smart_contracts/fabric/custompin-sample/.gitignore b/smart_contracts/fabric/custompin-sample/.gitignore
index b2a69e9a03..c2bab4fd8e 100644
--- a/smart_contracts/fabric/custompin-sample/.gitignore
+++ b/smart_contracts/fabric/custompin-sample/.gitignore
@@ -1,2 +1,3 @@
.DS_Store
vendor
+custompin_sample
diff --git a/smart_contracts/fabric/custompin-sample/go.mod b/smart_contracts/fabric/custompin-sample/go.mod
index 5577683ebe..7585e7201a 100644
--- a/smart_contracts/fabric/custompin-sample/go.mod
+++ b/smart_contracts/fabric/custompin-sample/go.mod
@@ -1,6 +1,6 @@
module github.com/hyperledger/firefly/custompin_sample
-go 1.21
+go 1.22
require (
github.com/hyperledger/fabric-chaincode-go v0.0.0-20240124143825-7dec3c7e7d45
@@ -26,16 +26,16 @@ require (
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
- golang.org/x/net v0.20.0 // indirect
- golang.org/x/sys v0.16.0 // indirect
- golang.org/x/text v0.14.0 // indirect
+ golang.org/x/net v0.33.0 // indirect
+ golang.org/x/sys v0.28.0 // indirect
+ golang.org/x/text v0.21.0 // indirect
google.golang.org/grpc v1.61.0 // indirect
)
require (
github.com/golang/protobuf v1.5.3
github.com/josharian/intern v1.0.0 // indirect
- golang.org/x/mod v0.14.0 // indirect
+ golang.org/x/mod v0.17.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/smart_contracts/fabric/custompin-sample/go.sum b/smart_contracts/fabric/custompin-sample/go.sum
index b6c804998d..b355fbf866 100644
--- a/smart_contracts/fabric/custompin-sample/go.sum
+++ b/smart_contracts/fabric/custompin-sample/go.sum
@@ -101,10 +101,13 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
+golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -113,9 +116,11 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe h1:bQnxqljG/wqi4NTXu2+DJ3n7APcEA882QZ1JvhQAq9o=
diff --git a/smart_contracts/fabric/firefly-go/.gitignore b/smart_contracts/fabric/firefly-go/.gitignore
index b2a69e9a03..a76ee7d820 100644
--- a/smart_contracts/fabric/firefly-go/.gitignore
+++ b/smart_contracts/fabric/firefly-go/.gitignore
@@ -1,2 +1,3 @@
.DS_Store
vendor
+chaincode-go
diff --git a/smart_contracts/fabric/firefly-go/go.mod b/smart_contracts/fabric/firefly-go/go.mod
index 11f2be79fd..8ecbb36039 100644
--- a/smart_contracts/fabric/firefly-go/go.mod
+++ b/smart_contracts/fabric/firefly-go/go.mod
@@ -1,6 +1,6 @@
module github.com/hyperledger/firefly/chaincode-go
-go 1.21
+go 1.22
require (
github.com/golang/protobuf v1.5.3
@@ -28,10 +28,10 @@ require (
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
- golang.org/x/mod v0.14.0 // indirect
- golang.org/x/net v0.20.0 // indirect
- golang.org/x/sys v0.16.0 // indirect
- golang.org/x/text v0.14.0 // indirect
+ golang.org/x/mod v0.17.0 // indirect
+ golang.org/x/net v0.33.0 // indirect
+ golang.org/x/sys v0.28.0 // indirect
+ golang.org/x/text v0.21.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe // indirect
google.golang.org/grpc v1.61.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
diff --git a/smart_contracts/fabric/firefly-go/go.sum b/smart_contracts/fabric/firefly-go/go.sum
index b6c804998d..8e55b0de23 100644
--- a/smart_contracts/fabric/firefly-go/go.sum
+++ b/smart_contracts/fabric/firefly-go/go.sum
@@ -99,23 +99,23 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
-golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
-golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
+golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
-golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe h1:bQnxqljG/wqi4NTXu2+DJ3n7APcEA882QZ1JvhQAq9o=
diff --git a/test/data/contracts/assetcreator/go.mod b/test/data/contracts/assetcreator/go.mod
index a90777bf1e..3a64e86205 100644
--- a/test/data/contracts/assetcreator/go.mod
+++ b/test/data/contracts/assetcreator/go.mod
@@ -1,8 +1,8 @@
module github.com/hyperledger/firefly/test/data/assetcreator
-go 1.21
+go 1.22
-toolchain go1.21.0
+toolchain go1.22.0
require github.com/hyperledger/fabric-contract-api-go v1.2.2
@@ -24,10 +24,10 @@ require (
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
- golang.org/x/mod v0.14.0 // indirect
- golang.org/x/net v0.20.0 // indirect
- golang.org/x/sys v0.16.0 // indirect
- golang.org/x/text v0.14.0 // indirect
+ golang.org/x/mod v0.17.0 // indirect
+ golang.org/x/net v0.33.0 // indirect
+ golang.org/x/sys v0.28.0 // indirect
+ golang.org/x/text v0.21.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe // indirect
google.golang.org/grpc v1.61.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
diff --git a/test/data/contracts/assetcreator/go.sum b/test/data/contracts/assetcreator/go.sum
index b6c804998d..8e55b0de23 100644
--- a/test/data/contracts/assetcreator/go.sum
+++ b/test/data/contracts/assetcreator/go.sum
@@ -99,23 +99,23 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
-golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
-golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
+golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
-golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe h1:bQnxqljG/wqi4NTXu2+DJ3n7APcEA882QZ1JvhQAq9o=