From 1bad8070aa45cd329626f23ff611bca579fcc031 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Tue, 24 May 2022 22:57:46 -0400 Subject: [PATCH 1/4] Initial commit Signed-off-by: Peter Broadhurst --- .dockerignore | 4 + .editorconfig | 7 + .gitattributes | 1 + .github/settings.yml | 14 + .github/workflows/docker_main.yml | 43 + .github/workflows/docker_release.yml | 37 + .github/workflows/go.yml | 28 + .gitignore | 6 + .golangci.yml | 60 ++ .vscode/settings.json | 49 + CHANGELOG.md | 2 +- CODEOWNERS | 4 + CODE_OF_CONDUCT.md | 2 +- CONTRIBUTING.md | 2 +- Dockerfile | 15 + LICENSE | 2 +- MAINTAINERS.md | 52 + Makefile | 50 + README.md | 45 + cmd/config.go | 48 + cmd/config_docs_generate_test.go | 43 + cmd/config_docs_test.go | 46 + cmd/config_test.go | 30 + cmd/evmconnect.go | 152 +++ cmd/evmconnect_test.go | 100 ++ cmd/version.go | 103 ++ cmd/version_test.go | 65 ++ codecov.yml | 10 + config.md | 126 +++ evmconnect/main.go | 32 + go.mod | 53 + go.sum | 935 ++++++++++++++++++ internal/ethereum/config.go | 39 + internal/ethereum/create_block_listener.go | 45 + .../ethereum/create_block_listener_test.go | 84 ++ internal/ethereum/error_mapping.go | 68 ++ internal/ethereum/estimate_gas.go | 52 + internal/ethereum/ethereum.go | 79 ++ internal/ethereum/ethereum_test.go | 42 + internal/ethereum/exec_query.go | 130 +++ internal/ethereum/exec_query_test.go | 112 +++ internal/ethereum/get_block_info.go | 95 ++ internal/ethereum/get_block_info_test.go | 215 ++++ internal/ethereum/get_gas_price.go | 49 + internal/ethereum/get_gas_price_test.go | 84 ++ internal/ethereum/get_new_block_hashes.go | 47 + .../ethereum/get_new_block_hashes_test.go | 103 ++ internal/ethereum/get_next_nonce.go | 46 + internal/ethereum/get_next_nonce_test.go | 84 ++ internal/ethereum/get_receipt.go | 78 ++ internal/ethereum/get_receipt_test.go | 141 +++ internal/ethereum/prepare_transaction.go | 162 +++ internal/ethereum/prepare_transaction_test.go | 227 +++++ internal/ethereum/send_transaction.go | 63 ++ internal/ethereum/send_transaction_test.go | 196 ++++ internal/ffconnector/connector.go | 31 + internal/ffcserver/config.go | 32 + internal/ffcserver/server.go | 175 ++++ internal/ffcserver/server_test.go | 205 ++++ internal/jsonrpc/rpc.go | 107 ++ internal/msgs/en_config_descriptions.go | 43 + internal/msgs/en_error_messges.go | 53 + mocks/ffconnectormocks/connector.go | 50 + mocks/ffcservermocks/server.go | 63 ++ mocks/jsonrpcmocks/client.go | 31 + test/bad-config.evmconnect.yaml | 1 + test/bad-server.evmconnect.yaml | 6 + test/firefly.evmconnect.yaml | 6 + test/unknown-connector.evmconnect.yaml | 2 + 69 files changed, 5208 insertions(+), 4 deletions(-) create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/workflows/docker_main.yml create mode 100644 .github/workflows/docker_release.yml create mode 100644 .github/workflows/go.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 .vscode/settings.json create mode 100644 CODEOWNERS create mode 100644 Dockerfile create mode 100644 MAINTAINERS.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 cmd/config.go create mode 100644 cmd/config_docs_generate_test.go create mode 100644 cmd/config_docs_test.go create mode 100644 cmd/config_test.go create mode 100644 cmd/evmconnect.go create mode 100644 cmd/evmconnect_test.go create mode 100644 cmd/version.go create mode 100644 cmd/version_test.go create mode 100644 codecov.yml create mode 100644 config.md create mode 100644 evmconnect/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/ethereum/config.go create mode 100644 internal/ethereum/create_block_listener.go create mode 100644 internal/ethereum/create_block_listener_test.go create mode 100644 internal/ethereum/error_mapping.go create mode 100644 internal/ethereum/estimate_gas.go create mode 100644 internal/ethereum/ethereum.go create mode 100644 internal/ethereum/ethereum_test.go create mode 100644 internal/ethereum/exec_query.go create mode 100644 internal/ethereum/exec_query_test.go create mode 100644 internal/ethereum/get_block_info.go create mode 100644 internal/ethereum/get_block_info_test.go create mode 100644 internal/ethereum/get_gas_price.go create mode 100644 internal/ethereum/get_gas_price_test.go create mode 100644 internal/ethereum/get_new_block_hashes.go create mode 100644 internal/ethereum/get_new_block_hashes_test.go create mode 100644 internal/ethereum/get_next_nonce.go create mode 100644 internal/ethereum/get_next_nonce_test.go create mode 100644 internal/ethereum/get_receipt.go create mode 100644 internal/ethereum/get_receipt_test.go create mode 100644 internal/ethereum/prepare_transaction.go create mode 100644 internal/ethereum/prepare_transaction_test.go create mode 100644 internal/ethereum/send_transaction.go create mode 100644 internal/ethereum/send_transaction_test.go create mode 100644 internal/ffconnector/connector.go create mode 100644 internal/ffcserver/config.go create mode 100644 internal/ffcserver/server.go create mode 100644 internal/ffcserver/server_test.go create mode 100644 internal/jsonrpc/rpc.go create mode 100644 internal/msgs/en_config_descriptions.go create mode 100644 internal/msgs/en_error_messges.go create mode 100644 mocks/ffconnectormocks/connector.go create mode 100644 mocks/ffcservermocks/server.go create mode 100644 mocks/jsonrpcmocks/client.go create mode 100644 test/bad-config.evmconnect.yaml create mode 100644 test/bad-server.evmconnect.yaml create mode 100644 test/firefly.evmconnect.yaml create mode 100644 test/unknown-connector.evmconnect.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d07bda2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +**/node_modules +**/coverage +**/.nyc_output +firefly-evmconnect \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3098b1b --- /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 0000000..3c6aa9e --- /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 index 010a2de..3d06257 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -1,3 +1,4 @@ +<<<<<<< HEAD # # SPDX-License-Identifier: Apache-2.0 # @@ -15,4 +16,17 @@ repository: private: false allow_squash_merge: true allow_merge_commit: true +======= +repository: + name: firefly-evmconnect + 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 +>>>>>>> 1ac993c (Initial commit) allow_rebase_merge: true diff --git a/.github/workflows/docker_main.yml b/.github/workflows/docker_main.yml new file mode 100644 index 0000000..09042d6 --- /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-evmconnect/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-evmconnect:${{ steps.build_tag_generator.outputs.BUILD_TAG }}" \ + docker + + - name: Tag release + run: docker tag ghcr.io/hyperledger/firefly-evmconnect:${{ steps.build_tag_generator.outputs.BUILD_TAG }} ghcr.io/hyperledger/firefly-evmconnect: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-evmconnect:${{ 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-evmconnect:head diff --git a/.github/workflows/docker_release.yml b/.github/workflows/docker_release.yml new file mode 100644 index 0000000..9178c6f --- /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-evmconnect:${GITHUB_REF##*/}" \ + docker + + - name: Tag release + if: github.event.action == 'released' + run: docker tag ghcr.io/hyperledger/firefly-evmconnect:${GITHUB_REF##*/} ghcr.io/hyperledger/firefly-evmconnect: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-evmconnect:${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-evmconnect:latest diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..91371aa --- /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.17 + + - 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 0000000..a908e70 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +**/*.jar +firefly-evmconnect +coverage.txt +**/debug.test +.DS_Store +__debug* diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..6c6e82f --- /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 0000000..1519a64 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,49 @@ +{ + "go.formatFlags": [ + "-s" + ], + "go.lintTool": "golangci-lint", + "cSpell.words": [ + "btcec", + "ccache", + "Debugf", + "dklen", + "ethsigner", + "ethtypes", + "evmconnect", + "ffcapi", + "ffconnector", + "ffconnectormocks", + "FFCPI", + "ffcserver", + "ffcservermocks", + "ffresty", + "fftypes", + "GJSON", + "httpserver", + "hyperledger", + "Infof", + "jsonrpcmocks", + "Kaleido", + "kdfparams", + "Keccak", + "keystorev", + "logrus", + "Nowarn", + "resty", + "rpcbackendmocks", + "secp", + "signerconfig", + "signermsgs", + "sigs", + "stretchr", + "Tracef", + "ufixed", + "unmarshalled", + "unmarshalling", + "Vyper", + "Warnf", + "wsclient" + ], + "go.testTimeout": "10s" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index de8bc2f..67649c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,3 @@ # Changelog -The changelog will be updated on the next release +[FireFly EVM Connector Releases](https://github.com/hyperledger/firefly-evmconnect/releases) diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..a77ad46 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 + +# FireFly Core Maintainers +* @peterbroadhurst @nguyer @awrichar diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 339bf98..2f1f0e3 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -5,4 +5,4 @@ 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. \ No newline at end of file +Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4479ebe..da89b2d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,4 +7,4 @@ Please visit the [contributors guide](https://labs.hyperledger.org/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. \ No newline at end of file +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 0000000..5c5348e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.17-buster AS builder +ARG BUILD_VERSION +ENV BUILD_VERSION=${BUILD_VERSION} +ADD . /evmconnect +WORKDIR /evmconnect +RUN make + +FROM debian:buster-slim +WORKDIR /evmconnect +RUN apt update -y \ + && apt install -y curl jq \ + && rm -rf /var/lib/apt/lists/* +COPY --from=builder /evmconnect/firefly-evmconnect /usr/bin/evmconnect + +ENTRYPOINT [ "/usr/bin/evmconnect" ] diff --git a/LICENSE b/LICENSE index 261eeb9..25c9ee6 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2022 Kaleido Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000..5ceb7e9 --- /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 0000000..4ffa365 --- /dev/null +++ b/Makefile @@ -0,0 +1,50 @@ +VGO=go +GOFILES := $(shell find cmd internal -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, internal/jsonrpc, Client, jsonrpcmocks)) +$(eval $(call makemock, internal/ffcserver, Server, ffcservermocks)) +$(eval $(call makemock, internal/ffconnector, Connector, ffconnectormocks)) + +firefly-evmconnect: ${GOFILES} + $(VGO) build -o ./firefly-evmconnect -ldflags "-X main.buildDate=`date -u +\"%Y-%m-%dT%H:%M:%SZ\"` -X main.buildVersion=$(BUILD_VERSION)" -tags=prod -tags=prod -v ./evmconnect +go-mod-tidy: .ALWAYS + $(VGO) mod tidy +build: firefly-evmconnect +.ALWAYS: ; +clean: + $(VGO) clean +deps: + $(VGO) get ./evmconnect +docs: + $(VGO) test ./cmd -timeout=10s -tags docs +docker: + docker build --build-arg BUILD_VERSION=${BUILD_VERSION} ${DOCKER_ARGS} -t hyperledger/firefly-evmconnect . diff --git a/README.md b/README.md new file mode 100644 index 0000000..e7567e8 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +[![codecov](https://codecov.io/gh/hyperledger/firefly-evmconnect/branch/main/graph/badge.svg?token=OEI8A08P0R)](https://codecov.io/gh/hyperledger/firefly-evmconnect) +[![Go Reference](https://pkg.go.dev/badge/github.com/hyperledger/firefly-evmconnect.svg)](https://pkg.go.dev/github.com/hyperledger/firefly-evmconnect) + +# Hyperledger FireFly EVM Connector + +This repo provides a reference implementation of the FireFly Connector API (FFCAPI) +for EVM Based blockchains. + +See the [Hyperledger Firefly Documentation](https://hyperledger.github.io/firefly/overview/public_vs_permissioned.html#firefly-architecture-for-public-chains) +and the [FireFly Transaction Manager](https://github.com/hyperledger/firefly-transaction-manager) repository for +more information. + +> Also see [firefly-ethconnect](https://github.com/hyperledger/firefly-ethconnect) for the hardened +> connector optimized for private Ethereum sidechains, optimized for finality assured consensus +> algorithms and throughput. + +# License + +Apache 2.0 + +## ABI Encoding + +A key responsibility of the FFCAPI connector is to map from developer friendly JSON inputs/outputs +down to the binary encoding of the blockchain. + +This repo uses the Apache 2.0 RLP encoding/decoding utilities from the +[firefly-signer](https://github.com/hyperledger/firefly-signer) repository. + +## Configuration + +For a full list of configuration options see [config.md](./config.md) + +## Example configuration + +```yaml +connectors: +connectors: +- type: ethereum + server: + port: 5102 + ethereum: + url: http://localhost:8545 +``` + + diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 0000000..c354b6c --- /dev/null +++ b/cmd/config.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 cmd + +import ( + "context" + "fmt" + + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/spf13/cobra" +) + +const ( + ConfigConnectors = "connectors" + ConfigConnectorType = "type" + ConfigConnectorServer = "server" + ConfigConnectorEthereum = "ethereum" + ConfigCORS = "cors" +) + +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_docs_generate_test.go b/cmd/config_docs_generate_test.go new file mode 100644 index 0000000..5c7098c --- /dev/null +++ b/cmd/config_docs_generate_test.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. + +//go:build docs +// +build docs + +package cmd + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/stretchr/testify/assert" +) + +func TestGenerateConfigDocs(t *testing.T) { + // Initialize config of all plugins + initConfig() + f, err := os.Create(filepath.Join("..", "config.md")) + assert.NoError(t, err) + generatedConfig, err := config.GenerateConfigMarkdown(context.Background(), config.GetKnownKeys()) + assert.NoError(t, err) + _, err = f.Write(generatedConfig) + assert.NoError(t, err) + err = f.Close() + assert.NoError(t, err) +} diff --git a/cmd/config_docs_test.go b/cmd/config_docs_test.go new file mode 100644 index 0000000..52adc18 --- /dev/null +++ b/cmd/config_docs_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. + +//go:build !docs +// +build !docs + +package cmd + +import ( + "context" + "crypto/sha1" + "os" + "path/filepath" + "testing" + + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/stretchr/testify/assert" +) + +func TestConfigDocsUpToDate(t *testing.T) { + // Initialize config of all plugins + initConfig() + generatedConfig, err := config.GenerateConfigMarkdown(context.Background(), config.GetKnownKeys()) + assert.NoError(t, err) + configOnDisk, err := os.ReadFile(filepath.Join("..", "config.md")) + assert.NoError(t, err) + + generatedConfigHash := sha1.New() + generatedConfigHash.Write(generatedConfig) + configOnDiskHash := sha1.New() + configOnDiskHash.Write(configOnDisk) + assert.Equal(t, configOnDiskHash.Sum(nil), generatedConfigHash.Sum(nil), "The config reference docs generated by the code did not match the config.md file in git. Did you forget to run `make docs`?") +} diff --git a/cmd/config_test.go b/cmd/config_test.go new file mode 100644 index 0000000..8ccdd1b --- /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/evmconnect.go b/cmd/evmconnect.go new file mode 100644 index 0000000..58a7c0f --- /dev/null +++ b/cmd/evmconnect.go @@ -0,0 +1,152 @@ +// 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-common/pkg/config" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-evmconnect/internal/ethereum" + "github.com/hyperledger/firefly-evmconnect/internal/ffconnector" + "github.com/hyperledger/firefly-evmconnect/internal/ffcserver" + "github.com/hyperledger/firefly-evmconnect/internal/msgs" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var sigs = make(chan os.Signal, 1) + +var rootCmd = &cobra.Command{ + Use: "evmconnect", + Short: "Hyperledger FireFly Connector for EVM based blockchains", + 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() +} + +var connectors config.ArraySection + +func initConfig() { + config.RootConfigReset() + + // Read the configuration + connectors = config.RootArray(ConfigConnectors) + connectors.AddKnownKey(ConfigConnectorType) + + serverConf := connectors.SubSection(ConfigConnectorServer) + corsConf := config.RootSection(ConfigCORS) + ffcserver.InitConfig(serverConf, corsConf) + + ethereumConf := connectors.SubSection(ConfigConnectorEthereum) + ethereum.InitConfig(ethereumConf) +} + +func run() error { + + initConfig() + err := config.ReadConfig("evmconnect", 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", "evmconnect")) + + 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() + }() + + // Initialize the server for each of the connectors defined + numConnectors := connectors.ArraySize() + serversDone := make(chan error, numConnectors) + servers := make([]ffcserver.Server, numConnectors) + for i := 0; i < numConnectors; i++ { + baseConnectorConf := connectors.ArrayEntry(i) + serverConnectorConf := baseConnectorConf.SubSection(ConfigConnectorServer) + connectorType := baseConnectorConf.GetString(ConfigConnectorType) + childConnectorConf := baseConnectorConf.SubSection(connectorType) + var c ffconnector.Connector + switch connectorType { + case ConfigConnectorEthereum: + c = ethereum.NewEthereumConnector(childConnectorConf) + default: + return i18n.NewError(ctx, msgs.MsgUnknownConnector, connectorType) + } + err := c.Init(ctx, childConnectorConf) + if err == nil { + servers[i] = ffcserver.NewServer(serverConnectorConf, c) + err = servers[i].Init(ctx, serverConnectorConf, config.RootSection(ConfigCORS)) + } + if err != nil { + return err + } + } + // Start all the servers + for _, s := range servers { + go runServer(s, serversDone) + } + // Whenever the first one stops (due to ctrl+c or error), stop them all and exit + var firstError error + for range servers { + err = <-serversDone + cancelCtx() + if firstError == nil { + firstError = err + } + } + return firstError +} + +func runServer(server ffcserver.Server, done chan error) { + err := server.Start() + if err != nil { + done <- err + return + } + done <- server.WaitStopped() +} diff --git a/cmd/evmconnect_test.go b/cmd/evmconnect_test.go new file mode 100644 index 0000000..55ce862 --- /dev/null +++ b/cmd/evmconnect_test.go @@ -0,0 +1,100 @@ +// 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 ( + "fmt" + "os" + "testing" + "time" + + "github.com/hyperledger/firefly-evmconnect/mocks/ffcservermocks" + "github.com/stretchr/testify/assert" +) + +const configDir = "../test/data/config" + +func TestRunOK(t *testing.T) { + + rootCmd.SetArgs([]string{"-f", "../test/firefly.evmconnect.yaml"}) + defer rootCmd.SetArgs([]string{}) + + done := make(chan struct{}) + go func() { + defer close(done) + err := Execute() + if err != nil { + assert.Regexp(t, "context deadline", err) + } + }() + + time.Sleep(10 * time.Millisecond) + sigs <- os.Kill + + <-done + +} + +func TestRunUnknownConnector(t *testing.T) { + + rootCmd.SetArgs([]string{"-f", "../test/unknown-connector.evmconnect.yaml"}) + defer rootCmd.SetArgs([]string{}) + + err := Execute() + assert.Regexp(t, "FF23031", err) + +} + +func TestRunBadConfig(t *testing.T) { + + rootCmd.SetArgs([]string{"-f", "../test/bad-config.evmconnect.yaml"}) + defer rootCmd.SetArgs([]string{}) + + err := Execute() + assert.Regexp(t, "FF00101", err) + +} + +func TestRunFailStartup(t *testing.T) { + rootCmd.SetArgs([]string{"-f", "../test/bad-server.evmconnect.yaml"}) + defer rootCmd.SetArgs([]string{}) + + err := Execute() + assert.Regexp(t, "FF00151", err) + +} + +func TestRunFailServer(t *testing.T) { + + s := &ffcservermocks.Server{} + s.On("Start").Return(fmt.Errorf("pop")) + done := make(chan error, 1) + runServer(s, done) + assert.Regexp(t, <-done, "pop") + +} + +func TestRunServerReturnErr(t *testing.T) { + + s := &ffcservermocks.Server{} + s.On("Start").Return(nil) + done := make(chan error, 1) + s.On("WaitStopped").Return(fmt.Errorf("pop")) + runServer(s, done) + assert.Regexp(t, <-done, "pop") + +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..03aa84a --- /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-common/pkg/i18n" + "github.com/hyperledger/firefly-evmconnect/internal/msgs" + "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(), msgs.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 0000000..40c4a12 --- /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, "FF23016", 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 0000000..02e77f8 --- /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/config.md b/config.md new file mode 100644 index 0000000..86db5ef --- /dev/null +++ b/config.md @@ -0,0 +1,126 @@ +--- +layout: default +title: Configuration Reference +parent: Reference +nav_order: 3 +--- + +# Configuration Reference +{: .no_toc } + + + +--- + + +## connectors[] + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|type|The type of connector|string|`` + +## connectors[].ethereum + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|connectionTimeout|The maximum amount of time that a connection is allowed to remain with no data transmitted|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` +|dataFormat|Configure the JSON data format for query output and events|map,flat_array,self_describing|`map` +|expectContinueTimeout|See [ExpectContinueTimeout in the Go docs](https://pkg.go.dev/net/http#Transport)|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s` +|gasEstimationFactor|The factor to apply to the gas estimation to determine the gas limit|float|`1.5` +|headers|Adds custom headers to HTTP requests|`map[string]string`|`` +|idleTimeout|The max duration to hold a HTTP keepalive connection between calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`475ms` +|maxIdleConns|The max number of idle connections to hold pooled|`int`|`100` +|requestTimeout|The maximum amount of time that a request is allowed to remain open|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` +|tlsHandshakeTimeout|The maximum amount of time to wait for a successful TLS handshake|[`time.Duration`](https://pkg.go.dev/time#Duration)|`10s` +|url|URL of JSON/RPC endpoint for the Ethereum node/gateway|string|`` + +## connectors[].ethereum.auth + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|password|Password|`string`|`` +|username|Username|`string`|`` + +## connectors[].ethereum.proxy + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|url|Optional HTTP proxy url|string|`` + +## connectors[].ethereum.retry + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|count|The maximum number of times to retry|`int`|`5` +|enabled|Enables retries|`boolean`|`false` +|initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms` +|maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` + +## connectors[].server + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|address|Local address for the FFCAPI connector server to listen on|string|`127.0.0.1` +|port|Port for the FFCAPI connector server to listen on|number|`5102` +|publicURL|External address callers should access API over|string|`` +|readTimeout|The maximum time to wait when reading from an HTTP connection|duration|`15s` +|shutdownTimeout|The maximum amount of time to wait for any open HTTP requests to finish before shutting down the HTTP server|[`time.Duration`](https://pkg.go.dev/time#Duration)|`10s` +|writeTimeout|The maximum time to wait when writing to a HTTP connection|duration|`15s` + +## connectors[].server.tls + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|caFile|The path to the CA file for TLS on this API|`string`|`` +|certFile|The path to the certificate file for TLS on this API|`string`|`` +|clientAuth|Enables or disables client auth for TLS on this API|`string`|`` +|enabled|Enables or disables TLS on this API|`boolean`|`false` +|keyFile|The path to the private key file for TLS on this API|`string`|`` + +## cors + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|credentials|CORS setting to control whether a browser allows credentials to be sent to this API|`boolean`|`true` +|debug|Whether debug is enabled for the CORS implementation|`boolean`|`false` +|enabled|Whether CORS is enabled|`boolean`|`true` +|headers|CORS setting to control the allowed headers|`string`|`[*]` +|maxAge|The maximum age a browser should rely on CORS checks|[`time.Duration`](https://pkg.go.dev/time#Duration)|`600` +|methods| CORS setting to control the allowed methods|`string`|`[GET POST PUT PATCH DELETE]` +|origins|CORS setting to control the allowed origins|`string`|`[*]` + +## log + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|compress|Determines if the rotated log files should be compressed using gzip|`boolean`|`` +|filename|Filename is the file to write logs to. Backup log files will be retained in the same directory|`string`|`` +|filesize|MaxSize is the maximum size the log file before it gets rotated|[`BytesSize`](https://pkg.go.dev/github.com/docker/go-units#BytesSize)|`100m` +|forceColor|Force color to be enabled, even when a non-TTY output is detected|`boolean`|`` +|includeCodeInfo|Enables the report caller for including the calling file and line number, and the calling function. If using text logs, it uses the logrus text format rather than the default prefix format.|`boolean`|`false` +|level|The log level - error, warn, info, debug, trace|`string`|`info` +|maxAge|The maximum time to retain old log files based on the timestamp encoded in their filename.|[`time.Duration`](https://pkg.go.dev/time#Duration)|`24h` +|maxBackups|Maximum number of old log files to retain|`int`|`2` +|noColor|Force color to be disabled, event when TTY output is detected|`boolean`|`` +|timeFormat|Custom time format for logs|[Time format](https://pkg.go.dev/time#pkg-constants) `string`|`2006-01-02T15:04:05.000Z07:00` +|utc|Use UTC timestamps for logs|`boolean`|`false` + +## log.json + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|enabled|Enables JSON formatted logs rather than text. All log color settings are ignored when enabled.|`boolean`|`false` + +## log.json.fields + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|file|configures the JSON key containing the calling file|`string`|`file` +|func|Configures the JSON key containing the calling function|`string`|`func` +|level|Configures the JSON key containing the log level|`string`|`level` +|message|Configures the JSON key containing the log message|`string`|`message` +|timestamp|Configures the JSON key containing the timestamp of the log|`string`|`@timestamp` \ No newline at end of file diff --git a/evmconnect/main.go b/evmconnect/main.go new file mode 100644 index 0000000..1f07855 --- /dev/null +++ b/evmconnect/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-evmconnect/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 0000000..38d090e --- /dev/null +++ b/go.mod @@ -0,0 +1,53 @@ +module github.com/hyperledger/firefly-evmconnect + +go 1.17 + +require ( + github.com/Masterminds/semver v1.5.0 + github.com/go-resty/resty/v2 v2.7.0 + github.com/gorilla/mux v1.8.0 + github.com/hyperledger/firefly-common v0.1.6-0.20220525024151-51140308969f + github.com/hyperledger/firefly-signer v0.9.4-0.20220524183528-6af8062c7cda + github.com/sirupsen/logrus v1.8.1 + github.com/spf13/cobra v1.4.0 + github.com/stretchr/testify v1.7.1 + golang.org/x/text v0.3.7 + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/aidarkhanov/nanoid v1.0.8 // indirect + github.com/btcsuite/btcd v0.22.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/magiconair/properties v1.8.6 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rs/cors v1.8.2 // indirect + github.com/spf13/afero v1.8.2 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.11.0 // indirect + github.com/stretchr/objx v0.1.1 // indirect + github.com/subosito/gotenv v1.3.0 // indirect + github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect + golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 // indirect + golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect + golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect + gopkg.in/ini.v1 v1.66.4 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/yaml.v3 v3.0.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..46660a4 --- /dev/null +++ b/go.sum @@ -0,0 +1,935 @@ +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.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.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +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/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +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/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= +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/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +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/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/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +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/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/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.1 h1:CnwP9LM/M9xuRrGSCGeMVs9iv09uMqwsVX7EeIpgV2c= +github.com/btcsuite/btcd v0.22.1/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/census-instrumentation/opencensus-proto v0.2.1/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/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/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/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-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +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/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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +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/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +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/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +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-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-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-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-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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +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.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.3/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/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-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/gofuzz v1.0.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-20210407192527-94a9f03dee38/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-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/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/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +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 v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +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 v1.0.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.2.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 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-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/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.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hyperledger/firefly-common v0.1.5/go.mod h1:qGy7i8eWlE8Ed7jFn2Hpn8bYaflL204j4NJB1mAfTms= +github.com/hyperledger/firefly-common v0.1.6-0.20220525024151-51140308969f h1:Xb6gqgaNI+6SN+Bq42d/sViD7EZPk0Vqsp7edg99PNs= +github.com/hyperledger/firefly-common v0.1.6-0.20220525024151-51140308969f/go.mod h1:qGy7i8eWlE8Ed7jFn2Hpn8bYaflL204j4NJB1mAfTms= +github.com/hyperledger/firefly-signer v0.9.4-0.20220524183528-6af8062c7cda h1:TmxLK03OuH/I39H6P+KOlLVNP295gYYEej/fF7jIkLc= +github.com/hyperledger/firefly-signer v0.9.4-0.20220524183528-6af8062c7cda/go.mod h1:BjzIWMj4e1Em52liuyCV6dQhPeVc3WOuLOnEDGS4QK4= +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/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jarcoal/httpmock v1.1.0 h1:F47ChZj1Y2zFsCXxNkBPwNNKnAyOATcdQibk0qEdVCE= +github.com/jarcoal/httpmock v1.1.0/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +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/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/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/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.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +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/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +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-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +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/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/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 v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +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/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +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.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.0-beta.8/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= +github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= +github.com/pkg/errors v0.8.0/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.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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +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.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= +github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sagikazarmark/crypt v0.5.0/go.mod h1:l+nzl7KWh51rpzp2h7t4MZWyiEWdhNpOAnclKvg+mdA= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= +github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= +github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +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 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.11.0 h1:7OX/1FS6n7jHD1zGrZTM7WtY13ZELRyosK4k93oPr44= +github.com/spf13/viper v1.11.0/go.mod h1:djo0X/bA5+tYVoCn+C7cAYJGcVn/qYLFTG8gdUsX7Zk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= +github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +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/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= +go.etcd.io/etcd/api/v3 v3.5.2/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= +go.etcd.io/etcd/client/pkg/v3 v3.5.2/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.2/go.mod h1:2D7ZejHVMIfog1221iLSYlQRzrtECw3kz4I4VAQm3qI= +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.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/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-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-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-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 h1:SLP7Q4Di66FONjDJbCYrCRrh97focO6sLogHO7/g8F0= +golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/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-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-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/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/net v0.0.0-20180719180050-a680a1efc54d/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-20181114220301-adae6a3d119a/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-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-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-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-20190923162816-aa69164e4478/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-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-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-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/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-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/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/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +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-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-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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-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-20190606165138-5da285871e9c/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-20190904154756-749cb33beabd/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-20191026070338-33540a1f6037/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-20191228213918-04cbcbbfeed8/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-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-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-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/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-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-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-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-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-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-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-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-20211124211545-fe61309f8881/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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8= +golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/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-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/tools v0.0.0-20180917221912-90fa682c2a6e/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-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-20190425150028-36563e24a262/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-20190606124116-d0a3d012864b/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-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-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-20191012152004-8de300cfc20a/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-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-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= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +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/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-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-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-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-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-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-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-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-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +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.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +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.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +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= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= +gopkg.in/ini.v1 v1.66.4/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/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.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.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-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/ethereum/config.go b/internal/ethereum/config.go new file mode 100644 index 0000000..cff04ed --- /dev/null +++ b/internal/ethereum/config.go @@ -0,0 +1,39 @@ +// 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 ethereum + +import ( + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/hyperledger/firefly-common/pkg/ffresty" +) + +const ( + ConfigGasEstimationFactor = "gasEstimationFactor" + ConfigDataFormat = "dataFormat" +) + +const ( + DefaultListenerPort = 5102 + DefaultGasEstimationFactor = 1.5 +) + +func InitConfig(conf config.Section) { + ffresty.InitConfig(conf) + + conf.AddKnownKey(ConfigGasEstimationFactor, DefaultGasEstimationFactor) + conf.AddKnownKey(ConfigDataFormat, "map") +} diff --git a/internal/ethereum/create_block_listener.go b/internal/ethereum/create_block_listener.go new file mode 100644 index 0000000..0c76e84 --- /dev/null +++ b/internal/ethereum/create_block_listener.go @@ -0,0 +1,45 @@ +// 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 ethereum + +import ( + "context" + "encoding/json" + + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" +) + +func (c *ethConnector) createBlockListener(ctx context.Context, payload []byte) (interface{}, ffcapi.ErrorReason, error) { + + var req ffcapi.CreateBlockListenerRequest + err := json.Unmarshal(payload, &req) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + + var listenerID ethtypes.HexInteger + err = c.backend.Invoke(ctx, &listenerID, "eth_newBlockFilter") + if err != nil { + return nil, "", err + } + + return &ffcapi.CreateBlockListenerResponse{ + ListenerID: listenerID.String(), + }, "", nil + +} diff --git a/internal/ethereum/create_block_listener_test.go b/internal/ethereum/create_block_listener_test.go new file mode 100644 index 0000000..ebbf7e4 --- /dev/null +++ b/internal/ethereum/create_block_listener_test.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 ethereum + +import ( + "context" + "fmt" + "testing" + + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +const sampleCreateBlockListener = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "create_block_listener" + } +}` + +func TestCreateBlockListenerOK(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_newBlockFilter"). + Return(nil). + Run(func(args mock.Arguments) { + (args[1].(*ethtypes.HexInteger)).BigInt().SetString("12345", 10) + }) + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_getFilterChanges", mock.Anything).Return(nil).Maybe() + + iRes, reason, err := c.createBlockListener(ctx, []byte(sampleCreateBlockListener)) + assert.NoError(t, err) + assert.Empty(t, reason) + + res := iRes.(*ffcapi.CreateBlockListenerResponse) + assert.Equal(t, "0x3039", res.ListenerID) + +} + +func TestCreateBlockListenerFail(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_newBlockFilter"). + Return(fmt.Errorf("pop")) + + iRes, reason, err := c.createBlockListener(ctx, []byte(sampleCreateBlockListener)) + assert.Regexp(t, "pop", err) + assert.Empty(t, reason) + assert.Nil(t, iRes) + +} + +func TestCreateBlockListenerBadPayload(t *testing.T) { + + c, _ := newTestConnector(t) + ctx := context.Background() + + iRes, reason, err := c.createBlockListener(ctx, []byte("!json")) + assert.Regexp(t, "invalid", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + assert.Nil(t, iRes) + +} diff --git a/internal/ethereum/error_mapping.go b/internal/ethereum/error_mapping.go new file mode 100644 index 0000000..b551b24 --- /dev/null +++ b/internal/ethereum/error_mapping.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 ethereum + +import ( + "strings" + + "github.com/hyperledger/firefly-common/pkg/ffcapi" +) + +type ethRPCMethodCategory int + +const ( + filterRPCMethods ethRPCMethodCategory = iota + sendRPCMethods + callRPCMethods +) + +// mapErrorToReason provides a common place for mapping Ethereum client +// error strings, to a more consistent set of cross-client (and +// cross blockchain) reasons for errors defined by FFCPI for use by +// FireFly Transaction Manager. +// +// Sadly there is no place in Ethereum JSON/RPC where these are +// formally defined. So this logic may get complex over time to +// deal with the differences between client implementations. +func mapError(methodType ethRPCMethodCategory, err error) ffcapi.ErrorReason { + + errString := err.Error() + + switch methodType { + case filterRPCMethods: + if strings.Contains(errString, "filter not found") { + return ffcapi.ErrorReasonNotFound + } + return "" + case sendRPCMethods: + switch { + case strings.Contains(errString, "nonce too low"): + return ffcapi.ErrorReasonNonceTooLow + case strings.Contains(errString, "insufficient funds"): + return ffcapi.ErrorReasonInsufficientFunds + case strings.Contains(errString, "transaction underpriced"): + return ffcapi.ErrorReasonTransactionUnderpriced + case strings.Contains(errString, "known transaction"): + return ffcapi.ErrorKnownTransaction + default: + return "" + } + } + + // Best default in FFCAPI is to provide no mapping + return "" +} diff --git a/internal/ethereum/estimate_gas.go b/internal/ethereum/estimate_gas.go new file mode 100644 index 0000000..64ffdf9 --- /dev/null +++ b/internal/ethereum/estimate_gas.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 ethereum + +import ( + "context" + "math/big" + + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-signer/pkg/abi" + "github.com/hyperledger/firefly-signer/pkg/ethsigner" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" +) + +func (c *ethConnector) estimateGas(ctx context.Context, tx *ethsigner.Transaction, method *abi.Entry) (*ethtypes.HexInteger, ffcapi.ErrorReason, error) { + + // Do the gas estimation + var gasEstimate ethtypes.HexInteger + err := c.backend.Invoke(ctx, &gasEstimate, "eth_estimateGas", tx) + if err != nil { + // If it fails, fall back to an eth_call to see if we get a reverted reason + _, reason, errCall := c.callTransaction(ctx, tx, method) + if reason == ffcapi.ErrorReasonTransactionReverted { + return nil, reason, errCall + } + log.L(ctx).Errorf("Gas estimation failed for a non-revert reason: %s (call result: %v)", err, errCall) + // Return the original error - as the eth_call did not give us a revert result (it might even + // have succeeded). So we need to fall back to the original error. + return nil, mapError(callRPCMethods, err), err + } + + // Multiply the gas estimate by the configured factor + fGasEstimate := new(big.Float).SetInt(gasEstimate.BigInt()) + _ = fGasEstimate.Mul(fGasEstimate, c.gasEstimationFactor) + _, _ = fGasEstimate.Int(gasEstimate.BigInt()) + return &gasEstimate, "", nil +} diff --git a/internal/ethereum/ethereum.go b/internal/ethereum/ethereum.go new file mode 100644 index 0000000..f6029ac --- /dev/null +++ b/internal/ethereum/ethereum.go @@ -0,0 +1,79 @@ +// 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 ethereum + +import ( + "context" + "math/big" + + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-common/pkg/ffresty" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-evmconnect/internal/ffconnector" + "github.com/hyperledger/firefly-evmconnect/internal/jsonrpc" + "github.com/hyperledger/firefly-evmconnect/internal/msgs" + "github.com/hyperledger/firefly-signer/pkg/abi" +) + +type ethConnector struct { + backend jsonrpc.Client + serializer *abi.Serializer + gasEstimationFactor *big.Float +} + +func NewEthereumConnector(conf config.Section) ffconnector.Connector { + return ðConnector{} +} + +func (c *ethConnector) HandlerMap() map[ffcapi.RequestType]ffconnector.FFCHandler { + return map[ffcapi.RequestType]ffconnector.FFCHandler{ + ffcapi.RequestTypeCreateBlockListener: c.createBlockListener, + ffcapi.RequestTypeExecQuery: c.execQuery, + ffcapi.RequestTypeGetBlockInfoByHash: c.getBlockInfoByHash, + ffcapi.RequestTypeGetBlockInfoByNumber: c.getBlockInfoByNumber, + ffcapi.RequestTypeGetGasPrice: c.getGasPrice, + ffcapi.RequestTypeGetNewBlockHashes: c.getNewBlockHashes, + ffcapi.RequestTypeGetNextNonce: c.getNextNonce, + ffcapi.RequestTypeGetReceipt: c.getReceipt, + ffcapi.RequestTypePrepareTransaction: c.prepareTransaction, + ffcapi.RequestTypeSendTransaction: c.sendTransaction, + } +} + +func (c *ethConnector) Init(ctx context.Context, conf config.Section) error { + if conf.GetString(ffresty.HTTPConfigURL) == "" { + return i18n.NewError(ctx, msgs.MsgMissingBackendURL) + } + c.gasEstimationFactor = big.NewFloat(conf.GetFloat64(ConfigGasEstimationFactor)) + + c.backend = jsonrpc.NewRPCClient(ffresty.New(ctx, conf)) + + c.serializer = abi.NewSerializer() + switch conf.Get(ConfigDataFormat) { + case "map": + c.serializer.SetFormattingMode(abi.FormatAsObjects) + case "flat_array": + c.serializer.SetFormattingMode(abi.FormatAsFlatArrays) + case "self_describing": + c.serializer.SetFormattingMode(abi.FormatAsSelfDescribingArrays) + default: + return i18n.NewError(ctx, msgs.MsgBadDataFormat, conf.Get(ConfigDataFormat), "map,flat_array,self_describing") + } + + return nil +} diff --git a/internal/ethereum/ethereum_test.go b/internal/ethereum/ethereum_test.go new file mode 100644 index 0000000..337f4e8 --- /dev/null +++ b/internal/ethereum/ethereum_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 ethereum + +import ( + "context" + "testing" + + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/hyperledger/firefly-common/pkg/ffresty" + "github.com/hyperledger/firefly-evmconnect/mocks/jsonrpcmocks" + "github.com/stretchr/testify/assert" +) + +func newTestConnector(t *testing.T) (*ethConnector, *jsonrpcmocks.Client) { + + mRPC := &jsonrpcmocks.Client{} + config.RootConfigReset() + conf := config.RootSection("unittest") + InitConfig(conf) + c := ðConnector{} + conf.Set(ffresty.HTTPConfigURL, "http://backend.example.invalid") + err := c.Init(context.Background(), conf) + assert.NoError(t, err) + c.backend = mRPC + return c, mRPC + +} diff --git a/internal/ethereum/exec_query.go b/internal/ethereum/exec_query.go new file mode 100644 index 0000000..373e5e3 --- /dev/null +++ b/internal/ethereum/exec_query.go @@ -0,0 +1,130 @@ +// 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 ethereum + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-evmconnect/internal/msgs" + "github.com/hyperledger/firefly-signer/pkg/abi" + "github.com/hyperledger/firefly-signer/pkg/ethsigner" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" +) + +var ( + // See https://docs.soliditylang.org/en/v0.8.14/control-structures.html#revert + // There default error for `revert("some error")` is a function Error(string) + defaultError = &abi.Entry{ + Type: abi.Error, + Name: "Error", + Inputs: abi.ParameterArray{ + { + Type: "string", + }, + }, + } + defaultErrorID = defaultError.IDBytes() +) + +func (c *ethConnector) execQuery(ctx context.Context, payload []byte) (interface{}, ffcapi.ErrorReason, error) { + + var req ffcapi.ExecQueryRequest + err := json.Unmarshal(payload, &req) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + + // Parse the input JSON data, to build the call data + callData, method, err := c.prepareCallData(ctx, &req.TransactionInput) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + + // Build the base transaction object + tx, err := c.buildTx(ctx, req.From, req.To, req.Nonce, req.Gas, req.Value, callData) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + + // Do the call, with processing of revert reasons + outputs, reason, err := c.callTransaction(ctx, tx, method) + if err != nil { + return nil, reason, err + } + + return &ffcapi.ExecQueryResponse{ + Outputs: outputs, + }, "", nil + +} + +func (c *ethConnector) callTransaction(ctx context.Context, tx *ethsigner.Transaction, method *abi.Entry) (*fftypes.JSONAny, ffcapi.ErrorReason, error) { + + // Do the raw call + var outputData ethtypes.HexBytes0xPrefix + err := c.backend.Invoke(ctx, &outputData, "eth_call", tx, "latest") + if err != nil { + return nil, mapError(callRPCMethods, err), err + } + + // If we get back nil, then send back nil + if len(outputData) == 0 { + return nil, "", nil + } + + // Check for a revert - we can determine it is calldata (with an error signature) + // that is returned by the fact the output is not a multiple of 32 (as all ABI encodings + // result in a multiple of 32 bytes) and has exactly 4 extra bytes for a function + // signature + if len(outputData)%32 == 4 { + if bytes.Equal(outputData[0:4], defaultErrorID) { + errorInfo, err := defaultError.DecodeCallDataCtx(ctx, outputData) + if err == nil && len(errorInfo.Children) == 1 { + if strError, ok := errorInfo.Children[0].Value.(string); ok { + return nil, ffcapi.ErrorReasonTransactionReverted, i18n.NewError(ctx, msgs.MsgRevertedWithMessage, strError) + } + } + log.L(ctx).Warnf("Invalid revert data: %s", outputData) + } + // Note: We do not support custom errors (with custom IDs/signatures). + // This would require the FFCAPI to be enhanced to allow an array + // "error" ABI definition entries to be passed alongside the + // "method" ABI definition entry. + return nil, ffcapi.ErrorReasonTransactionReverted, i18n.NewError(ctx, msgs.MsgRevertedRawRevertData, outputData) + } + + // Parse the data against the outputs + outputValueTree, err := method.Outputs.DecodeABIDataCtx(ctx, outputData, 0) + if err != nil { + log.L(ctx).Warnf("Invalid return data: %s", outputData) + return nil, "", i18n.NewError(ctx, msgs.MsgReturnDataInvalid, err) + } + + // Serialize down to JSON, and wrap in a JSONAny + jsonData, err := c.serializer.SerializeJSONCtx(ctx, outputValueTree) + if err != nil { + return nil, "", i18n.NewError(ctx, msgs.MsgReturnDataInvalid, err) + } + return fftypes.JSONAnyPtrBytes(jsonData), "", nil + +} diff --git a/internal/ethereum/exec_query_test.go b/internal/ethereum/exec_query_test.go new file mode 100644 index 0000000..5bb2515 --- /dev/null +++ b/internal/ethereum/exec_query_test.go @@ -0,0 +1,112 @@ +// 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 ethereum + +import ( + "context" + "testing" + + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-signer/pkg/ethsigner" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +const sampleExecQuery = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "exec_query" + }, + "from": "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", + "to": "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771", + "nonce": "222", + "method": { + "inputs": [], + "name":"do", + "outputs":[], + "stateMutability":"nonpayable", + "type":"function" + }, + "method": { + "inputs": [ + { + "internalType":" uint256", + "name": "x", + "type": "uint256" + } + ], + "name":"set", + "outputs":[ + { + "internalType":" uint256", + "name": "y", + "type": "uint256" + } + ], + "stateMutability":"nonpayable", + "type":"function" + }, + "params": [ 4276993775 ] +}` + +func TestExecQueryOKResponse(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_call", + mock.MatchedBy(func(tx *ethsigner.Transaction) bool { + assert.Equal(t, "0x60fe47b100000000000000000000000000000000000000000000000000000000feedbeef", tx.Data.String()) + return true + }), + "latest"). + Run(func(args mock.Arguments) { + *(args[1].(*ethtypes.HexBytes0xPrefix)) = ethtypes.MustNewHexBytes0xPrefix("0x00000000000000000000000000000000000000000000000000000000baadf00d") + }). + Return(nil) + + iRes, reason, err := c.execQuery(ctx, []byte(sampleExecQuery)) + assert.NoError(t, err) + assert.Empty(t, reason) + assert.JSONEq(t, `{"y": "3131961357"}`, iRes.(*ffcapi.ExecQueryResponse).Outputs.String()) + +} + +func TestExecQueryOKNilResponse(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_call", + mock.MatchedBy(func(tx *ethsigner.Transaction) bool { + assert.Equal(t, "0x60fe47b100000000000000000000000000000000000000000000000000000000feedbeef", tx.Data.String()) + return true + }), + "latest"). + Run(func(args mock.Arguments) { + *(args[1].(*ethtypes.HexBytes0xPrefix)) = ethtypes.MustNewHexBytes0xPrefix("0x") + }). + Return(nil) + + iRes, reason, err := c.execQuery(ctx, []byte(sampleExecQuery)) + assert.NoError(t, err) + assert.Empty(t, reason) + assert.JSONEq(t, "null", iRes.(*ffcapi.ExecQueryResponse).Outputs.String()) + +} diff --git a/internal/ethereum/get_block_info.go b/internal/ethereum/get_block_info.go new file mode 100644 index 0000000..d6c761f --- /dev/null +++ b/internal/ethereum/get_block_info.go @@ -0,0 +1,95 @@ +// 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 ethereum + +import ( + "context" + "encoding/json" + + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-evmconnect/internal/msgs" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" +) + +// blockInfoJSONRPC are the fields we parse from the JSON/RPC response +type blockInfoJSONRPC struct { + Number *ethtypes.HexInteger `json:"number"` + Hash ethtypes.HexBytes0xPrefix `json:"hash"` + ParentHash ethtypes.HexBytes0xPrefix `json:"parentHash"` + Timestamp *ethtypes.HexInteger `json:"timestamp"` + Transactions []ethtypes.HexBytes0xPrefix `json:"transactions"` +} + +func transformBlockInfo(bi *blockInfoJSONRPC, t *ffcapi.BlockInfo) { + t.BlockNumber = (*fftypes.FFBigInt)(bi.Number) + t.BlockHash = bi.Hash.String() + t.ParentHash = bi.ParentHash.String() + stringHashes := make([]string, len(bi.Transactions)) + for i, th := range bi.Transactions { + stringHashes[i] = th.String() + } + t.TransactionHashes = stringHashes +} + +func (c *ethConnector) getBlockInfoByNumber(ctx context.Context, payload []byte) (interface{}, ffcapi.ErrorReason, error) { + + var req ffcapi.GetBlockInfoByNumberRequest + err := json.Unmarshal(payload, &req) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + + blockNumber := req.BlockNumber + var blockInfo *blockInfoJSONRPC + err = c.backend.Invoke(ctx, &blockInfo, "eth_getBlockByNumber", blockNumber, false /* only the txn hashes */) + if err != nil { + return nil, "", err + } + if blockInfo == nil { + return nil, ffcapi.ErrorReasonNotFound, i18n.NewError(ctx, msgs.MsgBlockNotAvailable) + } + + res := &ffcapi.GetBlockInfoByNumberResponse{} + transformBlockInfo(blockInfo, &res.BlockInfo) + return res, "", nil + +} + +func (c *ethConnector) getBlockInfoByHash(ctx context.Context, payload []byte) (interface{}, ffcapi.ErrorReason, error) { + + var req ffcapi.GetBlockInfoByHashRequest + err := json.Unmarshal(payload, &req) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + + var blockInfo *blockInfoJSONRPC + err = c.backend.Invoke(ctx, &blockInfo, "eth_getBlockByHash", req.BlockHash, false /* only the txn hashes */) + if err != nil { + return nil, "", err + } + if blockInfo == nil { + return nil, ffcapi.ErrorReasonNotFound, i18n.NewError(ctx, msgs.MsgBlockNotAvailable) + } + + res := &ffcapi.GetBlockInfoByHashResponse{} + transformBlockInfo(blockInfo, &res.BlockInfo) + return res, "", nil + +} diff --git a/internal/ethereum/get_block_info_test.go b/internal/ethereum/get_block_info_test.go new file mode 100644 index 0000000..ff4669e --- /dev/null +++ b/internal/ethereum/get_block_info_test.go @@ -0,0 +1,215 @@ +// 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 ethereum + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +const sampleGetBlockInfoByNumber = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "get_block_info_by_number" + }, + "blockNumber": "12345" +}` + +const sampleGetBlockInfoByHash = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "get_block_info_by_hash" + }, + "blockHash": "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6" +}` + +const sampleBlockJSONRPC = `{ + "difficulty": "0x2", + "extraData": "0xd683010a11846765746886676f312e3138856c696e7578000000000000000000ebe2ceb710450c390fbbf76e379cca8b5dac0444c2d49f5039b0fb61b9d6d0912ed4afe89227b39b21c78398824e9feb4b6d6f9f17c2b4c3bfa0e5975f3e12df01", + "gasLimit": "0x48112a", + "gasUsed": "0x8414", + "hash": "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", + "logsBloom": "0x00000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000100000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000", + "miner": "0x0000000000000000000000000000000000000000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0000000000000000", + "number": "0x3039", + "parentHash": "0x124ca6245d8ddd48203346c2f80b9bc07ce2fcdb8ccb3251b03d8748c1c73b92", + "receiptsRoot": "0x9b2a34bd8b935ade9cbdc016872e59d3abafe3f73d8471523cbb05b24fe2a620", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size": "0x44c", + "stateRoot": "0x07990588ecb235a7d5a483e94347356b10c2a68e876c023c9eb78ee5706d4315", + "timestamp": "0x625829cc", + "totalDifficulty": "0xb", + "transactions": [ + "0x7d48ae971faf089878b57e3c28e3035540d34f38af395958d2c73c36c57c83a2" + ], + "transactionsRoot": "0x8ae1c0f1c985972257ed1719c6fb9524a3c5a43eaa5493fb83c00ca070d7a460", + "uncles": [] +}` + +func TestGetBlockInfoByNumberOK(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_getBlockByNumber", + mock.MatchedBy( + func(blockNumber *fftypes.FFBigInt) bool { + return blockNumber.Int().String() == "12345" + }), + false). + Return(nil). + Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte(sampleBlockJSONRPC), args[1]) + assert.NoError(t, err) + }) + + iRes, reason, err := c.getBlockInfoByNumber(ctx, []byte(sampleGetBlockInfoByNumber)) + assert.NoError(t, err) + assert.Empty(t, reason) + + res := iRes.(*ffcapi.GetBlockInfoByNumberResponse) + assert.Equal(t, "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", res.BlockHash) + assert.Equal(t, "0x124ca6245d8ddd48203346c2f80b9bc07ce2fcdb8ccb3251b03d8748c1c73b92", res.ParentHash) + assert.Equal(t, int64(12345), res.BlockNumber.Int64()) + +} + +func TestGetBlockInfoByNumberNotFound(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.Anything, false). + Return(nil). + Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte("null"), args[1]) + assert.NoError(t, err) + }) + + iRes, reason, err := c.getBlockInfoByNumber(ctx, []byte(sampleGetBlockInfoByNumber)) + assert.Regexp(t, "FF23011", err) + assert.Equal(t, ffcapi.ErrorReasonNotFound, reason) + assert.Nil(t, iRes) + +} + +func TestGetBlockInfoByNumberFail(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.Anything, false). + Return(fmt.Errorf("pop")) + + iRes, reason, err := c.getBlockInfoByNumber(ctx, []byte(sampleGetBlockInfoByNumber)) + assert.Regexp(t, "pop", err) + assert.Empty(t, reason) + assert.Nil(t, iRes) + +} + +func TestGetBlockInfoByNumberBadPayload(t *testing.T) { + + c, _ := newTestConnector(t) + ctx := context.Background() + + iRes, reason, err := c.getBlockInfoByNumber(ctx, []byte("!json")) + assert.Regexp(t, "invalid", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + assert.Nil(t, iRes) + +} + +func TestGetBlockInfoByHashOK(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_getBlockByHash", "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", false). + Return(nil). + Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte(sampleBlockJSONRPC), args[1]) + assert.NoError(t, err) + }) + + iRes, reason, err := c.getBlockInfoByHash(ctx, []byte(sampleGetBlockInfoByHash)) + assert.NoError(t, err) + assert.Empty(t, reason) + + res := iRes.(*ffcapi.GetBlockInfoByHashResponse) + assert.Equal(t, "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", res.BlockHash) + assert.Equal(t, "0x124ca6245d8ddd48203346c2f80b9bc07ce2fcdb8ccb3251b03d8748c1c73b92", res.ParentHash) + assert.Equal(t, int64(12345), res.BlockNumber.Int64()) + +} + +func TestGetBlockInfoByHashNotFound(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_getBlockByHash", mock.Anything, false). + Return(nil). + Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte("null"), args[1]) + assert.NoError(t, err) + }) + + iRes, reason, err := c.getBlockInfoByHash(ctx, []byte(sampleGetBlockInfoByHash)) + assert.Regexp(t, "FF23011", err) + assert.Equal(t, ffcapi.ErrorReasonNotFound, reason) + assert.Nil(t, iRes) + +} + +func TestGetBlockInfoByHashFail(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_getBlockByHash", mock.Anything, false). + Return(fmt.Errorf("pop")) + + iRes, reason, err := c.getBlockInfoByHash(ctx, []byte(sampleGetBlockInfoByHash)) + assert.Regexp(t, "pop", err) + assert.Empty(t, reason) + assert.Nil(t, iRes) + +} + +func TestGetBlockInfoByHashBadPayload(t *testing.T) { + + c, _ := newTestConnector(t) + ctx := context.Background() + + iRes, reason, err := c.getBlockInfoByHash(ctx, []byte("!json")) + assert.Regexp(t, "invalid", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + assert.Nil(t, iRes) + +} diff --git a/internal/ethereum/get_gas_price.go b/internal/ethereum/get_gas_price.go new file mode 100644 index 0000000..4df748e --- /dev/null +++ b/internal/ethereum/get_gas_price.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 ethereum + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" +) + +func (c *ethConnector) getGasPrice(ctx context.Context, payload []byte) (interface{}, ffcapi.ErrorReason, error) { + + var req ffcapi.GetGasPriceRequest + err := json.Unmarshal(payload, &req) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + + // Note we use simple (pre London fork) gas fee approach. + // See https://github.com/ethereum/pm/issues/328#issuecomment-853234014 for a bit of color + var gasPrice ethtypes.HexInteger + err = c.backend.Invoke(ctx, &gasPrice, "eth_gasPrice") + if err != nil { + return nil, "", err + } + + return &ffcapi.GetGasPriceResponse{ + GasPrice: fftypes.JSONAnyPtr(fmt.Sprintf(`"%s"`, gasPrice.BigInt().Text(10))), + }, "", nil + +} diff --git a/internal/ethereum/get_gas_price_test.go b/internal/ethereum/get_gas_price_test.go new file mode 100644 index 0000000..0d1a7aa --- /dev/null +++ b/internal/ethereum/get_gas_price_test.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 ethereum + +import ( + "context" + "fmt" + "testing" + + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +const sampleGetGasPrice = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "get_next_nonce" + }, + "signer": "0x302259069aaa5b10dc6f29a9a3f72a8e52837cc3" +}` + +func TestGetGasPriceOK(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_gasPrice"). + Return(nil). + Run(func(args mock.Arguments) { + (args[1].(*ethtypes.HexInteger)).BigInt().SetString("12345", 10) + }) + + iRes, reason, err := c.getGasPrice(ctx, []byte(sampleGetGasPrice)) + assert.NoError(t, err) + assert.Empty(t, reason) + + res := iRes.(*ffcapi.GetGasPriceResponse) + assert.Equal(t, `"12345"`, res.GasPrice.String()) + +} + +func TestGetGasPriceFail(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_gasPrice"). + Return(fmt.Errorf("pop")) + + iRes, reason, err := c.getGasPrice(ctx, []byte(sampleGetGasPrice)) + assert.Regexp(t, "pop", err) + assert.Empty(t, reason) + assert.Nil(t, iRes) + +} + +func TestGetGasPriceBadPayload(t *testing.T) { + + c, _ := newTestConnector(t) + ctx := context.Background() + + iRes, reason, err := c.getGasPrice(ctx, []byte("!json")) + assert.Regexp(t, "invalid", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + assert.Nil(t, iRes) + +} diff --git a/internal/ethereum/get_new_block_hashes.go b/internal/ethereum/get_new_block_hashes.go new file mode 100644 index 0000000..9ef2ba5 --- /dev/null +++ b/internal/ethereum/get_new_block_hashes.go @@ -0,0 +1,47 @@ +// 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 ethereum + +import ( + "context" + "encoding/json" + + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" +) + +func (c *ethConnector) getNewBlockHashes(ctx context.Context, payload []byte) (interface{}, ffcapi.ErrorReason, error) { + + var req ffcapi.GetNewBlockHashesRequest + err := json.Unmarshal(payload, &req) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + + var listenerID ethtypes.HexInteger + listenerID.BigInt().SetString(req.ListenerID, 0 /* big.Int strips the 0x */) + var blockHashes []string + err = c.backend.Invoke(ctx, &blockHashes, "eth_getFilterChanges", &listenerID) + if err != nil { + return nil, mapError(filterRPCMethods, err), err + } + + return &ffcapi.GetNewBlockHashesResponse{ + BlockHashes: blockHashes, + }, "", nil + +} diff --git a/internal/ethereum/get_new_block_hashes_test.go b/internal/ethereum/get_new_block_hashes_test.go new file mode 100644 index 0000000..cf43d25 --- /dev/null +++ b/internal/ethereum/get_new_block_hashes_test.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 ethereum + +import ( + "context" + "fmt" + "testing" + + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +const sampleGetNewBlockHashes = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "get_new_block_hashes" + }, + "listenerId": "0x3039" +}` + +func TestGetNewBlockHashesOK(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_getFilterChanges", + mock.MatchedBy( + func(listenerID *ethtypes.HexInteger) bool { + return listenerID.String() == "0x3039" + })). + Return(nil). + Run(func(args mock.Arguments) { + *(args[1].(*[]string)) = []string{"0x12345", "0x23456"} + }) + + iRes, reason, err := c.getNewBlockHashes(ctx, []byte(sampleGetNewBlockHashes)) + assert.NoError(t, err) + assert.Empty(t, reason) + + res := iRes.(*ffcapi.GetNewBlockHashesResponse) + assert.Equal(t, []string{"0x12345", "0x23456"}, res.BlockHashes) + +} + +func TestGetNewBlockHashesFail(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_getFilterChanges", mock.Anything). + Return(fmt.Errorf("pop")) + + iRes, reason, err := c.getNewBlockHashes(ctx, []byte(sampleGetNewBlockHashes)) + assert.Regexp(t, "pop", err) + assert.Empty(t, reason) + assert.Nil(t, iRes) + +} + +func TestGetNewBlockHashesFailNotFound(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_getFilterChanges", mock.Anything). + Return(fmt.Errorf("filter not found")) + + iRes, reason, err := c.getNewBlockHashes(ctx, []byte(sampleGetNewBlockHashes)) + assert.Regexp(t, "filter not found", err) + assert.Equal(t, ffcapi.ErrorReasonNotFound, reason) + assert.Nil(t, iRes) + +} + +func TestGetNewBlockHashesBadPayload(t *testing.T) { + + c, _ := newTestConnector(t) + ctx := context.Background() + + iRes, reason, err := c.getNewBlockHashes(ctx, []byte("!json")) + assert.Regexp(t, "invalid", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + assert.Nil(t, iRes) + +} diff --git a/internal/ethereum/get_next_nonce.go b/internal/ethereum/get_next_nonce.go new file mode 100644 index 0000000..e93bc1c --- /dev/null +++ b/internal/ethereum/get_next_nonce.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 ethereum + +import ( + "context" + "encoding/json" + + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" +) + +func (c *ethConnector) getNextNonce(ctx context.Context, payload []byte) (interface{}, ffcapi.ErrorReason, error) { + + var req ffcapi.GetNextNonceRequest + err := json.Unmarshal(payload, &req) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + + var txnCount ethtypes.HexInteger + err = c.backend.Invoke(ctx, &txnCount, "eth_getTransactionCount", req.Signer, "pending") + if err != nil { + return nil, "", err + } + + return &ffcapi.GetNextNonceResponse{ + Nonce: (*fftypes.FFBigInt)(&txnCount), + }, "", nil + +} diff --git a/internal/ethereum/get_next_nonce_test.go b/internal/ethereum/get_next_nonce_test.go new file mode 100644 index 0000000..a482ab0 --- /dev/null +++ b/internal/ethereum/get_next_nonce_test.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 ethereum + +import ( + "context" + "fmt" + "testing" + + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +const sampleGetNextNonce = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "get_next_nonce" + }, + "signer": "0x302259069aaa5b10dc6f29a9a3f72a8e52837cc3" +}` + +func TestGetNextNonceOK(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_getTransactionCount", "0x302259069aaa5b10dc6f29a9a3f72a8e52837cc3", "pending"). + Return(nil). + Run(func(args mock.Arguments) { + args[1].(*ethtypes.HexInteger).BigInt().SetString("12345", 10) + }) + + iRes, reason, err := c.getNextNonce(ctx, []byte(sampleGetNextNonce)) + assert.NoError(t, err) + assert.Empty(t, reason) + + res := iRes.(*ffcapi.GetNextNonceResponse) + assert.Equal(t, int64(12345), res.Nonce.Int64()) + +} + +func TestGetNextNonceFail(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_getTransactionCount", "0x302259069aaa5b10dc6f29a9a3f72a8e52837cc3", "pending"). + Return(fmt.Errorf("pop")) + + iRes, reason, err := c.getNextNonce(ctx, []byte(sampleGetNextNonce)) + assert.Regexp(t, "pop", err) + assert.Empty(t, reason) + assert.Nil(t, iRes) + +} + +func TestGetNextNonceBadPayload(t *testing.T) { + + c, _ := newTestConnector(t) + ctx := context.Background() + + iRes, reason, err := c.getNextNonce(ctx, []byte("!json")) + assert.Regexp(t, "invalid", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + assert.Nil(t, iRes) + +} diff --git a/internal/ethereum/get_receipt.go b/internal/ethereum/get_receipt.go new file mode 100644 index 0000000..de69291 --- /dev/null +++ b/internal/ethereum/get_receipt.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 ethereum + +import ( + "context" + "encoding/json" + + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-evmconnect/internal/msgs" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" +) + +// TransactionReceipt is the receipt obtained over JSON/RPC from the ethereum client +type TransactionReceipt struct { + BlockHash *ethtypes.HexBytes0xPrefix `json:"blockHash"` + BlockNumber *ethtypes.HexInteger `json:"blockNumber"` + ContractAddress *ethtypes.Address0xHex `json:"contractAddress"` + CumulativeGasUsed *ethtypes.HexInteger `json:"cumulativeGasUsed"` + TransactionHash *ethtypes.HexBytes0xPrefix `json:"transactionHash"` + From *ethtypes.Address0xHex `json:"from"` + GasUsed *ethtypes.HexInteger `json:"gasUsed"` + Status *ethtypes.HexInteger `json:"status"` + To *ethtypes.Address0xHex `json:"to"` + TransactionIndex *ethtypes.HexInteger `json:"transactionIndex"` +} + +func (c *ethConnector) getReceipt(ctx context.Context, payload []byte) (interface{}, ffcapi.ErrorReason, error) { + + var req ffcapi.GetReceiptRequest + err := json.Unmarshal(payload, &req) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + + // Get the receipt in the back-end JSON/RPC format + var ethReceipt TransactionReceipt + err = c.backend.Invoke(ctx, ðReceipt, "eth_getTransactionReceipt", req.TransactionHash) + if err != nil { + return nil, "", err + } + isMined := ethReceipt.BlockHash != nil && ethReceipt.BlockNumber != nil && ethReceipt.BlockNumber.BigInt().Uint64() > 0 + if !isMined { + return nil, ffcapi.ErrorReasonNotFound, i18n.NewError(ctx, msgs.MsgReceiptNotAvailable, req.TransactionHash) + } + isSuccess := (ethReceipt.Status != nil && ethReceipt.Status.BigInt().Int64() > 0) + + fullReceipt, _ := json.Marshal(ðReceipt) + + var txIndex int64 + if ethReceipt.TransactionIndex != nil { + txIndex = ethReceipt.TransactionIndex.BigInt().Int64() + } + return &ffcapi.GetReceiptResponse{ + BlockNumber: (*fftypes.FFBigInt)(ethReceipt.BlockNumber), + TransactionIndex: fftypes.NewFFBigInt(txIndex), + BlockHash: ethReceipt.BlockHash.String(), + Success: isSuccess, + ExtraInfo: *fftypes.JSONAnyPtrBytes(fullReceipt), + }, "", nil + +} diff --git a/internal/ethereum/get_receipt_test.go b/internal/ethereum/get_receipt_test.go new file mode 100644 index 0000000..f1fb3e9 --- /dev/null +++ b/internal/ethereum/get_receipt_test.go @@ -0,0 +1,141 @@ +// 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 ethereum + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +const sampleGetReceipt = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "get_receipt" + }, + "transactionHash": "0x7d48ae971faf089878b57e3c28e3035540d34f38af395958d2c73c36c57c83a2" +}` + +const sampleJSONRPCReceipt = `{ + "blockHash": "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", + "blockNumber": "0x7b9", + "contractAddress": null, + "cumulativeGasUsed": "0x8414", + "effectiveGasPrice": "0x0", + "from": "0x2b1c769ef5ad304a4889f2a07a6617cd935849ae", + "gasUsed": "0x8414", + "logs": [ + { + "address": "0x302259069aaa5b10dc6f29a9a3f72a8e52837cc3", + "topics": [ + "0x805721bc246bccc732581be0c0aa2dd8f7ec93e97ba4b307be84428c98b0a12f" + ], + "data": "0x0000000000000000000000002b1c769ef5ad304a4889f2a07a6617cd935849ae00000000000000000000000000000000000000000000000000000000625829cc00000000000000000000000000000000000000000000000000000000000000e01f64cabbf2b44bff810396f2cb08186c2d460c2bd1c44058bc058267d554e724973b16c67dbcade6c509329de6aad8037bb024b7a996129f731b9f68ac5fcd9f00000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000000966665f73797374656d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002e516d546a587065445154326a377063583145347445764379334665554a71744374737036464c5762535553724a4e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014a1ad4027f59715bca7fd30dc0121be0542c713f7a2470c415e8b1d9e7df372c", + "blockNumber": "0x5", + "transactionHash": "0x7d48ae971faf089878b57e3c28e3035540d34f38af395958d2c73c36c57c83a2", + "transactionIndex": "0x0", + "blockHash": "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", + "logIndex": "0x0", + "removed": false + } + ], + "logsBloom": "0x00000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000100000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000", + "status": "0x1", + "to": "0x302259069aaa5b10dc6f29a9a3f72a8e52837cc3", + "transactionHash": "0x7d48ae971faf089878b57e3c28e3035540d34f38af395958d2c73c36c57c83a2", + "transactionIndex": "0x1e", + "type": "0x0" +}` + +func TestGetReceiptOkSuccess(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_getTransactionReceipt", + mock.MatchedBy(func(txHash string) bool { + assert.Equal(t, "0x7d48ae971faf089878b57e3c28e3035540d34f38af395958d2c73c36c57c83a2", txHash) + return true + })). + Return(nil). + Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte(sampleJSONRPCReceipt), args[1]) + assert.NoError(t, err) + }) + + iRes, reason, err := c.getReceipt(ctx, []byte(sampleGetReceipt)) + assert.NoError(t, err) + assert.Empty(t, reason) + + res := iRes.(*ffcapi.GetReceiptResponse) + assert.True(t, res.Success) + assert.Equal(t, int64(1977), res.BlockNumber.Int64()) + assert.Equal(t, int64(30), res.TransactionIndex.Int64()) + +} + +func TestGetReceiptNotFound(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_getTransactionReceipt", mock.Anything). + Return(nil). + Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte("null"), args[1]) + assert.NoError(t, err) + }) + + iRes, reason, err := c.getReceipt(ctx, []byte(sampleGetReceipt)) + assert.Regexp(t, "FF23012", err) + assert.Equal(t, ffcapi.ErrorReasonNotFound, reason) + assert.Nil(t, iRes) + +} + +func TestGetReceiptError(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_getTransactionReceipt", mock.Anything). + Return(fmt.Errorf("pop")) + + iRes, reason, err := c.getReceipt(ctx, []byte(sampleGetReceipt)) + assert.Regexp(t, "pop", err) + assert.Empty(t, "", reason) + assert.Nil(t, iRes) + +} + +func TestGetReceiptWithBadPayload(t *testing.T) { + + c, _ := newTestConnector(t) + ctx := context.Background() + + iRes, reason, err := c.getReceipt(ctx, []byte("!json")) + assert.Regexp(t, "invalid", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + assert.Nil(t, iRes) + +} diff --git a/internal/ethereum/prepare_transaction.go b/internal/ethereum/prepare_transaction.go new file mode 100644 index 0000000..7b6c6d1 --- /dev/null +++ b/internal/ethereum/prepare_transaction.go @@ -0,0 +1,162 @@ +// 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 ethereum + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-evmconnect/internal/msgs" + "github.com/hyperledger/firefly-signer/pkg/abi" + "github.com/hyperledger/firefly-signer/pkg/ethsigner" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" +) + +func (c *ethConnector) prepareTransaction(ctx context.Context, payload []byte) (interface{}, ffcapi.ErrorReason, error) { + + var req ffcapi.PrepareTransactionRequest + err := json.Unmarshal(payload, &req) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + + // Parse the input JSON data, to build the call data + callData, method, err := c.prepareCallData(ctx, &req.TransactionInput) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + + // Build the base transaction object + tx, err := c.buildTx(ctx, req.From, req.To, req.Nonce, req.Gas, req.Value, callData) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + + if req.Gas == nil || req.Gas.Int().Sign() == 0 { + // If a value for gas has not been supplied, do a gas estimate + gas, reason, err := c.estimateGas(ctx, tx, method) + if err != nil { + return nil, reason, err + } + req.Gas = (*fftypes.FFBigInt)(gas) + } + log.L(ctx).Infof("Prepared transaction method=%s dataLen=%d gas=%s", method.String(), len(callData), req.Gas.Int()) + + return &ffcapi.PrepareTransactionResponse{ + Gas: req.Gas, + TransactionData: ethtypes.HexBytes0xPrefix(callData).String(), + }, "", nil + +} + +func (c *ethConnector) prepareCallData(ctx context.Context, req *ffcapi.TransactionInput) ([]byte, *abi.Entry, error) { + + // Parse the method ABI + var method *abi.Entry + err := json.Unmarshal([]byte(req.Method), &method) + if err != nil { + return nil, nil, i18n.NewError(ctx, msgs.MsgUnmarshalABIFail, err) + } + + // Parse the params into the standard semantics of Go JSON unmarshalling, with []interface{} + ethParams := make([]interface{}, len(req.Params)) + for i, p := range req.Params { + if p != nil { + err := json.Unmarshal([]byte(*p), ðParams[i]) + if err != nil { + return nil, nil, i18n.NewError(ctx, msgs.MsgUnmarshalParamFail, i, err) + } + } + } + + // Match the parameters to the ABI call data for the method. + // Note the FireFly ABI decoding package handles formatting errors / translation etc. + paramValues, err := method.Inputs.ParseExternalDataCtx(ctx, ethParams) + if err != nil { + return nil, nil, err + } + callData, err := method.EncodeCallDataCtx(ctx, paramValues) + if err != nil { + return nil, nil, err + } + + return callData, method, err + +} + +func (c *ethConnector) buildTx(ctx context.Context, fromString, toString string, nonce, gas, value *fftypes.FFBigInt, data []byte) (*ethsigner.Transaction, error) { + + // Verify the from address, and normalize formatting to pass downstream + from, err := ethtypes.NewAddress(fromString) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgInvalidFromAddress, fromString, err) + } + + // Parse the to address (if set) + var to *ethtypes.Address0xHex + if toString != "" { + to, err = ethtypes.NewAddress(toString) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgInvalidToAddress, toString, err) + } + } + + return ðsigner.Transaction{ + From: json.RawMessage(fmt.Sprintf(`"%s"`, from)), + To: to, + Nonce: (*ethtypes.HexInteger)(nonce), + GasLimit: (*ethtypes.HexInteger)(gas), + Value: (*ethtypes.HexInteger)(value), + Data: data, + }, nil + +} + +// mapGasPrice handles a variety of inputs from the Transaction Manager policy engine +// sending the FFCAPI request. Specifically: +// - {"maxFeePerGas": "12345", "maxPriorityFeePerGas": "2345"} - EIP-1559 gas price +// - {"gasPrice": "12345"} - legacy gas price +// - "12345" - same as {"gasPrice": "12345"} +// - nil - same as {"gasPrice": "0"} +// Anything else will return an error +func (c *ethConnector) mapGasPrice(ctx context.Context, input *fftypes.JSONAny, tx *ethsigner.Transaction) error { + if input == nil { + tx.GasPrice = ethtypes.NewHexInteger64(0) + return nil + } + gasPriceObject := input.JSONObjectNowarn() + tx.MaxPriorityFeePerGas = (*ethtypes.HexInteger)(gasPriceObject.GetInteger("maxPriorityFeePerGas")) + tx.MaxFeePerGas = (*ethtypes.HexInteger)(gasPriceObject.GetInteger("maxFeePerGas")) + if tx.MaxPriorityFeePerGas.BigInt().Sign() > 0 || tx.MaxFeePerGas.BigInt().Sign() > 0 { + log.L(ctx).Debugf("maxPriorityFeePerGas=%s maxFeePerGas=%s", tx.MaxPriorityFeePerGas, tx.MaxFeePerGas) + return nil + } + tx.GasPrice = (*ethtypes.HexInteger)(gasPriceObject.GetInteger("gasPrice")) + if tx.GasPrice.BigInt().Sign() == 0 { + err := json.Unmarshal(input.Bytes(), &tx.GasPrice) + if err != nil { + return i18n.NewError(ctx, msgs.MsgGasPriceError, input.String()) + } + } + log.L(ctx).Debugf("gasPrice=%s", tx.GasPrice) + return nil +} diff --git a/internal/ethereum/prepare_transaction_test.go b/internal/ethereum/prepare_transaction_test.go new file mode 100644 index 0000000..5dba1c6 --- /dev/null +++ b/internal/ethereum/prepare_transaction_test.go @@ -0,0 +1,227 @@ +// 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 ethereum + +import ( + "context" + "fmt" + "testing" + + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-signer/pkg/ethsigner" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +const samplePrepareTXWithGas = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "prepare_transaction" + }, + "from": "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", + "to": "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771", + "gas": 1000000, + "nonce": "111", + "value": "12345678901234567890123456789", + "method": { + "inputs": [], + "name":"do", + "outputs":[], + "stateMutability":"nonpayable", + "type":"function" + }, + "params": [] +}` + +const samplePrepareTXEstimateGas = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "prepare_transaction" + }, + "from": "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", + "to": "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771", + "nonce": "222", + "method": { + "inputs": [], + "name":"do", + "outputs":[], + "stateMutability":"nonpayable", + "type":"function" + }, + "method": { + "inputs": [ + { + "internalType":" uint256", + "name": "x", + "type": "uint256" + } + ], + "name":"set", + "outputs":[], + "stateMutability":"nonpayable", + "type":"function" + }, + "params": [ 4276993775 ] +}` + +const samplePrepareTXBadMethod = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "prepare_transaction" + }, + "from": "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", + "to": "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771", + "gas": 1000000, + "method": false, + "params": [] +}` + +const samplePrepareTXBadParam = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "prepare_transaction" + }, + "from": "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", + "to": "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771", + "gas": 1000000, + "nonce": "111", + "method": { + "inputs": [ + { + "internalType":" uint256", + "name": "x", + "type": "uint256" + } + ], + "name":"set", + "outputs":[], + "stateMutability":"nonpayable", + "type":"function" + }, + "params": [ "wrong type" ] +}` + +func TestPrepareTransactionOkNoEstimate(t *testing.T) { + + c, _ := newTestConnector(t) + ctx := context.Background() + iRes, reason, err := c.prepareTransaction(ctx, []byte(samplePrepareTXWithGas)) + + assert.NoError(t, err) + assert.Empty(t, reason) + + res := iRes.(*ffcapi.PrepareTransactionResponse) + assert.Equal(t, int64(1000000), res.Gas.Int64()) + +} + +func TestPrepareTransactionWithEstimate(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_estimateGas", + mock.MatchedBy(func(tx *ethsigner.Transaction) bool { + assert.Equal(t, "0x60fe47b100000000000000000000000000000000000000000000000000000000feedbeef", tx.Data.String()) + return true + })). + Return(nil). + Run(func(args mock.Arguments) { + args[1].(*ethtypes.HexInteger).BigInt().SetString("12345", 10) + }) + + iRes, reason, err := c.prepareTransaction(ctx, []byte(samplePrepareTXEstimateGas)) + assert.NoError(t, err) + assert.Empty(t, reason) + + res := iRes.(*ffcapi.PrepareTransactionResponse) + assert.Equal(t, int64(18517) /* 1.5 uplift */, res.Gas.Int64()) + +} + +func TestPrepareTransactionWithEstimateFail(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_estimateGas", mock.Anything).Return(fmt.Errorf("pop")) + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_call", mock.Anything, "latest").Run( + func(args mock.Arguments) { + *(args[1].(*ethtypes.HexBytes0xPrefix)) = ethtypes.MustNewHexBytes0xPrefix("0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000114d75707065747279206465746563746564000000000000000000000000000000") + }, + ).Return(nil) + + iRes, reason, err := c.prepareTransaction(ctx, []byte(samplePrepareTXEstimateGas)) + assert.Regexp(t, "FF23021", err) + assert.Equal(t, ffcapi.ErrorReasonTransactionReverted, reason) + assert.Nil(t, iRes) + +} + +func TestPrepareTransactionWithBadMethod(t *testing.T) { + + c, _ := newTestConnector(t) + ctx := context.Background() + + iRes, reason, err := c.prepareTransaction(ctx, []byte(samplePrepareTXBadMethod)) + assert.Regexp(t, "FF23013", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + assert.Nil(t, iRes) + +} + +func TestPrepareTransactionWithBadParam(t *testing.T) { + + c, _ := newTestConnector(t) + ctx := context.Background() + + iRes, reason, err := c.prepareTransaction(ctx, []byte(samplePrepareTXBadParam)) + assert.Regexp(t, "FF22030", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + assert.Nil(t, iRes) + +} + +func TestPrepareTransactionWithBadPayload(t *testing.T) { + + c, _ := newTestConnector(t) + ctx := context.Background() + + iRes, reason, err := c.prepareTransaction(ctx, []byte("!json")) + assert.Regexp(t, "invalid", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + assert.Nil(t, iRes) + +} + +func TestMapFFCAPIToEthBadParams(t *testing.T) { + + c, _ := newTestConnector(t) + + _, _, err := c.prepareCallData(context.Background(), &ffcapi.TransactionInput{ + Method: "{}", + Params: []*fftypes.JSONAny{fftypes.JSONAnyPtr("!wrong")}, + }) + assert.Regexp(t, "FF23014", err) + +} diff --git a/internal/ethereum/send_transaction.go b/internal/ethereum/send_transaction.go new file mode 100644 index 0000000..99455da --- /dev/null +++ b/internal/ethereum/send_transaction.go @@ -0,0 +1,63 @@ +// 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 ethereum + +import ( + "context" + "encoding/hex" + "encoding/json" + "strings" + + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-evmconnect/internal/msgs" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" +) + +func (c *ethConnector) sendTransaction(ctx context.Context, payload []byte) (interface{}, ffcapi.ErrorReason, error) { + + var req ffcapi.SendTransactionRequest + err := json.Unmarshal(payload, &req) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + + txData, err := hex.DecodeString(strings.TrimPrefix(req.TransactionData, "0x")) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, i18n.NewError(ctx, msgs.MsgInvalidTXData, req.TransactionData, err) + } + + tx, err := c.buildTx(ctx, req.From, req.To, req.Nonce, req.Gas, req.Value, txData) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + + err = c.mapGasPrice(ctx, req.GasPrice, tx) + if err != nil { + return nil, ffcapi.ErrorReasonInvalidInputs, err + } + + var txHash ethtypes.HexBytes0xPrefix + err = c.backend.Invoke(ctx, &txHash, "eth_sendTransaction", tx) + if err != nil { + return nil, mapError(sendRPCMethods, err), err + } + return &ffcapi.SendTransactionResponse{ + TransactionHash: txHash.String(), + }, "", nil + +} diff --git a/internal/ethereum/send_transaction_test.go b/internal/ethereum/send_transaction_test.go new file mode 100644 index 0000000..6355f78 --- /dev/null +++ b/internal/ethereum/send_transaction_test.go @@ -0,0 +1,196 @@ +// 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 ethereum + +import ( + "context" + "fmt" + "testing" + + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-signer/pkg/ethsigner" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +const sampleSendTX = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "send_transaction" + }, + "from": "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", + "to": "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771", + "gas": 1000000, + "nonce": "111", + "value": "12345678901234567890123456789", + "transactionData": "0x60fe47b100000000000000000000000000000000000000000000000000000000feedbeef" +}` + +const sampleSendTXBadFrom = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "send_transaction" + } +}` + +const sampleSendTXBadTo = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "send_transaction" + }, + "from": "0x3088C3B2361e5b12c5270fA0692d2Fa6b29bdB63", + "to": "bad to" +}` + +const sampleSendTXBadData = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "send_transaction" + }, + "transactionData": "not hex" +}` + +const sampleSendTXBadGasPrice = `{ + "ffcapi": { + "version": "v1.0.0", + "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "send_transaction" + }, + "from": "0x3088C3B2361e5b12c5270fA0692d2Fa6b29bdB63", + "gasPrice": "not a number" +}` + +func TestSendTransactionOK(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_sendTransaction", + mock.MatchedBy(func(tx *ethsigner.Transaction) bool { + assert.Equal(t, "0x60fe47b100000000000000000000000000000000000000000000000000000000feedbeef", tx.Data.String()) + return true + })). + Run(func(args mock.Arguments) { + *(args[1].(*ethtypes.HexBytes0xPrefix)) = ethtypes.MustNewHexBytes0xPrefix("0x123456") + }). + Return(nil) + + iRes, reason, err := c.sendTransaction(ctx, []byte(sampleSendTX)) + assert.NoError(t, err) + assert.Empty(t, reason) + + res := iRes.(*ffcapi.SendTransactionResponse) + assert.Equal(t, "0x123456", res.TransactionHash) + + mRPC.AssertExpectations(t) + +} + +func TestSendTransactionFail(t *testing.T) { + + c, mRPC := newTestConnector(t) + ctx := context.Background() + + mRPC.On("Invoke", mock.Anything, mock.Anything, "eth_sendTransaction", + mock.MatchedBy(func(tx *ethsigner.Transaction) bool { + assert.Equal(t, "0x60fe47b100000000000000000000000000000000000000000000000000000000feedbeef", tx.Data.String()) + return true + })). + Return(fmt.Errorf("pop")) + + iRes, reason, err := c.sendTransaction(ctx, []byte(sampleSendTX)) + assert.Regexp(t, "pop", err) + assert.Empty(t, reason) + assert.Nil(t, iRes) + + mRPC.AssertExpectations(t) + +} + +func TestSendErrorMapping(t *testing.T) { + + assert.Equal(t, ffcapi.ErrorReasonNonceTooLow, mapError(sendRPCMethods, fmt.Errorf("nonce too low"))) + assert.Equal(t, ffcapi.ErrorReasonInsufficientFunds, mapError(sendRPCMethods, fmt.Errorf("insufficient funds"))) + assert.Equal(t, ffcapi.ErrorReasonTransactionUnderpriced, mapError(sendRPCMethods, fmt.Errorf("transaction underpriced"))) + assert.Equal(t, ffcapi.ErrorKnownTransaction, mapError(sendRPCMethods, fmt.Errorf("known transaction"))) + +} + +func TestSendTransactionBadFrom(t *testing.T) { + + c, _ := newTestConnector(t) + ctx := context.Background() + + iRes, reason, err := c.sendTransaction(ctx, []byte(sampleSendTXBadFrom)) + assert.Regexp(t, "FF23019", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + assert.Nil(t, iRes) + +} + +func TestSendTransactionBadTo(t *testing.T) { + + c, _ := newTestConnector(t) + ctx := context.Background() + + iRes, reason, err := c.sendTransaction(ctx, []byte(sampleSendTXBadTo)) + assert.Regexp(t, "FF23020", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + assert.Nil(t, iRes) + +} + +func TestSendTransactionBadData(t *testing.T) { + + c, _ := newTestConnector(t) + ctx := context.Background() + + iRes, reason, err := c.sendTransaction(ctx, []byte(sampleSendTXBadData)) + assert.Regexp(t, "FF23018", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + assert.Nil(t, iRes) + +} + +func TestSendTransactionBadGasPrice(t *testing.T) { + + c, _ := newTestConnector(t) + ctx := context.Background() + + iRes, reason, err := c.sendTransaction(ctx, []byte(sampleSendTXBadGasPrice)) + assert.Regexp(t, "FF23015", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + assert.Nil(t, iRes) + +} + +func TestSendTransactionBadPayload(t *testing.T) { + + c, _ := newTestConnector(t) + ctx := context.Background() + + iRes, reason, err := c.sendTransaction(ctx, []byte("!not json!")) + assert.Regexp(t, "invalid", err) + assert.Equal(t, ffcapi.ErrorReasonInvalidInputs, reason) + assert.Nil(t, iRes) + +} diff --git a/internal/ffconnector/connector.go b/internal/ffconnector/connector.go new file mode 100644 index 0000000..165b68a --- /dev/null +++ b/internal/ffconnector/connector.go @@ -0,0 +1,31 @@ +// 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 ffconnector + +import ( + "context" + + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/hyperledger/firefly-common/pkg/ffcapi" +) + +type FFCHandler func(ctx context.Context, payload []byte) (res interface{}, reason ffcapi.ErrorReason, err error) + +type Connector interface { + HandlerMap() map[ffcapi.RequestType]FFCHandler + Init(ctx context.Context, conf config.Section) (err error) +} diff --git a/internal/ffcserver/config.go b/internal/ffcserver/config.go new file mode 100644 index 0000000..de79d1e --- /dev/null +++ b/internal/ffcserver/config.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 ffcserver + +import ( + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/hyperledger/firefly-common/pkg/httpserver" +) + +const ( + DefaultListenerPort = 5102 +) + +func InitConfig(conf, corsConf config.Section) { + httpserver.InitHTTPConfig(conf, DefaultListenerPort) + httpserver.InitCORSConfig(corsConf) + +} diff --git a/internal/ffcserver/server.go b/internal/ffcserver/server.go new file mode 100644 index 0000000..0ad2f35 --- /dev/null +++ b/internal/ffcserver/server.go @@ -0,0 +1,175 @@ +// 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 ffcserver + +import ( + "context" + "encoding/json" + "io/ioutil" + "net/http" + "strconv" + + "github.com/Masterminds/semver" + "github.com/gorilla/mux" + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-common/pkg/httpserver" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-evmconnect/internal/ffconnector" + "github.com/hyperledger/firefly-evmconnect/internal/msgs" +) + +type Server interface { + Init(ctx context.Context, conf, corsConf config.Section) error + Start() error + RequestStop() + WaitStopped() error +} + +var supportedAPIVersions = "1.0.x" + +type ffcServer struct { + ctx context.Context + cancelCtx func() + connector ffconnector.Connector + handlerMap map[ffcapi.RequestType]ffconnector.FFCHandler + + started bool + apiServer httpserver.HTTPServer + apiServerDone chan error + versionCheck *semver.Constraints +} + +// NewServer performs static initialization. You must still call Init() before Start(). +func NewServer(conf config.Section, connector ffconnector.Connector) Server { + s := &ffcServer{ + connector: connector, + } + s.handlerMap = connector.HandlerMap() + return s +} + +// Init verifies the configuration values (distinct from InitConfig which simply initializes +// what configuration is allowed to be set). +func (s *ffcServer) Init(ctx context.Context, conf, corsConf config.Section) (err error) { + s.ctx, s.cancelCtx = context.WithCancel(ctx) + + s.apiServerDone = make(chan error) + s.apiServer, err = httpserver.NewHTTPServer(s.ctx, "server", s.router(), s.apiServerDone, conf, corsConf) + if err != nil { + return err + } + + s.versionCheck, _ = semver.NewConstraint(supportedAPIVersions) + + return err +} + +func (s *ffcServer) runAPIServer() { + s.apiServer.ServeHTTP(s.ctx) +} + +func (s *ffcServer) Start() error { + if s.cancelCtx == nil { + return i18n.NewError(context.Background(), msgs.MsgNotInitialized) + } + if s.started { + return nil + } + go s.runAPIServer() + s.started = true + return nil +} + +func (s *ffcServer) RequestStop() { + if s.cancelCtx != nil { + s.cancelCtx() + } +} + +func (s *ffcServer) WaitStopped() (err error) { + if s.started { + err = <-s.apiServerDone + s.started = false + } + return err +} + +func (s *ffcServer) router() *mux.Router { + mux := mux.NewRouter() + mux.Path("/").Methods(http.MethodPost).Handler(http.HandlerFunc(s.serveFFCAPI)) + return mux +} + +func (s *ffcServer) serveFFCAPI(res http.ResponseWriter, req *http.Request) { + ctx := req.Context() + var resBody interface{} + status := 200 + reason := ffcapi.ErrorReasonInvalidInputs + payload, err := ioutil.ReadAll(req.Body) + if err == nil { + var resBase ffcapi.RequestBase + var handler ffconnector.FFCHandler + _ = json.Unmarshal(payload, &resBase) + handler, err = s.validateHeader(ctx, &resBase.FFCAPI) + if err == nil { + log.L(ctx).Tracef("--> %s %s", resBase.FFCAPI.RequestType, payload) + resBody, reason, err = handler(ctx, payload) + log.L(ctx).Tracef("<-- %s %s %v", resBase.FFCAPI.RequestType, reason, err) + } + } + if err != nil { + log.L(ctx).Errorf("Request failed: %s", err) + resBody = &ffcapi.ErrorResponse{Error: err.Error(), Reason: reason} + status = s.mapReasonStatus(reason) + } + res.Header().Set("Content-Type", "application/json") + resBytes, _ := json.Marshal(resBody) + res.Header().Set("Content-Length", strconv.FormatInt(int64(len(resBytes)), 10)) + res.WriteHeader(status) + _, _ = res.Write(resBytes) +} + +func (s *ffcServer) mapReasonStatus(reason ffcapi.ErrorReason) int { + switch reason { + case ffcapi.ErrorReasonNotFound: + return http.StatusNotFound + case ffcapi.ErrorReasonInvalidInputs: + return http.StatusBadRequest + default: + return http.StatusInternalServerError + } +} + +func (s *ffcServer) validateHeader(ctx context.Context, header *ffcapi.Header) (ffconnector.FFCHandler, error) { + v, err := semver.NewVersion(string(header.Version)) + if err != nil { + return nil, i18n.NewError(ctx, msgs.MsgBadVersion, header.Version, err) + } + if !s.versionCheck.Check(v) { + return nil, i18n.NewError(ctx, msgs.MsgUnsupportedVersion, header.Version) + } + if header.RequestID == nil { + return nil, i18n.NewError(ctx, msgs.MsgMissingRequestID) + } + handler, ok := s.handlerMap[header.RequestType] + if !ok { + return nil, i18n.NewError(ctx, msgs.MsgUnsupportedRequestType, header.RequestType) + } + return handler, nil +} diff --git a/internal/ffcserver/server_test.go b/internal/ffcserver/server_test.go new file mode 100644 index 0000000..770b252 --- /dev/null +++ b/internal/ffcserver/server_test.go @@ -0,0 +1,205 @@ +// 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 ffcserver + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/httpserver" + "github.com/hyperledger/firefly-evmconnect/internal/ffconnector" + "github.com/hyperledger/firefly-evmconnect/mocks/ffconnectormocks" + "github.com/stretchr/testify/assert" +) + +func newTestFFCAPIServer(t *testing.T) (*ffcServer, func()) { + config.RootConfigReset() + conf := config.RootSection("unittest") + InitConfig(conf, conf) + mConn := &ffconnectormocks.Connector{} + mConn.On("HandlerMap").Return(map[ffcapi.RequestType]ffconnector.FFCHandler{}) + s := NewServer(conf, mConn).(*ffcServer) + conf.Set(httpserver.HTTPConfPort, 0) + err := s.Init(context.Background(), conf, conf) + assert.NoError(t, err) + s.Start() + return s, func() { + s.RequestStop() + err := s.WaitStopped() + assert.NoError(t, err) + } +} + +func TestServerInitFail(t *testing.T) { + config.RootConfigReset() + conf := config.RootSection("unittest") + InitConfig(conf, conf) + mConn := &ffconnectormocks.Connector{} + mConn.On("HandlerMap").Return(map[ffcapi.RequestType]ffconnector.FFCHandler{}) + s := NewServer(conf, mConn).(*ffcServer) + conf.Set(httpserver.HTTPConfAddress, ":::::") + err := s.Init(context.Background(), conf, conf) + assert.Regexp(t, "FF00151", err) +} + +func TestServerStartNotInitialized(t *testing.T) { + config.RootConfigReset() + conf := config.RootSection("unittest") + mConn := &ffconnectormocks.Connector{} + mConn.On("HandlerMap").Return(map[ffcapi.RequestType]ffconnector.FFCHandler{}) + s := NewServer(conf, mConn).(*ffcServer) + err := s.Start() + assert.Regexp(t, "FF23024", err) +} + +func TestServerStartStop(t *testing.T) { + + s, done := newTestFFCAPIServer(t) + + // Double start should be a no-op + s.Start() + + done() +} + +func TestServerBadVersion(t *testing.T) { + + s, done := newTestFFCAPIServer(t) + defer done() + recorder := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{ + "ffcapi": { + "version": "not a sem ver" + } + }`)) + s.serveFFCAPI(recorder, req) + + assert.Equal(t, 400, recorder.Result().StatusCode) + var errRes ffcapi.ErrorResponse + err := json.NewDecoder(recorder.Body).Decode(&errRes) + assert.NoError(t, err) + assert.Regexp(t, "FF23026", errRes.Error) + assert.Regexp(t, ffcapi.ErrorReasonInvalidInputs, errRes.Reason) + +} + +func TestServerIncompatibleVersion(t *testing.T) { + + s, done := newTestFFCAPIServer(t) + defer done() + recorder := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{ + "ffcapi": { + "version": "v99.0.0" + } + }`)) + s.serveFFCAPI(recorder, req) + + assert.Equal(t, 400, recorder.Result().StatusCode) + var errRes ffcapi.ErrorResponse + err := json.NewDecoder(recorder.Body).Decode(&errRes) + assert.NoError(t, err) + assert.Regexp(t, "FF23027", errRes.Error) + assert.Regexp(t, ffcapi.ErrorReasonInvalidInputs, errRes.Reason) + +} + +func TestServerMissingID(t *testing.T) { + + s, done := newTestFFCAPIServer(t) + defer done() + recorder := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{ + "ffcapi": { + "version": "v1.0.1" + } + }`)) + s.serveFFCAPI(recorder, req) + + assert.Equal(t, 400, recorder.Result().StatusCode) + var errRes ffcapi.ErrorResponse + err := json.NewDecoder(recorder.Body).Decode(&errRes) + assert.NoError(t, err) + assert.Regexp(t, "FF23029", errRes.Error) + assert.Regexp(t, ffcapi.ErrorReasonInvalidInputs, errRes.Reason) + +} + +func TestServerUnknownRequestType(t *testing.T) { + + s, done := newTestFFCAPIServer(t) + defer done() + recorder := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{ + "ffcapi": { + "version": "v1.0.1", + "id": "`+fftypes.NewUUID().String()+`", + "type": "test" + } + }`)) + s.serveFFCAPI(recorder, req) + + assert.Equal(t, 400, recorder.Result().StatusCode) + var errRes ffcapi.ErrorResponse + err := json.NewDecoder(recorder.Body).Decode(&errRes) + assert.NoError(t, err) + assert.Regexp(t, "FF23028", errRes.Error) + assert.Regexp(t, ffcapi.ErrorReasonInvalidInputs, errRes.Reason) + +} + +func TestServerUnknownRequestOK(t *testing.T) { + + s, done := newTestFFCAPIServer(t) + defer done() + s.handlerMap[ffcapi.RequestType("test")] = func(ctx context.Context, payload []byte) (res interface{}, reason ffcapi.ErrorReason, err error) { + return map[string]interface{}{ + "test": "data", + }, "", nil + } + recorder := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{ + "ffcapi": { + "version": "v1.0.1", + "id": "`+fftypes.NewUUID().String()+`", + "type": "test" + } + }`)) + s.serveFFCAPI(recorder, req) + + assert.Equal(t, 200, recorder.Result().StatusCode) + var mapRes map[string]interface{} + err := json.NewDecoder(recorder.Body).Decode(&mapRes) + assert.NoError(t, err) + assert.Regexp(t, "data", mapRes["test"]) + +} + +func TestMapReasonStatus(t *testing.T) { + s, done := newTestFFCAPIServer(t) + defer done() + assert.Equal(t, 404, s.mapReasonStatus(ffcapi.ErrorReasonNotFound)) + assert.Equal(t, 400, s.mapReasonStatus(ffcapi.ErrorReasonInvalidInputs)) + assert.Equal(t, 500, s.mapReasonStatus("")) +} diff --git a/internal/jsonrpc/rpc.go b/internal/jsonrpc/rpc.go new file mode 100644 index 0000000..e73380c --- /dev/null +++ b/internal/jsonrpc/rpc.go @@ -0,0 +1,107 @@ +// 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 jsonrpc + +import ( + "context" + "encoding/json" + "fmt" + "sync/atomic" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-evmconnect/internal/msgs" +) + +type Client interface { + Invoke(ctx context.Context, result interface{}, method string, params ...interface{}) error +} + +func NewRPCClient(httpClient *resty.Client) Client { + return &jsonRPC{ + httpClient: httpClient, + } +} + +type jsonRPC struct { + httpClient *resty.Client + nextRPCRequestID int64 +} + +type RPCRequest struct { + JSONRpc string `json:"jsonrpc"` + ID json.RawMessage `json:"id"` + Method string `json:"method"` + Params []interface{} `json:"params,omitempty"` +} + +type RPCError struct { + Code int64 `json:"code"` + Message string `json:"message"` + Data []interface{} `json:"data,omitempty"` +} + +type RPCResponse struct { + JSONRpc string `json:"jsonrpc"` + ID json.RawMessage `json:"id"` + Result json.RawMessage `json:"result,omitempty"` + Error *RPCError `json:"error,omitempty"` +} + +func (r *RPCResponse) Message() string { + if r.Error != nil { + return r.Error.Message + } + return "" +} + +func (r *jsonRPC) Invoke(ctx context.Context, result interface{}, method string, params ...interface{}) error { + id := atomic.AddInt64(&r.nextRPCRequestID, 1) + rpcReq := &RPCRequest{ + JSONRpc: "2.0", + ID: json.RawMessage(fmt.Sprintf(`"%d"`, id)), + Method: method, + Params: params, + } + var rpcRes RPCResponse + + log.L(ctx).Infof("RPC:%s:%s --> %s", rpcReq.ID, rpcReq.ID, rpcReq.Method) + res, err := r.httpClient.R(). + SetContext(ctx). + SetBody(&rpcReq). + SetResult(rpcRes). + SetError(rpcRes). + Post("") + // Restore the original ID + rpcRes.ID = rpcReq.ID + if err != nil { + err := i18n.NewError(ctx, msgs.MsgRPCRequestFailed) + log.L(ctx).Errorf("RPC[%d] <-- ERROR: %s", id, err) + return err + } + if res.IsError() { + log.L(ctx).Errorf("RPC[%d] <-- [%d]: %s", id, res.StatusCode(), rpcRes.Message()) + err := fmt.Errorf(rpcRes.Message()) + return err + } + log.L(ctx).Infof("RPC[%d] <-- [%d] OK", id, res.StatusCode()) + if rpcRes.Result == nil { + return nil + } + return json.Unmarshal(rpcRes.Result, &result) +} diff --git a/internal/msgs/en_config_descriptions.go b/internal/msgs/en_config_descriptions.go new file mode 100644 index 0000000..b90eb0e --- /dev/null +++ b/internal/msgs/en_config_descriptions.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 msgs + +import ( + "github.com/hyperledger/firefly-common/pkg/i18n" + "golang.org/x/text/language" +) + +var ffc = func(key, translation string, fieldType string) i18n.ConfigMessageKey { + return i18n.FFC(language.AmericanEnglish, key, translation, fieldType) +} + +//revive:disable +var ( + ConfigConnectorsType = ffc("config.connectors[].type", "The type of connector", "string") + + ConfigServerAddress = ffc("config.connectors[].server.address", "Local address for the FFCAPI connector server to listen on", "string") + ConfigServerPort = ffc("config.connectors[].server.port", "Port for the FFCAPI connector server to listen on", "number") + ConfigAPIPublicURL = ffc("config.connectors[].server.publicURL", "External address callers should access API over", "string") + ConfigServerReadTimeout = ffc("config.connectors[].server.readTimeout", "The maximum time to wait when reading from an HTTP connection", "duration") + ConfigServerWriteTimeout = ffc("config.connectors[].server.writeTimeout", "The maximum time to wait when writing to a HTTP connection", "duration") + ConfigAPIShutdownTimeout = ffc("config.connectors[].server.shutdownTimeout", "The maximum amount of time to wait for any open HTTP requests to finish before shutting down the HTTP server", i18n.TimeDurationType) + + ConfigEthereumURL = ffc("config.connectors[].ethereum.url", "URL of JSON/RPC endpoint for the Ethereum node/gateway", "string") + ConfigEthereumProxyURL = ffc("config.connectors[].ethereum.proxy.url", "Optional HTTP proxy url", "string") + ConfigEthereumDataFormat = ffc("config.connectors[].ethereum.dataFormat", "Configure the JSON data format for query output and events", "map,flat_array,self_describing") + ConfigEthereumGasEstimationFactor = ffc("config.connectors[].ethereum.gasEstimationFactor", "The factor to apply to the gas estimation to determine the gas limit", "float") +) diff --git a/internal/msgs/en_error_messges.go b/internal/msgs/en_error_messges.go new file mode 100644 index 0000000..6115d0c --- /dev/null +++ b/internal/msgs/en_error_messges.go @@ -0,0 +1,53 @@ +// 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 msgs + +import ( + "github.com/hyperledger/firefly-common/pkg/i18n" + "golang.org/x/text/language" +) + +var ffe = func(key, translation string, statusHint ...int) i18n.ErrorMessageKey { + return i18n.FFE(language.AmericanEnglish, key, translation, statusHint...) +} + +//revive:disable +var ( + MsgRequestTypeNotImplemented = ffe("FF23010", "FFCAPI request '%s' not currently supported") + MsgBlockNotAvailable = ffe("FF23011", "Block not available") + MsgReceiptNotAvailable = ffe("FF23012", "Receipt not available for transaction '%s'") + MsgUnmarshalABIFail = ffe("FF23013", "Failed to parse method ABI: %s") + MsgUnmarshalParamFail = ffe("FF23014", "Failed to parse parameter %d: %s") + MsgGasPriceError = ffe("FF23015", `The gasPrice '%s' could not be parsed. Please supply a numeric string, or an object with 'gasPrice' field, or 'maxFeePerGas'/'maxPriorityFeePerGas' fields (EIP-1559)`) + MsgInvalidOutputType = ffe("FF23016", "Invalid output type: %s") + MsgInvalidGasPrice = ffe("FF23017", "Failed to parse gasPrice '%s': %s") + MsgInvalidTXData = ffe("FF23018", "Failed to parse transaction data as hex '%s': %s") + MsgInvalidFromAddress = ffe("FF23019", "Invalid 'from' address '%s': %s") + MsgInvalidToAddress = ffe("FF23020", "Invalid 'to' address '%s': %s") + MsgRevertedWithMessage = ffe("FF23021", "EVM reverted: %s") + MsgRevertedRawRevertData = ffe("FF23022", "EVM reverted: %s") + MsgReturnDataInvalid = ffe("FF23023", "EVM return data invalid: %s") + MsgNotInitialized = ffe("FF23024", "Not initialized") + MsgMissingBackendURL = ffe("FF23025", "URL must be set for the backend JSON/RPC endpoint") + MsgBadVersion = ffe("FF23026", "Bad FFCAPI Version '%s': %s") + MsgUnsupportedVersion = ffe("FF23027", "Unsupported FFCAPI Version '%s'") + MsgUnsupportedRequestType = ffe("FF23028", "Unsupported FFCAPI request type '%s'") + MsgMissingRequestID = ffe("FF23029", "Missing FFCAPI request id") + MsgRPCRequestFailed = ffe("FF23030", "Backend RPC request failed") + MsgUnknownConnector = ffe("FF23031", "Unknown connector type: '%s'") + MsgBadDataFormat = ffe("FF23032", "Unknown data format option '%s' supported: %s") +) diff --git a/mocks/ffconnectormocks/connector.go b/mocks/ffconnectormocks/connector.go new file mode 100644 index 0000000..be5f206 --- /dev/null +++ b/mocks/ffconnectormocks/connector.go @@ -0,0 +1,50 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package ffconnectormocks + +import ( + context "context" + + config "github.com/hyperledger/firefly-common/pkg/config" + + ffcapi "github.com/hyperledger/firefly-common/pkg/ffcapi" + + ffconnector "github.com/hyperledger/firefly-evmconnect/internal/ffconnector" + + mock "github.com/stretchr/testify/mock" +) + +// Connector is an autogenerated mock type for the Connector type +type Connector struct { + mock.Mock +} + +// HandlerMap provides a mock function with given fields: +func (_m *Connector) HandlerMap() map[ffcapi.RequestType]ffconnector.FFCHandler { + ret := _m.Called() + + var r0 map[ffcapi.RequestType]ffconnector.FFCHandler + if rf, ok := ret.Get(0).(func() map[ffcapi.RequestType]ffconnector.FFCHandler); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[ffcapi.RequestType]ffconnector.FFCHandler) + } + } + + return r0 +} + +// Init provides a mock function with given fields: ctx, conf +func (_m *Connector) Init(ctx context.Context, conf config.Section) error { + ret := _m.Called(ctx, conf) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, config.Section) error); ok { + r0 = rf(ctx, conf) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/mocks/ffcservermocks/server.go b/mocks/ffcservermocks/server.go new file mode 100644 index 0000000..0856731 --- /dev/null +++ b/mocks/ffcservermocks/server.go @@ -0,0 +1,63 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package ffcservermocks + +import ( + context "context" + + config "github.com/hyperledger/firefly-common/pkg/config" + + mock "github.com/stretchr/testify/mock" +) + +// Server is an autogenerated mock type for the Server type +type Server struct { + mock.Mock +} + +// Init provides a mock function with given fields: ctx, conf, corsConf +func (_m *Server) Init(ctx context.Context, conf config.Section, corsConf config.Section) error { + ret := _m.Called(ctx, conf, corsConf) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, config.Section, config.Section) error); ok { + r0 = rf(ctx, conf, corsConf) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RequestStop provides a mock function with given fields: +func (_m *Server) RequestStop() { + _m.Called() +} + +// Start provides a mock function with given fields: +func (_m *Server) Start() 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 +} + +// WaitStopped provides a mock function with given fields: +func (_m *Server) WaitStopped() 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/jsonrpcmocks/client.go b/mocks/jsonrpcmocks/client.go new file mode 100644 index 0000000..246ec1c --- /dev/null +++ b/mocks/jsonrpcmocks/client.go @@ -0,0 +1,31 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package jsonrpcmocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// Client is an autogenerated mock type for the Client type +type Client struct { + mock.Mock +} + +// Invoke provides a mock function with given fields: ctx, result, method, params +func (_m *Client) Invoke(ctx context.Context, result interface{}, method string, params ...interface{}) error { + var _ca []interface{} + _ca = append(_ca, ctx, result, method) + _ca = append(_ca, params...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, interface{}, string, ...interface{}) error); ok { + r0 = rf(ctx, result, method, params...) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/test/bad-config.evmconnect.yaml b/test/bad-config.evmconnect.yaml new file mode 100644 index 0000000..1c8db0b --- /dev/null +++ b/test/bad-config.evmconnect.yaml @@ -0,0 +1 @@ +!!!{ Not parsable \ No newline at end of file diff --git a/test/bad-server.evmconnect.yaml b/test/bad-server.evmconnect.yaml new file mode 100644 index 0000000..fb46088 --- /dev/null +++ b/test/bad-server.evmconnect.yaml @@ -0,0 +1,6 @@ +connectors: +- type: ethereum + server: + address: :::::::wrong + ethereum: + url: http://localhost:8545 diff --git a/test/firefly.evmconnect.yaml b/test/firefly.evmconnect.yaml new file mode 100644 index 0000000..e3f3fd9 --- /dev/null +++ b/test/firefly.evmconnect.yaml @@ -0,0 +1,6 @@ +connectors: +- type: ethereum + server: + port: 0 + ethereum: + url: http://localhost:8545 diff --git a/test/unknown-connector.evmconnect.yaml b/test/unknown-connector.evmconnect.yaml new file mode 100644 index 0000000..5c1a856 --- /dev/null +++ b/test/unknown-connector.evmconnect.yaml @@ -0,0 +1,2 @@ +connectors: +- type: unkonwn From 84297e0b7445091d281ab1a12e7b01173b3143fa Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Tue, 24 May 2022 23:08:48 -0400 Subject: [PATCH 2/4] Correct readme config sample Signed-off-by: Peter Broadhurst --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index e7567e8..0ac0256 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,6 @@ For a full list of configuration options see [config.md](./config.md) ```yaml connectors: -connectors: - type: ethereum server: port: 5102 From a18356f4423ed1ef19abab6d4a40916d30123fe9 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Thu, 26 May 2022 12:16:31 -0400 Subject: [PATCH 3/4] Updates from review Signed-off-by: Peter Broadhurst --- .github/settings.yml | 15 --------------- Makefile | 2 +- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/.github/settings.yml b/.github/settings.yml index 3d06257..3f164bf 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -1,4 +1,3 @@ -<<<<<<< HEAD # # SPDX-License-Identifier: Apache-2.0 # @@ -16,17 +15,3 @@ repository: private: false allow_squash_merge: true allow_merge_commit: true -======= -repository: - name: firefly-evmconnect - 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 ->>>>>>> 1ac993c (Initial commit) - allow_rebase_merge: true diff --git a/Makefile b/Makefile index 4ffa365..e61e824 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,7 @@ clean: $(VGO) clean deps: $(VGO) get ./evmconnect -docs: +reference: $(VGO) test ./cmd -timeout=10s -tags docs docker: docker build --build-arg BUILD_VERSION=${BUILD_VERSION} ${DOCKER_ARGS} -t hyperledger/firefly-evmconnect . From 4f319e28d9e38ab9ea248066a89d2f40baf2c311 Mon Sep 17 00:00:00 2001 From: Peter Broadhurst Date: Thu, 26 May 2022 12:18:15 -0400 Subject: [PATCH 4/4] Restore original license text Signed-off-by: Peter Broadhurst --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 25c9ee6..f5a8ee4 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2022 Kaleido + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.