diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..368c77c6
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,4 @@
+**/node_modules
+**/coverage
+**/.nyc_output
+firefly-transaction-manager
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..3098b1be
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,7 @@
+root = true
+
+[*.{yaml,yml,json}]
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 2
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..3c6aa9e7
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+*.go licensefile=.githooks/license-maintainer/LICENSE-go
\ No newline at end of file
diff --git a/.github/settings.yml b/.github/settings.yml
new file mode 100644
index 00000000..4a3f72a2
--- /dev/null
+++ b/.github/settings.yml
@@ -0,0 +1,12 @@
+repository:
+ name: firefly-transaction-manager
+ default_branch: main
+ has_downloads: false
+ has_issues: true
+ has_projects: false
+ has_wiki: false
+ archived: false
+ private: false
+ allow_squash_merge: false
+ allow_merge_commit: false
+ allow_rebase_merge: true
diff --git a/.github/workflows/docker_main.yml b/.github/workflows/docker_main.yml
new file mode 100644
index 00000000..e3814e3a
--- /dev/null
+++ b/.github/workflows/docker_main.yml
@@ -0,0 +1,43 @@
+name: Docker Main Build
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ docker:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+
+ - name: Set build tag
+ id: build_tag_generator
+ run: |
+ RELEASE_TAG=$(curl https://api.github.com/repos/hyperledger/firefly-transaction-manager/releases/latest -s | jq .tag_name -r)
+ BUILD_TAG=$RELEASE_TAG-$(date +"%Y%m%d")-$GITHUB_RUN_NUMBER
+ echo ::set-output name=BUILD_TAG::$BUILD_TAG
+
+ - name: Build
+ run: |
+ make BUILD_VERSION="${GITHUB_REF##*/}" DOCKER_ARGS="\
+ --label commit=$GITHUB_SHA \
+ --label build_date=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
+ --label tag=${{ steps.build_tag_generator.outputs.BUILD_TAG }} \
+ --tag ghcr.io/hyperledger/firefly-transaction-manager:${{ steps.build_tag_generator.outputs.BUILD_TAG }}" \
+ docker
+
+ - name: Tag release
+ run: docker tag ghcr.io/hyperledger/firefly-transaction-manager:${{ steps.build_tag_generator.outputs.BUILD_TAG }} ghcr.io/hyperledger/firefly-transaction-manager:head
+
+ - name: Push docker image
+ run: |
+ echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin
+ docker push ghcr.io/hyperledger/firefly-transaction-manager:${{ steps.build_tag_generator.outputs.BUILD_TAG }}
+
+ - name: Push head tag
+ run: |
+ echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin
+ docker push ghcr.io/hyperledger/firefly-transaction-manager:head
diff --git a/.github/workflows/docker_release.yml b/.github/workflows/docker_release.yml
new file mode 100644
index 00000000..79632ee2
--- /dev/null
+++ b/.github/workflows/docker_release.yml
@@ -0,0 +1,37 @@
+name: Docker Release Build
+
+on:
+ release:
+ types: [released, prereleased]
+
+jobs:
+ docker:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+
+ - name: Build
+ run: |
+ make BUILD_VERSION="${GITHUB_REF##*/}" DOCKER_ARGS="\
+ --label commit=$GITHUB_SHA \
+ --label build_date=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
+ --label tag=${GITHUB_REF##*/} \
+ --tag ghcr.io/hyperledger/firefly-transaction-manager:${GITHUB_REF##*/}" \
+ docker
+
+ - name: Tag release
+ if: github.event.action == 'released'
+ run: docker tag ghcr.io/hyperledger/firefly-transaction-manager:${GITHUB_REF##*/} ghcr.io/hyperledger/firefly-transaction-manager:latest
+
+ - name: Push docker image
+ run: |
+ echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin
+ docker push ghcr.io/hyperledger/firefly-transaction-manager:${GITHUB_REF##*/}
+
+ - name: Push latest tag
+ if: github.event.action == 'released'
+ run: |
+ echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin
+ docker push ghcr.io/hyperledger/firefly-transaction-manager:latest
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
new file mode 100644
index 00000000..502c74c9
--- /dev/null
+++ b/.github/workflows/go.yml
@@ -0,0 +1,28 @@
+name: Go
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+
+ - name: Set up Go
+ uses: actions/setup-go@v2
+ with:
+ go-version: 1.16
+
+ - name: Build and Test
+ run: make
+
+ - name: Upload coverage
+ run: bash <(curl -s https://codecov.io/bash)
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..807ddea6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+**/*.jar
+firefly-transaction-manager
+coverage.txt
+**/debug.test
+.DS_Store
+__debug*
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 00000000..6c6e82f6
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,60 @@
+run:
+ tests: false
+ skip-dirs:
+ - "mocks"
+linters-settings:
+ golint: {}
+ gocritic:
+ enabled-checks: []
+ disabled-checks:
+ - regexpMust
+ goheader:
+ values:
+ regexp:
+ COMPANY: .*
+ template: |-
+ Copyright © {{ YEAR }} {{ COMPANY }}
+
+ 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.
+linters:
+ disable-all: false
+ enable:
+ - bodyclose
+ - deadcode
+ - depguard
+ - dogsled
+ - errcheck
+ - goconst
+ - gocritic
+ - gocyclo
+ - gofmt
+ - goheader
+ - goimports
+ - goprintffuncname
+ - gosec
+ - gosimple
+ - govet
+ - ineffassign
+ - misspell
+ - nakedret
+ - revive
+ - staticcheck
+ - structcheck
+ - stylecheck
+ - typecheck
+ - unconvert
+ - unparam
+ - unused
+ - varcheck
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..c47a7b82
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,63 @@
+{
+ "go.formatFlags": [
+ "-s"
+ ],
+ "go.lintTool": "golangci-lint",
+ "cSpell.words": [
+ "APIID",
+ "ccache",
+ "dataexchange",
+ "Debugf",
+ "devdocs",
+ "Devel",
+ "ethconnect",
+ "fabconnect",
+ "ffcapi",
+ "ffcapimocks",
+ "ffcore",
+ "FFDX",
+ "ffenum",
+ "ffexclude",
+ "ffexcludeinput",
+ "ffexcludeoutput",
+ "ffievents",
+ "FFIID",
+ "ffimethods",
+ "ffresty",
+ "ffstruct",
+ "fftm",
+ "fftypes",
+ "finalizers",
+ "GJSON",
+ "Infof",
+ "IPFS",
+ "mtxs",
+ "NATS",
+ "Nowarn",
+ "oapispec",
+ "optype",
+ "policyengine",
+ "protocolid",
+ "resty",
+ "santhosh",
+ "secp",
+ "sigs",
+ "stretchr",
+ "sysmessaging",
+ "tekuri",
+ "tmconfig",
+ "tmmsgs",
+ "Tracef",
+ "txcommon",
+ "txcommonmocks",
+ "txid",
+ "txtype",
+ "unflushed",
+ "upgrader",
+ "upserts",
+ "Warnf",
+ "wsclient",
+ "wsconfig"
+ ],
+ "go.testTimeout": "10s"
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..3fd705da
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+# Changelog
+
+[FireFly Transaction Manager Releases](https://github.com/hyperledger/firefly-transaction-manager/releases)
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 00000000..01106b8d
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: Apache-2.0
+
+# FireFly Core Maintainers
+* @peterbroadhurst @nguyer @awrichar
+
+# FireFly Documentation Maintainers
+/docs @peterbroadhurst @nguyer @awrichar @nickgaski
\ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000..3449f095
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,7 @@
+# Code of Conduct Guidelines
+
+Please review the Hyperledger [Code of
+Conduct](https://wiki.hyperledger.org/community/hyperledger-project-code-of-conduct)
+before participating. It is important that we keep things civil.
+
+
This work is licensed under a Creative Commons Attribution 4.0 International License.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..0d1cf0b1
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,10 @@
+## Contributing
+
+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/contributors.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
new file mode 100644
index 00000000..5e4a0239
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,12 @@
+FROM golang:1.16-buster AS builder
+ARG BUILD_VERSION
+ENV BUILD_VERSION=${BUILD_VERSION}
+ADD . /fftm
+WORKDIR /fftm
+RUN make
+
+FROM debian:buster-slim
+WORKDIR /fftm
+COPY --from=builder /fftm/firefly-transaction-manager /usr/bin/fftm
+
+ENTRYPOINT [ "/usr/bin/fftm" ]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..4af13528
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2018 Kaleido
+
+ 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.
diff --git a/MAINTAINERS.md b/MAINTAINERS.md
new file mode 100644
index 00000000..5ceb7e93
--- /dev/null
+++ b/MAINTAINERS.md
@@ -0,0 +1,52 @@
+# Maintainers
+
+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 |
+| Andrew Richardson | awrichar | andrew.richardson@kaleido.io | Andrew.Richardson |
+
+
+This list is to be kept up to date as maintainers are added or removed.
+
+# Expectations of Maintainers
+
+Maintainers are expected to regularly:
+
+- Make contributions to FireFly code repositories including code or documentation
+- Review pull requests
+- Investigate open GitHub issues
+- Participate in Community Calls
+
+# Becoming a Maintainer
+
+The FireFly Project welcomes and encourages people to become maintainers of the project if they are interested and meet the following criteria:
+
+## Criteria for becoming a member
+
+- Expressed interest and commitment to meet the expectations of a maintainer for at least 6 months
+- A consistent track record of contributions to FireFly code repositories which could be:
+ - Enhancements
+ - Bug fixes
+ - Tests
+ - Documentation
+- A consistent track record of helpful code reviews on FireFly code repositories
+- Regular participation in Community Calls
+- A demonstrated interest and aptitude in thought leadership within the FireFly Project
+- Sponsorship from an existing maintainer
+
+There is no specific quantity of contributions or pull requests, or a specific time period over which the candidate must prove their track record. This will be left up to the discretion of the existing maintainers.
+
+## Process for becoming a maintainer
+
+Once the above criteria have been met, the sponsoring maintainer shall propose the addition of the new maintainer at a public Community Call. Existing maintainers shall vote at the next public Community Call whether the new maintainer should be added or not. Proxy votes may be submitted via email _before_ the meeting. A simple majority of the existing maintainers is required for the vote to pass.
+
+## Maintainer resignation
+
+While maintainers are expected in good faith to be committed to the project for a significant period of time, they are under no binding obligation to do so. Maintainers may resign at any time for any reason. If a maintainer wishes to resign they shall open a pull request to update the maintainers list removing themselves from the list.
+
+## Maintainer inactivity
+
+If a maintainer has remained inactive (not meeting the expectations of a maintainer) for a period of time (at least several months), an email should be sent to that maintainer noting their inactivity and asking if they still wish to be a maintainer. If they continue to be inactive after being notified via email, an existing maintainer may propose to remove the inactive maintainer at a public Community Call. Existing maintainers shall vote at the next public Community Call whether the inactive maintainer should be removed or not. Proxy votes may be submitted via email _before_ the meeting. A simple majority of the existing maintainers is required for the vote to pass.
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..aca355dc
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,51 @@
+VGO=go
+GOFILES := $(shell find cmd internal pkg -name '*.go' -print)
+GOBIN := $(shell $(VGO) env GOPATH)/bin
+LINT := $(GOBIN)/golangci-lint
+MOCKERY := $(GOBIN)/mockery
+
+# Expect that FireFly compiles with CGO disabled
+CGO_ENABLED=0
+GOGC=30
+
+.DELETE_ON_ERROR:
+
+all: build test go-mod-tidy
+test: deps lint
+ $(VGO) test ./internal/... ./cmd/... -cover -coverprofile=coverage.txt -covermode=atomic -timeout=30s
+coverage.html:
+ $(VGO) tool cover -html=coverage.txt
+coverage: test coverage.html
+lint: ${LINT}
+ GOGC=20 $(LINT) run -v --timeout 5m
+${MOCKERY}:
+ $(VGO) install github.com/vektra/mockery/cmd/mockery@latest
+${LINT}:
+ $(VGO) install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
+
+
+define makemock
+mocks: mocks-$(strip $(1))-$(strip $(2))
+mocks-$(strip $(1))-$(strip $(2)): ${MOCKERY}
+ ${MOCKERY} --case underscore --dir $(1) --name $(2) --outpkg $(3) --output mocks/$(strip $(3))
+endef
+
+$(eval $(call makemock, pkg/ffcapi, API, ffcapimocks))
+$(eval $(call makemock, pkg/policyengine, PolicyEngine, policyenginemocks))
+$(eval $(call makemock, internal/confirmations, Manager, confirmationsmocks))
+$(eval $(call makemock, internal/manager, Manager, managermocks))
+
+firefly-transaction-manager: ${GOFILES}
+ $(VGO) build -o ./firefly-transaction-manager -ldflags "-X main.buildDate=`date -u +\"%Y-%m-%dT%H:%M:%SZ\"` -X main.buildVersion=$(BUILD_VERSION)" -tags=prod -tags=prod -v ./fftm
+go-mod-tidy: .ALWAYS
+ $(VGO) mod tidy
+build: firefly-transaction-manager
+.ALWAYS: ;
+clean:
+ $(VGO) clean
+deps:
+ $(VGO) get ./fftm
+swagger:
+ $(VGO) test ./internal/apiserver -timeout=10s -tags swagger
+docker:
+ docker build --build-arg BUILD_VERSION=${BUILD_VERSION} ${DOCKER_ARGS} -t hyperledger/firefly-transaction-manager .
\ No newline at end of file
diff --git a/README.md b/README.md
index 46e32b7e..a2a3bf54 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,24 @@
-# firefly-transaction-manager
+[![codecov](https://codecov.io/gh/hyperledger/firefly-transaction-manager/branch/main/graph/badge.svg?token=G6TaoNNBx9)](https://codecov.io/gh/hyperledger/firefly-transaction-manager) [![Go Reference](https://pkg.go.dev/badge/github.com/hyperledger/firefly-transaction-manager.svg)](https://pkg.go.dev/github.com/hyperledger/firefly-transaction-manager)
+
+# Hyperledger FireFly Transaction Manager
+
+Plugable microservice component of Hyperledger FireFly, responsible for:
+
+- Submission of transactions to blockchains of all types
+ - Protocol connectivity decoupled with additional lightweight API connector
+ - Easy to add additional protocols that conform to normal patterns of TX submission / events
+
+- Monitoring and updating blockchain operations
+ - Receipts
+ - Confirmations
+
+- Gas calculation and rescue of stalled transactions
+ - Extensible policy engine
+ - Gas station API integration
+
+- Event streaming [* work in progress to extract from previous location in ethconnect]
+ - Protocol agnostic event polling/streaming support
+ - Reliable checkpoint restart
+ - At least once delivery API
+
+![Hyperledger FireFly Transaction Manager](./images/firefly_transaction_manager.jpg)
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..733a396b
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,20 @@
+# Hyperledger Security Policy
+
+## Reporting a Security Bug
+
+If you think you have discovered a security issue in any of the Hyperledger projects, we'd love to
+hear from you. We will take all security bugs seriously and if confirmed upon investigation we will
+patch it within a reasonable amount of time and release a public security bulletin discussing the
+impact and credit the discoverer.
+
+There are two ways to report a security bug. The easiest is to email a description of the flaw and
+any related information (e.g. reproduction steps, version) to
+[security at hyperledger dot org](mailto:security@hyperledger.org).
+
+The other way is to file a confidential security bug in our
+[JIRA bug tracking system](https://jira.hyperledger.org). Be sure to set the “Security Level” to
+“Security issue”.
+
+The process by which the Hyperledger Security Team handles security bugs is documented further in
+our [Defect Response page](https://wiki.hyperledger.org/display/SEC/Defect+Response) on our
+[wiki](https://wiki.hyperledger.org).
\ No newline at end of file
diff --git a/cmd/config.go b/cmd/config.go
new file mode 100644
index 00000000..3c3d3724
--- /dev/null
+++ b/cmd/config.go
@@ -0,0 +1,40 @@
+// 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 cmd
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/hyperledger/firefly/pkg/config"
+ "github.com/spf13/cobra"
+)
+
+func configCommand() *cobra.Command {
+ versionCmd := &cobra.Command{
+ Use: "docs",
+ Short: "Prints the config info as markdown",
+ Long: "",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ initConfig()
+ b, err := config.GenerateConfigMarkdown(context.Background(), config.GetKnownKeys())
+ fmt.Println(string(b))
+ return err
+ },
+ }
+ return versionCmd
+}
diff --git a/cmd/config_test.go b/cmd/config_test.go
new file mode 100644
index 00000000..8ccdd1bf
--- /dev/null
+++ b/cmd/config_test.go
@@ -0,0 +1,30 @@
+// 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 cmd
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestConfigMarkdown(t *testing.T) {
+ rootCmd.SetArgs([]string{"docs"})
+ defer rootCmd.SetArgs([]string{})
+ err := rootCmd.Execute()
+ assert.NoError(t, err)
+}
diff --git a/cmd/fftm.go b/cmd/fftm.go
new file mode 100644
index 00000000..9294accf
--- /dev/null
+++ b/cmd/fftm.go
@@ -0,0 +1,102 @@
+// 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 cmd
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "github.com/hyperledger/firefly-transaction-manager/internal/manager"
+ "github.com/hyperledger/firefly-transaction-manager/internal/policyengines"
+ "github.com/hyperledger/firefly-transaction-manager/internal/policyengines/simple"
+ "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig"
+ "github.com/hyperledger/firefly/pkg/config"
+ "github.com/hyperledger/firefly/pkg/i18n"
+ "github.com/hyperledger/firefly/pkg/log"
+ "github.com/sirupsen/logrus"
+ "github.com/spf13/cobra"
+)
+
+var sigs = make(chan os.Signal, 1)
+
+var rootCmd = &cobra.Command{
+ Use: "fftm",
+ Short: "Hyperledger FireFly Tranansaction Manager",
+ Long: ``,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return run()
+ },
+}
+
+var cfgFile string
+
+func init() {
+ rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "f", "", "config file")
+ rootCmd.AddCommand(versionCommand())
+ rootCmd.AddCommand(configCommand())
+}
+
+func Execute() error {
+ return rootCmd.Execute()
+}
+
+func initConfig() {
+ // Read the configuration, and register our policy engines
+ tmconfig.Reset()
+ policyengines.RegisterEngine(tmconfig.PolicyEngineBasePrefix, &simple.PolicyEngineFactory{})
+}
+
+func run() error {
+
+ initConfig()
+ err := config.ReadConfig("fftm", cfgFile)
+
+ // Setup logging after reading config (even if failed), to output header correctly
+ ctx, cancelCtx := context.WithCancel(context.Background())
+ defer cancelCtx()
+ ctx = log.WithLogger(ctx, logrus.WithField("pid", fmt.Sprintf("%d", os.Getpid())))
+ ctx = log.WithLogger(ctx, logrus.WithField("prefix", "fftm"))
+
+ config.SetupLogging(ctx)
+
+ // Deferred error return from reading config
+ if err != nil {
+ cancelCtx()
+ return i18n.WrapError(ctx, err, i18n.MsgConfigFailed)
+ }
+
+ // Setup signal handling to cancel the context, which shuts down the API Server
+ signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+ go func() {
+ sig := <-sigs
+ log.L(ctx).Infof("Shutting down due to %s", sig.String())
+ cancelCtx()
+ }()
+
+ manager, err := manager.NewManager(ctx)
+ if err != nil {
+ return err
+ }
+ err = manager.Start()
+ if err != nil {
+ return err
+ }
+ return manager.WaitStop()
+}
diff --git a/cmd/fftm_test.go b/cmd/fftm_test.go
new file mode 100644
index 00000000..d093b084
--- /dev/null
+++ b/cmd/fftm_test.go
@@ -0,0 +1,76 @@
+// Copyright © 2021 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 cmd
+
+import (
+ "os"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+const configDir = "../test/data/config"
+
+func TestRunOK(t *testing.T) {
+
+ rootCmd.SetArgs([]string{"-f", "../test/firefly.fftm.yaml"})
+ defer rootCmd.SetArgs([]string{})
+
+ done := make(chan struct{})
+ go func() {
+ defer close(done)
+ err := Execute()
+ assert.NoError(t, err)
+ }()
+
+ time.Sleep(10 * time.Millisecond)
+ sigs <- os.Kill
+
+ <-done
+
+}
+
+func TestRunMissingConfig(t *testing.T) {
+
+ rootCmd.SetArgs([]string{"-f", "../test/does-not-exist.fftm.yaml"})
+ defer rootCmd.SetArgs([]string{})
+
+ err := Execute()
+ assert.Regexp(t, "FF00101", err)
+
+}
+
+func TestRunBadConfig(t *testing.T) {
+
+ rootCmd.SetArgs([]string{"-f", "../test/empty-config.fftm.yaml"})
+ defer rootCmd.SetArgs([]string{})
+
+ err := Execute()
+ assert.Regexp(t, "FF201018", err)
+
+}
+
+func TestRunFailStartup(t *testing.T) {
+
+ rootCmd.SetArgs([]string{"-f", "../test/quick-fail.fftm.yaml"})
+ defer rootCmd.SetArgs([]string{})
+
+ err := Execute()
+ assert.Regexp(t, "FF201017", err)
+
+}
diff --git a/cmd/version.go b/cmd/version.go
new file mode 100644
index 00000000..2ed691ed
--- /dev/null
+++ b/cmd/version.go
@@ -0,0 +1,103 @@
+// 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 cmd
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "runtime/debug"
+
+ "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs"
+ "github.com/hyperledger/firefly/pkg/i18n"
+ "github.com/spf13/cobra"
+ "gopkg.in/yaml.v2"
+)
+
+var shortened = false
+var output = "json"
+
+var BuildDate string // set by go-releaser
+var BuildCommit string // set by go-releaser
+var BuildVersionOverride string // set by go-releaser
+
+type Info struct {
+ Version string `json:"Version,omitempty" yaml:"Version,omitempty"`
+ Commit string `json:"Commit,omitempty" yaml:"Commit,omitempty"`
+ Date string `json:"Date,omitempty" yaml:"Date,omitempty"`
+ License string `json:"License,omitempty" yaml:"License,omitempty"`
+}
+
+func setBuildInfo(info *Info, buildInfo *debug.BuildInfo, ok bool) {
+ if ok {
+ info.Version = buildInfo.Main.Version
+ }
+}
+
+func versionCommand() *cobra.Command {
+ versionCmd := &cobra.Command{
+ Use: "version",
+ Short: "Prints the version info",
+ Long: "",
+ RunE: func(cmd *cobra.Command, args []string) error {
+
+ info := &Info{
+ Version: BuildVersionOverride,
+ Date: BuildDate,
+ Commit: BuildCommit,
+ License: "Apache-2.0",
+ }
+
+ // Where you are using go install, we will get good version information usefully from Go
+ // When we're in go-releaser in a Github action, we will have the version passed in explicitly
+ if info.Version == "" {
+ buildInfo, ok := debug.ReadBuildInfo()
+ setBuildInfo(info, buildInfo, ok)
+ }
+
+ if shortened {
+ fmt.Println(info.Version)
+ } else {
+ var (
+ bytes []byte
+ err error
+ )
+
+ switch output {
+ case "json":
+ bytes, err = json.MarshalIndent(info, "", " ")
+ case "yaml":
+ bytes, err = yaml.Marshal(info)
+ default:
+ err = i18n.NewError(context.Background(), tmmsgs.MsgInvalidOutputType, output)
+ }
+
+ if err != nil {
+ return err
+ }
+
+ fmt.Println(string(bytes))
+ }
+
+ return nil
+ },
+ }
+
+ versionCmd.Flags().BoolVarP(&shortened, "short", "s", false, "print only the version")
+ versionCmd.Flags().StringVarP(&output, "output", "o", "json", "output format (\"yaml\"|\"json\")")
+ return versionCmd
+}
diff --git a/cmd/version_test.go b/cmd/version_test.go
new file mode 100644
index 00000000..01c84084
--- /dev/null
+++ b/cmd/version_test.go
@@ -0,0 +1,65 @@
+// 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 cmd
+
+import (
+ "runtime/debug"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestVersionCmdDefault(t *testing.T) {
+ rootCmd.SetArgs([]string{"version"})
+ defer rootCmd.SetArgs([]string{})
+ err := rootCmd.Execute()
+ assert.NoError(t, err)
+}
+
+func TestVersionCmdYAML(t *testing.T) {
+ rootCmd.SetArgs([]string{"version", "-o", "yaml"})
+ defer rootCmd.SetArgs([]string{})
+ err := rootCmd.Execute()
+ assert.NoError(t, err)
+}
+
+func TestVersionCmdJSON(t *testing.T) {
+ rootCmd.SetArgs([]string{"version", "-o", "json"})
+ defer rootCmd.SetArgs([]string{})
+ err := rootCmd.Execute()
+ assert.NoError(t, err)
+}
+
+func TestVersionCmdInvalidType(t *testing.T) {
+ rootCmd.SetArgs([]string{"version", "-o", "wrong"})
+ defer rootCmd.SetArgs([]string{})
+ err := rootCmd.Execute()
+ assert.Regexp(t, "FF201010", err)
+}
+
+func TestVersionCmdShorthand(t *testing.T) {
+ rootCmd.SetArgs([]string{"version", "-s"})
+ defer rootCmd.SetArgs([]string{})
+ err := rootCmd.Execute()
+ assert.NoError(t, err)
+}
+
+func TestSetBuildInfoWithBI(t *testing.T) {
+ info := &Info{}
+ setBuildInfo(info, &debug.BuildInfo{Main: debug.Module{Version: "12345"}}, true)
+ assert.Equal(t, "12345", info.Version)
+}
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 00000000..02e77f8a
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,10 @@
+coverage:
+ status:
+ project:
+ default:
+ threshold: 0.1%
+ patch:
+ default:
+ threshold: 0.1%
+ ignore:
+ - "mocks/**/*.go"
diff --git a/fftm/main.go b/fftm/main.go
new file mode 100644
index 00000000..80b54974
--- /dev/null
+++ b/fftm/main.go
@@ -0,0 +1,32 @@
+// 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"
+
+ "github.com/hyperledger/firefly-transaction-manager/cmd"
+)
+
+func main() {
+ if err := cmd.Execute(); err != nil {
+ fmt.Fprintf(os.Stderr, "%s\n", err)
+ os.Exit(1)
+ }
+ os.Exit(0)
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 00000000..15bfee35
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,16 @@
+module github.com/hyperledger/firefly-transaction-manager
+
+go 1.16
+
+require (
+ github.com/go-resty/resty/v2 v2.7.0
+ github.com/gorilla/mux v1.8.0
+ github.com/hashicorp/golang-lru v0.5.4
+ github.com/hyperledger/firefly v1.0.0-rc.4.0.20220419045021-4e8daade6f4d
+ github.com/sirupsen/logrus v1.8.1
+ github.com/spf13/cobra v1.3.0
+ github.com/spf13/viper v1.10.1
+ github.com/stretchr/testify v1.7.1
+ github.com/tidwall/gjson v1.14.0
+ gopkg.in/yaml.v2 v2.4.0
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 00000000..14ed3872
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,1765 @@
+bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
+cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
+cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
+cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
+cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
+cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
+cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
+cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
+cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
+cloud.google.com/go v0.88.0/go.mod h1:dnKwfYbP9hQhefiUvpbcAyoGSHUrOxR20JVElLiUvEY=
+cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
+cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
+cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
+cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
+cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM=
+cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/spanner v1.24.0/go.mod h1:EZI0yH1D/PrXK0XH9Ba5LGXTXWeqZv0ClOD/19a0Z58=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
+github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
+github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck=
+github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
+github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
+github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg=
+github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=
+github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=
+github.com/Azure/go-autorest/autorest/adal v0.9.14/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=
+github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
+github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
+github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
+github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
+github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
+github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=
+github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
+github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
+github.com/Masterminds/squirrel v1.5.2/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
+github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
+github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
+github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
+github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
+github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
+github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
+github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
+github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
+github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
+github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
+github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
+github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ=
+github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8=
+github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg=
+github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00=
+github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600=
+github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4=
+github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg=
+github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU=
+github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY=
+github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
+github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
+github.com/aidarkhanov/nanoid v1.0.8 h1:yxyJkgsEDFXP7+97vc6JevMcjyb03Zw+/9fqhlVXBXA=
+github.com/aidarkhanov/nanoid v1.0.8/go.mod h1:vadfZHT+m4uDhttg0yY4wW3GKtl2T6i4d2Age+45pYk=
+github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
+github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/apache/arrow/go/arrow v0.0.0-20210818145353-234c94e4ce64/go.mod h1:2qMFB56yOP3KzkB3PbYZ4AlUFg3a88F67TIx5lB/WwY=
+github.com/apache/arrow/go/arrow v0.0.0-20211013220434-5962184e7a30/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs=
+github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
+github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
+github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
+github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/aws/aws-sdk-go-v2 v1.8.0/go.mod h1:xEFuWz+3TYdlPRuo+CqATbeDWIWyaT5uAPwPaWtgse0=
+github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
+github.com/aws/aws-sdk-go-v2/config v1.6.0/go.mod h1:TNtBVmka80lRPk5+S9ZqVfFszOQAGJJ9KbT3EM3CHNU=
+github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw=
+github.com/aws/aws-sdk-go-v2/credentials v1.3.2/go.mod h1:PACKuTJdt6AlXvEq8rFI4eDmoqDFC5DpVKQbWysaDgM=
+github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.4.0/go.mod h1:Mj/U8OpDbcVcoctrYwA2bak8k/HFPdcLzI/vaiXMwuM=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.4.0/go.mod h1:eHwXu2+uE/T6gpnYWwBwqoeqRf9IXyCcolyOWDRAErQ=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.5.4/go.mod h1:Ex7XQmbFmgFHrjUX6TN3mApKW5Hglyga+F7wZHTtYhA=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.2.0/go.mod h1:Q5jATQc+f1MfZp3PDMhn6ry18hGvE0i8yvbXoKbnZaE=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.2.2/go.mod h1:EASdTcM1lGhUe1/p4gkojHwlGJkeoRjjr1sRCzup3Is=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.3.0/go.mod h1:v8ygadNyATSm6elwJ/4gzJwcFhri9RqS8skgHKiwXPU=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.2.2/go.mod h1:NXmNI41bdEsJMrD0v9rUvbGCB5GwdBEpKvUvIY3vTFg=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.5.2/go.mod h1:QuL2Ym8BkrLmN4lUofXYq6000/i5jPjosCNK//t6gak=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.7.2/go.mod h1:np7TMuJNT83O0oDOSF8i4dF3dvGqA6hPYYo6YYkzgRA=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.12.0/go.mod h1:6J++A5xpo7QDsIeSqPK4UHqMSyPOCopa+zKtqAMhqVQ=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.16.1/go.mod h1:CQe/KvWV1AqRc65KqeJjrLzr5X2ijnFTTVzJW0VBRCI=
+github.com/aws/aws-sdk-go-v2/service/sso v1.3.2/go.mod h1:J21I6kF+d/6XHVk7kp/cx9YVD2TMD2TbLwtRGVcinXo=
+github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk=
+github.com/aws/aws-sdk-go-v2/service/sts v1.6.1/go.mod h1:hLZ/AnkIKHLuPGjEiyghNEdvJ2PP0MgOxcmv9EBJ4xs=
+github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g=
+github.com/aws/smithy-go v1.7.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
+github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
+github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
+github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
+github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
+github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
+github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
+github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
+github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4=
+github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
+github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
+github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
+github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
+github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
+github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
+github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
+github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg=
+github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
+github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=
+github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc=
+github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=
+github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
+github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
+github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
+github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
+github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
+github.com/cockroachdb/cockroach-go/v2 v2.1.1/go.mod h1:7NtUnP6eK+l6k483WSYNrq3Kb23bWV10IRV1TyeSpwM=
+github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
+github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE=
+github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU=
+github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=
+github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=
+github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E=
+github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss=
+github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss=
+github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI=
+github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
+github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM=
+github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo=
+github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo=
+github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE=
+github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU=
+github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
+github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
+github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE=
+github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=
+github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
+github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.4.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ=
+github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU=
+github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI=
+github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s=
+github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g=
+github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c=
+github.com/containerd/containerd v1.5.10/go.mod h1:fvQqCfadDGga5HZyn3j4+dx56qj2I9YwBrlSdalvJYQ=
+github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
+github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
+github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
+github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo=
+github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y=
+github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ=
+github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM=
+github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
+github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
+github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=
+github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=
+github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4=
+github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4=
+github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU=
+github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk=
+github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
+github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
+github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g=
+github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok=
+github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok=
+github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0=
+github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA=
+github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow=
+github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms=
+github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c=
+github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY=
+github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY=
+github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
+github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
+github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8=
+github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=
+github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=
+github.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ=
+github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
+github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk=
+github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg=
+github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s=
+github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw=
+github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y=
+github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY=
+github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY=
+github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY=
+github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
+github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
+github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
+github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM=
+github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8=
+github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc=
+github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4=
+github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
+github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
+github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
+github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
+github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
+github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
+github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
+github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=
+github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=
+github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
+github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
+github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/dhui/dktest v0.3.7/go.mod h1:nYMOkafiA07WchSwKnKFUSbGMb2hMm5DrCGiXYG6gwM=
+github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
+github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
+github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v20.10.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
+github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
+github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
+github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
+github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
+github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
+github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
+github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
+github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
+github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
+github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
+github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
+github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
+github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
+github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
+github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
+github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
+github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
+github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
+github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
+github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
+github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw=
+github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
+github.com/gabriel-vasile/mimetype v1.3.1/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
+github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
+github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
+github.com/getkin/kin-openapi v0.94.1-0.20220401165309-136a868a30c2/go.mod h1:LWZfzOd7PRy8GJ1dJ6mCU6tNdSfOwRac1BUPam4aw6Q=
+github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
+github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
+github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
+github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
+github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
+github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
+github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
+github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
+github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
+github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
+github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
+github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
+github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
+github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
+github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
+github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
+github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
+github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
+github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=
+github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
+github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
+github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs=
+github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
+github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
+github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk=
+github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28=
+github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo=
+github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk=
+github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw=
+github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360=
+github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg=
+github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE=
+github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM=
+github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8=
+github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
+github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
+github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
+github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
+github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
+github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
+github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
+github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY=
+github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
+github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
+github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
+github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU=
+github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang-migrate/migrate/v4 v4.15.1/go.mod h1:/CrBenUbcDqsW29jGTR/XFqCfVi/Y6mHXlooCcSOJMQ=
+github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
+github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
+github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-github/v35 v35.2.0/go.mod h1:s0515YVTI+IMrDoy9Y4pHt9ShGpzHvHO8rZ7L7acgvs=
+github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210715191844-86eeefc3e471/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
+github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
+github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
+github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
+github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
+github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
+github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
+github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
+github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
+github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=
+github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
+github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
+github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
+github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
+github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
+github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
+github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
+github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
+github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+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/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
+github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
+github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
+github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
+github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
+github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
+github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/hyperledger/firefly v1.0.0-rc.4.0.20220419045021-4e8daade6f4d h1:+arnJUkOhynylO7qN6Q/FW3i6/KrPvUxELXHbaCxzTk=
+github.com/hyperledger/firefly v1.0.0-rc.4.0.20220419045021-4e8daade6f4d/go.mod h1:ZQtNj2FjfqK2UdQWiQphDewGbLWMhx+G98XYALKzPy0=
+github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
+github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
+github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
+github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
+github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
+github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
+github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
+github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
+github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
+github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
+github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
+github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
+github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
+github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
+github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
+github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
+github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
+github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
+github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgproto3/v2 v2.0.7/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
+github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
+github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
+github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
+github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
+github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
+github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
+github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
+github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
+github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
+github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
+github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
+github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
+github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
+github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
+github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA=
+github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jarcoal/httpmock v1.1.0 h1:F47ChZj1Y2zFsCXxNkBPwNNKnAyOATcdQibk0qEdVCE=
+github.com/jarcoal/httpmock v1.1.0/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
+github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
+github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
+github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
+github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
+github.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg=
+github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
+github.com/karlseguin/ccache v2.0.3+incompatible/go.mod h1:CM9tNPzT6EdRh14+jiW8mEF9mkNZuuE51qmgGYUB93w=
+github.com/karlseguin/expect v1.0.8/go.mod h1:lXdI8iGiQhmzpnnmU/EGA60vqKs8NbRNFnhhrJGoD5g=
+github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
+github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
+github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
+github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
+github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4=
+github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
+github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
+github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
+github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
+github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
+github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
+github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
+github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
+github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
+github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
+github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
+github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
+github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
+github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
+github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
+github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
+github.com/microcosm-cc/bluemonday v1.0.16 h1:kHmAq2t7WPWLjiGvzKa5o3HzSfahUKiOq7fAPUiMNIc=
+github.com/microcosm-cc/bluemonday v1.0.16/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
+github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
+github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
+github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
+github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
+github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
+github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
+github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
+github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
+github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
+github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ=
+github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=
+github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
+github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/mutecomm/go-sqlcipher/v4 v4.4.0/go.mod h1:PyN04SaWalavxRGH9E8ZftG6Ju7rsPrGmQRjrEaVpiY=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
+github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA=
+github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
+github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba/go.mod h1:ncO5VaFWh0Nrt+4KT4mOZboaczBZcLuHrG+/sUeP8gI=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.16.1 h1:foqVmeWDD6yYpK+Yz3fHyNIxFYNxswxqNFjSKe+vI54=
+github.com/onsi/ginkgo v1.16.1/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
+github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
+github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
+github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
+github.com/onsi/gomega v1.11.0 h1:+CqWgvj0OZycCaqclBD1pxKHAU+tOkHmQIWvDHq2aug=
+github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg=
+github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0=
+github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0=
+github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs=
+github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE=
+github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo=
+github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8=
+github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
+github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
+github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
+github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
+github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
+github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
+github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
+github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
+github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
+github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
+github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
+github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
+github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
+github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
+github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
+github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
+github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
+github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
+github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
+github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
+github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
+github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/qeesung/image2ascii v1.0.1/go.mod h1:kZKhyX0h2g/YXa/zdJR3JnLnJ8avHjZ3LrvEKSYyAyU=
+github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U=
+github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
+github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
+github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
+github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
+github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
+github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
+github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM=
+github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE=
+github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
+github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
+github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
+github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
+github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/snowflakedb/gosnowflake v1.6.3/go.mod h1:6hLajn6yxuJ4xUHZegMekpq9rnQbGJ7TMwXjgTmA6lg=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
+github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
+github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
+github.com/spf13/afero v1.7.1 h1:F37zV8E8RLstLpZ0RUGK2NGg1X57y6/B0Eg6S8oqdoA=
+github.com/spf13/afero v1.7.1/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
+github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
+github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0=
+github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
+github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
+github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
+github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM=
+github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk=
+github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU=
+github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
+github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
+github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
+github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
+github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
+github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
+github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w=
+github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
+github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
+github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
+github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=
+github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
+github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
+github.com/wayneashleyberry/terminal-dimensions v1.0.0/go.mod h1:PW2XrtV6KmKOPhuf7wbtcmw1/IFnC39mryRET2XbxeE=
+github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
+github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
+github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM=
+github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
+github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
+github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs=
+github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
+github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
+github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
+github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
+github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
+github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
+github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
+github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
+gitlab.com/hfuss/mux-prometheus v0.0.4/go.mod h1:4dALqvZzJisEAII64a6zhtdDEfvs+BjemTynBDWuRK0=
+gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
+go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg=
+go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
+go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
+go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs=
+go.mongodb.org/mongo-driver v1.7.0/go.mod h1:Q4oFMbo1+MSNqICAdYMlC/zSTrwCogR4R8NzkI+yfU8=
+go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
+go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
+golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/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-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
+golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M=
+golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
+golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190225153610-fe579d43d832/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+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.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
+golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211013171255-e13a2654a71e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM=
+golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210818153620-00dd8d7831e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
+golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
+gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
+gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
+gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
+gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
+gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
+google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
+google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
+google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
+google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
+google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
+google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
+google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
+google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
+google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
+google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
+google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
+google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
+google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
+google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
+google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
+google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=
+google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
+google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210207032614-bba0dbe2a9ea/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
+google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
+google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
+google.golang.org/genproto v0.0.0-20210630183607-d20f26d13c79/go.mod h1:yiaVoXHpRzHGyxV3o4DktVWY4mSUErTKaeEOq6C3t3U=
+google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
+google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
+google.golang.org/genproto v0.0.0-20210721163202-f1cecdd8b78a/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
+google.golang.org/genproto v0.0.0-20210726143408-b02e89920bf0/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
+google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
+google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
+google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
+google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211013025323-ce878158c4d4/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
+google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
+google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
+google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
+google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
+google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
+google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
+gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI=
+gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg=
+gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
+gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
+gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
+gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
+gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo=
+k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ=
+k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8=
+k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
+k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
+k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc=
+k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU=
+k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM=
+k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q=
+k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y=
+k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k=
+k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0=
+k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk=
+k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI=
+k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM=
+k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM=
+k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI=
+k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI=
+k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc=
+k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
+k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
+k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
+k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
+k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
+k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
+modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg=
+modernc.org/cc/v3 v3.32.4/go.mod h1:0R6jl1aZlIl2avnYfbfHBS1QB6/f+16mihBObaBC878=
+modernc.org/ccgo/v3 v3.9.2/go.mod h1:gnJpy6NIVqkETT+L5zPsQFj7L2kkhfPMzOghRNv/CFo=
+modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8=
+modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw=
+modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8=
+modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=
+modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
+modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM=
+modernc.org/libc v1.7.13-0.20210308123627-12f642a52bb8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
+modernc.org/libc v1.9.5/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
+modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8=
+modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
+modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
+modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
+modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
+modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
+modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY=
+modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k=
+modernc.org/sqlite v1.10.6/go.mod h1:Z9FEjUtZP4qFEg6/SiADg9XCER7aYy9a/j7Pg9P7CPs=
+modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
+modernc.org/tcl v1.5.2/go.mod h1:pmJYOLgpiys3oI4AeAafkcUfE+TKKilminxNyU/+Zlo=
+modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=
+modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=
+modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
+sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
+sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
+sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
+sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
+sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
diff --git a/images/firefly_transaction_manager.jpg b/images/firefly_transaction_manager.jpg
new file mode 100644
index 00000000..d7b9c7f5
Binary files /dev/null and b/images/firefly_transaction_manager.jpg differ
diff --git a/internal/confirmations/confirmations.go b/internal/confirmations/confirmations.go
new file mode 100644
index 00000000..944957ac
--- /dev/null
+++ b/internal/confirmations/confirmations.go
@@ -0,0 +1,570 @@
+// 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 confirmations
+
+import (
+ "context"
+ "fmt"
+ "sort"
+ "strconv"
+ "time"
+
+ lru "github.com/hashicorp/golang-lru"
+ "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig"
+ "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi"
+ "github.com/hyperledger/firefly/pkg/config"
+ "github.com/hyperledger/firefly/pkg/fftypes"
+ "github.com/hyperledger/firefly/pkg/i18n"
+ "github.com/hyperledger/firefly/pkg/log"
+)
+
+// Manager listens to the blocks on the chain, and attributes confirmations to
+// pending events. Once those events meet a threshold they are considered final and
+// dispatched to the relevant listener.
+type Manager interface {
+ Notify(n *Notification) error
+ Start()
+ Stop()
+}
+
+type NotificationType int
+
+const (
+ NewEventLog NotificationType = iota
+ RemovedEventLog
+ NewTransaction
+ RemovedTransaction
+ StopStream
+)
+
+type Notification struct {
+ NotificationType NotificationType
+ Event *EventInfo
+ Transaction *TransactionInfo
+ StoppedStream *StoppedStreamInfo
+}
+
+type EventInfo struct {
+ StreamID string
+ BlockHash string
+ BlockNumber uint64
+ TransactionHash string
+ TransactionIndex uint64
+ LogIndex uint64
+ Confirmed func(confirmations []BlockInfo)
+}
+
+type TransactionInfo struct {
+ BlockHash string
+ BlockNumber uint64
+ TransactionHash string
+ Confirmed func(confirmations []BlockInfo)
+}
+
+type StoppedStreamInfo struct {
+ StreamID string
+ Completed chan struct{}
+}
+
+type BlockInfo struct {
+ BlockNumber uint64
+ BlockHash string
+ ParentHash string
+}
+
+type blockConfirmationManager struct {
+ ctx context.Context
+ cancelFunc func()
+ blockListenerID string
+ blockListenerStale bool
+ connectorAPI ffcapi.API
+ requiredConfirmations int
+ pollingInterval time.Duration
+ blockCache *lru.Cache
+ bcmNotifications chan *Notification
+ highestBlockSeen uint64
+ pending map[string]*pendingItem
+ done chan struct{}
+}
+
+func NewBlockConfirmationManager(ctx context.Context, connectorAPI ffcapi.API) (Manager, error) {
+ var err error
+ bcm := &blockConfirmationManager{
+ connectorAPI: connectorAPI,
+ requiredConfirmations: config.GetInt(tmconfig.ConfirmationsRequired),
+ pollingInterval: config.GetDuration(tmconfig.ConfirmationsBlockPollingInterval),
+ blockListenerStale: true,
+ bcmNotifications: make(chan *Notification, config.GetInt(tmconfig.ConfirmationsNotificationQueueLength)),
+ pending: make(map[string]*pendingItem),
+ }
+ bcm.ctx, bcm.cancelFunc = context.WithCancel(ctx)
+ bcm.blockCache, err = lru.New(config.GetInt(tmconfig.ConfirmationsBlockCacheSize))
+ if err != nil {
+ return nil, i18n.WrapError(bcm.ctx, err, tmmsgs.MsgCacheInitFail)
+ }
+ return bcm, nil
+}
+
+type pendingType int
+
+const (
+ pendingTypeEvent pendingType = iota
+ pendingTypeTransaction
+)
+
+// pendingItem could be a specific event that has been detected, but not confirmed yet.
+// Or it could be a transaction
+type pendingItem struct {
+ pType pendingType
+ added time.Time
+ confirmations []*BlockInfo
+ confirmed func(confirmations []BlockInfo)
+ transactionHash string
+ streamID string // events only
+ blockHash string // can be notified of changes to this for receipts
+ blockNumber uint64 // known at creation time for event logs
+ transactionIndex uint64 // known at creation time for event logs
+ logIndex uint64 // events only
+}
+
+func (pi *pendingItem) getKey() string {
+ switch pi.pType {
+ case pendingTypeEvent:
+ // For events they are identified by their hash, blockNumber, transactionIndex and logIndex
+ // If any of those change, it's a new new event - and as such we should get informed of it separately by the blockchain connector.
+ return fmt.Sprintf("Event[%s]:th=%s,bh=%s,bn=%d,ti=%d,li=%d", pi.streamID, pi.transactionHash, pi.blockHash, pi.blockNumber, pi.transactionIndex, pi.logIndex)
+ case pendingTypeTransaction:
+ // For transactions, it's simply the transaction hash that identifies it. It can go into any block
+ return fmt.Sprintf("TX:th=%s", pi.transactionHash)
+ default:
+ panic("invalid pending item type")
+ }
+}
+
+func (pi *pendingItem) copyConfirmations() []BlockInfo {
+ copy := make([]BlockInfo, len(pi.confirmations))
+ for i, c := range pi.confirmations {
+ copy[i] = *c
+ }
+ return copy
+}
+
+func (n *Notification) eventPendingItem() *pendingItem {
+ return &pendingItem{
+ pType: pendingTypeEvent,
+ blockNumber: n.Event.BlockNumber,
+ blockHash: n.Event.BlockHash,
+ streamID: n.Event.StreamID,
+ transactionHash: n.Event.TransactionHash,
+ transactionIndex: n.Event.TransactionIndex,
+ logIndex: n.Event.LogIndex,
+ confirmed: n.Event.Confirmed,
+ }
+}
+
+func (n *Notification) transactionPendingItem() *pendingItem {
+ return &pendingItem{
+ pType: pendingTypeTransaction,
+ blockNumber: n.Transaction.BlockNumber,
+ blockHash: n.Transaction.BlockHash,
+ transactionHash: n.Transaction.TransactionHash,
+ confirmed: n.Transaction.Confirmed,
+ }
+}
+
+type pendingItems []*pendingItem
+
+func (pi pendingItems) Len() int { return len(pi) }
+func (pi pendingItems) Swap(i, j int) { pi[i], pi[j] = pi[j], pi[i] }
+func (pi pendingItems) Less(i, j int) bool {
+ // At the point we emit the confirmations, we ensure to sort them by:
+ // - Block number
+ // - Transaction index within the block
+ // - Log index within the transaction (only for events)
+ return pi[i].blockNumber < pi[j].blockNumber ||
+ (pi[i].blockNumber == pi[j].blockNumber && (pi[i].transactionIndex < pi[j].transactionIndex ||
+ (pi[i].transactionIndex == pi[j].transactionIndex && pi[i].logIndex < pi[j].logIndex)))
+}
+
+func (bcm *blockConfirmationManager) Start() {
+ bcm.done = make(chan struct{})
+ go bcm.confirmationsListener()
+}
+
+func (bcm *blockConfirmationManager) Stop() {
+ bcm.cancelFunc()
+ <-bcm.done
+}
+
+// Notify is used to notify the confirmation manager of detection of a new logEntry addition or removal
+func (bcm *blockConfirmationManager) Notify(n *Notification) error {
+ switch n.NotificationType {
+ case NewEventLog, RemovedEventLog:
+ if n.Event == nil || n.Event.StreamID == "" || n.Event.TransactionHash == "" || n.Event.BlockHash == "" {
+ return i18n.NewError(bcm.ctx, tmmsgs.MsgInvalidConfirmationRequest, n)
+ }
+ case NewTransaction, RemovedTransaction:
+ if n.Transaction == nil || n.Transaction.TransactionHash == "" || n.Transaction.BlockHash == "" {
+ return i18n.NewError(bcm.ctx, tmmsgs.MsgInvalidConfirmationRequest, n)
+ }
+ case StopStream:
+ if n.StoppedStream == nil || n.StoppedStream.Completed == nil {
+ return i18n.NewError(bcm.ctx, tmmsgs.MsgInvalidConfirmationRequest, n)
+ }
+ }
+ select {
+ case bcm.bcmNotifications <- n:
+ case <-bcm.ctx.Done():
+ log.L(bcm.ctx).Debugf("Shut down while queuing notification")
+ return nil
+ }
+ return nil
+}
+
+func (bcm *blockConfirmationManager) createBlockListener() error {
+ res, _, err := bcm.connectorAPI.CreateBlockListener(bcm.ctx, &ffcapi.CreateBlockListenerRequest{})
+ if err != nil {
+ return err
+ }
+ bcm.blockListenerStale = false
+ bcm.blockListenerID = res.ListenerID
+ log.L(bcm.ctx).Infof("Created blockListener: %s", bcm.blockListenerID)
+ return err
+}
+
+func (bcm *blockConfirmationManager) pollBlockListener() ([]string, error) {
+ ctx, cancel := context.WithTimeout(bcm.ctx, 30*time.Second)
+ defer cancel()
+ res, reason, err := bcm.connectorAPI.GetNewBlockHashes(ctx, &ffcapi.GetNewBlockHashesRequest{
+ ListenerID: bcm.blockListenerID,
+ })
+ if err != nil {
+ if reason == ffcapi.ErrorReasonNotFound {
+ bcm.blockListenerStale = true
+ }
+ return nil, err
+ }
+ return res.BlockHashes, nil
+}
+
+func (bcm *blockConfirmationManager) addToCache(blockInfo *BlockInfo) {
+ bcm.blockCache.Add(blockInfo.BlockHash, blockInfo)
+ bcm.blockCache.Add(strconv.FormatUint(blockInfo.BlockNumber, 10), blockInfo)
+}
+
+func (bcm *blockConfirmationManager) getBlockByHash(blockHash string) (*BlockInfo, error) {
+ cached, ok := bcm.blockCache.Get(blockHash)
+ if ok {
+ return cached.(*BlockInfo), nil
+ }
+
+ res, reason, err := bcm.connectorAPI.GetBlockInfoByHash(bcm.ctx, &ffcapi.GetBlockInfoByHashRequest{
+ BlockHash: blockHash,
+ })
+ if err != nil {
+ if reason == ffcapi.ErrorReasonNotFound {
+ return nil, nil
+ }
+ return nil, err
+ }
+ blockInfo := &BlockInfo{
+ BlockNumber: res.BlockNumber.Uint64(),
+ BlockHash: res.BlockHash,
+ ParentHash: res.ParentHash,
+ }
+ log.L(bcm.ctx).Debugf("Downloaded block header by hash: %d / %s parent=%s", blockInfo.BlockNumber, blockInfo.BlockHash, blockInfo.ParentHash)
+
+ bcm.addToCache(blockInfo)
+ return blockInfo, nil
+}
+
+func (bcm *blockConfirmationManager) getBlockByNumber(blockNumber uint64, expectedParentHash string) (*BlockInfo, error) {
+ cached, ok := bcm.blockCache.Get(strconv.FormatUint(blockNumber, 10))
+ if ok {
+ blockInfo := cached.(*BlockInfo)
+ if blockInfo.ParentHash != expectedParentHash {
+ // Treat a missing block, or a mismatched block, both as a cache miss and query the node
+ log.L(bcm.ctx).Debugf("Block cache miss due to parent hash mismatch: %d / %s parent=%s required=%s ", blockInfo.BlockNumber, blockInfo.BlockHash, blockInfo.ParentHash, expectedParentHash)
+ } else {
+ return blockInfo, nil
+ }
+ }
+ res, reason, err := bcm.connectorAPI.GetBlockInfoByNumber(bcm.ctx, &ffcapi.GetBlockInfoByNumberRequest{
+ BlockNumber: fftypes.NewFFBigInt(int64(blockNumber)),
+ })
+ if err != nil {
+ if reason == ffcapi.ErrorReasonNotFound {
+ return nil, nil
+ }
+ return nil, err
+ }
+ blockInfo := &BlockInfo{
+ BlockNumber: res.BlockNumber.Uint64(),
+ BlockHash: res.BlockHash,
+ ParentHash: res.ParentHash,
+ }
+ log.L(bcm.ctx).Debugf("Downloaded block header by number: %d / %s parent=%s", blockInfo.BlockNumber, blockInfo.BlockHash, blockInfo.ParentHash)
+
+ bcm.addToCache(blockInfo)
+ return blockInfo, nil
+}
+
+func (bcm *blockConfirmationManager) confirmationsListener() {
+ defer close(bcm.done)
+ pollTimer := time.NewTimer(0)
+ notifications := make([]*Notification, 0)
+ for {
+ popped := false
+ for !popped {
+ select {
+ case <-pollTimer.C:
+ popped = true
+ case <-bcm.ctx.Done():
+ log.L(bcm.ctx).Debugf("Block confirmation listener stopping")
+ return
+ case notification := <-bcm.bcmNotifications:
+ if notification.NotificationType == StopStream {
+ // Handle stream notifications immediately
+ bcm.streamStopped(notification)
+ } else {
+ // Defer until after we've got new logs
+ notifications = append(notifications, notification)
+ }
+ }
+ }
+ pollTimer = time.NewTimer(bcm.pollingInterval)
+
+ // Setup a blockListener if we're missing one
+ if bcm.blockListenerStale {
+ if err := bcm.createBlockListener(); err != nil {
+ log.L(bcm.ctx).Errorf("Failed to create blockListener: %s", err)
+ continue
+ }
+
+ if err := bcm.walkChain(); err != nil {
+ log.L(bcm.ctx).Errorf("Failed to create walk chain after restoring blockListener: %s", err)
+ continue
+ }
+ }
+
+ // Do the poll
+ blockHashes, err := bcm.pollBlockListener()
+ if err != nil {
+ log.L(bcm.ctx).Errorf("Failed to retrieve blocks from blockListener: %s", err)
+ continue
+ }
+
+ // Process each new block
+ bcm.processBlockHashes(blockHashes)
+
+ // Process any new notifications - we do this at the end, so it can benefit
+ // from knowing the latest highestBlockSeen
+ if err := bcm.processNotifications(notifications); err != nil {
+ log.L(bcm.ctx).Errorf("Failed processing notifications: %s", err)
+ continue
+ }
+
+ // Clear the notifications array now we've processed them (we keep the slice memory)
+ notifications = notifications[:0]
+
+ }
+
+}
+
+func (bcm *blockConfirmationManager) processNotifications(notifications []*Notification) error {
+
+ var newItem *pendingItem
+ for _, n := range notifications {
+ switch n.NotificationType {
+ case NewEventLog:
+ newItem = n.eventPendingItem()
+ case NewTransaction:
+ newItem = n.transactionPendingItem()
+ case RemovedEventLog:
+ bcm.removeItem(n.eventPendingItem())
+ case RemovedTransaction:
+ bcm.removeItem(n.transactionPendingItem())
+ default:
+ // Note that streamStopped is handled in the polling loop directly
+ log.L(bcm.ctx).Warnf("Unexpected notification type: %d", n.NotificationType)
+ }
+ }
+
+ if newItem != nil {
+ bcm.addOrReplaceItem(newItem)
+ if err := bcm.walkChainForItem(newItem); err != nil {
+ return err
+ }
+
+ }
+
+ return nil
+}
+
+// streamStopped removes all pending work for a given stream, and notifies once done
+func (bcm *blockConfirmationManager) streamStopped(notification *Notification) {
+ for eventKey, pending := range bcm.pending {
+ if pending.streamID == notification.StoppedStream.StreamID {
+ delete(bcm.pending, eventKey)
+ }
+ }
+ close(notification.StoppedStream.Completed)
+}
+
+// addEvent is called by the goroutine on receipt of a new event/transaction notification
+func (bcm *blockConfirmationManager) addOrReplaceItem(pending *pendingItem) {
+ pending.added = time.Now()
+ pending.confirmations = make([]*BlockInfo, 0, bcm.requiredConfirmations)
+ eventKey := pending.getKey()
+ bcm.pending[eventKey] = pending
+ log.L(bcm.ctx).Infof("Added pending item %s", eventKey)
+}
+
+// removeEvent is called by the goroutine on receipt of a remove event notification
+func (bcm *blockConfirmationManager) removeItem(pending *pendingItem) {
+ eventKey := pending.getKey()
+ log.L(bcm.ctx).Infof("Removing stale item %s", eventKey)
+ delete(bcm.pending, eventKey)
+}
+
+func (bcm *blockConfirmationManager) processBlockHashes(blockHashes []string) {
+ if len(blockHashes) > 0 {
+ log.L(bcm.ctx).Debugf("New block notifications %v", blockHashes)
+ }
+
+ for _, blockHash := range blockHashes {
+ // Get the block header
+ block, err := bcm.getBlockByHash(blockHash)
+ if err != nil || block == nil {
+ log.L(bcm.ctx).Errorf("Failed to retrieve block %s: %v", blockHash, err)
+ continue
+ }
+
+ // Process the block for confirmations
+ bcm.processBlock(block)
+
+ // Update the highest block (used for efficiency in chain walks)
+ if block.BlockNumber > bcm.highestBlockSeen {
+ bcm.highestBlockSeen = block.BlockNumber
+ }
+ }
+}
+
+func (bcm *blockConfirmationManager) processBlock(block *BlockInfo) {
+
+ // Go through all the events, adding in the confirmations, and popping any out
+ // that have reached their threshold. Then drop the log before logging/processing them.
+ blockNumber := block.BlockNumber
+ var confirmed pendingItems
+ for eventKey, pending := range bcm.pending {
+ // The block might appear at any point in the confirmation list
+ expectedParentHash := pending.blockHash
+ expectedBlockNumber := pending.blockNumber + 1
+ for i := 0; i < (len(pending.confirmations) + 1); i++ {
+ if block.ParentHash == expectedParentHash && blockNumber == expectedBlockNumber {
+ pending.confirmations = append(pending.confirmations[0:i], block)
+ log.L(bcm.ctx).Infof("Confirmation %d at block %d / %s item=%s",
+ len(pending.confirmations), block.BlockNumber, block.BlockHash, pending.getKey())
+ break
+ }
+ if i < len(pending.confirmations) {
+ expectedParentHash = pending.confirmations[i].BlockHash
+ }
+ expectedBlockNumber++
+ }
+ if len(pending.confirmations) >= bcm.requiredConfirmations {
+ delete(bcm.pending, eventKey)
+ confirmed = append(confirmed, pending)
+ }
+ }
+
+ // Sort the events to dispatch them in the correct order
+ sort.Sort(confirmed)
+ for _, c := range confirmed {
+ bcm.dispatchConfirmed(c)
+ }
+
+}
+
+// dispatchConfirmed drive the event stream for any events that are confirmed, and prunes the state
+func (bcm *blockConfirmationManager) dispatchConfirmed(item *pendingItem) {
+ eventKey := item.getKey()
+ log.L(bcm.ctx).Infof("Confirmed with %d confirmations event=%s", len(item.confirmations), eventKey)
+ delete(bcm.pending, eventKey)
+ item.confirmed(item.copyConfirmations() /* a safe copy outside of our cache */)
+}
+
+// walkChain goes through each event and sees whether it's valid,
+// purging any stale confirmations - or whole events if the blockListener is invalid
+// We do this each time our blockListener is invalidated
+func (bcm *blockConfirmationManager) walkChain() error {
+
+ // Grab a copy of all the pending in order
+ pendingItems := make(pendingItems, 0, len(bcm.pending))
+ for _, pending := range bcm.pending {
+ pendingItems = append(pendingItems, pending)
+ }
+ sort.Sort(pendingItems)
+
+ // Go through them in order - using the cache for efficiency
+ for _, pending := range pendingItems {
+ if err := bcm.walkChainForItem(pending); err != nil {
+ return err
+ }
+ }
+
+ return nil
+
+}
+
+func (bcm *blockConfirmationManager) walkChainForItem(pending *pendingItem) (err error) {
+
+ eventKey := pending.getKey()
+
+ blockNumber := pending.blockNumber + 1
+ expectedParentHash := pending.blockHash
+ pending.confirmations = pending.confirmations[:0]
+ for {
+ // No point in walking past the highest block we've seen via the notifier
+ if bcm.highestBlockSeen > 0 && blockNumber > bcm.highestBlockSeen {
+ log.L(bcm.ctx).Debugf("Waiting for confirmation after block %d event=%s", bcm.highestBlockSeen, eventKey)
+ return nil
+ }
+ block, err := bcm.getBlockByNumber(blockNumber, expectedParentHash)
+ if err != nil {
+ return err
+ }
+ if block == nil {
+ log.L(bcm.ctx).Infof("Block %d unavailable walking chain event=%s", blockNumber, eventKey)
+ return nil
+ }
+ candidateParentHash := block.ParentHash
+ if candidateParentHash != expectedParentHash {
+ log.L(bcm.ctx).Infof("Block mismatch in confirmations: block=%d expected=%s actual=%s confirmations=%d event=%s", blockNumber, expectedParentHash, candidateParentHash, len(pending.confirmations), eventKey)
+ return nil
+ }
+ pending.confirmations = append(pending.confirmations, block)
+ if len(pending.confirmations) >= bcm.requiredConfirmations {
+ // Ready for dispatch
+ bcm.dispatchConfirmed(pending)
+ return nil
+ }
+ blockNumber++
+ expectedParentHash = block.BlockHash
+ }
+
+}
diff --git a/internal/confirmations/confirmations_test.go b/internal/confirmations/confirmations_test.go
new file mode 100644
index 00000000..595375ca
--- /dev/null
+++ b/internal/confirmations/confirmations_test.go
@@ -0,0 +1,992 @@
+// Copyright 2019 Kaleido
+
+// 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 confirmations
+
+import (
+ "context"
+ "fmt"
+ "sort"
+ "testing"
+
+ "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig"
+ "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi"
+ "github.com/hyperledger/firefly/pkg/config"
+ "github.com/hyperledger/firefly/pkg/fftypes"
+ "github.com/sirupsen/logrus"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+)
+
+func newTestBlockConfirmationManager(t *testing.T, enabled bool) (*blockConfirmationManager, *ffcapimocks.API) {
+ tmconfig.Reset()
+ config.Set(tmconfig.ConfirmationsRequired, 3)
+ config.Set(tmconfig.ConfirmationsBlockPollingInterval, "10ms")
+ config.Set(tmconfig.ConfirmationsNotificationQueueLength, 1)
+ return newTestBlockConfirmationManagerCustomConfig(t)
+}
+
+func newTestBlockConfirmationManagerCustomConfig(t *testing.T) (*blockConfirmationManager, *ffcapimocks.API) {
+ logrus.SetLevel(logrus.DebugLevel)
+ mca := &ffcapimocks.API{}
+ bcm, err := NewBlockConfirmationManager(context.Background(), mca)
+ assert.NoError(t, err)
+ return bcm.(*blockConfirmationManager), mca
+}
+
+func TestBCMInitError(t *testing.T) {
+ tmconfig.Reset()
+ config.Set(tmconfig.ConfirmationsBlockCacheSize, -1)
+ mca := &ffcapimocks.API{}
+ _, err := NewBlockConfirmationManager(context.Background(), mca)
+ assert.Regexp(t, "FF201015", err)
+}
+
+func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) {
+ bcm, mca := newTestBlockConfirmationManager(t, true)
+
+ confirmed := make(chan []BlockInfo, 1)
+ eventToConfirm := &EventInfo{
+ StreamID: "stream1",
+ TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7",
+ BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542",
+ BlockNumber: 1001,
+ TransactionIndex: 5,
+ LogIndex: 10,
+ Confirmed: func(confirmations []BlockInfo) {
+ confirmed <- confirmations
+ },
+ }
+ lastBlockDetected := false
+
+ // Establish the block filter
+ mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{
+ ListenerID: "listener1",
+ }, ffcapi.ErrorReason(""), nil).Once()
+
+ // First poll for changes gives nothing, but we load up the event at this point for the next round
+ mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool {
+ return r.ListenerID == "listener1"
+ })).Run(func(args mock.Arguments) {
+ bcm.Notify(&Notification{
+ NotificationType: NewEventLog,
+ Event: eventToConfirm,
+ })
+ }).Return(&ffcapi.GetNewBlockHashesResponse{
+ BlockHashes: []string{},
+ }, ffcapi.ErrorReason(""), nil).Once()
+
+ // Next time round gives a block that is in the confirmation chain, but one block ahead
+ block1003 := &BlockInfo{
+ BlockNumber: 1003,
+ BlockHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df",
+ ParentHash: "0x46210d224888265c269359529618bf2f6adb2697ff52c63c10f16a2391bdd295",
+ }
+ mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool {
+ return r.ListenerID == "listener1"
+ })).Return(&ffcapi.GetNewBlockHashesResponse{
+ BlockHashes: []string{block1003.BlockHash},
+ }, ffcapi.ErrorReason(""), nil).Once()
+
+ // The next filter gives us 1003 - which is two blocks ahead of our notified log
+ mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool {
+ return r.BlockHash == block1003.BlockHash
+ })).Return(&ffcapi.GetBlockInfoByHashResponse{
+ BlockNumber: fftypes.NewFFBigInt(int64(block1003.BlockNumber)),
+ BlockHash: block1003.BlockHash,
+ ParentHash: block1003.ParentHash,
+ }, ffcapi.ErrorReason(""), nil).Once()
+
+ // Then we should walk the chain by number to fill in 1002/1003, because our HWM is 1003
+ block1002 := &BlockInfo{
+ BlockNumber: 1002,
+ BlockHash: "0x46210d224888265c269359529618bf2f6adb2697ff52c63c10f16a2391bdd295",
+ ParentHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542",
+ }
+ mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool {
+ return r.BlockNumber.Uint64() == 1002
+ })).Return(&ffcapi.GetBlockInfoByNumberResponse{
+ BlockNumber: fftypes.NewFFBigInt(int64(block1002.BlockNumber)),
+ BlockHash: block1002.BlockHash,
+ ParentHash: block1002.ParentHash,
+ }, ffcapi.ErrorReason(""), nil).Once()
+
+ // Then we should walk the chain by number to fill in 1002, because our HWM is 1003.
+ // Note this doesn't result in any RPC calls, as we just cached the block and it matches
+
+ // Then we get notified of 1004 to complete the last confirmation
+ block1004 := &BlockInfo{
+ BlockNumber: 1004,
+ BlockHash: "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8",
+ ParentHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df",
+ }
+ mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool {
+ return r.ListenerID == "listener1"
+ })).Return(&ffcapi.GetNewBlockHashesResponse{
+ BlockHashes: []string{block1004.BlockHash},
+ }, ffcapi.ErrorReason(""), nil).Once()
+
+ // Which then gets downloaded, and should complete the confirmation
+ mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool {
+ return r.BlockHash == block1004.BlockHash
+ })).Return(&ffcapi.GetBlockInfoByHashResponse{
+ BlockNumber: fftypes.NewFFBigInt(int64(block1004.BlockNumber)),
+ BlockHash: block1004.BlockHash,
+ ParentHash: block1004.ParentHash,
+ }, ffcapi.ErrorReason(""), nil).Once()
+
+ // Subsequent calls get nothing, and blocks until close anyway
+ mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool {
+ return r.ListenerID == "listener1"
+ })).Run(func(args mock.Arguments) {
+ if lastBlockDetected {
+ <-bcm.ctx.Done()
+ }
+ }).Return(&ffcapi.GetNewBlockHashesResponse{
+ BlockHashes: []string{},
+ }, ffcapi.ErrorReason(""), nil).Maybe()
+
+ bcm.Start()
+
+ dispatched := <-confirmed
+ assert.Equal(t, []BlockInfo{
+ *block1002,
+ *block1003,
+ *block1004,
+ }, dispatched)
+
+ bcm.Stop()
+ <-bcm.done
+
+ mca.AssertExpectations(t)
+}
+
+func TestBlockConfirmationManagerE2EFork(t *testing.T) {
+ bcm, mca := newTestBlockConfirmationManager(t, true)
+
+ confirmed := make(chan []BlockInfo, 1)
+ eventToConfirm := &EventInfo{
+ StreamID: "stream1",
+ TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7",
+ BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542",
+ BlockNumber: 1001,
+ TransactionIndex: 5,
+ LogIndex: 10,
+ Confirmed: func(confirmations []BlockInfo) {
+ confirmed <- confirmations
+ },
+ }
+ lastBlockDetected := false
+
+ // Establish the block filter
+ mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{
+ ListenerID: "listener1",
+ }, ffcapi.ErrorReason(""), nil).Once()
+
+ // The next filter gives us 1002, and a first 1003 block - which will later be removed
+ block1002 := &BlockInfo{
+ BlockNumber: 1002,
+ BlockHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df",
+ ParentHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542",
+ }
+ block1003a := &BlockInfo{
+ BlockNumber: 1003,
+ BlockHash: "0x46210d224888265c269359529618bf2f6adb2697ff52c63c10f16a2391bdd295",
+ ParentHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df",
+ }
+ mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool {
+ return r.ListenerID == "listener1"
+ })).Return(&ffcapi.GetNewBlockHashesResponse{
+ BlockHashes: []string{
+ block1002.BlockHash,
+ block1003a.BlockHash,
+ },
+ }, ffcapi.ErrorReason(""), nil).Once()
+ mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool {
+ return r.BlockHash == block1002.BlockHash
+ })).Return(&ffcapi.GetBlockInfoByHashResponse{
+ BlockNumber: fftypes.NewFFBigInt(int64(block1002.BlockNumber)),
+ BlockHash: block1002.BlockHash,
+ ParentHash: block1002.ParentHash,
+ }, ffcapi.ErrorReason(""), nil).Once()
+ mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool {
+ return r.BlockHash == block1003a.BlockHash
+ })).Return(&ffcapi.GetBlockInfoByHashResponse{
+ BlockNumber: fftypes.NewFFBigInt(int64(block1003a.BlockNumber)),
+ BlockHash: block1003a.BlockHash,
+ ParentHash: block1003a.ParentHash,
+ }, ffcapi.ErrorReason(""), nil).Once()
+
+ // Then we get the final fork up to our confirmation
+ block1003b := &BlockInfo{
+ BlockNumber: 1003,
+ BlockHash: "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8",
+ ParentHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df",
+ }
+ block1004 := &BlockInfo{
+ BlockNumber: 1004,
+ BlockHash: "0x110282339db2dfe4bfd13d78375f7883048cac6bc12f8393bd080a4e263d5d21",
+ ParentHash: "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8",
+ }
+ mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool {
+ return r.ListenerID == "listener1"
+ })).Return(&ffcapi.GetNewBlockHashesResponse{
+ BlockHashes: []string{
+ block1003b.BlockHash,
+ block1004.BlockHash,
+ },
+ }, ffcapi.ErrorReason(""), nil).Once()
+ mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool {
+ return r.BlockHash == block1003b.BlockHash
+ })).Return(&ffcapi.GetBlockInfoByHashResponse{
+ BlockNumber: fftypes.NewFFBigInt(int64(block1003b.BlockNumber)),
+ BlockHash: block1003b.BlockHash,
+ ParentHash: block1003b.ParentHash,
+ }, ffcapi.ErrorReason(""), nil).Once()
+ mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool {
+ return r.BlockHash == block1004.BlockHash
+ })).Return(&ffcapi.GetBlockInfoByHashResponse{
+ BlockNumber: fftypes.NewFFBigInt(int64(block1004.BlockNumber)),
+ BlockHash: block1004.BlockHash,
+ ParentHash: block1004.ParentHash,
+ }, ffcapi.ErrorReason(""), nil).Once()
+
+ // Subsequent calls get nothing, and blocks until close anyway
+ mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool {
+ return r.ListenerID == "listener1"
+ })).Run(func(args mock.Arguments) {
+ if lastBlockDetected {
+ <-bcm.ctx.Done()
+ }
+ }).Return(&ffcapi.GetNewBlockHashesResponse{
+ BlockHashes: []string{},
+ }, ffcapi.ErrorReason(""), nil).Maybe()
+
+ bcm.Start()
+
+ bcm.Notify(&Notification{
+ NotificationType: NewEventLog,
+ Event: eventToConfirm,
+ })
+
+ dispatched := <-confirmed
+ assert.Equal(t, []BlockInfo{
+ *block1002,
+ *block1003b,
+ *block1004,
+ }, dispatched)
+
+ bcm.Stop()
+ <-bcm.done
+
+ mca.AssertExpectations(t)
+
+}
+
+func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) {
+ bcm, mca := newTestBlockConfirmationManager(t, true)
+
+ confirmed := make(chan []BlockInfo, 1)
+ txToConfirmForkA := &TransactionInfo{
+ TransactionHash: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
+ BlockHash: "0x33eb56730878a08e126f2d52b19242d3b3127dc7611447255928be91b2dda455",
+ BlockNumber: 1001,
+ Confirmed: func(confirmations []BlockInfo) {
+ assert.Fail(t, "this is not the fork we are looking for")
+ },
+ }
+ block1002a := &BlockInfo{
+ BlockNumber: 1002,
+ BlockHash: "0x46210d224888265c269359529618bf2f6adb2697ff52c63c10f16a2391bdd295",
+ ParentHash: "0x33eb56730878a08e126f2d52b19242d3b3127dc7611447255928be91b2dda455",
+ }
+ // We start with a notification for this one
+ bcm.Notify(&Notification{
+ NotificationType: NewTransaction,
+ Transaction: txToConfirmForkA,
+ })
+
+ txToConfirmForkB := &TransactionInfo{
+ TransactionHash: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
+ BlockHash: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
+ BlockNumber: 1001,
+ Confirmed: func(confirmations []BlockInfo) {
+ confirmed <- confirmations
+ },
+ }
+ block1002b := &BlockInfo{
+ BlockNumber: 1002,
+ BlockHash: "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8",
+ ParentHash: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
+ }
+ lastBlockDetected := false
+
+ // Establish the block filter
+ mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{
+ ListenerID: "listener1",
+ }, ffcapi.ErrorReason(""), nil).Once()
+
+ // The next filter gives us 1002a, which will later be removed
+ mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool {
+ return r.ListenerID == "listener1"
+ })).Return(&ffcapi.GetNewBlockHashesResponse{
+ BlockHashes: []string{
+ block1002a.BlockHash,
+ },
+ }, ffcapi.ErrorReason(""), nil).Once()
+ mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool {
+ return r.BlockHash == block1002a.BlockHash
+ })).Return(&ffcapi.GetBlockInfoByHashResponse{
+ BlockNumber: fftypes.NewFFBigInt(int64(block1002a.BlockNumber)),
+ BlockHash: block1002a.BlockHash,
+ ParentHash: block1002a.ParentHash,
+ }, ffcapi.ErrorReason(""), nil).Once().
+ Run(func(args mock.Arguments) {
+ // At this point submit the notification for the TX moving to the other fork
+ bcm.Notify(&Notification{
+ NotificationType: NewTransaction,
+ Transaction: txToConfirmForkB,
+ })
+ })
+
+ // Then we get the final fork up to our confirmation
+ block1003 := &BlockInfo{
+ BlockNumber: 1003,
+ BlockHash: "0xaf47ddbd9ba81736f808045b7fccc2179bba360573b362c82544f7360db0802e",
+ ParentHash: "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8",
+ }
+ block1004 := &BlockInfo{
+ BlockNumber: 1004,
+ BlockHash: "0x110282339db2dfe4bfd13d78375f7883048cac6bc12f8393bd080a4e263d5d21",
+ ParentHash: "0xaf47ddbd9ba81736f808045b7fccc2179bba360573b362c82544f7360db0802e",
+ }
+ mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool {
+ return r.ListenerID == "listener1"
+ })).Return(&ffcapi.GetNewBlockHashesResponse{
+ BlockHashes: []string{
+ block1003.BlockHash,
+ block1004.BlockHash,
+ },
+ }, ffcapi.ErrorReason(""), nil).Once()
+ mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool {
+ return r.BlockHash == block1003.BlockHash
+ })).Return(&ffcapi.GetBlockInfoByHashResponse{
+ BlockNumber: fftypes.NewFFBigInt(int64(block1003.BlockNumber)),
+ BlockHash: block1003.BlockHash,
+ ParentHash: block1003.ParentHash,
+ }, ffcapi.ErrorReason(""), nil).Once()
+ mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool {
+ return r.BlockHash == block1004.BlockHash
+ })).Return(&ffcapi.GetBlockInfoByHashResponse{
+ BlockNumber: fftypes.NewFFBigInt(int64(block1004.BlockNumber)),
+ BlockHash: block1004.BlockHash,
+ ParentHash: block1004.ParentHash,
+ }, ffcapi.ErrorReason(""), nil).Once()
+
+ // We will go back and ask for block 1002 again, as the hash mismatches our updated notification
+ mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool {
+ return r.BlockNumber.Uint64() == 1002
+ })).Return(&ffcapi.GetBlockInfoByNumberResponse{
+ BlockNumber: fftypes.NewFFBigInt(int64(block1002b.BlockNumber)),
+ BlockHash: block1002b.BlockHash,
+ ParentHash: block1002b.ParentHash,
+ }, ffcapi.ErrorReason(""), nil).Once()
+
+ // Subsequent calls get nothing, and blocks until close anyway
+ mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool {
+ return r.ListenerID == "listener1"
+ })).Run(func(args mock.Arguments) {
+ if lastBlockDetected {
+ <-bcm.ctx.Done()
+ }
+ }).Return(&ffcapi.GetNewBlockHashesResponse{
+ BlockHashes: []string{},
+ }, ffcapi.ErrorReason(""), nil).Maybe()
+
+ bcm.Start()
+
+ dispatched := <-confirmed
+ assert.Equal(t, []BlockInfo{
+ *block1002b,
+ *block1003,
+ *block1004,
+ }, dispatched)
+
+ bcm.Stop()
+ <-bcm.done
+
+ mca.AssertExpectations(t)
+
+}
+
+func TestBlockConfirmationManagerE2EHistoricalEvent(t *testing.T) {
+ bcm, mca := newTestBlockConfirmationManager(t, true)
+
+ confirmed := make(chan []BlockInfo, 1)
+ eventToConfirm := &EventInfo{
+ StreamID: "stream1",
+ TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7",
+ BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542",
+ BlockNumber: 1001,
+ TransactionIndex: 5,
+ LogIndex: 10,
+ Confirmed: func(confirmations []BlockInfo) {
+ confirmed <- confirmations
+ },
+ }
+
+ // Establish the block filter
+ mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{
+ ListenerID: "listener1",
+ }, ffcapi.ErrorReason(""), nil).Once()
+
+ // We don't notify of any new blocks
+ mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool {
+ return r.ListenerID == "listener1"
+ })).Return(&ffcapi.GetNewBlockHashesResponse{
+ BlockHashes: []string{},
+ }, ffcapi.ErrorReason(""), nil)
+
+ // Then we should walk the chain by number to fill in 1002/1003, because our HWM is 1003
+ block1002 := &BlockInfo{
+ BlockNumber: 1002,
+ BlockHash: "0x46210d224888265c269359529618bf2f6adb2697ff52c63c10f16a2391bdd295",
+ ParentHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542",
+ }
+ block1003 := &BlockInfo{
+ BlockNumber: 1003,
+ BlockHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df",
+ ParentHash: "0x46210d224888265c269359529618bf2f6adb2697ff52c63c10f16a2391bdd295",
+ }
+ block1004 := &BlockInfo{
+ BlockNumber: 1004,
+ BlockHash: "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8",
+ ParentHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df",
+ }
+ mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool {
+ return r.BlockNumber.Uint64() == 1002
+ })).Return(&ffcapi.GetBlockInfoByNumberResponse{
+ BlockNumber: fftypes.NewFFBigInt(int64(block1002.BlockNumber)),
+ BlockHash: block1002.BlockHash,
+ ParentHash: block1002.ParentHash,
+ }, ffcapi.ErrorReason(""), nil).Once()
+ mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool {
+ return r.BlockNumber.Uint64() == 1003
+ })).Return(&ffcapi.GetBlockInfoByNumberResponse{
+ BlockNumber: fftypes.NewFFBigInt(int64(block1003.BlockNumber)),
+ BlockHash: block1003.BlockHash,
+ ParentHash: block1003.ParentHash,
+ }, ffcapi.ErrorReason(""), nil).Once()
+ mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool {
+ return r.BlockNumber.Uint64() == 1004
+ })).Return(&ffcapi.GetBlockInfoByNumberResponse{
+ BlockNumber: fftypes.NewFFBigInt(int64(block1004.BlockNumber)),
+ BlockHash: block1004.BlockHash,
+ ParentHash: block1004.ParentHash,
+ }, ffcapi.ErrorReason(""), nil).Once()
+
+ bcm.Notify(&Notification{
+ NotificationType: NewEventLog,
+ Event: eventToConfirm,
+ })
+
+ bcm.Start()
+
+ dispatched := <-confirmed
+ assert.Equal(t, []BlockInfo{
+ *block1002,
+ *block1003,
+ *block1004,
+ }, dispatched)
+
+ bcm.Stop()
+ <-bcm.done
+
+ mca.AssertExpectations(t)
+}
+
+func TestSortPendingEvents(t *testing.T) {
+ events := pendingItems{
+ {blockNumber: 1000, transactionIndex: 10, logIndex: 2},
+ {blockNumber: 1003, transactionIndex: 0, logIndex: 10},
+ {blockNumber: 1000, transactionIndex: 5, logIndex: 5},
+ {blockNumber: 1000, transactionIndex: 10, logIndex: 0},
+ {blockNumber: 1002, transactionIndex: 0, logIndex: 0},
+ }
+ sort.Sort(events)
+ assert.Equal(t, pendingItems{
+ {blockNumber: 1000, transactionIndex: 5, logIndex: 5},
+ {blockNumber: 1000, transactionIndex: 10, logIndex: 0},
+ {blockNumber: 1000, transactionIndex: 10, logIndex: 2},
+ {blockNumber: 1002, transactionIndex: 0, logIndex: 0},
+ {blockNumber: 1003, transactionIndex: 0, logIndex: 10},
+ }, events)
+}
+
+func TestCreateBlockFilterFail(t *testing.T) {
+
+ bcm, mca := newTestBlockConfirmationManager(t, false)
+ bcm.done = make(chan struct{})
+ bcm.blockListenerID = "listener1"
+
+ mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(
+ &ffcapi.CreateBlockListenerResponse{ListenerID: "listener1"},
+ ffcapi.ErrorReason(""),
+ fmt.Errorf("pop"),
+ ).Once().Run(func(args mock.Arguments) {
+ bcm.cancelFunc()
+ })
+
+ bcm.confirmationsListener()
+
+ mca.AssertExpectations(t)
+}
+
+func TestConfirmationsListenerFailWalkingChain(t *testing.T) {
+
+ bcm, mca := newTestBlockConfirmationManager(t, false)
+ bcm.done = make(chan struct{})
+
+ n := &Notification{
+ NotificationType: NewTransaction,
+ Transaction: &TransactionInfo{
+ TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7",
+ BlockNumber: 1001,
+ BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542",
+ },
+ }
+ bcm.addOrReplaceItem(n.transactionPendingItem())
+
+ mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{
+ ListenerID: "listener1",
+ }, ffcapi.ErrorReason(""), nil).Once().Run(func(args mock.Arguments) {
+ bcm.cancelFunc()
+ })
+ mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool {
+ return r.BlockNumber.Uint64() == 1002
+ })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once()
+
+ bcm.confirmationsListener()
+
+ mca.AssertExpectations(t)
+}
+
+func TestConfirmationsListenerFailPollingBlocks(t *testing.T) {
+
+ bcm, mca := newTestBlockConfirmationManager(t, false)
+ bcm.done = make(chan struct{})
+
+ mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{
+ ListenerID: "listener1",
+ }, ffcapi.ErrorReason(""), nil).Once().Run(func(args mock.Arguments) {
+ bcm.cancelFunc()
+ })
+ mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool {
+ return r.ListenerID == "listener1"
+ })).Return(&ffcapi.GetNewBlockHashesResponse{
+ BlockHashes: []string{},
+ }, ffcapi.ErrorReason(""), fmt.Errorf("pop"))
+
+ bcm.confirmationsListener()
+
+ mca.AssertExpectations(t)
+}
+
+func TestConfirmationsListenerLostFilterReestablish(t *testing.T) {
+
+ bcm, mca := newTestBlockConfirmationManager(t, false)
+ bcm.done = make(chan struct{})
+
+ mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{
+ ListenerID: "listener1",
+ }, ffcapi.ErrorReason(""), nil).Once().Twice()
+ mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool {
+ return r.ListenerID == "listener1"
+ })).Return(&ffcapi.GetNewBlockHashesResponse{
+ BlockHashes: []string{},
+ }, ffcapi.ErrorReasonNotFound, fmt.Errorf("pop")).Once()
+ mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool {
+ return r.ListenerID == "listener1"
+ })).Return(&ffcapi.GetNewBlockHashesResponse{
+ BlockHashes: []string{},
+ }, ffcapi.ErrorReason(""), nil).Run(func(args mock.Arguments) {
+ bcm.cancelFunc()
+ })
+
+ bcm.confirmationsListener()
+
+ mca.AssertExpectations(t)
+}
+
+func TestConfirmationsListenerFailWalkingChainForNewEvent(t *testing.T) {
+
+ bcm, mca := newTestBlockConfirmationManager(t, false)
+ bcm.done = make(chan struct{})
+
+ confirmed := make(chan []BlockInfo, 1)
+ eventToConfirm := &EventInfo{
+ StreamID: "stream1",
+ TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7",
+ BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542",
+ BlockNumber: 1001,
+ TransactionIndex: 5,
+ LogIndex: 10,
+ Confirmed: func(confirmations []BlockInfo) {
+ confirmed <- confirmations
+ },
+ }
+ bcm.Notify(&Notification{
+ NotificationType: NewEventLog,
+ Event: eventToConfirm,
+ })
+
+ mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{
+ ListenerID: "listener1",
+ }, ffcapi.ErrorReason(""), nil).Once()
+ mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool {
+ return r.ListenerID == "listener1"
+ })).Return(&ffcapi.GetNewBlockHashesResponse{
+ BlockHashes: []string{},
+ }, ffcapi.ErrorReason(""), nil).Once()
+ mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool {
+ return r.BlockNumber.Uint64() == 1002
+ })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once().Run(func(args mock.Arguments) {
+ bcm.cancelFunc()
+ })
+
+ bcm.confirmationsListener()
+
+ mca.AssertExpectations(t)
+}
+
+func TestConfirmationsListenerStopStream(t *testing.T) {
+
+ bcm, mca := newTestBlockConfirmationManager(t, false)
+ bcm.done = make(chan struct{})
+
+ n := &Notification{
+ Event: &EventInfo{
+ StreamID: "stream1",
+ TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7",
+ BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542",
+ BlockNumber: 1001,
+ TransactionIndex: 5,
+ LogIndex: 10,
+ },
+ }
+ bcm.addOrReplaceItem(n.eventPendingItem())
+ completed := make(chan struct{})
+ bcm.Notify(&Notification{
+ NotificationType: StopStream,
+ StoppedStream: &StoppedStreamInfo{
+ StreamID: "stream1",
+ Completed: completed,
+ },
+ })
+
+ mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{
+ ListenerID: "listener1",
+ }, ffcapi.ErrorReason(""), nil).Maybe()
+ mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool {
+ return r.ListenerID == "listener1"
+ })).Return(&ffcapi.GetNewBlockHashesResponse{
+ BlockHashes: []string{},
+ }, ffcapi.ErrorReason(""), nil).Maybe()
+
+ bcm.Start()
+
+ <-completed
+ assert.Empty(t, bcm.pending)
+
+ bcm.Stop()
+ mca.AssertExpectations(t)
+}
+
+func TestConfirmationsRemoveEvent(t *testing.T) {
+
+ bcm, mca := newTestBlockConfirmationManager(t, false)
+ bcm.done = make(chan struct{})
+
+ eventInfo := &EventInfo{
+ StreamID: "stream1",
+ TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7",
+ BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542",
+ BlockNumber: 1001,
+ TransactionIndex: 5,
+ LogIndex: 10,
+ }
+ bcm.addOrReplaceItem((&Notification{
+ Event: eventInfo,
+ }).eventPendingItem())
+ bcm.Notify(&Notification{
+ NotificationType: RemovedEventLog,
+ Event: eventInfo,
+ })
+
+ mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{
+ ListenerID: "listener1",
+ }, ffcapi.ErrorReason(""), nil).Once()
+ mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool {
+ return r.ListenerID == "listener1"
+ })).Return(&ffcapi.GetNewBlockHashesResponse{
+ BlockHashes: []string{},
+ }, ffcapi.ErrorReason(""), nil).Once()
+ mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool {
+ return r.BlockNumber.Uint64() == 1002
+ })).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Run(func(args mock.Arguments) {
+ bcm.cancelFunc()
+ })
+
+ bcm.confirmationsListener()
+ <-bcm.done
+
+ assert.Empty(t, bcm.pending)
+ mca.AssertExpectations(t)
+}
+
+func TestConfirmationsRemoveTransaction(t *testing.T) {
+
+ bcm, mca := newTestBlockConfirmationManager(t, false)
+ bcm.done = make(chan struct{})
+
+ txInfo := &TransactionInfo{
+ TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7",
+ BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542",
+ BlockNumber: 1001,
+ }
+ bcm.addOrReplaceItem((&Notification{
+ Transaction: txInfo,
+ }).transactionPendingItem())
+ bcm.Notify(&Notification{
+ NotificationType: RemovedTransaction,
+ Transaction: txInfo,
+ })
+
+ mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{
+ ListenerID: "listener1",
+ }, ffcapi.ErrorReason(""), nil).Once()
+ mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool {
+ return r.ListenerID == "listener1"
+ })).Return(&ffcapi.GetNewBlockHashesResponse{
+ BlockHashes: []string{},
+ }, ffcapi.ErrorReason(""), nil).Once()
+ mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool {
+ return r.BlockNumber.Uint64() == 1002
+ })).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Run(func(args mock.Arguments) {
+ bcm.cancelFunc()
+ })
+
+ bcm.confirmationsListener()
+ <-bcm.done
+
+ assert.Empty(t, bcm.pending)
+ mca.AssertExpectations(t)
+}
+
+func TestWalkChainForEventBlockNotInConfirmationChain(t *testing.T) {
+
+ bcm, mca := newTestBlockConfirmationManager(t, false)
+
+ pending := (&Notification{
+ Event: &EventInfo{
+ StreamID: "stream1",
+ TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7",
+ BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542",
+ BlockNumber: 1001,
+ TransactionIndex: 5,
+ LogIndex: 10,
+ },
+ }).eventPendingItem()
+
+ mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool {
+ return r.BlockNumber.Uint64() == 1002
+ })).Return(&ffcapi.GetBlockInfoByNumberResponse{
+ BlockNumber: fftypes.NewFFBigInt(1002),
+ BlockHash: "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8",
+ ParentHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df",
+ }, ffcapi.ErrorReason(""), nil).Once()
+
+ err := bcm.walkChainForItem(pending)
+ assert.NoError(t, err)
+
+ mca.AssertExpectations(t)
+}
+
+func TestWalkChainForEventBlockLookupFail(t *testing.T) {
+
+ bcm, mca := newTestBlockConfirmationManager(t, false)
+
+ pending := (&Notification{
+ Event: &EventInfo{
+ StreamID: "stream1",
+ TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7",
+ BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542",
+ BlockNumber: 1001,
+ TransactionIndex: 5,
+ LogIndex: 10,
+ },
+ }).eventPendingItem()
+
+ mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool {
+ return r.BlockNumber.Uint64() == 1002
+ })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once()
+
+ err := bcm.walkChainForItem(pending)
+ assert.Regexp(t, "pop", err)
+
+ mca.AssertExpectations(t)
+}
+
+func TestProcessBlockHashesLookupFail(t *testing.T) {
+
+ bcm, mca := newTestBlockConfirmationManager(t, false)
+
+ blockHash := "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8"
+ mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool {
+ return r.BlockHash == blockHash
+ })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once()
+
+ bcm.processBlockHashes([]string{
+ blockHash,
+ })
+
+ mca.AssertExpectations(t)
+}
+
+func TestProcessNotificationsSwallowsUnknownType(t *testing.T) {
+
+ bcm, _ := newTestBlockConfirmationManager(t, false)
+ bcm.processNotifications([]*Notification{
+ {NotificationType: NotificationType(999)},
+ })
+}
+
+func TestGetBlockByNumberForceLookupMismatchedBlockType(t *testing.T) {
+
+ bcm, mca := newTestBlockConfirmationManager(t, false)
+
+ mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool {
+ return r.BlockNumber.Uint64() == 1002
+ })).Return(&ffcapi.GetBlockInfoByNumberResponse{
+ BlockNumber: fftypes.NewFFBigInt(1002),
+ BlockHash: "0x110282339db2dfe4bfd13d78375f7883048cac6bc12f8393bd080a4e263d5d21",
+ ParentHash: "0xaf47ddbd9ba81736f808045b7fccc2179bba360573b362c82544f7360db0802e",
+ }, ffcapi.ErrorReason(""), nil).Once()
+ mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool {
+ return r.BlockNumber.Uint64() == 1002
+ })).Return(&ffcapi.GetBlockInfoByNumberResponse{
+ BlockNumber: fftypes.NewFFBigInt(1002),
+ BlockHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7",
+ ParentHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542",
+ }, ffcapi.ErrorReason(""), nil).Once()
+
+ // Make the first call that caches
+ blockInfo, err := bcm.getBlockByNumber(1002, "0xaf47ddbd9ba81736f808045b7fccc2179bba360573b362c82544f7360db0802e")
+ assert.NoError(t, err)
+ assert.Equal(t, "0xaf47ddbd9ba81736f808045b7fccc2179bba360573b362c82544f7360db0802e", blockInfo.ParentHash)
+
+ // Make second call that is cached as parent matches
+ blockInfo, err = bcm.getBlockByNumber(1002, "0xaf47ddbd9ba81736f808045b7fccc2179bba360573b362c82544f7360db0802e")
+ assert.NoError(t, err)
+ assert.Equal(t, "0xaf47ddbd9ba81736f808045b7fccc2179bba360573b362c82544f7360db0802e", blockInfo.ParentHash)
+
+ // Make third call that does not as parent mismatched
+ blockInfo, err = bcm.getBlockByNumber(1002, "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542")
+ assert.NoError(t, err)
+ assert.Equal(t, "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", blockInfo.ParentHash)
+
+}
+
+func TestGetBlockByHashCached(t *testing.T) {
+
+ bcm, mca := newTestBlockConfirmationManager(t, false)
+
+ mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool {
+ return r.BlockHash == "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df"
+ })).Return(&ffcapi.GetBlockInfoByHashResponse{
+ BlockNumber: fftypes.NewFFBigInt(1003),
+ BlockHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df",
+ ParentHash: "0x46210d224888265c269359529618bf2f6adb2697ff52c63c10f16a2391bdd295",
+ }, ffcapi.ErrorReason(""), nil).Once()
+
+ blockInfo, err := bcm.getBlockByHash("0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df")
+ assert.NoError(t, err)
+ assert.Equal(t, "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", blockInfo.BlockHash)
+
+ // Get again cached
+ blockInfo, err = bcm.getBlockByHash("0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df")
+ assert.NoError(t, err)
+ assert.Equal(t, "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", blockInfo.BlockHash)
+
+}
+
+func TestGetBlockNotFound(t *testing.T) {
+
+ bcm, mca := newTestBlockConfirmationManager(t, false)
+
+ mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool {
+ return r.BlockHash == "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df"
+ })).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Once()
+
+ blockInfo, err := bcm.getBlockByHash("0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df")
+ assert.NoError(t, err)
+ assert.Nil(t, blockInfo)
+
+}
+
+func TestPanicBadKey(t *testing.T) {
+
+ pi := &pendingItem{
+ pType: pendingType(999),
+ }
+ assert.Panics(t, func() {
+ pi.getKey()
+ })
+
+}
+
+func TestNotificationValidation(t *testing.T) {
+
+ bcm, _ := newTestBlockConfirmationManager(t, false)
+ bcm.bcmNotifications = make(chan *Notification)
+
+ err := bcm.Notify(&Notification{
+ NotificationType: NewTransaction,
+ })
+ assert.Regexp(t, "FF201016", err)
+
+ err = bcm.Notify(&Notification{
+ NotificationType: NewEventLog,
+ })
+ assert.Regexp(t, "FF201016", err)
+
+ err = bcm.Notify(&Notification{
+ NotificationType: StopStream,
+ })
+ assert.Regexp(t, "FF201016", err)
+
+ bcm.cancelFunc()
+ err = bcm.Notify(&Notification{
+ NotificationType: NewTransaction,
+ Transaction: &TransactionInfo{
+ TransactionHash: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
+ BlockHash: "0x33eb56730878a08e126f2d52b19242d3b3127dc7611447255928be91b2dda455",
+ BlockNumber: 1001,
+ Confirmed: func(confirmations []BlockInfo) {},
+ },
+ })
+ assert.NoError(t, err)
+
+}
diff --git a/internal/manager/api.go b/internal/manager/api.go
new file mode 100644
index 00000000..4d8c6c2d
--- /dev/null
+++ b/internal/manager/api.go
@@ -0,0 +1,84 @@
+// 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 manager
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "strconv"
+
+ "github.com/gorilla/mux"
+ "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/fftm"
+ "github.com/hyperledger/firefly/pkg/fftypes"
+ "github.com/hyperledger/firefly/pkg/i18n"
+ "github.com/hyperledger/firefly/pkg/log"
+)
+
+func (m *manager) router() *mux.Router {
+ mux := mux.NewRouter()
+ mux.Path("/").Methods(http.MethodPost).Handler(http.HandlerFunc(m.apiHandler))
+ return mux
+}
+
+func (m *manager) runAPIServer() {
+ m.apiServer.ServeHTTP(m.ctx)
+}
+
+func (m *manager) validateRequest(ctx context.Context, tReq *fftm.TransactionRequest) error {
+ if tReq == nil || tReq.Headers.ID == nil || tReq.Headers.Type == "" {
+ log.L(ctx).Warnf("Invalid request: %+v", tReq)
+ return i18n.NewError(ctx, tmmsgs.MsgErrorInvalidRequest)
+ }
+ return nil
+}
+
+func (m *manager) apiHandler(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+ var tReq *fftm.TransactionRequest
+ statusCode := 200
+ err := json.NewDecoder(r.Body).Decode(&tReq)
+ if err == nil {
+ err = m.validateRequest(ctx, tReq)
+ }
+ var resBody interface{}
+ if err != nil {
+ statusCode = 400
+ } else {
+ ctx = log.WithLogField(ctx, "requestId", tReq.Headers.ID.String())
+ switch tReq.Headers.Type {
+ case fftm.RequestTypeSendTransaction:
+ resBody, err = m.sendManagedTransaction(ctx, tReq)
+ default:
+ err = i18n.NewError(ctx, tmmsgs.MsgUnsupportedRequestType, tReq.Headers.Type)
+ statusCode = 400
+ }
+ }
+ if err != nil {
+ log.L(ctx).Errorf("Request failed: %s", err)
+ resBody = &fftypes.RESTError{Error: err.Error()}
+ if statusCode < 400 {
+ statusCode = 500
+ }
+ }
+ w.Header().Set("Content-Type", "application/json")
+ resBytes, _ := json.Marshal(&resBody)
+ w.Header().Set("Content-Length", strconv.FormatInt(int64(len(resBytes)), 10))
+ w.WriteHeader(statusCode)
+ _, _ = w.Write(resBytes)
+}
diff --git a/internal/manager/api_test.go b/internal/manager/api_test.go
new file mode 100644
index 00000000..b08250fb
--- /dev/null
+++ b/internal/manager/api_test.go
@@ -0,0 +1,310 @@
+// 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 manager
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "strings"
+ "testing"
+
+ "github.com/go-resty/resty/v2"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi"
+ "github.com/hyperledger/firefly/pkg/fftypes"
+ "github.com/stretchr/testify/assert"
+)
+
+const sampleSendTX = `{
+ "headers": {
+ "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E",
+ "type": "SendTransaction"
+ },
+ "from": "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8",
+ "to": "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771",
+ "gas": 1000000,
+ "method": {
+ "inputs": [
+ {
+ "internalType":" uint256",
+ "name": "x",
+ "type": "uint256"
+ }
+ ],
+ "name":"set",
+ "outputs":[],
+ "stateMutability":"nonpayable",
+ "type":"function"
+ },
+ "params": [
+ {
+ "value": 4276993775,
+ "type": "uint256"
+ }
+ ]
+}`
+
+func testFFCAPIHandler(t *testing.T, fn func(reqType ffcapi.RequestType, b []byte) (res interface{}, status int)) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ var reqHeader ffcapi.RequestBase
+ b, err := ioutil.ReadAll(r.Body)
+ assert.NoError(t, err)
+ err = json.Unmarshal(b, &reqHeader)
+ assert.NoError(t, err)
+
+ assert.NotNil(t, reqHeader.FFCAPI.RequestID)
+ assert.Equal(t, ffcapi.VersionCurrent, reqHeader.FFCAPI.Version)
+ assert.Equal(t, ffcapi.Variant("evm"), reqHeader.FFCAPI.Variant)
+
+ res, status := fn(reqHeader.FFCAPI.RequestType, b)
+
+ b, err = json.Marshal(res)
+ assert.NoError(t, err)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(status)
+ w.Write(b)
+
+ }
+}
+
+func TestSendTransactionE2E(t *testing.T) {
+
+ txSent := make(chan struct{})
+
+ url, m, cancel := newTestManager(t,
+ testFFCAPIHandler(t, func(reqType ffcapi.RequestType, b []byte) (res interface{}, status int) {
+ status = 200
+ switch reqType {
+
+ case ffcapi.RequestTypeGetNextNonce:
+ var nonceReq ffcapi.GetNextNonceRequest
+ err := json.Unmarshal(b, &nonceReq)
+ assert.NoError(t, err)
+ assert.Equal(t, "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", nonceReq.Signer)
+ res = ffcapi.GetNextNonceResponse{
+ Nonce: fftypes.NewFFBigInt(12345),
+ }
+
+ case ffcapi.RequestTypePrepareTransaction:
+ var prepTX ffcapi.PrepareTransactionRequest
+ err := json.Unmarshal(b, &prepTX)
+ assert.NoError(t, err)
+ assert.Equal(t, "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771", prepTX.To)
+ assert.Equal(t, uint64(1000000), prepTX.Gas.Uint64())
+ assert.Equal(t, "set", prepTX.Method.JSONObject().GetString("name"))
+ assert.Len(t, prepTX.Params, 1)
+ assert.Equal(t, "4276993775", prepTX.Params[0].JSONObject().GetString("value"))
+ res = ffcapi.PrepareTransactionResponse{
+ TransactionHash: "0x106215b9c0c9372e3f541beff0cdc3cd061a26f69f3808e28fd139a1abc9d345",
+ RawTransaction: "RAW_UNSIGNED_BYTES",
+ Gas: fftypes.NewFFBigInt(2000000), // gas estimate simulation
+ }
+
+ case ffcapi.RequestTypeSendTransaction:
+ var sendTX ffcapi.SendTransactionRequest
+ err := json.Unmarshal(b, &sendTX)
+ assert.NoError(t, err)
+ assert.Equal(t, "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", sendTX.From)
+ assert.Equal(t, `223344556677`, sendTX.GasPrice.String())
+ assert.Equal(t, "RAW_UNSIGNED_BYTES", sendTX.RawTransaction)
+
+ // We're at end of job for this test
+ close(txSent)
+
+ default:
+ assert.Fail(t, fmt.Sprintf("Unexpected type: %s", reqType))
+ status = 500
+ }
+ return res, status
+ }),
+ func(w http.ResponseWriter, r *http.Request) {
+
+ },
+ )
+ defer cancel()
+
+ m.Start()
+
+ req := strings.NewReader(sampleSendTX)
+ res, err := resty.New().R().
+ SetBody(req).
+ Post(url)
+ assert.NoError(t, err)
+ assert.Equal(t, 200, res.StatusCode())
+
+ <-txSent
+
+}
+
+func TestSendInvalidRequestNoHeaders(t *testing.T) {
+
+ url, m, cancel := newTestManager(t,
+ func(w http.ResponseWriter, r *http.Request) {},
+ func(w http.ResponseWriter, r *http.Request) {},
+ )
+ defer cancel()
+ m.Start()
+
+ req := strings.NewReader(`{
+ "noHeaders": true
+ }`)
+ var errRes fftypes.RESTError
+ res, err := resty.New().R().
+ SetBody(req).
+ SetError(&errRes).
+ Post(url)
+ assert.NoError(t, err)
+ assert.Equal(t, 400, res.StatusCode())
+ assert.Regexp(t, "FF201022", errRes.Error)
+}
+
+func TestSendInvalidRequestWrongType(t *testing.T) {
+
+ url, m, cancel := newTestManager(t,
+ func(w http.ResponseWriter, r *http.Request) {},
+ func(w http.ResponseWriter, r *http.Request) {},
+ )
+ defer cancel()
+ m.Start()
+
+ req := strings.NewReader(`{
+ "headers": {
+ "id": "` + fftypes.NewUUID().String() + `",
+ "type": "wrong"
+ }
+ }`)
+ var errRes fftypes.RESTError
+ res, err := resty.New().R().
+ SetBody(req).
+ SetError(&errRes).
+ Post(url)
+ assert.NoError(t, err)
+ assert.Equal(t, 400, res.StatusCode())
+ assert.Regexp(t, "FF201023", errRes.Error)
+}
+
+func TestSendInvalidRequestFail(t *testing.T) {
+
+ url, m, cancel := newTestManager(t,
+ func(w http.ResponseWriter, r *http.Request) {
+ backendError := &fftypes.RESTError{Error: "pop"}
+ b, err := json.Marshal(&backendError)
+ assert.NoError(t, err)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(503)
+ w.Write(b)
+ },
+ func(w http.ResponseWriter, r *http.Request) {},
+ )
+ defer cancel()
+ m.Start()
+
+ req := strings.NewReader(`{
+ "headers": {
+ "id": "` + fftypes.NewUUID().String() + `",
+ "type": "SendTransaction"
+ }
+ }`)
+ var errRes fftypes.RESTError
+ res, err := resty.New().R().
+ SetBody(req).
+ SetError(&errRes).
+ Post(url)
+ assert.NoError(t, err)
+ assert.Equal(t, 500, res.StatusCode())
+ assert.Regexp(t, "FF201012", errRes.Error)
+}
+
+func TestSendTransactionPrepareFail(t *testing.T) {
+
+ url, m, cancel := newTestManager(t,
+ testFFCAPIHandler(t, func(reqType ffcapi.RequestType, b []byte) (res interface{}, status int) {
+ status = 200
+ switch reqType {
+ case ffcapi.RequestTypeGetNextNonce:
+ res = ffcapi.GetNextNonceResponse{
+ Nonce: fftypes.NewFFBigInt(12345),
+ }
+
+ case ffcapi.RequestTypePrepareTransaction:
+ res = ffcapi.ErrorResponse{
+ Error: "pop",
+ }
+ status = 500
+ }
+ return res, status
+ }),
+ func(w http.ResponseWriter, r *http.Request) {
+
+ },
+ )
+ defer cancel()
+
+ m.Start()
+
+ req := strings.NewReader(sampleSendTX)
+ res, err := resty.New().R().
+ SetBody(req).
+ Post(url)
+ assert.NoError(t, err)
+ assert.Equal(t, 500, res.StatusCode())
+
+}
+
+func TestSendTransactionUpdateFireFlyFail(t *testing.T) {
+
+ url, m, cancel := newTestManager(t,
+ testFFCAPIHandler(t, func(reqType ffcapi.RequestType, b []byte) (res interface{}, status int) {
+ status = 200
+ switch reqType {
+ case ffcapi.RequestTypeGetNextNonce:
+ res = ffcapi.GetNextNonceResponse{
+ Nonce: fftypes.NewFFBigInt(12345),
+ }
+
+ case ffcapi.RequestTypePrepareTransaction:
+ res = ffcapi.PrepareTransactionResponse{}
+ status = 200
+ }
+ return res, status
+ }),
+ func(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodPut {
+ errRes := fftypes.RESTError{Error: "pop"}
+ b, err := json.Marshal(&errRes)
+ assert.NoError(t, err)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(500)
+ w.Write(b)
+ } else {
+ w.WriteHeader(200)
+ }
+ },
+ )
+ defer cancel()
+
+ m.Start()
+
+ req := strings.NewReader(sampleSendTX)
+ res, err := resty.New().R().
+ SetBody(req).
+ Post(url)
+ assert.NoError(t, err)
+ assert.Equal(t, 500, res.StatusCode())
+
+}
diff --git a/internal/manager/ffcore.go b/internal/manager/ffcore.go
new file mode 100644
index 00000000..9f1de4bb
--- /dev/null
+++ b/internal/manager/ffcore.go
@@ -0,0 +1,120 @@
+// 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 manager
+
+import (
+ "context"
+ "fmt"
+ "net/url"
+ "strconv"
+
+ "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/fftm"
+ "github.com/hyperledger/firefly/pkg/fftypes"
+ "github.com/hyperledger/firefly/pkg/i18n"
+ "github.com/hyperledger/firefly/pkg/log"
+)
+
+// opUpdate allows us to avoid JSONObject serialization to a map before we upload our managedTXOutput
+type opUpdate struct {
+ ID *fftypes.UUID `json:"id"`
+ Status fftypes.OpStatus `json:"status"`
+ Output *fftm.ManagedTXOutput `json:"output"`
+ Error string `json:"error,omitempty"`
+}
+
+func (m *manager) writeManagedTX(ctx context.Context, opUpdate *opUpdate) error {
+ log.L(ctx).Debugf("Updating operation %s status=%s", opUpdate.ID, opUpdate.Status)
+ var errorInfo fftypes.RESTError
+ var ops []*fftypes.Operation
+ res, err := m.ffCoreClient.R().
+ SetResult(&ops).
+ SetError(&errorInfo).
+ SetBody(opUpdate).
+ SetContext(ctx).
+ Put(fmt.Sprintf("/admin/api/v1/operations/%s", opUpdate.ID))
+ if err != nil {
+ return err
+ }
+ if res.IsError() {
+ return i18n.NewError(m.ctx, tmmsgs.MsgCoreError, res.StatusCode(), errorInfo.Error)
+ }
+ return nil
+}
+
+func (m *manager) queryAndAddPending(opID *fftypes.UUID) {
+ var errorInfo fftypes.RESTError
+ var op *fftypes.Operation
+ res, err := m.ffCoreClient.R().
+ SetResult(&op).
+ SetError(&errorInfo).
+ Get(fmt.Sprintf("/admin/api/v1/operations/%s", opID))
+ if err == nil {
+ // Operations are not deleted, so we consider not found the same as any other error
+ if res.IsError() {
+ err = i18n.NewError(m.ctx, tmmsgs.MsgCoreError, res.StatusCode(), errorInfo.Error)
+ }
+ }
+ if err != nil {
+ // We logo the error, then schedule a full poll (rather than retrying here)
+ log.L(m.ctx).Errorf("Scheduling full poll due to error from core: %s", err)
+ m.requestFullScan()
+ return
+ }
+ // If the operation has been marked as success (by us or otherwise), or failed, then
+ // we can remove it. If we resolved it, then we would have cleared it up on the .
+ switch op.Status {
+ case fftypes.OpStatusSucceeded, fftypes.OpStatusFailed:
+ m.markCancelledIfTracked(op.ID)
+ case fftypes.OpStatusPending:
+ m.trackIfManaged(op)
+ }
+}
+
+func (m *manager) readOperationPage(lastOp *fftypes.Operation) ([]*fftypes.Operation, error) {
+ var errorInfo fftypes.RESTError
+ var ops []*fftypes.Operation
+ query := url.Values{
+ "sort": []string{"created"},
+ "type": m.opTypes,
+ "status": []string{string(fftypes.OpStatusPending)},
+ }
+ if lastOp != nil {
+ // For all but the 1st page, we use the last operation as the reference point.
+ // Extremely unlikely to get multiple ops withe same creation date, but not impossible
+ // so >= check, and removal of the duplicate at the end of the function.
+ query.Set("created", fmt.Sprintf(">=%d", lastOp.Created.UnixNano()))
+ query.Set("limit", strconv.FormatInt(m.fullScanPageSize+1, 10))
+ } else {
+ query.Set("limit", strconv.FormatInt(m.fullScanPageSize, 10))
+ }
+ res, err := m.ffCoreClient.R().
+ SetQueryParamsFromValues(query).
+ SetResult(&ops).
+ SetError(&errorInfo).
+ Get("/admin/api/v1/operations")
+ if err != nil {
+ return nil, i18n.WrapError(m.ctx, err, tmmsgs.MsgCoreError, -1, err)
+ }
+ if res.IsError() {
+ return nil, i18n.NewError(m.ctx, tmmsgs.MsgCoreError, res.StatusCode(), errorInfo.Error)
+ }
+ if lastOp != nil && len(ops) > 0 && ops[0].ID.Equals(lastOp.ID) {
+ ops = ops[1:]
+ }
+ return ops, nil
+}
diff --git a/internal/manager/manager.go b/internal/manager/manager.go
new file mode 100644
index 00000000..c9425f72
--- /dev/null
+++ b/internal/manager/manager.go
@@ -0,0 +1,315 @@
+// 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 manager
+
+import (
+ "context"
+ "encoding/json"
+ "sync"
+ "time"
+
+ "github.com/go-resty/resty/v2"
+ "github.com/hyperledger/firefly-transaction-manager/internal/confirmations"
+ "github.com/hyperledger/firefly-transaction-manager/internal/policyengines"
+ "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig"
+ "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/fftm"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine"
+ "github.com/hyperledger/firefly/pkg/config"
+ "github.com/hyperledger/firefly/pkg/ffresty"
+ "github.com/hyperledger/firefly/pkg/fftypes"
+ "github.com/hyperledger/firefly/pkg/httpserver"
+ "github.com/hyperledger/firefly/pkg/i18n"
+ "github.com/hyperledger/firefly/pkg/log"
+)
+
+type Manager interface {
+ Start() error
+ WaitStop() error
+}
+
+type manager struct {
+ ctx context.Context
+ cancelCtx func()
+ changeEvents chan *fftypes.ChangeEvent
+ connectorAPI ffcapi.API
+ confirmations confirmations.Manager
+ policyEngine policyengine.PolicyEngine
+ apiServer httpserver.HTTPServer
+ ffCoreClient *resty.Client
+
+ mux sync.Mutex
+ nextNonces map[string]uint64
+ lockedNonces map[string]*lockedNonce
+ pendingOpsByID map[fftypes.UUID]*pendingState
+ changeEventLoopDone chan struct{}
+ firstFullScanDone chan error
+ receiptPollerDone chan struct{}
+ fullScanLoopDone chan struct{}
+ fullScanRequests chan bool
+ started bool
+ apiServerDone chan error
+
+ name string
+ opTypes []string
+ startupScanMaxRetries int
+ fullScanPageSize int64
+ fullScanMinDelay time.Duration
+ receiptsPollingInterval time.Duration
+}
+
+func NewManager(ctx context.Context) (Manager, error) {
+ var err error
+ m := &manager{
+ changeEvents: make(chan *fftypes.ChangeEvent),
+ connectorAPI: ffcapi.NewFFCAPI(ctx),
+ ffCoreClient: ffresty.New(ctx, tmconfig.FFCorePrefix),
+ fullScanRequests: make(chan bool, 1),
+ nextNonces: make(map[string]uint64),
+ lockedNonces: make(map[string]*lockedNonce),
+ apiServerDone: make(chan error),
+ pendingOpsByID: make(map[fftypes.UUID]*pendingState),
+
+ name: config.GetString(tmconfig.ManagerName),
+ opTypes: config.GetStringSlice(tmconfig.OperationsTypes),
+ startupScanMaxRetries: config.GetInt(tmconfig.OperationsFullScanStartupMaxRetries),
+ fullScanPageSize: config.GetInt64(tmconfig.OperationsFullScanPageSize),
+ fullScanMinDelay: config.GetDuration(tmconfig.OperationsFullScanMinimumDelay),
+ receiptsPollingInterval: config.GetDuration(tmconfig.ReceiptsPollingInterval),
+ }
+ m.ctx, m.cancelCtx = context.WithCancel(ctx)
+ if m.name == "" {
+ return nil, i18n.NewError(ctx, tmmsgs.MsgConfigParamNotSet, tmconfig.ManagerName)
+ }
+ m.confirmations, err = confirmations.NewBlockConfirmationManager(ctx, m.connectorAPI)
+ if err != nil {
+ return nil, err
+ }
+ m.policyEngine, err = policyengines.NewPolicyEngine(ctx, tmconfig.PolicyEngineBasePrefix, config.GetString(tmconfig.PolicyEngineName))
+ if err != nil {
+ return nil, err
+ }
+ m.apiServer, err = httpserver.NewHTTPServer(ctx, "api", m.router(), m.apiServerDone, tmconfig.APIPrefix)
+ if err != nil {
+ return nil, err
+ }
+ return m, nil
+}
+
+type pendingState struct {
+ mtx *fftm.ManagedTXOutput
+ confirmed bool
+ removed bool
+ lastReceiptBlockHash string
+}
+
+func (m *manager) requestFullScan() {
+ select {
+ case m.fullScanRequests <- true:
+ log.L(m.ctx).Debugf("Full scan of pending ops requested")
+ default:
+ log.L(m.ctx).Debugf("Full scan of pending ops already queued")
+ }
+}
+
+func (m *manager) waitScanDelay(lastFullScan *fftypes.FFTime) {
+ scanDelay := m.fullScanMinDelay - time.Since(*lastFullScan.Time())
+ log.L(m.ctx).Errorf("Delaying %dms before next full scan", scanDelay.Milliseconds())
+ timer := time.NewTimer(scanDelay)
+ select {
+ case <-timer.C:
+ case <-m.ctx.Done():
+ log.L(m.ctx).Infof("Full scan loop exiting waiting for retry")
+ return
+ }
+}
+
+func (m *manager) fullScanLoop() {
+ defer close(m.fullScanLoopDone)
+ firstFullScanDone := m.firstFullScanDone
+ var lastFullScan *fftypes.FFTime
+ errorCount := 0
+ for {
+ select {
+ case <-m.fullScanRequests:
+ if lastFullScan != nil {
+ m.waitScanDelay(lastFullScan)
+ }
+ lastFullScan = fftypes.Now()
+ err := m.fullScan()
+ if err != nil {
+ errorCount++
+ if firstFullScanDone != nil && errorCount > m.startupScanMaxRetries {
+ firstFullScanDone <- err
+ return
+ }
+ log.L(m.ctx).Errorf("Full scan failed (will be retried) count=%d: %s", errorCount, err)
+ m.requestFullScan()
+ continue
+ }
+ errorCount = 0
+ // On startup we need to know the first scan has completed to populate the nonces,
+ // before we complete startup
+ if firstFullScanDone != nil {
+ firstFullScanDone <- nil
+ firstFullScanDone = nil
+ }
+ case <-m.ctx.Done():
+ log.L(m.ctx).Infof("Full scan loop exiting")
+ return
+ }
+ }
+}
+
+func (m *manager) fullScan() error {
+ log.L(m.ctx).Debugf("Reading all operations after connect")
+ var page int64
+ var read, added int
+ var lastOp *fftypes.Operation
+ for {
+ ops, err := m.readOperationPage(lastOp)
+ if err != nil {
+ return err
+ }
+ if len(ops) == 0 {
+ log.L(m.ctx).Debugf("Finished reading all operations - %d read, %d added", read, added)
+ return nil
+ }
+ lastOp = ops[len(ops)-1]
+ read += len(ops)
+ for _, op := range ops {
+ added++
+ m.trackIfManaged(op)
+ }
+ page++
+ }
+}
+
+func (m *manager) trackIfManaged(op *fftypes.Operation) {
+ outputJSON := []byte(op.Output.String())
+ var mtx fftm.ManagedTXOutput
+ err := json.Unmarshal(outputJSON, &mtx)
+ if err != nil {
+ log.L(m.ctx).Warnf("Failed to parse output from operation %s", err)
+ return
+ }
+ if mtx.FFTMName != m.name {
+ log.L(m.ctx).Debugf("Operation %s is not managed by us (fftm=%s)", op.ID, mtx.FFTMName)
+ return
+ }
+ if !op.ID.Equals(mtx.ID) {
+ log.L(m.ctx).Warnf("Operation %s contains an invalid ID %s in the output", op.ID, mtx.ID)
+ return
+ }
+ m.trackManaged(&mtx)
+}
+
+func (m *manager) trackManaged(mtx *fftm.ManagedTXOutput) {
+ m.mux.Lock()
+ defer m.mux.Unlock()
+ _, existing := m.pendingOpsByID[*mtx.ID]
+ if !existing {
+ nextNonce, ok := m.nextNonces[mtx.Signer]
+ nonce := mtx.Nonce.Uint64()
+ if !ok || nextNonce <= nonce {
+ log.L(m.ctx).Debugf("Nonce %d in-flight. Next nonce: %d", nonce, nonce+1)
+ m.nextNonces[mtx.Signer] = nonce + 1
+ }
+ m.pendingOpsByID[*mtx.ID] = &pendingState{
+ mtx: mtx,
+ }
+ }
+}
+
+func (m *manager) markCancelledIfTracked(opID *fftypes.UUID) {
+ m.mux.Lock()
+ pending, existing := m.pendingOpsByID[*opID]
+ if existing {
+ pending.removed = true
+ }
+ m.mux.Unlock()
+
+}
+
+func (m *manager) handleEvent(ce *fftypes.ChangeEvent) {
+ log.L(m.ctx).Debugf("%s:%s/%s operation change event received", ce.Namespace, ce.ID, ce.Type)
+ // Note that we only subscribe the events on update (this check is just belt and braces).
+ // The operation gets created before any connector is called, so the first event should be
+ // after we do the update from the prepare.
+ if ce.Collection == "operations" && ce.Type == fftypes.ChangeEventTypeUpdated {
+ m.mux.Lock()
+ _, knownID := m.pendingOpsByID[*ce.ID]
+ m.mux.Unlock()
+ if !knownID {
+ m.queryAndAddPending(ce.ID)
+ }
+ }
+}
+
+func (m *manager) changeEventLoop() {
+ defer close(m.changeEventLoopDone)
+ for {
+ select {
+ case ce := <-m.changeEvents:
+ m.handleEvent(ce)
+ case <-m.ctx.Done():
+ log.L(m.ctx).Infof("Change event loop exiting")
+ return
+ }
+ }
+}
+
+func (m *manager) Start() error {
+ m.fullScanRequests <- true
+ m.firstFullScanDone = make(chan error)
+ m.fullScanLoopDone = make(chan struct{})
+ go m.fullScanLoop()
+ return m.waitForFirstScanAndStart()
+}
+
+func (m *manager) waitForFirstScanAndStart() error {
+ log.L(m.ctx).Infof("Waiting for first full scan of operations to build state")
+ select {
+ case err := <-m.firstFullScanDone:
+ if err != nil {
+ return err
+ }
+ case <-m.ctx.Done():
+ log.L(m.ctx).Infof("Cancelled before startup completed")
+ return nil
+ }
+ log.L(m.ctx).Infof("Scan complete. Completing startup")
+ m.changeEventLoopDone = make(chan struct{})
+ m.receiptPollerDone = make(chan struct{})
+ go m.changeEventLoop()
+ go m.receiptPollingLoop()
+ go m.runAPIServer()
+ m.started = true
+ return nil
+}
+
+func (m *manager) WaitStop() (err error) {
+ m.cancelCtx()
+ if m.started {
+ err = <-m.apiServerDone
+ <-m.changeEventLoopDone
+ <-m.fullScanLoopDone
+ <-m.receiptPollerDone
+ }
+ return err
+}
diff --git a/internal/manager/manager_test.go b/internal/manager/manager_test.go
new file mode 100644
index 00000000..b6dd2d07
--- /dev/null
+++ b/internal/manager/manager_test.go
@@ -0,0 +1,465 @@
+// 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 manager
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/hyperledger/firefly-transaction-manager/internal/policyengines"
+ "github.com/hyperledger/firefly-transaction-manager/internal/policyengines/simple"
+ "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig"
+ "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/fftm"
+ "github.com/hyperledger/firefly/pkg/config"
+ "github.com/hyperledger/firefly/pkg/ffresty"
+ "github.com/hyperledger/firefly/pkg/fftypes"
+ "github.com/hyperledger/firefly/pkg/httpserver"
+ "github.com/stretchr/testify/assert"
+)
+
+const testManagerName = "unittest"
+
+func newTestManager(t *testing.T, cAPIHandler http.HandlerFunc, ffCoreHandler http.HandlerFunc) (string, *manager, func()) {
+ tmconfig.Reset()
+ policyengines.RegisterEngine(tmconfig.PolicyEngineBasePrefix, &simple.PolicyEngineFactory{})
+
+ cAPIServer := httptest.NewServer(cAPIHandler)
+ tmconfig.ConnectorPrefix.Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", cAPIServer.Listener.Addr()))
+
+ ffCoreServer := httptest.NewServer(ffCoreHandler)
+ tmconfig.FFCorePrefix.Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", ffCoreServer.Listener.Addr()))
+
+ ln, err := net.Listen("tcp", "127.0.0.1:0")
+ assert.NoError(t, err)
+ managerPort := strings.Split(ln.Addr().String(), ":")[1]
+ ln.Close()
+ tmconfig.APIPrefix.Set(httpserver.HTTPConfPort, managerPort)
+ tmconfig.APIPrefix.Set(httpserver.HTTPConfAddress, "127.0.0.1")
+
+ config.Set(tmconfig.ManagerName, testManagerName)
+ config.Set(tmconfig.ReceiptsPollingInterval, "1ms")
+ tmconfig.PolicyEngineBasePrefix.SubPrefix("simple").Set(simple.FixedGas, "223344556677")
+
+ mm, err := NewManager(context.Background())
+ assert.NoError(t, err)
+ m := mm.(*manager)
+ m.confirmations = &confirmationsmocks.Manager{}
+
+ return fmt.Sprintf("http://127.0.0.1:%s", managerPort),
+ m,
+ func() {
+ cAPIServer.Close()
+ ffCoreServer.Close()
+ _ = m.WaitStop()
+ }
+
+}
+
+func newTestOperation(t *testing.T, mtx *fftm.ManagedTXOutput, status fftypes.OpStatus) *fftypes.Operation {
+ b, err := json.Marshal(&mtx)
+ assert.NoError(t, err)
+ op := &fftypes.Operation{
+ ID: mtx.ID,
+ Status: status,
+ }
+ err = json.Unmarshal(b, &op.Output)
+ assert.NoError(t, err)
+ return op
+}
+
+func TestNewManagerMissingName(t *testing.T) {
+
+ tmconfig.Reset()
+ config.Set(tmconfig.ManagerName, "")
+
+ _, err := NewManager(context.Background())
+ assert.Regexp(t, "FF201018", err)
+
+}
+
+func TestNewManagerBadHttpConfig(t *testing.T) {
+
+ tmconfig.Reset()
+ config.Set(tmconfig.ManagerName, "test")
+ tmconfig.APIPrefix.Set(httpserver.HTTPConfAddress, "::::")
+
+ policyengines.RegisterEngine(tmconfig.PolicyEngineBasePrefix, &simple.PolicyEngineFactory{})
+ tmconfig.PolicyEngineBasePrefix.SubPrefix("simple").Set(simple.FixedGas, "223344556677")
+
+ _, err := NewManager(context.Background())
+ assert.Regexp(t, "FF10104", err)
+
+}
+
+func TestNewManagerBadConfirmationsCacheSize(t *testing.T) {
+
+ tmconfig.Reset()
+ config.Set(tmconfig.ManagerName, "test")
+ config.Set(tmconfig.ConfirmationsBlockCacheSize, -1)
+
+ _, err := NewManager(context.Background())
+ assert.Regexp(t, "FF201015", err)
+
+}
+
+func TestNewManagerBadPolicyEngine(t *testing.T) {
+
+ tmconfig.Reset()
+ config.Set(tmconfig.ManagerName, "test")
+ config.Set(tmconfig.PolicyEngineName, "wrong")
+
+ _, err := NewManager(context.Background())
+ assert.Regexp(t, "FF201019", err)
+
+}
+
+func TestChangeEventsNewTracked(t *testing.T) {
+
+ ce := &fftypes.ChangeEvent{
+ ID: fftypes.NewUUID(),
+ Type: fftypes.ChangeEventTypeUpdated,
+ Collection: "operations",
+ Namespace: "ns1",
+ }
+
+ var m *manager
+ _, m, cancel := newTestManager(t,
+ func(w http.ResponseWriter, r *http.Request) {},
+ func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, http.MethodGet, r.Method)
+ assert.Equal(t, fmt.Sprintf("/admin/api/v1/operations/%s", ce.ID), r.URL.Path)
+ b, err := json.Marshal(newTestOperation(t, &fftm.ManagedTXOutput{
+ ID: ce.ID,
+ FFTMName: testManagerName,
+ }, fftypes.OpStatusPending))
+ assert.NoError(t, err)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(200)
+ w.Write(b)
+ // Cancel context here so loop ends
+ m.cancelCtx()
+ },
+ )
+ defer cancel()
+
+ m.changeEvents = make(chan *fftypes.ChangeEvent, 1)
+ m.changeEventLoopDone = make(chan struct{})
+ m.changeEvents <- ce
+
+ m.changeEventLoop()
+
+ assert.Equal(t, ce.ID, m.pendingOpsByID[*ce.ID].mtx.ID)
+
+}
+
+func TestChangeEventsNewBadOutput(t *testing.T) {
+
+ ce := &fftypes.ChangeEvent{
+ ID: fftypes.NewUUID(),
+ Type: fftypes.ChangeEventTypeUpdated,
+ Collection: "operations",
+ Namespace: "ns1",
+ }
+
+ var m *manager
+ _, m, cancel := newTestManager(t,
+ func(w http.ResponseWriter, r *http.Request) {},
+ func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, http.MethodGet, r.Method)
+ assert.Equal(t, fmt.Sprintf("/admin/api/v1/operations/%s", ce.ID), r.URL.Path)
+ b, err := json.Marshal(&fftypes.Operation{
+ ID: ce.ID,
+ Status: fftypes.OpStatusPending,
+ Output: fftypes.JSONObject{
+ "id": "!not a UUID",
+ },
+ })
+ assert.NoError(t, err)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(200)
+ w.Write(b)
+ },
+ )
+ defer cancel()
+
+ m.handleEvent(ce)
+ assert.Empty(t, m.pendingOpsByID)
+
+}
+
+func TestChangeEventsWrongName(t *testing.T) {
+
+ ce := &fftypes.ChangeEvent{
+ ID: fftypes.NewUUID(),
+ Type: fftypes.ChangeEventTypeUpdated,
+ Collection: "operations",
+ Namespace: "ns1",
+ }
+
+ var m *manager
+ _, m, cancel := newTestManager(t,
+ func(w http.ResponseWriter, r *http.Request) {},
+ func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, http.MethodGet, r.Method)
+ assert.Equal(t, fmt.Sprintf("/admin/api/v1/operations/%s", ce.ID), r.URL.Path)
+ b, err := json.Marshal(newTestOperation(t, &fftm.ManagedTXOutput{
+ ID: ce.ID,
+ FFTMName: "wrong",
+ }, fftypes.OpStatusPending))
+ assert.NoError(t, err)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(200)
+ w.Write(b)
+ },
+ )
+ defer cancel()
+
+ m.handleEvent(ce)
+ assert.Empty(t, m.pendingOpsByID)
+
+}
+
+func TestChangeEventsWrongID(t *testing.T) {
+
+ ce := &fftypes.ChangeEvent{
+ ID: fftypes.NewUUID(),
+ Type: fftypes.ChangeEventTypeUpdated,
+ Collection: "operations",
+ Namespace: "ns1",
+ }
+
+ var m *manager
+ _, m, cancel := newTestManager(t,
+ func(w http.ResponseWriter, r *http.Request) {},
+ func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, http.MethodGet, r.Method)
+ assert.Equal(t, fmt.Sprintf("/admin/api/v1/operations/%s", ce.ID), r.URL.Path)
+ op := newTestOperation(t, &fftm.ManagedTXOutput{
+ ID: fftypes.NewUUID(),
+ FFTMName: testManagerName,
+ }, fftypes.OpStatusPending)
+ op.ID = fftypes.NewUUID()
+ b, err := json.Marshal(&op)
+ assert.NoError(t, err)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(200)
+ w.Write(b)
+ },
+ )
+ defer cancel()
+
+ m.handleEvent(ce)
+ assert.Empty(t, m.pendingOpsByID)
+
+}
+
+func TestChangeEventsQueryFail(t *testing.T) {
+
+ ce := &fftypes.ChangeEvent{
+ ID: fftypes.NewUUID(),
+ Type: fftypes.ChangeEventTypeUpdated,
+ Collection: "operations",
+ Namespace: "ns1",
+ }
+
+ var m *manager
+ _, m, cancel := newTestManager(t,
+ func(w http.ResponseWriter, r *http.Request) {},
+ func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, http.MethodGet, r.Method)
+ assert.Equal(t, fmt.Sprintf("/admin/api/v1/operations/%s", ce.ID), r.URL.Path)
+ w.WriteHeader(404)
+ },
+ )
+ defer cancel()
+
+ m.fullScanRequests = make(chan bool, 1)
+
+ m.handleEvent(ce)
+ assert.Empty(t, m.pendingOpsByID)
+
+ // Full scan should have been requested after this failure
+ <-m.fullScanRequests
+
+}
+
+func TestChangeEventsMarkForCleanup(t *testing.T) {
+
+ ce := &fftypes.ChangeEvent{
+ ID: fftypes.NewUUID(),
+ Type: fftypes.ChangeEventTypeUpdated,
+ Collection: "operations",
+ Namespace: "ns1",
+ }
+
+ op := newTestOperation(t, &fftm.ManagedTXOutput{
+ ID: fftypes.NewUUID(),
+ FFTMName: testManagerName,
+ }, fftypes.OpStatusFailed)
+
+ var m *manager
+ _, m, cancel := newTestManager(t,
+ func(w http.ResponseWriter, r *http.Request) {},
+ func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, http.MethodGet, r.Method)
+ assert.Equal(t, fmt.Sprintf("/admin/api/v1/operations/%s", ce.ID), r.URL.Path)
+ b, err := json.Marshal(&op)
+ assert.NoError(t, err)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(200)
+ w.Write(b)
+ },
+ )
+ defer cancel()
+
+ m.trackIfManaged(op)
+ m.handleEvent(ce)
+ assert.True(t, m.pendingOpsByID[*op.ID].removed)
+
+}
+
+func TestStartupScanMultiPageOK(t *testing.T) {
+
+ op1 := newTestOperation(t, &fftm.ManagedTXOutput{
+ ID: fftypes.NewUUID(),
+ FFTMName: testManagerName,
+ }, fftypes.OpStatusPending)
+ t1 := fftypes.FFTime(time.Now().Add(-10 * time.Minute))
+ op1.Created = &t1
+ op2 := newTestOperation(t, &fftm.ManagedTXOutput{
+ ID: fftypes.NewUUID(),
+ FFTMName: testManagerName,
+ }, fftypes.OpStatusPending)
+ t2 := fftypes.FFTime(time.Now().Add(-5 * time.Minute))
+ op2.Created = &t2
+ op3 := newTestOperation(t, &fftm.ManagedTXOutput{
+ ID: fftypes.NewUUID(),
+ FFTMName: testManagerName,
+ }, fftypes.OpStatusPending)
+ t3 := fftypes.FFTime(time.Now().Add(-1 * time.Minute))
+ op3.Created = &t3
+
+ call := 0
+
+ var m *manager
+ _, m, cancel := newTestManager(t,
+ func(w http.ResponseWriter, r *http.Request) {},
+ func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, http.MethodGet, r.Method)
+ assert.Equal(t, "/admin/api/v1/operations", r.URL.Path)
+ status := 200
+ var res interface{}
+ switch call {
+ case 0:
+ res = &fftypes.RESTError{Error: "not ready yet"}
+ status = 500
+ case 1:
+ res = []*fftypes.Operation{op1, op2}
+ assert.Equal(t, "", r.URL.Query().Get("created"))
+ case 2:
+ res = []*fftypes.Operation{op2 /* simulate overlap */, op3}
+ assert.Equal(t, fmt.Sprintf(">=%d", op2.Created.Time().UnixNano()), r.URL.Query().Get("created"))
+ case 3:
+ res = []*fftypes.Operation{}
+ assert.Equal(t, fmt.Sprintf(">=%d", op3.Created.Time().UnixNano()), r.URL.Query().Get("created"))
+ default:
+ assert.Fail(t, "should have stopped after empty page")
+ }
+ call++
+ b, err := json.Marshal(res)
+ assert.NoError(t, err)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(status)
+ w.Write(b)
+ },
+ )
+ defer cancel()
+ m.fullScanMinDelay = 1 * time.Microsecond
+
+ err := m.Start()
+ assert.NoError(t, err)
+
+ assert.Len(t, m.pendingOpsByID, 3)
+
+}
+
+func TestStartupScanFail(t *testing.T) {
+
+ var m *manager
+ _, m, cancel := newTestManager(t,
+ func(w http.ResponseWriter, r *http.Request) {},
+ func(w http.ResponseWriter, r *http.Request) {},
+ )
+ cancel() // close servers
+ m.ctx = context.Background()
+ m.startupScanMaxRetries = 2
+ m.fullScanMinDelay = 1 * time.Microsecond
+
+ err := m.Start()
+ assert.Regexp(t, "FF201017", err)
+
+}
+
+func TestRequestFullScanNonBlocking(t *testing.T) {
+
+ _, m, cancel := newTestManager(t,
+ func(w http.ResponseWriter, r *http.Request) {},
+ func(w http.ResponseWriter, r *http.Request) {},
+ )
+ defer cancel()
+
+ m.requestFullScan()
+ m.requestFullScan()
+ m.requestFullScan()
+
+}
+
+func TestRequestFullScanCancelledBeforeStart(t *testing.T) {
+
+ _, m, cancel := newTestManager(t,
+ func(w http.ResponseWriter, r *http.Request) {},
+ func(w http.ResponseWriter, r *http.Request) {},
+ )
+ defer cancel()
+
+ m.cancelCtx()
+ m.waitForFirstScanAndStart()
+
+}
+
+func TestStartupCancelledDuringRetry(t *testing.T) {
+
+ var m *manager
+ _, m, cancel := newTestManager(t,
+ func(w http.ResponseWriter, r *http.Request) {},
+ func(w http.ResponseWriter, r *http.Request) {},
+ )
+ cancel() // close servers
+ m.startupScanMaxRetries = 2
+ m.fullScanMinDelay = 1 * time.Second
+
+ m.waitScanDelay(fftypes.Now())
+
+}
diff --git a/internal/manager/nonces.go b/internal/manager/nonces.go
new file mode 100644
index 00000000..c7068f91
--- /dev/null
+++ b/internal/manager/nonces.go
@@ -0,0 +1,104 @@
+// 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 manager
+
+import (
+ "context"
+
+ "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/fftm"
+ "github.com/hyperledger/firefly/pkg/fftypes"
+ "github.com/hyperledger/firefly/pkg/log"
+)
+
+type lockedNonce struct {
+ m *manager
+ opID *fftypes.UUID
+ signer string
+ unlocked chan struct{}
+ nonce uint64
+ spent *fftm.ManagedTXOutput
+}
+
+// complete must be called for any lockedNonce returned from a successful assignAndLockNonce call
+func (ln *lockedNonce) complete(ctx context.Context) {
+ if ln.spent != nil {
+ log.L(ctx).Debugf("Next nonce %d for signer %s spent", ln.nonce, ln.signer)
+ ln.m.trackManaged(ln.spent)
+ } else {
+ log.L(ctx).Debugf("Returning next nonce %d for signer %s unspent", ln.nonce, ln.signer)
+ // Do not
+ }
+ ln.m.mux.Lock()
+ delete(ln.m.lockedNonces, ln.signer)
+ close(ln.unlocked)
+ ln.m.mux.Unlock()
+}
+
+func (m *manager) assignAndLockNonce(ctx context.Context, opID *fftypes.UUID, signer string) (*lockedNonce, error) {
+
+ for {
+ // Take the lock to query our nonce cache, and check if we are already locked
+ m.mux.Lock()
+ doLookup := false
+ locked, isLocked := m.lockedNonces[signer]
+ if !isLocked {
+ locked = &lockedNonce{
+ m: m,
+ opID: opID,
+ signer: signer,
+ unlocked: make(chan struct{}),
+ }
+ m.lockedNonces[signer] = locked
+ // We might know the highest nonce straight away
+ nextNonce, nonceCached := m.nextNonces[signer]
+ if nonceCached {
+ locked.nonce = nextNonce
+ log.L(ctx).Debugf("Locking next nonce %d from cache for signer %s", locked.nonce, signer)
+ // We can return the nonce to use without any query
+ m.mux.Unlock()
+ return locked, nil
+ }
+ // Otherwise, defer a lookup to outside of the mutex
+ doLookup = true
+ }
+ m.mux.Unlock()
+
+ // If we're locked, then wait
+ if isLocked {
+ log.L(ctx).Debugf("Contention for next nonce for signer %s", signer)
+ <-locked.unlocked
+ } else if doLookup {
+ // We have to ensure we either successfully return a nonce,
+ // or otherwise we unlock when we send the error
+ nextNonceRes, _, err := m.connectorAPI.GetNextNonce(ctx, &ffcapi.GetNextNonceRequest{
+ Signer: signer,
+ })
+ if err != nil {
+ close(locked.unlocked)
+ return nil, err
+ }
+ nextNonce := nextNonceRes.Nonce.Uint64()
+ m.mux.Lock()
+ m.nextNonces[signer] = nextNonce
+ locked.nonce = nextNonce
+ m.mux.Unlock()
+ return locked, nil
+ }
+ }
+
+}
diff --git a/internal/manager/nonces_test.go b/internal/manager/nonces_test.go
new file mode 100644
index 00000000..00eec680
--- /dev/null
+++ b/internal/manager/nonces_test.go
@@ -0,0 +1,80 @@
+// 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 manager
+
+import (
+ "context"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/fftm"
+ "github.com/hyperledger/firefly/pkg/fftypes"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestNonceCached(t *testing.T) {
+
+ _, m, cancel := newTestManager(t,
+ testFFCAPIHandler(t, func(reqType ffcapi.RequestType, b []byte) (res interface{}, status int) {
+ return &ffcapi.GetNextNonceResponse{
+ Nonce: fftypes.NewFFBigInt(1111),
+ }, 200
+ }),
+ func(w http.ResponseWriter, r *http.Request) {},
+ )
+ defer cancel()
+
+ locked1 := make(chan struct{})
+ done1 := make(chan struct{})
+ done2 := make(chan struct{})
+
+ go func() {
+ defer close(done1)
+
+ ln, err := m.assignAndLockNonce(context.Background(), fftypes.NewUUID(), "0x12345")
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(1111), ln.nonce)
+ close(locked1)
+
+ time.Sleep(1 * time.Millisecond)
+ ln.spent = &fftm.ManagedTXOutput{
+ ID: fftypes.NewUUID(),
+ Nonce: fftypes.NewFFBigInt(int64(ln.nonce)),
+ Signer: "0x12345",
+ }
+ ln.complete(context.Background())
+ }()
+
+ go func() {
+ defer close(done2)
+
+ <-locked1
+ ln, err := m.assignAndLockNonce(context.Background(), fftypes.NewUUID(), "0x12345")
+ assert.NoError(t, err)
+
+ assert.Equal(t, uint64(1112), ln.nonce)
+
+ ln.complete(context.Background())
+
+ }()
+
+ <-done1
+ <-done2
+
+}
diff --git a/internal/manager/receipt_test.go b/internal/manager/receipt_test.go
new file mode 100644
index 00000000..177c91c7
--- /dev/null
+++ b/internal/manager/receipt_test.go
@@ -0,0 +1,239 @@
+// 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 manager
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/hyperledger/firefly-transaction-manager/internal/confirmations"
+ "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks"
+ "github.com/hyperledger/firefly-transaction-manager/mocks/policyenginemocks"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/fftm"
+ "github.com/hyperledger/firefly/pkg/fftypes"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+)
+
+func TestCheckReceiptE2EOk(t *testing.T) {
+
+ mtx := &fftm.ManagedTXOutput{
+ ID: fftypes.NewUUID(),
+ FirstSubmit: fftypes.Now(),
+ }
+
+ _, m, cancel := newTestManager(t,
+ testFFCAPIHandler(t, func(reqType ffcapi.RequestType, b []byte) (res interface{}, status int) {
+ assert.Equal(t, ffcapi.RequestTypeGetReceipt, reqType)
+ return &ffcapi.GetReceiptResponse{
+ BlockNumber: fftypes.NewFFBigInt(12345),
+ TransactionIndex: fftypes.NewFFBigInt(10),
+ BlockHash: fftypes.NewRandB32().String(),
+ }, 200
+ }),
+ func(w http.ResponseWriter, r *http.Request) {
+ var op fftypes.Operation
+ err := json.NewDecoder(r.Body).Decode(&op)
+ assert.NoError(t, err)
+ assert.Equal(t, mtx.ID, op.ID)
+ assert.Equal(t, fftypes.OpStatusSucceeded, op.Status)
+ w.WriteHeader(200)
+ },
+ )
+ defer cancel()
+
+ mc := m.confirmations.(*confirmationsmocks.Manager)
+ mc.On("Notify", mock.Anything).Run(func(args mock.Arguments) {
+ n := args[0].(*confirmations.Notification)
+ assert.Equal(t, confirmations.NewTransaction, n.NotificationType)
+ n.Transaction.Confirmed([]confirmations.BlockInfo{})
+ }).Return(nil).Once()
+ mc.On("Notify", mock.Anything).Run(func(args mock.Arguments) {
+ n := args[0].(*confirmations.Notification)
+ assert.Equal(t, confirmations.RemovedTransaction, n.NotificationType)
+ }).Return(nil).Once()
+
+ m.trackManaged(mtx)
+ m.checkReceipts()
+
+ err := m.checkReceiptCycle(m.pendingOpsByID[*mtx.ID])
+ assert.NoError(t, err)
+ assert.Empty(t, m.pendingOpsByID)
+
+ mc.AssertExpectations(t)
+}
+
+func TestCheckReceiptUpdateFFCoreWithError(t *testing.T) {
+
+ mtx := &fftm.ManagedTXOutput{
+ ID: fftypes.NewUUID(),
+ }
+
+ _, m, cancel := newTestManager(t,
+ func(w http.ResponseWriter, r *http.Request) {},
+ func(w http.ResponseWriter, r *http.Request) {
+ var op fftypes.Operation
+ err := json.NewDecoder(r.Body).Decode(&op)
+ assert.NoError(t, err)
+ assert.Equal(t, mtx.ID, op.ID)
+ assert.Equal(t, fftypes.OpStatusPending, op.Status)
+ w.WriteHeader(200)
+ },
+ )
+ defer cancel()
+
+ m.policyEngine = &policyenginemocks.PolicyEngine{}
+ pc := m.policyEngine.(*policyenginemocks.PolicyEngine)
+ pc.On("Execute", mock.Anything, mock.Anything, mtx).Return(false, fmt.Errorf("pop"))
+
+ m.trackManaged(mtx)
+ m.checkReceipts()
+
+ err := m.checkReceiptCycle(m.pendingOpsByID[*mtx.ID])
+ assert.NoError(t, err)
+ assert.NotEmpty(t, m.pendingOpsByID)
+}
+
+func TestCheckReceiptUpdateOpFail(t *testing.T) {
+
+ mtx := &fftm.ManagedTXOutput{
+ ID: fftypes.NewUUID(),
+ FirstSubmit: fftypes.Now(),
+ }
+
+ _, m, cancel := newTestManager(t,
+ testFFCAPIHandler(t, func(reqType ffcapi.RequestType, b []byte) (res interface{}, status int) {
+ assert.Equal(t, ffcapi.RequestTypeGetReceipt, reqType)
+ return &ffcapi.GetReceiptResponse{
+ BlockNumber: fftypes.NewFFBigInt(12345),
+ TransactionIndex: fftypes.NewFFBigInt(10),
+ BlockHash: fftypes.NewRandB32().String(),
+ }, 200
+ }),
+ func(w http.ResponseWriter, r *http.Request) {
+ errRes := fftypes.RESTError{Error: "pop"}
+ b, err := json.Marshal(&errRes)
+ assert.NoError(t, err)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(500)
+ w.Write(b)
+ },
+ )
+ defer cancel()
+
+ mc := m.confirmations.(*confirmationsmocks.Manager)
+ mc.On("Notify", mock.Anything).Run(func(args mock.Arguments) {
+ n := args[0].(*confirmations.Notification)
+ assert.Equal(t, confirmations.NewTransaction, n.NotificationType)
+ n.Transaction.Confirmed([]confirmations.BlockInfo{})
+ }).Return(nil).Once()
+
+ m.trackManaged(mtx)
+ m.checkReceipts()
+
+ err := m.checkReceiptCycle(m.pendingOpsByID[*mtx.ID])
+ assert.Regexp(t, "FF201017.*pop", err)
+ assert.NotEmpty(t, m.pendingOpsByID)
+
+ mc.AssertExpectations(t)
+}
+
+func TestCheckReceiptGetReceiptFail(t *testing.T) {
+
+ mtx := &fftm.ManagedTXOutput{
+ ID: fftypes.NewUUID(),
+ FirstSubmit: fftypes.Now(),
+ }
+
+ _, m, cancel := newTestManager(t,
+ testFFCAPIHandler(t, func(reqType ffcapi.RequestType, b []byte) (res interface{}, status int) {
+ assert.Equal(t, ffcapi.RequestTypeGetReceipt, reqType)
+ return &ffcapi.ErrorResponse{Error: "pop"}, 500
+ }),
+ func(w http.ResponseWriter, r *http.Request) {},
+ )
+ defer cancel()
+
+ m.trackManaged(mtx)
+ m.checkReceipts()
+
+ err := m.checkReceiptCycle(m.pendingOpsByID[*mtx.ID])
+ assert.Regexp(t, "FF201012.*pop", err)
+ assert.NotEmpty(t, m.pendingOpsByID)
+}
+
+func TestCheckReceiptGetReceiptForkRemoved(t *testing.T) {
+
+ mtx := &fftm.ManagedTXOutput{
+ ID: fftypes.NewUUID(),
+ FirstSubmit: fftypes.Now(),
+ Receipt: &ffcapi.GetReceiptResponse{
+ BlockNumber: fftypes.NewFFBigInt(12345),
+ TransactionIndex: fftypes.NewFFBigInt(10),
+ BlockHash: fftypes.NewRandB32().String(),
+ },
+ }
+
+ _, m, cancel := newTestManager(t,
+ testFFCAPIHandler(t, func(reqType ffcapi.RequestType, b []byte) (res interface{}, status int) {
+ assert.Equal(t, ffcapi.RequestTypeGetReceipt, reqType)
+ return &ffcapi.ErrorResponse{Error: "not found", Reason: ffcapi.ErrorReasonNotFound}, 404
+ }),
+ func(w http.ResponseWriter, r *http.Request) {},
+ )
+ defer cancel()
+
+ mc := m.confirmations.(*confirmationsmocks.Manager)
+ mc.On("Notify", mock.Anything).Run(func(args mock.Arguments) {
+ n := args[0].(*confirmations.Notification)
+ assert.Equal(t, confirmations.RemovedTransaction, n.NotificationType)
+ }).Return(nil).Once()
+
+ m.trackManaged(mtx)
+
+ err := m.checkReceiptCycle(m.pendingOpsByID[*mtx.ID])
+ assert.NoError(t, err)
+ assert.NotEmpty(t, m.pendingOpsByID)
+
+ mc.AssertExpectations(t)
+}
+
+func TestCheckReceiptCycleCleanupRemoved(t *testing.T) {
+
+ mtx := &fftm.ManagedTXOutput{
+ ID: fftypes.NewUUID(),
+ }
+
+ _, m, cancel := newTestManager(t,
+ func(w http.ResponseWriter, r *http.Request) {},
+ func(w http.ResponseWriter, r *http.Request) {},
+ )
+ defer cancel()
+
+ mc := m.confirmations.(*confirmationsmocks.Manager)
+ mc.On("Notify", mock.Anything).Return(nil).Once()
+
+ m.trackManaged(mtx)
+ m.markCancelledIfTracked(mtx.ID)
+
+ err := m.checkReceiptCycle(m.pendingOpsByID[*mtx.ID])
+ assert.NoError(t, err)
+ assert.Empty(t, m.pendingOpsByID)
+}
diff --git a/internal/manager/receipts.go b/internal/manager/receipts.go
new file mode 100644
index 00000000..c3d896a1
--- /dev/null
+++ b/internal/manager/receipts.go
@@ -0,0 +1,185 @@
+// 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 manager
+
+import (
+ "time"
+
+ "github.com/hyperledger/firefly-transaction-manager/internal/confirmations"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/fftm"
+ "github.com/hyperledger/firefly/pkg/fftypes"
+ "github.com/hyperledger/firefly/pkg/log"
+)
+
+func (m *manager) receiptPollingLoop() {
+ defer close(m.receiptPollerDone)
+
+ for {
+ timer := time.NewTimer(m.receiptsPollingInterval)
+ select {
+ case <-timer.C:
+ m.checkReceipts()
+ case <-m.ctx.Done():
+ log.L(m.ctx).Infof("Receipt poller exiting")
+ return
+ }
+ }
+}
+
+func (m *manager) checkReceipts() {
+
+ // Grab the lock to build a list of things to check
+ m.mux.Lock()
+ allPending := make([]*pendingState, 0, len(m.pendingOpsByID))
+ for _, pending := range m.pendingOpsByID {
+ allPending = append(allPending, pending)
+ }
+ m.mux.Unlock()
+
+ // Go through trying to query all of them
+ for _, pending := range allPending {
+ err := m.checkReceiptCycle(pending)
+ if err != nil {
+ log.L(m.ctx).Errorf("Failed to receipt cycle transaction=%s operation=%s", pending.mtx.TransactionHash, pending.mtx.ID)
+ }
+ }
+
+}
+
+// checkReceiptCycle runs against each pending item, on each cycle, and is the one place responsible
+// for state updates - to avoid those happening in parallel.
+func (m *manager) checkReceiptCycle(pending *pendingState) (err error) {
+
+ updated := true
+ newStatus := fftypes.OpStatusPending
+ mtx := pending.mtx
+ switch {
+ case pending.confirmed:
+ updated = true
+ newStatus = fftypes.OpStatusSucceeded
+ case pending.removed:
+ // Remove from our state
+ m.removeIfTracked(mtx.ID)
+ default:
+ if mtx.FirstSubmit != nil {
+ if err = m.checkReceipt(pending); err != nil {
+ return err
+ }
+ }
+
+ // Pass the state to the pluggable policy engine to potentially perform more actions against it,
+ // such as submitting for the first time, or raising the gas etc.
+ updated, err = m.policyEngine.Execute(m.ctx, m.connectorAPI, pending.mtx)
+ }
+
+ if updated || err != nil {
+ errorString := ""
+ if err != nil {
+ // In the case of errors, we keep the record updated with the latest error - but leave it in Pending
+ errorString = err.Error()
+ }
+ err := m.writeManagedTX(m.ctx, &opUpdate{
+ ID: mtx.ID,
+ Status: newStatus,
+ Output: mtx,
+ Error: errorString,
+ })
+ if err != nil {
+ log.L(m.ctx).Errorf("Failed to update operation %s (status=%s): %s", mtx.ID, newStatus, err)
+ return err
+ }
+ if pending.confirmed {
+ // We can remove it now
+ m.removeIfTracked(mtx.ID)
+ }
+ }
+
+ return nil
+}
+
+func (m *manager) checkReceipt(pending *pendingState) error {
+ mtx := pending.mtx
+ res, reason, err := m.connectorAPI.GetReceipt(m.ctx, &ffcapi.GetReceiptRequest{
+ TransactionHash: mtx.TransactionHash,
+ })
+ if err != nil {
+ if reason == ffcapi.ErrorReasonNotFound {
+ // If we previously thought we had a receipt, then tell the confirmation manager
+ // to remove this transaction, and update our state.
+ if mtx.Receipt != nil {
+ m.clearConfirmationTracking(mtx)
+ pending.lastReceiptBlockHash = ""
+
+ }
+ } else {
+ // Others errors are logged
+ return err
+ }
+ } else {
+ // If the receipt has been changed (it's new, or the block hash changed) then let
+ // the confirmation manager know to track it
+ pending.mtx.Receipt = res
+ if pending.lastReceiptBlockHash == "" || pending.lastReceiptBlockHash != res.BlockHash {
+ m.requestConfirmationsNewReceipt(pending)
+ }
+ }
+ return nil
+}
+
+func (m *manager) clearConfirmationTracking(mtx *fftm.ManagedTXOutput) {
+ // The only error condition on confirmations manager is if we are exiting, which it logs
+ _ = m.confirmations.Notify(&confirmations.Notification{
+ NotificationType: confirmations.RemovedTransaction,
+ Transaction: &confirmations.TransactionInfo{
+ TransactionHash: mtx.TransactionHash,
+ },
+ })
+}
+
+func (m *manager) requestConfirmationsNewReceipt(pending *pendingState) {
+ pending.lastReceiptBlockHash = pending.mtx.Receipt.BlockHash
+ // The only error condition on confirmations manager is if we are exiting, which it logs
+ _ = m.confirmations.Notify(&confirmations.Notification{
+ NotificationType: confirmations.NewTransaction,
+ Transaction: &confirmations.TransactionInfo{
+ TransactionHash: pending.mtx.TransactionHash,
+ BlockHash: pending.mtx.Receipt.BlockHash,
+ BlockNumber: pending.mtx.Receipt.BlockNumber.Uint64(),
+ Confirmed: func(confirmations []confirmations.BlockInfo) {
+ // Will be picked up on the next receipt loop cycle
+ m.mux.Lock()
+ pending.confirmed = true
+ pending.mtx.Confirmations = confirmations
+ m.mux.Unlock()
+ },
+ },
+ })
+}
+
+func (m *manager) removeIfTracked(opID *fftypes.UUID) {
+ m.mux.Lock()
+ pending, existing := m.pendingOpsByID[*opID]
+ if existing {
+ delete(m.pendingOpsByID, *opID)
+ }
+ m.mux.Unlock()
+ // Outside the lock tap the confirmation manager on the shoulder so it can clean up too
+ if existing {
+ m.clearConfirmationTracking(pending.mtx)
+ }
+}
diff --git a/internal/manager/routes.go b/internal/manager/routes.go
new file mode 100644
index 00000000..d7310d0e
--- /dev/null
+++ b/internal/manager/routes.go
@@ -0,0 +1,76 @@
+// 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 manager
+
+import (
+ "context"
+
+ "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/fftm"
+ "github.com/hyperledger/firefly/pkg/fftypes"
+)
+
+func (m *manager) sendManagedTransaction(ctx context.Context, request *fftm.TransactionRequest) (*fftm.ManagedTXOutput, error) {
+
+ // First job is to assign the next nonce to this request.
+ // We block any further sends on this nonce until we've got this one successfully into the node, or
+ // fail deterministically in a way that allows us to return it.
+ lockedNonce, err := m.assignAndLockNonce(ctx, request.Headers.ID, request.From)
+ if err != nil {
+ return nil, err
+ }
+ // We will call markSpent() once we reach the point the nonce has been used
+ defer lockedNonce.complete(ctx)
+
+ // Prepare the transaction, which will mean we have a transaction that should be submittable.
+ // If we fail at this stage, we don't need to write any state as we are sure we haven't submitted
+ // anything to the blockchain itself.
+ prepared, _, err := m.connectorAPI.PrepareTransaction(ctx, &ffcapi.PrepareTransactionRequest{
+ TransactionPrepareInputs: request.TransactionPrepareInputs,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ // Next we update FireFly core with the pre-submitted record pending record, with the allocated nonce.
+ // From this point on, we will guide this transaction through to submission.
+ // We return an "ack" at this point, and dispatch the work of getting the transaction submitted
+ // to the background worker.
+ mtx := &fftm.ManagedTXOutput{
+ FFTMName: m.name,
+ ID: request.Headers.ID, // on input the request ID must be the Operation ID
+ Nonce: fftypes.NewFFBigInt(int64(lockedNonce.nonce)),
+ Signer: request.From,
+ Gas: prepared.Gas,
+ TransactionHash: prepared.TransactionHash,
+ RawTransaction: prepared.RawTransaction,
+ Request: request,
+ }
+ if err = m.writeManagedTX(ctx, &opUpdate{
+ ID: mtx.ID,
+ Status: fftypes.OpStatusPending,
+ Output: mtx,
+ }); err != nil {
+ return nil, err
+ }
+
+ // Ok - we've spent it. The rest of the processing will be triggered off of lockedNonce
+ // completion adding this transaction to the pool (and/or the change event that comes in from
+ // FireFly core from the update to the transaction)
+ lockedNonce.spent = mtx
+ return mtx, nil
+}
diff --git a/internal/policyengines/registry.go b/internal/policyengines/registry.go
new file mode 100644
index 00000000..34fc229b
--- /dev/null
+++ b/internal/policyengines/registry.go
@@ -0,0 +1,49 @@
+// 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 policyengines
+
+import (
+ "context"
+
+ "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine"
+ "github.com/hyperledger/firefly/pkg/config"
+ "github.com/hyperledger/firefly/pkg/i18n"
+)
+
+var policyEngines = make(map[string]Factory)
+
+func NewPolicyEngine(ctx context.Context, basePrefix config.Prefix, name string) (policyengine.PolicyEngine, error) {
+ factory, ok := policyEngines[name]
+ if !ok {
+ return nil, i18n.NewError(ctx, tmmsgs.MsgPolicyEngineNotRegistered, name)
+ }
+ return factory.NewPolicyEngine(ctx, basePrefix.SubPrefix(name))
+}
+
+type Factory interface {
+ Name() string
+ InitPrefix(prefix config.Prefix)
+ NewPolicyEngine(ctx context.Context, prefix config.Prefix) (policyengine.PolicyEngine, error)
+}
+
+func RegisterEngine(basePrefix config.Prefix, factory Factory) string {
+ name := factory.Name()
+ policyEngines[name] = factory
+ factory.InitPrefix(basePrefix.SubPrefix(name))
+ return name
+}
diff --git a/internal/policyengines/registry_test.go b/internal/policyengines/registry_test.go
new file mode 100644
index 00000000..10ccec29
--- /dev/null
+++ b/internal/policyengines/registry_test.go
@@ -0,0 +1,42 @@
+// 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 policyengines
+
+import (
+ "context"
+ "testing"
+
+ "github.com/hyperledger/firefly-transaction-manager/internal/policyengines/simple"
+ "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRegistry(t *testing.T) {
+
+ tmconfig.Reset()
+ RegisterEngine(tmconfig.PolicyEngineBasePrefix, &simple.PolicyEngineFactory{})
+
+ tmconfig.PolicyEngineBasePrefix.SubPrefix("simple").Set(simple.FixedGas, "12345")
+ p, err := NewPolicyEngine(context.Background(), tmconfig.PolicyEngineBasePrefix, "simple")
+ assert.NotNil(t, p)
+ assert.NoError(t, err)
+
+ p, err = NewPolicyEngine(context.Background(), tmconfig.PolicyEngineBasePrefix, "bob")
+ assert.Nil(t, p)
+ assert.Regexp(t, "FF201019", err)
+
+}
diff --git a/internal/policyengines/simple/config.go b/internal/policyengines/simple/config.go
new file mode 100644
index 00000000..76b4a649
--- /dev/null
+++ b/internal/policyengines/simple/config.go
@@ -0,0 +1,51 @@
+// 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 simple
+
+import (
+ "net/http"
+
+ "github.com/hyperledger/firefly/pkg/config"
+ "github.com/hyperledger/firefly/pkg/ffresty"
+)
+
+const (
+ FixedGas = "fixedGas" // when not using a gas station - will be treated as a raw JSON string, so can be numeric 123, or string "123", or object {"maxPriorityFeePerGas":123})
+ WarnInterval = "warnInterval" // warnings will be written to the log at this interval if mining has not occurred
+ GasStationPrefix = "gasstation"
+ GasStationMethod = "method"
+ GasStationEnabled = "enabled"
+ GasStationGJSON = "gjson" // executes a GJSON query against the returned output: https://github.com/tidwall/gjson/blob/master/SYNTAX.md
+)
+
+const (
+ defaultWarnInterval = "15m"
+ defaultGasStationMethod = http.MethodGet
+ defaultGasStationEnabled = false
+)
+
+func (f *PolicyEngineFactory) InitPrefix(prefix config.Prefix) {
+ prefix.AddKnownKey(FixedGas)
+ prefix.AddKnownKey(WarnInterval, defaultWarnInterval)
+
+ gasStationPrefix := prefix.SubPrefix(GasStationPrefix)
+ ffresty.InitPrefix(gasStationPrefix)
+ gasStationPrefix.AddKnownKey(GasStationMethod, defaultGasStationMethod)
+ gasStationPrefix.AddKnownKey(GasStationEnabled, defaultGasStationEnabled)
+ gasStationPrefix.AddKnownKey(GasStationGJSON)
+
+}
diff --git a/internal/policyengines/simple/simple_policy_engine.go b/internal/policyengines/simple/simple_policy_engine.go
new file mode 100644
index 00000000..7ed1787b
--- /dev/null
+++ b/internal/policyengines/simple/simple_policy_engine.go
@@ -0,0 +1,163 @@
+// 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 simple
+
+import (
+ "context"
+ "encoding/json"
+ "io/ioutil"
+ "time"
+
+ "github.com/go-resty/resty/v2"
+ "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/fftm"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine"
+ "github.com/hyperledger/firefly/pkg/config"
+ "github.com/hyperledger/firefly/pkg/ffresty"
+ "github.com/hyperledger/firefly/pkg/fftypes"
+ "github.com/hyperledger/firefly/pkg/i18n"
+ "github.com/hyperledger/firefly/pkg/log"
+ "github.com/tidwall/gjson"
+)
+
+type PolicyEngineFactory struct{}
+
+func (f *PolicyEngineFactory) Name() string {
+ return "simple"
+}
+
+// simplePolicyEngine is a base policy engine forming an example for extension:
+// - It uses a public gas estimation
+// - It submits the transaction once
+// - It logs errors transactions breach certain configured thresholds of staleness
+func (f *PolicyEngineFactory) NewPolicyEngine(ctx context.Context, prefix config.Prefix) (pe policyengine.PolicyEngine, err error) {
+ gasStationPrefix := prefix.SubPrefix(GasStationPrefix)
+ gasStationEnabled := gasStationPrefix.GetBool(GasStationEnabled)
+ p := &simplePolicyEngine{
+ warnInterval: prefix.GetDuration(WarnInterval),
+ fixedGasEstimate: fftypes.JSONAnyPtr(prefix.GetString(FixedGas)),
+
+ gasStationMethod: gasStationPrefix.GetString(GasStationMethod),
+ gasStationGJSON: gasStationPrefix.GetString(GasStationGJSON),
+ }
+ if gasStationEnabled {
+ p.gasStationClient = ffresty.New(ctx, gasStationPrefix)
+ }
+ if p.fixedGasEstimate.IsNil() && p.gasStationClient == nil {
+ return nil, i18n.NewError(ctx, tmmsgs.MsgNoGasConfigSetForPolicyEngine, prefix.Resolve(FixedGas), gasStationPrefix.Resolve(GasStationEnabled))
+ }
+ return p, nil
+}
+
+type simplePolicyEngine struct {
+ fixedGasEstimate *fftypes.JSONAny
+ warnInterval time.Duration
+
+ gasStationClient *resty.Client
+ gasStationMethod string
+ gasStationGJSON string
+}
+
+type simplePolicyInfo struct {
+ LastWarnTime *fftypes.FFTime `json:"lastWarnTime"`
+}
+
+// withPolicyInfo is a convenience helper to run some logic that accesses/updates our policy section
+func (p *simplePolicyEngine) withPolicyInfo(ctx context.Context, mtx *fftm.ManagedTXOutput, fn func(info *simplePolicyInfo) (updated bool, err error)) (updated bool, err error) {
+ var info simplePolicyInfo
+ infoBytes := []byte(mtx.PolicyInfo.String())
+ if len(infoBytes) > 0 {
+ err := json.Unmarshal(infoBytes, &info)
+ if err != nil {
+ log.L(ctx).Warnf("Failed to parse existing info `%s`: %s", infoBytes, err)
+ }
+ }
+ updated, err = fn(&info)
+ if updated {
+ infoBytes, _ = json.Marshal(&info)
+ mtx.PolicyInfo = fftypes.JSONAnyPtrBytes(infoBytes)
+ }
+ return updated, err
+}
+
+func (p *simplePolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx *fftm.ManagedTXOutput) (updated bool, err error) {
+ // Simple policy engine only submits once.
+ if mtx.FirstSubmit == nil {
+
+ mtx.GasPrice, err = p.getGasPrice(ctx)
+ if err != nil {
+ return false, err
+ }
+ _, _, err = cAPI.SendTransaction(ctx, &ffcapi.SendTransactionRequest{
+ From: mtx.Signer,
+ GasPrice: mtx.GasPrice,
+ RawTransaction: mtx.RawTransaction,
+ })
+ if err != nil {
+ // A more sophisticated policy engine would consider the reason here, and potentially adjust the transaction for future attempts
+ return false, err
+ }
+ mtx.FirstSubmit = fftypes.Now()
+ mtx.LastSubmit = mtx.FirstSubmit
+ return true, nil
+
+ } else if mtx.Receipt == nil {
+
+ // A more sophisticated policy engine would look at the reason for the lack of a receipt, and consider taking progressive
+ // action such as increasing the gas cost slowly over time. This simple example shows how the policy engine
+ // can use the FireFly core operation as a store for its historical state/decisions (in this case the last time we warned).
+ return p.withPolicyInfo(ctx, mtx, func(info *simplePolicyInfo) (updated bool, err error) {
+ lastWarnTime := info.LastWarnTime
+ if lastWarnTime == nil {
+ lastWarnTime = mtx.FirstSubmit
+ }
+ now := fftypes.Now()
+ if now.Time().Sub(*lastWarnTime.Time()) > p.warnInterval {
+ secsSinceSubmit := float64(now.Time().Sub(*mtx.FirstSubmit.Time())) / float64(time.Second)
+ log.L(ctx).Warnf("Transaction %s (op=%s) has not been mined after %.2fs", mtx.TransactionHash, mtx.ID, secsSinceSubmit)
+ info.LastWarnTime = now
+ return true, nil
+ }
+ return false, nil
+ })
+
+ }
+ // No action in the case we have a receipt
+ return false, nil
+}
+
+// getGasPrice either uses a fixed gas price, or invokes a gas station API
+func (p *simplePolicyEngine) getGasPrice(ctx context.Context) (gasPrice *fftypes.JSONAny, err error) {
+ if p.gasStationClient != nil {
+ res, err := p.gasStationClient.R().
+ SetDoNotParseResponse(true).
+ Execute(p.gasStationMethod, "")
+ var rawResponse []byte
+ if err == nil {
+ rawResponse, err = ioutil.ReadAll(res.RawBody())
+ }
+ if err != nil {
+ return nil, i18n.WrapError(ctx, err, tmmsgs.MsgErrorQueryingGasStationAPI, -1, rawResponse)
+ }
+ if res.IsError() {
+ return nil, i18n.WrapError(ctx, err, tmmsgs.MsgErrorQueryingGasStationAPI, res.StatusCode(), rawResponse)
+ }
+ return fftypes.JSONAnyPtr(gjson.Get(string(rawResponse), p.gasStationGJSON).Raw), nil
+ }
+ return p.fixedGasEstimate, nil
+}
diff --git a/internal/policyengines/simple/simple_policy_engine_test.go b/internal/policyengines/simple/simple_policy_engine_test.go
new file mode 100644
index 00000000..24064747
--- /dev/null
+++ b/internal/policyengines/simple/simple_policy_engine_test.go
@@ -0,0 +1,320 @@
+// 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 simple
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/fftm"
+ "github.com/hyperledger/firefly/pkg/config"
+ "github.com/hyperledger/firefly/pkg/ffresty"
+ "github.com/hyperledger/firefly/pkg/fftypes"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+)
+
+func newTestPolicyEngineFactory(t *testing.T) (*PolicyEngineFactory, config.Prefix) {
+ prefix := config.NewPluginConfig("unittest.simple")
+ f := &PolicyEngineFactory{}
+ f.InitPrefix(prefix)
+ assert.Equal(t, "simple", f.Name())
+ return f, prefix
+}
+
+func TestMissingGasConfig(t *testing.T) {
+ f, prefix := newTestPolicyEngineFactory(t)
+ _, err := f.NewPolicyEngine(context.Background(), prefix)
+ assert.Regexp(t, "FF201020", err)
+}
+
+func TestFixedGasOK(t *testing.T) {
+ f, prefix := newTestPolicyEngineFactory(t)
+ prefix.Set(FixedGas, `{
+ "maxPriorityFee":32.146027800733336,
+ "maxFee":32.14602781673334
+ }`)
+ p, err := f.NewPolicyEngine(context.Background(), prefix)
+ assert.NoError(t, err)
+
+ mtx := &fftm.ManagedTXOutput{
+ Signer: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712",
+ RawTransaction: "SOME_RAW_TX_BYTES",
+ }
+
+ mockFFCAPI := &ffcapimocks.API{}
+ mockFFCAPI.On("SendTransaction", mock.Anything, mock.MatchedBy(func(req *ffcapi.SendTransactionRequest) bool {
+ return req.GasPrice.JSONObject().GetString("maxPriorityFee") == "32.146027800733336" &&
+ req.GasPrice.JSONObject().GetString("maxFee") == "32.14602781673334" &&
+ req.From == "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712" &&
+ req.RawTransaction == "SOME_RAW_TX_BYTES"
+ })).Return(&ffcapi.SendTransactionResponse{}, ffcapi.ErrorReason(""), nil)
+
+ ctx := context.Background()
+ updated, err := p.Execute(ctx, mockFFCAPI, mtx)
+ assert.NoError(t, err)
+ assert.True(t, updated)
+ assert.NotNil(t, mtx.FirstSubmit)
+ assert.NotNil(t, mtx.LastSubmit)
+ assert.Equal(t, `{
+ "maxPriorityFee":32.146027800733336,
+ "maxFee":32.14602781673334
+ }`, mtx.GasPrice.String())
+
+ mockFFCAPI.AssertExpectations(t)
+}
+
+func TestGasStationSendOK(t *testing.T) {
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ assert.Equal(t, r.Method, http.MethodGet)
+ w.Write([]byte(`{
+ "safeLow": {
+ "maxPriorityFee":30.7611840636,
+ "maxFee":30.7611840796
+ },
+ "standard": {
+ "maxPriorityFee":32.146027800733336,
+ "maxFee":32.14602781673334
+ },
+ "fast": {
+ "maxPriorityFee":33.284344224133335,
+ "maxFee":33.284344240133336
+ },
+ "estimatedBaseFee":1.6e-8,
+ "blockTime":6,
+ "blockNumber":24962816
+ }`))
+ }))
+ defer server.Close()
+
+ f, prefix := newTestPolicyEngineFactory(t)
+ prefix.SubPrefix(GasStationPrefix).Set(GasStationEnabled, true)
+ prefix.SubPrefix(GasStationPrefix).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr()))
+ prefix.SubPrefix(GasStationPrefix).Set(GasStationGJSON, `{"unit":!"gwei","value":standard.maxPriorityFee}`)
+ p, err := f.NewPolicyEngine(context.Background(), prefix)
+ assert.NoError(t, err)
+
+ mtx := &fftm.ManagedTXOutput{
+ Signer: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712",
+ RawTransaction: "SOME_RAW_TX_BYTES",
+ }
+
+ mockFFCAPI := &ffcapimocks.API{}
+ mockFFCAPI.On("SendTransaction", mock.Anything, mock.MatchedBy(func(req *ffcapi.SendTransactionRequest) bool {
+ return req.GasPrice.JSONObject().GetString("unit") == "gwei" &&
+ req.GasPrice.JSONObject().GetString("value") == "32.146027800733336" &&
+ req.From == "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712" &&
+ req.RawTransaction == "SOME_RAW_TX_BYTES"
+ })).Return(&ffcapi.SendTransactionResponse{}, ffcapi.ErrorReason(""), nil)
+
+ ctx := context.Background()
+ updated, err := p.Execute(ctx, mockFFCAPI, mtx)
+ assert.NoError(t, err)
+ assert.True(t, updated)
+ assert.NotNil(t, mtx.FirstSubmit)
+ assert.NotNil(t, mtx.LastSubmit)
+ assert.Equal(t, `{"unit":"gwei","value":32.146027800733336}`, mtx.GasPrice.String())
+
+ mockFFCAPI.AssertExpectations(t)
+}
+
+func TestGasStationSendFail(t *testing.T) {
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(404)
+ w.Write([]byte(`Not the gas station you are looking for`))
+ }))
+ defer server.Close()
+
+ f, prefix := newTestPolicyEngineFactory(t)
+ prefix.SubPrefix(GasStationPrefix).Set(GasStationEnabled, true)
+ prefix.SubPrefix(GasStationPrefix).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr()))
+ p, err := f.NewPolicyEngine(context.Background(), prefix)
+ assert.NoError(t, err)
+
+ mtx := &fftm.ManagedTXOutput{
+ Signer: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712",
+ RawTransaction: "SOME_RAW_TX_BYTES",
+ }
+
+ mockFFCAPI := &ffcapimocks.API{}
+ ctx := context.Background()
+ _, err = p.Execute(ctx, mockFFCAPI, mtx)
+ assert.Regexp(t, "FF201021", err)
+
+}
+
+func TestGasStationNonJSON(t *testing.T) {
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
+ server.Close()
+
+ f, prefix := newTestPolicyEngineFactory(t)
+ prefix.SubPrefix(GasStationPrefix).Set(GasStationEnabled, true)
+ prefix.SubPrefix(GasStationPrefix).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr()))
+ p, err := f.NewPolicyEngine(context.Background(), prefix)
+ assert.NoError(t, err)
+
+ mtx := &fftm.ManagedTXOutput{
+ Signer: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712",
+ RawTransaction: "SOME_RAW_TX_BYTES",
+ }
+
+ mockFFCAPI := &ffcapimocks.API{}
+ ctx := context.Background()
+ _, err = p.Execute(ctx, mockFFCAPI, mtx)
+ assert.Regexp(t, "FF201021", err)
+
+}
+
+func TestTXSendFail(t *testing.T) {
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(200)
+ w.Write([]byte(`{}`))
+ }))
+ defer server.Close()
+
+ f, prefix := newTestPolicyEngineFactory(t)
+ prefix.SubPrefix(GasStationPrefix).Set(GasStationEnabled, true)
+ prefix.SubPrefix(GasStationPrefix).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr()))
+ p, err := f.NewPolicyEngine(context.Background(), prefix)
+ assert.NoError(t, err)
+
+ mtx := &fftm.ManagedTXOutput{
+ Signer: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712",
+ RawTransaction: "SOME_RAW_TX_BYTES",
+ }
+
+ mockFFCAPI := &ffcapimocks.API{}
+ mockFFCAPI.On("SendTransaction", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonInvalidInputs, fmt.Errorf("pop"))
+ ctx := context.Background()
+ _, err = p.Execute(ctx, mockFFCAPI, mtx)
+ assert.Regexp(t, "pop", err)
+
+}
+
+func TestWarnStaleWarningCannotParse(t *testing.T) {
+ f, prefix := newTestPolicyEngineFactory(t)
+ prefix.Set(FixedGas, `12345`)
+ p, err := f.NewPolicyEngine(context.Background(), prefix)
+ assert.NoError(t, err)
+
+ submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour))
+ mtx := &fftm.ManagedTXOutput{
+ Signer: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712",
+ RawTransaction: "SOME_RAW_TX_BYTES",
+ FirstSubmit: &submitTime,
+ PolicyInfo: fftypes.JSONAnyPtr("!not json!"),
+ }
+
+ mockFFCAPI := &ffcapimocks.API{}
+
+ ctx := context.Background()
+ updated, err := p.Execute(ctx, mockFFCAPI, mtx)
+ assert.NoError(t, err)
+ assert.True(t, updated)
+ assert.NotEmpty(t, mtx.PolicyInfo.JSONObject().GetString("lastWarnTime"))
+
+ mockFFCAPI.AssertExpectations(t)
+}
+
+func TestWarnStaleAdditionalWarning(t *testing.T) {
+ f, prefix := newTestPolicyEngineFactory(t)
+ prefix.Set(FixedGas, `12345`)
+ p, err := f.NewPolicyEngine(context.Background(), prefix)
+ assert.NoError(t, err)
+
+ submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour))
+ lastWarning := fftypes.FFTime(time.Now().Add(-50 * time.Hour))
+ mtx := &fftm.ManagedTXOutput{
+ Signer: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712",
+ RawTransaction: "SOME_RAW_TX_BYTES",
+ FirstSubmit: &submitTime,
+ PolicyInfo: fftypes.JSONAnyPtr(fmt.Sprintf(`{"lastWarnTime": "%s"}`, lastWarning.String())),
+ }
+
+ mockFFCAPI := &ffcapimocks.API{}
+
+ ctx := context.Background()
+ updated, err := p.Execute(ctx, mockFFCAPI, mtx)
+ assert.NoError(t, err)
+ assert.True(t, updated)
+ assert.NotEmpty(t, mtx.PolicyInfo.JSONObject().GetString("lastWarnTime"))
+
+ mockFFCAPI.AssertExpectations(t)
+}
+
+func TestWarnStaleNoWarning(t *testing.T) {
+ f, prefix := newTestPolicyEngineFactory(t)
+ prefix.Set(FixedGas, `12345`)
+ prefix.Set(WarnInterval, "100s")
+ p, err := f.NewPolicyEngine(context.Background(), prefix)
+ assert.NoError(t, err)
+
+ submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour))
+ lastWarning := fftypes.Now()
+ mtx := &fftm.ManagedTXOutput{
+ Signer: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712",
+ RawTransaction: "SOME_RAW_TX_BYTES",
+ FirstSubmit: &submitTime,
+ PolicyInfo: fftypes.JSONAnyPtr(fmt.Sprintf(`{"lastWarnTime": "%s"}`, lastWarning.String())),
+ }
+
+ mockFFCAPI := &ffcapimocks.API{}
+
+ ctx := context.Background()
+ updated, err := p.Execute(ctx, mockFFCAPI, mtx)
+ assert.NoError(t, err)
+ assert.False(t, updated)
+
+ mockFFCAPI.AssertExpectations(t)
+}
+
+func TestNoOpWithReceipt(t *testing.T) {
+ f, prefix := newTestPolicyEngineFactory(t)
+ prefix.Set(FixedGas, `12345`)
+ prefix.Set(WarnInterval, "100s")
+ p, err := f.NewPolicyEngine(context.Background(), prefix)
+ assert.NoError(t, err)
+
+ submitTime := fftypes.Now()
+ mtx := &fftm.ManagedTXOutput{
+ Signer: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712",
+ RawTransaction: "SOME_RAW_TX_BYTES",
+ FirstSubmit: submitTime,
+ Receipt: &ffcapi.GetReceiptResponse{
+ BlockHash: "0x39e2664effa5ad0651c35f1fe3b4c4b90492b1955fee731c2e9fb4d6518de114",
+ },
+ }
+
+ mockFFCAPI := &ffcapimocks.API{}
+
+ ctx := context.Background()
+ updated, err := p.Execute(ctx, mockFFCAPI, mtx)
+ assert.NoError(t, err)
+ assert.False(t, updated)
+
+ mockFFCAPI.AssertExpectations(t)
+}
diff --git a/internal/tmconfig/tmconfig.go b/internal/tmconfig/tmconfig.go
new file mode 100644
index 00000000..e9dc75a0
--- /dev/null
+++ b/internal/tmconfig/tmconfig.go
@@ -0,0 +1,97 @@
+// 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 tmconfig
+
+import (
+ "github.com/hyperledger/firefly/pkg/config"
+ "github.com/hyperledger/firefly/pkg/ffresty"
+ "github.com/hyperledger/firefly/pkg/fftypes"
+ "github.com/hyperledger/firefly/pkg/httpserver"
+ "github.com/spf13/viper"
+)
+
+var ffc = config.AddRootKey
+
+var (
+ // ManagerName is a name for this manager, that must be unique if there are multiple managers on this node
+ ManagerName = ffc("manager.name")
+ // ConnectorVariant is the variant setting to add to all requests to the backend connector
+ ConnectorVariant = ffc("connector.variant")
+ // ConfirmationsRequired is the number of confirmations required for a transaction to be considered final
+ ConfirmationsRequired = ffc("confirmations.required")
+ // ConfirmationsBlockCacheSize is the size of the block cache
+ ConfirmationsBlockCacheSize = ffc("confirmations.blockCacheSize")
+ // ConfirmationsBlockPollingInterval is the time between block polling
+ ConfirmationsBlockPollingInterval = ffc("confirmations.blockPollingInterval")
+ // ConfirmationsNotificationQueueLength is the length of the internal queue to the block confirmations manager
+ ConfirmationsNotificationQueueLength = ffc("confirmations.notificationQueueLength")
+ // OperationsTypes the type of operations to monitor - only those that were submitted through the manager will have the required output format, so this is the superset
+ OperationsTypes = ffc("operations.types")
+ // OperationsFullScanStartupMaxRetries is the maximum times to try the scan on first startup, before failing startup
+ OperationsFullScanStartupMaxRetries = ffc("operations.fullScan.startupMaxRetries")
+ // OperationsPageSize page size for polling
+ OperationsFullScanPageSize = ffc("operations.fullScan.pageSize")
+ // OperationsFullScanMinimumDelay the minimum delay between full scan attempts
+ OperationsFullScanMinimumDelay = ffc("operations.fullScan.minimumDelay")
+ // ReceiptPollingInterval how often to poll for transaction receipts (the policy engine gets a chance to intervene for each outstanding receipt, on each polling cycle)
+ ReceiptsPollingInterval = ffc("receipts.pollingInteval")
+ // PolicyEngineName the name of the policy engine to use
+ PolicyEngineName = ffc("policyengine.name")
+)
+
+var ConnectorPrefix config.Prefix
+
+var FFCorePrefix config.Prefix
+
+var APIPrefix config.Prefix
+
+var PolicyEngineBasePrefix config.Prefix
+
+func setDefaults() {
+ viper.SetDefault(string(OperationsFullScanPageSize), 100)
+ viper.SetDefault(string(OperationsFullScanMinimumDelay), "5s")
+ viper.SetDefault(string(OperationsTypes), []string{
+ fftypes.OpTypeBlockchainInvoke.String(),
+ fftypes.OpTypeBlockchainPinBatch.String(),
+ fftypes.OpTypeTokenCreatePool.String(),
+ })
+ viper.SetDefault(string(OperationsFullScanStartupMaxRetries), 10)
+ viper.SetDefault(string(ConnectorVariant), "evm")
+ viper.SetDefault(string(ConfirmationsRequired), 20)
+ viper.SetDefault(string(ConfirmationsBlockCacheSize), 1000)
+ viper.SetDefault(string(ConfirmationsBlockPollingInterval), "3s")
+ viper.SetDefault(string(ConfirmationsNotificationQueueLength), 50)
+ viper.SetDefault(string(ReceiptsPollingInterval), "1s")
+ viper.SetDefault(string(PolicyEngineName), "simple")
+}
+
+func Reset() {
+ config.RootConfigReset(setDefaults)
+
+ ConnectorPrefix = config.NewPluginConfig("connector")
+ ffresty.InitPrefix(ConnectorPrefix)
+
+ FFCorePrefix = config.NewPluginConfig("ffcore")
+ ffresty.InitPrefix(FFCorePrefix)
+
+ APIPrefix = config.NewPluginConfig("api")
+ httpserver.InitHTTPConfPrefix(APIPrefix, 5008)
+
+ PolicyEngineBasePrefix = config.NewPluginConfig("policyengine")
+ // policy engines must be registered outside of this package
+
+}
diff --git a/internal/tmconfig/tmconfig_test.go b/internal/tmconfig/tmconfig_test.go
new file mode 100644
index 00000000..b37e64b7
--- /dev/null
+++ b/internal/tmconfig/tmconfig_test.go
@@ -0,0 +1,32 @@
+// 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 tmconfig
+
+import (
+ "testing"
+
+ "github.com/hyperledger/firefly/pkg/config"
+ "github.com/stretchr/testify/assert"
+)
+
+const configDir = "../../test/data/config"
+
+func TestInitConfigOK(t *testing.T) {
+ Reset()
+
+ assert.Equal(t, 100, config.GetInt(OperationsFullScanPageSize))
+}
diff --git a/internal/tmmsgs/en_config_descriptions.go b/internal/tmmsgs/en_config_descriptions.go
new file mode 100644
index 00000000..aa25f26f
--- /dev/null
+++ b/internal/tmmsgs/en_config_descriptions.go
@@ -0,0 +1,61 @@
+// 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 tmmsgs
+
+import "github.com/hyperledger/firefly/pkg/i18n"
+
+var ffc = i18n.FFC
+
+//revive:disable
+var (
+ ConfigAPIAddress = ffc("config.api.address", "Listener address for API", "string")
+ ConfigAPIPort = ffc("config.api.port", "Listener port for API", "number")
+ ConfigAPIPublicURL = ffc("config.api.publicURL", "External address callers should access API over", "string")
+ ConfigAPIReadTimeout = ffc("config.api.readTimeout", "The maximum time to wait when reading from an HTTP connection", "duration")
+ ConfigAPIWriteTimeout = ffc("config.api.writeTimeout", "The maximum time to wait when writing to a HTTP connection", "duration")
+
+ ConfigConfirmationsBlockCacheSize = ffc("config.confirmations.blockCacheSize", "The maximum number of block headers to keep in the cache", "number")
+ ConfigConfirmationsBlockPollingInterval = ffc("config.confirmations.blockPollingInterval", "How often to poll for new block headers", "duration")
+ ConfigConfirmationsNotificationsQueueLength = ffc("config.confirmations.notificationQueueLength", "Internal queue length for notifying the confirmations manager of new transactions/events", "number")
+ ConfigConfirmationsRequired = ffc("config.confirmations.required", "Number of confirmations required to consider a transaction/event final", "number")
+
+ ConfigConnectorURL = ffc("config.connector.url", "The URL of the blockchain connector", "string")
+ ConfigConnectorVariant = ffc("config.connector.variant", "The variant is the overall category of blockchain connector, defining things like how input/output definitions are passed", "string")
+ ConfigConnectorProxyURL = ffc("config.connector.proxy.url", "Optional HTTP proxy URL to use for the blockchain connector", "string")
+
+ ConfigFFCoreURL = ffc("config.ffcore.url", "The URL of the FireFly core admin API server to connect to", "string")
+ ConfigFFCoreProxyURL = ffc("config.ffcore.proxy.url", "Optional HTTP proxy URL to use for the FireFly core admin API server", "string")
+
+ ConfigManagerName = ffc("config.manager.name", "The name of this Transaction Manager, used in operation metadata to track which operations are to be updated", "string")
+
+ ConfigOperationsTypes = ffc("config.operations.types", "The operation types to query in FireFly core, that might have been submitted via this Transaction Manager", "string[]")
+ ConfigOperationsFullScanMinimumDelay = ffc("config.operations.fullScan.minimumDelay", "The minimum delay between full scans of the FireFly core API, when reconnecting, or recovering from missed events / errors", "duration")
+ ConfigOperationsFullScanPageSize = ffc("config.operations.fullScan.pageSize", "The page size to use when performing a full scan of the ForeFly core API on startup, or recovery", "number")
+ ConfigOperationsFullScanStartupMaxRetries = ffc("config.operations.fullScan.startupMaxRetries", "The page size to use when performing a full scan of the ForeFly core API on startup, or recovery", "number")
+
+ ConfigPolicyEngineName = ffc("config.policyengine.name", "The name of the policy engine to use", "string")
+
+ ConfigPolicyEngineSimpleFixedGas = ffc("config.policyengine.simple.fixedGas", "A fixed gasPrice value/structure to pass to the connector", "Raw JSON")
+ ConfigPolicyEngineSimpleWarnInterval = ffc("config.policyengine.simple.warnInterval", "The time between warnings when a blockchain transaction has not been allocated a receipt", "duration")
+ ConfigPolicyEngineSimpleGasStationEnabled = ffc("config.policyengine.simple.gasstation.enabled", "When true the configured gasstation URL will be queried before submitting each", "boolean")
+ ConfigPolicyEngineSimpleGasStationGJSON = ffc("config.policyengine.simple.gasstation.gjson", "A GJSON query to execute against the response from the Gas Station API. The raw json will then be passed as the gasPrice to the connector", "see [GJSON syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md)")
+ ConfigPolicyEngineSimpleGasStationURL = ffc("config.policyengine.simple.gasstation.url", "The URL of a Gas Station API to call", "string")
+ ConfigPolicyEngineSimpleGasStationProxyURL = ffc("config.policyengine.simple.gasstation.proxy.url", "Optional HTTP proxy URL to use for the Gas Station API", "string")
+ ConfigPolicyEngineSimpleGasStationMethod = ffc("config.policyengine.simple.gasstation.method", "The HTTP Method to use when invoking the Gas STation API", "string")
+
+ ConfigReceiptsPollingInterval = ffc("config.receipts.pollingInteval", "Interval between queries for receipts for all in-flight transactions that have not met the confirmation threshold", "duration")
+)
diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go
new file mode 100644
index 00000000..9fea4abe
--- /dev/null
+++ b/internal/tmmsgs/en_error_messges.go
@@ -0,0 +1,38 @@
+// 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 tmmsgs
+
+import "github.com/hyperledger/firefly/pkg/i18n"
+
+var ffe = i18n.FFE
+
+//revive:disable
+var (
+ MsgInvalidOutputType = ffe("FF201010", "Invalid output type: %s")
+ MsgConnectorError = ffe("FF201012", "Connector failed request. requestId=%s reason=%s error: %s")
+ MsgConnectorInvalidConentType = ffe("FF201013", "Connector failed request. requestId=%s invalid response content type: %s")
+ MsgConnectorFailInvoke = ffe("FF201014", "Connector failed request. requestId=%s failed to call connector API")
+ MsgCacheInitFail = ffe("FF201015", "Failed to initialize cache")
+ MsgInvalidConfirmationRequest = ffe("FF201016", "Invalid confirmation request %+v")
+ MsgCoreError = ffe("FF201017", "Error from core status=%d: %s")
+ MsgConfigParamNotSet = ffe("FF201018", "Configuration parameter '%s' must be set")
+ MsgPolicyEngineNotRegistered = ffe("FF201019", "No policy engine registered with name '%s'")
+ MsgNoGasConfigSetForPolicyEngine = ffe("FF201020", "No gas configuration has been set for policy engine. Set %s or %s")
+ MsgErrorQueryingGasStationAPI = ffe("FF201021", "Error from gas station API [%d]: %s")
+ MsgErrorInvalidRequest = ffe("FF201022", "Invalid request")
+ MsgUnsupportedRequestType = ffe("FF201023", "Unsupported request type: %s")
+)
diff --git a/mocks/confirmationsmocks/manager.go b/mocks/confirmationsmocks/manager.go
new file mode 100644
index 00000000..68cf5f7d
--- /dev/null
+++ b/mocks/confirmationsmocks/manager.go
@@ -0,0 +1,37 @@
+// Code generated by mockery v1.0.0. DO NOT EDIT.
+
+package confirmationsmocks
+
+import (
+ confirmations "github.com/hyperledger/firefly-transaction-manager/internal/confirmations"
+ mock "github.com/stretchr/testify/mock"
+)
+
+// Manager is an autogenerated mock type for the Manager type
+type Manager struct {
+ mock.Mock
+}
+
+// Notify provides a mock function with given fields: n
+func (_m *Manager) Notify(n *confirmations.Notification) error {
+ ret := _m.Called(n)
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(*confirmations.Notification) error); ok {
+ r0 = rf(n)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// Start provides a mock function with given fields:
+func (_m *Manager) Start() {
+ _m.Called()
+}
+
+// Stop provides a mock function with given fields:
+func (_m *Manager) Stop() {
+ _m.Called()
+}
diff --git a/mocks/ffcapimocks/api.go b/mocks/ffcapimocks/api.go
new file mode 100644
index 00000000..401b49ee
--- /dev/null
+++ b/mocks/ffcapimocks/api.go
@@ -0,0 +1,285 @@
+// Code generated by mockery v1.0.0. DO NOT EDIT.
+
+package ffcapimocks
+
+import (
+ context "context"
+
+ ffcapi "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi"
+ mock "github.com/stretchr/testify/mock"
+)
+
+// API is an autogenerated mock type for the API type
+type API struct {
+ mock.Mock
+}
+
+// CreateBlockListener provides a mock function with given fields: ctx, req
+func (_m *API) CreateBlockListener(ctx context.Context, req *ffcapi.CreateBlockListenerRequest) (*ffcapi.CreateBlockListenerResponse, ffcapi.ErrorReason, error) {
+ ret := _m.Called(ctx, req)
+
+ var r0 *ffcapi.CreateBlockListenerResponse
+ if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.CreateBlockListenerRequest) *ffcapi.CreateBlockListenerResponse); ok {
+ r0 = rf(ctx, req)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*ffcapi.CreateBlockListenerResponse)
+ }
+ }
+
+ var r1 ffcapi.ErrorReason
+ if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.CreateBlockListenerRequest) ffcapi.ErrorReason); ok {
+ r1 = rf(ctx, req)
+ } else {
+ r1 = ret.Get(1).(ffcapi.ErrorReason)
+ }
+
+ var r2 error
+ if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.CreateBlockListenerRequest) error); ok {
+ r2 = rf(ctx, req)
+ } else {
+ r2 = ret.Error(2)
+ }
+
+ return r0, r1, r2
+}
+
+// ExecQuery provides a mock function with given fields: ctx, req
+func (_m *API) ExecQuery(ctx context.Context, req *ffcapi.ExecQueryRequest) (*ffcapi.ExecQueryResponse, ffcapi.ErrorReason, error) {
+ ret := _m.Called(ctx, req)
+
+ var r0 *ffcapi.ExecQueryResponse
+ if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.ExecQueryRequest) *ffcapi.ExecQueryResponse); ok {
+ r0 = rf(ctx, req)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*ffcapi.ExecQueryResponse)
+ }
+ }
+
+ var r1 ffcapi.ErrorReason
+ if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.ExecQueryRequest) ffcapi.ErrorReason); ok {
+ r1 = rf(ctx, req)
+ } else {
+ r1 = ret.Get(1).(ffcapi.ErrorReason)
+ }
+
+ var r2 error
+ if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.ExecQueryRequest) error); ok {
+ r2 = rf(ctx, req)
+ } else {
+ r2 = ret.Error(2)
+ }
+
+ return r0, r1, r2
+}
+
+// GetBlockInfoByHash provides a mock function with given fields: ctx, req
+func (_m *API) GetBlockInfoByHash(ctx context.Context, req *ffcapi.GetBlockInfoByHashRequest) (*ffcapi.GetBlockInfoByHashResponse, ffcapi.ErrorReason, error) {
+ ret := _m.Called(ctx, req)
+
+ var r0 *ffcapi.GetBlockInfoByHashResponse
+ if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.GetBlockInfoByHashRequest) *ffcapi.GetBlockInfoByHashResponse); ok {
+ r0 = rf(ctx, req)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*ffcapi.GetBlockInfoByHashResponse)
+ }
+ }
+
+ var r1 ffcapi.ErrorReason
+ if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.GetBlockInfoByHashRequest) ffcapi.ErrorReason); ok {
+ r1 = rf(ctx, req)
+ } else {
+ r1 = ret.Get(1).(ffcapi.ErrorReason)
+ }
+
+ var r2 error
+ if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.GetBlockInfoByHashRequest) error); ok {
+ r2 = rf(ctx, req)
+ } else {
+ r2 = ret.Error(2)
+ }
+
+ return r0, r1, r2
+}
+
+// GetBlockInfoByNumber provides a mock function with given fields: ctx, req
+func (_m *API) GetBlockInfoByNumber(ctx context.Context, req *ffcapi.GetBlockInfoByNumberRequest) (*ffcapi.GetBlockInfoByNumberResponse, ffcapi.ErrorReason, error) {
+ ret := _m.Called(ctx, req)
+
+ var r0 *ffcapi.GetBlockInfoByNumberResponse
+ if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.GetBlockInfoByNumberRequest) *ffcapi.GetBlockInfoByNumberResponse); ok {
+ r0 = rf(ctx, req)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*ffcapi.GetBlockInfoByNumberResponse)
+ }
+ }
+
+ var r1 ffcapi.ErrorReason
+ if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.GetBlockInfoByNumberRequest) ffcapi.ErrorReason); ok {
+ r1 = rf(ctx, req)
+ } else {
+ r1 = ret.Get(1).(ffcapi.ErrorReason)
+ }
+
+ var r2 error
+ if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.GetBlockInfoByNumberRequest) error); ok {
+ r2 = rf(ctx, req)
+ } else {
+ r2 = ret.Error(2)
+ }
+
+ return r0, r1, r2
+}
+
+// GetNewBlockHashes provides a mock function with given fields: ctx, req
+func (_m *API) GetNewBlockHashes(ctx context.Context, req *ffcapi.GetNewBlockHashesRequest) (*ffcapi.GetNewBlockHashesResponse, ffcapi.ErrorReason, error) {
+ ret := _m.Called(ctx, req)
+
+ var r0 *ffcapi.GetNewBlockHashesResponse
+ if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.GetNewBlockHashesRequest) *ffcapi.GetNewBlockHashesResponse); ok {
+ r0 = rf(ctx, req)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*ffcapi.GetNewBlockHashesResponse)
+ }
+ }
+
+ var r1 ffcapi.ErrorReason
+ if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.GetNewBlockHashesRequest) ffcapi.ErrorReason); ok {
+ r1 = rf(ctx, req)
+ } else {
+ r1 = ret.Get(1).(ffcapi.ErrorReason)
+ }
+
+ var r2 error
+ if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.GetNewBlockHashesRequest) error); ok {
+ r2 = rf(ctx, req)
+ } else {
+ r2 = ret.Error(2)
+ }
+
+ return r0, r1, r2
+}
+
+// GetNextNonce provides a mock function with given fields: ctx, req
+func (_m *API) GetNextNonce(ctx context.Context, req *ffcapi.GetNextNonceRequest) (*ffcapi.GetNextNonceResponse, ffcapi.ErrorReason, error) {
+ ret := _m.Called(ctx, req)
+
+ var r0 *ffcapi.GetNextNonceResponse
+ if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.GetNextNonceRequest) *ffcapi.GetNextNonceResponse); ok {
+ r0 = rf(ctx, req)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*ffcapi.GetNextNonceResponse)
+ }
+ }
+
+ var r1 ffcapi.ErrorReason
+ if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.GetNextNonceRequest) ffcapi.ErrorReason); ok {
+ r1 = rf(ctx, req)
+ } else {
+ r1 = ret.Get(1).(ffcapi.ErrorReason)
+ }
+
+ var r2 error
+ if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.GetNextNonceRequest) error); ok {
+ r2 = rf(ctx, req)
+ } else {
+ r2 = ret.Error(2)
+ }
+
+ return r0, r1, r2
+}
+
+// GetReceipt provides a mock function with given fields: ctx, req
+func (_m *API) GetReceipt(ctx context.Context, req *ffcapi.GetReceiptRequest) (*ffcapi.GetReceiptResponse, ffcapi.ErrorReason, error) {
+ ret := _m.Called(ctx, req)
+
+ var r0 *ffcapi.GetReceiptResponse
+ if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.GetReceiptRequest) *ffcapi.GetReceiptResponse); ok {
+ r0 = rf(ctx, req)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*ffcapi.GetReceiptResponse)
+ }
+ }
+
+ var r1 ffcapi.ErrorReason
+ if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.GetReceiptRequest) ffcapi.ErrorReason); ok {
+ r1 = rf(ctx, req)
+ } else {
+ r1 = ret.Get(1).(ffcapi.ErrorReason)
+ }
+
+ var r2 error
+ if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.GetReceiptRequest) error); ok {
+ r2 = rf(ctx, req)
+ } else {
+ r2 = ret.Error(2)
+ }
+
+ return r0, r1, r2
+}
+
+// PrepareTransaction provides a mock function with given fields: ctx, req
+func (_m *API) PrepareTransaction(ctx context.Context, req *ffcapi.PrepareTransactionRequest) (*ffcapi.PrepareTransactionResponse, ffcapi.ErrorReason, error) {
+ ret := _m.Called(ctx, req)
+
+ var r0 *ffcapi.PrepareTransactionResponse
+ if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.PrepareTransactionRequest) *ffcapi.PrepareTransactionResponse); ok {
+ r0 = rf(ctx, req)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*ffcapi.PrepareTransactionResponse)
+ }
+ }
+
+ var r1 ffcapi.ErrorReason
+ if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.PrepareTransactionRequest) ffcapi.ErrorReason); ok {
+ r1 = rf(ctx, req)
+ } else {
+ r1 = ret.Get(1).(ffcapi.ErrorReason)
+ }
+
+ var r2 error
+ if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.PrepareTransactionRequest) error); ok {
+ r2 = rf(ctx, req)
+ } else {
+ r2 = ret.Error(2)
+ }
+
+ return r0, r1, r2
+}
+
+// SendTransaction provides a mock function with given fields: ctx, req
+func (_m *API) SendTransaction(ctx context.Context, req *ffcapi.SendTransactionRequest) (*ffcapi.SendTransactionResponse, ffcapi.ErrorReason, error) {
+ ret := _m.Called(ctx, req)
+
+ var r0 *ffcapi.SendTransactionResponse
+ if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.SendTransactionRequest) *ffcapi.SendTransactionResponse); ok {
+ r0 = rf(ctx, req)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(*ffcapi.SendTransactionResponse)
+ }
+ }
+
+ var r1 ffcapi.ErrorReason
+ if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.SendTransactionRequest) ffcapi.ErrorReason); ok {
+ r1 = rf(ctx, req)
+ } else {
+ r1 = ret.Get(1).(ffcapi.ErrorReason)
+ }
+
+ var r2 error
+ if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.SendTransactionRequest) error); ok {
+ r2 = rf(ctx, req)
+ } else {
+ r2 = ret.Error(2)
+ }
+
+ return r0, r1, r2
+}
diff --git a/mocks/managermocks/manager.go b/mocks/managermocks/manager.go
new file mode 100644
index 00000000..ad4dd342
--- /dev/null
+++ b/mocks/managermocks/manager.go
@@ -0,0 +1,29 @@
+// Code generated by mockery v1.0.0. DO NOT EDIT.
+
+package managermocks
+
+import mock "github.com/stretchr/testify/mock"
+
+// Manager is an autogenerated mock type for the Manager type
+type Manager struct {
+ mock.Mock
+}
+
+// Start provides a mock function with given fields:
+func (_m *Manager) Start() {
+ _m.Called()
+}
+
+// WaitStop provides a mock function with given fields:
+func (_m *Manager) WaitStop() error {
+ ret := _m.Called()
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func() error); ok {
+ r0 = rf()
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
diff --git a/mocks/policyenginemocks/policy_engine.go b/mocks/policyenginemocks/policy_engine.go
new file mode 100644
index 00000000..1b64ed23
--- /dev/null
+++ b/mocks/policyenginemocks/policy_engine.go
@@ -0,0 +1,38 @@
+// Code generated by mockery v1.0.0. DO NOT EDIT.
+
+package policyenginemocks
+
+import (
+ context "context"
+
+ ffcapi "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi"
+ fftm "github.com/hyperledger/firefly-transaction-manager/pkg/fftm"
+
+ mock "github.com/stretchr/testify/mock"
+)
+
+// PolicyEngine is an autogenerated mock type for the PolicyEngine type
+type PolicyEngine struct {
+ mock.Mock
+}
+
+// Execute provides a mock function with given fields: ctx, cAPI, mtx
+func (_m *PolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx *fftm.ManagedTXOutput) (bool, error) {
+ ret := _m.Called(ctx, cAPI, mtx)
+
+ var r0 bool
+ if rf, ok := ret.Get(0).(func(context.Context, ffcapi.API, *fftm.ManagedTXOutput) bool); ok {
+ r0 = rf(ctx, cAPI, mtx)
+ } else {
+ r0 = ret.Get(0).(bool)
+ }
+
+ var r1 error
+ if rf, ok := ret.Get(1).(func(context.Context, ffcapi.API, *fftm.ManagedTXOutput) error); ok {
+ r1 = rf(ctx, cAPI, mtx)
+ } else {
+ r1 = ret.Error(1)
+ }
+
+ return r0, r1
+}
diff --git a/pkg/ffcapi/api_common.go b/pkg/ffcapi/api_common.go
new file mode 100644
index 00000000..9903dfdd
--- /dev/null
+++ b/pkg/ffcapi/api_common.go
@@ -0,0 +1,133 @@
+// 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 ffcapi
+
+import (
+ "github.com/hyperledger/firefly/pkg/fftypes"
+)
+
+// RequestType for each request is defined in the individual file
+type RequestType string
+
+// Semver API versioning
+type Version string
+
+const (
+ Version1_0_0 Version = "v1.0.0"
+)
+
+const VersionCurrent = Version1_0_0
+
+type Variant string
+
+const (
+ VariantEVM Variant = "evm"
+)
+
+// ErrorReason are a set of standard error conditions that a blockchain connector can return
+// from execution, that affect the action of the transaction manager to the response.
+// It is important that error mapping is performed for each of these classification
+type ErrorReason string
+
+const (
+ // ErrorReasonInvalidInputs transaction inputs could not be parsed by the connector according to the interface (nothing was sent to the blockchain)
+ ErrorReasonInvalidInputs ErrorReason = "invalid_inputs"
+ // ErrorReasonTransactionReverted on-chain execution (only expected to be returned when the connector is doing gas estimation, or executing a query)
+ ErrorReasonTransactionReverted ErrorReason = "transaction_reverted"
+ // ErrorReasonNonceTooLow on transaction submission, if the nonce has already been used for a transaction that has made it into a block on canonical chain known to the local node
+ ErrorReasonNonceTooLow ErrorReason = "nonce_too_low"
+ // ErrorReasonTransactionUnderpriced if the transaction is rejected due to too low gas price. Either because it was too low according to the minimum configured on the node, or because it's a rescue transaction without a price bump.
+ ErrorReasonTransactionUnderpriced ErrorReason = "transaction_underpriced"
+ // ErrorReasonNotFound if the requested object (block/receipt etc.) was not found
+ ErrorReasonNotFound ErrorReason = "not_found"
+)
+
+// Header is included consistently as a "ffcapi" structure on each request
+type Header struct {
+ RequestID *fftypes.UUID `json:"id"` // Unique for each request
+ Version Version `json:"version"` // The API version
+ Variant Variant `json:"variant"` // Defines the format of the input/output bodies, which FFTM operates pass-through on from FireFly core to the Blockchain connector
+ RequestType RequestType `json:"type"` // The type of the request, which defines how it should be processed, and the structure of the rest of the payload
+}
+
+// TransactionInput is a standardized set of parameters that describe a transaction submission to a blockchain.
+// For convenience, ths structure is compatible with the EthConnect `SendTransaction` structure, for the subset of usage made by FireFly core / Tokens connectors.
+// - Numberic values such as nonce/gas/gasPrice, are all passed as string encoded Base 10 integers
+// - From/To are passed as strings, and are pass-through for FFTM from the values it receives from FireFly core after signing key resolution
+// - The interface is a structure describing the method to invoke. The `variant` in the header tells you how to decode it. For variant=evm it will be an ABI method definition
+// - The supplied value is passed through for each input parameter. It could be any JSON type (simple number/boolean/string, or complex object/array). The blockchain connection is responsible for serializing these according to the rules in the interface.
+type TransactionInput struct {
+ GasPrice *fftypes.JSONAny `json:"gasPrice,omitempty"` // can be a simple string/number, or a complex object - contract is between policy engine and blockchain connector
+ TransactionPrepareInputs
+}
+
+type TransactionPrepareInputs struct {
+ From string `json:"from"`
+ To string `json:"to"`
+ Nonce *fftypes.FFBigInt `json:"nonce"`
+ Gas *fftypes.FFBigInt `json:"gas,omitempty"`
+ Value *fftypes.FFBigInt `json:"value"`
+ Method fftypes.JSONAny `json:"method"`
+ Params []fftypes.JSONAny `json:"params"`
+}
+
+// ErrorResponse allows blockchain connectors to encode useful information about an error in a JSON response body.
+// This should be accompanied with a suitable non-success HTTP response code. However, the "reason" (if supplied)
+// is the only information that will be used to change the transaction manager's handling of the error.
+type ErrorResponse struct {
+ Reason ErrorReason `json:"reason,omitempty"`
+ Error string `json:"error"`
+}
+
+type RequestBase struct {
+ FFCAPI Header `json:"ffcapi"`
+}
+
+func (r *RequestBase) FFCAPIHeader() *Header {
+ return &r.FFCAPI
+}
+
+type ResponseBase struct {
+ ErrorResponse
+}
+
+func (r *ResponseBase) ErrorMessage() string {
+ return r.Error
+}
+
+func (r *ResponseBase) ErrorReason() ErrorReason {
+ return r.Reason
+}
+
+type ffcapiRequest interface {
+ FFCAPIHeader() *Header
+ RequestType() RequestType
+}
+
+type ffcapiResponse interface {
+ ErrorMessage() string
+ ErrorReason() ErrorReason
+}
+
+type RequestID string
+
+func initHeader(header *Header, variant Variant, requestType RequestType) {
+ header.RequestID = fftypes.NewUUID()
+ header.Version = VersionCurrent
+ header.Variant = variant
+ header.RequestType = requestType
+}
diff --git a/pkg/ffcapi/apiclient.go b/pkg/ffcapi/apiclient.go
new file mode 100644
index 00000000..d916e572
--- /dev/null
+++ b/pkg/ffcapi/apiclient.go
@@ -0,0 +1,78 @@
+// 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 ffcapi
+
+import (
+ "context"
+ "strings"
+
+ "github.com/go-resty/resty/v2"
+ "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig"
+ "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs"
+ "github.com/hyperledger/firefly/pkg/config"
+ "github.com/hyperledger/firefly/pkg/ffresty"
+ "github.com/hyperledger/firefly/pkg/i18n"
+)
+
+type API interface {
+ CreateBlockListener(ctx context.Context, req *CreateBlockListenerRequest) (*CreateBlockListenerResponse, ErrorReason, error)
+ ExecQuery(ctx context.Context, req *ExecQueryRequest) (*ExecQueryResponse, ErrorReason, error)
+ GetBlockInfoByHash(ctx context.Context, req *GetBlockInfoByHashRequest) (*GetBlockInfoByHashResponse, ErrorReason, error)
+ GetBlockInfoByNumber(ctx context.Context, req *GetBlockInfoByNumberRequest) (*GetBlockInfoByNumberResponse, ErrorReason, error)
+ GetNewBlockHashes(ctx context.Context, req *GetNewBlockHashesRequest) (*GetNewBlockHashesResponse, ErrorReason, error)
+ GetNextNonce(ctx context.Context, req *GetNextNonceRequest) (*GetNextNonceResponse, ErrorReason, error)
+ GetReceipt(ctx context.Context, req *GetReceiptRequest) (*GetReceiptResponse, ErrorReason, error)
+ PrepareTransaction(ctx context.Context, req *PrepareTransactionRequest) (*PrepareTransactionResponse, ErrorReason, error)
+ SendTransaction(ctx context.Context, req *SendTransactionRequest) (*SendTransactionResponse, ErrorReason, error)
+}
+
+type api struct {
+ client *resty.Client
+ variant Variant
+}
+
+func NewFFCAPI(ctx context.Context) API {
+ return newAPI(ctx, tmconfig.ConnectorPrefix)
+}
+
+func newAPI(ctx context.Context, prefix config.Prefix) *api {
+ return &api{
+ client: ffresty.New(ctx, prefix),
+ variant: Variant(config.GetString(tmconfig.ConnectorVariant)),
+ }
+}
+
+func (a *api) invokeAPI(ctx context.Context, input ffcapiRequest, output ffcapiResponse) (ErrorReason, error) {
+
+ initHeader(input.FFCAPIHeader(), a.variant, input.RequestType())
+ res, err := a.client.R().
+ SetBody(input).
+ SetResult(output).
+ SetError(output).
+ Post("/")
+ if err != nil {
+ return "", i18n.WrapError(ctx, err, tmmsgs.MsgConnectorFailInvoke, input.FFCAPIHeader().RequestID)
+ }
+ if !strings.Contains(res.Header().Get("Content-Type"), "application/json") {
+ return "", i18n.NewError(ctx, tmmsgs.MsgConnectorInvalidConentType, input.FFCAPIHeader().RequestID, res.Header().Get("Content-Type"))
+ }
+ if res.IsError() {
+ return output.ErrorReason(), i18n.NewError(ctx, tmmsgs.MsgConnectorError, input.FFCAPIHeader().RequestID, output.ErrorReason(), output.ErrorMessage())
+ }
+
+ return "", nil
+}
diff --git a/pkg/ffcapi/apiclient_test.go b/pkg/ffcapi/apiclient_test.go
new file mode 100644
index 00000000..103290fd
--- /dev/null
+++ b/pkg/ffcapi/apiclient_test.go
@@ -0,0 +1,85 @@
+// 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 ffcapi
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig"
+ "github.com/hyperledger/firefly/pkg/config"
+ "github.com/hyperledger/firefly/pkg/ffresty"
+ "github.com/stretchr/testify/assert"
+)
+
+func newTestClient(t *testing.T, response ffcapiResponse) (*api, func()) {
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ assert.Contains(t, "application/json", r.Header.Get("Content-Type"))
+ assert.Equal(t, http.MethodPost, r.Method)
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ resBytes, err := json.Marshal(response)
+ w.Header().Set("Content-Length", fmt.Sprintf("%d", len(resBytes)))
+ if response.ErrorMessage() != "" {
+ w.WriteHeader(500)
+ }
+ _, err = w.Write(resBytes)
+ assert.NoError(t, err)
+ }))
+ prefix := config.NewPluginConfig("unittest")
+ ffresty.InitPrefix(prefix)
+ prefix.Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr()))
+ ctx := context.Background()
+ api := newAPI(ctx, prefix)
+ return api, server.Close
+}
+
+func TestBadResponseContentType(t *testing.T) {
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Write([]byte("not JSON"))
+ }))
+ defer server.Close()
+ prefix := config.NewPluginConfig("unittest")
+ ffresty.InitPrefix(prefix)
+ prefix.Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr()))
+ ctx := context.Background()
+
+ api := newAPI(ctx, prefix)
+ _, err := api.invokeAPI(ctx, &ExecQueryRequest{}, &ResponseBase{})
+ assert.Regexp(t, "FF201032", err)
+
+}
+
+func TestBadResponseError(t *testing.T) {
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Write([]byte("not JSON"))
+ }))
+ tmconfig.Reset()
+ tmconfig.ConnectorPrefix.Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr()))
+ ctx := context.Background()
+
+ server.Close()
+ api := NewFFCAPI(ctx)
+ _, _, err := api.ExecQuery(ctx, &ExecQueryRequest{})
+ assert.Regexp(t, "FF201033", err)
+
+}
diff --git a/pkg/ffcapi/create_block_listener.go b/pkg/ffcapi/create_block_listener.go
new file mode 100644
index 00000000..52256061
--- /dev/null
+++ b/pkg/ffcapi/create_block_listener.go
@@ -0,0 +1,43 @@
+// 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 ffcapi
+
+import "context"
+
+type CreateBlockListenerRequest struct {
+ RequestBase
+}
+
+type CreateBlockListenerResponse struct {
+ ResponseBase
+ ListenerID string `json:"listenerId"`
+}
+
+const RequestTypeCreateBlockListener = "create_block_listener"
+
+func (r *CreateBlockListenerRequest) RequestType() RequestType {
+ return RequestTypeCreateBlockListener
+}
+
+func (a *api) CreateBlockListener(ctx context.Context, req *CreateBlockListenerRequest) (*CreateBlockListenerResponse, ErrorReason, error) {
+ res := &CreateBlockListenerResponse{}
+ reason, err := a.invokeAPI(ctx, req, res)
+ if err != nil {
+ return nil, reason, err
+ }
+ return res, "", nil
+}
diff --git a/pkg/ffcapi/create_block_listener_test.go b/pkg/ffcapi/create_block_listener_test.go
new file mode 100644
index 00000000..e11774ff
--- /dev/null
+++ b/pkg/ffcapi/create_block_listener_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 ffcapi
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCreateBlockListenerOK(t *testing.T) {
+ a, cancel := newTestClient(t, &CreateBlockListenerResponse{
+ ListenerID: "0x12345",
+ })
+ defer cancel()
+ res, reason, err := a.CreateBlockListener(context.Background(), &CreateBlockListenerRequest{})
+ assert.NoError(t, err)
+ assert.Empty(t, reason)
+ assert.Equal(t, "0x12345", res.ListenerID)
+}
+
+func TestCreateBlockListenerFail(t *testing.T) {
+ a, cancel := newTestClient(t, &ResponseBase{
+ ErrorResponse: ErrorResponse{
+ Error: "pop",
+ Reason: ErrorReasonInvalidInputs,
+ },
+ })
+ defer cancel()
+ _, reason, err := a.CreateBlockListener(context.Background(), &CreateBlockListenerRequest{})
+ assert.Equal(t, ErrorReasonInvalidInputs, reason)
+ assert.Regexp(t, "FF201012.*pop", err)
+}
diff --git a/pkg/ffcapi/exec_query.go b/pkg/ffcapi/exec_query.go
new file mode 100644
index 00000000..b0589bea
--- /dev/null
+++ b/pkg/ffcapi/exec_query.go
@@ -0,0 +1,59 @@
+// 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 ffcapi
+
+import (
+ "context"
+
+ "github.com/hyperledger/firefly/pkg/fftypes"
+)
+
+// ExecQueryRequest requests execution of a smart contract method in order to either:
+// 1) Query state
+// 2) Attempt to extract the revert reason from an on-chain failure to execute a transaction
+//
+// See the list of standard error reasons that should be returned for situations that can be
+// detected by the back-end connector.
+type ExecQueryRequest struct {
+ RequestBase
+ TransactionInput
+ BlockNumber **fftypes.FFBigInt `json:"blockNumber,omitempty"`
+}
+
+type ExecQueryResponse struct {
+ ResponseBase
+ Valid bool `json:"valid"` // false if the inputs could not be parsed
+ ValidationError string `json:"validationError,omitempty"`
+ Success bool `json:"success"`
+ OnchainError string `json:"onchainError,omitempty"`
+ Outputs []fftypes.JSONAny `json:"outputs"`
+}
+
+const RequestTypeExecQuery RequestType = "exec_query"
+
+func (r *ExecQueryRequest) RequestType() RequestType {
+ return RequestTypeExecQuery
+}
+
+func (a *api) ExecQuery(ctx context.Context, req *ExecQueryRequest) (*ExecQueryResponse, ErrorReason, error) {
+ res := &ExecQueryResponse{}
+ reason, err := a.invokeAPI(ctx, req, res)
+ if err != nil {
+ return nil, reason, err
+ }
+ return res, "", nil
+}
diff --git a/pkg/ffcapi/exec_query_test.go b/pkg/ffcapi/exec_query_test.go
new file mode 100644
index 00000000..761b6545
--- /dev/null
+++ b/pkg/ffcapi/exec_query_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 ffcapi
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestExecQueryOK(t *testing.T) {
+ a, cancel := newTestClient(t, &ExecQueryResponse{
+ Success: true,
+ })
+ defer cancel()
+ res, reason, err := a.ExecQuery(context.Background(), &ExecQueryRequest{})
+ assert.NoError(t, err)
+ assert.Empty(t, reason)
+ assert.True(t, res.Success)
+}
+
+func TestExecQueryFail(t *testing.T) {
+ a, cancel := newTestClient(t, &ResponseBase{
+ ErrorResponse: ErrorResponse{
+ Error: "pop",
+ Reason: ErrorReasonInvalidInputs,
+ },
+ })
+ defer cancel()
+ _, reason, err := a.ExecQuery(context.Background(), &ExecQueryRequest{})
+ assert.Equal(t, ErrorReasonInvalidInputs, reason)
+ assert.Regexp(t, "FF201012.*pop", err)
+}
diff --git a/pkg/ffcapi/get_block_info_by_hash.go b/pkg/ffcapi/get_block_info_by_hash.go
new file mode 100644
index 00000000..447d447e
--- /dev/null
+++ b/pkg/ffcapi/get_block_info_by_hash.go
@@ -0,0 +1,50 @@
+// 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 ffcapi
+
+import (
+ "context"
+
+ "github.com/hyperledger/firefly/pkg/fftypes"
+)
+
+type GetBlockInfoByHashRequest struct {
+ RequestBase
+ BlockHash string `json:"blockHash"`
+}
+
+type GetBlockInfoByHashResponse struct {
+ ResponseBase
+ BlockNumber *fftypes.FFBigInt `json:"blockNumber"`
+ BlockHash string `json:"blockHash"`
+ ParentHash string `json:"parentHash"`
+}
+
+const RequestTypeGetBlockInfoByHash RequestType = "get_block_info_by_hash"
+
+func (r *GetBlockInfoByHashRequest) RequestType() RequestType {
+ return RequestTypeGetBlockInfoByHash
+}
+
+func (a *api) GetBlockInfoByHash(ctx context.Context, req *GetBlockInfoByHashRequest) (*GetBlockInfoByHashResponse, ErrorReason, error) {
+ res := &GetBlockInfoByHashResponse{}
+ reason, err := a.invokeAPI(ctx, req, res)
+ if err != nil {
+ return nil, reason, err
+ }
+ return res, "", nil
+}
diff --git a/pkg/ffcapi/get_block_info_by_hash_test.go b/pkg/ffcapi/get_block_info_by_hash_test.go
new file mode 100644
index 00000000..b55acd9f
--- /dev/null
+++ b/pkg/ffcapi/get_block_info_by_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 ffcapi
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetBlockInfoByHashOK(t *testing.T) {
+ a, cancel := newTestClient(t, &GetBlockInfoByHashResponse{
+ BlockHash: "0x12345",
+ })
+ defer cancel()
+ res, reason, err := a.GetBlockInfoByHash(context.Background(), &GetBlockInfoByHashRequest{})
+ assert.NoError(t, err)
+ assert.Empty(t, reason)
+ assert.Equal(t, "0x12345", res.BlockHash)
+}
+
+func TestGetBlockInfoByHashFail(t *testing.T) {
+ a, cancel := newTestClient(t, &ResponseBase{
+ ErrorResponse: ErrorResponse{
+ Error: "pop",
+ Reason: ErrorReasonInvalidInputs,
+ },
+ })
+ defer cancel()
+ _, reason, err := a.GetBlockInfoByHash(context.Background(), &GetBlockInfoByHashRequest{})
+ assert.Equal(t, ErrorReasonInvalidInputs, reason)
+ assert.Regexp(t, "FF201012.*pop", err)
+}
diff --git a/pkg/ffcapi/get_block_info_by_number.go b/pkg/ffcapi/get_block_info_by_number.go
new file mode 100644
index 00000000..fb636ee4
--- /dev/null
+++ b/pkg/ffcapi/get_block_info_by_number.go
@@ -0,0 +1,50 @@
+// 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 ffcapi
+
+import (
+ "context"
+
+ "github.com/hyperledger/firefly/pkg/fftypes"
+)
+
+type GetBlockInfoByNumberRequest struct {
+ RequestBase
+ BlockNumber *fftypes.FFBigInt `json:"blockNumber"`
+}
+
+type GetBlockInfoByNumberResponse struct {
+ ResponseBase
+ BlockNumber *fftypes.FFBigInt `json:"blockNumber"`
+ BlockHash string `json:"blockHash"`
+ ParentHash string `json:"parentHash"`
+}
+
+const RequestTypeGetBlockInfoByNumber RequestType = "get_block_info_by_number"
+
+func (r *GetBlockInfoByNumberRequest) RequestType() RequestType {
+ return RequestTypeGetBlockInfoByNumber
+}
+
+func (a *api) GetBlockInfoByNumber(ctx context.Context, req *GetBlockInfoByNumberRequest) (*GetBlockInfoByNumberResponse, ErrorReason, error) {
+ res := &GetBlockInfoByNumberResponse{}
+ reason, err := a.invokeAPI(ctx, req, res)
+ if err != nil {
+ return nil, reason, err
+ }
+ return res, "", nil
+}
diff --git a/pkg/ffcapi/get_block_info_by_number_test.go b/pkg/ffcapi/get_block_info_by_number_test.go
new file mode 100644
index 00000000..86984376
--- /dev/null
+++ b/pkg/ffcapi/get_block_info_by_number_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 ffcapi
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetBlockInfoByNumberOK(t *testing.T) {
+ a, cancel := newTestClient(t, &GetBlockInfoByNumberResponse{
+ BlockHash: "0x12345",
+ })
+ defer cancel()
+ res, reason, err := a.GetBlockInfoByNumber(context.Background(), &GetBlockInfoByNumberRequest{})
+ assert.NoError(t, err)
+ assert.Empty(t, reason)
+ assert.Equal(t, "0x12345", res.BlockHash)
+}
+
+func TestGetBlockInfoByNumberFail(t *testing.T) {
+ a, cancel := newTestClient(t, &ResponseBase{
+ ErrorResponse: ErrorResponse{
+ Error: "pop",
+ Reason: ErrorReasonInvalidInputs,
+ },
+ })
+ defer cancel()
+ _, reason, err := a.GetBlockInfoByNumber(context.Background(), &GetBlockInfoByNumberRequest{})
+ assert.Equal(t, ErrorReasonInvalidInputs, reason)
+ assert.Regexp(t, "FF201012.*pop", err)
+}
diff --git a/pkg/ffcapi/get_new_block_hashes.go b/pkg/ffcapi/get_new_block_hashes.go
new file mode 100644
index 00000000..53e7358e
--- /dev/null
+++ b/pkg/ffcapi/get_new_block_hashes.go
@@ -0,0 +1,44 @@
+// 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 ffcapi
+
+import "context"
+
+type GetNewBlockHashesRequest struct {
+ RequestBase
+ ListenerID string `json:"listenerId"`
+}
+
+type GetNewBlockHashesResponse struct {
+ ResponseBase
+ BlockHashes []string `json:"blockHashes"`
+}
+
+const RequestTypeGetNewBlockHashes RequestType = "get_new_block_hashes"
+
+func (r *GetNewBlockHashesRequest) RequestType() RequestType {
+ return RequestTypeGetNewBlockHashes
+}
+
+func (a *api) GetNewBlockHashes(ctx context.Context, req *GetNewBlockHashesRequest) (*GetNewBlockHashesResponse, ErrorReason, error) {
+ res := &GetNewBlockHashesResponse{}
+ reason, err := a.invokeAPI(ctx, req, res)
+ if err != nil {
+ return nil, reason, err
+ }
+ return res, "", nil
+}
diff --git a/pkg/ffcapi/get_new_block_hashes_test.go b/pkg/ffcapi/get_new_block_hashes_test.go
new file mode 100644
index 00000000..3f71e7e7
--- /dev/null
+++ b/pkg/ffcapi/get_new_block_hashes_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 ffcapi
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetNewBlockHashesOK(t *testing.T) {
+ a, cancel := newTestClient(t, &GetNewBlockHashesResponse{
+ BlockHashes: []string{"test"},
+ })
+ defer cancel()
+ res, reason, err := a.GetNewBlockHashes(context.Background(), &GetNewBlockHashesRequest{})
+ assert.NoError(t, err)
+ assert.Empty(t, reason)
+ assert.Equal(t, []string{"test"}, res.BlockHashes)
+}
+
+func TestGetNewBlockHashesFail(t *testing.T) {
+ a, cancel := newTestClient(t, &ResponseBase{
+ ErrorResponse: ErrorResponse{
+ Error: "pop",
+ Reason: ErrorReasonInvalidInputs,
+ },
+ })
+ defer cancel()
+ _, reason, err := a.GetNewBlockHashes(context.Background(), &GetNewBlockHashesRequest{})
+ assert.Equal(t, ErrorReasonInvalidInputs, reason)
+ assert.Regexp(t, "FF201012.*pop", err)
+}
diff --git a/pkg/ffcapi/get_next_nonce.go b/pkg/ffcapi/get_next_nonce.go
new file mode 100644
index 00000000..f345cce1
--- /dev/null
+++ b/pkg/ffcapi/get_next_nonce.go
@@ -0,0 +1,51 @@
+// 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 ffcapi
+
+import (
+ "context"
+
+ "github.com/hyperledger/firefly/pkg/fftypes"
+)
+
+// GetNextNonceRequest used to do a query for the next nonce to use for a
+// given signing identity. This is only used when there are no pending
+// operations outstanding for this signer known to the transaction manager.
+type GetNextNonceRequest struct {
+ RequestBase
+ Signer string `json:"signer"`
+}
+
+type GetNextNonceResponse struct {
+ ResponseBase
+ Nonce *fftypes.FFBigInt `json:"nonce"`
+}
+
+const RequestTypeGetNextNonce RequestType = "get_next_nonce"
+
+func (r *GetNextNonceRequest) RequestType() RequestType {
+ return RequestTypeGetNextNonce
+}
+
+func (a *api) GetNextNonce(ctx context.Context, req *GetNextNonceRequest) (*GetNextNonceResponse, ErrorReason, error) {
+ res := &GetNextNonceResponse{}
+ reason, err := a.invokeAPI(ctx, req, res)
+ if err != nil {
+ return nil, reason, err
+ }
+ return res, "", nil
+}
diff --git a/pkg/ffcapi/get_next_nonce_test.go b/pkg/ffcapi/get_next_nonce_test.go
new file mode 100644
index 00000000..2730cd56
--- /dev/null
+++ b/pkg/ffcapi/get_next_nonce_test.go
@@ -0,0 +1,49 @@
+// 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 ffcapi
+
+import (
+ "context"
+ "testing"
+
+ "github.com/hyperledger/firefly/pkg/fftypes"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetNextNonceOK(t *testing.T) {
+ a, cancel := newTestClient(t, &GetNextNonceResponse{
+ Nonce: fftypes.NewFFBigInt(10),
+ })
+ defer cancel()
+ res, reason, err := a.GetNextNonce(context.Background(), &GetNextNonceRequest{})
+ assert.NoError(t, err)
+ assert.Empty(t, reason)
+ assert.Equal(t, int64(10), res.Nonce.Int64())
+}
+
+func TestGetNextNonceFail(t *testing.T) {
+ a, cancel := newTestClient(t, &ResponseBase{
+ ErrorResponse: ErrorResponse{
+ Error: "pop",
+ Reason: ErrorReasonInvalidInputs,
+ },
+ })
+ defer cancel()
+ _, reason, err := a.GetNextNonce(context.Background(), &GetNextNonceRequest{})
+ assert.Equal(t, ErrorReasonInvalidInputs, reason)
+ assert.Regexp(t, "FF201012.*pop", err)
+}
diff --git a/pkg/ffcapi/get_receipt.go b/pkg/ffcapi/get_receipt.go
new file mode 100644
index 00000000..c5ee5335
--- /dev/null
+++ b/pkg/ffcapi/get_receipt.go
@@ -0,0 +1,51 @@
+// 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 ffcapi
+
+import (
+ "context"
+
+ "github.com/hyperledger/firefly/pkg/fftypes"
+)
+
+type GetReceiptRequest struct {
+ RequestBase
+ TransactionHash string `json:"transactionHash"`
+}
+
+type GetReceiptResponse struct {
+ ResponseBase
+ BlockNumber *fftypes.FFBigInt `json:"blockNumber"`
+ TransactionIndex *fftypes.FFBigInt `json:"transactinIndex"`
+ BlockHash string `json:"blockHash"`
+ ExtraInfo fftypes.JSONAny `json:"extraInfo"`
+}
+
+const RequestTypeGetReceipt RequestType = "get_receipt"
+
+func (r *GetReceiptRequest) RequestType() RequestType {
+ return RequestTypeGetReceipt
+}
+
+func (a *api) GetReceipt(ctx context.Context, req *GetReceiptRequest) (*GetReceiptResponse, ErrorReason, error) {
+ res := &GetReceiptResponse{}
+ reason, err := a.invokeAPI(ctx, req, res)
+ if err != nil {
+ return nil, reason, err
+ }
+ return res, "", nil
+}
diff --git a/pkg/ffcapi/get_receipt_test.go b/pkg/ffcapi/get_receipt_test.go
new file mode 100644
index 00000000..efb37d6a
--- /dev/null
+++ b/pkg/ffcapi/get_receipt_test.go
@@ -0,0 +1,49 @@
+// 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 ffcapi
+
+import (
+ "context"
+ "testing"
+
+ "github.com/hyperledger/firefly/pkg/fftypes"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetReceiptOK(t *testing.T) {
+ a, cancel := newTestClient(t, &GetReceiptResponse{
+ BlockNumber: fftypes.NewFFBigInt(10),
+ })
+ defer cancel()
+ res, reason, err := a.GetReceipt(context.Background(), &GetReceiptRequest{})
+ assert.NoError(t, err)
+ assert.Empty(t, reason)
+ assert.Equal(t, int64(10), res.BlockNumber.Int64())
+}
+
+func TestGetReceiptFail(t *testing.T) {
+ a, cancel := newTestClient(t, &ResponseBase{
+ ErrorResponse: ErrorResponse{
+ Error: "pop",
+ Reason: ErrorReasonInvalidInputs,
+ },
+ })
+ defer cancel()
+ _, reason, err := a.GetReceipt(context.Background(), &GetReceiptRequest{})
+ assert.Equal(t, ErrorReasonInvalidInputs, reason)
+ assert.Regexp(t, "FF201012.*pop", err)
+}
diff --git a/pkg/ffcapi/prepare_transaction.go b/pkg/ffcapi/prepare_transaction.go
new file mode 100644
index 00000000..6d780593
--- /dev/null
+++ b/pkg/ffcapi/prepare_transaction.go
@@ -0,0 +1,68 @@
+// 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 ffcapi
+
+import (
+ "context"
+
+ "github.com/hyperledger/firefly/pkg/fftypes"
+)
+
+// PrepareTransactionRequest is used to prepare a set of JSON formatted developer friendly
+// inputs, into a raw transaction ready for submission to the blockchain.
+//
+// The connector is responsible for encoding the transaction ready for sumission,
+// and returning the hash for the transaction as well as a string serialization of
+// the pre-signed raw transaction in a format of its own choosing (hex etc.).
+// The hash is expected to be a function of:
+// - the method signature
+// - the signing identity
+// - the nonce
+// - the particular blockchain the transaction is submitted to
+// - the input parameters
+//
+// If "gas" is not supplied, the connector is expected to perform gas estimation
+// prior to generating the payload.
+//
+// See the list of standard error reasons that should be returned for situations that can be
+// detected by the back-end connector.
+type PrepareTransactionRequest struct {
+ RequestBase
+ TransactionPrepareInputs
+}
+
+type PrepareTransactionResponse struct {
+ ResponseBase
+ Gas *fftypes.FFBigInt `json:"gas"`
+ TransactionHash string `json:"transactionHash"`
+ RawTransaction string `json:"rawTransaction"`
+}
+
+const RequestTypePrepareTransaction RequestType = "prepare_transaction"
+
+func (r *PrepareTransactionRequest) RequestType() RequestType {
+ return RequestTypePrepareTransaction
+}
+
+func (a *api) PrepareTransaction(ctx context.Context, req *PrepareTransactionRequest) (*PrepareTransactionResponse, ErrorReason, error) {
+ res := &PrepareTransactionResponse{}
+ reason, err := a.invokeAPI(ctx, req, res)
+ if err != nil {
+ return nil, reason, err
+ }
+ return res, "", nil
+}
diff --git a/pkg/ffcapi/prepare_transaction_test.go b/pkg/ffcapi/prepare_transaction_test.go
new file mode 100644
index 00000000..b0ff6b4a
--- /dev/null
+++ b/pkg/ffcapi/prepare_transaction_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 ffcapi
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPrepareTransactionOK(t *testing.T) {
+ a, cancel := newTestClient(t, &PrepareTransactionResponse{
+ TransactionHash: "0x12345",
+ })
+ defer cancel()
+ res, reason, err := a.PrepareTransaction(context.Background(), &PrepareTransactionRequest{})
+ assert.NoError(t, err)
+ assert.Empty(t, reason)
+ assert.Equal(t, "0x12345", res.TransactionHash)
+}
+
+func TestPrepareTransactionFail(t *testing.T) {
+ a, cancel := newTestClient(t, &ResponseBase{
+ ErrorResponse: ErrorResponse{
+ Error: "pop",
+ Reason: ErrorReasonInvalidInputs,
+ },
+ })
+ defer cancel()
+ _, reason, err := a.PrepareTransaction(context.Background(), &PrepareTransactionRequest{})
+ assert.Equal(t, ErrorReasonInvalidInputs, reason)
+ assert.Regexp(t, "FF201012.*pop", err)
+}
diff --git a/pkg/ffcapi/send_transaction.go b/pkg/ffcapi/send_transaction.go
new file mode 100644
index 00000000..de1bd935
--- /dev/null
+++ b/pkg/ffcapi/send_transaction.go
@@ -0,0 +1,52 @@
+// 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 ffcapi
+
+import (
+ "context"
+
+ "github.com/hyperledger/firefly/pkg/fftypes"
+)
+
+// SendTransactionRequest is used to send a transaction to the blockchain.
+// The connector is responsible for adding it to the transaction pool of the blockchain,
+// noting the transaction hash has already been calculated in the prepare step previously.
+type SendTransactionRequest struct {
+ RequestBase
+ From string `json:"from"`
+ GasPrice *fftypes.JSONAny `json:"gasPrice,omitempty"` // can be a simple string/number, or a complex object - contract is between policy engine and blockchain connector
+ RawTransaction string `json:"rawTransaction"`
+}
+
+type SendTransactionResponse struct {
+ ResponseBase
+}
+
+const RequestTypeSendTransaction RequestType = "send_transaction"
+
+func (r *SendTransactionRequest) RequestType() RequestType {
+ return RequestTypeSendTransaction
+}
+
+func (a *api) SendTransaction(ctx context.Context, req *SendTransactionRequest) (*SendTransactionResponse, ErrorReason, error) {
+ res := &SendTransactionResponse{}
+ reason, err := a.invokeAPI(ctx, req, res)
+ if err != nil {
+ return nil, reason, err
+ }
+ return res, "", nil
+}
diff --git a/pkg/ffcapi/send_transaction_test.go b/pkg/ffcapi/send_transaction_test.go
new file mode 100644
index 00000000..b6678e94
--- /dev/null
+++ b/pkg/ffcapi/send_transaction_test.go
@@ -0,0 +1,46 @@
+// 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 ffcapi
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSendTransactionOK(t *testing.T) {
+ a, cancel := newTestClient(t, &SendTransactionResponse{})
+ defer cancel()
+ res, reason, err := a.SendTransaction(context.Background(), &SendTransactionRequest{})
+ assert.NoError(t, err)
+ assert.Empty(t, reason)
+ assert.NotNil(t, res)
+}
+
+func TestSendTransactionFail(t *testing.T) {
+ a, cancel := newTestClient(t, &ResponseBase{
+ ErrorResponse: ErrorResponse{
+ Error: "pop",
+ Reason: ErrorReasonInvalidInputs,
+ },
+ })
+ defer cancel()
+ _, reason, err := a.SendTransaction(context.Background(), &SendTransactionRequest{})
+ assert.Equal(t, ErrorReasonInvalidInputs, reason)
+ assert.Regexp(t, "FF201012.*pop", err)
+}
diff --git a/pkg/fftm/managed_tx.go b/pkg/fftm/managed_tx.go
new file mode 100644
index 00000000..49b967b4
--- /dev/null
+++ b/pkg/fftm/managed_tx.go
@@ -0,0 +1,42 @@
+// 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 fftm
+
+import (
+ "github.com/hyperledger/firefly-transaction-manager/internal/confirmations"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi"
+ "github.com/hyperledger/firefly/pkg/fftypes"
+)
+
+// ManagedTXOutput is the structure stored into the operation in FireFly, that the policy
+// engine can use to apply policy, and apply updates to
+type ManagedTXOutput struct {
+ FFTMName string `json:"fftmName"`
+ ID *fftypes.UUID `json:"id"`
+ Nonce *fftypes.FFBigInt `json:"nonce"`
+ Gas *fftypes.FFBigInt `json:"gas"`
+ Signer string `json:"signer"`
+ TransactionHash string `json:"transactionHash,omitempty"`
+ RawTransaction string `json:"rawTransaction,omitempty"`
+ GasPrice *fftypes.JSONAny `json:"gasPrice"`
+ PolicyInfo *fftypes.JSONAny `json:"policyInfo"`
+ FirstSubmit *fftypes.FFTime `json:"firstSubmit,omitempty"`
+ LastSubmit *fftypes.FFTime `json:"lastSubmit,omitempty"`
+ Request *TransactionRequest `json:"request,omitempty"`
+ Receipt *ffcapi.GetReceiptResponse `json:"receipt,omitempty"`
+ Confirmations []confirmations.BlockInfo `json:"confirmations,omitempty"`
+}
diff --git a/pkg/fftm/tx_request.go b/pkg/fftm/tx_request.go
new file mode 100644
index 00000000..48efc19b
--- /dev/null
+++ b/pkg/fftm/tx_request.go
@@ -0,0 +1,41 @@
+// 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 fftm
+
+import (
+ "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi"
+ "github.com/hyperledger/firefly/pkg/fftypes"
+)
+
+// TransactionRequest is the external interface into sending transactions to the front-side of Transaction Manager
+// Note this is a deliberate match for the EthConnect subset that is supported by FireFly core
+type TransactionRequest struct {
+ Headers RequestHeaders `json:"headers"`
+ ffcapi.TransactionInput
+}
+
+type RequestHeaders struct {
+ ID *fftypes.UUID `json:"id"`
+ Type RequestType `json:"type"`
+}
+
+type RequestType string
+
+const (
+ RequestTypeSendTransaction = "SendTransaction"
+ RequestTypeQuery = "Query"
+)
diff --git a/pkg/policyengine/policyengine.go b/pkg/policyengine/policyengine.go
new file mode 100644
index 00000000..74bf0de5
--- /dev/null
+++ b/pkg/policyengine/policyengine.go
@@ -0,0 +1,28 @@
+// 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 policyengine
+
+import (
+ "context"
+
+ "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi"
+ "github.com/hyperledger/firefly-transaction-manager/pkg/fftm"
+)
+
+type PolicyEngine interface {
+ Execute(ctx context.Context, cAPI ffcapi.API, mtx *fftm.ManagedTXOutput) (updated bool, err error)
+}
diff --git a/test/empty-config.fftm.yaml b/test/empty-config.fftm.yaml
new file mode 100644
index 00000000..5d47d453
--- /dev/null
+++ b/test/empty-config.fftm.yaml
@@ -0,0 +1,4 @@
+manager:
+ no-name-set: "should have a name to start"
+api:
+ port: 0 # allocate one
\ No newline at end of file
diff --git a/test/firefly.fftm.yaml b/test/firefly.fftm.yaml
new file mode 100644
index 00000000..59c94dd3
--- /dev/null
+++ b/test/firefly.fftm.yaml
@@ -0,0 +1,7 @@
+manager:
+ name: test
+policyengine:
+ simple:
+ fixedGas: 12345
+api:
+ port: 0 # allocate one
\ No newline at end of file
diff --git a/test/quick-fail.fftm.yaml b/test/quick-fail.fftm.yaml
new file mode 100644
index 00000000..5e29eec1
--- /dev/null
+++ b/test/quick-fail.fftm.yaml
@@ -0,0 +1,13 @@
+manager:
+ name: test
+operations:
+ fullScan:
+ startupMaxRetries: 1
+ minimumDelay: 1ms
+policyengine:
+ simple:
+ fixedGas: 12345
+ffcore:
+ url: ":::: not a url"
+api:
+ port: 0 # allocate one
\ No newline at end of file