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. + +Creative Commons License
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. + +Creative Commons License
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