diff --git a/.github/workflows/docker_main.yml b/.github/workflows/docker_main.yml deleted file mode 100644 index e3814e3a..00000000 --- a/.github/workflows/docker_main.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Docker Main Build - -on: - push: - branches: - - main - -jobs: - docker: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Set build tag - id: build_tag_generator - run: | - RELEASE_TAG=$(curl https://api.github.com/repos/hyperledger/firefly-transaction-manager/releases/latest -s | jq .tag_name -r) - BUILD_TAG=$RELEASE_TAG-$(date +"%Y%m%d")-$GITHUB_RUN_NUMBER - echo ::set-output name=BUILD_TAG::$BUILD_TAG - - - name: Build - run: | - make BUILD_VERSION="${GITHUB_REF##*/}" DOCKER_ARGS="\ - --label commit=$GITHUB_SHA \ - --label build_date=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ - --label tag=${{ steps.build_tag_generator.outputs.BUILD_TAG }} \ - --tag ghcr.io/hyperledger/firefly-transaction-manager:${{ steps.build_tag_generator.outputs.BUILD_TAG }}" \ - docker - - - name: Tag release - run: docker tag ghcr.io/hyperledger/firefly-transaction-manager:${{ steps.build_tag_generator.outputs.BUILD_TAG }} ghcr.io/hyperledger/firefly-transaction-manager:head - - - name: Push docker image - run: | - echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin - docker push ghcr.io/hyperledger/firefly-transaction-manager:${{ steps.build_tag_generator.outputs.BUILD_TAG }} - - - name: Push head tag - run: | - echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin - docker push ghcr.io/hyperledger/firefly-transaction-manager:head diff --git a/.github/workflows/docker_release.yml b/.github/workflows/docker_release.yml deleted file mode 100644 index 79632ee2..00000000 --- a/.github/workflows/docker_release.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Docker Release Build - -on: - release: - types: [released, prereleased] - -jobs: - docker: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Build - run: | - make BUILD_VERSION="${GITHUB_REF##*/}" DOCKER_ARGS="\ - --label commit=$GITHUB_SHA \ - --label build_date=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ - --label tag=${GITHUB_REF##*/} \ - --tag ghcr.io/hyperledger/firefly-transaction-manager:${GITHUB_REF##*/}" \ - docker - - - name: Tag release - if: github.event.action == 'released' - run: docker tag ghcr.io/hyperledger/firefly-transaction-manager:${GITHUB_REF##*/} ghcr.io/hyperledger/firefly-transaction-manager:latest - - - name: Push docker image - run: | - echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin - docker push ghcr.io/hyperledger/firefly-transaction-manager:${GITHUB_REF##*/} - - - name: Push latest tag - if: github.event.action == 'released' - run: | - echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin - docker push ghcr.io/hyperledger/firefly-transaction-manager:latest diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 91371aa9..ff3dd072 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -21,6 +21,8 @@ jobs: go-version: 1.17 - name: Build and Test + env: + TEST_FLAGS: -v run: make - name: Upload coverage diff --git a/.golangci.yml b/.golangci.yml index 6c6e82f6..39595b42 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -8,6 +8,9 @@ linters-settings: enabled-checks: [] disabled-checks: - regexpMust + gosec: + excludes: + - G402 goheader: values: regexp: diff --git a/.vscode/settings.json b/.vscode/settings.json index a537eed0..aa256abf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,13 +5,26 @@ "go.lintTool": "golangci-lint", "cSpell.words": [ "APIID", + "apitypes", + "badurl", + "blocklistener", "ccache", + "confirmationsmocks", "dataexchange", "Debugf", "devdocs", "Devel", + "distmode", + "Dont", + "ehtype", + "estype", "ethconnect", + "ethtypes", + "eventsmocks", + "eventstream", + "eventstreams", "fabconnect", + "ffapi", "ffcapi", "ffcapimocks", "ffcore", @@ -26,24 +39,43 @@ "ffresty", "ffstruct", "fftm", + "fftmrequest", "fftypes", "finalizers", + "getkin", "GJSON", + "goleveldb", + "httpserver", "hyperledger", + "idempotence", "Infof", "IPFS", + "jsonmap", + "Kaleido", + "leveldb", + "loadbalanced", + "logrus", "mtxs", "NATS", "Nowarn", "oapispec", + "oklog", + "openapi", "optype", + "persistencemocks", + "pluggable", "policyengine", + "policyenginemocks", + "policyengines", + "policyloop", "protocolid", + "restapi", "resty", "santhosh", "secp", "sigs", "stretchr", + "syndtr", "sysmessaging", "tekuri", "tmconfig", @@ -52,13 +84,20 @@ "txcommon", "txcommonmocks", "txid", + "txns", "txtype", "unflushed", + "unmarshalled", + "unmarshalling", "upgrader", + "upsert", "upserts", "Warnf", + "whconfig", + "workloaddistribution", "wsclient", - "wsconfig" + "wsconfig", + "wsmocks" ], "go.testTimeout": "10s" } diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 06dc8f3c..00000000 --- a/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM golang:1.17-buster AS builder -ARG BUILD_VERSION -ENV BUILD_VERSION=${BUILD_VERSION} -ADD . /fftm -WORKDIR /fftm -RUN make - -FROM debian:buster-slim -WORKDIR /fftm -COPY --from=builder /fftm/firefly-transaction-manager /usr/bin/fftm - -ENTRYPOINT [ "/usr/bin/fftm" ] diff --git a/Makefile b/Makefile index 051a6ecd..2fb5969e 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ VGO=go -GOFILES := $(shell find cmd internal pkg -name '*.go' -print) +GOFILES := $(shell find internal pkg -name '*.go' -print) GOBIN := $(shell $(VGO) env GOPATH)/bin LINT := $(GOBIN)/golangci-lint MOCKERY := $(GOBIN)/mockery @@ -12,43 +12,40 @@ GOGC=30 all: build test go-mod-tidy test: deps lint - $(VGO) test ./internal/... ./cmd/... -cover -coverprofile=coverage.txt -covermode=atomic -timeout=30s + $(VGO) test ./internal/... ./pkg/... -cover -coverprofile=coverage.txt -covermode=atomic -timeout=30s ${TEST_FLAGS} coverage.html: $(VGO) tool cover -html=coverage.txt coverage: test coverage.html lint: ${LINT} GOGC=20 $(LINT) run -v --timeout 5m -ffcapi: - $(eval FFCAPI_PATH := $(shell $(VGO) list -f '{{.Dir}}' github.com/hyperledger/firefly-common/pkg/ffcapi)) - + ${MOCKERY}: $(VGO) install github.com/vektra/mockery/cmd/mockery@latest ${LINT}: - $(VGO) install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + $(VGO) install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.47.0 define makemock mocks: mocks-$(strip $(1))-$(strip $(2)) -mocks-$(strip $(1))-$(strip $(2)): ${MOCKERY} ffcapi +mocks-$(strip $(1))-$(strip $(2)): ${MOCKERY} ${MOCKERY} --case underscore --dir $(1) --name $(2) --outpkg $(3) --output mocks/$(strip $(3)) endef -$(eval $(call makemock, $${FFCAPI_PATH}, API, ffcapimocks)) -$(eval $(call makemock, pkg/policyengine, PolicyEngine, policyenginemocks)) -$(eval $(call makemock, internal/confirmations, Manager, confirmationsmocks)) -$(eval $(call makemock, internal/manager, Manager, managermocks)) +$(eval $(call makemock, pkg/ffcapi, API, ffcapimocks)) +$(eval $(call makemock, pkg/policyengine, PolicyEngine, policyenginemocks)) +$(eval $(call makemock, internal/confirmations, Manager, confirmationsmocks)) +$(eval $(call makemock, internal/persistence, Persistence, persistencemocks)) +$(eval $(call makemock, internal/ws, WebSocketChannels, wsmocks)) +$(eval $(call makemock, internal/events, Stream, eventsmocks)) -firefly-transaction-manager: ${GOFILES} - $(VGO) build -o ./firefly-transaction-manager -ldflags "-X main.buildDate=`date -u +\"%Y-%m-%dT%H:%M:%SZ\"` -X main.buildVersion=$(BUILD_VERSION)" -tags=prod -tags=prod -v ./fftm go-mod-tidy: .ALWAYS $(VGO) mod tidy -build: firefly-transaction-manager +build: test .ALWAYS: ; clean: $(VGO) clean deps: - $(VGO) get ./fftm -docs: - $(VGO) test ./cmd -timeout=10s -tags docs -docker: - docker build --build-arg BUILD_VERSION=${BUILD_VERSION} ${DOCKER_ARGS} -t hyperledger/firefly-transaction-manager . \ No newline at end of file + $(VGO) get ./internal/... ./pkg/... + $(VGO) get -t ./internal/... ./pkg/... +reference: + $(VGO) test ./pkg/fftm -timeout=10s -tags docs diff --git a/cmd/fftm.go b/cmd/fftm.go deleted file mode 100644 index 54e8ba65..00000000 --- a/cmd/fftm.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package 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-transaction-manager/internal/manager" - "github.com/hyperledger/firefly-transaction-manager/internal/policyengines" - "github.com/hyperledger/firefly-transaction-manager/internal/policyengines/simple" - "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" -) - -var sigs = make(chan os.Signal, 1) - -var rootCmd = &cobra.Command{ - Use: "fftm", - Short: "Hyperledger FireFly Tranansaction Manager", - Long: ``, - RunE: func(cmd *cobra.Command, args []string) error { - return run() - }, -} - -var cfgFile string - -func init() { - rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "f", "", "config file") - rootCmd.AddCommand(versionCommand()) - rootCmd.AddCommand(configCommand()) -} - -func Execute() error { - return rootCmd.Execute() -} - -func initConfig() { - // Read the configuration, and register our policy engines - tmconfig.Reset() - policyengines.RegisterEngine(tmconfig.PolicyEngineBasePrefix, &simple.PolicyEngineFactory{}) -} - -func run() error { - - initConfig() - err := config.ReadConfig("fftm", cfgFile) - - // Setup logging after reading config (even if failed), to output header correctly - ctx, cancelCtx := context.WithCancel(context.Background()) - defer cancelCtx() - ctx = log.WithLogger(ctx, logrus.WithField("pid", fmt.Sprintf("%d", os.Getpid()))) - ctx = log.WithLogger(ctx, logrus.WithField("prefix", config.GetString(tmconfig.ManagerName))) - - config.SetupLogging(ctx) - - // Deferred error return from reading config - if err != nil { - cancelCtx() - return i18n.WrapError(ctx, err, i18n.MsgConfigFailed) - } - - // Setup signal handling to cancel the context, which shuts down the API Server - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - go func() { - sig := <-sigs - log.L(ctx).Infof("Shutting down due to %s", sig.String()) - cancelCtx() - }() - - manager, err := manager.NewManager(ctx) - if err != nil { - return err - } - err = manager.Start() - if err != nil { - return err - } - return manager.WaitStop() -} diff --git a/cmd/fftm_test.go b/cmd/fftm_test.go deleted file mode 100644 index af0ac56c..00000000 --- a/cmd/fftm_test.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright © 2021 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cmd - -import ( - "os" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -const configDir = "../test/data/config" - -func TestRunOK(t *testing.T) { - - rootCmd.SetArgs([]string{"-f", "../test/firefly.fftm.yaml"}) - defer rootCmd.SetArgs([]string{}) - - done := make(chan struct{}) - go func() { - defer close(done) - err := Execute() - assert.NoError(t, err) - }() - - time.Sleep(10 * time.Millisecond) - sigs <- os.Kill - - <-done - -} - -func TestRunMissingConfig(t *testing.T) { - - rootCmd.SetArgs([]string{"-f", "../test/does-not-exist.fftm.yaml"}) - defer rootCmd.SetArgs([]string{}) - - err := Execute() - assert.Regexp(t, "FF00101", err) - -} - -func TestRunBadConfig(t *testing.T) { - - rootCmd.SetArgs([]string{"-f", "../test/empty-config.fftm.yaml"}) - defer rootCmd.SetArgs([]string{}) - - err := Execute() - assert.Regexp(t, "FF21018", err) - -} - -func TestRunFailStartup(t *testing.T) { - rootCmd.SetArgs([]string{"-f", "../test/quick-fail.fftm.yaml"}) - defer rootCmd.SetArgs([]string{}) - - err := Execute() - assert.Regexp(t, "FF21017", err) - -} diff --git a/cmd/version.go b/cmd/version.go deleted file mode 100644 index 483baafd..00000000 --- a/cmd/version.go +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cmd - -import ( - "context" - "encoding/json" - "fmt" - "runtime/debug" - - "github.com/hyperledger/firefly-common/pkg/i18n" - "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" - "github.com/spf13/cobra" - "gopkg.in/yaml.v2" -) - -var shortened = false -var output = "json" - -var BuildDate string // set by go-releaser -var BuildCommit string // set by go-releaser -var BuildVersionOverride string // set by go-releaser - -type Info struct { - Version string `json:"Version,omitempty" yaml:"Version,omitempty"` - Commit string `json:"Commit,omitempty" yaml:"Commit,omitempty"` - Date string `json:"Date,omitempty" yaml:"Date,omitempty"` - License string `json:"License,omitempty" yaml:"License,omitempty"` -} - -func setBuildInfo(info *Info, buildInfo *debug.BuildInfo, ok bool) { - if ok { - info.Version = buildInfo.Main.Version - } -} - -func versionCommand() *cobra.Command { - versionCmd := &cobra.Command{ - Use: "version", - Short: "Prints the version info", - Long: "", - RunE: func(cmd *cobra.Command, args []string) error { - - info := &Info{ - Version: BuildVersionOverride, - Date: BuildDate, - Commit: BuildCommit, - License: "Apache-2.0", - } - - // Where you are using go install, we will get good version information usefully from Go - // When we're in go-releaser in a Github action, we will have the version passed in explicitly - if info.Version == "" { - buildInfo, ok := debug.ReadBuildInfo() - setBuildInfo(info, buildInfo, ok) - } - - if shortened { - fmt.Println(info.Version) - } else { - var ( - bytes []byte - err error - ) - - switch output { - case "json": - bytes, err = json.MarshalIndent(info, "", " ") - case "yaml": - bytes, err = yaml.Marshal(info) - default: - err = i18n.NewError(context.Background(), tmmsgs.MsgInvalidOutputType, output) - } - - if err != nil { - return err - } - - fmt.Println(string(bytes)) - } - - return nil - }, - } - - versionCmd.Flags().BoolVarP(&shortened, "short", "s", false, "print only the version") - versionCmd.Flags().StringVarP(&output, "output", "o", "json", "output format (\"yaml\"|\"json\")") - return versionCmd -} diff --git a/cmd/version_test.go b/cmd/version_test.go deleted file mode 100644 index 44d4e5f5..00000000 --- a/cmd/version_test.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package 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, "FF21010", 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/config.md b/config.md index 744b4396..afb12c8c 100644 --- a/config.md +++ b/config.md @@ -1,8 +1,8 @@ --- layout: default -title: Configuration Reference +title: pages.reference parent: Reference -nav_order: 3 +nav_order: 2 --- # Configuration Reference @@ -22,12 +22,26 @@ nav_order: 3 |Key|Description|Type|Default Value| |---|-----------|----|-------------| |address|Listener address for API|`string`|`127.0.0.1` +|defaultRequestTimeout|Default server-side request timeout for API calls|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` +|maxRequestTimeout|Maximum server-side request timeout a caller can request with a Request-Timeout header|[`time.Duration`](https://pkg.go.dev/time#Duration)|`10m` |port|Listener port for API|`int`|`5008` |publicURL|External address callers should access API over|`string`|`` |readTimeout|The maximum time to wait when reading from an HTTP connection|[`time.Duration`](https://pkg.go.dev/time#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|[`time.Duration`](https://pkg.go.dev/time#Duration)|`15s` +## api.auth + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|type|The auth plugin to use for server side authentication of requests|`string`|`` + +## api.auth.basic + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|passwordfile|The path to a .htpasswd file to use for authenticating requests. Passwords should be hashed with bcrypt.|`string`|`` + ## api.tls |Key|Description|Type|Default Value| @@ -42,48 +56,11 @@ nav_order: 3 |Key|Description|Type|Default Value| |---|-----------|----|-------------| -|blockCacheSize|The maximum number of block headers to keep in the cache|`int`|`1000` -|blockPollingInterval|How often to poll for new block headers|[`time.Duration`](https://pkg.go.dev/time#Duration)|`3s` +|blockQueueLength|Internal queue length for notifying the confirmations manager of new blocks|`int`|`50` |notificationQueueLength|Internal queue length for notifying the confirmations manager of new transactions/events|`int`|`50` |required|Number of confirmations required to consider a transaction/event final|`int`|`20` |staleReceiptTimeout|Duration after which to force a receipt check for a pending transaction|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1m` -## connector - -|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` -|expectContinueTimeout|See [ExpectContinueTimeout in the Go docs](https://pkg.go.dev/net/http#Transport)|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s` -|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|The URL of the blockchain connector|`string`|`` -|variant|The variant is the overall category of blockchain connector, defining things like how input/output definitions are passed|`string`|`evm` - -## connector.auth - -|Key|Description|Type|Default Value| -|---|-----------|----|-------------| -|password|Password|`string`|`` -|username|Username|`string`|`` - -## connector.proxy - -|Key|Description|Type|Default Value| -|---|-----------|----|-------------| -|url|Optional HTTP proxy URL to use for the blockchain connector|`string`|`` - -## connector.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` - ## cors |Key|Description|Type|Default Value| @@ -96,50 +73,31 @@ nav_order: 3 |methods| CORS setting to control the allowed methods|`string`|`[GET POST PUT PATCH DELETE]` |origins|CORS setting to control the allowed origins|`string`|`[*]` -## ffcore +## eventstreams |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` -|expectContinueTimeout|See [ExpectContinueTimeout in the Go docs](https://pkg.go.dev/net/http#Transport)|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s` -|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|The URL of the FireFly core admin API server to connect to|`string`|`` +|checkpointInterval|Regular interval to write checkpoints for an event stream listener that is not actively detecting/delivering events|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1m` -## ffcore.auth +## eventstreams.defaults |Key|Description|Type|Default Value| |---|-----------|----|-------------| -|password|Password|`string`|`` -|username|Username|`string`|`` +|batchSize|Default batch size for newly created event streams|`int`|`50` +|batchTimeout|Default batch timeout for newly created event streams|[`time.Duration`](https://pkg.go.dev/time#Duration)|`5s` +|blockedRetryDelay|Default blocked retry delay for newly created event streams|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` +|errorHandling|Default error handling for newly created event streams|'skip' or 'block'|`block` +|retryTimeout|Default retry timeout for newly created event streams|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` +|webhookRequestTimeout|Default WebHook request timeout for newly created event streams|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` +|websocketDistributionMode|Default WebSocket distribution mode for newly created event streams|'load_balance' or 'broadcast'|`load_balance` -## ffcore.proxy +## eventstreams.retry |Key|Description|Type|Default Value| |---|-----------|----|-------------| -|url|Optional HTTP proxy URL to use for the FireFly core admin API server|`string`|`` - -## ffcore.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` - -## ffcore.ws - -|Key|Description|Type|Default Value| -|---|-----------|----|-------------| -|heartbeatInterval|The amount of time to wait between heartbeat signals on the WebSocket connection|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` -|initialConnectAttempts|The number of attempts FireFly will make to connect to the WebSocket when starting up, before failing|`int`|`5` -|path|The WebSocket sever URL to which FireFly should connect|WebSocket URL `string`|`/admin/ws` -|readBufferSize|The size in bytes of the read buffer for the WebSocket connection|[`BytesSize`](https://pkg.go.dev/github.com/docker/go-units#BytesSize)|`16Kb` -|writeBufferSize|The size in bytes of the write buffer for the WebSocket connection|[`BytesSize`](https://pkg.go.dev/github.com/docker/go-units#BytesSize)|`16Kb` +|factor|Factor to increase the delay by, between each retry|`boolean`|`2` +|initialDelay|Initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms` +|maxDelay|Maximum delay between retries|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` ## log @@ -173,87 +131,125 @@ nav_order: 3 |message|Configures the JSON key containing the log message|`string`|`message` |timestamp|Configures the JSON key containing the timestamp of the log|`string`|`@timestamp` -## manager +## persistence |Key|Description|Type|Default Value| |---|-----------|----|-------------| -|name|The name of this Transaction Manager, used in operation metadata to track which operations are to be updated|`string`|`` +|type|The type of persistence to use|Only 'leveldb' currently supported|`leveldb` -## operations +## persistence.leveldb |Key|Description|Type|Default Value| |---|-----------|----|-------------| -|errorHistoryCount|The number of historical errors to retain in the operation|`int`|`25` -|types|The operation types to query in FireFly core, that might have been submitted via this Transaction Manager|string[]|`[blockchain_invoke blockchain_pin_batch token_create_pool]` +|maxHandles|The maximum number of cached file handles LevelDB should keep open|`int`|`100` +|path|The path for the LevelDB persistence directory|`string`|`` +|syncWrites|Whether to synchronously perform writes to the storage|`boolean`|`false` -## operations.changeListener +## policyengine |Key|Description|Type|Default Value| |---|-----------|----|-------------| -|enabled|Whether to enable the change event listener to detect updates made to operations outside of the FFTM|`boolean`|`` +|name|The name of the policy engine to use|`string`|`simple` -## operations.fullScan +## policyengine.simple |Key|Description|Type|Default Value| |---|-----------|----|-------------| -|minimumDelay|The minimum delay between full scans of the FireFly core API, when reconnecting, or recovering from missed events / errors|[`time.Duration`](https://pkg.go.dev/time#Duration)|`5s` -|pageSize|The page size to use when performing a full scan of the ForeFly core API on startup, or recovery|`int`|`100` -|startupMaxRetries|The page size to use when performing a full scan of the ForeFly core API on startup, or recovery|`int`|`10` +|fixedGasPrice|A fixed gasPrice value/structure to pass to the connector|Raw JSON|`` +|resubmitInterval|The time between warning and re-sending a transaction (same nonce) when a blockchain transaction has not been allocated a receipt|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` -## policyengine +## policyengine.simple.gasOracle |Key|Description|Type|Default Value| |---|-----------|----|-------------| -|name|The name of the policy engine to use|`string`|`simple` +|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)|`` +|expectContinueTimeout|See [ExpectContinueTimeout in the Go docs](https://pkg.go.dev/net/http#Transport)|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` +|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)|`` +|maxIdleConns|The max number of idle connections to hold pooled|`int`|`` +|method|The HTTP Method to use when invoking the Gas Oracle REST API|`string`|`` +|mode|The gas oracle mode|connector | restapi | disabled|`` +|queryInterval|The minimum interval between queries to the Gas Oracle|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` +|requestTimeout|The maximum amount of time that a request is allowed to remain open|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` +|template|REST API Gas Oracle: A go template to execute against the result from the Gas Oracle, to create a JSON block that will be passed as the gas price to the connector|[Go Template](https://pkg.go.dev/text/template) `string`|`` +|tlsHandshakeTimeout|The maximum amount of time to wait for a successful TLS handshake|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` +|url|REST API Gas Oracle: The URL of a Gas Oracle REST API to call|`string`|`` -## policyengine.simple +## policyengine.simple.gasOracle.auth |Key|Description|Type|Default Value| |---|-----------|----|-------------| -|fixedGasPrice|A fixed gasPrice value/structure to pass to the connector|Raw JSON|`` -|warnInterval|The time between warnings when a blockchain transaction has not been allocated a receipt|[`time.Duration`](https://pkg.go.dev/time#Duration)|`15m` +|password|Password|`string`|`` +|username|Username|`string`|`` -## policyengine.simple.gasOracle +## policyengine.simple.gasOracle.proxy + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|url|Optional HTTP proxy URL to use for the Gas Oracle REST API|`string`|`` + +## policyengine.simple.gasOracle.retry + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|count|The maximum number of times to retry|`int`|`` +|enabled|Enables retries|`boolean`|`` +|initWaitTime|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` +|maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`` + +## policyloop + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|interval|Interval at which to invoke the policy engine to evaluate outstanding transactions|[`time.Duration`](https://pkg.go.dev/time#Duration)|`10s` + +## policyloop.retry |Key|Description|Type|Default Value| |---|-----------|----|-------------| +|factor|The retry backoff factor|`boolean`|`2` +|initialDelay|The initial retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`250ms` +|maxDelay|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` + +## transactions + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|errorHistoryCount|The number of historical errors to retain in the operation|`int`|`25` +|maxInFlight|The maximum number of transactions to have in-flight with the policy engine / blockchain transaction pool|`int`|`100` +|nonceStateTimeout|How old the most recently submitted transaction record in our local state needs to be, before we make a request to the node to query the next nonce for a signing address|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1h` + +## webhooks + +|Key|Description|Type|Default Value| +|---|-----------|----|-------------| +|allowPrivateIPs|Whether to allow WebHook URLs that resolve to Private IP address ranges (vs. internet addresses)|`boolean`|`true` |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` |expectContinueTimeout|See [ExpectContinueTimeout in the Go docs](https://pkg.go.dev/net/http#Transport)|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s` |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` -|method|The HTTP Method to use when invoking the Gas Oracle REST API|`string`|`GET` -|mode|The gas oracle mode|connector | restapi | disabled|`disabled` -|queryInterval|The minimum interval between queries to the Gas Oracle|[`time.Duration`](https://pkg.go.dev/time#Duration)|`5m` |requestTimeout|The maximum amount of time that a request is allowed to remain open|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` -|template|REST API Gas Oracle: A go template to execute against the result from the Gas Oracle, to create a JSON block that will be passed as the gas price to the connector|[Go Template](https://pkg.go.dev/text/template) `string`|`` |tlsHandshakeTimeout|The maximum amount of time to wait for a successful TLS handshake|[`time.Duration`](https://pkg.go.dev/time#Duration)|`10s` -|url|REST API Gas Oracle: The URL of a Gas Oracle REST API to call|`string`|`` -## policyengine.simple.gasOracle.auth +## webhooks.auth |Key|Description|Type|Default Value| |---|-----------|----|-------------| |password|Password|`string`|`` |username|Username|`string`|`` -## policyengine.simple.gasOracle.proxy +## webhooks.proxy |Key|Description|Type|Default Value| |---|-----------|----|-------------| -|url|Optional HTTP proxy URL to use for the Gas Oracle REST API|`string`|`` +|url|Optional HTTP proxy to use when invoking WebHooks|`string`|`` -## policyengine.simple.gasOracle.retry +## webhooks.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` - -## policyloop - -|Key|Description|Type|Default Value| -|---|-----------|----|-------------| -|interval|Interval at which to invoke the policy engine to evaluate outstanding transactions|[`time.Duration`](https://pkg.go.dev/time#Duration)|`1s` \ No newline at end of file +|maxWaitTime|The maximum retry delay|[`time.Duration`](https://pkg.go.dev/time#Duration)|`30s` \ No newline at end of file diff --git a/go.mod b/go.mod index 6cb35195..53c2fafc 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,61 @@ module github.com/hyperledger/firefly-transaction-manager -go 1.16 +go 1.17 require ( + github.com/getkin/kin-openapi v0.96.0 + github.com/ghodss/yaml v1.0.0 github.com/go-resty/resty/v2 v2.7.0 github.com/gorilla/mux v1.8.0 - github.com/hashicorp/golang-lru v0.5.4 - github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e - github.com/hyperledger/firefly-common v0.1.2 + github.com/gorilla/websocket v1.5.0 + github.com/hyperledger/firefly-common v0.1.17-0.20220808193503-961a6b241a1a + github.com/oklog/ulid/v2 v2.1.0 github.com/sirupsen/logrus v1.8.1 - github.com/spf13/cobra v1.3.0 - github.com/spf13/viper v1.11.0 + github.com/spf13/viper v1.12.0 github.com/stretchr/testify v1.7.1 - gopkg.in/yaml.v2 v2.4.0 + github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 + golang.org/x/text v0.3.7 +) + +require ( + github.com/aidarkhanov/nanoid v1.0.8 // 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/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/invopop/yaml v0.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/magiconair/properties v1.8.6 // indirect + github.com/mailru/easyjson v0.7.7 // 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/nxadm/tail v1.4.8 // 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/santhosh-tekuri/jsonschema/v5 v5.0.0 // 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/stretchr/objx v0.2.0 // indirect + github.com/subosito/gotenv v1.4.0 // indirect + github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect + golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect + golang.org/x/net v0.0.0-20220531201128-c960675eff93 // indirect + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect + golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/ini.v1 v1.66.6 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 63f5928f..ed8e0870 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,3 @@ -bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -24,12 +23,10 @@ cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.88.0/go.mod h1:dnKwfYbP9hQhefiUvpbcAyoGSHUrOxR20JVElLiUvEY= cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= @@ -41,6 +38,8 @@ cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM7 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/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= 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= @@ -48,7 +47,6 @@ cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2k cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/spanner v1.24.0/go.mod h1:EZI0yH1D/PrXK0XH9Ba5LGXTXWeqZv0ClOD/19a0Z58= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= @@ -56,144 +54,37 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= -github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= -github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= -github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= -github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= -github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= -github.com/Azure/go-autorest/autorest/adal v0.9.14/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= -github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/Masterminds/squirrel v1.5.2/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= -github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= -github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= -github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= -github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= -github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= -github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= -github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= -github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= -github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= -github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= -github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg= -github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= -github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/aidarkhanov/nanoid v1.0.8 h1:yxyJkgsEDFXP7+97vc6JevMcjyb03Zw+/9fqhlVXBXA= github.com/aidarkhanov/nanoid v1.0.8/go.mod h1:vadfZHT+m4uDhttg0yY4wW3GKtl2T6i4d2Age+45pYk= -github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/apache/arrow/go/arrow v0.0.0-20210818145353-234c94e4ce64/go.mod h1:2qMFB56yOP3KzkB3PbYZ4AlUFg3a88F67TIx5lB/WwY= -github.com/apache/arrow/go/arrow v0.0.0-20211013220434-5962184e7a30/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= -github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go-v2 v1.8.0/go.mod h1:xEFuWz+3TYdlPRuo+CqATbeDWIWyaT5uAPwPaWtgse0= -github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= -github.com/aws/aws-sdk-go-v2/config v1.6.0/go.mod h1:TNtBVmka80lRPk5+S9ZqVfFszOQAGJJ9KbT3EM3CHNU= -github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw= -github.com/aws/aws-sdk-go-v2/credentials v1.3.2/go.mod h1:PACKuTJdt6AlXvEq8rFI4eDmoqDFC5DpVKQbWysaDgM= -github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.4.0/go.mod h1:Mj/U8OpDbcVcoctrYwA2bak8k/HFPdcLzI/vaiXMwuM= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.4.0/go.mod h1:eHwXu2+uE/T6gpnYWwBwqoeqRf9IXyCcolyOWDRAErQ= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.5.4/go.mod h1:Ex7XQmbFmgFHrjUX6TN3mApKW5Hglyga+F7wZHTtYhA= -github.com/aws/aws-sdk-go-v2/internal/ini v1.2.0/go.mod h1:Q5jATQc+f1MfZp3PDMhn6ry18hGvE0i8yvbXoKbnZaE= -github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.2.2/go.mod h1:EASdTcM1lGhUe1/p4gkojHwlGJkeoRjjr1sRCzup3Is= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.3.0/go.mod h1:v8ygadNyATSm6elwJ/4gzJwcFhri9RqS8skgHKiwXPU= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.2.2/go.mod h1:NXmNI41bdEsJMrD0v9rUvbGCB5GwdBEpKvUvIY3vTFg= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.5.2/go.mod h1:QuL2Ym8BkrLmN4lUofXYq6000/i5jPjosCNK//t6gak= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.7.2/go.mod h1:np7TMuJNT83O0oDOSF8i4dF3dvGqA6hPYYo6YYkzgRA= -github.com/aws/aws-sdk-go-v2/service/s3 v1.12.0/go.mod h1:6J++A5xpo7QDsIeSqPK4UHqMSyPOCopa+zKtqAMhqVQ= -github.com/aws/aws-sdk-go-v2/service/s3 v1.16.1/go.mod h1:CQe/KvWV1AqRc65KqeJjrLzr5X2ijnFTTVzJW0VBRCI= -github.com/aws/aws-sdk-go-v2/service/sso v1.3.2/go.mod h1:J21I6kF+d/6XHVk7kp/cx9YVD2TMD2TbLwtRGVcinXo= -github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= -github.com/aws/aws-sdk-go-v2/service/sts v1.6.1/go.mod h1:hLZ/AnkIKHLuPGjEiyghNEdvJ2PP0MgOxcmv9EBJ4xs= -github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= -github.com/aws/smithy-go v1.7.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= -github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= -github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= -github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4= -github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= -github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= -github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= -github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= -github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= -github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= -github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= -github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= -github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= -github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -203,153 +94,15 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/cockroachdb/cockroach-go/v2 v2.1.1/go.mod h1:7NtUnP6eK+l6k483WSYNrq3Kb23bWV10IRV1TyeSpwM= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= -github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= -github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= -github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= -github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E= -github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= -github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= -github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= -github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= -github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= -github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= -github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= -github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= -github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= -github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= -github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= -github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= -github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= -github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= -github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ= -github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= -github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= -github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= -github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= -github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= -github.com/containerd/containerd v1.5.10/go.mod h1:fvQqCfadDGga5HZyn3j4+dx56qj2I9YwBrlSdalvJYQ= -github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= -github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= -github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= -github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= -github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= -github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= -github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= -github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= -github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= -github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= -github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU= -github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk= -github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= -github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= -github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g= -github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= -github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= -github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0= -github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA= -github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow= -github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms= -github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= -github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= -github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= -github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= -github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= -github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= -github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= -github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= -github.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ= -github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= -github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk= -github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg= -github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= -github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw= -github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y= -github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= -github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8= -github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= -github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= -github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= -github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= -github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= -github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= -github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= -github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= -github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= -github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= -github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= -github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dhui/dktest v0.3.7/go.mod h1:nYMOkafiA07WchSwKnKFUSbGMb2hMm5DrCGiXYG6gwM= -github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= -github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= -github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v20.10.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= -github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -358,109 +111,43 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= -github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/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 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= -github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= -github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= -github.com/gabriel-vasile/mimetype v1.3.1/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= -github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= -github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= -github.com/getkin/kin-openapi v0.94.1-0.20220401165309-136a868a30c2/go.mod h1:LWZfzOd7PRy8GJ1dJ6mCU6tNdSfOwRac1BUPam4aw6Q= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/getkin/kin-openapi v0.96.0 h1:VVbcSdQAJzfc5kCLU7z2ezw84czu3rbC6UG1BGGzahY= +github.com/getkin/kin-openapi v0.96.0/go.mod h1:w4lRPHiyOdwGbOkLIyk+P0qCwlu7TXPCHD/64nSXzgE= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= -github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= -github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= -github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= -github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= -github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= -github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= -github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= -github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= -github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= -github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= -github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= -github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= -github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= -github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= -github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= -github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= -github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= -github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= -github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= -github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= -github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= -github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= -github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= -github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= -github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= -github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= -github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= -github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY= -github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= -github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= -github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= -github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= -github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-migrate/migrate/v4 v4.15.1/go.mod h1:/CrBenUbcDqsW29jGTR/XFqCfVi/Y6mHXlooCcSOJMQ= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -474,7 +161,6 @@ github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -492,13 +178,11 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -512,10 +196,9 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ 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/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-github/v35 v35.2.0/go.mod h1:s0515YVTI+IMrDoy9Y4pHt9ShGpzHvHO8rZ7L7acgvs= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -535,13 +218,9 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe 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-20210715191844-86eeefc3e471/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -550,48 +229,27 @@ github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pf 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/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= -github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= -github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-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 v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= @@ -600,422 +258,173 @@ github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= -github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/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 v1.0.1-0.20220505194321-9f59036d0b4e h1:QP+Yykyq7C670zb4Fs7s4lAtYmvIll4rP/y00hdOsg4= -github.com/hyperledger/firefly v1.0.1-0.20220505194321-9f59036d0b4e/go.mod h1:434LxYn4ntyK/E0dY+2dTc55caBy6BdUMYBM2gLndAI= -github.com/hyperledger/firefly-common v0.1.2 h1:zwz7WAHrK5we8bku7FO1rGZSFY+N4sDO3fWrabsnL+E= -github.com/hyperledger/firefly-common v0.1.2/go.mod h1:ytB+404+Qg00wJKx602Yi/V6Spx9d0g65lZR9y5fzlo= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/hyperledger/firefly-common v0.1.17-0.20220808193503-961a6b241a1a h1:KQJuUGh4CdZ5XXwKjyY9M0hm//uqwcUkwpxkiIEaziQ= +github.com/hyperledger/firefly-common v0.1.17-0.20220808193503-961a6b241a1a/go.mod h1:MNbaI2spBsdZYOub6Duj9xueE7Qyu9itOmJ4vE8tjYw= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= -github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= -github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= -github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= -github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.0.7/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= -github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= -github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= -github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= -github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= -github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= -github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= +github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/jarcoal/httpmock v1.1.0 h1:F47ChZj1Y2zFsCXxNkBPwNNKnAyOATcdQibk0qEdVCE= github.com/jarcoal/httpmock v1.1.0/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= -github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= -github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= -github.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= -github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= -github.com/karlseguin/ccache v2.0.3+incompatible/go.mod h1:CM9tNPzT6EdRh14+jiW8mEF9mkNZuuE51qmgGYUB93w= -github.com/karlseguin/expect v1.0.8/go.mod h1:lXdI8iGiQhmzpnnmU/EGA60vqKs8NbRNFnhhrJGoD5g= -github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= -github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/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/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4= -github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= -github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= -github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= -github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= -github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= -github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= -github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= -github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= -github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= -github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= -github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= -github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/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/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mutecomm/go-sqlcipher/v4 v4.4.0/go.mod h1:PyN04SaWalavxRGH9E8ZftG6Ju7rsPrGmQRjrEaVpiY= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA= -github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= -github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba/go.mod h1:ncO5VaFWh0Nrt+4KT4mOZboaczBZcLuHrG+/sUeP8gI= -github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= +github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 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 v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/onsi/gomega v1.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/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= -github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= -github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= -github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= -github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= -github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= -github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= -github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= -github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.0-beta.8 h1:dy81yyLYJDwMTifq24Oi/IslOslRrDSb3jwDggjz3Z0= -github.com/pelletier/go-toml/v2 v2.0.0-beta.8/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= -github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +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.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-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= -github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/qeesung/image2ascii v1.0.1/go.mod h1:kZKhyX0h2g/YXa/zdJR3JnLnJ8avHjZ3LrvEKSYyAyU= -github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/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/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= -github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= -github.com/sagikazarmark/crypt v0.5.0/go.mod h1:l+nzl7KWh51rpzp2h7t4MZWyiEWdhNpOAnclKvg+mdA= +github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE= github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/snowflakedb/gosnowflake v1.6.3/go.mod h1:6hLajn6yxuJ4xUHZegMekpq9rnQbGJ7TMwXjgTmA6lg= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= -github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0= -github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= -github.com/spf13/viper v1.11.0 h1:7OX/1FS6n7jHD1zGrZTM7WtY13ZELRyosK4k93oPr44= -github.com/spf13/viper v1.11.0/go.mod h1:djo0X/bA5+tYVoCn+C7cAYJGcVn/qYLFTG8gdUsX7Zk= -github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= -github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= +github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -1024,66 +433,23 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= -github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= +github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs= +github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= -github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= -github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= -github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= -github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= -github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/wayneashleyberry/terminal-dimensions v1.0.0/go.mod h1:PW2XrtV6KmKOPhuf7wbtcmw1/IFnC39mryRET2XbxeE= -github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= -github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= -github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM= github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= -github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= -github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= -github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= -github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= -github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -gitlab.com/hfuss/mux-prometheus v0.0.4/go.mod h1:4dALqvZzJisEAII64a6zhtdDEfvs+BjemTynBDWuRK0= -gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= -go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/api/v3 v3.5.2/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= -go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/pkg/v3 v3.5.2/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= -go.etcd.io/etcd/client/v2 v2.305.2/go.mod h1:2D7ZejHVMIfog1221iLSYlQRzrtECw3kz4I4VAQm3qI= -go.mongodb.org/mongo-driver v1.7.0/go.mod h1:Q4oFMbo1+MSNqICAdYMlC/zSTrwCogR4R8NzkI+yfU8= -go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= +go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= +go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= 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= @@ -1092,69 +458,33 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/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-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1178,35 +508,22 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190225153610-fe579d43d832/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1221,8 +538,8 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ 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-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -1235,21 +552,17 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b 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-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211013171255-e13a2654a71e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-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 h1:bRb386wvrE+oBNdF1d/Xh9mQrfQ4ecYhW5qJ5GvTGT4= golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220531201128-c960675eff93 h1:MYimHLfoXEpOhqd/zgoA/uoXzHB86AEky4LAx5ij9xA= +golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1273,7 +586,6 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1281,60 +593,39 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/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-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1342,25 +633,16 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w 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-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1368,45 +650,40 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210818153620-00dd8d7831e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/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 h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/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 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/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= @@ -1417,43 +694,25 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1482,7 +741,6 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -1495,20 +753,12 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= -gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= -gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= -gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= -gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= -google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= 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= @@ -1540,28 +790,26 @@ google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqiv google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= google.golang.org/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.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= @@ -1570,7 +818,6 @@ google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= @@ -1590,12 +837,10 @@ google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210207032614-bba0dbe2a9ea/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -1607,11 +852,8 @@ google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxH google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210630183607-d20f26d13c79/go.mod h1:yiaVoXHpRzHGyxV3o4DktVWY4mSUErTKaeEOq6C3t3U= google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210721163202-f1cecdd8b78a/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210726143408-b02e89920bf0/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= @@ -1622,11 +864,8 @@ google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEc google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211013025323-ce878158c4d4/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= @@ -1638,14 +877,16 @@ google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2 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 v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= @@ -1668,10 +909,10 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.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 v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= 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= @@ -1687,53 +928,37 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 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/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= +gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= -gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= -gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/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= @@ -1741,64 +966,7 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= -k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= -k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= -k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= -k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= -k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= -k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= -k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= -k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= -k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= -k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= -k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= -k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= -k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= -k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= -k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= -k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc= -k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= -k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= -k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg= -modernc.org/cc/v3 v3.32.4/go.mod h1:0R6jl1aZlIl2avnYfbfHBS1QB6/f+16mihBObaBC878= -modernc.org/ccgo/v3 v3.9.2/go.mod h1:gnJpy6NIVqkETT+L5zPsQFj7L2kkhfPMzOghRNv/CFo= -modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8= -modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw= -modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= -modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= -modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= -modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM= -modernc.org/libc v1.7.13-0.20210308123627-12f642a52bb8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= -modernc.org/libc v1.9.5/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= -modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8= -modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= -modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= -modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY= -modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k= -modernc.org/sqlite v1.10.6/go.mod h1:Z9FEjUtZP4qFEg6/SiADg9XCER7aYy9a/j7Pg9P7CPs= -modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= -modernc.org/tcl v1.5.2/go.mod h1:pmJYOLgpiys3oI4AeAafkcUfE+TKKilminxNyU/+Zlo= -modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= -modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= -modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/internal/blocklistener/blocklistener.go b/internal/blocklistener/blocklistener.go new file mode 100644 index 00000000..98c0c55a --- /dev/null +++ b/internal/blocklistener/blocklistener.go @@ -0,0 +1,85 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package blocklistener + +import ( + "context" + + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" +) + +type NewBlockHashConsumer interface { + NewBlockHashes() chan<- *ffcapi.BlockHashEvent +} + +// BufferChannel ensures it always pulls blocks from the channel passed to the connector +// for new block events, regardless of whether the downstream confirmations update queue +// is full blocked (likely because the event stream is blocked). +// This is critical to avoid the situation where one blocked stream, stops another stream +// from receiving block events. +// We use the same "GapPotential" flag that the connector can mark on a reconnect, to mark +// when we've had to discard events for a blocked event listener (event listeners could stay +// blocked indefinitely, so we can't leak memory by storing up an indefinite number of new +// block events). +func BufferChannel(ctx context.Context, target NewBlockHashConsumer) (buffered chan *ffcapi.BlockHashEvent, done chan struct{}) { + buffered = make(chan *ffcapi.BlockHashEvent) + done = make(chan struct{}) + go func() { + defer close(done) + var blockedUpdate *ffcapi.BlockHashEvent + for { + if blockedUpdate != nil { + select { + case blockUpdate := <-buffered: + // Have to discard this + blockedUpdate.GapPotential = true // there is a gap for sure at this point + log.L(ctx).Debugf("Blocked event stream missed new block event: %v", blockUpdate.BlockHashes) + case target.NewBlockHashes() <- blockedUpdate: + // We're not blocked any more + log.L(ctx).Infof("Event stream block-listener unblocked") + blockedUpdate = nil + case <-ctx.Done(): + log.L(ctx).Debugf("Block listener exiting (previously blocked)") + return + } + } else { + select { + case update := <-buffered: + log.L(ctx).Debugf("Received block event: %v", update.BlockHashes) + // Nothing to do unless we have confirmations turned on + if target != nil { + select { + case target.NewBlockHashes() <- update: + // all good, we passed it on + default: + // we can't deliver it immediately, we switch to blocked mode + log.L(ctx).Infof("Event stream block-listener became blocked") + // Take a copy of the block update, so we can modify (to mark a gap) without affecting other streams + var bu = *update + blockedUpdate = &bu + } + } + case <-ctx.Done(): + log.L(ctx).Debugf("Block listener exiting") + return + } + } + } + }() + return buffered, done +} diff --git a/internal/blocklistener/blocklistener_test.go b/internal/blocklistener/blocklistener_test.go new file mode 100644 index 00000000..b4d8ed29 --- /dev/null +++ b/internal/blocklistener/blocklistener_test.go @@ -0,0 +1,74 @@ +// 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 blocklistener + +import ( + "context" + "testing" + + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" +) + +type testBlockConsumer struct { + c chan *ffcapi.BlockHashEvent +} + +func (tbc *testBlockConsumer) NewBlockHashes() chan<- *ffcapi.BlockHashEvent { + return tbc.c +} + +func TestBlockListenerDoesNotBlock(t *testing.T) { + + unBuffered := make(chan *ffcapi.BlockHashEvent, 1) + ctx, cancelCtx := context.WithCancel(context.Background()) + + buffered, blockListenerDone := BufferChannel(ctx, &testBlockConsumer{c: unBuffered}) + + for i := 0; i < 100; i++ { + buffered <- &ffcapi.BlockHashEvent{} + } + + // Get the one that was stuck in the pipe + bhe := <-unBuffered + assert.False(t, bhe.GapPotential) + + // We should get the unblocking one too, with GapPotential set + bhe = <-unBuffered + assert.True(t, bhe.GapPotential) + + // Block it again + for i := 0; i < 100; i++ { + buffered <- &ffcapi.BlockHashEvent{} + } + + // And check we can exit while blocked + cancelCtx() + <-blockListenerDone + +} + +func TestExitOnContextCancel(t *testing.T) { + + unBuffered := make(chan *ffcapi.BlockHashEvent) + ctx, cancelCtx := context.WithCancel(context.Background()) + cancelCtx() + + _, blockListenerDone := BufferChannel(ctx, &testBlockConsumer{c: unBuffered}) + <-blockListenerDone + +} diff --git a/internal/confirmations/confirmations.go b/internal/confirmations/confirmations.go index ea3719f8..0fa15039 100644 --- a/internal/confirmations/confirmations.go +++ b/internal/confirmations/confirmations.go @@ -19,17 +19,16 @@ import ( "context" "fmt" "sort" - "strconv" + "sync" "time" - lru "github.com/hashicorp/golang-lru" "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/i18n" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) // Manager listens to the blocks on the chain, and attributes confirmations to @@ -39,6 +38,8 @@ type Manager interface { Notify(n *Notification) error Start() Stop() + NewBlockHashes() chan<- *ffcapi.BlockHashEvent + CheckInFlight(listenerID *fftypes.UUID) bool } type NotificationType int @@ -48,80 +49,72 @@ const ( RemovedEventLog NewTransaction RemovedTransaction - StopStream + ListenerRemoved ) type Notification struct { NotificationType NotificationType Event *EventInfo Transaction *TransactionInfo - StoppedStream *StoppedStreamInfo + RemovedListener *RemovedListenerInfo } type EventInfo struct { - StreamID string - BlockHash string - BlockNumber uint64 - TransactionHash string - TransactionIndex uint64 - LogIndex uint64 - Receipt func(receipt *ffcapi.GetReceiptResponse) - Confirmed func(confirmations []BlockInfo) + ID *ffcapi.EventID + Confirmed func(ctx context.Context, confirmations []BlockInfo) } type TransactionInfo struct { TransactionHash string - Receipt func(receipt *ffcapi.GetReceiptResponse) - Confirmed func(confirmations []BlockInfo) + Receipt func(ctx context.Context, receipt *ffcapi.TransactionReceiptResponse) + Confirmed func(ctx context.Context, confirmations []BlockInfo) } -type StoppedStreamInfo struct { - StreamID string - Completed chan struct{} +type RemovedListenerInfo struct { + ListenerID *fftypes.UUID + Completed chan struct{} } type BlockInfo struct { - BlockNumber uint64 `json:"blockNumber"` - BlockHash string `json:"blockHash"` - ParentHash string `json:"parentHash"` - TransactionHashes []string `json:"transactionHashes,omitempty"` + BlockNumber fftypes.FFuint64 `json:"blockNumber"` + BlockHash string `json:"blockHash"` + ParentHash string `json:"parentHash"` + TransactionHashes []string `json:"transactionHashes,omitempty"` } type blockConfirmationManager struct { + baseContext context.Context ctx context.Context cancelFunc func() - blockListenerID string + newBlockHashes chan *ffcapi.BlockHashEvent + connector ffcapi.API blockListenerStale bool - connectorAPI ffcapi.API requiredConfirmations int - pollingInterval time.Duration staleReceiptTimeout time.Duration - blockCache *lru.Cache bcmNotifications chan *Notification highestBlockSeen uint64 pending map[string]*pendingItem + pendingMux sync.Mutex staleReceipts map[string]bool done chan struct{} } -func NewBlockConfirmationManager(ctx context.Context, connectorAPI ffcapi.API) (Manager, error) { - var err error +func NewBlockConfirmationManager(baseContext context.Context, connector ffcapi.API, desc string) Manager { bcm := &blockConfirmationManager{ - connectorAPI: connectorAPI, + baseContext: baseContext, + connector: connector, + blockListenerStale: true, requiredConfirmations: config.GetInt(tmconfig.ConfirmationsRequired), - pollingInterval: config.GetDuration(tmconfig.ConfirmationsBlockPollingInterval), staleReceiptTimeout: config.GetDuration(tmconfig.ConfirmationsStaleReceiptTimeout), - blockListenerStale: true, bcmNotifications: make(chan *Notification, config.GetInt(tmconfig.ConfirmationsNotificationQueueLength)), pending: make(map[string]*pendingItem), staleReceipts: make(map[string]bool), + newBlockHashes: make(chan *ffcapi.BlockHashEvent, config.GetInt(tmconfig.ConfirmationsBlockQueueLength)), } - bcm.ctx, bcm.cancelFunc = context.WithCancel(ctx) - bcm.blockCache, err = lru.New(config.GetInt(tmconfig.ConfirmationsBlockCacheSize)) - if err != nil { - return nil, i18n.WrapError(bcm.ctx, err, tmmsgs.MsgCacheInitFail) - } - return bcm, nil + bcm.ctx, bcm.cancelFunc = context.WithCancel(baseContext) + // add a log context for this specific confirmation manager (as there are many within the ) + bcm.ctx = log.WithLogField(bcm.ctx, "role", fmt.Sprintf("confirmations_%s", desc)) + return bcm } type pendingType int @@ -137,15 +130,15 @@ type pendingItem struct { pType pendingType added time.Time confirmations []*BlockInfo - lastReceiptcheck time.Time - receiptCallback func(receipt *ffcapi.GetReceiptResponse) - confirmedCallback func(confirmations []BlockInfo) + lastReceiptCheck time.Time + receiptCallback func(ctx context.Context, receipt *ffcapi.TransactionReceiptResponse) + confirmedCallback func(ctx context.Context, confirmations []BlockInfo) transactionHash string - streamID string // events only - blockHash string // can be notified of changes to this for receipts - blockNumber uint64 // known at creation time for event logs - transactionIndex uint64 // known at creation time for event logs - logIndex uint64 // events only + blockHash string // can be notified of changes to this for receipts + blockNumber uint64 // known at creation time for event logs + transactionIndex uint64 // known at creation time for event logs + logIndex uint64 // events only + listenerID *fftypes.UUID // events only } func pendingKeyForTX(txHash string) string { @@ -157,7 +150,7 @@ func (pi *pendingItem) getKey() string { case pendingTypeEvent: // For events they are identified by their hash, blockNumber, transactionIndex and logIndex // If any of those change, it's a new new event - and as such we should get informed of it separately by the blockchain connector. - return fmt.Sprintf("Event[%s]:th=%s,bh=%s,bn=%d,ti=%d,li=%d", pi.streamID, pi.transactionHash, pi.blockHash, pi.blockNumber, pi.transactionIndex, pi.logIndex) + return fmt.Sprintf("Event:l=%s,th=%s,bh=%s,bn=%d,ti=%d,li=%d", pi.listenerID, pi.transactionHash, pi.blockHash, pi.blockNumber, pi.transactionIndex, pi.logIndex) case pendingTypeTransaction: // For transactions, it's simply the transaction hash that identifies it. It can go into any block return pendingKeyForTX(pi.transactionHash) @@ -182,12 +175,12 @@ func (pi *pendingItem) copyConfirmations() []BlockInfo { func (n *Notification) eventPendingItem() *pendingItem { return &pendingItem{ pType: pendingTypeEvent, - blockNumber: n.Event.BlockNumber, - blockHash: n.Event.BlockHash, - streamID: n.Event.StreamID, - transactionHash: n.Event.TransactionHash, - transactionIndex: n.Event.TransactionIndex, - logIndex: n.Event.LogIndex, + listenerID: n.Event.ID.ListenerID, + blockNumber: n.Event.ID.BlockNumber.Uint64(), + blockHash: n.Event.ID.BlockHash, + transactionHash: n.Event.ID.TransactionHash, + transactionIndex: n.Event.ID.TransactionIndex.Uint64(), + logIndex: n.Event.ID.LogIndex.Uint64(), confirmedCallback: n.Event.Confirmed, } } @@ -195,7 +188,7 @@ func (n *Notification) eventPendingItem() *pendingItem { func (n *Notification) transactionPendingItem() *pendingItem { return &pendingItem{ pType: pendingTypeTransaction, - lastReceiptcheck: time.Now(), + lastReceiptCheck: time.Now(), transactionHash: n.Transaction.TransactionHash, receiptCallback: n.Transaction.Receipt, confirmedCallback: n.Transaction.Confirmed, @@ -216,29 +209,44 @@ func (pi pendingItems) Less(i, j int) bool { (pi[i].transactionIndex == pi[j].transactionIndex && pi[i].logIndex < pi[j].logIndex))) } +type blockState struct { + bcm *blockConfirmationManager + blocks map[uint64]*BlockInfo + lowestNil uint64 +} + func (bcm *blockConfirmationManager) Start() { bcm.done = make(chan struct{}) go bcm.confirmationsListener() } func (bcm *blockConfirmationManager) Stop() { - bcm.cancelFunc() - <-bcm.done + if bcm.done != nil { + bcm.cancelFunc() + <-bcm.done + bcm.done = nil + // Reset context ready for restart + bcm.ctx, bcm.cancelFunc = context.WithCancel(bcm.baseContext) + } +} + +func (bcm *blockConfirmationManager) NewBlockHashes() chan<- *ffcapi.BlockHashEvent { + return bcm.newBlockHashes } // Notify is used to notify the confirmation manager of detection of a new logEntry addition or removal func (bcm *blockConfirmationManager) Notify(n *Notification) error { switch n.NotificationType { case NewEventLog, RemovedEventLog: - if n.Event == nil || n.Event.StreamID == "" || n.Event.TransactionHash == "" || n.Event.BlockHash == "" { + if n.Event == nil || n.Event.ID.ListenerID == nil || n.Event.ID.TransactionHash == "" || n.Event.ID.BlockHash == "" { return i18n.NewError(bcm.ctx, tmmsgs.MsgInvalidConfirmationRequest, n) } case NewTransaction, RemovedTransaction: if n.Transaction == nil || n.Transaction.TransactionHash == "" { return i18n.NewError(bcm.ctx, tmmsgs.MsgInvalidConfirmationRequest, n) } - case StopStream: - if n.StoppedStream == nil || n.StoppedStream.Completed == nil { + case ListenerRemoved: + if n.RemovedListener == nil || n.RemovedListener.Completed == nil { return i18n.NewError(bcm.ctx, tmmsgs.MsgInvalidConfirmationRequest, n) } } @@ -251,44 +259,19 @@ func (bcm *blockConfirmationManager) Notify(n *Notification) error { return nil } -func (bcm *blockConfirmationManager) createBlockListener() error { - res, _, err := bcm.connectorAPI.CreateBlockListener(bcm.ctx, &ffcapi.CreateBlockListenerRequest{}) - if err != nil { - return err - } - bcm.blockListenerStale = false - bcm.blockListenerID = res.ListenerID - log.L(bcm.ctx).Infof("Created blockListener: %s", bcm.blockListenerID) - return err -} - -func (bcm *blockConfirmationManager) pollBlockListener() ([]string, error) { - ctx, cancel := context.WithTimeout(bcm.ctx, 30*time.Second) - defer cancel() - res, reason, err := bcm.connectorAPI.GetNewBlockHashes(ctx, &ffcapi.GetNewBlockHashesRequest{ - ListenerID: bcm.blockListenerID, - }) - if err != nil { - if reason == ffcapi.ErrorReasonNotFound { - bcm.blockListenerStale = true +func (bcm *blockConfirmationManager) CheckInFlight(listenerID *fftypes.UUID) bool { + bcm.pendingMux.Lock() + defer bcm.pendingMux.Unlock() + for _, p := range bcm.pending { + if listenerID.Equals(p.listenerID) { + return true } - return nil, err } - return res.BlockHashes, nil -} - -func (bcm *blockConfirmationManager) addToCache(blockInfo *BlockInfo) { - bcm.blockCache.Add(blockInfo.BlockHash, blockInfo) - bcm.blockCache.Add(strconv.FormatUint(blockInfo.BlockNumber, 10), blockInfo) + return false } func (bcm *blockConfirmationManager) getBlockByHash(blockHash string) (*BlockInfo, error) { - cached, ok := bcm.blockCache.Get(blockHash) - if ok { - return cached.(*BlockInfo), nil - } - - res, reason, err := bcm.connectorAPI.GetBlockInfoByHash(bcm.ctx, &ffcapi.GetBlockInfoByHashRequest{ + res, reason, err := bcm.connector.BlockInfoByHash(bcm.ctx, &ffcapi.BlockInfoByHashRequest{ BlockHash: blockHash, }) if err != nil { @@ -300,23 +283,13 @@ func (bcm *blockConfirmationManager) getBlockByHash(blockHash string) (*BlockInf blockInfo := transformBlockInfo(&res.BlockInfo) log.L(bcm.ctx).Debugf("Downloaded block header by hash: %d / %s parent=%s", blockInfo.BlockNumber, blockInfo.BlockHash, blockInfo.ParentHash) - bcm.addToCache(blockInfo) return blockInfo, nil } func (bcm *blockConfirmationManager) getBlockByNumber(blockNumber uint64, expectedParentHash string) (*BlockInfo, error) { - cached, ok := bcm.blockCache.Get(strconv.FormatUint(blockNumber, 10)) - if ok { - blockInfo := cached.(*BlockInfo) - if blockInfo.ParentHash != expectedParentHash { - // Treat a missing block, or a mismatched block, both as a cache miss and query the node - log.L(bcm.ctx).Debugf("Block cache miss due to parent hash mismatch: %d / %s parent=%s required=%s ", blockInfo.BlockNumber, blockInfo.BlockHash, blockInfo.ParentHash, expectedParentHash) - } else { - return blockInfo, nil - } - } - res, reason, err := bcm.connectorAPI.GetBlockInfoByNumber(bcm.ctx, &ffcapi.GetBlockInfoByNumberRequest{ - BlockNumber: fftypes.NewFFBigInt(int64(blockNumber)), + res, reason, err := bcm.connector.BlockInfoByNumber(bcm.ctx, &ffcapi.BlockInfoByNumberRequest{ + BlockNumber: fftypes.NewFFBigInt(int64(blockNumber)), + ExpectedParentHash: expectedParentHash, }) if err != nil { if reason == ffcapi.ErrorReasonNotFound { @@ -326,13 +299,12 @@ func (bcm *blockConfirmationManager) getBlockByNumber(blockNumber uint64, expect } blockInfo := transformBlockInfo(&res.BlockInfo) log.L(bcm.ctx).Debugf("Downloaded block header by number: %d / %s parent=%s", blockInfo.BlockNumber, blockInfo.BlockHash, blockInfo.ParentHash) - bcm.addToCache(blockInfo) return blockInfo, nil } func transformBlockInfo(res *ffcapi.BlockInfo) *BlockInfo { return &BlockInfo{ - BlockNumber: res.BlockNumber.Uint64(), + BlockNumber: fftypes.FFuint64(res.BlockNumber.Uint64()), BlockHash: res.BlockHash, ParentHash: res.ParentHash, TransactionHashes: res.TransactionHashes, @@ -341,55 +313,49 @@ func transformBlockInfo(res *ffcapi.BlockInfo) *BlockInfo { func (bcm *blockConfirmationManager) confirmationsListener() { defer close(bcm.done) - pollTimer := time.NewTimer(0) notifications := make([]*Notification, 0) + blockHashes := make([]string, 0) for { - popped := false - for !popped { - select { - case <-pollTimer.C: - popped = true - case <-bcm.ctx.Done(): - log.L(bcm.ctx).Debugf("Block confirmation listener stopping") - return - case notification := <-bcm.bcmNotifications: - if notification.NotificationType == StopStream { - // Handle stream notifications immediately - bcm.streamStopped(notification) - } else { - // Defer until after we've got new logs - notifications = append(notifications, notification) - } + select { + case bhe := <-bcm.newBlockHashes: + if bhe.GapPotential { + bcm.blockListenerStale = true + } + blockHashes = append(blockHashes, bhe.BlockHashes...) + case <-bcm.ctx.Done(): + log.L(bcm.ctx).Debugf("Block confirmation listener stopping") + return + case notification := <-bcm.bcmNotifications: + if notification.NotificationType == ListenerRemoved { + // Handle listener notifications immediately + bcm.listenerRemoved(notification) + } else { + // Defer until after we've got new logs + notifications = append(notifications, notification) } } - pollTimer = time.NewTimer(bcm.pollingInterval) - // Setup a blockListener if we're missing one - if bcm.blockListenerStale { - if err := bcm.createBlockListener(); err != nil { - log.L(bcm.ctx).Errorf("Failed to create blockListener: %s", err) - continue - } + // Each time round the loop we need to have a consistent view of the chain. + // This view must not add later blocks (by number) in, or change the hash of blocks, + // otherwise we could potentially deliver things out of order. + blocks := bcm.newBlockState() - if err := bcm.walkChain(); err != nil { + if bcm.blockListenerStale { + if err := bcm.walkChain(blocks); err != nil { log.L(bcm.ctx).Errorf("Failed to create walk chain after restoring blockListener: %s", err) continue } - } - - // Do the poll - blockHashes, err := bcm.pollBlockListener() - if err != nil { - log.L(bcm.ctx).Errorf("Failed to retrieve blocks from blockListener: %s", err) - continue + bcm.blockListenerStale = false } // Process each new block bcm.processBlockHashes(blockHashes) + // Truncate the block hashes now we've processed them + blockHashes = blockHashes[:0] // Process any new notifications - we do this at the end, so it can benefit // from knowing the latest highestBlockSeen - if err := bcm.processNotifications(notifications); err != nil { + if err := bcm.processNotifications(notifications, blocks); err != nil { log.L(bcm.ctx).Errorf("Failed processing notifications: %s", err) continue } @@ -403,7 +369,7 @@ func (bcm *blockConfirmationManager) confirmationsListener() { // receipt checks, or processing block headers for pendingKey := range bcm.staleReceipts { if pending, ok := bcm.pending[pendingKey]; ok { - bcm.checkReceipt(pending) + bcm.checkReceipt(pending, blocks) } } @@ -414,7 +380,7 @@ func (bcm *blockConfirmationManager) confirmationsListener() { func (bcm *blockConfirmationManager) staleReceiptCheck() { now := time.Now() for _, pending := range bcm.pending { - if pending.pType == pendingTypeTransaction && now.Sub(pending.lastReceiptcheck) > bcm.staleReceiptTimeout { + if pending.pType == pendingTypeTransaction && now.Sub(pending.lastReceiptCheck) > bcm.staleReceiptTimeout { pendingKey := pending.getKey() log.L(bcm.ctx).Infof("Marking receipt check stale for %s", pendingKey) bcm.staleReceipts[pendingKey] = true @@ -422,14 +388,14 @@ func (bcm *blockConfirmationManager) staleReceiptCheck() { } } -func (bcm *blockConfirmationManager) processNotifications(notifications []*Notification) error { +func (bcm *blockConfirmationManager) processNotifications(notifications []*Notification, blocks *blockState) error { for _, n := range notifications { switch n.NotificationType { case NewEventLog: newItem := n.eventPendingItem() bcm.addOrReplaceItem(newItem) - if err := bcm.walkChainForItem(newItem); err != nil { + if err := bcm.walkChainForItem(newItem, blocks); err != nil { return err } case NewTransaction: @@ -437,9 +403,9 @@ func (bcm *blockConfirmationManager) processNotifications(notifications []*Notif bcm.addOrReplaceItem(newItem) bcm.staleReceipts[newItem.getKey()] = true case RemovedEventLog: - bcm.removeItem(n.eventPendingItem()) + bcm.removeItem(n.eventPendingItem().getKey(), true) case RemovedTransaction: - bcm.removeItem(n.transactionPendingItem()) + bcm.removeItem(n.transactionPendingItem().getKey(), true) default: // Note that streamStopped is handled in the polling loop directly log.L(bcm.ctx).Warnf("Unexpected notification type: %d", n.NotificationType) @@ -449,8 +415,8 @@ func (bcm *blockConfirmationManager) processNotifications(notifications []*Notif return nil } -func (bcm *blockConfirmationManager) checkReceipt(pending *pendingItem) { - res, reason, err := bcm.connectorAPI.GetReceipt(bcm.ctx, &ffcapi.GetReceiptRequest{ +func (bcm *blockConfirmationManager) checkReceipt(pending *pendingItem, blocks *blockState) { + res, reason, err := bcm.connector.TransactionReceipt(bcm.ctx, &ffcapi.TransactionReceiptRequest{ TransactionHash: pending.transactionHash, }) @@ -468,30 +434,39 @@ func (bcm *blockConfirmationManager) checkReceipt(pending *pendingItem) { log.L(bcm.ctx).Infof("Receipt for transaction %s downloaded. BlockNumber=%d BlockHash=%s", pending.transactionHash, pending.blockNumber, pending.blockHash) // Notify of the receipt if pending.receiptCallback != nil { - pending.receiptCallback(res) + pending.receiptCallback(bcm.ctx, res) } - // Need to walk the chain for this new receipt - if err = bcm.walkChainForItem(pending); err != nil { - log.L(bcm.ctx).Debugf("Failed to walk chain for transaction %s: %s", pending.transactionHash, err) - return + + if bcm.requiredConfirmations == 0 { + bcm.dispatchConfirmed(pending) + } else { + // Need to walk the chain for this new receipt + if err = bcm.walkChainForItem(pending, blocks); err != nil { + log.L(bcm.ctx).Debugf("Failed to walk chain for transaction %s: %s", pending.transactionHash, err) + return + } } } // No need to keep polling - either we now have a receipt, or normal block header monitoring will pick this one up delete(bcm.staleReceipts, pending.getKey()) } -// streamStopped removes all pending work for a given stream, and notifies once done -func (bcm *blockConfirmationManager) streamStopped(notification *Notification) { +// listenerRemoved removes all pending work for a given listener, and notifies once done +func (bcm *blockConfirmationManager) listenerRemoved(notification *Notification) { + bcm.pendingMux.Lock() + defer bcm.pendingMux.Unlock() for pendingKey, pending := range bcm.pending { - if pending.streamID == notification.StoppedStream.StreamID { + if notification.RemovedListener.ListenerID.Equals(pending.listenerID) { delete(bcm.pending, pendingKey) } } - close(notification.StoppedStream.Completed) + close(notification.RemovedListener.Completed) } // addEvent is called by the goroutine on receipt of a new event/transaction notification func (bcm *blockConfirmationManager) addOrReplaceItem(pending *pendingItem) { + bcm.pendingMux.Lock() + defer bcm.pendingMux.Unlock() pending.added = time.Now() pending.confirmations = make([]*BlockInfo, 0, bcm.requiredConfirmations) pendingKey := pending.getKey() @@ -500,9 +475,10 @@ func (bcm *blockConfirmationManager) addOrReplaceItem(pending *pendingItem) { } // removeEvent is called by the goroutine on receipt of a remove event notification -func (bcm *blockConfirmationManager) removeItem(pending *pendingItem) { - pendingKey := pending.getKey() - log.L(bcm.ctx).Infof("Removing stale item %s", pendingKey) +func (bcm *blockConfirmationManager) removeItem(pendingKey string, stale bool) { + bcm.pendingMux.Lock() + defer bcm.pendingMux.Unlock() + log.L(bcm.ctx).Debugf("Removing pending item %s (stale=%t)", pendingKey, stale) delete(bcm.pending, pendingKey) delete(bcm.staleReceipts, pendingKey) } @@ -524,8 +500,8 @@ func (bcm *blockConfirmationManager) processBlockHashes(blockHashes []string) { bcm.processBlock(block) // Update the highest block (used for efficiency in chain walks) - if block.BlockNumber > bcm.highestBlockSeen { - bcm.highestBlockSeen = block.BlockNumber + if block.BlockNumber.Uint64() > bcm.highestBlockSeen { + bcm.highestBlockSeen = block.BlockNumber.Uint64() } } } @@ -534,19 +510,23 @@ func (bcm *blockConfirmationManager) processBlock(block *BlockInfo) { // For any transactions in the block that are known to us, we need to mark them // stale to go query the receipt + l := log.L(bcm.ctx) + l.Debugf("Transactions mined in block %d / %s: %v", block.BlockNumber, block.BlockHash, block.TransactionHashes) + bcm.pendingMux.Lock() for _, txHash := range block.TransactionHashes { txKey := pendingKeyForTX(txHash) if pending, ok := bcm.pending[txKey]; ok { if pending.blockHash != block.BlockHash { - log.L(bcm.ctx).Infof("Detected transaction %s added to block %d / %s - receipt check scheduled", txHash, block.BlockNumber, block.BlockHash) + l.Infof("Detected transaction %s added to block %d / %s - receipt check scheduled", txHash, block.BlockNumber, block.BlockHash) bcm.staleReceipts[txKey] = true } } } + bcm.pendingMux.Unlock() // Go through all the events, adding in the confirmations, and popping any out // that have reached their threshold. Then drop the log before logging/processing them. - blockNumber := block.BlockNumber + blockNumber := block.BlockNumber.Uint64() var confirmed pendingItems for pendingKey, pending := range bcm.pending { if pending.blockHash != "" { @@ -555,10 +535,10 @@ func (bcm *blockConfirmationManager) processBlock(block *BlockInfo) { expectedParentHash := pending.blockHash expectedBlockNumber := pending.blockNumber + 1 for i := 0; i < (len(pending.confirmations) + 1); i++ { - log.L(bcm.ctx).Tracef("Comparing block number=%d parent=%s to %d / %s for %s", blockNumber, block.ParentHash, expectedBlockNumber, expectedParentHash, pendingKey) + l.Tracef("Comparing block number=%d parent=%s to %d / %s for %s", blockNumber, block.ParentHash, expectedBlockNumber, expectedParentHash, pendingKey) if block.ParentHash == expectedParentHash && blockNumber == expectedBlockNumber { pending.confirmations = append(pending.confirmations[0:i], block) - log.L(bcm.ctx).Infof("Confirmation %d at block %d / %s item=%s", + l.Infof("Confirmation %d at block %d / %s item=%s", len(pending.confirmations), block.BlockNumber, block.BlockHash, pending.getKey()) break } @@ -568,7 +548,6 @@ func (bcm *blockConfirmationManager) processBlock(block *BlockInfo) { expectedBlockNumber++ } if len(pending.confirmations) >= bcm.requiredConfirmations { - delete(bcm.pending, pendingKey) confirmed = append(confirmed, pending) } @@ -586,25 +565,34 @@ func (bcm *blockConfirmationManager) processBlock(block *BlockInfo) { // dispatchConfirmed drive the event stream for any events that are confirmed, and prunes the state func (bcm *blockConfirmationManager) dispatchConfirmed(item *pendingItem) { pendingKey := item.getKey() + bcm.removeItem(pendingKey, false) + log.L(bcm.ctx).Infof("Confirmed with %d confirmations event=%s", len(item.confirmations), pendingKey) - item.confirmedCallback(item.copyConfirmations() /* a safe copy outside of our cache */) + item.confirmedCallback(bcm.ctx, item.copyConfirmations() /* a safe copy outside of our cache */) } // walkChain goes through each event and sees whether it's valid, // purging any stale confirmations - or whole events if the blockListener is invalid // We do this each time our blockListener is invalidated -func (bcm *blockConfirmationManager) walkChain() error { +func (bcm *blockConfirmationManager) walkChain(blocks *blockState) error { // Grab a copy of all the pending in order + bcm.pendingMux.Lock() pendingItems := make(pendingItems, 0, len(bcm.pending)) for _, pending := range bcm.pending { pendingItems = append(pendingItems, pending) } + bcm.pendingMux.Unlock() sort.Sort(pendingItems) - // Go through them in order - using the cache for efficiency + // Go through them in order, as we must deliver them in the order on the chain. + // For the same reason we use a map _including misses_ of blocks: + // Without this map we could deliver out of order: + // If a new block were to be mined+detected while we were traversing a long list, + // then only walking the chain for later events in the list would find the block. + // This means those later events would be delivered, but the earlier ones would not. for _, pending := range pendingItems { - if err := bcm.walkChainForItem(pending); err != nil { + if err := bcm.walkChainForItem(pending, blocks); err != nil { return err } } @@ -613,7 +601,44 @@ func (bcm *blockConfirmationManager) walkChain() error { } -func (bcm *blockConfirmationManager) walkChainForItem(pending *pendingItem) (err error) { +func (bcm *blockConfirmationManager) newBlockState() *blockState { + return &blockState{ + bcm: bcm, + blocks: make(map[uint64]*BlockInfo), + } +} + +func (bs *blockState) getByNumber(blockNumber uint64, expectedParentHash string) (*BlockInfo, error) { + // blockState gives a consistent view of the chain throughout a cycle, where we perform a carefully ordered + // set of actions against our pending items. + // - We never return newer blocks after a query has been made that found a nil result at a lower block number + // - We never change the hash of a block + // If these changes happen during a cycle, we will pick them up on the next cycle rather than risk out-of-order + // delivery of events by detecting them half way through. + if bs.lowestNil > 0 && blockNumber >= bs.lowestNil { + log.L(bs.bcm.ctx).Debugf("Block %d is after chain head (cached)", blockNumber) + return nil, nil + } + block := bs.blocks[blockNumber] + if block != nil { + return block, nil + } + block, err := bs.bcm.getBlockByNumber(blockNumber, expectedParentHash) + if err != nil { + return nil, err + } + if block == nil { + if bs.lowestNil == 0 || blockNumber <= bs.lowestNil { + log.L(bs.bcm.ctx).Debugf("Block %d is after chain head", blockNumber) + bs.lowestNil = blockNumber + } + return nil, nil + } + bs.blocks[blockNumber] = block + return block, nil +} + +func (bcm *blockConfirmationManager) walkChainForItem(pending *pendingItem, blocks *blockState) (err error) { if pending.blockHash == "" { // This is a transaction that we don't yet have the receipt for @@ -632,7 +657,7 @@ func (bcm *blockConfirmationManager) walkChainForItem(pending *pendingItem) (err log.L(bcm.ctx).Debugf("Waiting for confirmation after block %d event=%s", bcm.highestBlockSeen, pendingKey) return nil } - block, err := bcm.getBlockByNumber(blockNumber, expectedParentHash) + block, err := blocks.getByNumber(blockNumber, expectedParentHash) if err != nil { return err } diff --git a/internal/confirmations/confirmations_test.go b/internal/confirmations/confirmations_test.go index d4ba4959..761a3725 100644 --- a/internal/confirmations/confirmations_test.go +++ b/internal/confirmations/confirmations_test.go @@ -22,10 +22,10 @@ import ( "time" "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-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -34,7 +34,6 @@ import ( func newTestBlockConfirmationManager(t *testing.T, enabled bool) (*blockConfirmationManager, *ffcapimocks.API) { tmconfig.Reset() config.Set(tmconfig.ConfirmationsRequired, 3) - config.Set(tmconfig.ConfirmationsBlockPollingInterval, "10ms") config.Set(tmconfig.ConfirmationsNotificationQueueLength, 1) return newTestBlockConfirmationManagerCustomConfig(t) } @@ -42,52 +41,30 @@ func newTestBlockConfirmationManager(t *testing.T, enabled bool) (*blockConfirma func newTestBlockConfirmationManagerCustomConfig(t *testing.T) (*blockConfirmationManager, *ffcapimocks.API) { logrus.SetLevel(logrus.DebugLevel) mca := &ffcapimocks.API{} - bcm, err := NewBlockConfirmationManager(context.Background(), mca) - assert.NoError(t, err) + bcm := NewBlockConfirmationManager(context.Background(), mca, "ut") return bcm.(*blockConfirmationManager), mca } -func TestBCMInitError(t *testing.T) { - tmconfig.Reset() - config.Set(tmconfig.ConfirmationsBlockCacheSize, -1) - mca := &ffcapimocks.API{} - _, err := NewBlockConfirmationManager(context.Background(), mca) - assert.Regexp(t, "FF21015", err) -} - func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, true) confirmed := make(chan []BlockInfo, 1) eventToConfirm := &EventInfo{ - StreamID: "stream1", - TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", - BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", - BlockNumber: 1001, - TransactionIndex: 5, - LogIndex: 10, - Confirmed: func(confirmations []BlockInfo) { + ID: &ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), + TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", + BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", + BlockNumber: 1001, + TransactionIndex: 5, + LogIndex: 10, + }, + Confirmed: func(ctx context.Context, confirmations []BlockInfo) { confirmed <- confirmations }, } - lastBlockDetected := false - - // Establish the block filter - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Once() // First poll for changes gives nothing, but we load up the event at this point for the next round - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Run(func(args mock.Arguments) { - bcm.Notify(&Notification{ - NotificationType: NewEventLog, - Event: eventToConfirm, - }) - }).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil).Once() + blockHashes := bcm.NewBlockHashes() // Next time round gives a block that is in the confirmation chain, but one block ahead block1003 := &BlockInfo{ @@ -95,16 +72,20 @@ func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { BlockHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", ParentHash: "0x46210d224888265c269359529618bf2f6adb2697ff52c63c10f16a2391bdd295", } - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ + blockHashes <- &ffcapi.BlockHashEvent{ BlockHashes: []string{block1003.BlockHash}, - }, ffcapi.ErrorReason(""), nil).Once() + } // The next filter gives us 1003 - which is two blocks ahead of our notified log - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1003.BlockHash - })).Return(&ffcapi.GetBlockInfoByHashResponse{ + })).Run(func(args mock.Arguments) { + err := bcm.Notify(&Notification{ + NotificationType: NewEventLog, + Event: eventToConfirm, + }) + assert.NoError(t, err) + }).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1003.BlockNumber)), BlockHash: block1003.BlockHash, @@ -118,35 +99,42 @@ func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { BlockHash: "0x46210d224888265c269359529618bf2f6adb2697ff52c63c10f16a2391bdd295", ParentHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", } - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1002 - })).Return(&ffcapi.GetBlockInfoByNumberResponse{ + })).Return(&ffcapi.BlockInfoByNumberResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1002.BlockNumber)), BlockHash: block1002.BlockHash, ParentHash: block1002.ParentHash, }, - }, ffcapi.ErrorReason(""), nil).Once() - - // Then we should walk the chain by number to fill in 1002, because our HWM is 1003. - // Note this doesn't result in any RPC calls, as we just cached the block and it matches + }, ffcapi.ErrorReason(""), nil) - // Then we get notified of 1004 to complete the last confirmation + // Notify of 1004 after we download 1003 block1004 := &BlockInfo{ BlockNumber: 1004, BlockHash: "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8", ParentHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", } - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{block1004.BlockHash}, - }, ffcapi.ErrorReason(""), nil).Once() + + // Then we should walk the chain by number to fill in 1003, because our HWM is 1003. + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { + return r.BlockNumber.Uint64() == 1003 + })).Run(func(args mock.Arguments) { + blockHashes <- &ffcapi.BlockHashEvent{ + BlockHashes: []string{block1004.BlockHash}, + } + }).Return(&ffcapi.BlockInfoByNumberResponse{ + BlockInfo: ffcapi.BlockInfo{ + BlockNumber: fftypes.NewFFBigInt(int64(block1003.BlockNumber)), + BlockHash: block1003.BlockHash, + ParentHash: block1003.ParentHash, + }, + }, ffcapi.ErrorReason(""), nil) // Which then gets downloaded, and should complete the confirmation - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1004.BlockHash - })).Return(&ffcapi.GetBlockInfoByHashResponse{ + })).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1004.BlockNumber)), BlockHash: block1004.BlockHash, @@ -154,17 +142,6 @@ func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { }, }, ffcapi.ErrorReason(""), nil).Once() - // Subsequent calls get nothing, and blocks until close anyway - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Run(func(args mock.Arguments) { - if lastBlockDetected { - <-bcm.ctx.Done() - } - }).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil).Maybe() - bcm.Start() dispatched := <-confirmed @@ -175,7 +152,6 @@ func TestBlockConfirmationManagerE2ENewEvent(t *testing.T) { }, dispatched) bcm.Stop() - <-bcm.done mca.AssertExpectations(t) } @@ -185,22 +161,18 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { confirmed := make(chan []BlockInfo, 1) eventToConfirm := &EventInfo{ - StreamID: "stream1", - TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", - BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", - BlockNumber: 1001, - TransactionIndex: 5, - LogIndex: 10, - Confirmed: func(confirmations []BlockInfo) { + ID: &ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), + TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", + BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", + BlockNumber: 1001, + TransactionIndex: 5, + LogIndex: 10, + }, + Confirmed: func(ctx context.Context, confirmations []BlockInfo) { confirmed <- confirmations }, } - lastBlockDetected := false - - // Establish the block filter - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Once() // The next filter gives us 1002, and a first 1003 block - which will later be removed block1002 := &BlockInfo{ @@ -213,26 +185,34 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { BlockHash: "0x46210d224888265c269359529618bf2f6adb2697ff52c63c10f16a2391bdd295", ParentHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", } - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ + + blockHashes := bcm.NewBlockHashes() + blockHashes <- &ffcapi.BlockHashEvent{ BlockHashes: []string{ block1002.BlockHash, block1003a.BlockHash, }, - }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + } + + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1002.BlockHash - })).Return(&ffcapi.GetBlockInfoByHashResponse{ + })).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1002.BlockNumber)), BlockHash: block1002.BlockHash, ParentHash: block1002.ParentHash, }, }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1003a.BlockHash - })).Return(&ffcapi.GetBlockInfoByHashResponse{ + })).Run(func(args mock.Arguments) { + // Notify of event after we've downloaded the 1002/1003a + err := bcm.Notify(&Notification{ + NotificationType: NewEventLog, + Event: eventToConfirm, + }) + assert.NoError(t, err) + }).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1003a.BlockNumber)), BlockHash: block1003a.BlockHash, @@ -251,26 +231,18 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { BlockHash: "0x110282339db2dfe4bfd13d78375f7883048cac6bc12f8393bd080a4e263d5d21", ParentHash: "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8", } - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{ - block1003b.BlockHash, - block1004.BlockHash, - }, - }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1003b.BlockHash - })).Return(&ffcapi.GetBlockInfoByHashResponse{ + })).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1003b.BlockNumber)), BlockHash: block1003b.BlockHash, ParentHash: block1003b.ParentHash, }, }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1004.BlockHash - })).Return(&ffcapi.GetBlockInfoByHashResponse{ + })).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1004.BlockNumber)), BlockHash: block1004.BlockHash, @@ -278,24 +250,30 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { }, }, ffcapi.ErrorReason(""), nil).Once() - // Subsequent calls get nothing, and blocks until close anyway - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { + return r.BlockNumber.Uint64() == 1002 + })).Return(&ffcapi.BlockInfoByNumberResponse{ + BlockInfo: ffcapi.BlockInfo{ + BlockNumber: fftypes.NewFFBigInt(int64(block1002.BlockNumber)), + BlockHash: block1002.BlockHash, + ParentHash: block1002.ParentHash, + }, + }, ffcapi.ErrorReason(""), nil) + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { + // Simulate 1003 disappearing from the chain + return r.BlockNumber.Uint64() == 1003 })).Run(func(args mock.Arguments) { - if lastBlockDetected { - <-bcm.ctx.Done() + // Then notify about a new 1003 which matches the event, and a 1004 + blockHashes <- &ffcapi.BlockHashEvent{ + BlockHashes: []string{ + block1003b.BlockHash, + block1004.BlockHash, + }, } - }).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil).Maybe() + }).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")) bcm.Start() - bcm.Notify(&Notification{ - NotificationType: NewEventLog, - Event: eventToConfirm, - }) - dispatched := <-confirmed assert.Equal(t, []BlockInfo{ *block1002, @@ -304,7 +282,6 @@ func TestBlockConfirmationManagerE2EFork(t *testing.T) { }, dispatched) bcm.Stop() - <-bcm.done mca.AssertExpectations(t) @@ -314,13 +291,13 @@ func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, true) confirmed := make(chan []BlockInfo, 1) - receiptReceived := make(chan *ffcapi.GetReceiptResponse, 1) + receiptReceived := make(chan *ffcapi.TransactionReceiptResponse, 1) txToConfirmForkA := &TransactionInfo{ TransactionHash: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - Confirmed: func(confirmations []BlockInfo) { + Confirmed: func(ctx context.Context, confirmations []BlockInfo) { confirmed <- confirmations }, - Receipt: func(receipt *ffcapi.GetReceiptResponse) { + Receipt: func(ctx context.Context, receipt *ffcapi.TransactionReceiptResponse) { receiptReceived <- receipt }, } @@ -330,10 +307,11 @@ func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { ParentHash: "0xea681fadcf56ee6254a0d30b255c56636ee9199c73c45f0dd5823759b2ad1ef8", } // We start with a notification for this one - bcm.Notify(&Notification{ + err := bcm.Notify(&Notification{ NotificationType: NewTransaction, Transaction: txToConfirmForkA, }) + assert.NoError(t, err) block1001b := &BlockInfo{ BlockNumber: 1001, @@ -346,52 +324,52 @@ func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { BlockHash: "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8", ParentHash: "0x33eb56730878a08e126f2d52b19242d3b3127dc7611447255928be91b2dda455", } - lastBlockDetected := false - - // Establish the block filter - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Once() // The next filter gives us 1002a, which will later be removed - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{ - block1002a.BlockHash, - }, - }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { - return r.BlockHash == block1002a.BlockHash - })).Return(&ffcapi.GetBlockInfoByHashResponse{ - BlockInfo: ffcapi.BlockInfo{ - BlockNumber: fftypes.NewFFBigInt(int64(block1002a.BlockNumber)), - BlockHash: block1002a.BlockHash, - ParentHash: block1002a.ParentHash, - }, - }, ffcapi.ErrorReason(""), nil).Once() + blockHashes := bcm.NewBlockHashes() + + // First check while walking the chain does not yield a block + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { + return r.BlockNumber.Uint64() == 1002 + })).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Once() // Transaction receipt is immediately available on fork A - mca.On("GetReceipt", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetReceiptRequest) bool { + mca.On("TransactionReceipt", mock.Anything, mock.MatchedBy(func(r *ffcapi.TransactionReceiptRequest) bool { return r.TransactionHash == txToConfirmForkA.TransactionHash - })).Return(&ffcapi.GetReceiptResponse{ + })).Run(func(args mock.Arguments) { + // Notify of the first confirmation for the first receipt - 1002a + blockHashes <- &ffcapi.BlockHashEvent{ + BlockHashes: []string{ + block1002a.BlockHash, + }, + } + }).Return(&ffcapi.TransactionReceiptResponse{ BlockHash: block1002a.ParentHash, BlockNumber: fftypes.NewFFBigInt(1001), TransactionIndex: fftypes.NewFFBigInt(0), Success: true, }, ffcapi.ErrorReason(""), nil).Once() - // Next we notify of the new block 1001b - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{ - block1001b.BlockHash, + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { + return r.BlockHash == block1002a.BlockHash + })).Run(func(args mock.Arguments) { + // Next we notify of the new block 1001b + blockHashes <- &ffcapi.BlockHashEvent{ + BlockHashes: []string{ + block1001b.BlockHash, + }, + } + }).Return(&ffcapi.BlockInfoByHashResponse{ + BlockInfo: ffcapi.BlockInfo{ + BlockNumber: fftypes.NewFFBigInt(int64(block1002a.BlockNumber)), + BlockHash: block1002a.BlockHash, + ParentHash: block1002a.ParentHash, }, }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1001b.BlockHash - })).Return(&ffcapi.GetBlockInfoByHashResponse{ + })).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1001b.BlockNumber)), BlockHash: block1001b.BlockHash, @@ -401,26 +379,15 @@ func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { }, ffcapi.ErrorReason(""), nil).Once() // Transaction receipt is then found on fork B via new block header notification - mca.On("GetReceipt", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetReceiptRequest) bool { + mca.On("TransactionReceipt", mock.Anything, mock.MatchedBy(func(r *ffcapi.TransactionReceiptRequest) bool { return r.TransactionHash == txToConfirmForkA.TransactionHash - })).Return(&ffcapi.GetReceiptResponse{ + })).Return(&ffcapi.TransactionReceiptResponse{ BlockHash: block1001b.BlockHash, BlockNumber: fftypes.NewFFBigInt(1001), TransactionIndex: fftypes.NewFFBigInt(0), Success: true, }, ffcapi.ErrorReason(""), nil).Once() - // We will go and ask for block 1002 again, as the hash mismatches our updated notification - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { - return r.BlockNumber.Uint64() == 1002 - })).Return(&ffcapi.GetBlockInfoByNumberResponse{ - BlockInfo: ffcapi.BlockInfo{ - BlockNumber: fftypes.NewFFBigInt(int64(block1002b.BlockNumber)), - BlockHash: block1002b.BlockHash, - ParentHash: block1002b.ParentHash, - }, - }, ffcapi.ErrorReason(""), nil).Once() - // Then we get the final fork up to our confirmation block1003 := &BlockInfo{ BlockNumber: 1003, @@ -432,26 +399,39 @@ func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { BlockHash: "0x110282339db2dfe4bfd13d78375f7883048cac6bc12f8393bd080a4e263d5d21", ParentHash: "0xaf47ddbd9ba81736f808045b7fccc2179bba360573b362c82544f7360db0802e", } - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{ - block1003.BlockHash, - block1004.BlockHash, + + // We will go and ask for block 1002 again, as the hash mismatches our updated notification + // Give the right answer now + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { + return r.BlockNumber.Uint64() == 1002 + })).Run(func(args mock.Arguments) { + // Notify of the new block 1003/1004 + blockHashes <- &ffcapi.BlockHashEvent{ + BlockHashes: []string{ + block1003.BlockHash, + block1004.BlockHash, + }, + } + }).Return(&ffcapi.BlockInfoByNumberResponse{ + BlockInfo: ffcapi.BlockInfo{ + BlockNumber: fftypes.NewFFBigInt(int64(block1002b.BlockNumber)), + BlockHash: block1002b.BlockHash, + ParentHash: block1002b.ParentHash, }, }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1003.BlockHash - })).Return(&ffcapi.GetBlockInfoByHashResponse{ + })).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1003.BlockNumber)), BlockHash: block1003.BlockHash, ParentHash: block1003.ParentHash, }, }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == block1004.BlockHash - })).Return(&ffcapi.GetBlockInfoByHashResponse{ + })).Return(&ffcapi.BlockInfoByHashResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1004.BlockNumber)), BlockHash: block1004.BlockHash, @@ -459,17 +439,6 @@ func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { }, }, ffcapi.ErrorReason(""), nil).Once() - // Subsequent calls get nothing, and blocks until close anyway - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Run(func(args mock.Arguments) { - if lastBlockDetected { - <-bcm.ctx.Done() - } - }).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil).Maybe() - bcm.Start() receipt := <-receiptReceived @@ -483,7 +452,6 @@ func TestBlockConfirmationManagerE2ETransactionMovedFork(t *testing.T) { }, dispatched) bcm.Stop() - <-bcm.done mca.AssertExpectations(t) @@ -494,29 +462,19 @@ func TestBlockConfirmationManagerE2EHistoricalEvent(t *testing.T) { confirmed := make(chan []BlockInfo, 1) eventToConfirm := &EventInfo{ - StreamID: "stream1", - TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", - BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", - BlockNumber: 1001, - TransactionIndex: 5, - LogIndex: 10, - Confirmed: func(confirmations []BlockInfo) { + ID: &ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), + TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", + BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", + BlockNumber: 1001, + TransactionIndex: 5, + LogIndex: 10, + }, + Confirmed: func(ctx context.Context, confirmations []BlockInfo) { confirmed <- confirmations }, } - // Establish the block filter - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Once() - - // We don't notify of any new blocks - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil) - // Then we should walk the chain by number to fill in 1002/1003, because our HWM is 1003 block1002 := &BlockInfo{ BlockNumber: 1002, @@ -533,27 +491,27 @@ func TestBlockConfirmationManagerE2EHistoricalEvent(t *testing.T) { BlockHash: "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8", ParentHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", } - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1002 - })).Return(&ffcapi.GetBlockInfoByNumberResponse{ + })).Return(&ffcapi.BlockInfoByNumberResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1002.BlockNumber)), BlockHash: block1002.BlockHash, ParentHash: block1002.ParentHash, }, }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1003 - })).Return(&ffcapi.GetBlockInfoByNumberResponse{ + })).Return(&ffcapi.BlockInfoByNumberResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1003.BlockNumber)), BlockHash: block1003.BlockHash, ParentHash: block1003.ParentHash, }, }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1004 - })).Return(&ffcapi.GetBlockInfoByNumberResponse{ + })).Return(&ffcapi.BlockInfoByNumberResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(int64(block1004.BlockNumber)), BlockHash: block1004.BlockHash, @@ -561,10 +519,11 @@ func TestBlockConfirmationManagerE2EHistoricalEvent(t *testing.T) { }, }, ffcapi.ErrorReason(""), nil).Once() - bcm.Notify(&Notification{ + err := bcm.Notify(&Notification{ NotificationType: NewEventLog, Event: eventToConfirm, }) + assert.NoError(t, err) bcm.Start() @@ -576,7 +535,6 @@ func TestBlockConfirmationManagerE2EHistoricalEvent(t *testing.T) { }, dispatched) bcm.Stop() - <-bcm.done mca.AssertExpectations(t) } @@ -599,95 +557,29 @@ func TestSortPendingEvents(t *testing.T) { }, events) } -func TestCreateBlockFilterFail(t *testing.T) { - - bcm, mca := newTestBlockConfirmationManager(t, false) - bcm.done = make(chan struct{}) - bcm.blockListenerID = "listener1" - - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return( - &ffcapi.CreateBlockListenerResponse{ListenerID: "listener1"}, - ffcapi.ErrorReason(""), - fmt.Errorf("pop"), - ).Once().Run(func(args mock.Arguments) { - bcm.cancelFunc() - }) - - bcm.confirmationsListener() - - mca.AssertExpectations(t) -} - func TestConfirmationsListenerFailWalkingChain(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) bcm.done = make(chan struct{}) - n := &Notification{ + err := bcm.Notify(&Notification{ NotificationType: NewEventLog, Event: &EventInfo{ - TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", - BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", - BlockNumber: 1001, + ID: &ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), + TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", + BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", + BlockNumber: 1001, + }, }, - } - bcm.addOrReplaceItem(n.eventPendingItem()) - - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Once().Run(func(args mock.Arguments) { - bcm.cancelFunc() }) - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { - return r.BlockNumber.Uint64() == 1002 - })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() - - bcm.confirmationsListener() - - mca.AssertExpectations(t) -} - -func TestConfirmationsListenerFailPollingBlocks(t *testing.T) { - - bcm, mca := newTestBlockConfirmationManager(t, false) - bcm.done = make(chan struct{}) - - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Once().Run(func(args mock.Arguments) { - bcm.cancelFunc() - }) - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), fmt.Errorf("pop")) - - bcm.confirmationsListener() - - mca.AssertExpectations(t) -} - -func TestConfirmationsListenerLostFilterReestablish(t *testing.T) { - - bcm, mca := newTestBlockConfirmationManager(t, false) - bcm.done = make(chan struct{}) + assert.NoError(t, err) - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Once().Twice() - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReasonNotFound, fmt.Errorf("pop")).Once() - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil).Run(func(args mock.Arguments) { + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { + return r.BlockNumber.Uint64() == 1002 + })).Run(func(args mock.Arguments) { bcm.cancelFunc() - }) + }).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() bcm.confirmationsListener() @@ -701,30 +593,25 @@ func TestConfirmationsListenerFailWalkingChainForNewEvent(t *testing.T) { confirmed := make(chan []BlockInfo, 1) eventToConfirm := &EventInfo{ - StreamID: "stream1", - TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", - BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", - BlockNumber: 1001, - TransactionIndex: 5, - LogIndex: 10, - Confirmed: func(confirmations []BlockInfo) { + ID: &ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), + TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", + BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", + BlockNumber: 1001, + TransactionIndex: 5, + LogIndex: 10, + }, + Confirmed: func(ctx context.Context, confirmations []BlockInfo) { confirmed <- confirmations }, } - bcm.Notify(&Notification{ + err := bcm.Notify(&Notification{ NotificationType: NewEventLog, Event: eventToConfirm, }) + assert.NoError(t, err) - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil) - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1002 })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once().Run(func(args mock.Arguments) { bcm.cancelFunc() @@ -735,39 +622,36 @@ func TestConfirmationsListenerFailWalkingChainForNewEvent(t *testing.T) { mca.AssertExpectations(t) } -func TestConfirmationsListenerStopStream(t *testing.T) { +func TestConfirmationsListenerRemoved(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) bcm.done = make(chan struct{}) + lid := fftypes.NewUUID() n := &Notification{ Event: &EventInfo{ - StreamID: "stream1", - TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", - BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", - BlockNumber: 1001, - TransactionIndex: 5, - LogIndex: 10, + ID: &ffcapi.EventID{ + ListenerID: lid, + TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", + BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", + BlockNumber: 1001, + TransactionIndex: 5, + LogIndex: 10, + }, }, } bcm.addOrReplaceItem(n.eventPendingItem()) completed := make(chan struct{}) - bcm.Notify(&Notification{ - NotificationType: StopStream, - StoppedStream: &StoppedStreamInfo{ - StreamID: "stream1", - Completed: completed, + err := bcm.Notify(&Notification{ + NotificationType: ListenerRemoved, + RemovedListener: &RemovedListenerInfo{ + ListenerID: lid, + Completed: completed, }, }) + assert.NoError(t, err) - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Maybe() - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil).Maybe() + mca.On("BlockInfoByNumber", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Maybe() mca.On("GetBlockInfoByNumber", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Maybe() bcm.Start() @@ -785,30 +669,25 @@ func TestConfirmationsRemoveEvent(t *testing.T) { bcm.done = make(chan struct{}) eventInfo := &EventInfo{ - StreamID: "stream1", - TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", - BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", - BlockNumber: 1001, - TransactionIndex: 5, - LogIndex: 10, + ID: &ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), + TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", + BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", + BlockNumber: 1001, + TransactionIndex: 5, + LogIndex: 10, + }, } bcm.addOrReplaceItem((&Notification{ Event: eventInfo, }).eventPendingItem()) - bcm.Notify(&Notification{ + err := bcm.Notify(&Notification{ NotificationType: RemovedEventLog, Event: eventInfo, }) + assert.NoError(t, err) - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetNewBlockHashes", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetNewBlockHashesRequest) bool { - return r.ListenerID == "listener1" - })).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1002 })).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Run(func(args mock.Arguments) { bcm.cancelFunc() @@ -818,6 +697,47 @@ func TestConfirmationsRemoveEvent(t *testing.T) { <-bcm.done assert.Empty(t, bcm.pending) + assert.False(t, bcm.CheckInFlight(eventInfo.ID.ListenerID)) + mca.AssertExpectations(t) +} + +func TestConfirmationsFailWalkChainAfterBlockGap(t *testing.T) { + + bcm, mca := newTestBlockConfirmationManager(t, false) + bcm.done = make(chan struct{}) + + eventNotification := &Notification{ + NotificationType: NewEventLog, + Event: &EventInfo{ + ID: &ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), + TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", + BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", + BlockNumber: 1001, + TransactionIndex: 5, + LogIndex: 10, + }, + }, + } + err := bcm.Notify(eventNotification) + assert.NoError(t, err) + + mca.On("BlockInfoByNumber", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Run(func(args mock.Arguments) { + bcm.NewBlockHashes() <- &ffcapi.BlockHashEvent{ + GapPotential: true, + } + }).Once() + + mca.On("BlockInfoByNumber", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Run(func(args mock.Arguments) { + bcm.cancelFunc() + }) + + bcm.confirmationsListener() + <-bcm.done + + assert.Len(t, bcm.pending, 1) + assert.True(t, bcm.CheckInFlight(eventNotification.Event.ID.ListenerID)) + assert.NotNil(t, eventNotification.eventPendingItem().getKey()) // should be the event in there, the TX should be removed mca.AssertExpectations(t) } @@ -829,30 +749,43 @@ func TestConfirmationsRemoveTransaction(t *testing.T) { txInfo := &TransactionInfo{ TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", } + eventNotification := &Notification{ + NotificationType: NewEventLog, + Event: &EventInfo{ + ID: &ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), + TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", + BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", + BlockNumber: 1001, + TransactionIndex: 5, + LogIndex: 10, + }, + }, + } bcm.addOrReplaceItem((&Notification{ Transaction: txInfo, }).transactionPendingItem()) - bcm.Notify(&Notification{ - NotificationType: RemovedTransaction, - Transaction: txInfo, - }) + go func() { + // The notification we want to test + err := bcm.Notify(&Notification{ + NotificationType: RemovedTransaction, + Transaction: txInfo, + }) + assert.NoError(t, err) + // Another notification that causes BlockInfoByNumber, so we can break the loop + err = bcm.Notify(eventNotification) + assert.NoError(t, err) + }() - mca.On("CreateBlockListener", mock.Anything, mock.Anything).Return(&ffcapi.CreateBlockListenerResponse{ - ListenerID: "listener1", - }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetNewBlockHashes", mock.Anything, mock.Anything).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetNewBlockHashes", mock.Anything, mock.Anything).Return(&ffcapi.GetNewBlockHashesResponse{ - BlockHashes: []string{}, - }, ffcapi.ErrorReason(""), nil).Once().Run(func(args mock.Arguments) { + mca.On("BlockInfoByNumber", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Run(func(args mock.Arguments) { bcm.cancelFunc() }) bcm.confirmationsListener() <-bcm.done - assert.Empty(t, bcm.pending) + assert.Len(t, bcm.pending, 1) + assert.NotNil(t, eventNotification.eventPendingItem().getKey()) // should be the event in there, the TX should be removed mca.AssertExpectations(t) } @@ -862,18 +795,20 @@ func TestWalkChainForEventBlockNotInConfirmationChain(t *testing.T) { pending := (&Notification{ Event: &EventInfo{ - StreamID: "stream1", - TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", - BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", - BlockNumber: 1001, - TransactionIndex: 5, - LogIndex: 10, + ID: &ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), + TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", + BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", + BlockNumber: 1001, + TransactionIndex: 5, + LogIndex: 10, + }, }, }).eventPendingItem() - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1002 - })).Return(&ffcapi.GetBlockInfoByNumberResponse{ + })).Return(&ffcapi.BlockInfoByNumberResponse{ BlockInfo: ffcapi.BlockInfo{ BlockNumber: fftypes.NewFFBigInt(1002), BlockHash: "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8", @@ -881,7 +816,8 @@ func TestWalkChainForEventBlockNotInConfirmationChain(t *testing.T) { }, }, ffcapi.ErrorReason(""), nil).Once() - err := bcm.walkChainForItem(pending) + blocks := bcm.newBlockState() + err := bcm.walkChainForItem(pending, blocks) assert.NoError(t, err) mca.AssertExpectations(t) @@ -893,20 +829,23 @@ func TestWalkChainForEventBlockLookupFail(t *testing.T) { pending := (&Notification{ Event: &EventInfo{ - StreamID: "stream1", - TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", - BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", - BlockNumber: 1001, - TransactionIndex: 5, - LogIndex: 10, + ID: &ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), + TransactionHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", + BlockHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", + BlockNumber: 1001, + TransactionIndex: 5, + LogIndex: 10, + }, }, }).eventPendingItem() - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 1002 })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() - err := bcm.walkChainForItem(pending) + blocks := bcm.newBlockState() + err := bcm.walkChainForItem(pending, blocks) assert.Regexp(t, "pop", err) mca.AssertExpectations(t) @@ -917,7 +856,7 @@ func TestProcessBlockHashesLookupFail(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) blockHash := "0xed21f4f73d150f16f922ae82b7485cd936ae1eca4c027516311b928360a347e8" - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == blockHash })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() @@ -931,81 +870,17 @@ func TestProcessBlockHashesLookupFail(t *testing.T) { func TestProcessNotificationsSwallowsUnknownType(t *testing.T) { bcm, _ := newTestBlockConfirmationManager(t, false) + blocks := bcm.newBlockState() bcm.processNotifications([]*Notification{ {NotificationType: NotificationType(999)}, - }) -} - -func TestGetBlockByNumberForceLookupMismatchedBlockType(t *testing.T) { - - bcm, mca := newTestBlockConfirmationManager(t, false) - - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { - return r.BlockNumber.Uint64() == 1002 - })).Return(&ffcapi.GetBlockInfoByNumberResponse{ - BlockInfo: ffcapi.BlockInfo{ - BlockNumber: fftypes.NewFFBigInt(1002), - BlockHash: "0x110282339db2dfe4bfd13d78375f7883048cac6bc12f8393bd080a4e263d5d21", - ParentHash: "0xaf47ddbd9ba81736f808045b7fccc2179bba360573b362c82544f7360db0802e", - }, - }, ffcapi.ErrorReason(""), nil).Once() - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { - return r.BlockNumber.Uint64() == 1002 - })).Return(&ffcapi.GetBlockInfoByNumberResponse{ - BlockInfo: ffcapi.BlockInfo{ - BlockNumber: fftypes.NewFFBigInt(1002), - BlockHash: "0x531e219d98d81dc9f9a14811ac537479f5d77a74bdba47629bfbebe2d7663ce7", - ParentHash: "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", - }, - }, ffcapi.ErrorReason(""), nil).Once() - - // Make the first call that caches - blockInfo, err := bcm.getBlockByNumber(1002, "0xaf47ddbd9ba81736f808045b7fccc2179bba360573b362c82544f7360db0802e") - assert.NoError(t, err) - assert.Equal(t, "0xaf47ddbd9ba81736f808045b7fccc2179bba360573b362c82544f7360db0802e", blockInfo.ParentHash) - - // Make second call that is cached as parent matches - blockInfo, err = bcm.getBlockByNumber(1002, "0xaf47ddbd9ba81736f808045b7fccc2179bba360573b362c82544f7360db0802e") - assert.NoError(t, err) - assert.Equal(t, "0xaf47ddbd9ba81736f808045b7fccc2179bba360573b362c82544f7360db0802e", blockInfo.ParentHash) - - // Make third call that does not as parent mismatched - blockInfo, err = bcm.getBlockByNumber(1002, "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542") - assert.NoError(t, err) - assert.Equal(t, "0x0e32d749a86cfaf551d528b5b121cea456f980a39e5b8136eb8e85dbc744a542", blockInfo.ParentHash) - -} - -func TestGetBlockByHashCached(t *testing.T) { - - bcm, mca := newTestBlockConfirmationManager(t, false) - - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { - return r.BlockHash == "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df" - })).Return(&ffcapi.GetBlockInfoByHashResponse{ - BlockInfo: ffcapi.BlockInfo{ - BlockNumber: fftypes.NewFFBigInt(1003), - BlockHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", - ParentHash: "0x46210d224888265c269359529618bf2f6adb2697ff52c63c10f16a2391bdd295", - }, - }, ffcapi.ErrorReason(""), nil).Once() - - blockInfo, err := bcm.getBlockByHash("0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df") - assert.NoError(t, err) - assert.Equal(t, "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", blockInfo.BlockHash) - - // Get again cached - blockInfo, err = bcm.getBlockByHash("0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df") - assert.NoError(t, err) - assert.Equal(t, "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", blockInfo.BlockHash) - + }, blocks) } func TestGetBlockNotFound(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) - mca.On("GetBlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByHashRequest) bool { + mca.On("BlockInfoByHash", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByHashRequest) bool { return r.BlockHash == "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df" })).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Once() @@ -1042,7 +917,7 @@ func TestNotificationValidation(t *testing.T) { assert.Regexp(t, "FF21016", err) err = bcm.Notify(&Notification{ - NotificationType: StopStream, + NotificationType: ListenerRemoved, }) assert.Regexp(t, "FF21016", err) @@ -1051,7 +926,7 @@ func TestNotificationValidation(t *testing.T) { NotificationType: NewTransaction, Transaction: &TransactionInfo{ TransactionHash: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - Confirmed: func(confirmations []BlockInfo) {}, + Confirmed: func(ctx context.Context, confirmations []BlockInfo) {}, }, }) assert.NoError(t, err) @@ -1062,7 +937,7 @@ func TestCheckReceiptNotFound(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) - mca.On("GetReceipt", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")) + mca.On("TransactionReceipt", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")) txHash := "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" pending := &pendingItem{ @@ -1071,17 +946,46 @@ func TestCheckReceiptNotFound(t *testing.T) { } bcm.pending[pending.getKey()] = pending bcm.staleReceipts[pendingKeyForTX(txHash)] = true - bcm.checkReceipt(pending) + blocks := bcm.newBlockState() + bcm.checkReceipt(pending, blocks) assert.False(t, bcm.staleReceipts[pendingKeyForTX(txHash)]) } +func TestCheckReceiptImmediateConfirm(t *testing.T) { + + bcm, mca := newTestBlockConfirmationManager(t, false) + bcm.requiredConfirmations = 0 + + mca.On("TransactionReceipt", mock.Anything, mock.Anything).Return(&ffcapi.TransactionReceiptResponse{ + BlockHash: fftypes.NewRandB32().String(), + BlockNumber: fftypes.NewFFBigInt(1001), + TransactionIndex: fftypes.NewFFBigInt(0), + Success: true, + }, ffcapi.ErrorReasonNotFound, nil) + + done := make(chan struct{}) + txHash := "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + pending := &pendingItem{ + pType: pendingTypeTransaction, + transactionHash: txHash, + confirmedCallback: func(ctx context.Context, confirmations []BlockInfo) { + close(done) + }, + } + bcm.pending[pending.getKey()] = pending + blocks := bcm.newBlockState() + go bcm.checkReceipt(pending, blocks) + + <-done +} + func TestCheckReceiptFail(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) - mca.On("GetReceipt", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + mca.On("TransactionReceipt", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) txHash := "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" pending := &pendingItem{ @@ -1090,7 +994,8 @@ func TestCheckReceiptFail(t *testing.T) { } bcm.pending[pending.getKey()] = pending bcm.staleReceipts[pendingKeyForTX(txHash)] = true - bcm.checkReceipt(pending) + blocks := bcm.newBlockState() + bcm.checkReceipt(pending, blocks) assert.True(t, bcm.staleReceipts[pendingKeyForTX(txHash)]) @@ -1100,12 +1005,12 @@ func TestCheckReceiptWalkFail(t *testing.T) { bcm, mca := newTestBlockConfirmationManager(t, false) - mca.On("GetReceipt", mock.Anything, mock.Anything).Return(&ffcapi.GetReceiptResponse{ + mca.On("TransactionReceipt", mock.Anything, mock.Anything).Return(&ffcapi.TransactionReceiptResponse{ BlockNumber: fftypes.NewFFBigInt(12345), BlockHash: "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df", TransactionIndex: fftypes.NewFFBigInt(10), }, ffcapi.ErrorReason(""), nil) - mca.On("GetBlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.GetBlockInfoByNumberRequest) bool { + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { return r.BlockNumber.Uint64() == 12346 })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) @@ -1116,7 +1021,8 @@ func TestCheckReceiptWalkFail(t *testing.T) { } bcm.pending[pending.getKey()] = pending bcm.staleReceipts[pendingKeyForTX(txHash)] = true - bcm.checkReceipt(pending) + blocks := bcm.newBlockState() + bcm.checkReceipt(pending, blocks) assert.True(t, bcm.staleReceipts[pendingKeyForTX(txHash)]) @@ -1129,7 +1035,7 @@ func TestStaleReceiptCheck(t *testing.T) { txHash := "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" pending := &pendingItem{ pType: pendingTypeTransaction, - lastReceiptcheck: time.Now().Add(-1 * time.Hour), + lastReceiptCheck: time.Now().Add(-1 * time.Hour), transactionHash: txHash, } bcm.pending[pending.getKey()] = pending @@ -1138,3 +1044,41 @@ func TestStaleReceiptCheck(t *testing.T) { assert.True(t, bcm.staleReceipts[pendingKeyForTX(txHash)]) } + +func TestBlockState(t *testing.T) { + + bcm, mca := newTestBlockConfirmationManager(t, false) + + block1002 := &ffcapi.BlockInfoByNumberResponse{ + BlockInfo: ffcapi.BlockInfo{ + BlockNumber: fftypes.NewFFBigInt(1002), + BlockHash: fftypes.NewRandB32().String(), + }, + } + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { + return r.BlockNumber.Uint64() == 1002 + })).Return(block1002, ffcapi.ErrorReason(""), nil).Once() + mca.On("BlockInfoByNumber", mock.Anything, mock.MatchedBy(func(r *ffcapi.BlockInfoByNumberRequest) bool { + return r.BlockNumber.Uint64() == 1003 + })).Return(nil, ffcapi.ErrorReasonNotFound, fmt.Errorf("not found")).Once() + + blocks := bcm.newBlockState() + + block, err := blocks.getByNumber(1002, "") + assert.NoError(t, err) + assert.Equal(t, block1002.BlockHash, block.BlockHash) + + block, err = blocks.getByNumber(1002, "") + assert.NoError(t, err) + assert.Equal(t, block1002.BlockHash, block.BlockHash) // cached + + block, err = blocks.getByNumber(1003, "") + assert.NoError(t, err) + assert.Nil(t, block) + + block, err = blocks.getByNumber(1004, "") + assert.NoError(t, err) + assert.Nil(t, block) // above high water mark + + mca.AssertExpectations(t) +} diff --git a/internal/events/eventstream.go b/internal/events/eventstream.go new file mode 100644 index 00000000..8b6aee7e --- /dev/null +++ b/internal/events/eventstream.go @@ -0,0 +1,878 @@ +// 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 events + +import ( + "context" + "encoding/json" + "sync" + "time" + + "github.com/hyperledger/firefly-common/pkg/config" + "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-common/pkg/retry" + "github.com/hyperledger/firefly-transaction-manager/internal/blocklistener" + "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" + "github.com/hyperledger/firefly-transaction-manager/internal/persistence" + "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/internal/ws" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" +) + +type Stream interface { + AddOrUpdateListener(ctx context.Context, id *fftypes.UUID, + updates *apitypes.Listener, reset bool) (*apitypes.Listener, error) // Add or update a listener + RemoveListener(ctx context.Context, id *fftypes.UUID) error // Stop and remove a listener + UpdateSpec(ctx context.Context, updates *apitypes.EventStream) error // Apply definition updates (if there are changes) + Spec() *apitypes.EventStream // Retrieve the merged definition to persist + Status() apitypes.EventStreamStatus // Get the current status + Start(ctx context.Context) error // Start delivery + Stop(ctx context.Context) error // Stop delivery (does not remove checkpoints) + Delete(ctx context.Context) error // Stop delivery, and clean up any checkpoint +} + +// esDefaults are the defaults for new event streams, read from the config once in InitDefaults() +var esDefaults struct { + initialized bool + batchSize int64 + batchTimeout fftypes.FFDuration + errorHandling apitypes.ErrorHandlingType + retryTimeout fftypes.FFDuration + blockedRetryDelay fftypes.FFDuration + webhookRequestTimeout fftypes.FFDuration + websocketDistributionMode apitypes.DistributionMode + topic string + retry *retry.Retry +} + +func InitDefaults() { + esDefaults.batchSize = config.GetInt64(tmconfig.EventStreamsDefaultsBatchSize) + esDefaults.batchTimeout = fftypes.FFDuration(config.GetDuration(tmconfig.EventStreamsDefaultsBatchTimeout)) + esDefaults.errorHandling = fftypes.FFEnum(config.GetString(tmconfig.EventStreamsDefaultsErrorHandling)) + esDefaults.retryTimeout = fftypes.FFDuration(config.GetDuration(tmconfig.EventStreamsDefaultsRetryTimeout)) + esDefaults.blockedRetryDelay = fftypes.FFDuration(config.GetDuration(tmconfig.EventStreamsDefaultsBlockedRetryDelay)) + esDefaults.webhookRequestTimeout = fftypes.FFDuration(config.GetDuration(tmconfig.EventStreamsDefaultsWebhookRequestTimeout)) + esDefaults.websocketDistributionMode = fftypes.FFEnum(config.GetString(tmconfig.EventStreamsDefaultsWebsocketDistributionMode)) + esDefaults.retry = &retry.Retry{ + InitialDelay: config.GetDuration(tmconfig.EventStreamsRetryInitDelay), + MaximumDelay: config.GetDuration(tmconfig.EventStreamsRetryMaxDelay), + Factor: config.GetFloat64(tmconfig.EventStreamsRetryFactor), + } +} + +type eventStreamAction func(ctx context.Context, batchNumber, attempt int, events []*apitypes.EventWithContext) error + +type eventStreamBatch struct { + number int + events []*apitypes.EventWithContext + checkpoints map[fftypes.UUID]ffcapi.EventListenerCheckpoint + timeout *time.Timer +} + +type startedStreamState struct { + ctx context.Context + cancelCtx func() + startTime *fftypes.FFTime + action eventStreamAction + eventLoopDone chan struct{} + batchLoopDone chan struct{} + blockListenerDone chan struct{} + updates chan *ffcapi.ListenerEvent + blocks chan *ffcapi.BlockHashEvent +} + +type eventStream struct { + bgCtx context.Context + spec *apitypes.EventStream + mux sync.Mutex + status apitypes.EventStreamStatus + connector ffcapi.API + persistence persistence.Persistence + confirmations confirmations.Manager + listeners map[fftypes.UUID]*listener + wsChannels ws.WebSocketChannels + retry *retry.Retry + currentState *startedStreamState + checkpointInterval time.Duration + batchChannel chan *ffcapi.ListenerEvent +} + +func NewEventStream( + bgCtx context.Context, + persistedSpec *apitypes.EventStream, + connector ffcapi.API, + persistence persistence.Persistence, + wsChannels ws.WebSocketChannels, + initialListeners []*apitypes.Listener, +) (ees Stream, err error) { + esCtx := log.WithLogField(bgCtx, "eventstream", persistedSpec.ID.String()) + es := &eventStream{ + bgCtx: esCtx, + status: apitypes.EventStreamStatusStopped, + spec: persistedSpec, + connector: connector, + persistence: persistence, + listeners: make(map[fftypes.UUID]*listener), + wsChannels: wsChannels, + retry: esDefaults.retry, + checkpointInterval: config.GetDuration(tmconfig.EventStreamsCheckpointInterval), + } + if config.GetInt(tmconfig.ConfirmationsRequired) > 0 { + es.confirmations = confirmations.NewBlockConfirmationManager(esCtx, connector, "_es_"+persistedSpec.ID.String()) + } + // The configuration we have in memory, applies all the defaults to what is passed in + // to ensure there are no nil fields on the configuration object. + if es.spec, _, err = mergeValidateEsConfig(esCtx, nil, persistedSpec); err != nil { + return nil, err + } + es.batchChannel = make(chan *ffcapi.ListenerEvent, *es.spec.BatchSize) + for _, existing := range initialListeners { + spec, err := es.verifyListenerOptions(esCtx, existing.ID, existing) + if err != nil { + return nil, err + } + es.listeners[*spec.ID] = &listener{ + es: es, + spec: spec, + } + } + log.L(esCtx).Infof("Initialized Event Stream") + return es, nil +} + +func (es *eventStream) initAction(startedState *startedStreamState) { + ctx := startedState.ctx + switch *es.spec.Type { + case apitypes.EventStreamTypeWebhook: + startedState.action = newWebhookAction(ctx, es.spec.Webhook).attemptBatch + case apitypes.EventStreamTypeWebSocket: + startedState.action = newWebSocketAction(es.wsChannels, es.spec.WebSocket, *es.spec.Name).attemptBatch + default: + // mergeValidateEsConfig always be called previous to this + panic(i18n.NewError(ctx, tmmsgs.MsgInvalidStreamType, *es.spec.Type)) + } +} + +func mergeValidateEsConfig(ctx context.Context, base *apitypes.EventStream, updates *apitypes.EventStream) (merged *apitypes.EventStream, changed bool, err error) { + + // Merged is assured to not have any unset values (default set in all cases), or any EthCompat fields + if base == nil { + base = &apitypes.EventStream{} + } + merged = &apitypes.EventStream{ + ID: base.ID, + Created: base.Created, + Updated: fftypes.Now(), + } + if merged.Created == nil || merged.ID == nil { + merged.Created = updates.Created + merged.ID = updates.ID + if merged.Created == nil { + merged.Created = merged.Updated + } + if merged.ID == nil { + return nil, false, i18n.NewError(ctx, tmmsgs.MsgMissingID) + } + } + // Name (no default - must be set) + // - Note we do not check for uniqueness of the name at this layer in the code, but we do require unique names. + // That's the responsibility of the calling code that manages the persistence of the configured streams. + changed = apitypes.CheckUpdateString(changed, &merged.Name, base.Name, updates.Name, "") + if *merged.Name == "" { + return nil, false, i18n.NewError(ctx, tmmsgs.MsgMissingName) + } + + // Suspended + changed = apitypes.CheckUpdateBool(changed, &merged.Suspended, base.Suspended, updates.Suspended, false) + + // Batch size + changed = apitypes.CheckUpdateUint64(changed, &merged.BatchSize, base.BatchSize, updates.BatchSize, esDefaults.batchSize) + + // Error handling mode + changed = apitypes.CheckUpdateEnum(changed, &merged.ErrorHandling, base.ErrorHandling, updates.ErrorHandling, esDefaults.errorHandling) + + // Batch timeout + if updates.EthCompatBatchTimeoutMS != nil { + dv := fftypes.FFDuration(*updates.EthCompatBatchTimeoutMS) * fftypes.FFDuration(time.Millisecond) + changed = apitypes.CheckUpdateDuration(changed, &merged.BatchTimeout, base.BatchTimeout, &dv, esDefaults.batchTimeout) + } else { + changed = apitypes.CheckUpdateDuration(changed, &merged.BatchTimeout, base.BatchTimeout, updates.BatchTimeout, esDefaults.batchTimeout) + } + + // Retry timeout + if updates.EthCompatRetryTimeoutSec != nil { + dv := fftypes.FFDuration(*updates.EthCompatRetryTimeoutSec) * fftypes.FFDuration(time.Second) + changed = apitypes.CheckUpdateDuration(changed, &merged.RetryTimeout, base.RetryTimeout, &dv, esDefaults.retryTimeout) + } else { + changed = apitypes.CheckUpdateDuration(changed, &merged.RetryTimeout, base.RetryTimeout, updates.RetryTimeout, esDefaults.retryTimeout) + } + + // Blocked retry delay + if updates.EthCompatBlockedRetryDelaySec != nil { + dv := fftypes.FFDuration(*updates.EthCompatBlockedRetryDelaySec) * fftypes.FFDuration(time.Second) + changed = apitypes.CheckUpdateDuration(changed, &merged.BlockedRetryDelay, base.BlockedRetryDelay, &dv, esDefaults.blockedRetryDelay) + } else { + changed = apitypes.CheckUpdateDuration(changed, &merged.BlockedRetryDelay, base.BlockedRetryDelay, updates.BlockedRetryDelay, esDefaults.blockedRetryDelay) + } + + // Type + changed = apitypes.CheckUpdateEnum(changed, &merged.Type, base.Type, updates.Type, apitypes.EventStreamTypeWebSocket) + switch *merged.Type { + case apitypes.EventStreamTypeWebSocket: + if merged.WebSocket, changed, err = mergeValidateWsConfig(ctx, changed, base.WebSocket, updates.WebSocket); err != nil { + return nil, false, err + } + case apitypes.EventStreamTypeWebhook: + if merged.Webhook, changed, err = mergeValidateWhConfig(ctx, changed, base.Webhook, updates.Webhook); err != nil { + return nil, false, err + } + default: + return nil, false, i18n.NewError(ctx, tmmsgs.MsgInvalidStreamType, *merged.Type) + } + + return merged, changed, nil +} + +func (es *eventStream) Spec() *apitypes.EventStream { + return es.spec +} + +func (es *eventStream) UpdateSpec(ctx context.Context, updates *apitypes.EventStream) error { + merged, changed, err := mergeValidateEsConfig(ctx, es.spec, updates) + if err != nil { + return err + } + + es.mux.Lock() + es.spec = merged + isStarted := es.status == apitypes.EventStreamStatusStarted + es.mux.Unlock() + + if changed && isStarted { + if err := es.Stop(ctx); err != nil { + return i18n.NewError(ctx, tmmsgs.MsgStopFailedUpdatingESConfig, err) + } + if err := es.Start(ctx); err != nil { + return i18n.NewError(ctx, tmmsgs.MsgStartFailedUpdatingESConfig, err) + } + } + + return nil +} + +func (es *eventStream) mergeListenerOptions(id *fftypes.UUID, updates *apitypes.Listener) *apitypes.Listener { + + es.mux.Lock() + l := es.listeners[*id] + var base *apitypes.Listener + now := fftypes.Now() + if l != nil { + base = l.spec + } else { + latest := ffcapi.FromBlockLatest + base = &apitypes.Listener{ + ID: id, + Created: now, + FromBlock: &latest, + StreamID: es.spec.ID, + } + } + es.mux.Unlock() + + merged := *base + + if updates.Name != nil { + merged.Name = updates.Name + } + + if updates.FromBlock != nil { + merged.FromBlock = updates.FromBlock + } + + if updates.Options != nil { + merged.Options = updates.Options + } else { + merged.Options = base.Options + } + + if updates.Filters != nil { + merged.Filters = updates.Filters + } else { + // Allow a single "event" object to be specified instead of a filter, with an optional "address". + // This is migrated to the new syntax: `"filters":[{"address":"0x1235","event":{...}}]` + // (only expected to work for the eth connector that supports address/event) + if updates.EthCompatEvent != nil { + migrationFilter := fftypes.JSONObject{ + "event": updates.EthCompatEvent, + } + if updates.EthCompatAddress != nil { + migrationFilter["address"] = *updates.EthCompatAddress + } + merged.Filters = []fftypes.JSONAny{fftypes.JSONAny(migrationFilter.String())} + } else { + merged.Filters = base.Filters + } + } + + return &merged + +} + +func (es *eventStream) verifyListenerOptions(ctx context.Context, id *fftypes.UUID, updatesOrNew *apitypes.Listener) (*apitypes.Listener, error) { + // Merge the supplied options with defaults and any existing config. + spec := es.mergeListenerOptions(id, updatesOrNew) + + // The connector needs to validate the options, building a set of options that are assured to be non-nil + res, _, err := es.connector.EventListenerVerifyOptions(ctx, &ffcapi.EventListenerVerifyOptionsRequest{ + EventListenerOptions: listenerSpecToOptions(spec), + }) + if err != nil { + return nil, i18n.NewError(ctx, tmmsgs.MsgBadListenerOptions, err) + } + + // We update the spec object in-place for the signature and resolved options + spec.Signature = res.ResolvedSignature + spec.Options = &res.ResolvedOptions + if spec.Name == nil || *spec.Name == "" { + sig := spec.Signature + spec.Name = &sig + } + log.L(ctx).Infof("Listener %s signature: %s", spec.ID, spec.Signature) + return spec, nil +} + +func (es *eventStream) AddOrUpdateListener(ctx context.Context, id *fftypes.UUID, updates *apitypes.Listener, reset bool) (merged *apitypes.Listener, err error) { + log.L(ctx).Infof("Adding/updating listener %s", id) + + // Ask the connector to verify the options, and apply defaults + spec, err := es.verifyListenerOptions(ctx, id, updates) + if err != nil { + return nil, err + } + + // Do the locked part - which checks if this is a new listener, or just an update to the options. + new, l, startedState, err := es.lockedListenerUpdate(ctx, spec, reset) + if err != nil { + return nil, err + } + if reset { + // Only safe to do the reset with the event stream stopped + if startedState != nil { + if err := es.Stop(ctx); err != nil { + return nil, err + } + } + // Clear out the checkpoint for this listener + if err := es.resetListenerCheckpoint(ctx, l); err != nil { + return nil, err + } + // Restart if we were started + if startedState != nil { + if err := es.Start(ctx); err != nil { + return nil, err + } + } + } else if new && startedState != nil { + // Start the new listener - no checkpoint needed here + return spec, l.start(startedState, nil) + } + return spec, nil +} + +func (es *eventStream) resetListenerCheckpoint(ctx context.Context, l *listener) error { + cp, err := es.persistence.GetCheckpoint(ctx, es.spec.ID) + if err != nil || cp == nil { + return err + } + delete(cp.Listeners, *l.spec.ID) + return es.persistence.WriteCheckpoint(ctx, cp) +} + +func (es *eventStream) lockedListenerUpdate(ctx context.Context, spec *apitypes.Listener, reset bool) (bool, *listener, *startedStreamState, error) { + es.mux.Lock() + defer es.mux.Unlock() + + l, exists := es.listeners[*spec.ID] + switch { + case exists: + if spec.Signature != l.spec.Signature { + // We do not allow the filters to be updated, because that would lead to a confusing situation + // where the previously emitted events are a subset/mismatch to the filters configured now. + return false, nil, nil, i18n.NewError(ctx, tmmsgs.MsgFilterUpdateNotAllowed, l.spec.Signature, spec.Signature) + } + l.spec = spec + case reset: + return false, nil, nil, i18n.NewError(ctx, tmmsgs.MsgResetStreamNotFound, spec.ID, es.spec.ID) + default: + l = &listener{ + es: es, + spec: spec, + } + es.listeners[*spec.ID] = l + } + // Take a copy of the current started status, before unlocking + return !exists, l, es.currentState, nil +} + +func (es *eventStream) RemoveListener(ctx context.Context, id *fftypes.UUID) (err error) { + es.mux.Lock() + l, exists := es.listeners[*id] + if !exists { + log.L(ctx).Warnf("Removing listener not in map: %s", id) + es.mux.Unlock() + return nil + } + startedState := es.currentState + delete(es.listeners, *id) + es.mux.Unlock() + + log.L(ctx).Warnf("Removing listener: %s", id) + if startedState != nil { + err = l.stop(startedState) + } + return err +} + +func (es *eventStream) String() string { + return es.spec.ID.String() +} + +// checkSetStatus - caller must have locked the mux when calling this +func (es *eventStream) checkSetStatus(ctx context.Context, requiredState apitypes.EventStreamStatus, newState ...apitypes.EventStreamStatus) error { + if es.status != requiredState { + return i18n.NewError(ctx, tmmsgs.MsgStreamStateError, es.status) + } + if len(newState) == 1 { + es.status = newState[0] + } + return nil +} + +func (es *eventStream) Start(ctx context.Context) error { + es.mux.Lock() + defer es.mux.Unlock() + if err := es.checkSetStatus(ctx, apitypes.EventStreamStatusStopped, apitypes.EventStreamStatusStarted); err != nil { + return err + } + log.L(ctx).Infof("Starting event stream %s", es) + + startedState := &startedStreamState{ + startTime: fftypes.Now(), + eventLoopDone: make(chan struct{}), + batchLoopDone: make(chan struct{}), + updates: make(chan *ffcapi.ListenerEvent, int(*es.spec.BatchSize)), + } + startedState.ctx, startedState.cancelCtx = context.WithCancel(es.bgCtx) + es.currentState = startedState + es.initAction(startedState) + + cp, err := es.persistence.GetCheckpoint(ctx, es.spec.ID) + if err != nil { + return err + } + + initialListeners := make([]*ffcapi.EventListenerAddRequest, 0) + for _, l := range es.listeners { + initialListeners = append(initialListeners, l.buildAddRequest(ctx, cp)) + } + startedState.blocks, startedState.blockListenerDone = blocklistener.BufferChannel(startedState.ctx, es.confirmations) + _, _, err = es.connector.EventStreamStart(startedState.ctx, &ffcapi.EventStreamStartRequest{ + ID: es.spec.ID, + EventStream: startedState.updates, + StreamContext: startedState.ctx, + BlockListener: startedState.blocks, + InitialListeners: initialListeners, + }) + if err != nil { + _ = es.checkSetStatus(ctx, apitypes.EventStreamStatusStarted, apitypes.EventStreamStatusStopped) + return err + } + + // Kick off the loops + go es.eventLoop(startedState) + go es.batchLoop(startedState) + + // Start the confirmations manager + if es.confirmations != nil { + es.confirmations.Start() + } + + return err +} + +func (es *eventStream) requestStop(ctx context.Context) (*startedStreamState, error) { + es.mux.Lock() + defer es.mux.Unlock() + startedState := es.currentState + if es.status == apitypes.EventStreamStatusStopping { + // Already stopping, just return + return startedState, nil + } + if err := es.checkSetStatus(ctx, apitypes.EventStreamStatusStarted, apitypes.EventStreamStatusStopping); err != nil { + return nil, err + } + log.L(ctx).Infof("Stopping event stream %s", es) + + // Cancel the context, stop stop the event loop, and shut down the action (WebSockets in particular) + startedState.cancelCtx() + + return startedState, nil +} + +func (es *eventStream) Status() apitypes.EventStreamStatus { + es.mux.Lock() + defer es.mux.Unlock() + return es.status +} + +func (es *eventStream) Stop(ctx context.Context) error { + + // Request the stop - this phase is locked, and gives us a safe copy of the listeners array to use outside the lock + startedState, err := es.requestStop(ctx) + if err != nil || startedState == nil { + return err + } + + // Inform the connector explicitly of the stream stop (it should be shutting down all it's listeners anyway + // due to the cancelled context) + if _, _, err = es.connector.EventStreamStopped(ctx, &ffcapi.EventStreamStoppedRequest{ + ID: es.spec.ID, + }); err != nil { + log.L(ctx).Errorf("Connector returned error when notified of stopped stream: %s", err) + return err + } + + // Stop the confirmations manager + es.confirmations.Stop() + + // Wait for our event loop to stop + <-startedState.eventLoopDone + + // Wait for our batch loop to stop + <-startedState.batchLoopDone + + // Wait for our block listener to stop + <-startedState.blockListenerDone + + // Transition to stopped (takes the lock again) + es.mux.Lock() + es.currentState = nil + defer es.mux.Unlock() + return es.checkSetStatus(ctx, apitypes.EventStreamStatusStopping, apitypes.EventStreamStatusStopped) +} + +func (es *eventStream) Delete(ctx context.Context) error { + // Check we are stopped + if err := es.checkSetStatus(ctx, apitypes.EventStreamStatusStopped); err != nil { + if err := es.Stop(ctx); err != nil { + return err + } + } + log.L(ctx).Infof("Deleting event stream %s", es) + + // Hold the lock for the whole of delete, rather than transitioning into a deleting status. + // If we error out, that way the caller can retry. + es.mux.Lock() + defer es.mux.Unlock() + if err := es.persistence.DeleteCheckpoint(ctx, es.spec.ID); err != nil { + return err + } + return es.checkSetStatus(ctx, apitypes.EventStreamStatusStopped, apitypes.EventStreamStatusDeleted) +} + +func (es *eventStream) processNewEvent(ctx context.Context, fev *ffcapi.ListenerEvent) { + event := fev.Event + if event == nil || event.ID.ListenerID == nil || fev.Checkpoint == nil { + log.L(ctx).Warnf("Invalid event from connector: %+v", fev) + return + } + es.mux.Lock() + l := es.listeners[*fev.Event.ID.ListenerID] + es.mux.Unlock() + if l != nil { + log.L(ctx).Debugf("%s event detected: %s", l.spec.ID, event) + if es.confirmations == nil { + // Updates that are just a checkpoint update, go straight to the batch loop. + // Or if the confirmation manager is disabled. + // - Note this will block the eventLoop when the event stream is blocked + es.batchChannel <- fev + } else { + // Notify will block, when the confirmation manager is blocked, which per below + // will flow back from when the event stream is blocked + err := es.confirmations.Notify(&confirmations.Notification{ + NotificationType: confirmations.NewEventLog, + Event: &confirmations.EventInfo{ + ID: &event.ID, + Confirmed: func(ctx context.Context, confirmations []confirmations.BlockInfo) { + // Push it to the batch when confirmed + // - Note this will block the confirmation manager when the event stream is blocked + es.batchChannel <- fev + }, + }, + }) + if err != nil { + log.L(ctx).Warnf("Failed to notify confirmation manager for event '%s': %s", event, err) + } + } + } +} + +func (es *eventStream) processRemovedEvent(ctx context.Context, fev *ffcapi.ListenerEvent) { + if fev.Event != nil && fev.Event.ID.ListenerID != nil && es.confirmations != nil { + err := es.confirmations.Notify(&confirmations.Notification{ + NotificationType: confirmations.RemovedEventLog, + Event: &confirmations.EventInfo{ + ID: &fev.Event.ID, + }, + }) + if err != nil { + log.L(ctx).Warnf("Failed to notify confirmation manager for removed event '%s': %s", fev.Event, err) + } + } +} + +func (es *eventStream) eventLoop(startedState *startedStreamState) { + defer close(startedState.eventLoopDone) + ctx := startedState.ctx + + for { + select { + case lev := <-startedState.updates: + if lev.Removed { + es.processRemovedEvent(ctx, lev) + } else { + es.processNewEvent(ctx, lev) + } + case <-ctx.Done(): + log.L(ctx).Debugf("Event loop exiting") + return + } + } +} + +// batchLoop receives confirmed events from the confirmation manager, +// batches them together, and drives the actions. +func (es *eventStream) batchLoop(startedState *startedStreamState) { + defer close(startedState.batchLoopDone) + ctx := startedState.ctx + maxSize := int(*es.spec.BatchSize) + batchNumber := 0 + + var batch *eventStreamBatch + var checkpointTimer = time.NewTimer(es.checkpointInterval) + for { + var timeoutChannel <-chan time.Time + if batch != nil { + // Once a batch has started, the batch timeout always takes precedence (even if it slows down the checkpoint slightly) + timeoutChannel = batch.timeout.C + } else { + // If we don't have a batch in-flight, then the (longer) checkpoint timer is used + timeoutChannel = checkpointTimer.C + } + timedOut := false + select { + case fev := <-es.batchChannel: + if fev.Event != nil { + es.mux.Lock() + l := es.listeners[*fev.Event.ID.ListenerID] + es.mux.Unlock() + if l != nil { + currentCheckpoint := l.checkpoint + if currentCheckpoint != nil && !currentCheckpoint.LessThan(fev.Checkpoint) { + // This event is behind the current checkpoint - this is a re-detection. + // We're perfectly happy to accept re-detections from the connector, as it can be + // very efficient to batch operations between listeners that cause re-detections. + // However, we need to protect the application from receiving the re-detections. + // This loop is the right place for this check, as we are responsible for writing the checkpoints and + // delivering to the application. So we are the one source of truth. + log.L(es.bgCtx).Debugf("%s '%s' event re-detected behind checkpoint: %s", l.spec.ID, l.spec.Signature, fev.Event) + continue + } + + if batch == nil { + batchNumber++ + batch = &eventStreamBatch{ + number: batchNumber, + timeout: time.NewTimer(time.Duration(*es.spec.BatchTimeout)), + checkpoints: make(map[fftypes.UUID]ffcapi.EventListenerCheckpoint), + } + } + if fev.Checkpoint != nil { + batch.checkpoints[*fev.Event.ID.ListenerID] = fev.Checkpoint + } + + log.L(es.bgCtx).Debugf("%s '%s' event confirmed: %s", l.spec.ID, l.spec.Signature, fev.Event) + batch.events = append(batch.events, &apitypes.EventWithContext{ + StandardContext: apitypes.EventContext{ + StreamID: es.spec.ID, + EthCompatSubID: l.spec.ID, + ListenerName: *l.spec.Name, + }, + Event: *fev.Event, + }) + } + } + case <-timeoutChannel: + timedOut = true + if batch == nil { + checkpointTimer = time.NewTimer(es.checkpointInterval) + } + case <-ctx.Done(): + // The started context exited, we are stopping + if checkpointTimer != nil { + checkpointTimer.Stop() + } + log.L(ctx).Debugf("Batch loop exiting") + return + } + + if timedOut || len(batch.events) >= maxSize { + var err error + if batch != nil { + batch.timeout.Stop() + err = es.performActionsWithRetry(startedState, batch) + } + if err == nil { + checkpointTimer = time.NewTimer(es.checkpointInterval) // Reset the checkpoint timeout + err = es.writeCheckpoint(startedState, batch) + } + if err != nil { + log.L(ctx).Debugf("Batch loop exiting: %s", err) + return + } + batch = nil + } + } +} + +// performActionWithRetry performs an action, with exponential back-off retry up +// to a given threshold. Only returns error in the case that the context is closed. +func (es *eventStream) performActionsWithRetry(startedState *startedStreamState, batch *eventStreamBatch) (err error) { + // We may not have anything to do, if we only had checkpoints in the batch timeout cycle + if len(batch.events) == 0 { + return nil + } + + ctx := startedState.ctx + startTime := time.Now() + for { + // Short exponential back-off retry + err := es.retry.Do(ctx, "action", func(attempt int) (retry bool, err error) { + err = startedState.action(ctx, batch.number, attempt, batch.events) + if err != nil { + log.L(ctx).Errorf("Batch %d attempt %d failed. err=%s", + batch.number, attempt, err) + return time.Since(startTime) < time.Duration(*es.spec.RetryTimeout), err + } + return false, nil + }) + if err == nil { + return nil + } + // We're in blocked retry delay + log.L(ctx).Errorf("Batch failed short retry after %.2fs secs. ErrorHandling=%s BlockedRetryDelay=%.2fs ", + time.Since(startTime).Seconds(), *es.spec.ErrorHandling, time.Duration(*es.spec.BlockedRetryDelay).Seconds()) + if *es.spec.ErrorHandling == apitypes.ErrorHandlingTypeSkip { + // Swallow the error now we have logged it + return nil + } + select { + case <-time.After(time.Duration(*es.spec.BlockedRetryDelay)): + case <-ctx.Done(): + // Only way we exit with error, is if the context is cancelled + return i18n.NewError(ctx, i18n.MsgContextCanceled) + } + } +} + +func (es *eventStream) checkUpdateHWMCheckpoint(ctx context.Context, l *listener) ffcapi.EventListenerCheckpoint { + + checkpoint := l.checkpoint + + inFlight := false + if es.confirmations != nil { + inFlight = es.confirmations.CheckInFlight(l.spec.ID) + } + + // If there's in-flight messages in the confirmation manager, we wait for these to be confirmed or purged before + // writing a checkpoint. + if inFlight { + log.L(ctx).Infof("Stale checkpoint for listener '%s' will not be updated as events are in-flight", l.spec.ID) + } else { + res, _, err := es.connector.EventListenerHWM(ctx, &ffcapi.EventListenerHWMRequest{ + StreamID: es.spec.ID, + ListenerID: l.spec.ID, + }) + if err != nil { + log.L(ctx).Errorf("Failed to obtain high watermark checkpoint for listener '%s': %s", l.spec.ID, err) + return checkpoint + } + es.mux.Lock() + if l.checkpoint == checkpoint /* double check it hasn't changed */ { + checkpoint = res.Checkpoint + l.checkpoint = checkpoint + l.lastCheckpoint = fftypes.Now() + } + es.mux.Unlock() + } + + return checkpoint + +} + +func (es *eventStream) writeCheckpoint(startedState *startedStreamState, batch *eventStreamBatch) (err error) { + // We update the checkpoints (under lock) for all listeners with events in this batch. + // The last event for any listener in the batch wins. + es.mux.Lock() + cp := &apitypes.EventStreamCheckpoint{ + StreamID: es.spec.ID, + Time: fftypes.Now(), + Listeners: make(map[fftypes.UUID]json.RawMessage), + } + if batch != nil { + for lID, lCP := range batch.checkpoints { + if l, ok := es.listeners[lID]; ok { + l.checkpoint = lCP + l.lastCheckpoint = fftypes.Now() + log.L(es.bgCtx).Tracef("%s (%s) checkpoint: %+v", l.spec.Signature, l.spec.ID, lCP) + } + } + } + staleCheckpoints := make([]*listener, 0) + for lID, l := range es.listeners { + cp.Listeners[lID], _ = json.Marshal(l.checkpoint) + if l.checkpoint == nil || l.lastCheckpoint == nil || time.Since(*l.lastCheckpoint.Time()) > es.checkpointInterval { + staleCheckpoints = append(staleCheckpoints, l) + } + } + es.mux.Unlock() + + // Ask the connector for any updated high watermark checkpoints - checking we don't have any in-flight confirmations + for _, l := range staleCheckpoints { + cpb, _ := json.Marshal(es.checkUpdateHWMCheckpoint(startedState.ctx, l)) + cp.Listeners[*l.spec.ID] = cpb + } + + // We only return if the context is cancelled, or the checkpoint succeeds + return es.retry.Do(startedState.ctx, "checkpoint", func(attempt int) (retry bool, err error) { + return true, es.persistence.WriteCheckpoint(startedState.ctx, cp) + }) +} diff --git a/internal/events/eventstream_test.go b/internal/events/eventstream_test.go new file mode 100644 index 00000000..d1671c23 --- /dev/null +++ b/internal/events/eventstream_test.go @@ -0,0 +1,1777 @@ +// 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 events + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" + "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" + "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/mocks/persistencemocks" + "github.com/hyperledger/firefly-transaction-manager/mocks/wsmocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func strPtr(s string) *string { return &s } + +type testInfo struct { + BlockNumber string `json:"blockNumber"` + TransactionIndex string `json:"transactionIndex"` + LogIndex string `json:"logIndex"` +} + +type utCheckpointType struct { + SomeSequenceNumber int64 `json:"someSequenceNumber"` +} + +func (cp *utCheckpointType) LessThan(b ffcapi.EventListenerCheckpoint) bool { + return cp.SomeSequenceNumber < b.(*utCheckpointType).SomeSequenceNumber +} + +func testESConf(t *testing.T, j string) (spec *apitypes.EventStream) { + err := json.Unmarshal([]byte(j), &spec) + assert.NoError(t, err) + spec.ID = apitypes.NewULID() + return spec +} + +func newTestEventStream(t *testing.T, conf string) (es *eventStream) { + es, err := newTestEventStreamWithListener(t, &ffcapimocks.API{}, conf) + assert.NoError(t, err) + return es +} + +func newTestEventStreamWithListener(t *testing.T, mfc *ffcapimocks.API, conf string, listeners ...*apitypes.Listener) (es *eventStream, err error) { + tmconfig.Reset() + config.Set(tmconfig.EventStreamsDefaultsBatchTimeout, "1us") + InitDefaults() + ees, err := NewEventStream(context.Background(), testESConf(t, conf), + mfc, + &persistencemocks.Persistence{}, + &wsmocks.WebSocketChannels{}, + listeners, + ) + mfc.On("EventStreamNewCheckpointStruct").Return(&utCheckpointType{}).Maybe() + if err != nil { + return nil, err + } + es = ees.(*eventStream) + mcm := &confirmationsmocks.Manager{} + es.confirmations = mcm + mcm.On("Start").Return(nil).Maybe() + mcm.On("Stop").Return(nil).Maybe() + mcm.On("Notify", mock.Anything).Run(func(args mock.Arguments) { + n := args[0].(*confirmations.Notification) + if n.Event != nil { + go n.Event.Confirmed(context.Background(), []confirmations.BlockInfo{}) + } + }).Return(nil).Maybe() + return es, err +} + +func mockWSChannels(wsc *wsmocks.WebSocketChannels) (chan interface{}, chan interface{}, chan error) { + senderChannel := make(chan interface{}, 1) + broadcastChannel := make(chan interface{}, 1) + receiverChannel := make(chan error, 1) + wsc.On("GetChannels", "ut_stream").Return((chan<- interface{})(senderChannel), (chan<- interface{})(broadcastChannel), (<-chan error)(receiverChannel)) + return senderChannel, broadcastChannel, receiverChannel +} + +func TestNewTestEventStreamMissingID(t *testing.T) { + tmconfig.Reset() + InitDefaults() + _, err := NewEventStream(context.Background(), &apitypes.EventStream{}, + &ffcapimocks.API{}, + &persistencemocks.Persistence{}, + &wsmocks.WebSocketChannels{}, + []*apitypes.Listener{}, + ) + assert.Regexp(t, "FF21048", err) +} + +func TestNewTestEventStreamBadConfig(t *testing.T) { + tmconfig.Reset() + InitDefaults() + _, err := NewEventStream(context.Background(), testESConf(t, `{}`), + &ffcapimocks.API{}, + &persistencemocks.Persistence{}, + &wsmocks.WebSocketChannels{}, + []*apitypes.Listener{}, + ) + assert.Regexp(t, "FF21028", err) +} + +func TestConfigNewDefaultsUpdate(t *testing.T) { + tmconfig.Reset() + InitDefaults() + + es := testESConf(t, `{ + "name": "test1" + }`) + es, changed, err := mergeValidateEsConfig(context.Background(), nil, es) + assert.NoError(t, err) + assert.True(t, changed) + + b, err := json.Marshal(&es) + assert.NoError(t, err) + assert.JSONEq(t, `{ + "id":"`+es.ID.String()+`", + "created":"`+es.Created.String()+`", + "updated":"`+es.Created.String()+`", + "batchSize": 50, + "batchTimeout": "5s", + "blockedRetryDelay": "30s", + "errorHandling":"block", + "name":"test1", + "retryTimeout":"30s", + "suspended":false, + "type":"websocket", + "websocket": { + "distributionMode":"load_balance", + "topic":"" + } + }`, string(b)) + + es, changed, err = mergeValidateEsConfig(context.Background(), es, es) + assert.NoError(t, err) + assert.False(t, changed) + + es2, changed, err := mergeValidateEsConfig(context.Background(), es, testESConf(t, `{ + "id": "4023945d-ea5d-43aa-ab4f-f39f8c055c7e",`+ /* ignored */ ` + "batchSize": 111, + "batchTimeoutMS": 222, + "blockedRetryDelaySec": 333, + "errorHandling": "skip", + "name": "test2", + "retryTimeoutSec": 444, + "suspended": true, + "type": "webhook", + "webhook": { + "url": "http://test.example.com" + } + }`)) + + assert.NoError(t, err) + assert.True(t, changed) + + b, err = json.Marshal(&es2) + assert.NoError(t, err) + assert.JSONEq(t, `{ + "id":"`+es.ID.String()+`", + "created":"`+es.Created.String()+`", + "updated":"`+es2.Updated.String()+`", + "batchSize": 111, + "batchTimeout": "222ms", + "blockedRetryDelay": "5m33s", + "errorHandling":"skip", + "name":"test2", + "retryTimeout":"7m24s", + "suspended":true, + "type":"webhook", + "webhook": { + "tlsSkipHostVerify": false, + "requestTimeout": "30s", + "url": "http://test.example.com" + } + }`, string(b)) + +} + +func TestConfigNewMissingWebhookConf(t *testing.T) { + tmconfig.Reset() + InitDefaults() + + _, _, err := mergeValidateEsConfig(context.Background(), nil, testESConf(t, `{ + "name": "test", + "type": "webhook", + "websocket": {} + }`)) + assert.Regexp(t, "FF21030", err) + +} + +func TestConfigBadWebSocketDistModeConf(t *testing.T) { + tmconfig.Reset() + InitDefaults() + + _, _, err := mergeValidateEsConfig(context.Background(), nil, testESConf(t, `{ + "name": "test", + "type": "websocket", + "websocket": { + "distributionMode":"wrong" + } + }`)) + assert.Regexp(t, "FF21034", err) + +} + +func TestConfigWebSocketBroadcast(t *testing.T) { + tmconfig.Reset() + InitDefaults() + + es, _, err := mergeValidateEsConfig(context.Background(), nil, testESConf(t, `{ + "name": "test", + "type": "websocket", + "websocket": { + "distributionMode":"broadcast" + } + }`)) + assert.NoError(t, err) + assert.Equal(t, apitypes.DistributionModeBroadcast, *es.WebSocket.DistributionMode) + +} + +func TestConfigNewBadType(t *testing.T) { + tmconfig.Reset() + InitDefaults() + + _, _, err := mergeValidateEsConfig(context.Background(), nil, testESConf(t, `{ + "name": "test", + "type": "wrong" + }`)) + assert.Regexp(t, "FF21029", err) + +} + +func TestConfigNewWebhookRetryMigration(t *testing.T) { + tmconfig.Reset() + InitDefaults() + + es, changed, err := mergeValidateEsConfig(context.Background(), nil, testESConf(t, `{ + "name": "test", + "type": "webhook", + "webhook": { + "urL": "http://www.example.com", + "requestTimeoutSec": 5 + } + }`)) + assert.NoError(t, err) + assert.True(t, changed) + + assert.Equal(t, fftypes.FFDuration(5*time.Second), *es.Webhook.RequestTimeout) + +} + +func TestInitActionBadAction(t *testing.T) { + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + badType := apitypes.EventStreamType("wrong") + es.spec.Type = &badType + assert.Panics(t, func() { + es.initAction(&startedStreamState{ + ctx: context.Background(), + }) + }) +} + +func TestWebSocketEventStreamsE2EMigrationThenStart(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + addr := "0x12345" + l := &apitypes.Listener{ + ID: apitypes.NewULID(), + Name: strPtr("ut_listener"), + EthCompatAddress: &addr, + EthCompatEvent: fftypes.JSONAnyPtr(`{"event":"definition"}`), + Options: fftypes.JSONAnyPtr(`{"option1":"value1"}`), + FromBlock: strPtr("12345"), + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.MatchedBy(func(req *ffcapi.EventListenerVerifyOptionsRequest) bool { + return req.FromBlock == "12345" && req.Options.JSONObject().GetString("option1") == "value1" + })).Return(&ffcapi.EventListenerVerifyOptionsResponse{ + ResolvedSignature: "EventSig(uint256)", + ResolvedOptions: *fftypes.JSONAnyPtr(`{"option1":"value1","option2":"value2"}`), + }, ffcapi.ErrorReason(""), nil) + + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + r := args[1].(*ffcapi.EventStreamStartRequest) + started <- r + assert.Len(t, r.InitialListeners, 1) + assert.JSONEq(t, `{ + "event": {"event":"definition"}, + "address": "0x12345" + }`, r.InitialListeners[0].Filters[0].String()) + assert.JSONEq(t, `{ + "option1":"value1", + "option2":"value2" + }`, r.InitialListeners[0].Options.String()) + assert.Equal(t, int64(12000), r.InitialListeners[0].Checkpoint.(*utCheckpointType).SomeSequenceNumber) + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(&apitypes.EventStreamCheckpoint{ + StreamID: es.spec.ID, + Time: fftypes.Now(), + Listeners: map[fftypes.UUID]json.RawMessage{ + *l.ID: []byte(`{"someSequenceNumber":12000}`), + }, + }, nil) // existing checkpoint + msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { + return cp.StreamID.Equals(es.spec.ID) && string(cp.Listeners[*l.ID]) == `{"someSequenceNumber":12345}` + })).Return(nil) + + senderChannel, _, receiverChannel := mockWSChannels(es.wsChannels.(*wsmocks.WebSocketChannels)) + + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + assert.NoError(t, err) + + err = es.Start(es.bgCtx) + assert.NoError(t, err) + + assert.Equal(t, apitypes.EventStreamStatusStarted, es.Status()) + + err = es.Start(es.bgCtx) // double start is error + assert.Regexp(t, "FF21027", err) + + r := <-started + + r.EventStream <- &ffcapi.ListenerEvent{ + Checkpoint: &utCheckpointType{SomeSequenceNumber: 12345}, + Event: &ffcapi.Event{ + ID: ffcapi.EventID{ + ListenerID: l.ID, + BlockNumber: 42, + TransactionIndex: 13, + LogIndex: 1, + }, + Data: fftypes.JSONAnyPtr(`{"k1":"v1"}`), + Info: &testInfo{ + BlockNumber: "42", + TransactionIndex: "13", + LogIndex: "1", + }, + }, + } + + batch1 := (<-senderChannel).([]*apitypes.EventWithContext) + assert.Len(t, batch1, 1) + assert.Equal(t, "v1", batch1[0].Data.JSONObject().GetString("k1")) + + receiverChannel <- nil // ack + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + + <-r.StreamContext.Done() + + mfc.AssertExpectations(t) +} + +func TestStartEventStreamCheckpointReadFail(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, fmt.Errorf("pop")) + + err := es.Start(es.bgCtx) + assert.Regexp(t, "pop", err) +} + +func TestStartEventStreamCheckpointInvalid(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l := &apitypes.Listener{ + ID: apitypes.NewULID(), + Name: strPtr("ut_listener"), + Options: fftypes.JSONAnyPtr(`{"option1":"value1"}`), + FromBlock: strPtr("12345"), + } + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(&apitypes.EventStreamCheckpoint{ + StreamID: es.spec.ID, + Time: fftypes.Now(), + Listeners: map[fftypes.UUID]json.RawMessage{ + *l.ID: []byte(`{"bad": JSON!`), + }, + }, nil) + + mfc := es.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{ + ResolvedSignature: "EventSig(uint256)", + ResolvedOptions: *fftypes.JSONAnyPtr(`{"option1":"value1","option2":"value2"}`), + }, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(req *ffcapi.EventStreamStartRequest) bool { + return req.InitialListeners[0].Checkpoint == nil + })).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) + + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + + err = es.Start(es.bgCtx) + assert.NoError(t, err) + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + + mfc.AssertExpectations(t) +} + +func TestWebhookEventStreamsE2EAddAfterStart(t *testing.T) { + + receivedWebhook := make(chan []*apitypes.EventWithContext, 1) + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/test/path", r.URL.Path) + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "application/json", r.Header.Get("content-type")) + var events []*apitypes.EventWithContext + err := json.NewDecoder(r.Body).Decode(&events) + assert.NoError(t, err) + receivedWebhook <- events + })) + defer s.Close() + + es := newTestEventStream(t, `{ + "name": "ut_stream", + "type": "webhook", + "webhook": { + "url": "`+fmt.Sprintf("http://%s/test/path", s.Listener.Addr())+`" + } + }`) + + l := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Filters: []fftypes.JSONAny{ + `{"event":"definition1"}`, + `{"event":"definition2"}`, + }, + Options: fftypes.JSONAnyPtr(`{"option1":"value1"}`), + FromBlock: strPtr("12345"), + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.MatchedBy(func(req *ffcapi.EventListenerVerifyOptionsRequest) bool { + return req.FromBlock == "12345" && req.Options.JSONObject().GetString("option1") == "value1" + })).Return(&ffcapi.EventListenerVerifyOptionsResponse{ + ResolvedSignature: "EventSig(uint256)", + ResolvedOptions: *fftypes.JSONAnyPtr(`{"option1":"value1","option2":"value2"}`), + }, ffcapi.ErrorReason(""), nil) + + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + r := args[1].(*ffcapi.EventStreamStartRequest) + started <- r + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + + mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { + return r.ListenerID.Equals(l.ID) + })).Run(func(args mock.Arguments) { + r := args[1].(*ffcapi.EventListenerAddRequest) + assert.JSONEq(t, `{"event":"definition1"}`, r.Filters[0].String()) + assert.JSONEq(t, `{"event":"definition2"}`, r.Filters[1].String()) + assert.JSONEq(t, `{ + "option1":"value1", + "option2":"value2" + }`, r.Options.String()) + }).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { + return cp.StreamID.Equals(es.spec.ID) && bytes.Equal(cp.Listeners[*l.ID], json.RawMessage(`{"someSequenceNumber":12345}`)) + })).Return(nil) + + err := es.Start(es.bgCtx) + assert.NoError(t, err) + + l, err = es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + assert.NoError(t, err) + assert.Equal(t, "EventSig(uint256)", *l.Name) // Defaulted + + r := <-started + + r.EventStream <- &ffcapi.ListenerEvent{ + Checkpoint: &utCheckpointType{SomeSequenceNumber: 12345}, + Event: &ffcapi.Event{ + ID: ffcapi.EventID{ + ListenerID: l.ID, + BlockNumber: 42, + TransactionIndex: 13, + LogIndex: 1, + }, + Data: fftypes.JSONAnyPtr(`{"k1":"v1"}`), + Info: &testInfo{ + BlockNumber: "42", + TransactionIndex: "13", + LogIndex: "1", + }, + }, + } + + batch1 := <-receivedWebhook + assert.Len(t, batch1, 1) + assert.Equal(t, "v1", batch1[0].Data.JSONObject().GetString("k1")) + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + + <-r.StreamContext.Done() + + mfc.AssertExpectations(t) +} + +func TestConnectorRejectListener(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Filters: []fftypes.JSONAny{`badness`}, + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + assert.Regexp(t, "FF21040.*pop", err) + + mfc.AssertExpectations(t) +} + +func TestStartWithExistingStreamOk(t *testing.T) { + + l := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := &ffcapimocks.API{} + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + + _, err := newTestEventStreamWithListener(t, mfc, `{ + "name": "ut_stream" + }`, l) + assert.NoError(t, err) + + mfc.AssertExpectations(t) +} + +func TestStartWithExistingStreamFail(t *testing.T) { + + l := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := &ffcapimocks.API{} + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + _, err := newTestEventStreamWithListener(t, mfc, `{ + "name": "ut_stream" + }`, l) + assert.Regexp(t, "pop", err) + + mfc.AssertExpectations(t) +} + +func TestUpdateStreamStarted(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + r := args[1].(*ffcapi.EventStreamStartRequest) + started <- r + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + assert.NoError(t, err) + + err = es.Start(es.bgCtx) + assert.NoError(t, err) + + r := <-started + + defNoChange := testESConf(t, `{ + "name": "ut_stream" + }`) + err = es.UpdateSpec(context.Background(), defNoChange) + assert.NoError(t, err) + + defChanged := testESConf(t, `{ + "name": "ut_stream2" + }`) + err = es.UpdateSpec(context.Background(), defChanged) + assert.NoError(t, err) + + assert.Equal(t, "ut_stream2", *es.Spec().Name) + + <-r.StreamContext.Done() + r = <-started + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + + <-r.StreamContext.Done() + + mfc.AssertExpectations(t) +} + +func TestAddRemoveListener(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + r := args[1].(*ffcapi.EventStreamStartRequest) + started <- r + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + + mfc.On("EventListenerRemove", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerRemoveRequest) bool { + return r.ListenerID.Equals(l.ID) + })).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Once() + + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + assert.NoError(t, err) + + err = es.Start(es.bgCtx) + assert.NoError(t, err) + + r := <-started + + err = es.RemoveListener(es.bgCtx, l.ID) + assert.NoError(t, err) + + err = es.RemoveListener(es.bgCtx, l.ID) + assert.NoError(t, err) + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + + <-r.StreamContext.Done() + + mfc.AssertExpectations(t) +} + +func TestUpdateListenerAndDeleteStarted(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l1 := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + r := args[1].(*ffcapi.EventStreamStartRequest) + started <- r + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + + mfc.On("EventListenerAdd", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventListenerAddRequest) bool { + return r.ListenerID.Equals(l1.ID) + })).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, es.spec.ID).Return(nil, nil) + msp.On("DeleteCheckpoint", mock.Anything, es.spec.ID).Return(fmt.Errorf("pop")).Once() + msp.On("DeleteCheckpoint", mock.Anything, es.spec.ID).Return(nil) + + err := es.Start(es.bgCtx) + assert.NoError(t, err) + + _, err = es.AddOrUpdateListener(es.bgCtx, l1.ID, l1, false) + assert.NoError(t, err) + + r := <-started + + // Double add the same - no change, already started + _, err = es.AddOrUpdateListener(es.bgCtx, l1.ID, l1, false) + assert.NoError(t, err) + + updates := &apitypes.Listener{ + Name: strPtr("ut_listener"), + Filters: []fftypes.JSONAny{`{"event":"definition2"}`}, + } + + // Change the event definition, with reset + _, err = es.AddOrUpdateListener(es.bgCtx, l1.ID, updates, true) + assert.NoError(t, err) + + r = <-started + + err = es.Delete(es.bgCtx) + assert.Regexp(t, "pop", err) + + err = es.Delete(es.bgCtx) + assert.NoError(t, err) + + <-r.StreamContext.Done() + + mfc.AssertExpectations(t) +} + +func TestUpdateListenerFail(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l1 := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + r := args[1].(*ffcapi.EventStreamStartRequest) + started <- r + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + + _, err := es.AddOrUpdateListener(es.bgCtx, l1.ID, l1, false) + assert.NoError(t, err) + + err = es.Start(es.bgCtx) + assert.NoError(t, err) + + r := <-started + + // Double add the same + _, err = es.AddOrUpdateListener(es.bgCtx, l1.ID, l1, false) + assert.NoError(t, err) + + updates := &apitypes.Listener{ + Name: strPtr("ut_listener"), + FromBlock: strPtr("0"), + } + + // Update and reset + _, err = es.AddOrUpdateListener(es.bgCtx, l1.ID, updates, true) + assert.Regexp(t, "pop", err) + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + + <-r.StreamContext.Done() + + mfc.AssertExpectations(t) +} + +func TestUpdateEventStreamBad(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "old_name" + }`) + + defNoChange := testESConf(t, `{ + "name": "new_name", + "type": "wrong" + }`) + err := es.UpdateSpec(context.Background(), defNoChange) + assert.Regexp(t, "FF21029", err) + + assert.Equal(t, "old_name", *es.Spec().Name) + +} + +func TestUpdateStreamRestartFail(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + started <- args[1].(*ffcapi.EventStreamStartRequest) + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() + + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + started <- args[1].(*ffcapi.EventStreamStartRequest) + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() + + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), nil).Once() + + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + + err := es.Start(es.bgCtx) + assert.NoError(t, err) + + _, err = es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + assert.NoError(t, err) + + r := <-started + + defChanged := testESConf(t, `{ + "name": "ut_stream2" + }`) + err = es.UpdateSpec(context.Background(), defChanged) + assert.Regexp(t, "FF21032.*pop", err) + + <-r.StreamContext.Done() + + assert.Equal(t, apitypes.EventStreamStatusStopped, es.status) + + mfc.AssertExpectations(t) +} + +func TestUpdateAttemptChangeSignature(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{ + ResolvedSignature: "sig1", + }, ffcapi.ErrorReason(""), nil).Once() + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{ + ResolvedSignature: "sig2", + }, ffcapi.ErrorReason(""), nil).Once() + + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + started <- args[1].(*ffcapi.EventStreamStartRequest) + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() + + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + assert.NoError(t, err) + + err = es.Start(es.bgCtx) + assert.NoError(t, err) + + // Attempt to update filters + _, err = es.AddOrUpdateListener(es.bgCtx, l.ID, &apitypes.Listener{ + Filters: []fftypes.JSONAny{*fftypes.JSONAnyPtr(`{"new":"filter"}`)}, + }, false) + assert.Regexp(t, "FF21051", err) + + r := <-started + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + + <-r.StreamContext.Done() + + mfc.AssertExpectations(t) +} + +func TestAttemptResetNonExistentListener(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + + l := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, true) + assert.Regexp(t, "FF21052", err) + + mfc.AssertExpectations(t) + +} + +func TestUpdateStreamStopFail(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + started <- args[1].(*ffcapi.EventStreamStartRequest) + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() + + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Twice() + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + assert.NoError(t, err) + + err = es.Start(es.bgCtx) + assert.NoError(t, err) + + r := <-started + + defChanged := testESConf(t, `{ + "name": "ut_stream2" + }`) + err = es.UpdateSpec(context.Background(), defChanged) + assert.Regexp(t, "FF21031.*pop", err) + + err = es.Delete(context.Background()) + assert.Regexp(t, "pop", err) + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + + <-r.StreamContext.Done() + + mfc.AssertExpectations(t) +} + +func TestResetListenerRestartFail(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := es.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + started <- args[1].(*ffcapi.EventStreamStartRequest) + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), fmt.Errorf("pop")).Once() + + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil).Once() + + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, es.spec.ID).Return(&apitypes.EventStreamCheckpoint{ + StreamID: es.spec.ID, + Listeners: make(map[fftypes.UUID]json.RawMessage), + }, nil) + msp.On("WriteCheckpoint", mock.Anything, mock.Anything).Return(nil) + msp.On("DeleteCheckpoint", mock.Anything, es.spec.ID).Return(nil) + + err := es.Start(es.bgCtx) + assert.NoError(t, err) + + _, err = es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + assert.NoError(t, err) + + _, err = es.AddOrUpdateListener(es.bgCtx, l.ID, l, true) + assert.Regexp(t, "pop", err) + + err = es.Delete(es.bgCtx) + assert.NoError(t, err) + + mfc.AssertExpectations(t) +} + +func TestResetListenerWriteCheckpointFail(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + l := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + } + + mfc := es.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, es.spec.ID).Return(&apitypes.EventStreamCheckpoint{ + StreamID: es.spec.ID, + Listeners: make(map[fftypes.UUID]json.RawMessage), + }, nil) + msp.On("WriteCheckpoint", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")) + + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + assert.NoError(t, err) + + _, err = es.AddOrUpdateListener(es.bgCtx, l.ID, l, true) + assert.Regexp(t, "pop", err) + + mfc.AssertExpectations(t) +} + +func TestStopWhenNotStarted(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + err := es.Stop(es.bgCtx) + assert.Regexp(t, "FF21027", err) + +} + +func TestWebSocketBroadcastActionCloseDuringCheckpoint(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream", + "websocket": { + "distributionMode": "broadcast" + } + }`) + + l := &apitypes.Listener{ + ID: fftypes.NewUUID(), + Name: strPtr("ut_listener"), + Filters: []fftypes.JSONAny{`{"event":"definition1"}`}, + Options: fftypes.JSONAnyPtr(`{"option1":"value1"}`), + FromBlock: strPtr("12345"), + } + + mfc := es.connector.(*ffcapimocks.API) + + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + + started := make(chan *ffcapi.EventStreamStartRequest, 1) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Run(func(args mock.Arguments) { + started <- args[1].(*ffcapi.EventStreamStartRequest) + }).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) + + first := true + done := make(chan struct{}) + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("WriteCheckpoint", mock.Anything, mock.Anything).Return(fmt.Errorf("pop")).Run(func(args mock.Arguments) { + if first { + go func() { + // Close here so we exit the loop + err := es.Stop(es.bgCtx) + assert.NoError(t, err) + close(done) + }() + first = false + } + }) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + + _, broadcastChannel, _ := mockWSChannels(es.wsChannels.(*wsmocks.WebSocketChannels)) + + _, err := es.AddOrUpdateListener(es.bgCtx, l.ID, l, false) + assert.NoError(t, err) + + err = es.Start(es.bgCtx) + assert.NoError(t, err) + + r := <-started + + r.EventStream <- &ffcapi.ListenerEvent{ + Checkpoint: &utCheckpointType{SomeSequenceNumber: 12345}, + Event: &ffcapi.Event{ + ID: ffcapi.EventID{ + ListenerID: l.ID, + BlockNumber: 42, + TransactionIndex: 13, + LogIndex: 1, + }, + Data: fftypes.JSONAnyPtr(`{"k1":"v1"}`), + Info: &testInfo{ + BlockNumber: "42", + TransactionIndex: "13", + LogIndex: "1", + }, + }, + } + batch1 := (<-broadcastChannel).([]*apitypes.EventWithContext) + assert.Len(t, batch1, 1) + assert.Equal(t, "v1", batch1[0].Data.JSONObject().GetString("k1")) + + <-r.StreamContext.Done() + <-done + + mfc.AssertExpectations(t) +} + +func TestActionRetryOk(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream", + "errorHandling": "skip", + "retryTimeout": "1s" + }`) + + mfc := es.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + + err := es.Start(es.bgCtx) + assert.NoError(t, err) + + es.mux.Lock() + callCount := 0 + es.currentState.action = func(ctx context.Context, batchNumber, attempt int, events []*apitypes.EventWithContext) error { + callCount++ + if callCount > 1 { + return nil + } + return fmt.Errorf("pop") + } + es.mux.Unlock() + + // No-op + err = es.performActionsWithRetry(es.currentState, &eventStreamBatch{}) + assert.NoError(t, err) + + // retry then ok + err = es.performActionsWithRetry(es.currentState, &eventStreamBatch{ + events: []*apitypes.EventWithContext{ + {StandardContext: apitypes.EventContext{StreamID: es.spec.ID}}, + }, + }) + assert.NoError(t, err) + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + +} + +func TestActionRetrySkip(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream", + "errorHandling": "skip", + "blockedRetryDelay": "0s", + "retryTimeout": "0s" + }`) + + mfc := es.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + + err := es.Start(es.bgCtx) + assert.NoError(t, err) + + es.mux.Lock() + es.currentState.action = func(ctx context.Context, batchNumber, attempt int, events []*apitypes.EventWithContext) error { + return fmt.Errorf("pop") + } + es.mux.Unlock() + + // Skip behavior + err = es.performActionsWithRetry(es.currentState, &eventStreamBatch{ + events: []*apitypes.EventWithContext{ + {StandardContext: apitypes.EventContext{StreamID: es.spec.ID}}, + }, + }) + assert.NoError(t, err) + + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + +} + +func TestActionRetryBlock(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream", + "errorHandling": "block", + "blockedRetryDelay": "0s", + "retryTimeout": "0s" + }`) + + mfc := es.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + + err := es.Start(es.bgCtx) + assert.NoError(t, err) + + es.mux.Lock() + callCount := 0 + done := make(chan struct{}) + es.currentState.action = func(ctx context.Context, batchNumber, attempt int, events []*apitypes.EventWithContext) error { + callCount++ + if callCount == 1 { + go func() { + err = es.Stop(es.bgCtx) + assert.NoError(t, err) + close(done) + }() + } + return fmt.Errorf("pop") + } + es.mux.Unlock() + + // Skip behavior + err = es.performActionsWithRetry(es.currentState, &eventStreamBatch{ + events: []*apitypes.EventWithContext{ + {StandardContext: apitypes.EventContext{StreamID: es.spec.ID}}, + }, + }) + assert.Regexp(t, "FF00154", err) + + <-done + assert.Greater(t, callCount, 0) +} + +func TestDeleteFail(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream", + "errorHandling": "block", + "blockedRetryDelay": "0s", + "retryTimeout": "0s" + }`) + + mfc := es.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStartRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil).Once() + mfc.On("EventStreamStopped", mock.Anything, mock.MatchedBy(func(r *ffcapi.EventStreamStoppedRequest) bool { + return r.ID.Equals(es.spec.ID) + })).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("GetCheckpoint", mock.Anything, mock.Anything).Return(nil, nil) // no existing checkpoint + + err := es.Start(es.bgCtx) + assert.NoError(t, err) + + err = es.Delete(es.bgCtx) + assert.Regexp(t, "pop", err) + +} + +func TestEventLoopProcessRemovedEvent(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + ss := &startedStreamState{ + updates: make(chan *ffcapi.ListenerEvent, 1), + eventLoopDone: make(chan struct{}), + } + ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) + + u1 := &ffcapi.ListenerEvent{ + Removed: true, + Event: &ffcapi.Event{ + ID: ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), + }, + }, + } + mcm := &confirmationsmocks.Manager{} + mcm.On("Notify", mock.Anything).Return(nil).Run(func(args mock.Arguments) { + ss.cancelCtx() + }) + es.confirmations = mcm + es.listeners[*u1.Event.ID.ListenerID] = &listener{ + spec: &apitypes.Listener{ID: u1.Event.ID.ListenerID}, + } + + go func() { + ss.updates <- u1 + }() + + es.eventLoop(ss) +} + +func TestEventLoopProcessRemovedEventFail(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + ss := &startedStreamState{ + updates: make(chan *ffcapi.ListenerEvent, 1), + eventLoopDone: make(chan struct{}), + } + ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) + + u1 := &ffcapi.ListenerEvent{ + Removed: true, + Event: &ffcapi.Event{ + ID: ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), + }, + }, + } + mcm := &confirmationsmocks.Manager{} + mcm.On("Notify", mock.Anything).Return(fmt.Errorf("pop")).Run(func(args mock.Arguments) { + ss.cancelCtx() + }) + es.confirmations = mcm + es.listeners[*u1.Event.ID.ListenerID] = &listener{ + spec: &apitypes.Listener{ID: u1.Event.ID.ListenerID}, + } + + go func() { + ss.updates <- u1 + }() + + es.eventLoop(ss) +} + +func TestEventLoopConfirmationsManagerBypass(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + ss := &startedStreamState{ + updates: make(chan *ffcapi.ListenerEvent, 1), + eventLoopDone: make(chan struct{}), + } + ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) + + u1 := &ffcapi.ListenerEvent{ + Checkpoint: &utCheckpointType{SomeSequenceNumber: 12345}, + Event: &ffcapi.Event{ + ID: ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), + }, + }, + } + es.confirmations = nil + es.listeners[*u1.Event.ID.ListenerID] = &listener{ + spec: &apitypes.Listener{ID: u1.Event.ID.ListenerID}, + } + + go func() { + ss.updates <- u1 + u2 := <-es.batchChannel + assert.Equal(t, u1, u2) + ss.cancelCtx() + }() + + es.eventLoop(ss) +} + +func TestEventLoopConfirmationsManagerFail(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + ss := &startedStreamState{ + updates: make(chan *ffcapi.ListenerEvent, 1), + eventLoopDone: make(chan struct{}), + } + ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) + + u1 := &ffcapi.ListenerEvent{ + Checkpoint: &utCheckpointType{SomeSequenceNumber: 12345}, + Event: &ffcapi.Event{ + ID: ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), + }, + }, + } + mcm := &confirmationsmocks.Manager{} + mcm.On("Notify", mock.Anything).Return(fmt.Errorf("pop")).Run(func(args mock.Arguments) { + ss.cancelCtx() + }) + es.confirmations = mcm + es.listeners[*u1.Event.ID.ListenerID] = &listener{ + spec: &apitypes.Listener{ID: u1.Event.ID.ListenerID}, + } + + go func() { + ss.updates <- u1 + }() + + es.eventLoop(ss) +} + +func TestEventLoopIgnoreBadEvent(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + es.processNewEvent(context.Background(), &ffcapi.ListenerEvent{}) +} + +func TestSkipEventsBehindCheckpoint(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + ss := &startedStreamState{ + updates: make(chan *ffcapi.ListenerEvent, 1), + batchLoopDone: make(chan struct{}), + action: func(ctx context.Context, batchNumber, attempt int, events []*apitypes.EventWithContext) error { + assert.Len(t, events, 1) + assert.Equal(t, events[0].ID.BlockNumber.Uint64(), uint64(2001)) + return nil + }, + } + ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) + + listenerID := fftypes.NewUUID() + li := &listener{ + spec: &apitypes.Listener{ID: listenerID, Name: strPtr("listener1")}, + checkpoint: &utCheckpointType{SomeSequenceNumber: 2000}, + } + es.listeners[*li.spec.ID] = li + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { + return cp.StreamID.Equals(es.spec.ID) && bytes.Equal(cp.Listeners[*li.spec.ID], json.RawMessage(`{"someSequenceNumber":2001}`)) + })).Return(nil).Run(func(args mock.Arguments) { + ss.cancelCtx() + }) + + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + es.batchLoop(ss) + wg.Done() + }() + es.batchChannel <- &ffcapi.ListenerEvent{ + Checkpoint: &utCheckpointType{SomeSequenceNumber: 1999}, // before checkpoint - redelivery + Event: &ffcapi.Event{ID: ffcapi.EventID{ListenerID: listenerID, BlockNumber: 1999}}, + } + es.batchChannel <- &ffcapi.ListenerEvent{ + Checkpoint: &utCheckpointType{SomeSequenceNumber: 2000}, // on checkpoint - redelivery + Event: &ffcapi.Event{ID: ffcapi.EventID{ListenerID: listenerID, BlockNumber: 2000}}, + } + es.batchChannel <- &ffcapi.ListenerEvent{ + Checkpoint: &utCheckpointType{SomeSequenceNumber: 2001}, // this is a new event + Event: &ffcapi.Event{ID: ffcapi.EventID{ListenerID: listenerID, BlockNumber: 2001}}, + } + wg.Wait() + + msp.AssertExpectations(t) +} + +func TestHWMCheckpointAfterInactivity(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + ss := &startedStreamState{ + updates: make(chan *ffcapi.ListenerEvent, 1), + batchLoopDone: make(chan struct{}), + } + ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) + + li := &listener{ + spec: &apitypes.Listener{ID: fftypes.NewUUID()}, + } + + mcm := &confirmationsmocks.Manager{} + mcm.On("CheckInFlight", li.spec.ID).Return(false) + es.confirmations = mcm + es.listeners[*li.spec.ID] = li + + mfc := es.connector.(*ffcapimocks.API) + mfc.On("EventListenerHWM", mock.Anything, mock.MatchedBy(func(req *ffcapi.EventListenerHWMRequest) bool { + return req.StreamID.Equals(es.spec.ID) && req.ListenerID.Equals(li.spec.ID) + })).Run(func(args mock.Arguments) { + ss.cancelCtx() + }).Return(&ffcapi.EventListenerHWMResponse{ + Checkpoint: &utCheckpointType{SomeSequenceNumber: 12345}, + }, ffcapi.ErrorReason(""), nil) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { + return cp.StreamID.Equals(es.spec.ID) && bytes.Equal(cp.Listeners[*li.spec.ID], json.RawMessage(`{"someSequenceNumber":12345}`)) + })).Return(nil) + + es.checkpointInterval = 1 * time.Microsecond + + es.batchLoop(ss) + + mfc.AssertExpectations(t) + msp.AssertExpectations(t) + mcm.AssertExpectations(t) +} + +func TestHWMCheckpointInFlightSkip(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + ss := &startedStreamState{ + updates: make(chan *ffcapi.ListenerEvent, 1), + batchLoopDone: make(chan struct{}), + } + ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) + + li := &listener{ + spec: &apitypes.Listener{ID: fftypes.NewUUID()}, + } + + mcm := &confirmationsmocks.Manager{} + mcm.On("CheckInFlight", li.spec.ID).Run(func(args mock.Arguments) { + ss.cancelCtx() + }).Return(true) + es.confirmations = mcm + es.listeners[*li.spec.ID] = li + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { + return cp.StreamID.Equals(es.spec.ID) && string(cp.Listeners[*li.spec.ID]) == `null` + })).Return(nil) + + es.checkpointInterval = 1 * time.Microsecond + + es.batchLoop(ss) + + msp.AssertExpectations(t) + mcm.AssertExpectations(t) +} + +func TestHWMCheckpointFail(t *testing.T) { + + es := newTestEventStream(t, `{ + "name": "ut_stream" + }`) + + ss := &startedStreamState{ + updates: make(chan *ffcapi.ListenerEvent, 1), + batchLoopDone: make(chan struct{}), + } + ss.ctx, ss.cancelCtx = context.WithCancel(context.Background()) + + li := &listener{ + spec: &apitypes.Listener{ID: fftypes.NewUUID()}, + } + + mcm := &confirmationsmocks.Manager{} + mcm.On("CheckInFlight", li.spec.ID).Return(false) + es.confirmations = mcm + es.listeners[*li.spec.ID] = li + + mfc := es.connector.(*ffcapimocks.API) + mfc.On("EventListenerHWM", mock.Anything, mock.MatchedBy(func(req *ffcapi.EventListenerHWMRequest) bool { + return req.StreamID.Equals(es.spec.ID) && req.ListenerID.Equals(li.spec.ID) + })).Run(func(args mock.Arguments) { + ss.cancelCtx() + }).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + msp := es.persistence.(*persistencemocks.Persistence) + msp.On("WriteCheckpoint", mock.Anything, mock.MatchedBy(func(cp *apitypes.EventStreamCheckpoint) bool { + return cp.StreamID.Equals(es.spec.ID) && string(cp.Listeners[*li.spec.ID]) == `null` + })).Return(nil) + + es.checkpointInterval = 1 * time.Microsecond + + es.batchLoop(ss) + + mfc.AssertExpectations(t) + msp.AssertExpectations(t) + mcm.AssertExpectations(t) +} diff --git a/internal/events/listener.go b/internal/events/listener.go new file mode 100644 index 00000000..6e4aa75f --- /dev/null +++ b/internal/events/listener.go @@ -0,0 +1,77 @@ +// 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 events + +import ( + "context" + "encoding/json" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" +) + +type listener struct { + es *eventStream + spec *apitypes.Listener + lastCheckpoint *fftypes.FFTime + checkpoint ffcapi.EventListenerCheckpoint +} + +func listenerSpecToOptions(spec *apitypes.Listener) ffcapi.EventListenerOptions { + return ffcapi.EventListenerOptions{ + FromBlock: *spec.FromBlock, + Filters: spec.Filters, + Options: spec.Options, + } +} + +func (l *listener) stop(startedState *startedStreamState) error { + _, _, err := l.es.connector.EventListenerRemove(startedState.ctx, &ffcapi.EventListenerRemoveRequest{ + StreamID: l.spec.StreamID, + ListenerID: l.spec.ID, + }) + return err +} + +func (l *listener) buildAddRequest(ctx context.Context, cp *apitypes.EventStreamCheckpoint) *ffcapi.EventListenerAddRequest { + req := &ffcapi.EventListenerAddRequest{ + EventListenerOptions: listenerSpecToOptions(l.spec), + Name: *l.spec.Name, + ListenerID: l.spec.ID, + StreamID: l.spec.StreamID, + } + if cp != nil { + jsonCP := cp.Listeners[*l.spec.ID] + if jsonCP != nil { + listenerCheckpoint := l.es.connector.EventStreamNewCheckpointStruct() + err := json.Unmarshal(jsonCP, &listenerCheckpoint) + if err != nil { + log.L(ctx).Errorf("Failed to restore checkpoint for listener '%s': %s", l.spec.ID, err) + } else { + req.Checkpoint = listenerCheckpoint + } + } + } + return req +} + +func (l *listener) start(startedState *startedStreamState, cp *apitypes.EventStreamCheckpoint) error { + _, _, err := l.es.connector.EventListenerAdd(startedState.ctx, l.buildAddRequest(startedState.ctx, cp)) + return err +} diff --git a/internal/events/webhooks.go b/internal/events/webhooks.go new file mode 100644 index 00000000..7a63617f --- /dev/null +++ b/internal/events/webhooks.go @@ -0,0 +1,135 @@ +// 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 events + +import ( + "context" + "crypto/tls" + "net" + "net/url" + "time" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/hyperledger/firefly-common/pkg/ffresty" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +func mergeValidateWhConfig(ctx context.Context, changed bool, base *apitypes.WebhookConfig, updates *apitypes.WebhookConfig) (*apitypes.WebhookConfig, bool, error) { + + if base == nil { + base = &apitypes.WebhookConfig{} + } + if updates == nil { + updates = &apitypes.WebhookConfig{} + } + merged := &apitypes.WebhookConfig{} + + // URL (no default - must be set) + changed = apitypes.CheckUpdateString(changed, &merged.URL, base.URL, updates.URL, "") + if *merged.URL == "" { + return nil, false, i18n.NewError(ctx, tmmsgs.MsgMissingWebhookURL) + } + + // Headers + changed = apitypes.CheckUpdateStringMap(changed, &merged.Headers, base.Headers, updates.Headers) + + // Skip host verify (disable TLS checking) + changed = apitypes.CheckUpdateBool(changed, &merged.TLSkipHostVerify, base.TLSkipHostVerify, updates.TLSkipHostVerify, false) + + // Request timeout + if updates.EthCompatRequestTimeoutSec != nil { + dv := fftypes.FFDuration(*updates.EthCompatRequestTimeoutSec) * fftypes.FFDuration(time.Second) + changed = apitypes.CheckUpdateDuration(changed, &merged.RequestTimeout, base.RequestTimeout, &dv, esDefaults.webhookRequestTimeout) + } else { + changed = apitypes.CheckUpdateDuration(changed, &merged.RequestTimeout, base.RequestTimeout, updates.RequestTimeout, esDefaults.webhookRequestTimeout) + } + + return merged, changed, nil +} + +type webhookAction struct { + allowPrivateIPs bool + spec *apitypes.WebhookConfig + client *resty.Client +} + +func newWebhookAction(bgCtx context.Context, spec *apitypes.WebhookConfig) *webhookAction { + client := ffresty.New(bgCtx, tmconfig.WebhookPrefix) // majority of settings come from config + client.SetTimeout(time.Duration(*spec.RequestTimeout)) // request timeout set per stream + if *spec.TLSkipHostVerify { + client.SetTLSClientConfig(&tls.Config{ + InsecureSkipVerify: true, + }) + } + + return &webhookAction{ + spec: spec, + allowPrivateIPs: config.GetBool(tmconfig.WebhooksAllowPrivateIPs), + client: client, + } +} + +// attemptWebhookAction performs a single attempt of a webhook action +func (w *webhookAction) attemptBatch(ctx context.Context, batchNumber, attempt int, events []*apitypes.EventWithContext) error { + // We perform DNS resolution before each attempt, to exclude private IP address ranges from the target + u, _ := url.Parse(*w.spec.URL) + addr, err := net.ResolveIPAddr("ip4", u.Hostname()) + if err != nil { + return i18n.NewError(ctx, tmmsgs.MsgInvalidHost, u.Hostname()) + } + if w.isAddressBlocked(addr) { + return i18n.NewError(ctx, tmmsgs.MsgBlockWebhookAddress, addr, u.Hostname()) + } + var resBody []byte + req := w.client.R(). + SetContext(ctx). + SetBody(events). + SetResult(&resBody). + SetError(&resBody) + req.Header.Set("Content-Type", "application/json") + for h, v := range w.spec.Headers { + req.Header.Set(h, v) + } + res, err := req.Post(u.String()) + if err != nil { + log.L(ctx).Errorf("Webhook %s (%s): %s", *w.spec.URL, u, err) + return i18n.NewError(ctx, tmmsgs.MsgWebhookErr, err) + } + if res.IsError() { + log.L(ctx).Errorf("Webhook %s (%s) [%d]: %s", *w.spec.URL, u, res.StatusCode(), resBody) + err = i18n.NewError(ctx, tmmsgs.MsgWebhookFailedStatus, res.StatusCode()) + } + return err +} + +// isAddressBlocked allows blocking of all of the "private" address blocks defined by IPv4 +func (w *webhookAction) isAddressBlocked(ip *net.IPAddr) bool { + ip4 := ip.IP.To4() + return !w.allowPrivateIPs && + (ip4[0] == 0 || + ip4[0] >= 224 || + ip4[0] == 127 || + ip4[0] == 10 || + (ip4[0] == 172 && ip4[1] >= 16 && ip4[1] < 32) || + (ip4[0] == 192 && ip4[1] == 168)) +} diff --git a/internal/events/webhooks_test.go b/internal/events/webhooks_test.go new file mode 100644 index 00000000..8b32e653 --- /dev/null +++ b/internal/events/webhooks_test.go @@ -0,0 +1,106 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package events + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/stretchr/testify/assert" +) + +func newTestWebhooks(url string) *webhookAction { + tmconfig.Reset() + truthy := true + oneSec := 1 * time.Second + return newWebhookAction(context.Background(), &apitypes.WebhookConfig{ + TLSkipHostVerify: &truthy, + URL: &url, + RequestTimeout: (*fftypes.FFDuration)(&oneSec), + }) +} + +func TestWebhooksBadHost(t *testing.T) { + tmconfig.Reset() + ws := newTestWebhooks("http://www.sample.invalid/guaranteed-to-fail") + + err := ws.attemptBatch(context.Background(), 0, 0, []*apitypes.EventWithContext{}) + assert.Regexp(t, "FF21041", err) +} + +func TestWebhooksPrivateBlocked(t *testing.T) { + tmconfig.Reset() + ws := newTestWebhooks("http://10.0.0.1/one-of-the-private-ranges") + falsy := false + ws.allowPrivateIPs = falsy + + err := ws.attemptBatch(context.Background(), 0, 0, []*apitypes.EventWithContext{}) + assert.Regexp(t, "FF21033", err) +} + +func TestWebhooksCustomHeaders403(t *testing.T) { + + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/test/path", r.URL.Path) + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "test-value", r.Header.Get("test-header")) + var events []*apitypes.EventWithContext + err := json.NewDecoder(r.Body).Decode(&events) + assert.NoError(t, err) + w.WriteHeader(403) + })) + defer s.Close() + + tmconfig.Reset() + ws := newTestWebhooks(fmt.Sprintf("http://%s/test/path", s.Listener.Addr())) + ws.spec.Headers = map[string]string{ + "test-header": "test-value", + } + + done := make(chan struct{}) + go func() { + err := ws.attemptBatch(context.Background(), 0, 0, []*apitypes.EventWithContext{}) + assert.Regexp(t, "FF21035.*403", err) + close(done) + }() + <-done +} + +func TestWebhooksCustomHeadersConnectFail(t *testing.T) { + + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + s.Close() + + tmconfig.Reset() + ws := newTestWebhooks(fmt.Sprintf("http://%s/test/path", s.Listener.Addr())) + + done := make(chan struct{}) + go func() { + err := ws.attemptBatch(context.Background(), 0, 0, []*apitypes.EventWithContext{}) + assert.Regexp(t, "FF21042", err) + close(done) + }() + <-done +} diff --git a/internal/events/websockets.go b/internal/events/websockets.go new file mode 100644 index 00000000..71e889b4 --- /dev/null +++ b/internal/events/websockets.go @@ -0,0 +1,125 @@ +// 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 events + +import ( + "context" + + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/internal/ws" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +func mergeValidateWsConfig(ctx context.Context, changed bool, base *apitypes.WebSocketConfig, updates *apitypes.WebSocketConfig) (*apitypes.WebSocketConfig, bool, error) { + + if base == nil { + base = &apitypes.WebSocketConfig{} + } + if updates == nil { + updates = &apitypes.WebSocketConfig{} + } + merged := &apitypes.WebSocketConfig{} + + // Distribution mode + changed = apitypes.CheckUpdateEnum(changed, &merged.DistributionMode, base.DistributionMode, updates.DistributionMode, esDefaults.websocketDistributionMode) + switch *merged.DistributionMode { + case apitypes.DistributionModeLoadBalance, apitypes.DistributionMode("workloaddistribution"): + // Migrate old "workloadDistribution" enum value to more consistent with other FF enums "load_balance" + *merged.DistributionMode = apitypes.DistributionModeLoadBalance + case apitypes.DistributionModeBroadcast: + default: + return nil, false, i18n.NewError(ctx, tmmsgs.MsgInvalidDistributionMode, *merged.DistributionMode) + } + + // Topic + changed = apitypes.CheckUpdateString(changed, &merged.Topic, base.Topic, updates.Topic, esDefaults.topic) + + return merged, changed, nil +} + +type webSocketAction struct { + topic string + spec *apitypes.WebSocketConfig + wsChannels ws.WebSocketChannels +} + +func newWebSocketAction(wsChannels ws.WebSocketChannels, spec *apitypes.WebSocketConfig, topic string) *webSocketAction { + return &webSocketAction{ + spec: spec, + wsChannels: wsChannels, + topic: topic, + } +} + +// attemptBatch attempts to deliver a batch over socket IO +func (w *webSocketAction) attemptBatch(ctx context.Context, batchNumber, attempt int, events []*apitypes.EventWithContext) error { + var err error + + // Get a blocking channel to send and receive on our chosen namespace + sender, broadcaster, receiver := w.wsChannels.GetChannels(w.topic) + + var channel chan<- interface{} + switch *w.spec.DistributionMode { + case apitypes.DistributionModeBroadcast: + channel = broadcaster + case apitypes.DistributionModeLoadBalance: + channel = sender + default: + return i18n.NewError(ctx, tmmsgs.MsgInvalidDistributionMode, *w.spec.DistributionMode) + } + + // Clear out any current ack/error + purging := true + for purging { + select { + case err1 := <-receiver: + log.L(ctx).Warnf("Cleared out spurious ack (could be from previous disconnect). err=%v", err1) + default: + purging = false + } + } + + // Send the batch of events + select { + case channel <- events: + break + case <-ctx.Done(): + err = i18n.NewError(ctx, tmmsgs.MsgWebSocketInterruptedSend) + } + + // If we ever add more distribution modes, we may want to change this logic from a simple if statement + if err == nil && *w.spec.DistributionMode != apitypes.DistributionModeBroadcast { + err = w.waitForAck(ctx, receiver) + } + + // Pass back any exception from the client + log.L(ctx).Infof("WebSocket event batch %d complete (len=%d). err=%v", batchNumber, len(events), err) + return err +} + +func (w *webSocketAction) waitForAck(ctx context.Context, receiver <-chan error) (err error) { + // Wait for the next ack or exception + select { + case err = <-receiver: + break + case <-ctx.Done(): + err = i18n.NewError(ctx, tmmsgs.MsgWebSocketInterruptedReceive) + } + return err +} diff --git a/internal/events/websockets_test.go b/internal/events/websockets_test.go new file mode 100644 index 00000000..b8eabcfe --- /dev/null +++ b/internal/events/websockets_test.go @@ -0,0 +1,97 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package events + +import ( + "context" + "testing" + + "github.com/hyperledger/firefly-transaction-manager/mocks/wsmocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/stretchr/testify/assert" +) + +func TestWSAttemptBatchBadDistMode(t *testing.T) { + + mws := &wsmocks.WebSocketChannels{} + mockWSChannels(mws) + + dmw := apitypes.DistributionMode("wrong") + wsa := newWebSocketAction(mws, &apitypes.WebSocketConfig{ + DistributionMode: &dmw, + }, "ut_stream") + + err := wsa.attemptBatch(context.Background(), 0, 0, []*apitypes.EventWithContext{}) + assert.Regexp(t, "FF21034", err) + +} + +func TestWSAttemptBatchPurge(t *testing.T) { + + mws := &wsmocks.WebSocketChannels{} + _, _, rc := mockWSChannels(mws) + rc <- nil + + dmw := apitypes.DistributionModeBroadcast + wsa := newWebSocketAction(mws, &apitypes.WebSocketConfig{ + DistributionMode: &dmw, + }, "ut_stream") + + err := wsa.attemptBatch(context.Background(), 0, 0, []*apitypes.EventWithContext{}) + assert.NoError(t, err) + + select { + case <-rc: + assert.Fail(t, "Should not be anything left on the ack channel - should have been purged before send") + default: + } +} + +func TestWSAttemptBatchExitPushingEvent(t *testing.T) { + + mws := &wsmocks.WebSocketChannels{} + _, bc, _ := mockWSChannels(mws) + bc <- []*apitypes.EventWithContext{} // block the broadcast channel + + dmw := apitypes.DistributionModeBroadcast + wsa := newWebSocketAction(mws, &apitypes.WebSocketConfig{ + DistributionMode: &dmw, + }, "ut_stream") + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err := wsa.attemptBatch(ctx, 0, 0, []*apitypes.EventWithContext{}) + assert.Regexp(t, "FF21038", err) + +} + +func TestWSAttemptBatchExitReceivingReply(t *testing.T) { + + mws := &wsmocks.WebSocketChannels{} + _, _, rc := mockWSChannels(mws) + + dmw := apitypes.DistributionModeBroadcast + wsa := newWebSocketAction(mws, &apitypes.WebSocketConfig{ + DistributionMode: &dmw, + }, "ut_stream") + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err := wsa.waitForAck(ctx, rc) + assert.Regexp(t, "FF21039", err) + +} diff --git a/internal/manager/api.go b/internal/manager/api.go deleted file mode 100644 index de3435cb..00000000 --- a/internal/manager/api.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package manager - -import ( - "context" - "encoding/json" - "net/http" - "strconv" - - "github.com/gorilla/mux" - "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-transaction-manager/internal/tmmsgs" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" -) - -func (m *manager) router() *mux.Router { - mux := mux.NewRouter() - mux.Path("/").Methods(http.MethodPost).Handler(http.HandlerFunc(m.apiHandler)) - return mux -} - -func (m *manager) runAPIServer() { - m.apiServer.ServeHTTP(m.ctx) -} - -func (m *manager) validateRequest(ctx context.Context, tReq *fftm.TransactionRequest) error { - if tReq == nil || tReq.Headers.ID == "" || tReq.Headers.Type == "" { - log.L(ctx).Warnf("Invalid request: %+v", tReq) - return i18n.NewError(ctx, tmmsgs.MsgErrorInvalidRequest) - } - return nil -} - -func (m *manager) apiHandler(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - var tReq *fftm.TransactionRequest - statusCode := 200 - err := json.NewDecoder(r.Body).Decode(&tReq) - if err == nil { - err = m.validateRequest(ctx, tReq) - } - var resBody interface{} - if err != nil { - statusCode = 400 - } else { - ctx = log.WithLogField(ctx, "requestId", tReq.Headers.ID) - switch tReq.Headers.Type { - case fftm.RequestTypeSendTransaction: - resBody, err = m.sendManagedTransaction(ctx, tReq) - default: - err = i18n.NewError(ctx, tmmsgs.MsgUnsupportedRequestType, tReq.Headers.Type) - statusCode = 400 - } - } - if err != nil { - log.L(ctx).Errorf("Request failed: %s", err) - resBody = &fftypes.RESTError{Error: err.Error()} - if statusCode < 400 { - statusCode = 500 - } - } - w.Header().Set("Content-Type", "application/json") - resBytes, _ := json.Marshal(&resBody) - w.Header().Set("Content-Length", strconv.FormatInt(int64(len(resBytes)), 10)) - w.WriteHeader(statusCode) - _, _ = w.Write(resBytes) -} diff --git a/internal/manager/api_test.go b/internal/manager/api_test.go deleted file mode 100644 index 0a2f9041..00000000 --- a/internal/manager/api_test.go +++ /dev/null @@ -1,323 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package manager - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "strings" - "testing" - - "github.com/go-resty/resty/v2" - "github.com/hyperledger/firefly-common/pkg/ffcapi" - "github.com/hyperledger/firefly-common/pkg/fftypes" - "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" - "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -const sampleSendTX = `{ - "headers": { - "id": "904F177C-C790-4B01-BDF4-F2B4E52E607E", - "type": "SendTransaction" - }, - "from": "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", - "to": "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771", - "gas": 1000000, - "method": { - "inputs": [ - { - "internalType":" uint256", - "name": "x", - "type": "uint256" - } - ], - "name":"set", - "outputs":[], - "stateMutability":"nonpayable", - "type":"function" - }, - "params": [ - { - "value": 4276993775, - "type": "uint256" - } - ] -}` - -func testFFCAPIHandler(t *testing.T, fn func(reqType ffcapi.RequestType, b []byte) (res interface{}, status int)) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var reqHeader ffcapi.RequestBase - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - err = json.Unmarshal(b, &reqHeader) - assert.NoError(t, err) - - assert.NotNil(t, reqHeader.FFCAPI.RequestID) - assert.Equal(t, ffcapi.VersionCurrent, reqHeader.FFCAPI.Version) - assert.Equal(t, ffcapi.Variant("evm"), reqHeader.FFCAPI.Variant) - - res, status := fn(reqHeader.FFCAPI.RequestType, b) - - b, err = json.Marshal(res) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - w.Write(b) - - } -} - -func TestSendTransactionE2E(t *testing.T) { - - txSent := make(chan struct{}) - - url, m, cancel := newTestManager(t, - testFFCAPIHandler(t, func(reqType ffcapi.RequestType, b []byte) (res interface{}, status int) { - status = 200 - switch reqType { - - case ffcapi.RequestTypeGetNextNonce: - var nonceReq ffcapi.GetNextNonceRequest - err := json.Unmarshal(b, &nonceReq) - assert.NoError(t, err) - assert.Equal(t, "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", nonceReq.Signer) - res = ffcapi.GetNextNonceResponse{ - Nonce: fftypes.NewFFBigInt(12345), - } - - case ffcapi.RequestTypePrepareTransaction: - var prepTX ffcapi.PrepareTransactionRequest - err := json.Unmarshal(b, &prepTX) - assert.NoError(t, err) - assert.Equal(t, "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", prepTX.From) - assert.Equal(t, "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771", prepTX.To) - assert.Equal(t, uint64(1000000), prepTX.Gas.Uint64()) - assert.Equal(t, "set", prepTX.Method.JSONObject().GetString("name")) - assert.Len(t, prepTX.Params, 1) - assert.Equal(t, "4276993775", prepTX.Params[0].JSONObject().GetString("value")) - res = ffcapi.PrepareTransactionResponse{ - TransactionData: "RAW_UNSIGNED_BYTES", - Gas: fftypes.NewFFBigInt(2000000), // gas estimate simulation - } - - case ffcapi.RequestTypeSendTransaction: - var sendTX ffcapi.SendTransactionRequest - err := json.Unmarshal(b, &sendTX) - assert.NoError(t, err) - assert.Equal(t, "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", sendTX.From) - assert.Equal(t, "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771", sendTX.To) - assert.Equal(t, uint64(2000000), sendTX.Gas.Uint64()) - assert.Equal(t, `223344556677`, sendTX.GasPrice.String()) - assert.Equal(t, "RAW_UNSIGNED_BYTES", sendTX.TransactionData) - res = ffcapi.SendTransactionResponse{ - TransactionHash: "0x106215b9c0c9372e3f541beff0cdc3cd061a26f69f3808e28fd139a1abc9d345", - } - - // We're at end of job for this test - close(txSent) - - default: - assert.Fail(t, fmt.Sprintf("Unexpected type: %s", reqType)) - status = 500 - } - return res, status - }), - func(w http.ResponseWriter, r *http.Request) { - - }, - ) - defer cancel() - - mc := m.confirmations.(*confirmationsmocks.Manager) - mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { - return n.NotificationType == confirmations.NewTransaction - })).Return(nil) - - m.Start() - - req := strings.NewReader(sampleSendTX) - res, err := resty.New().R(). - SetBody(req). - Post(url) - assert.NoError(t, err) - assert.Equal(t, 200, res.StatusCode()) - - <-txSent - -} - -func TestSendInvalidRequestNoHeaders(t *testing.T) { - - url, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, - ) - defer cancel() - m.Start() - - req := strings.NewReader(`{ - "noHeaders": true - }`) - var errRes fftypes.RESTError - res, err := resty.New().R(). - SetBody(req). - SetError(&errRes). - Post(url) - assert.NoError(t, err) - assert.Equal(t, 400, res.StatusCode()) - assert.Regexp(t, "FF21022", errRes.Error) -} - -func TestSendInvalidRequestWrongType(t *testing.T) { - - url, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, - ) - defer cancel() - m.Start() - - req := strings.NewReader(`{ - "headers": { - "id": "ns1:` + fftypes.NewUUID().String() + `", - "type": "wrong" - } - }`) - var errRes fftypes.RESTError - res, err := resty.New().R(). - SetBody(req). - SetError(&errRes). - Post(url) - assert.NoError(t, err) - assert.Equal(t, 400, res.StatusCode()) - assert.Regexp(t, "FF21023", errRes.Error) -} - -func TestSendInvalidRequestFail(t *testing.T) { - - url, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) { - backendError := &fftypes.RESTError{Error: "pop"} - b, err := json.Marshal(&backendError) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(503) - w.Write(b) - }, - func(w http.ResponseWriter, r *http.Request) {}, - ) - defer cancel() - m.Start() - - req := strings.NewReader(`{ - "headers": { - "id": "ns1:` + fftypes.NewUUID().String() + `", - "type": "SendTransaction" - } - }`) - var errRes fftypes.RESTError - res, err := resty.New().R(). - SetBody(req). - SetError(&errRes). - Post(url) - assert.NoError(t, err) - assert.Equal(t, 500, res.StatusCode()) - assert.Regexp(t, "FF00157", errRes.Error) -} - -func TestSendTransactionPrepareFail(t *testing.T) { - - url, m, cancel := newTestManager(t, - testFFCAPIHandler(t, func(reqType ffcapi.RequestType, b []byte) (res interface{}, status int) { - status = 200 - switch reqType { - case ffcapi.RequestTypeGetNextNonce: - res = ffcapi.GetNextNonceResponse{ - Nonce: fftypes.NewFFBigInt(12345), - } - - case ffcapi.RequestTypePrepareTransaction: - res = ffcapi.ErrorResponse{ - Error: "pop", - } - status = 500 - } - return res, status - }), - func(w http.ResponseWriter, r *http.Request) { - - }, - ) - defer cancel() - - m.Start() - - req := strings.NewReader(sampleSendTX) - res, err := resty.New().R(). - SetBody(req). - Post(url) - assert.NoError(t, err) - assert.Equal(t, 500, res.StatusCode()) - -} - -func TestSendTransactionUpdateFireFlyFail(t *testing.T) { - - url, m, cancel := newTestManager(t, - testFFCAPIHandler(t, func(reqType ffcapi.RequestType, b []byte) (res interface{}, status int) { - status = 200 - switch reqType { - case ffcapi.RequestTypeGetNextNonce: - res = ffcapi.GetNextNonceResponse{ - Nonce: fftypes.NewFFBigInt(12345), - } - - case ffcapi.RequestTypePrepareTransaction: - res = ffcapi.PrepareTransactionResponse{} - status = 200 - } - return res, status - }), - func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodPatch { - errRes := fftypes.RESTError{Error: "pop"} - b, err := json.Marshal(&errRes) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(500) - w.Write(b) - } else { - w.WriteHeader(200) - } - }, - ) - defer cancel() - - m.Start() - - req := strings.NewReader(sampleSendTX) - res, err := resty.New().R(). - SetBody(req). - Post(url) - assert.NoError(t, err) - assert.Equal(t, 500, res.StatusCode()) - -} diff --git a/internal/manager/changelistener.go b/internal/manager/changelistener.go deleted file mode 100644 index bb044a4f..00000000 --- a/internal/manager/changelistener.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package manager - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/hyperledger/firefly-common/pkg/log" - "github.com/hyperledger/firefly-common/pkg/wsclient" - "github.com/hyperledger/firefly/pkg/core" -) - -func (m *manager) startChangeListener(ctx context.Context, w wsclient.WSClient) error { - cmd := core.WSChangeEventCommand{ - Type: core.WSChangeEventCommandTypeStart, - Collections: []string{"operations"}, - Filter: core.ChangeEventFilter{ - Types: []core.ChangeEventType{core.ChangeEventTypeUpdated}, - }, - } - b, _ := json.Marshal(&cmd) - log.L(m.ctx).Infof("Change listener connected. Sent: %s", b) - return w.Send(ctx, b) -} - -func (m *manager) handleEvent(ce *core.ChangeEvent) { - log.L(m.ctx).Debugf("%s:%s/%s operation change event received", ce.Namespace, ce.ID, ce.Type) - if ce.Collection == "operations" && ce.Type == core.ChangeEventTypeUpdated { - nsOpID := fmt.Sprintf("%s:%s", ce.Namespace, ce.ID) - m.mux.Lock() - _, knownID := m.pendingOpsByID[nsOpID] - m.mux.Unlock() - if !knownID { - // Currently the only action taken for change events, is to check we are - // tracking the transaction. However, as only transactions we submitted are - // valid and we do a full query on startup - the change listener is a little - // redundant (and disabled by default) - m.queryAndAddPending(nsOpID) - } - } -} - -func (m *manager) changeEventLoop() { - defer close(m.changeEventLoopDone) - for { - select { - case b := <-m.wsClient.Receive(): - var ce *core.ChangeEvent - _ = json.Unmarshal(b, &ce) - m.handleEvent(ce) - case <-m.ctx.Done(): - log.L(m.ctx).Infof("Change event loop exiting") - return - } - } -} - -func (m *manager) startWS() error { - if m.enableChangeListener { - m.changeEventLoopDone = make(chan struct{}) - if err := m.wsClient.Connect(); err != nil { - return err - } - go m.changeEventLoop() - } - return nil -} - -func (m *manager) waitWSStop() { - if m.changeEventLoopDone != nil { - <-m.changeEventLoopDone - } -} diff --git a/internal/manager/changelistener_test.go b/internal/manager/changelistener_test.go deleted file mode 100644 index 34379914..00000000 --- a/internal/manager/changelistener_test.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package manager - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - "testing" - - "github.com/hyperledger/firefly-common/pkg/fftypes" - "github.com/hyperledger/firefly-common/pkg/wsclient" - "github.com/hyperledger/firefly/pkg/core" - "github.com/stretchr/testify/assert" -) - -func TestWSChangeDeliveryLookup(t *testing.T) { - - opID := fftypes.NewUUID() - lookedUp := make(chan struct{}) - toServer, fromServer, wsURL, done := wsclient.NewTestWSServer( - func(req *http.Request) { - switch req.URL.Path { - case `/admin/ws`: - return - case fmt.Sprintf("/spi/v1/operations/ns1:%s", opID): - close(lookedUp) - default: - assert.Fail(t, fmt.Sprintf("Unexpected path: %s", req.URL.Path)) - } - }, - ) - defer done() - - httpURL, err := url.Parse(wsURL) - assert.NoError(t, err) - httpURL.Scheme = "http" - - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, - httpURL.String(), - ) - defer cancel() - - m.startWS() - - cmdJSON := <-toServer - var startCmd core.WSChangeEventCommand - err = json.Unmarshal([]byte(cmdJSON), &startCmd) - assert.NoError(t, err) - assert.Equal(t, core.WSChangeEventCommandTypeStart, startCmd.Type) - - change := &core.ChangeEvent{ - Collection: "operations", - Type: core.ChangeEventTypeUpdated, - Namespace: "ns1", - ID: opID, - } - changeJSON, err := json.Marshal(&change) - assert.NoError(t, err) - fromServer <- string(changeJSON) - - <-lookedUp - - m.cancelCtx() - m.waitWSStop() - -} - -func TestWSConnectFail(t *testing.T) { - - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, - ) - cancel() - - m.enableChangeListener = true - err := m.startWS() - assert.Regexp(t, "FF00154", err) - -} diff --git a/internal/manager/ffcore.go b/internal/manager/ffcore.go deleted file mode 100644 index f8f4ad5b..00000000 --- a/internal/manager/ffcore.go +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package manager - -import ( - "context" - "fmt" - "net/url" - "strconv" - - "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-transaction-manager/internal/tmmsgs" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" - "github.com/hyperledger/firefly/pkg/core" -) - -// opUpdate allows us to avoid JSONObject serialization to a map before we upload our managedTXOutput -type opUpdate struct { - Status core.OpStatus `json:"status"` - Output *fftm.ManagedTXOutput `json:"output"` - Error string `json:"error"` -} - -func (m *manager) writeManagedTX(ctx context.Context, mtx *fftm.ManagedTXOutput, status core.OpStatus, errString string) error { - log.L(ctx).Debugf("Updating operation %s status=%s", mtx.ID, status) - var errorInfo fftypes.RESTError - var op core.Operation - res, err := m.ffCoreClient.R(). - SetResult(&op). - SetError(&errorInfo). - SetBody(&opUpdate{ - Output: mtx, - Status: status, - Error: errString, - }). - SetContext(ctx). - Patch(fmt.Sprintf("/spi/v1/operations/%s", mtx.ID)) - if err != nil { - return err - } - if res.IsError() { - return i18n.NewError(m.ctx, tmmsgs.MsgCoreError, res.StatusCode(), errorInfo.Error) - } - return nil -} - -func (m *manager) queryAndAddPending(nsOpID string) { - var errorInfo fftypes.RESTError - var op *core.Operation - res, err := m.ffCoreClient.R(). - SetResult(&op). - SetError(&errorInfo). - Get(fmt.Sprintf("/spi/v1/operations/%s", nsOpID)) - if err == nil { - // Operations are not deleted, so we consider not found the same as any other error - if res.IsError() { - err = i18n.NewError(m.ctx, tmmsgs.MsgCoreError, res.StatusCode(), errorInfo.Error) - } - } - if err != nil { - // We logo the error, then schedule a full poll (rather than retrying here) - log.L(m.ctx).Errorf("Scheduling full poll due to error from core: %s", err) - m.requestFullScan() - return - } - // If the operation has been marked as success (by us or otherwise), or failed, then - // we can remove it. If we resolved it, then we would have cleared it up on the . - switch op.Status { - case core.OpStatusSucceeded, core.OpStatusFailed: - m.markCancelledIfTracked(nsOpID) - case core.OpStatusPending: - m.trackIfManaged(op) - } -} - -func (m *manager) readOperationPage(lastOp *core.Operation) ([]*core.Operation, error) { - var errorInfo fftypes.RESTError - var ops []*core.Operation - query := url.Values{ - "sort": []string{"created"}, - "type": m.opTypes, - "status": []string{string(core.OpStatusPending)}, - } - if lastOp != nil { - // For all but the 1st page, we use the last operation as the reference point. - // Extremely unlikely to get multiple ops withe same creation date, but not impossible - // so >= check, and removal of the duplicate at the end of the function. - query.Set("created", fmt.Sprintf(">=%d", lastOp.Created.UnixNano())) - query.Set("limit", strconv.FormatInt(m.fullScanPageSize+1, 10)) - } else { - query.Set("limit", strconv.FormatInt(m.fullScanPageSize, 10)) - } - res, err := m.ffCoreClient.R(). - SetQueryParamsFromValues(query). - SetResult(&ops). - SetError(&errorInfo). - Get("/spi/v1/operations") - if err != nil { - return nil, i18n.WrapError(m.ctx, err, tmmsgs.MsgCoreError, -1, err) - } - if res.IsError() { - return nil, i18n.NewError(m.ctx, tmmsgs.MsgCoreError, res.StatusCode(), errorInfo.Error) - } - if lastOp != nil && len(ops) > 0 && ops[0].ID.Equals(lastOp.ID) { - ops = ops[1:] - } - return ops, nil -} diff --git a/internal/manager/manager.go b/internal/manager/manager.go deleted file mode 100644 index 9bf6e98f..00000000 --- a/internal/manager/manager.go +++ /dev/null @@ -1,309 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package manager - -import ( - "context" - "encoding/json" - "fmt" - "sync" - "time" - - "github.com/go-resty/resty/v2" - "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/fftypes" - "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-common/pkg/wsclient" - "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" - "github.com/hyperledger/firefly-transaction-manager/internal/policyengines" - "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" - "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" - "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" - "github.com/hyperledger/firefly/pkg/core" -) - -type Manager interface { - Start() error - Stop() - WaitStop() error -} - -type manager struct { - ctx context.Context - cancelCtx func() - connectorAPI ffcapi.API - confirmations confirmations.Manager - policyEngine policyengine.PolicyEngine - apiServer httpserver.HTTPServer - ffCoreClient *resty.Client - wsClient wsclient.WSClient - - mux sync.Mutex - nextNonces map[string]uint64 - lockedNonces map[string]*lockedNonce - pendingOpsByID map[string]*pendingState - changeEventLoopDone chan struct{} - firstFullScanDone chan error - policyLoopDone chan struct{} - fullScanLoopDone chan struct{} - fullScanRequests chan bool - started bool - apiServerDone chan error - - name string - opTypes []string - startupScanMaxRetries int - fullScanPageSize int64 - fullScanMinDelay time.Duration - policyLoopInterval time.Duration - errorHistoryCount int - enableChangeListener bool -} - -func NewManager(ctx context.Context) (Manager, error) { - var err error - m := &manager{ - connectorAPI: ffcapi.NewFFCAPIClient(ctx, tmconfig.ConnectorPrefix, ffcapi.Variant(config.GetString(tmconfig.ConnectorVariant))), - ffCoreClient: ffresty.New(ctx, tmconfig.FFCorePrefix), - fullScanRequests: make(chan bool, 1), - nextNonces: make(map[string]uint64), - lockedNonces: make(map[string]*lockedNonce), - apiServerDone: make(chan error), - pendingOpsByID: make(map[string]*pendingState), - - name: config.GetString(tmconfig.ManagerName), - opTypes: config.GetStringSlice(tmconfig.OperationsTypes), - startupScanMaxRetries: config.GetInt(tmconfig.OperationsFullScanStartupMaxRetries), - fullScanPageSize: config.GetInt64(tmconfig.OperationsFullScanPageSize), - fullScanMinDelay: config.GetDuration(tmconfig.OperationsFullScanMinimumDelay), - policyLoopInterval: config.GetDuration(tmconfig.PolicyLoopInterval), - errorHistoryCount: config.GetInt(tmconfig.OperationsErrorHistoryCount), - enableChangeListener: config.GetBool(tmconfig.OperationsChangeListenerEnabled), - } - m.ctx, m.cancelCtx = context.WithCancel(ctx) - if m.name == "" { - return nil, i18n.NewError(ctx, tmmsgs.MsgConfigParamNotSet, tmconfig.ManagerName) - } - m.confirmations, err = confirmations.NewBlockConfirmationManager(ctx, m.connectorAPI) - if err != nil { - return nil, err - } - m.policyEngine, err = policyengines.NewPolicyEngine(ctx, tmconfig.PolicyEngineBasePrefix, config.GetString(tmconfig.PolicyEngineName)) - if err != nil { - return nil, err - } - wsconfig := wsclient.GenerateConfigFromPrefix(tmconfig.FFCorePrefix) - m.wsClient, err = wsclient.New(m.ctx, wsconfig, nil, m.startChangeListener) - if err != nil { - return nil, err - } - m.apiServer, err = httpserver.NewHTTPServer(ctx, "api", m.router(), m.apiServerDone, tmconfig.APIPrefix, tmconfig.CorsConfig) - if err != nil { - return nil, err - } - return m, nil -} - -type pendingState struct { - mtx *fftm.ManagedTXOutput - confirmed bool - removed bool - trackingTransactionHash string -} - -func (m *manager) requestFullScan() { - select { - case m.fullScanRequests <- true: - log.L(m.ctx).Debugf("Full scan of pending ops requested") - default: - log.L(m.ctx).Debugf("Full scan of pending ops already queued") - } -} - -func (m *manager) waitScanDelay(lastFullScan *fftypes.FFTime) { - scanDelay := m.fullScanMinDelay - time.Since(*lastFullScan.Time()) - log.L(m.ctx).Debugf("Delaying %dms before next full scan", scanDelay.Milliseconds()) - timer := time.NewTimer(scanDelay) - select { - case <-timer.C: - case <-m.ctx.Done(): - log.L(m.ctx).Infof("Full scan loop exiting waiting for retry") - return - } -} - -func (m *manager) fullScanLoop() { - defer close(m.fullScanLoopDone) - firstFullScanDone := m.firstFullScanDone - var lastFullScan *fftypes.FFTime - errorCount := 0 - for { - select { - case <-m.fullScanRequests: - if lastFullScan != nil { - m.waitScanDelay(lastFullScan) - } - lastFullScan = fftypes.Now() - err := m.fullScan() - if err != nil { - errorCount++ - if firstFullScanDone != nil && errorCount > m.startupScanMaxRetries { - firstFullScanDone <- err - return - } - log.L(m.ctx).Errorf("Full scan failed (will be retried) count=%d: %s", errorCount, err) - m.requestFullScan() - continue - } - errorCount = 0 - // On startup we need to know the first scan has completed to populate the nonces, - // before we complete startup - if firstFullScanDone != nil { - firstFullScanDone <- nil - firstFullScanDone = nil - } - case <-m.ctx.Done(): - log.L(m.ctx).Infof("Full scan loop exiting") - return - } - } -} - -func (m *manager) fullScan() error { - log.L(m.ctx).Debugf("Reading all operations after connect") - var page int64 - var read, added int - var lastOp *core.Operation - for { - ops, err := m.readOperationPage(lastOp) - if err != nil { - return err - } - if len(ops) == 0 { - log.L(m.ctx).Debugf("Finished reading all operations - %d read, %d added", read, added) - return nil - } - lastOp = ops[len(ops)-1] - read += len(ops) - for _, op := range ops { - added++ - m.trackIfManaged(op) - } - page++ - } -} - -func (m *manager) trackIfManaged(op *core.Operation) { - outputJSON := []byte(op.Output.String()) - var mtx fftm.ManagedTXOutput - err := json.Unmarshal(outputJSON, &mtx) - if err != nil { - log.L(m.ctx).Warnf("Failed to parse output from operation %s", err) - return - } - if mtx.FFTMName != m.name { - log.L(m.ctx).Debugf("Operation %s is not managed by us (fftm=%s)", op.ID, mtx.FFTMName) - return - } - if fmt.Sprintf("%s:%s", op.Namespace, op.ID) != mtx.ID { - log.L(m.ctx).Warnf("Operation %s contains an invalid ID %s in the output", op.ID, mtx.ID) - return - } - if mtx.Request == nil { - log.L(m.ctx).Warnf("Operation %s contains a nil request in the output", op.ID) - return - } - m.trackManaged(&mtx) -} - -func (m *manager) trackManaged(mtx *fftm.ManagedTXOutput) { - m.mux.Lock() - defer m.mux.Unlock() - _, existing := m.pendingOpsByID[mtx.ID] - if !existing { - nextNonce, ok := m.nextNonces[mtx.Request.From] - nonce := mtx.Nonce.Uint64() - if !ok || nextNonce <= nonce { - log.L(m.ctx).Debugf("Nonce %d in-flight. Next nonce: %d", nonce, nonce+1) - m.nextNonces[mtx.Request.From] = nonce + 1 - } - m.pendingOpsByID[mtx.ID] = &pendingState{ - mtx: mtx, - } - } -} - -func (m *manager) markCancelledIfTracked(nsOpID string) { - m.mux.Lock() - pending, existing := m.pendingOpsByID[nsOpID] - if existing { - pending.removed = true - } - m.mux.Unlock() - -} - -func (m *manager) Start() error { - m.fullScanRequests <- true - m.firstFullScanDone = make(chan error) - m.fullScanLoopDone = make(chan struct{}) - go m.fullScanLoop() - return m.waitForFirstScanAndStart() -} - -func (m *manager) waitForFirstScanAndStart() error { - log.L(m.ctx).Infof("Waiting for first full scan of operations to build state") - select { - case err := <-m.firstFullScanDone: - if err != nil { - return err - } - case <-m.ctx.Done(): - log.L(m.ctx).Infof("Cancelled before startup completed") - return nil - } - log.L(m.ctx).Infof("Scan complete. Completing startup") - m.policyLoopDone = make(chan struct{}) - go m.receiptPollingLoop() - go m.runAPIServer() - go m.confirmations.Start() - err := m.startWS() - if err == nil { - m.started = true - } - return err -} - -func (m *manager) Stop() { - m.cancelCtx() -} - -func (m *manager) WaitStop() (err error) { - if m.started { - m.started = false - err = <-m.apiServerDone - <-m.fullScanLoopDone - <-m.policyLoopDone - m.waitWSStop() - } - return err -} diff --git a/internal/manager/manager_test.go b/internal/manager/manager_test.go deleted file mode 100644 index a8dc3730..00000000 --- a/internal/manager/manager_test.go +++ /dev/null @@ -1,547 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package manager - -import ( - "context" - "encoding/json" - "fmt" - "net" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "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/fftypes" - "github.com/hyperledger/firefly-common/pkg/httpserver" - "github.com/hyperledger/firefly-transaction-manager/internal/policyengines" - "github.com/hyperledger/firefly-transaction-manager/internal/policyengines/simple" - "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" - "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" - "github.com/hyperledger/firefly/pkg/core" - "github.com/stretchr/testify/assert" -) - -const testManagerName = "unittest" - -func newTestManager(t *testing.T, cAPIHandler http.HandlerFunc, ffCoreHandler http.HandlerFunc, wsURL ...string) (string, *manager, func()) { - tmconfig.Reset() - policyengines.RegisterEngine(tmconfig.PolicyEngineBasePrefix, &simple.PolicyEngineFactory{}) - - cAPIServer := httptest.NewServer(cAPIHandler) - tmconfig.ConnectorPrefix.Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", cAPIServer.Listener.Addr())) - - ffCoreServer := httptest.NewServer(ffCoreHandler) - tmconfig.FFCorePrefix.Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", ffCoreServer.Listener.Addr())) - - ln, err := net.Listen("tcp", "127.0.0.1:0") - assert.NoError(t, err) - managerPort := strings.Split(ln.Addr().String(), ":")[1] - ln.Close() - tmconfig.APIPrefix.Set(httpserver.HTTPConfPort, managerPort) - tmconfig.APIPrefix.Set(httpserver.HTTPConfAddress, "127.0.0.1") - - config.Set(tmconfig.ManagerName, testManagerName) - config.Set(tmconfig.PolicyLoopInterval, "1ms") - tmconfig.PolicyEngineBasePrefix.SubPrefix("simple").Set(simple.FixedGasPrice, "223344556677") - - if len(wsURL) > 0 { - config.Set(tmconfig.OperationsChangeListenerEnabled, true) - tmconfig.FFCorePrefix.Set(ffresty.HTTPConfigURL, wsURL[0]) - } - - mm, err := NewManager(context.Background()) - assert.NoError(t, err) - m := mm.(*manager) - mcm := &confirmationsmocks.Manager{} - m.confirmations = mcm - mcm.On("Start").Return().Maybe() - - return fmt.Sprintf("http://127.0.0.1:%s", managerPort), - m, - func() { - cAPIServer.Close() - ffCoreServer.Close() - m.Stop() - _ = m.WaitStop() - } - -} - -func newTestOperation(t *testing.T, mtx *fftm.ManagedTXOutput, status core.OpStatus) *core.Operation { - b, err := json.Marshal(&mtx) - assert.NoError(t, err) - op := &core.Operation{ - Namespace: strings.Split(mtx.ID, ":")[0], - ID: fftypes.MustParseUUID(strings.Split(mtx.ID, ":")[1]), - Status: status, - } - err = json.Unmarshal(b, &op.Output) - assert.NoError(t, err) - return op -} - -func TestNewManagerMissingName(t *testing.T) { - - tmconfig.Reset() - config.Set(tmconfig.ManagerName, "") - - _, err := NewManager(context.Background()) - assert.Regexp(t, "FF21018", err) - -} - -func TestNewManagerBadHttpConfig(t *testing.T) { - - tmconfig.Reset() - config.Set(tmconfig.ManagerName, "test") - tmconfig.APIPrefix.Set(httpserver.HTTPConfAddress, "::::") - - policyengines.RegisterEngine(tmconfig.PolicyEngineBasePrefix, &simple.PolicyEngineFactory{}) - tmconfig.PolicyEngineBasePrefix.SubPrefix("simple").Set(simple.FixedGasPrice, "223344556677") - - _, err := NewManager(context.Background()) - assert.Regexp(t, "FF00151", err) - -} - -func TestNewManagerFireFlyURLConfig(t *testing.T) { - - tmconfig.Reset() - config.Set(tmconfig.ManagerName, "test") - tmconfig.FFCorePrefix.Set(ffresty.HTTPConfigURL, ":::!badurl") - - policyengines.RegisterEngine(tmconfig.PolicyEngineBasePrefix, &simple.PolicyEngineFactory{}) - tmconfig.PolicyEngineBasePrefix.SubPrefix("simple").Set(simple.FixedGasPrice, "223344556677") - - _, err := NewManager(context.Background()) - assert.Regexp(t, "FF00149", err) - -} - -func TestNewManagerBadConfirmationsCacheSize(t *testing.T) { - - tmconfig.Reset() - config.Set(tmconfig.ManagerName, "test") - config.Set(tmconfig.ConfirmationsBlockCacheSize, -1) - - _, err := NewManager(context.Background()) - assert.Regexp(t, "FF21015", err) - -} - -func TestNewManagerBadPolicyEngine(t *testing.T) { - - tmconfig.Reset() - config.Set(tmconfig.ManagerName, "test") - config.Set(tmconfig.PolicyEngineName, "wrong") - - _, err := NewManager(context.Background()) - assert.Regexp(t, "FF21019", err) - -} - -func TestChangeEventsNewBadOutput(t *testing.T) { - - ce := &core.ChangeEvent{ - ID: fftypes.NewUUID(), - Type: core.ChangeEventTypeUpdated, - Collection: "operations", - Namespace: "ns1", - } - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodGet, r.Method) - assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) - b, err := json.Marshal(&core.Operation{ - ID: ce.ID, - Status: core.OpStatusPending, - Output: fftypes.JSONObject{ - "id": "!not a UUID", - }, - }) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - w.Write(b) - }, - ) - defer cancel() - - m.handleEvent(ce) - assert.Empty(t, m.pendingOpsByID) - -} - -func TestChangeEventsWrongName(t *testing.T) { - - ce := &core.ChangeEvent{ - ID: fftypes.NewUUID(), - Type: core.ChangeEventTypeUpdated, - Collection: "operations", - Namespace: "ns1", - } - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodGet, r.Method) - assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) - b, err := json.Marshal(newTestOperation(t, &fftm.ManagedTXOutput{ - ID: "ns1:" + ce.ID.String(), - FFTMName: "wrong", - Request: &fftm.TransactionRequest{}, - }, core.OpStatusPending)) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - w.Write(b) - }, - ) - defer cancel() - - m.handleEvent(ce) - assert.Empty(t, m.pendingOpsByID) - -} - -func TestChangeEventsWrongID(t *testing.T) { - - ce := &core.ChangeEvent{ - ID: fftypes.NewUUID(), - Type: core.ChangeEventTypeUpdated, - Collection: "operations", - Namespace: "ns1", - } - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodGet, r.Method) - assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) - op := newTestOperation(t, &fftm.ManagedTXOutput{ - ID: "ns1:" + ce.ID.String(), - FFTMName: testManagerName, - Request: &fftm.TransactionRequest{}, - }, core.OpStatusPending) - op.ID = fftypes.NewUUID() - b, err := json.Marshal(&op) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - w.Write(b) - }, - ) - defer cancel() - - m.handleEvent(ce) - assert.Empty(t, m.pendingOpsByID) - -} - -func TestChangeEventsNilRequest(t *testing.T) { - - ce := &core.ChangeEvent{ - ID: fftypes.NewUUID(), - Type: core.ChangeEventTypeUpdated, - Collection: "operations", - Namespace: "ns1", - } - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodGet, r.Method) - assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) - op := newTestOperation(t, &fftm.ManagedTXOutput{ - ID: "ns1:" + ce.ID.String(), - FFTMName: testManagerName, - }, core.OpStatusPending) - b, err := json.Marshal(&op) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - w.Write(b) - }, - ) - defer cancel() - - m.handleEvent(ce) - assert.Empty(t, m.pendingOpsByID) - -} - -func TestChangeEventsQueryFail(t *testing.T) { - - ce := &core.ChangeEvent{ - ID: fftypes.NewUUID(), - Type: core.ChangeEventTypeUpdated, - Collection: "operations", - Namespace: "ns1", - } - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodGet, r.Method) - assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) - w.WriteHeader(404) - }, - ) - defer cancel() - - m.fullScanRequests = make(chan bool, 1) - - m.handleEvent(ce) - assert.Empty(t, m.pendingOpsByID) - - // Full scan should have been requested after this failure - <-m.fullScanRequests - -} - -func TestChangeEventsMarkForCleanup(t *testing.T) { - - ce := &core.ChangeEvent{ - ID: fftypes.NewUUID(), - Type: core.ChangeEventTypeUpdated, - Collection: "operations", - Namespace: "ns1", - } - - op := newTestOperation(t, &fftm.ManagedTXOutput{ - ID: "ns1:" + ce.ID.String(), - FFTMName: testManagerName, - Request: &fftm.TransactionRequest{}, - }, core.OpStatusFailed) - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodGet, r.Method) - assert.Equal(t, fmt.Sprintf("/spi/v1/operations/ns1:%s", ce.ID), r.URL.Path) - b, err := json.Marshal(&op) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - w.Write(b) - }, - ) - defer cancel() - - m.trackIfManaged(op) - m.queryAndAddPending(fmt.Sprintf("%s:%s", ce.Namespace, ce.ID)) - assert.True(t, m.pendingOpsByID[fmt.Sprintf("%s:%s", ce.Namespace, ce.ID)].removed) - -} - -func TestStartupScanMultiPageOK(t *testing.T) { - - op1 := newTestOperation(t, &fftm.ManagedTXOutput{ - ID: "ns1:" + fftypes.NewUUID().String(), - FFTMName: testManagerName, - Request: &fftm.TransactionRequest{}, - }, core.OpStatusPending) - t1 := fftypes.FFTime(time.Now().Add(-10 * time.Minute)) - op1.Created = &t1 - op2 := newTestOperation(t, &fftm.ManagedTXOutput{ - ID: "ns1:" + fftypes.NewUUID().String(), - FFTMName: testManagerName, - Request: &fftm.TransactionRequest{}, - }, core.OpStatusPending) - t2 := fftypes.FFTime(time.Now().Add(-5 * time.Minute)) - op2.Created = &t2 - op3 := newTestOperation(t, &fftm.ManagedTXOutput{ - ID: "ns1:" + fftypes.NewUUID().String(), - FFTMName: testManagerName, - Request: &fftm.TransactionRequest{}, - }, core.OpStatusPending) - t3 := fftypes.FFTime(time.Now().Add(-1 * time.Minute)) - op3.Created = &t3 - - call := 0 - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, http.MethodGet, r.Method) - assert.Equal(t, "/spi/v1/operations", r.URL.Path) - status := 200 - var res interface{} - switch call { - case 0: - res = &fftypes.RESTError{Error: "not ready yet"} - status = 500 - case 1: - res = []*core.Operation{op1, op2} - assert.Equal(t, "", r.URL.Query().Get("created")) - case 2: - res = []*core.Operation{op2 /* simulate overlap */, op3} - assert.Equal(t, fmt.Sprintf(">=%d", op2.Created.Time().UnixNano()), r.URL.Query().Get("created")) - case 3: - res = []*core.Operation{} - assert.Equal(t, fmt.Sprintf(">=%d", op3.Created.Time().UnixNano()), r.URL.Query().Get("created")) - default: - assert.Fail(t, "should have stopped after empty page") - } - call++ - b, err := json.Marshal(res) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - w.Write(b) - }, - ) - m.fullScanMinDelay = 1 * time.Microsecond - - m.fullScanRequests <- true - m.firstFullScanDone = make(chan error) - m.fullScanLoopDone = make(chan struct{}) - go m.fullScanLoop() - - <-m.firstFullScanDone - assert.Len(t, m.pendingOpsByID, 3) - - cancel() - <-m.fullScanLoopDone - -} - -func TestStartupScanFail(t *testing.T) { - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, - ) - cancel() // close servers - m.ctx = context.Background() - m.startupScanMaxRetries = 2 - m.fullScanMinDelay = 1 * time.Microsecond - - err := m.Start() - assert.Regexp(t, "FF21017", err) - -} - -func TestRequestFullScanNonBlocking(t *testing.T) { - - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, - ) - defer cancel() - - m.requestFullScan() - m.requestFullScan() - m.requestFullScan() - -} - -func TestRequestFullScanCancelledBeforeStart(t *testing.T) { - - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, - ) - defer cancel() - - m.cancelCtx() - m.waitForFirstScanAndStart() - -} - -func TestStartupCancelledDuringRetry(t *testing.T) { - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, - ) - cancel() // close servers - m.startupScanMaxRetries = 2 - m.fullScanMinDelay = 1 * time.Second - - m.waitScanDelay(fftypes.Now()) - -} - -func TestStartChangeEventListener(t *testing.T) { - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, - ) - defer cancel() - - m.wsClient.Close() - err := m.startChangeListener(m.ctx, m.wsClient) - assert.Regexp(t, "FF00147", err) - -} - -func TestAddErrorMessageMax(t *testing.T) { - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, - ) - defer cancel() - - m.errorHistoryCount = 2 - mtx := &fftm.ManagedTXOutput{} - m.addError(mtx, ffcapi.ErrorReasonTransactionUnderpriced, fmt.Errorf("snap")) - m.addError(mtx, ffcapi.ErrorReasonTransactionUnderpriced, fmt.Errorf("crackle")) - m.addError(mtx, ffcapi.ErrorReasonTransactionUnderpriced, fmt.Errorf("pop")) - assert.Len(t, mtx.ErrorHistory, 2) - assert.Equal(t, "pop", mtx.ErrorHistory[0].Error) - assert.Equal(t, "crackle", mtx.ErrorHistory[1].Error) - -} - -func TestUnparsableOperation(t *testing.T) { - - var m *manager - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, - ) - defer cancel() - - m.trackIfManaged(&core.Operation{ - Output: fftypes.JSONObject{ - "test": map[bool]bool{false: true}, - }, - }) - -} diff --git a/internal/manager/nonces_test.go b/internal/manager/nonces_test.go deleted file mode 100644 index 365ac88e..00000000 --- a/internal/manager/nonces_test.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package manager - -import ( - "context" - "net/http" - "testing" - "time" - - "github.com/hyperledger/firefly-common/pkg/ffcapi" - "github.com/hyperledger/firefly-common/pkg/fftypes" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" - "github.com/stretchr/testify/assert" -) - -func TestNonceCached(t *testing.T) { - - _, m, cancel := newTestManager(t, - testFFCAPIHandler(t, func(reqType ffcapi.RequestType, b []byte) (res interface{}, status int) { - return &ffcapi.GetNextNonceResponse{ - Nonce: fftypes.NewFFBigInt(1111), - }, 200 - }), - func(w http.ResponseWriter, r *http.Request) {}, - ) - defer cancel() - - locked1 := make(chan struct{}) - done1 := make(chan struct{}) - done2 := make(chan struct{}) - - go func() { - defer close(done1) - - ln, err := m.assignAndLockNonce(context.Background(), "ns1:"+fftypes.NewUUID().String(), "0x12345") - assert.NoError(t, err) - assert.Equal(t, uint64(1111), ln.nonce) - close(locked1) - - time.Sleep(1 * time.Millisecond) - ln.spent = &fftm.ManagedTXOutput{ - ID: "ns1:" + fftypes.NewUUID().String(), - Nonce: fftypes.NewFFBigInt(int64(ln.nonce)), - Request: &fftm.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x12345", - }, - }, - }, - } - ln.complete(context.Background()) - }() - - go func() { - defer close(done2) - - <-locked1 - ln, err := m.assignAndLockNonce(context.Background(), "ns2:"+fftypes.NewUUID().String(), "0x12345") - assert.NoError(t, err) - - assert.Equal(t, uint64(1112), ln.nonce) - - ln.complete(context.Background()) - - }() - - <-done1 - <-done2 - -} diff --git a/internal/manager/policyloop.go b/internal/manager/policyloop.go deleted file mode 100644 index 81f37bc0..00000000 --- a/internal/manager/policyloop.go +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package manager - -import ( - "time" - - "github.com/hyperledger/firefly-common/pkg/ffcapi" - "github.com/hyperledger/firefly-common/pkg/fftypes" - "github.com/hyperledger/firefly-common/pkg/log" - "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" - "github.com/hyperledger/firefly/pkg/core" -) - -func (m *manager) receiptPollingLoop() { - defer close(m.policyLoopDone) - - for { - timer := time.NewTimer(m.policyLoopInterval) - select { - case <-timer.C: - m.policyLoopCycle() - case <-m.ctx.Done(): - log.L(m.ctx).Infof("Receipt poller exiting") - return - } - } -} - -func (m *manager) policyLoopCycle() { - - // Grab the lock to build a list of things to check - m.mux.Lock() - allPending := make([]*pendingState, 0, len(m.pendingOpsByID)) - for _, pending := range m.pendingOpsByID { - allPending = append(allPending, pending) - } - m.mux.Unlock() - - // Go through trying to query all of them - for _, pending := range allPending { - err := m.execPolicy(pending) - if err != nil { - log.L(m.ctx).Errorf("Failed policy cycle transaction=%s operation=%s: %s", pending.mtx.TransactionHash, pending.mtx.ID, err) - } - } - -} - -func (m *manager) addError(mtx *fftm.ManagedTXOutput, reason ffcapi.ErrorReason, err error) { - newLen := len(mtx.ErrorHistory) + 1 - if newLen > m.errorHistoryCount { - newLen = m.errorHistoryCount - } - oldHistory := mtx.ErrorHistory - mtx.ErrorHistory = make([]*fftm.ManagedTXError, newLen) - mtx.ErrorHistory[0] = &fftm.ManagedTXError{ - Time: fftypes.Now(), - Mapped: reason, - Error: err.Error(), - } - for i := 1; i < newLen; i++ { - mtx.ErrorHistory[i] = oldHistory[i-1] - } -} - -// checkReceiptCycle runs against each pending item, on each cycle, and is the one place responsible -// for state updates - to avoid those happening in parallel. -func (m *manager) execPolicy(pending *pendingState) (err error) { - - updated := true - completed := false - newStatus := core.OpStatusPending - mtx := pending.mtx - switch { - case pending.confirmed: - updated = true - completed = true - if mtx.Receipt.Success { - newStatus = core.OpStatusSucceeded - } else { - newStatus = core.OpStatusFailed - } - case pending.removed: - // Remove from our state - m.removeIfTracked(mtx.ID) - default: - // Pass the state to the pluggable policy engine to potentially perform more actions against it, - // such as submitting for the first time, or raising the gas etc. - var reason ffcapi.ErrorReason - updated, reason, err = m.policyEngine.Execute(m.ctx, m.connectorAPI, pending.mtx) - if err != nil { - log.L(m.ctx).Errorf("Policy engine returned error for operation %s reason=%s: %s", mtx.ID, reason, err) - m.addError(mtx, reason, err) - } else if mtx.FirstSubmit != nil && pending.trackingTransactionHash != mtx.TransactionHash { - // If now submitted, add to confirmations manager for receipt checking - m.trackSubmittedTransaction(pending) - } - } - - if updated || err != nil { - errorString := "" - if err != nil { - // In the case of errors, we keep the record updated with the latest error - but leave it in Pending - errorString = err.Error() - } - err := m.writeManagedTX(m.ctx, mtx, newStatus, errorString) - if err != nil { - log.L(m.ctx).Errorf("Failed to update operation %s (status=%s): %s", mtx.ID, newStatus, err) - return err - } - if completed { - // We can remove it now - m.removeIfTracked(mtx.ID) - } - } - - return nil -} - -func (m *manager) trackSubmittedTransaction(pending *pendingState) { - var err error - - // Clear any old transaction hash - if pending.trackingTransactionHash != "" { - err = m.confirmations.Notify(&confirmations.Notification{ - NotificationType: confirmations.RemovedTransaction, - Transaction: &confirmations.TransactionInfo{ - TransactionHash: pending.trackingTransactionHash, - }, - }) - } - - // Notify of the new - if err == nil { - err = m.confirmations.Notify(&confirmations.Notification{ - NotificationType: confirmations.NewTransaction, - Transaction: &confirmations.TransactionInfo{ - TransactionHash: pending.mtx.TransactionHash, - Receipt: func(receipt *ffcapi.GetReceiptResponse) { - // Will be picked up on the next policy loop cycle - guaranteed to occur before Confirmed - m.mux.Lock() - pending.mtx.Receipt = receipt - m.mux.Unlock() - }, - Confirmed: func(confirmations []confirmations.BlockInfo) { - // Will be picked up on the next policy loop cycle - m.mux.Lock() - pending.confirmed = true - pending.mtx.Confirmations = confirmations - m.mux.Unlock() - }, - }, - }) - } - - // Only reason for error here should be a cancelled context - if err != nil { - log.L(m.ctx).Infof("Error detected notifying confirmation manager: %s", err) - } else { - pending.trackingTransactionHash = pending.mtx.TransactionHash - } -} - -func (m *manager) clearConfirmationTracking(mtx *fftm.ManagedTXOutput) { - // The only error condition on confirmations manager is if we are exiting, which it logs - _ = m.confirmations.Notify(&confirmations.Notification{ - NotificationType: confirmations.RemovedTransaction, - Transaction: &confirmations.TransactionInfo{ - TransactionHash: mtx.TransactionHash, - }, - }) -} - -func (m *manager) removeIfTracked(nsOpID string) { - m.mux.Lock() - pending, existing := m.pendingOpsByID[nsOpID] - if existing { - delete(m.pendingOpsByID, nsOpID) - } - m.mux.Unlock() - // Outside the lock tap the confirmation manager on the shoulder so it can clean up too - if existing { - m.clearConfirmationTracking(pending.mtx) - } -} diff --git a/internal/manager/policyloop_test.go b/internal/manager/policyloop_test.go deleted file mode 100644 index f756b5ec..00000000 --- a/internal/manager/policyloop_test.go +++ /dev/null @@ -1,335 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package manager - -import ( - "encoding/json" - "fmt" - "net/http" - "testing" - - "github.com/hyperledger/firefly-common/pkg/ffcapi" - "github.com/hyperledger/firefly-common/pkg/fftypes" - "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" - "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" - "github.com/hyperledger/firefly-transaction-manager/mocks/policyenginemocks" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" - "github.com/hyperledger/firefly/pkg/core" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -const ( - sampleTXHash = "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" - sampleTXHash2 = "0x64fd8179b80dd255d52ce60d7f265c0506be810e2f3df52463fadeb44bb4d2df" -) - -func TestPolicyLoopE2EOk(t *testing.T) { - - mtx := &fftm.ManagedTXOutput{ - ID: "ns1:" + fftypes.NewUUID().String(), - FirstSubmit: fftypes.Now(), - TransactionHash: sampleTXHash, - Request: &fftm.TransactionRequest{}, - } - - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) { - var op core.Operation - err := json.NewDecoder(r.Body).Decode(&op) - assert.NoError(t, err) - assert.Equal(t, core.OpStatusSucceeded, op.Status) - w.WriteHeader(200) - }, - ) - defer cancel() - - mc := m.confirmations.(*confirmationsmocks.Manager) - mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { - return n.NotificationType == confirmations.NewTransaction - })).Run(func(args mock.Arguments) { - n := args[0].(*confirmations.Notification) - n.Transaction.Receipt(&ffcapi.GetReceiptResponse{ - BlockNumber: fftypes.NewFFBigInt(12345), - TransactionIndex: fftypes.NewFFBigInt(10), - BlockHash: fftypes.NewRandB32().String(), - Success: true, - }) - n.Transaction.Confirmed([]confirmations.BlockInfo{}) - }).Return(nil) - mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { - return n.NotificationType == confirmations.RemovedTransaction - })).Return(nil) - - m.trackManaged(mtx) - m.policyLoopCycle() - - err := m.execPolicy(m.pendingOpsByID[mtx.ID]) - assert.NoError(t, err) - assert.Empty(t, m.pendingOpsByID) - - mc.AssertExpectations(t) -} - -func TestPolicyLoopE2EOkReverted(t *testing.T) { - - mtx := &fftm.ManagedTXOutput{ - ID: "ns1:" + fftypes.NewUUID().String(), - FirstSubmit: fftypes.Now(), - TransactionHash: sampleTXHash, - Request: &fftm.TransactionRequest{}, - } - - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) { - var op core.Operation - err := json.NewDecoder(r.Body).Decode(&op) - assert.NoError(t, err) - assert.Equal(t, core.OpStatusFailed, op.Status) - w.WriteHeader(200) - }, - ) - defer cancel() - - mc := m.confirmations.(*confirmationsmocks.Manager) - mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { - return n.NotificationType == confirmations.NewTransaction - })).Run(func(args mock.Arguments) { - n := args[0].(*confirmations.Notification) - n.Transaction.Receipt(&ffcapi.GetReceiptResponse{ - BlockNumber: fftypes.NewFFBigInt(12345), - TransactionIndex: fftypes.NewFFBigInt(10), - BlockHash: fftypes.NewRandB32().String(), - Success: false, - }) - n.Transaction.Confirmed([]confirmations.BlockInfo{}) - }).Return(nil) - mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { - return n.NotificationType == confirmations.RemovedTransaction - })).Return(nil) - - m.trackManaged(mtx) - m.policyLoopCycle() - - err := m.execPolicy(m.pendingOpsByID[mtx.ID]) - assert.NoError(t, err) - assert.Empty(t, m.pendingOpsByID) - - mc.AssertExpectations(t) -} - -func TestPolicyLoopUpdateFFCoreWithError(t *testing.T) { - - mtx := &fftm.ManagedTXOutput{ - ID: "ns1:" + fftypes.NewUUID().String(), - FirstSubmit: fftypes.Now(), - TransactionHash: sampleTXHash, - Request: &fftm.TransactionRequest{}, - } - - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) { - var op core.Operation - err := json.NewDecoder(r.Body).Decode(&op) - assert.NoError(t, err) - assert.Equal(t, core.OpStatusPending, op.Status) - w.WriteHeader(200) - }, - ) - defer cancel() - - m.policyEngine = &policyenginemocks.PolicyEngine{} - pc := m.policyEngine.(*policyenginemocks.PolicyEngine) - pc.On("Execute", mock.Anything, mock.Anything, mtx).Return(false, ffcapi.ErrorReason(""), fmt.Errorf("pop")) - - m.trackManaged(mtx) - m.policyLoopCycle() - - err := m.execPolicy(m.pendingOpsByID[mtx.ID]) - assert.NoError(t, err) - assert.NotEmpty(t, m.pendingOpsByID) -} - -func TestPolicyLoopUpdateOpFail(t *testing.T) { - - mtx := &fftm.ManagedTXOutput{ - ID: "ns1:" + fftypes.NewUUID().String(), - FirstSubmit: fftypes.Now(), - TransactionHash: sampleTXHash, - Request: &fftm.TransactionRequest{}, - } - - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) { - errRes := fftypes.RESTError{Error: "pop"} - b, err := json.Marshal(&errRes) - assert.NoError(t, err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(500) - w.Write(b) - }, - ) - defer cancel() - - mc := m.confirmations.(*confirmationsmocks.Manager) - mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { - return n.NotificationType == confirmations.NewTransaction - })).Run(func(args mock.Arguments) { - n := args[0].(*confirmations.Notification) - n.Transaction.Receipt(&ffcapi.GetReceiptResponse{ - BlockNumber: fftypes.NewFFBigInt(12345), - TransactionIndex: fftypes.NewFFBigInt(10), - BlockHash: fftypes.NewRandB32().String(), - Success: true, - }) - n.Transaction.Confirmed([]confirmations.BlockInfo{}) - }).Return(nil) - - m.trackManaged(mtx) - m.policyLoopCycle() - - err := m.execPolicy(m.pendingOpsByID[mtx.ID]) - assert.Regexp(t, "FF21017.*pop", err) - assert.NotEmpty(t, m.pendingOpsByID) - - mc.AssertExpectations(t) -} - -func TestPolicyLoopResubmitNewTXID(t *testing.T) { - - mtx := &fftm.ManagedTXOutput{ - ID: "ns1:" + fftypes.NewUUID().String(), - Request: &fftm.TransactionRequest{}, - } - - opUpdateCount := 0 - _, m, cancel := newTestManager(t, - testFFCAPIHandler(t, func(reqType ffcapi.RequestType, b []byte) (res interface{}, status int) { - switch reqType { - case ffcapi.RequestTypePrepareTransaction: - return &ffcapi.PrepareTransactionResponse{ - Gas: fftypes.NewFFBigInt(12345), - TransactionData: "0x12345", - }, 200 - case ffcapi.RequestTypeSendTransaction: - return &ffcapi.SendTransactionResponse{ - TransactionHash: sampleTXHash2, - }, 200 - default: - return nil, 500 - } - }), - func(w http.ResponseWriter, r *http.Request) { - var op core.Operation - err := json.NewDecoder(r.Body).Decode(&op) - assert.NoError(t, err) - opUpdateCount++ - if opUpdateCount == 1 { - assert.Equal(t, core.OpStatusPending, op.Status) - } else { - assert.Equal(t, core.OpStatusSucceeded, op.Status) - } - w.WriteHeader(200) - }, - ) - defer cancel() - - mc := m.confirmations.(*confirmationsmocks.Manager) - mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { - // First we get notified to remove the old TX hash - return n.NotificationType == confirmations.RemovedTransaction && - n.Transaction.TransactionHash == sampleTXHash - })).Return(nil) - mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { - // Then we get the new TX hash, which we confirm - return n.NotificationType == confirmations.NewTransaction && - n.Transaction.TransactionHash == sampleTXHash2 - })).Run(func(args mock.Arguments) { - n := args[0].(*confirmations.Notification) - n.Transaction.Receipt(&ffcapi.GetReceiptResponse{ - BlockNumber: fftypes.NewFFBigInt(12345), - TransactionIndex: fftypes.NewFFBigInt(10), - BlockHash: fftypes.NewRandB32().String(), - Success: true, - }) - n.Transaction.Confirmed([]confirmations.BlockInfo{}) - }).Return(nil) - mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { - // Then we're done - return n.NotificationType == confirmations.RemovedTransaction && - n.Transaction.TransactionHash == sampleTXHash2 - })).Return(nil) - - m.trackManaged(mtx) - pending := m.pendingOpsByID[mtx.ID] - pending.trackingTransactionHash = sampleTXHash - - m.policyLoopCycle() - - err := m.execPolicy(m.pendingOpsByID[mtx.ID]) - assert.NoError(t, err) - assert.Empty(t, m.pendingOpsByID) - - mc.AssertExpectations(t) -} - -func TestPolicyLoopCycleCleanupRemoved(t *testing.T) { - - mtx := &fftm.ManagedTXOutput{ - ID: "ns1:" + fftypes.NewUUID().String(), - Request: &fftm.TransactionRequest{}, - } - - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, - ) - defer cancel() - - mc := m.confirmations.(*confirmationsmocks.Manager) - mc.On("Notify", mock.Anything).Return(nil).Once() - - m.trackManaged(mtx) - m.markCancelledIfTracked(mtx.ID) - - err := m.execPolicy(m.pendingOpsByID[mtx.ID]) - assert.NoError(t, err) - assert.Empty(t, m.pendingOpsByID) -} - -func TestNotifyConfirmationMgrFail(t *testing.T) { - - _, m, cancel := newTestManager(t, - func(w http.ResponseWriter, r *http.Request) {}, - func(w http.ResponseWriter, r *http.Request) {}, - ) - defer cancel() - - mc := m.confirmations.(*confirmationsmocks.Manager) - mc.On("Notify", mock.Anything).Return(fmt.Errorf("pop")) - - m.trackSubmittedTransaction(&pendingState{ - mtx: &fftm.ManagedTXOutput{ - TransactionHash: sampleSendTX, - }, - }) - -} diff --git a/internal/manager/routes.go b/internal/manager/routes.go deleted file mode 100644 index bd54f1ff..00000000 --- a/internal/manager/routes.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package manager - -import ( - "context" - - "github.com/hyperledger/firefly-common/pkg/ffcapi" - "github.com/hyperledger/firefly-common/pkg/fftypes" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" - "github.com/hyperledger/firefly/pkg/core" -) - -func (m *manager) sendManagedTransaction(ctx context.Context, request *fftm.TransactionRequest) (*fftm.ManagedTXOutput, error) { - - // First job is to assign the next nonce to this request. - // We block any further sends on this nonce until we've got this one successfully into the node, or - // fail deterministically in a way that allows us to return it. - lockedNonce, err := m.assignAndLockNonce(ctx, request.Headers.ID, request.From) - if err != nil { - return nil, err - } - // We will call markSpent() once we reach the point the nonce has been used - defer lockedNonce.complete(ctx) - - // Prepare the transaction, which will mean we have a transaction that should be submittable. - // If we fail at this stage, we don't need to write any state as we are sure we haven't submitted - // anything to the blockchain itself. - prepared, _, err := m.connectorAPI.PrepareTransaction(ctx, &ffcapi.PrepareTransactionRequest{ - TransactionInput: request.TransactionInput, - }) - if err != nil { - return nil, err - } - - // Next we update FireFly core with the pre-submitted record pending record, with the allocated nonce. - // From this point on, we will guide this transaction through to submission. - // We return an "ack" at this point, and dispatch the work of getting the transaction submitted - // to the background worker. - mtx := &fftm.ManagedTXOutput{ - FFTMName: m.name, - ID: request.Headers.ID, // on input the request ID must be the namespaced operation ID - Nonce: fftypes.NewFFBigInt(int64(lockedNonce.nonce)), - Gas: prepared.Gas, - TransactionData: prepared.TransactionData, - Request: request, - } - if err := m.writeManagedTX(m.ctx, mtx, core.OpStatusPending, ""); err != nil { - return nil, err - } - - // Ok - we've spent it. The rest of the processing will be triggered off of lockedNonce - // completion adding this transaction to the pool (and/or the change event that comes in from - // FireFly core from the update to the transaction) - lockedNonce.spent = mtx - return mtx, nil -} diff --git a/internal/persistence/leveldb_persistence.go b/internal/persistence/leveldb_persistence.go new file mode 100644 index 00000000..1e832648 --- /dev/null +++ b/internal/persistence/leveldb_persistence.go @@ -0,0 +1,469 @@ +// 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 persistence + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "sync" + + "github.com/hyperledger/firefly-common/pkg/config" + "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-transaction-manager/internal/tmconfig" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/iterator" + "github.com/syndtr/goleveldb/leveldb/opt" + "github.com/syndtr/goleveldb/leveldb/util" +) + +type leveldbPersistence struct { + db *leveldb.DB + syncWrites bool + txMux sync.RWMutex // allows us to draw conclusions on the cleanup of indexes +} + +func NewLevelDBPersistence(ctx context.Context) (Persistence, error) { + dbPath := config.GetString(tmconfig.PersistenceLevelDBPath) + if dbPath == "" { + return nil, i18n.NewError(ctx, tmmsgs.MsgLevelDBPathMissing) + } + db, err := leveldb.OpenFile(dbPath, &opt.Options{ + OpenFilesCacheCapacity: config.GetInt(tmconfig.PersistenceLevelDBMaxHandles), + }) + if err != nil { + return nil, i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceInitFailed, dbPath) + } + return &leveldbPersistence{ + db: db, + syncWrites: config.GetBool(tmconfig.PersistenceLevelDBSyncWrites), + }, nil +} + +type SortDirection int + +const ( + SortDirectionAscending SortDirection = iota + SortDirectionDescending +) + +const checkpointsPrefix = "checkpoints_0/" +const eventstreamsPrefix = "eventstreams_0/" +const eventstreamsEnd = "eventstreams_1" +const listenersPrefix = "listeners_0/" +const listenersEnd = "listeners_1" +const transactionsPrefix = "tx_0/" +const nonceAllocationPrefix = "nonce_0/" +const txPendingIndexPrefix = "tx_inflight_0/" +const txPendingIndexEnd = "tx_inflight_1" +const txCreatedIndexPrefix = "tx_created_0/" +const txCreatedIndexEnd = "tx_created_1" + +func signerNoncePrefix(signer string) string { + return fmt.Sprintf("%s%s_0/", nonceAllocationPrefix, signer) +} + +func signerNonceEnd(signer string) string { + return fmt.Sprintf("%s%s_1", nonceAllocationPrefix, signer) +} + +func txNonceAllocationKey(signer string, nonce *fftypes.FFBigInt) []byte { + return []byte(fmt.Sprintf("%s%s_0/%.24d", nonceAllocationPrefix, signer, nonce.Int())) +} + +func txPendingIndexKey(sequenceID *fftypes.UUID) []byte { + return []byte(fmt.Sprintf("%s%s", txPendingIndexPrefix, sequenceID)) +} + +func txCreatedIndexKey(tx *apitypes.ManagedTX) []byte { + return []byte(fmt.Sprintf("%s%.19d/%s", txCreatedIndexPrefix, tx.Created.UnixNano(), tx.SequenceID)) +} + +func txDataKey(k string) []byte { + return []byte(fmt.Sprintf("%s%s", transactionsPrefix, k)) +} + +func prefixedKey(prefix string, id fmt.Stringer) []byte { + return []byte(fmt.Sprintf("%s%s", prefix, id)) +} + +func (p *leveldbPersistence) writeKeyValue(ctx context.Context, key, value []byte) error { + err := p.db.Put(key, value, &opt.WriteOptions{Sync: p.syncWrites}) + if err != nil { + return i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceWriteFailed) + } + return nil +} + +func (p *leveldbPersistence) writeJSON(ctx context.Context, key []byte, value interface{}) error { + b, err := json.Marshal(value) + if err != nil { + return i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceMarshalFailed) + } + log.L(ctx).Debugf("Wrote %s", key) + return p.writeKeyValue(ctx, key, b) +} + +func (p *leveldbPersistence) getKeyValue(ctx context.Context, key []byte) ([]byte, error) { + b, err := p.db.Get(key, &opt.ReadOptions{}) + if err != nil { + if err == leveldb.ErrNotFound { + return nil, nil + } + return nil, i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceReadFailed, key) + } + return b, err +} + +func (p *leveldbPersistence) readJSONByIndex(ctx context.Context, idxKey []byte, target interface{}) error { + valKey, err := p.getKeyValue(ctx, idxKey) + if err != nil || valKey == nil { + return err + } + return p.readJSON(ctx, valKey, target) +} + +func (p *leveldbPersistence) readJSON(ctx context.Context, key []byte, target interface{}) error { + b, err := p.getKeyValue(ctx, key) + if err != nil || b == nil { + return err + } + err = json.Unmarshal(b, target) + if err != nil { + return i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceUnmarshalFailed) + } + log.L(ctx).Debugf("Read %s", key) + return nil +} + +func (p *leveldbPersistence) listJSON(ctx context.Context, collectionPrefix, collectionEnd, after string, limit int, + dir SortDirection, + val func() interface{}, // return a pointer to a pointer variable, of the type to unmarshal + add func(interface{}), // passes back the val() for adding to the list, if the filters match + indexResolver func(ctx context.Context, k []byte) ([]byte, error), // if non-nil then the initial lookup will be passed to this, to lookup the target bytes. Nil skips item + filters ...func(interface{}) bool, // filters to apply to the val() after unmarshalling +) ([][]byte, error) { + collectionRange := &util.Range{ + Start: []byte(collectionPrefix), + Limit: []byte(collectionEnd), + } + var it iterator.Iterator + switch dir { + case SortDirectionAscending: + afterKey := collectionPrefix + after + if after != "" { + collectionRange.Start = []byte(afterKey) + } + it = p.db.NewIterator(collectionRange, &opt.ReadOptions{DontFillCache: true}) + if after != "" && it.Next() { + if !strings.HasPrefix(string(it.Key()), afterKey) { + it.Prev() // skip back, as the first key was already after the "after" key + } + } + default: + if after != "" { + collectionRange.Limit = []byte(collectionPrefix + after) // exclusive for limit, so no need to fiddle here + } + it = p.db.NewIterator(collectionRange, &opt.ReadOptions{DontFillCache: true}) + } + defer it.Release() + return p.iterateJSON(ctx, it, limit, dir, val, add, indexResolver, filters...) +} + +func (p *leveldbPersistence) iterateJSON(ctx context.Context, it iterator.Iterator, limit int, + dir SortDirection, val func() interface{}, add func(interface{}), indexResolver func(ctx context.Context, k []byte) ([]byte, error), filters ...func(interface{}) bool, +) (orphanedIdxKeys [][]byte, err error) { + count := 0 + next := it.Next // forwards we enter this function before the first key + if dir == SortDirectionDescending { + next = it.Last // reverse we enter this function + } +itLoop: + for next() { + if dir == SortDirectionDescending { + next = it.Prev + } else { + next = it.Next + } + v := val() + b := it.Value() + if indexResolver != nil { + valKey := b + b, err = indexResolver(ctx, valKey) + if err != nil { + return nil, err + } + if b == nil { + log.L(ctx).Warnf("Skipping orphaned index key '%s' pointing to '%s'", it.Key(), valKey) + orphanedIdxKeys = append(orphanedIdxKeys, it.Key()) + continue itLoop + } + } + err := json.Unmarshal(b, v) + if err != nil { + return nil, i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceUnmarshalFailed) + } + for _, f := range filters { + if !f(v) { + continue itLoop + } + } + add(v) + count++ + if limit > 0 && count >= limit { + break + } + } + log.L(ctx).Debugf("Listed %d items", count) + return orphanedIdxKeys, nil +} + +func (p *leveldbPersistence) deleteKeys(ctx context.Context, keys ...[]byte) error { + for _, key := range keys { + err := p.db.Delete(key, &opt.WriteOptions{Sync: p.syncWrites}) + if err != nil && err != leveldb.ErrNotFound { + return i18n.WrapError(ctx, err, tmmsgs.MsgPersistenceDeleteFailed) + } + log.L(ctx).Debugf("Deleted %s", key) + } + return nil +} + +func (p *leveldbPersistence) WriteCheckpoint(ctx context.Context, checkpoint *apitypes.EventStreamCheckpoint) error { + return p.writeJSON(ctx, prefixedKey(checkpointsPrefix, checkpoint.StreamID), checkpoint) +} + +func (p *leveldbPersistence) GetCheckpoint(ctx context.Context, streamID *fftypes.UUID) (cp *apitypes.EventStreamCheckpoint, err error) { + err = p.readJSON(ctx, prefixedKey(checkpointsPrefix, streamID), &cp) + return cp, err +} + +func (p *leveldbPersistence) DeleteCheckpoint(ctx context.Context, streamID *fftypes.UUID) error { + return p.deleteKeys(ctx, prefixedKey(checkpointsPrefix, streamID)) +} + +func (p *leveldbPersistence) ListStreams(ctx context.Context, after *fftypes.UUID, limit int, dir SortDirection) ([]*apitypes.EventStream, error) { + streams := make([]*apitypes.EventStream, 0) + if _, err := p.listJSON(ctx, eventstreamsPrefix, eventstreamsEnd, after.String(), limit, dir, + func() interface{} { var v *apitypes.EventStream; return &v }, + func(v interface{}) { streams = append(streams, *(v.(**apitypes.EventStream))) }, + nil, + ); err != nil { + return nil, err + } + return streams, nil +} + +func (p *leveldbPersistence) GetStream(ctx context.Context, streamID *fftypes.UUID) (es *apitypes.EventStream, err error) { + err = p.readJSON(ctx, prefixedKey(eventstreamsPrefix, streamID), &es) + return es, err +} + +func (p *leveldbPersistence) WriteStream(ctx context.Context, spec *apitypes.EventStream) error { + return p.writeJSON(ctx, prefixedKey(eventstreamsPrefix, spec.ID), spec) +} + +func (p *leveldbPersistence) DeleteStream(ctx context.Context, streamID *fftypes.UUID) error { + return p.deleteKeys(ctx, prefixedKey(eventstreamsPrefix, streamID)) +} + +func (p *leveldbPersistence) ListListeners(ctx context.Context, after *fftypes.UUID, limit int, dir SortDirection) ([]*apitypes.Listener, error) { + listeners := make([]*apitypes.Listener, 0) + if _, err := p.listJSON(ctx, listenersPrefix, listenersEnd, after.String(), limit, dir, + func() interface{} { var v *apitypes.Listener; return &v }, + func(v interface{}) { listeners = append(listeners, *(v.(**apitypes.Listener))) }, + nil, + ); err != nil { + return nil, err + } + return listeners, nil +} + +func (p *leveldbPersistence) ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, dir SortDirection, streamID *fftypes.UUID) ([]*apitypes.Listener, error) { + listeners := make([]*apitypes.Listener, 0) + if _, err := p.listJSON(ctx, listenersPrefix, listenersEnd, after.String(), limit, dir, + func() interface{} { var v *apitypes.Listener; return &v }, + func(v interface{}) { listeners = append(listeners, *(v.(**apitypes.Listener))) }, + nil, + func(v interface{}) bool { return (*(v.(**apitypes.Listener))).StreamID.Equals(streamID) }, + ); err != nil { + return nil, err + } + return listeners, nil +} + +func (p *leveldbPersistence) GetListener(ctx context.Context, listenerID *fftypes.UUID) (l *apitypes.Listener, err error) { + err = p.readJSON(ctx, prefixedKey(listenersPrefix, listenerID), &l) + return l, err +} + +func (p *leveldbPersistence) WriteListener(ctx context.Context, spec *apitypes.Listener) error { + return p.writeJSON(ctx, prefixedKey(listenersPrefix, spec.ID), spec) +} + +func (p *leveldbPersistence) DeleteListener(ctx context.Context, listenerID *fftypes.UUID) error { + return p.deleteKeys(ctx, prefixedKey(listenersPrefix, listenerID)) +} + +func (p *leveldbPersistence) indexLookupCallback(ctx context.Context, key []byte) ([]byte, error) { + b, err := p.getKeyValue(ctx, key) + switch { + case err != nil: + return nil, err + case b == nil: + return nil, nil + } + return b, err +} + +func (p *leveldbPersistence) cleanupOrphanedTXIdxKeys(ctx context.Context, orphanedIdxKeys [][]byte) { + p.txMux.Lock() + defer p.txMux.Unlock() + err := p.deleteKeys(ctx, orphanedIdxKeys...) + if err != nil { + log.L(ctx).Warnf("Failed to clean up orphaned index keys: %s", err) + } +} + +func (p *leveldbPersistence) listTransactionsByIndex(ctx context.Context, collectionPrefix, collectionEnd, afterStr string, limit int, dir SortDirection) ([]*apitypes.ManagedTX, error) { + + p.txMux.RLock() + transactions := make([]*apitypes.ManagedTX, 0) + orphanedIdxKeys, err := p.listJSON(ctx, collectionPrefix, collectionEnd, afterStr, limit, dir, + func() interface{} { var v *apitypes.ManagedTX; return &v }, + func(v interface{}) { transactions = append(transactions, *(v.(**apitypes.ManagedTX))) }, + p.indexLookupCallback, + ) + p.txMux.RUnlock() + if err != nil { + return nil, err + } + // If we find orphaned index keys we clean them up - which requires the write lock (hence dropping read-lock first) + if len(orphanedIdxKeys) > 0 { + p.cleanupOrphanedTXIdxKeys(ctx, orphanedIdxKeys) + } + return transactions, nil +} + +func (p *leveldbPersistence) ListTransactionsByCreateTime(ctx context.Context, after *apitypes.ManagedTX, limit int, dir SortDirection) ([]*apitypes.ManagedTX, error) { + afterStr := "" + if after != nil { + afterStr = fmt.Sprintf("%.19d/%s", after.Created.UnixNano(), after.SequenceID) + } + return p.listTransactionsByIndex(ctx, txCreatedIndexPrefix, txCreatedIndexEnd, afterStr, limit, dir) +} + +func (p *leveldbPersistence) ListTransactionsByNonce(ctx context.Context, signer string, after *fftypes.FFBigInt, limit int, dir SortDirection) ([]*apitypes.ManagedTX, error) { + afterStr := "" + if after != nil { + afterStr = fmt.Sprintf("%.24d", after.Int()) + } + return p.listTransactionsByIndex(ctx, signerNoncePrefix(signer), signerNonceEnd(signer), afterStr, limit, dir) +} + +func (p *leveldbPersistence) ListTransactionsPending(ctx context.Context, after *fftypes.UUID, limit int, dir SortDirection) ([]*apitypes.ManagedTX, error) { + return p.listTransactionsByIndex(ctx, txPendingIndexPrefix, txPendingIndexEnd, after.String(), limit, dir) +} + +func (p *leveldbPersistence) GetTransactionByID(ctx context.Context, txID string) (tx *apitypes.ManagedTX, err error) { + p.txMux.RLock() + defer p.txMux.RUnlock() + err = p.readJSON(ctx, txDataKey(txID), &tx) + return tx, err +} + +func (p *leveldbPersistence) GetTransactionByNonce(ctx context.Context, signer string, nonce *fftypes.FFBigInt) (tx *apitypes.ManagedTX, err error) { + p.txMux.RLock() + defer p.txMux.RUnlock() + err = p.readJSONByIndex(ctx, txNonceAllocationKey(signer, nonce), &tx) + return tx, err +} + +func (p *leveldbPersistence) WriteTransaction(ctx context.Context, tx *apitypes.ManagedTX, new bool) (err error) { + // We take a write-lock here, because we are writing multiple values (the indexes), and anybody + // attempting to read the critical nonce allocation index must know the difference between a partial write + // (we crashed before we completed all the writes) and an incomplete write that's in process. + // The reading code detects partial writes and cleans them up if it finds them. + p.txMux.Lock() + defer p.txMux.Unlock() + + if tx.TransactionHeaders.From == "" || + tx.Nonce == nil || + tx.SequenceID == nil || + tx.Created == nil || + tx.ID == "" || + tx.Status == "" { + return i18n.NewError(ctx, tmmsgs.MsgPersistenceTXIncomplete) + } + idKey := txDataKey(tx.ID) + if new { + // This must be a unique ID, otherwise we return a conflict. + // Note we use the final record we write at the end for the conflict check, and also that we're write locked here + if existing, err := p.getKeyValue(ctx, idKey); err != nil { + return err + } else if existing != nil { + return i18n.NewError(ctx, tmmsgs.MsgDuplicateID, idKey) + } + + // We write the index records first - because if we crash, we need to be able to know if the + // index records are valid or not. When reading under the read lock, if there is an index key + // that does not have a corresponding managed TX available, we will clean up the + // orphaned index (after swapping the read lock for the write lock) + // See listTransactionsByIndex() for the other half of this logic. + err = p.writeKeyValue(ctx, txCreatedIndexKey(tx), idKey) + if err == nil && tx.Status == apitypes.TxStatusPending { + err = p.writeKeyValue(ctx, txPendingIndexKey(tx.SequenceID), idKey) + } + if err == nil { + err = p.writeKeyValue(ctx, txNonceAllocationKey(tx.TransactionHeaders.From, tx.Nonce), idKey) + } + } + // If we are creating/updating a record that is not pending, we need to ensure there is no pending index associated with it + if err == nil && tx.Status != apitypes.TxStatusPending { + err = p.deleteKeys(ctx, txPendingIndexKey(tx.SequenceID)) + } + if err == nil { + err = p.writeJSON(ctx, idKey, tx) + } + return err +} + +func (p *leveldbPersistence) DeleteTransaction(ctx context.Context, txID string) error { + var tx *apitypes.ManagedTX + err := p.readJSON(ctx, txDataKey(txID), &tx) + if err != nil || tx == nil { + return err + } + return p.deleteKeys(ctx, + txDataKey(txID), + txCreatedIndexKey(tx), + txPendingIndexKey(tx.SequenceID), + txNonceAllocationKey(tx.TransactionHeaders.From, tx.Nonce), + ) +} + +func (p *leveldbPersistence) Close(ctx context.Context) { + err := p.db.Close() + if err != nil { + log.L(ctx).Warnf("Error closing leveldb: %s", err) + } +} diff --git a/internal/persistence/leveldb_persistence_test.go b/internal/persistence/leveldb_persistence_test.go new file mode 100644 index 00000000..61d5912d --- /dev/null +++ b/internal/persistence/leveldb_persistence_test.go @@ -0,0 +1,637 @@ +// 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 persistence + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "testing" + + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/syndtr/goleveldb/leveldb/opt" +) + +func newTestLevelDBPersistence(t *testing.T) (*leveldbPersistence, func()) { + + dir, err := ioutil.TempDir("", "ldb_*") + assert.NoError(t, err) + + tmconfig.Reset() + config.Set(tmconfig.PersistenceLevelDBPath, dir) + + pp, err := NewLevelDBPersistence(context.Background()) + assert.NoError(t, err) + + // Write some random stuff to the DB + p := pp.(*leveldbPersistence) + for i := 0; i < 26; i++ { + letter := (byte)('a' + i) + key := make([]byte, 10) + for i := range key { + key[i] = letter + } + err := p.db.Put(key, key, &opt.WriteOptions{}) + assert.NoError(t, err) + } + + return p, func() { + p.Close(context.Background()) + os.RemoveAll(dir) + } + +} + +func strPtr(s string) *string { return &s } + +type badJSONCheckpointType map[bool]bool + +func (cp *badJSONCheckpointType) LessThan(b ffcapi.EventListenerCheckpoint) bool { + return false +} + +func TestLevelDBInitMissingPath(t *testing.T) { + + tmconfig.Reset() + + _, err := NewLevelDBPersistence(context.Background()) + assert.Regexp(t, "FF21050", err) + +} + +func TestLevelDBInitFail(t *testing.T) { + file, err := ioutil.TempFile("", "ldb_*") + assert.NoError(t, err) + ioutil.WriteFile(file.Name(), []byte("not a leveldb"), 0777) + defer os.Remove(file.Name()) + + tmconfig.Reset() + config.Set(tmconfig.PersistenceLevelDBPath, file.Name()) + + _, err = NewLevelDBPersistence(context.Background()) + assert.Error(t, err) + +} + +func TestReadWriteStreams(t *testing.T) { + + p, done := newTestLevelDBPersistence(t) + defer done() + + ctx := context.Background() + s1 := &apitypes.EventStream{ + ID: apitypes.NewULID(), // ensure we get sequentially ascending IDs + Name: strPtr("stream1"), + } + p.WriteStream(ctx, s1) + s2 := &apitypes.EventStream{ + ID: apitypes.NewULID(), + Name: strPtr("stream2"), + } + p.WriteStream(ctx, s2) + s3 := &apitypes.EventStream{ + ID: apitypes.NewULID(), + Name: strPtr("stream3"), + } + p.WriteStream(ctx, s3) + + streams, err := p.ListStreams(ctx, nil, 0, SortDirectionDescending) + assert.NoError(t, err) + assert.Len(t, streams, 3) + + assert.Equal(t, s3.ID, streams[0].ID) + assert.Equal(t, s2.ID, streams[1].ID) + assert.Equal(t, s1.ID, streams[2].ID) + + // Test pagination + + streams, err = p.ListStreams(ctx, nil, 2, SortDirectionDescending) + assert.NoError(t, err) + assert.Len(t, streams, 2) + assert.Equal(t, s3.ID, streams[0].ID) + assert.Equal(t, s2.ID, streams[1].ID) + + streams, err = p.ListStreams(ctx, streams[1].ID, 2, SortDirectionDescending) + assert.NoError(t, err) + assert.Len(t, streams, 1) + assert.Equal(t, s1.ID, streams[0].ID) + + // Test delete + + err = p.DeleteStream(ctx, s2.ID) + assert.NoError(t, err) + streams, err = p.ListStreams(ctx, nil, 2, SortDirectionDescending) + assert.NoError(t, err) + assert.Len(t, streams, 2) + assert.Equal(t, s3.ID, streams[0].ID) + assert.Equal(t, s1.ID, streams[1].ID) + + // Test get direct + + s, err := p.GetStream(ctx, s3.ID) + assert.NoError(t, err) + assert.Equal(t, s3.ID, s.ID) + assert.Equal(t, s3.Name, s.Name) + + s, err = p.GetStream(ctx, s2.ID) + assert.NoError(t, err) + assert.Nil(t, s) +} + +func TestReadWriteListeners(t *testing.T) { + + p, done := newTestLevelDBPersistence(t) + defer done() + + ctx := context.Background() + + sID1 := apitypes.NewULID() + sID2 := apitypes.NewULID() + + s1l1 := &apitypes.Listener{ + ID: apitypes.NewULID(), + StreamID: sID1, + } + err := p.WriteListener(ctx, s1l1) + assert.NoError(t, err) + + s2l1 := &apitypes.Listener{ + ID: apitypes.NewULID(), + StreamID: sID2, + } + err = p.WriteListener(ctx, s2l1) + assert.NoError(t, err) + + s1l2 := &apitypes.Listener{ + ID: apitypes.NewULID(), + StreamID: sID1, + } + err = p.WriteListener(ctx, s1l2) + assert.NoError(t, err) + + listeners, err := p.ListListeners(ctx, nil, 0, SortDirectionDescending) + assert.NoError(t, err) + assert.Len(t, listeners, 3) + + assert.Equal(t, s1l2.ID, listeners[0].ID) + assert.Equal(t, s2l1.ID, listeners[1].ID) + assert.Equal(t, s1l1.ID, listeners[2].ID) + + // Test stream filter + + listeners, err = p.ListStreamListeners(ctx, nil, 0, SortDirectionDescending, sID1) + assert.NoError(t, err) + assert.Len(t, listeners, 2) + assert.Equal(t, s1l2.ID, listeners[0].ID) + assert.Equal(t, s1l1.ID, listeners[1].ID) + + // Test delete + + err = p.DeleteListener(ctx, s2l1.ID) + assert.NoError(t, err) + listeners, err = p.ListStreamListeners(ctx, nil, 0, SortDirectionDescending, sID2) + assert.NoError(t, err) + assert.Len(t, listeners, 0) + + // Test get direct + + l, err := p.GetListener(ctx, s1l2.ID) + assert.NoError(t, err) + assert.Equal(t, s1l2.ID, l.ID) + + l, err = p.GetListener(ctx, s2l1.ID) + assert.NoError(t, err) + assert.Nil(t, l) +} + +func TestReadWriteCheckpoints(t *testing.T) { + + p, done := newTestLevelDBPersistence(t) + defer done() + + ctx := context.Background() + cp1 := &apitypes.EventStreamCheckpoint{ + StreamID: apitypes.NewULID(), + } + cp2 := &apitypes.EventStreamCheckpoint{ + StreamID: apitypes.NewULID(), + } + + err := p.WriteCheckpoint(ctx, cp1) + assert.NoError(t, err) + + err = p.WriteCheckpoint(ctx, cp2) + assert.NoError(t, err) + + err = p.DeleteCheckpoint(ctx, cp1.StreamID) + assert.NoError(t, err) + + err = p.DeleteCheckpoint(ctx, cp1.StreamID) + assert.NoError(t, err) // No-op + + cp, err := p.GetCheckpoint(ctx, cp1.StreamID) + assert.NoError(t, err) + assert.Nil(t, cp) + + cp, err = p.GetCheckpoint(ctx, cp2.StreamID) + assert.NoError(t, err) + assert.Equal(t, cp2.StreamID, cp.StreamID) +} + +func newTestTX(signer string, nonce int64, status apitypes.TxStatus) *apitypes.ManagedTX { + return &apitypes.ManagedTX{ + ID: fmt.Sprintf("ns1/%s", fftypes.NewUUID()), + Created: fftypes.Now(), + TransactionHeaders: ffcapi.TransactionHeaders{ + From: signer, + }, + SequenceID: apitypes.NewULID(), + Nonce: fftypes.NewFFBigInt(nonce), + Status: status, + } +} + +func TestReadWriteManagedTransactions(t *testing.T) { + + p, done := newTestLevelDBPersistence(t) + defer done() + + ctx := context.Background() + submitNewTX := func(signer string, nonce int64, status apitypes.TxStatus) *apitypes.ManagedTX { + tx := newTestTX(signer, nonce, status) + err := p.WriteTransaction(ctx, tx, true) + assert.NoError(t, err) + return tx + } + + s1t1 := submitNewTX("0xaaaaa", 10001, apitypes.TxStatusSucceeded) + s2t1 := submitNewTX("0xbbbbb", 10001, apitypes.TxStatusFailed) + s1t2 := submitNewTX("0xaaaaa", 10002, apitypes.TxStatusPending) + s1t3 := submitNewTX("0xaaaaa", 10003, apitypes.TxStatusPending) + + // Check dup + err := p.WriteTransaction(ctx, s1t1, true) + assert.Regexp(t, "FF21065", err) + + txns, err := p.ListTransactionsByCreateTime(ctx, nil, 0, SortDirectionDescending) + assert.NoError(t, err) + assert.Len(t, txns, 4) + + assert.Equal(t, s1t3.ID, txns[0].ID) + assert.Equal(t, s1t2.ID, txns[1].ID) + assert.Equal(t, s2t1.ID, txns[2].ID) + assert.Equal(t, s1t1.ID, txns[3].ID) + + // Only list pending + + txns, err = p.ListTransactionsPending(ctx, nil, 0, SortDirectionDescending) + assert.NoError(t, err) + assert.Len(t, txns, 2) + + assert.Equal(t, s1t3.ID, txns[0].ID) + assert.Equal(t, s1t2.ID, txns[1].ID) + + // List with time range + + txns, err = p.ListTransactionsByCreateTime(ctx, s1t2, 0, SortDirectionDescending) + assert.NoError(t, err) + assert.Len(t, txns, 2) + assert.Equal(t, s2t1.ID, txns[0].ID) + assert.Equal(t, s1t1.ID, txns[1].ID) + + // Test delete, and querying by nonce to limit TX returned + + err = p.DeleteTransaction(ctx, s1t2.ID) + assert.NoError(t, err) + txns, err = p.ListTransactionsByNonce(ctx, "0xaaaaa", s1t1.Nonce, 0, SortDirectionAscending) + assert.NoError(t, err) + assert.Len(t, txns, 1) + assert.Equal(t, s1t3.ID, txns[0].ID) + + // Check we can use after with the deleted nonce, and not skip the one after + txns, err = p.ListTransactionsByNonce(ctx, "0xaaaaa", s1t2.Nonce, 0, SortDirectionAscending) + assert.NoError(t, err) + assert.Len(t, txns, 1) + assert.Equal(t, s1t3.ID, txns[0].ID) + + // Test get direct + + v, err := p.GetTransactionByID(ctx, s1t3.ID) + assert.NoError(t, err) + assert.Equal(t, s1t3.ID, v.ID) + assert.Equal(t, s1t3.Nonce, v.Nonce) + + v, err = p.GetTransactionByNonce(ctx, "0xbbbbb", s2t1.Nonce) + assert.NoError(t, err) + assert.Equal(t, s2t1.ID, v.ID) + assert.Equal(t, s2t1.Nonce, v.Nonce) + + v, err = p.GetTransactionByID(ctx, s1t2.ID) + assert.NoError(t, err) + assert.Nil(t, v) +} + +func TestListStreamsBadJSON(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + sID := apitypes.NewULID() + err := p.db.Put(prefixedKey(eventstreamsPrefix, sID), []byte("{! not json"), &opt.WriteOptions{}) + assert.NoError(t, err) + + _, err = p.ListStreams(context.Background(), nil, 0, SortDirectionDescending) + assert.Error(t, err) + +} + +func TestListListenersBadJSON(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + lID := apitypes.NewULID() + err := p.db.Put(prefixedKey(listenersPrefix, lID), []byte("{! not json"), &opt.WriteOptions{}) + assert.NoError(t, err) + + _, err = p.ListListeners(context.Background(), nil, 0, SortDirectionDescending) + assert.Error(t, err) + + _, err = p.ListStreamListeners(context.Background(), nil, 0, SortDirectionDescending, apitypes.NewULID()) + assert.Error(t, err) + +} + +func TestDeleteStreamFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + p.db.Close() + + err := p.DeleteStream(context.Background(), apitypes.NewULID()) + assert.Error(t, err) + +} + +func TestWriteTXFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + p.db.Close() + + tx := newTestTX("0x1234", 1000, apitypes.TxStatusPending) + + err := p.WriteTransaction(context.Background(), tx, true) + assert.Error(t, err) + +} + +func TestWriteCheckpointFailMarshal(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + p.db.Close() + + id1 := apitypes.NewULID() + err := p.WriteCheckpoint(context.Background(), &apitypes.EventStreamCheckpoint{ + Listeners: map[fftypes.UUID]json.RawMessage{ + *id1: json.RawMessage([]byte(`{"bad": "json"!`)), + }, + }) + assert.Error(t, err) + +} + +func TestWriteCheckpointFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + p.db.Close() + + id1 := apitypes.NewULID() + err := p.WriteCheckpoint(context.Background(), &apitypes.EventStreamCheckpoint{ + Listeners: map[fftypes.UUID]json.RawMessage{ + *id1: json.RawMessage([]byte(`{}`)), + }, + }) + assert.Error(t, err) + +} + +func TestReadListenerFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + p.db.Close() + + _, err := p.GetListener(context.Background(), apitypes.NewULID()) + assert.Error(t, err) + +} + +func TestReadCheckpointFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + sID := apitypes.NewULID() + err := p.db.Put(prefixedKey(checkpointsPrefix, sID), []byte("{! not json"), &opt.WriteOptions{}) + assert.NoError(t, err) + + _, err = p.GetCheckpoint(context.Background(), sID) + assert.Error(t, err) + +} + +func TestListManagedTransactionFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + tx := &apitypes.ManagedTX{ + ID: fmt.Sprintf("ns1:%s", apitypes.NewULID()), + Created: fftypes.Now(), + SequenceID: apitypes.NewULID(), + } + err := p.writeKeyValue(context.Background(), txCreatedIndexKey(tx), txDataKey(tx.ID)) + assert.NoError(t, err) + err = p.db.Put(txDataKey(tx.ID), []byte("{! not json"), &opt.WriteOptions{}) + assert.NoError(t, err) + + _, err = p.ListTransactionsByCreateTime(context.Background(), nil, 0, SortDirectionDescending) + assert.Error(t, err) + +} + +func TestListManagedTransactionCleanupOrphans(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + tx := &apitypes.ManagedTX{ + ID: fmt.Sprintf("ns1:%s", apitypes.NewULID()), + Created: fftypes.Now(), + SequenceID: apitypes.NewULID(), + } + err := p.writeKeyValue(context.Background(), txCreatedIndexKey(tx), txDataKey(tx.ID)) + assert.NoError(t, err) + + txns, err := p.ListTransactionsByCreateTime(context.Background(), nil, 0, SortDirectionDescending) + assert.NoError(t, err) + assert.Empty(t, txns) + + cleanedUpIndex, err := p.getKeyValue(context.Background(), txCreatedIndexKey(tx)) + assert.NoError(t, err) + assert.Nil(t, cleanedUpIndex) + +} + +func TestListNonceAllocationsFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + txID := fmt.Sprintf("ns1:%s", apitypes.NewULID()) + err := p.writeKeyValue(context.Background(), txNonceAllocationKey("0xaaa", fftypes.NewFFBigInt(12345)), txDataKey(txID)) + assert.NoError(t, err) + err = p.db.Put(txDataKey(txID), []byte("{! not json"), &opt.WriteOptions{}) + assert.NoError(t, err) + + _, err = p.ListTransactionsByNonce(context.Background(), "0xaaa", nil, 0, SortDirectionDescending) + assert.Error(t, err) + +} + +func TestListInflightTransactionFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + txID := fmt.Sprintf("ns1:%s", apitypes.NewULID()) + err := p.writeKeyValue(context.Background(), txPendingIndexKey(apitypes.NewULID()), txDataKey(txID)) + assert.NoError(t, err) + err = p.db.Put(txDataKey(txID), []byte("{! not json"), &opt.WriteOptions{}) + assert.NoError(t, err) + + _, err = p.ListTransactionsPending(context.Background(), nil, 0, SortDirectionDescending) + assert.Error(t, err) + +} + +func TestIndexLookupCallbackErr(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + p.Close(context.Background()) + + _, err := p.indexLookupCallback(context.Background(), ([]byte("any key"))) + assert.NotNil(t, err) + +} + +func TestIndexLookupCallbackNotFound(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + b, err := p.indexLookupCallback(context.Background(), ([]byte("any key"))) + assert.Nil(t, err) + assert.Nil(t, b) + +} + +func TestGetTransactionByNonceFail(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + p.Close(context.Background()) + + _, err := p.GetTransactionByNonce(context.Background(), "0xaaa", fftypes.NewFFBigInt(12345)) + assert.Regexp(t, "FF21055", err) + +} + +func TestIterateReverseJSONFailIdxResolve(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + err := p.writeKeyValue(context.Background(), []byte(`test_0/key`), []byte(`test/value`)) + assert.NoError(t, err) + _, err = p.listJSON(context.Background(), + "test_0/", + "test_1", + "", + 0, + SortDirectionAscending, + func() interface{} { return make(map[string]interface{}) }, + func(i interface{}) {}, + func(ctx context.Context, k []byte) ([]byte, error) { + return nil, fmt.Errorf("pop") + }, + ) + assert.Regexp(t, "pop", err) + +} + +func TestIterateReverseJSONSkipIdxResolve(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + err := p.writeKeyValue(context.Background(), []byte(`test_0/key`), []byte(`test/value`)) + assert.NoError(t, err) + orphans, err := p.listJSON(context.Background(), + "test_0/", + "test_1", + "", + 0, + SortDirectionAscending, + func() interface{} { return make(map[string]interface{}) }, + func(_ interface{}) { + assert.Fail(t, "Should not be called") + }, + func(ctx context.Context, k []byte) ([]byte, error) { + return nil, nil + }, + ) + assert.NoError(t, err) + assert.Len(t, orphans, 1) + +} + +func TestCleanupOrphanedTXIdxKeysSwallowError(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + p.Close(context.Background()) + + p.cleanupOrphanedTXIdxKeys(context.Background(), [][]byte{[]byte("test")}) + +} + +func TestWriteTransactionIncomplete(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + err := p.WriteTransaction(context.Background(), &apitypes.ManagedTX{}, true) + assert.Regexp(t, "FF21059", err) + +} + +func TestDeleteTransactionMissing(t *testing.T) { + p, done := newTestLevelDBPersistence(t) + defer done() + + err := p.DeleteTransaction(context.Background(), "missing") + assert.NoError(t, err) + +} diff --git a/internal/persistence/persistence.go b/internal/persistence/persistence.go new file mode 100644 index 00000000..6eb34819 --- /dev/null +++ b/internal/persistence/persistence.go @@ -0,0 +1,51 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package persistence + +import ( + "context" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +type Persistence interface { + WriteCheckpoint(ctx context.Context, checkpoint *apitypes.EventStreamCheckpoint) error + GetCheckpoint(ctx context.Context, streamID *fftypes.UUID) (*apitypes.EventStreamCheckpoint, error) + DeleteCheckpoint(ctx context.Context, streamID *fftypes.UUID) error + + ListStreams(ctx context.Context, after *fftypes.UUID, limit int, dir SortDirection) ([]*apitypes.EventStream, error) // reverse UUIDv1 order + GetStream(ctx context.Context, streamID *fftypes.UUID) (*apitypes.EventStream, error) + WriteStream(ctx context.Context, spec *apitypes.EventStream) error + DeleteStream(ctx context.Context, streamID *fftypes.UUID) error + + ListListeners(ctx context.Context, after *fftypes.UUID, limit int, dir SortDirection) ([]*apitypes.Listener, error) // reverse UUIDv1 order + ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, dir SortDirection, streamID *fftypes.UUID) ([]*apitypes.Listener, error) + GetListener(ctx context.Context, listenerID *fftypes.UUID) (*apitypes.Listener, error) + WriteListener(ctx context.Context, spec *apitypes.Listener) error + DeleteListener(ctx context.Context, listenerID *fftypes.UUID) error + + ListTransactionsByCreateTime(ctx context.Context, after *apitypes.ManagedTX, limit int, dir SortDirection) ([]*apitypes.ManagedTX, error) // reverse create time order + ListTransactionsByNonce(ctx context.Context, signer string, after *fftypes.FFBigInt, limit int, dir SortDirection) ([]*apitypes.ManagedTX, error) // reverse nonce order within signer + ListTransactionsPending(ctx context.Context, after *fftypes.UUID, limit int, dir SortDirection) ([]*apitypes.ManagedTX, error) // reverse UUIDv1 order, only those in pending state + GetTransactionByID(ctx context.Context, txID string) (*apitypes.ManagedTX, error) + GetTransactionByNonce(ctx context.Context, signer string, nonce *fftypes.FFBigInt) (*apitypes.ManagedTX, error) + WriteTransaction(ctx context.Context, tx *apitypes.ManagedTX, new bool) error // must reject if new is true, and the request ID is no + DeleteTransaction(ctx context.Context, txID string) error + + Close(ctx context.Context) +} diff --git a/internal/tmconfig/tmconfig.go b/internal/tmconfig/tmconfig.go index 48a25959..746df23f 100644 --- a/internal/tmconfig/tmconfig.go +++ b/internal/tmconfig/tmconfig.go @@ -20,93 +20,101 @@ import ( "github.com/hyperledger/firefly-common/pkg/config" "github.com/hyperledger/firefly-common/pkg/ffresty" "github.com/hyperledger/firefly-common/pkg/httpserver" - "github.com/hyperledger/firefly-common/pkg/wsclient" - "github.com/hyperledger/firefly/pkg/core" "github.com/spf13/viper" ) var ffc = config.AddRootKey var ( - // ManagerName is a name for this manager, that must be unique if there are multiple managers on this node - ManagerName = ffc("manager.name") - // ConnectorVariant is the variant setting to add to all requests to the backend connector - ConnectorVariant = ffc("connector.variant") - // ConfirmationsRequired is the number of confirmations required for a transaction to be considered final - ConfirmationsRequired = ffc("confirmations.required") - // ConfirmationsBlockCacheSize is the size of the block cache - ConfirmationsBlockCacheSize = ffc("confirmations.blockCacheSize") - // ConfirmationsBlockPollingInterval is the time between block polling - ConfirmationsBlockPollingInterval = ffc("confirmations.blockPollingInterval") - // ConfirmationsStaleReceiptTimeout the duration after which to force a receipt check for a pending transaction - ConfirmationsStaleReceiptTimeout = ffc("confirmations.staleReceiptTimeout") - // ConfirmationsNotificationQueueLength is the length of the internal queue to the block confirmations manager - ConfirmationsNotificationQueueLength = ffc("confirmations.notificationQueueLength") - // OperationsTypes the type of operations to monitor - only those that were submitted through the manager will have the required output format, so this is the superset - OperationsTypes = ffc("operations.types") - // OperationsFullScanStartupMaxRetries is the maximum times to try the scan on first startup, before failing startup - OperationsFullScanStartupMaxRetries = ffc("operations.fullScan.startupMaxRetries") - // OperationsPageSize page size for polling - OperationsFullScanPageSize = ffc("operations.fullScan.pageSize") - // OperationsFullScanMinimumDelay the minimum delay between full scan attempts - OperationsFullScanMinimumDelay = ffc("operations.fullScan.minimumDelay") - // OperationsErrorHistoryCount the number of errors to retain in the operation - OperationsErrorHistoryCount = ffc("operations.errorHistoryCount") - // OperationsChangeListenerEnabled whether to enable the operation change listener - OperationsChangeListenerEnabled = ffc("operations.changeListener.enabled") - // PolicyLoopInterval how often to go round the loop executing the policy engine against all pending transactions to make decisions - PolicyLoopInterval = ffc("policyloop.interval") - // PolicyEngineName the name of the policy engine to use - PolicyEngineName = ffc("policyengine.name") + ConfirmationsRequired = ffc("confirmations.required") + ConfirmationsBlockQueueLength = ffc("confirmations.blockQueueLength") + ConfirmationsStaleReceiptTimeout = ffc("confirmations.staleReceiptTimeout") + ConfirmationsNotificationQueueLength = ffc("confirmations.notificationQueueLength") + TransactionsErrorHistoryCount = ffc("transactions.errorHistoryCount") + TransactionsMaxInFlight = ffc("transactions.maxInFlight") + TransactionsNonceStateTimeout = ffc("transactions.nonceStateTimeout") + PolicyLoopInterval = ffc("policyloop.interval") + PolicyLoopRetryInitDelay = ffc("policyloop.retry.initialDelay") + PolicyLoopRetryMaxDelay = ffc("policyloop.retry.maxDelay") + PolicyLoopRetryFactor = ffc("policyloop.retry.factor") + PolicyEngineName = ffc("policyengine.name") + EventStreamsDefaultsBatchSize = ffc("eventstreams.defaults.batchSize") + EventStreamsDefaultsBatchTimeout = ffc("eventstreams.defaults.batchTimeout") + EventStreamsDefaultsErrorHandling = ffc("eventstreams.defaults.errorHandling") + EventStreamsDefaultsRetryTimeout = ffc("eventstreams.defaults.retryTimeout") + EventStreamsDefaultsBlockedRetryDelay = ffc("eventstreams.defaults.blockedRetryDelay") + EventStreamsDefaultsWebhookRequestTimeout = ffc("eventstreams.defaults.webhookRequestTimeout") + EventStreamsDefaultsWebsocketDistributionMode = ffc("eventstreams.defaults.websocketDistributionMode") + EventStreamsCheckpointInterval = ffc("eventstreams.checkpointInterval") + EventStreamsRetryInitDelay = ffc("eventstreams.retry.initialDelay") + EventStreamsRetryMaxDelay = ffc("eventstreams.retry.maxDelay") + EventStreamsRetryFactor = ffc("eventstreams.retry.factor") + WebhooksAllowPrivateIPs = ffc("webhooks.allowPrivateIPs") + PersistenceType = ffc("persistence.type") + PersistenceLevelDBPath = ffc("persistence.leveldb.path") + PersistenceLevelDBMaxHandles = ffc("persistence.leveldb.maxHandles") + PersistenceLevelDBSyncWrites = ffc("persistence.leveldb.syncWrites") + APIDefaultRequestTimeout = ffc("api.defaultRequestTimeout") + APIMaxRequestTimeout = ffc("api.maxRequestTimeout") ) -var ConnectorPrefix config.Prefix +var APIConfig config.Section -var FFCorePrefix config.Prefix +var CorsConfig config.Section -var APIPrefix config.Prefix +var PolicyEngineBaseConfig config.Section -var CorsConfig config.Prefix - -var PolicyEngineBasePrefix config.Prefix +var WebhookPrefix config.Section func setDefaults() { - viper.SetDefault(string(OperationsFullScanPageSize), 100) - viper.SetDefault(string(OperationsFullScanMinimumDelay), "5s") - viper.SetDefault(string(OperationsTypes), []string{ - core.OpTypeBlockchainInvoke.String(), - core.OpTypeBlockchainPinBatch.String(), - core.OpTypeTokenCreatePool.String(), - }) - viper.SetDefault(string(OperationsFullScanStartupMaxRetries), 10) - viper.SetDefault(string(ConnectorVariant), "evm") + viper.SetDefault(string(TransactionsMaxInFlight), 100) + viper.SetDefault(string(TransactionsErrorHistoryCount), 25) + viper.SetDefault(string(TransactionsNonceStateTimeout), "1h") viper.SetDefault(string(ConfirmationsRequired), 20) - viper.SetDefault(string(ConfirmationsBlockCacheSize), 1000) - viper.SetDefault(string(ConfirmationsBlockPollingInterval), "3s") + viper.SetDefault(string(ConfirmationsBlockQueueLength), 50) viper.SetDefault(string(ConfirmationsNotificationQueueLength), 50) viper.SetDefault(string(ConfirmationsStaleReceiptTimeout), "1m") - viper.SetDefault(string(OperationsErrorHistoryCount), 25) - viper.SetDefault(string(PolicyLoopInterval), "1s") + viper.SetDefault(string(PolicyLoopInterval), "10s") viper.SetDefault(string(PolicyEngineName), "simple") + + viper.SetDefault(string(EventStreamsDefaultsBatchSize), 50) + viper.SetDefault(string(EventStreamsDefaultsBatchTimeout), "5s") + viper.SetDefault(string(EventStreamsDefaultsErrorHandling), "block") + viper.SetDefault(string(EventStreamsDefaultsRetryTimeout), "30s") + viper.SetDefault(string(EventStreamsDefaultsBlockedRetryDelay), "30s") + viper.SetDefault(string(EventStreamsDefaultsWebhookRequestTimeout), "30s") + viper.SetDefault(string(EventStreamsDefaultsWebsocketDistributionMode), "load_balance") + viper.SetDefault(string(EventStreamsCheckpointInterval), "1m") + viper.SetDefault(string(WebhooksAllowPrivateIPs), true) + + viper.SetDefault(string(PersistenceType), "leveldb") + viper.SetDefault(string(PersistenceLevelDBMaxHandles), 100) + viper.SetDefault(string(PersistenceLevelDBSyncWrites), false) + + viper.SetDefault(string(APIDefaultRequestTimeout), "30s") + viper.SetDefault(string(APIMaxRequestTimeout), "10m") + + viper.SetDefault(string(PolicyLoopRetryInitDelay), "250ms") + viper.SetDefault(string(PolicyLoopRetryMaxDelay), "30s") + viper.SetDefault(string(PolicyLoopRetryFactor), 2.0) + viper.SetDefault(string(EventStreamsRetryInitDelay), "250ms") + viper.SetDefault(string(EventStreamsRetryMaxDelay), "30s") + viper.SetDefault(string(EventStreamsRetryFactor), 2.0) } func Reset() { config.RootConfigReset(setDefaults) - ConnectorPrefix = config.NewPluginConfig("connector") - ffresty.InitPrefix(ConnectorPrefix) - - FFCorePrefix = config.NewPluginConfig("ffcore") - wsclient.InitPrefix(FFCorePrefix) - FFCorePrefix.SetDefault(wsclient.WSConfigKeyPath, "/admin/ws") + APIConfig = config.RootSection("api") + httpserver.InitHTTPConfig(APIConfig, 5008) - APIPrefix = config.NewPluginConfig("api") - httpserver.InitHTTPConfPrefix(APIPrefix, 5008) - - CorsConfig = config.NewPluginConfig("cors") + CorsConfig = config.RootSection("cors") httpserver.InitCORSConfig(CorsConfig) - PolicyEngineBasePrefix = config.NewPluginConfig("policyengine") + WebhookPrefix = config.RootSection("webhooks") + ffresty.InitConfig(WebhookPrefix) + + PolicyEngineBaseConfig = config.RootSection("policyengine") // policy engines must be registered outside of this package } diff --git a/internal/tmconfig/tmconfig_test.go b/internal/tmconfig/tmconfig_test.go index 2919e982..3a2413e4 100644 --- a/internal/tmconfig/tmconfig_test.go +++ b/internal/tmconfig/tmconfig_test.go @@ -28,5 +28,5 @@ const configDir = "../../test/data/config" func TestInitConfigOK(t *testing.T) { Reset() - assert.Equal(t, 100, config.GetInt(OperationsFullScanPageSize)) + assert.Equal(t, 50, config.GetInt(EventStreamsDefaultsBatchSize)) } diff --git a/internal/tmmsgs/en_api_descriptions.go b/internal/tmmsgs/en_api_descriptions.go new file mode 100644 index 00000000..1070cac8 --- /dev/null +++ b/internal/tmmsgs/en_api_descriptions.go @@ -0,0 +1,60 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tmmsgs + +import ( + "github.com/hyperledger/firefly-common/pkg/i18n" + "golang.org/x/text/language" +) + +var ffm = func(key, translation string) i18n.MessageKey { + return i18n.FFM(language.AmericanEnglish, key, translation) +} + +//revive:disable +var ( + APIEndpointPostRoot = ffm("api.endpoints.post.root", "RPC/webhook style interface initiate a submit transactions, and execute queries") + APIEndpointPostRootQueryOutput = ffm("api.endpoints.post.root.query.output", "The data result of a query against a smart contract") + APIEndpointPostEventStream = ffm("api.endpoints.post.eventstreams", "Create a new event stream") + APIEndpointPatchEventStream = ffm("api.endpoints.patch.eventstreams", "Update an existing event stream") + APIEndpointPostEventStreamSuspend = ffm("api.endpoints.post.eventstream.suspend", "Suspend an event stream") + APIEndpointPostEventStreamResume = ffm("api.endpoints.post.eventstream.resume", "Resume an event stream") + APIEndpointGetEventStreams = ffm("api.endpoints.get.eventstreams", "List event streams") + APIEndpointGetEventStream = ffm("api.endpoints.get.eventstream", "Get an event stream with status") + APIEndpointDeleteEventStream = ffm("api.endpoints.delete.eventstream", "Delete an event stream") + APIEndpointGetSubscriptions = ffm("api.endpoints.get.subscriptions", "Get listeners - route deprecated in favor of /eventstreams/{streamId}/listeners") + APIEndpointGetSubscription = ffm("api.endpoints.get.subscription", "Get listener - route deprecated in favor of /eventstreams/{streamId}/listeners/{listenerId}") + APIEndpointPostSubscriptions = ffm("api.endpoints.post.subscriptions", "Create new listener - route deprecated in favor of /eventstreams/{streamId}/listeners") + APIEndpointPostSubscriptionReset = ffm("api.endpoints.post.subscription.reset", "Reset listener - route deprecated in favor of /eventstreams/{streamId}/listeners/{listenerId}/reset") + APIEndpointPatchSubscription = ffm("api.endpoints.patch.subscription", "Update listener - route deprecated in favor of /eventstreams/{streamId}/listeners/{listenerId}") + APIEndpointDeleteSubscription = ffm("api.endpoints.delete.subscription", "Delete listener - route deprecated in favor of /eventstreams/{streamId}/listeners/{listenerId}") + APIEndpointGetEventStreamListeners = ffm("api.endpoints.get.eventstream.listeners", "List event stream listeners") + APIEndpointGetEventStreamListener = ffm("api.endpoints.get.eventstream.listener", "Get event stream listener") + APIEndpointPostEventStreamListener = ffm("api.endpoints.post.eventstream.listener", "Create event stream listener") + APIEndpointPostEventStreamListenerReset = ffm("api.endpoints.post.eventstream.listener.reset", "Reset an event stream listener, to redeliver all events since the specified block") + APIEndpointPatchEventStreamListener = ffm("api.endpoints.patch.eventstream.listener", "Update event stream listener") + APIEndpointDeleteEventStreamListener = ffm("api.endpoints.delete.eventstream.listener", "Delete event stream listener") + + APIParamStreamID = ffm("api.params.streamId", "Event Stream ID") + APIParamListenerID = ffm("api.params.listenerId", "Listener ID") + APIParamTransactionID = ffm("api.params.transactionId", "Transaction ID") + APIParamLimit = ffm("api.params.limit", "Maximum number of entries to return") + APIParamAfter = ffm("api.params.after", "Return entries after this ID - for pagination (non-inclusive)") + APIParamTXSigner = ffm("api.params.txSigner", "Return only transactions for a specific signing address, in reverse nonce order") + APIParamTXPending = ffm("api.params.txPending", "Return only pending transactions, in reverse submission sequence (a 'sequenceId' is assigned to each transaction to determine its sequence") + APIParamSortDirection = ffm("api.params.sortDirection", "Sort direction: 'asc'/'ascending' or 'desc'/'descending'") +) diff --git a/internal/tmmsgs/en_config_descriptions.go b/internal/tmmsgs/en_config_descriptions.go index c9aab861..b5322076 100644 --- a/internal/tmmsgs/en_config_descriptions.go +++ b/internal/tmmsgs/en_config_descriptions.go @@ -16,51 +16,67 @@ package tmmsgs -import "github.com/hyperledger/firefly-common/pkg/i18n" +import ( + "github.com/hyperledger/firefly-common/pkg/i18n" + "golang.org/x/text/language" +) -var ffc = i18n.FFC +var ffc = func(key, translation, fieldType string) i18n.ConfigMessageKey { + return i18n.FFC(language.AmericanEnglish, key, translation, fieldType) +} //revive:disable var ( - ConfigAPIAddress = ffc("config.api.address", "Listener address for API", i18n.StringType) - ConfigAPIPort = ffc("config.api.port", "Listener port for API", i18n.IntType) - ConfigAPIPublicURL = ffc("config.api.publicURL", "External address callers should access API over", i18n.StringType) - ConfigAPIReadTimeout = ffc("config.api.readTimeout", "The maximum time to wait when reading from an HTTP connection", i18n.TimeDurationType) - ConfigAPIWriteTimeout = ffc("config.api.writeTimeout", "The maximum time to wait when writing to a HTTP connection", i18n.TimeDurationType) - ConfigAPIShutdownTimeout = ffc("config.api.shutdownTimeout", "The maximum amount of time to wait for any open HTTP requests to finish before shutting down the HTTP server", i18n.TimeDurationType) + ConfigAPIDefaultRequestTimeout = ffc("config.api.defaultRequestTimeout", "Default server-side request timeout for API calls", i18n.TimeDurationType) + ConfigAPIMaxRequestTimeout = ffc("config.api.maxRequestTimeout", "Maximum server-side request timeout a caller can request with a Request-Timeout header", i18n.TimeDurationType) + ConfigAPIAddress = ffc("config.api.address", "Listener address for API", i18n.StringType) + ConfigAPIPort = ffc("config.api.port", "Listener port for API", i18n.IntType) + ConfigAPIPublicURL = ffc("config.api.publicURL", "External address callers should access API over", i18n.StringType) + ConfigAPIReadTimeout = ffc("config.api.readTimeout", "The maximum time to wait when reading from an HTTP connection", i18n.TimeDurationType) + ConfigAPIWriteTimeout = ffc("config.api.writeTimeout", "The maximum time to wait when writing to a HTTP connection", i18n.TimeDurationType) + ConfigAPIShutdownTimeout = ffc("config.api.shutdownTimeout", "The maximum amount of time to wait for any open HTTP requests to finish before shutting down the HTTP server", i18n.TimeDurationType) ConfigConfirmationsBlockCacheSize = ffc("config.confirmations.blockCacheSize", "The maximum number of block headers to keep in the cache", i18n.IntType) - ConfigConfirmationsBlockPollingInterval = ffc("config.confirmations.blockPollingInterval", "How often to poll for new block headers", i18n.TimeDurationType) + ConfigConfirmationsBlockQueueLength = ffc("config.confirmations.blockQueueLength", "Internal queue length for notifying the confirmations manager of new blocks", i18n.IntType) ConfigConfirmationsNotificationsQueueLength = ffc("config.confirmations.notificationQueueLength", "Internal queue length for notifying the confirmations manager of new transactions/events", i18n.IntType) ConfigConfirmationsRequired = ffc("config.confirmations.required", "Number of confirmations required to consider a transaction/event final", i18n.IntType) ConfigConfirmationsStaleReceiptTimeout = ffc("config.confirmations.staleReceiptTimeout", "Duration after which to force a receipt check for a pending transaction", i18n.TimeDurationType) - ConfigConnectorURL = ffc("config.connector.url", "The URL of the blockchain connector", i18n.StringType) - ConfigConnectorVariant = ffc("config.connector.variant", "The variant is the overall category of blockchain connector, defining things like how input/output definitions are passed", i18n.StringType) - ConfigConnectorProxyURL = ffc("config.connector.proxy.url", "Optional HTTP proxy URL to use for the blockchain connector", i18n.StringType) - - ConfigFFCoreURL = ffc("config.ffcore.url", "The URL of the FireFly core admin API server to connect to", i18n.StringType) - ConfigFFCoreProxyURL = ffc("config.ffcore.proxy.url", "Optional HTTP proxy URL to use for the FireFly core admin API server", i18n.StringType) - - ConfigManagerName = ffc("config.manager.name", "The name of this Transaction Manager, used in operation metadata to track which operations are to be updated", i18n.StringType) - - ConfigOperationsTypes = ffc("config.operations.types", "The operation types to query in FireFly core, that might have been submitted via this Transaction Manager", "string[]") - ConfigOperationsFullScanMinimumDelay = ffc("config.operations.fullScan.minimumDelay", "The minimum delay between full scans of the FireFly core API, when reconnecting, or recovering from missed events / errors", i18n.TimeDurationType) - ConfigOperationsFullScanPageSize = ffc("config.operations.fullScan.pageSize", "The page size to use when performing a full scan of the ForeFly core API on startup, or recovery", i18n.IntType) - ConfigOperationsFullScanStartupMaxRetries = ffc("config.operations.fullScan.startupMaxRetries", "The page size to use when performing a full scan of the ForeFly core API on startup, or recovery", i18n.IntType) - ConfigOperationsErrorHistoryCount = ffc("config.operations.errorHistoryCount", "The number of historical errors to retain in the operation", i18n.IntType) - ConfigOperationsChangeListenerEnabled = ffc("config.operations.changeListener.enabled", "Whether to enable the change event listener to detect updates made to operations outside of the FFTM", i18n.BooleanType) + ConfigTransactionsErrorHistoryCount = ffc("config.transactions.errorHistoryCount", "The number of historical errors to retain in the operation", i18n.IntType) + ConfigTransactionsMaxInflight = ffc("config.transactions.maxInFlight", "The maximum number of transactions to have in-flight with the policy engine / blockchain transaction pool", i18n.IntType) + ConfigTransactionsNonceStateTimeout = ffc("config.transactions.nonceStateTimeout", "How old the most recently submitted transaction record in our local state needs to be, before we make a request to the node to query the next nonce for a signing address", i18n.TimeDurationType) ConfigPolicyEngineName = ffc("config.policyengine.name", "The name of the policy engine to use", i18n.StringType) ConfigLoopInterval = ffc("config.policyloop.interval", "Interval at which to invoke the policy engine to evaluate outstanding transactions", i18n.TimeDurationType) ConfigPolicyEngineSimpleFixedGasPrice = ffc("config.policyengine.simple.fixedGasPrice", "A fixed gasPrice value/structure to pass to the connector", "Raw JSON") - ConfigPolicyEngineSimpleWarnInterval = ffc("config.policyengine.simple.warnInterval", "The time between warnings when a blockchain transaction has not been allocated a receipt", i18n.TimeDurationType) + ConfigPolicyEngineSimpleResubmitInterval = ffc("config.policyengine.simple.resubmitInterval", "The time between warning and re-sending a transaction (same nonce) when a blockchain transaction has not been allocated a receipt", i18n.TimeDurationType) ConfigPolicyEngineSimpleGasOracleEnabled = ffc("config.policyengine.simple.gasOracle.mode", "The gas oracle mode", "connector | restapi | disabled") ConfigPolicyEngineSimpleGasOracleGoTemplate = ffc("config.policyengine.simple.gasOracle.template", "REST API Gas Oracle: A go template to execute against the result from the Gas Oracle, to create a JSON block that will be passed as the gas price to the connector", i18n.GoTemplateType) ConfigPolicyEngineSimpleGasOracleURL = ffc("config.policyengine.simple.gasOracle.url", "REST API Gas Oracle: The URL of a Gas Oracle REST API to call", i18n.StringType) ConfigPolicyEngineSimpleGasOracleProxyURL = ffc("config.policyengine.simple.gasOracle.proxy.url", "Optional HTTP proxy URL to use for the Gas Oracle REST API", i18n.StringType) ConfigPolicyEngineSimpleGasOracleMethod = ffc("config.policyengine.simple.gasOracle.method", "The HTTP Method to use when invoking the Gas Oracle REST API", i18n.StringType) ConfigPolicyEngineSimpleGasOracleQueryInterval = ffc("config.policyengine.simple.gasOracle.queryInterval", "The minimum interval between queries to the Gas Oracle", i18n.TimeDurationType) + + ConfigEventStreamsDefaultsBatchSize = ffc("config.eventstreams.defaults.batchSize", "Default batch size for newly created event streams", i18n.IntType) + ConfigEventStreamsDefaultsBatchTimeout = ffc("config.eventstreams.defaults.batchTimeout", "Default batch timeout for newly created event streams", i18n.TimeDurationType) + ConfigEventStreamsDefaultsErrorHandling = ffc("config.eventstreams.defaults.errorHandling", "Default error handling for newly created event streams", "'skip' or 'block'") + ConfigEventStreamsDefaultsRetryTimeout = ffc("config.eventstreams.defaults.retryTimeout", "Default retry timeout for newly created event streams", i18n.TimeDurationType) + ConfigEventStreamsDefaultsBlockedRetryDelay = ffc("config.eventstreams.defaults.blockedRetryDelay", "Default blocked retry delay for newly created event streams", i18n.TimeDurationType) + ConfigEventStreamsDefaultsWebhookRequestTimeout = ffc("config.eventstreams.defaults.webhookRequestTimeout", "Default WebHook request timeout for newly created event streams", i18n.TimeDurationType) + ConfigEventStreamsDefaultsWebsocketDistributionMode = ffc("config.eventstreams.defaults.websocketDistributionMode", "Default WebSocket distribution mode for newly created event streams", "'load_balance' or 'broadcast'") + ConfigEventStreamsCheckpointInterval = ffc("config.eventstreams.checkpointInterval", "Regular interval to write checkpoints for an event stream listener that is not actively detecting/delivering events", i18n.TimeDurationType) + ConfigEventStreamsRetryInitDelay = ffc("config.eventstreams.retry.initialDelay", "Initial retry delay", i18n.TimeDurationType) + ConfigEventStreamsRetryMaxDelay = ffc("config.eventstreams.retry.maxDelay", "Maximum delay between retries", i18n.TimeDurationType) + ConfigEventStreamsRetryFactor = ffc("config.eventstreams.retry.factor", "Factor to increase the delay by, between each retry", i18n.FloatType) + + ConfigPersistenceType = ffc("config.persistence.type", "The type of persistence to use", "Only 'leveldb' currently supported") + ConfigPersistenceLevelDBPath = ffc("config.persistence.leveldb.path", "The path for the LevelDB persistence directory", i18n.StringType) + ConfigPersistenceLevelDBMaxHandles = ffc("config.persistence.leveldb.maxHandles", "The maximum number of cached file handles LevelDB should keep open", i18n.IntType) + ConfigPersistenceLevelDBSyncWrites = ffc("config.persistence.leveldb.syncWrites", "Whether to synchronously perform writes to the storage", i18n.BooleanType) + + ConfigWebhooksAllowPrivateIPs = ffc("config.webhooks.allowPrivateIPs", "Whether to allow WebHook URLs that resolve to Private IP address ranges (vs. internet addresses)", i18n.BooleanType) + ConfigWebhooksURL = ffc("config.webhooks.url", "Unused (overridden by the WebHook configuration of an individual event stream)", i18n.IgnoredType) + ConfigWebhooksProxyURL = ffc("config.webhooks.proxy.url", "Optional HTTP proxy to use when invoking WebHooks", i18n.StringType) ) diff --git a/internal/tmmsgs/en_error_messges.go b/internal/tmmsgs/en_error_messges.go index 57b9426b..96e45597 100644 --- a/internal/tmmsgs/en_error_messges.go +++ b/internal/tmmsgs/en_error_messges.go @@ -16,25 +16,70 @@ package tmmsgs -import "github.com/hyperledger/firefly-common/pkg/i18n" +import ( + "net/http" -var ffe = i18n.FFE + "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 ( MsgInvalidOutputType = ffe("FF21010", "Invalid output type: %s") MsgConnectorError = ffe("FF21012", "Connector failed request. requestId=%s reason=%s error: %s") - MsgConnectorInvalidConentType = ffe("FF21013", "Connector failed request. requestId=%s invalid response content type: %s") - MsgCacheInitFail = ffe("FF21015", "Failed to initialize cache") + MsgConnectorInvalidContentType = ffe("FF21013", "Connector failed request. requestId=%s invalid response content type: %s") MsgInvalidConfirmationRequest = ffe("FF21016", "Invalid confirmation request %+v") MsgCoreError = ffe("FF21017", "Error from core status=%d: %s") MsgConfigParamNotSet = ffe("FF21018", "Configuration parameter '%s' must be set") MsgPolicyEngineNotRegistered = ffe("FF21019", "No policy engine registered with name '%s'") MsgNoGasConfigSetForPolicyEngine = ffe("FF21020", "A fixed gas price must be set when not using a gas oracle") MsgErrorQueryingGasOracleAPI = ffe("FF21021", "Error from gas station API [%d]: %s") - MsgErrorInvalidRequest = ffe("FF21022", "Invalid request") - MsgUnsupportedRequestType = ffe("FF21023", "Unsupported request type: %s") + MsgInvalidRequestErr = ffe("FF21022", "Invalid '%s' request: %s", http.StatusBadRequest) + MsgUnsupportedRequestType = ffe("FF21023", "Unsupported request type: %s", http.StatusBadRequest) MsgMissingGOTemplate = ffe("FF21024", "Missing template for processing response from Gas Oracle REST API") MsgBadGOTemplate = ffe("FF21025", "Invalid Go template: %s") MsgGasOracleResultError = ffe("FF21026", "Error processing result from gas station API via template") + MsgStreamStateError = ffe("FF21027", "Event stream is in %s state", http.StatusConflict) + MsgMissingName = ffe("FF21028", "Name is required", http.StatusBadRequest) + MsgInvalidStreamType = ffe("FF21029", "Invalid event stream type '%s'", http.StatusBadRequest) + MsgMissingWebhookURL = ffe("FF21030", "'url' is required for webhook configuration", http.StatusBadRequest) + MsgStopFailedUpdatingESConfig = ffe("FF21031", "Failed to stop event stream to apply updated configuration: %s") + MsgStartFailedUpdatingESConfig = ffe("FF21032", "Failed to restart event stream while applying updated configuration: %s") + MsgBlockWebhookAddress = ffe("FF21033", "Cannot send Webhook POST to address '%s' for host '%s'") + MsgInvalidDistributionMode = ffe("FF21034", "Invalid distribution mode for WebSocket: %s", http.StatusBadRequest) + MsgWebhookFailedStatus = ffe("FF21035", "Webhook request failed with status %d") + MsgWSErrorFromClient = ffe("FF21036", "Error received from WebSocket client: %s") + MsgWebSocketClosed = ffe("FF21037", "WebSocket '%s' closed") + MsgWebSocketInterruptedSend = ffe("FF21038", "Interrupted waiting for WebSocket connection to send event") + MsgWebSocketInterruptedReceive = ffe("FF21039", "Interrupted waiting for WebSocket acknowledgment") + MsgBadListenerOptions = ffe("FF21040", "Invalid listener options: %s", http.StatusBadRequest) + MsgInvalidHost = ffe("FF21041", "Cannot send Webhook POST to host '%s': %s") + MsgWebhookErr = ffe("FF21042", "Webhook request failed: %s") + MsgUnknownPersistence = ffe("FF21043", "Unknown persistence type '%s'") + MsgInvalidLimit = ffe("FF21044", "Invalid limit string '%s': %s") + MsgStreamNotFound = ffe("FF21045", "Event stream '%v' not found", http.StatusNotFound) + MsgListenerNotFound = ffe("FF21046", "Event listener '%v' not found", http.StatusNotFound) + MsgDuplicateStreamName = ffe("FF21047", "Duplicate event stream name '%s' used by stream '%s'", http.StatusConflict) + MsgMissingID = ffe("FF21048", "ID is required", http.StatusBadRequest) + MsgPersistenceInitFail = ffe("FF21049", "Failed to initialize '%s' persistence: %s") + MsgLevelDBPathMissing = ffe("FF21050", "Path must be supplied for LevelDB persistence") + MsgFilterUpdateNotAllowed = ffe("FF21051", "Event filters cannot be updated after a listener is created. Previous signature: '%s'. New signature: '%s'") + MsgResetStreamNotFound = ffe("FF21052", "Attempt to reset listener '%s', which is not currently registered on stream '%s'", http.StatusNotFound) + MsgPersistenceMarshalFailed = ffe("FF21053", "JSON serialization failed while writing to persistence") + MsgPersistenceUnmarshalFailed = ffe("FF21054", "JSON parsing failed while reading from persistence") + MsgPersistenceReadFailed = ffe("FF21055", "Failed to read key '%s' from persistence") + MsgPersistenceWriteFailed = ffe("FF21056", "Failed to read key '%s' from persistence") + MsgPersistenceDeleteFailed = ffe("FF21057", "Failed to delete key '%s' from persistence") + MsgPersistenceInitFailed = ffe("FF21058", "Failed to initialize persistence at path '%s'") + MsgPersistenceTXIncomplete = ffe("FF21059", "Transaction is missing indexed fields") + MsgNotStarted = ffe("FF21060", "Connector has not fully started yet", http.StatusServiceUnavailable) + MsgPaginationErrTxNotFound = ffe("FF21062", "The ID specified in the 'after' option (for pagination) must match an existing transaction: '%s'", http.StatusNotFound) + MsgTXConflictSignerPending = ffe("FF21063", "Only one of 'signer' and 'pending' can be supplied when querying transactions", http.StatusBadRequest) + MsgInvalidSortDirection = ffe("FF21064", "Sort direction must be 'asc'/'ascending' or 'desc'/'descending': '%s'", http.StatusBadRequest) + MsgDuplicateID = ffe("FF21065", "ID '%s' is not unique", http.StatusConflict) + MsgTransactionFailed = ffe("FF21066", "Transaction execution failed") ) diff --git a/internal/ws/wsconn.go b/internal/ws/wsconn.go new file mode 100644 index 00000000..2968b4a0 --- /dev/null +++ b/internal/ws/wsconn.go @@ -0,0 +1,181 @@ +// 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 ws + +import ( + "context" + "reflect" + "strings" + "sync" + + ws "github.com/gorilla/websocket" + "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-transaction-manager/internal/tmmsgs" +) + +type webSocketConnection struct { + ctx context.Context + id string + server *webSocketServer + conn *ws.Conn + mux sync.Mutex + closed bool + topics map[string]*webSocketTopic + broadcast chan interface{} + newTopic chan bool + receive chan error + closing chan struct{} +} + +type webSocketCommandMessage struct { + Type string `json:"type,omitempty"` + Topic string `json:"topic,omitempty"` // synonym for "topic" - from a time when we let you configure the topic separate to the stream name + Stream string `json:"stream,omitempty"` // name of the event stream + Message string `json:"message,omitempty"` +} + +func newConnection(bgCtx context.Context, server *webSocketServer, conn *ws.Conn) *webSocketConnection { + id := fftypes.NewUUID().String() + wsc := &webSocketConnection{ + ctx: log.WithLogField(bgCtx, "wsc", id), + id: id, + server: server, + conn: conn, + newTopic: make(chan bool), + topics: make(map[string]*webSocketTopic), + broadcast: make(chan interface{}), + receive: make(chan error), + closing: make(chan struct{}), + } + go wsc.listen() + go wsc.sender() + return wsc +} + +func (c *webSocketConnection) close() { + c.mux.Lock() + if !c.closed { + c.closed = true + c.conn.Close() + close(c.closing) + } + c.mux.Unlock() + + for _, t := range c.topics { + c.server.cycleTopic(c.id, t) + log.L(c.ctx).Infof("Websocket closed while active on topic '%s'", t.topic) + } + c.server.connectionClosed(c) + log.L(c.ctx).Infof("Disconnected") +} + +func (c *webSocketConnection) sender() { + defer c.close() + buildCases := func() []reflect.SelectCase { + c.mux.Lock() + defer c.mux.Unlock() + cases := make([]reflect.SelectCase, len(c.topics)+3) + i := 0 + for _, t := range c.topics { + cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(t.senderChannel)} + i++ + } + cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(c.broadcast)} + i++ + cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(c.closing)} + i++ + cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(c.newTopic)} + return cases + } + cases := buildCases() + for { + chosen, value, ok := reflect.Select(cases) + if !ok { + log.L(c.ctx).Infof("Closing") + return + } + + if chosen == len(cases)-1 { + // Addition of a new topic + cases = buildCases() + } else { + // Message from one of the existing topics + _ = c.conn.WriteJSON(value.Interface()) + } + } +} + +func (c *webSocketConnection) listenTopic(t *webSocketTopic) { + c.mux.Lock() + c.topics[t.topic] = t + c.server.ListenOnTopic(c, t.topic) + c.mux.Unlock() + select { + case c.newTopic <- true: + case <-c.closing: + } +} + +func (c *webSocketConnection) listenReplies() { + c.server.ListenForReplies(c) +} + +func (c *webSocketConnection) listen() { + defer c.close() + log.L(c.ctx).Infof("Connected") + for { + var msg webSocketCommandMessage + err := c.conn.ReadJSON(&msg) + if err != nil { + log.L(c.ctx).Errorf("Error: %s", err) + return + } + log.L(c.ctx).Debugf("Received: %+v", msg) + + topic := msg.Stream + if topic == "" { + topic = msg.Topic + } + t := c.server.getTopic(topic) + switch strings.ToLower(msg.Type) { + case "listen": + c.listenTopic(t) + case "listenreplies": + c.listenReplies() + case "ack": + c.handleAckOrError(t, nil) + case "error": + c.handleAckOrError(t, i18n.NewError(c.ctx, tmmsgs.MsgWSErrorFromClient, msg.Message)) + default: + log.L(c.ctx).Errorf("Unexpected message type: %+v", msg) + } + } +} + +func (c *webSocketConnection) handleAckOrError(t *webSocketTopic, err error) { + isError := err != nil + select { + case t.receiverChannel <- err: + log.L(c.ctx).Debugf("response (error='%t') on topic '%s' passed on for processing", isError, t.topic) + break + default: + log.L(c.ctx).Debugf("spurious ack received (error='%t') on topic '%s'", isError, t.topic) + break + } +} diff --git a/internal/ws/wsserver.go b/internal/ws/wsserver.go new file mode 100644 index 00000000..0201ae2d --- /dev/null +++ b/internal/ws/wsserver.go @@ -0,0 +1,229 @@ +// 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 ws + +import ( + "context" + "net/http" + "reflect" + "sync" + "time" + + "github.com/gorilla/websocket" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" +) + +// WebSocketChannels is provided to allow us to do a blocking send to a namespace that will complete once a client connects on it +// We also provide a channel to listen on for closing of the connection, to allow a select to wake on a blocking send +type WebSocketChannels interface { + GetChannels(topic string) (senderChannel chan<- interface{}, broadcastChannel chan<- interface{}, receiverChannel <-chan error) + SendReply(message interface{}) +} + +// WebSocketServer is the full server interface with the init call +type WebSocketServer interface { + WebSocketChannels + Handler(w http.ResponseWriter, r *http.Request) + Close() +} + +type webSocketServer struct { + ctx context.Context + processingTimeout time.Duration + mux sync.Mutex + topics map[string]*webSocketTopic + topicMap map[string]map[string]*webSocketConnection + replyMap map[string]*webSocketConnection + newTopic chan bool + replyChannel chan interface{} + upgrader *websocket.Upgrader + connections map[string]*webSocketConnection +} + +type webSocketTopic struct { + topic string + senderChannel chan interface{} + broadcastChannel chan interface{} + receiverChannel chan error +} + +// NewWebSocketServer create a new server with a simplified interface +func NewWebSocketServer(bgCtx context.Context) WebSocketServer { + s := &webSocketServer{ + ctx: bgCtx, + connections: make(map[string]*webSocketConnection), + topics: make(map[string]*webSocketTopic), + topicMap: make(map[string]map[string]*webSocketConnection), + replyMap: make(map[string]*webSocketConnection), + newTopic: make(chan bool), + replyChannel: make(chan interface{}), + processingTimeout: 30 * time.Second, + upgrader: &websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + }, + } + go s.processBroadcasts() + go s.processReplies() + return s +} + +func (s *webSocketServer) Handler(w http.ResponseWriter, r *http.Request) { + conn, err := s.upgrader.Upgrade(w, r, nil) + if err != nil { + log.L(s.ctx).Errorf("WebSocket upgrade failed: %s", err) + return + } + s.mux.Lock() + defer s.mux.Unlock() + c := newConnection(s.ctx, s, conn) + s.connections[c.id] = c +} + +func (s *webSocketServer) cycleTopic(connInfo string, t *webSocketTopic) { + s.mux.Lock() + defer s.mux.Unlock() + + // When a connection that was listening on a topic closes, we need to wake anyone + // that was listening for a response + select { + case t.receiverChannel <- i18n.NewError(s.ctx, tmmsgs.MsgWebSocketClosed, connInfo): + default: + } +} + +func (s *webSocketServer) connectionClosed(c *webSocketConnection) { + s.mux.Lock() + defer s.mux.Unlock() + delete(s.connections, c.id) + delete(s.replyMap, c.id) + for _, topic := range c.topics { + delete(s.topicMap[topic.topic], c.id) + } +} + +func (s *webSocketServer) Close() { + for _, c := range s.connections { + c.close() + } +} + +func (s *webSocketServer) getTopic(topic string) *webSocketTopic { + s.mux.Lock() + t, exists := s.topics[topic] + if !exists { + t = &webSocketTopic{ + topic: topic, + senderChannel: make(chan interface{}), + broadcastChannel: make(chan interface{}), + receiverChannel: make(chan error, 1), + } + s.topics[topic] = t + s.topicMap[topic] = make(map[string]*webSocketConnection) + } + s.mux.Unlock() + if !exists { + // Signal to the broadcaster that a new topic has been added + s.newTopic <- true + } + return t +} + +func (s *webSocketServer) GetChannels(topic string) (chan<- interface{}, chan<- interface{}, <-chan error) { + t := s.getTopic(topic) + return t.senderChannel, t.broadcastChannel, t.receiverChannel +} + +func (s *webSocketServer) ListenOnTopic(c *webSocketConnection, topic string) { + // Track that this connection is interested in this topic + s.topicMap[topic][c.id] = c +} + +func (s *webSocketServer) ListenForReplies(c *webSocketConnection) { + s.replyMap[c.id] = c +} + +func (s *webSocketServer) SendReply(message interface{}) { + s.replyChannel <- message +} + +func (s *webSocketServer) processBroadcasts() { + var topics []string + buildCases := func() []reflect.SelectCase { + // only hold the lock while we're building the list of cases (not while doing the select) + s.mux.Lock() + defer s.mux.Unlock() + topics = make([]string, len(s.topics)) + cases := make([]reflect.SelectCase, len(s.topics)+1) + i := 0 + for _, t := range s.topics { + cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(t.broadcastChannel)} + topics[i] = t.topic + i++ + } + cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(s.newTopic)} + return cases + } + cases := buildCases() + for { + chosen, value, ok := reflect.Select(cases) + if !ok { + log.L(s.ctx).Warn("An error occurred broadcasting the message") + return + } + + if chosen == len(cases)-1 { + // Addition of a new topic + cases = buildCases() + } else { + // Message on one of the existing topics + // Gather all connections interested in this topic and send to them + s.mux.Lock() + topic := topics[chosen] + wsconns := getConnListFromMap(s.topicMap[topic]) + s.mux.Unlock() + s.broadcastToConnections(wsconns, value.Interface()) + } + } +} + +// getConnListFromMap is a simple helper to snapshot a map into a list, which can be called with a short-lived lock +func getConnListFromMap(tm map[string]*webSocketConnection) []*webSocketConnection { + wsconns := make([]*webSocketConnection, 0, len(tm)) + for _, c := range tm { + wsconns = append(wsconns, c) + } + return wsconns +} + +func (s *webSocketServer) processReplies() { + for { + message := <-s.replyChannel + s.mux.Lock() + wsconns := getConnListFromMap(s.replyMap) + s.mux.Unlock() + s.broadcastToConnections(wsconns, message) + } +} + +func (s *webSocketServer) broadcastToConnections(connections []*webSocketConnection, message interface{}) { + for _, c := range connections { + c.broadcast <- message + } +} diff --git a/internal/ws/wsserver_test.go b/internal/ws/wsserver_test.go new file mode 100644 index 00000000..91827be0 --- /dev/null +++ b/internal/ws/wsserver_test.go @@ -0,0 +1,371 @@ +// 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 ws + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "sync" + "testing" + "time" + + ws "github.com/gorilla/websocket" + + "github.com/stretchr/testify/assert" +) + +func newTestWebSocketServer() (*webSocketServer, *httptest.Server) { + s := NewWebSocketServer(context.Background()).(*webSocketServer) + ts := httptest.NewServer(http.HandlerFunc(s.Handler)) + return s, ts +} + +func TestConnectSendReceiveCycle(t *testing.T) { + assert := assert.New(t) + + w, ts := newTestWebSocketServer() + defer ts.Close() + + u, err := url.Parse(ts.URL) + u.Scheme = "ws" + u.Path = "/ws" + c, _, err := ws.DefaultDialer.Dial(u.String(), nil) + assert.NoError(err) + + c.WriteJSON(&webSocketCommandMessage{ + Type: "listen", + }) + + s, _, r := w.GetChannels("") + + s <- "Hello World" + + var val string + c.ReadJSON(&val) + assert.Equal("Hello World", val) + + c.WriteJSON(&webSocketCommandMessage{ + Type: "ignoreme", + }) + + c.WriteJSON(&webSocketCommandMessage{ + Type: "ack", + }) + err = <-r + assert.NoError(err) + + s <- "Don't Panic!" + + c.ReadJSON(&val) + assert.Equal("Don't Panic!", val) + + c.WriteJSON(&webSocketCommandMessage{ + Type: "error", + Message: "Panic!", + }) + + err = <-r + assert.Regexp("Error received from WebSocket client: Panic!", err) + + w.Close() + +} + +func TestConnectTopicIsolation(t *testing.T) { + assert := assert.New(t) + + w, ts := newTestWebSocketServer() + defer ts.Close() + + u, err := url.Parse(ts.URL) + u.Scheme = "ws" + u.Path = "/ws" + c1, _, err := ws.DefaultDialer.Dial(u.String(), nil) + assert.NoError(err) + c2, _, err := ws.DefaultDialer.Dial(u.String(), nil) + assert.NoError(err) + + c1.WriteJSON(&webSocketCommandMessage{ + Type: "listen", + Topic: "topic1", + }) + + c2.WriteJSON(&webSocketCommandMessage{ + Type: "listen", + Topic: "topic2", + }) + + s1, _, r1 := w.GetChannels("topic1") + s2, _, r2 := w.GetChannels("topic2") + + s1 <- "Hello Number 1" + s2 <- "Hello Number 2" + + var val string + c1.ReadJSON(&val) + assert.Equal("Hello Number 1", val) + c1.WriteJSON(&webSocketCommandMessage{ + Type: "ack", + Topic: "topic1", + }) + err = <-r1 + assert.NoError(err) + + c2.ReadJSON(&val) + assert.Equal("Hello Number 2", val) + c2.WriteJSON(&webSocketCommandMessage{ + Type: "ack", + Topic: "topic2", + }) + err = <-r2 + assert.NoError(err) + + w.Close() + +} + +func TestConnectAbandonRequest(t *testing.T) { + assert := assert.New(t) + + w, ts := newTestWebSocketServer() + defer ts.Close() + + u, err := url.Parse(ts.URL) + u.Scheme = "ws" + u.Path = "/ws" + c, _, err := ws.DefaultDialer.Dial(u.String(), nil) + assert.NoError(err) + + c.WriteJSON(&webSocketCommandMessage{ + Type: "listen", + }) + _, _, r := w.GetChannels("") + + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + select { + case <-r: + break + } + wg.Done() + }() + + // Close the client while we've got an active read stream + c.Close() + + // We whould find the read stream closes out + wg.Wait() + w.Close() + +} + +func TestSpuriousAckProcessing(t *testing.T) { + assert := assert.New(t) + + w, ts := newTestWebSocketServer() + defer ts.Close() + w.processingTimeout = 1 * time.Millisecond + + u, err := url.Parse(ts.URL) + u.Scheme = "ws" + u.Path = "/ws" + c, _, err := ws.DefaultDialer.Dial(u.String(), nil) + assert.NoError(err) + + c.WriteJSON(&webSocketCommandMessage{ + Type: "ack", + Topic: "mytopic", + }) + c.WriteJSON(&webSocketCommandMessage{ + Type: "ack", + Topic: "mytopic", + }) + c.Close() + + for len(w.connections) > 0 { + time.Sleep(1 * time.Millisecond) + } + w.Close() +} + +func TestConnectBadWebsocketHandshake(t *testing.T) { + assert := assert.New(t) + + w, ts := newTestWebSocketServer() + defer ts.Close() + + u, _ := url.Parse(ts.URL) + u.Path = "/ws" + + res, err := http.Get(u.String()) + assert.NoError(err) + assert.Equal(400, res.StatusCode) + + w.Close() + +} + +func TestBroadcast(t *testing.T) { + assert := assert.New(t) + + w, ts := newTestWebSocketServer() + defer ts.Close() + + u, _ := url.Parse(ts.URL) + u.Scheme = "ws" + u.Path = "/ws" + topic := "banana" + c, _, err := ws.DefaultDialer.Dial(u.String(), nil) + assert.NoError(err) + + c.WriteJSON(&webSocketCommandMessage{ + Type: "listen", + Topic: topic, + }) + + // Wait until the client has subscribed to the topic before proceeding + for len(w.topicMap[topic]) == 0 { + time.Sleep(10 * time.Millisecond) + } + + _, b, _ := w.GetChannels(topic) + b <- "Hello World" + + var val string + c.ReadJSON(&val) + assert.Equal("Hello World", val) + + b <- "Hello World Again" + + c.ReadJSON(&val) + assert.Equal("Hello World Again", val) + + w.Close() +} + +func TestBroadcastDefaultTopic(t *testing.T) { + assert := assert.New(t) + + w, ts := newTestWebSocketServer() + defer ts.Close() + + u, _ := url.Parse(ts.URL) + u.Scheme = "ws" + u.Path = "/ws" + topic := "" + c, _, err := ws.DefaultDialer.Dial(u.String(), nil) + assert.NoError(err) + + c.WriteJSON(&webSocketCommandMessage{ + Type: "listen", + }) + + // Wait until the client has subscribed to the topic before proceeding + for len(w.topicMap[topic]) == 0 { + time.Sleep(10 * time.Millisecond) + } + + _, b, _ := w.GetChannels(topic) + b <- "Hello World" + + var val string + c.ReadJSON(&val) + assert.Equal("Hello World", val) + + b <- "Hello World Again" + + c.ReadJSON(&val) + assert.Equal("Hello World Again", val) + + w.Close() +} + +func TestRecvNotOk(t *testing.T) { + assert := assert.New(t) + + w, ts := newTestWebSocketServer() + defer ts.Close() + + u, _ := url.Parse(ts.URL) + u.Scheme = "ws" + u.Path = "/ws" + topic := "" + c, _, err := ws.DefaultDialer.Dial(u.String(), nil) + assert.NoError(err) + + c.WriteJSON(&webSocketCommandMessage{ + Type: "listen", + }) + + // Wait until the client has subscribed to the topic before proceeding + for len(w.topicMap[topic]) == 0 { + time.Sleep(10 * time.Millisecond) + } + + _, b, _ := w.GetChannels(topic) + close(b) + w.Close() +} + +func TestSendReply(t *testing.T) { + assert := assert.New(t) + + w, ts := newTestWebSocketServer() + defer ts.Close() + + u, _ := url.Parse(ts.URL) + u.Scheme = "ws" + u.Path = "/ws" + c, _, err := ws.DefaultDialer.Dial(u.String(), nil) + assert.NoError(err) + + c.WriteJSON(&webSocketCommandMessage{ + Type: "listenReplies", + }) + + // Wait until the client has subscribed to the topic before proceeding + for len(w.replyMap) == 0 { + time.Sleep(10 * time.Millisecond) + } + + w.SendReply("Hello World") + + var val string + c.ReadJSON(&val) + assert.Equal("Hello World", val) +} + +func TestListenTopicClosing(t *testing.T) { + + w, ts := newTestWebSocketServer() + defer ts.Close() + w.getTopic("test") + + c := &webSocketConnection{ + server: w, + topics: make(map[string]*webSocketTopic), + closing: make(chan struct{}), + newTopic: make(chan bool), + } + close(c.closing) + c.listenTopic(&webSocketTopic{ + topic: "test", + }) +} diff --git a/mocks/confirmationsmocks/manager.go b/mocks/confirmationsmocks/manager.go index 68cf5f7d..059e3894 100644 --- a/mocks/confirmationsmocks/manager.go +++ b/mocks/confirmationsmocks/manager.go @@ -4,6 +4,10 @@ package confirmationsmocks import ( confirmations "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" + ffcapi "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + + fftypes "github.com/hyperledger/firefly-common/pkg/fftypes" + mock "github.com/stretchr/testify/mock" ) @@ -12,6 +16,36 @@ type Manager struct { mock.Mock } +// CheckInFlight provides a mock function with given fields: listenerID +func (_m *Manager) CheckInFlight(listenerID *fftypes.UUID) bool { + ret := _m.Called(listenerID) + + var r0 bool + if rf, ok := ret.Get(0).(func(*fftypes.UUID) bool); ok { + r0 = rf(listenerID) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// NewBlockHashes provides a mock function with given fields: +func (_m *Manager) NewBlockHashes() chan<- *ffcapi.BlockHashEvent { + ret := _m.Called() + + var r0 chan<- *ffcapi.BlockHashEvent + if rf, ok := ret.Get(0).(func() chan<- *ffcapi.BlockHashEvent); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(chan<- *ffcapi.BlockHashEvent) + } + } + + return r0 +} + // Notify provides a mock function with given fields: n func (_m *Manager) Notify(n *confirmations.Notification) error { ret := _m.Called(n) diff --git a/mocks/eventsmocks/stream.go b/mocks/eventsmocks/stream.go new file mode 100644 index 00000000..85b2bc05 --- /dev/null +++ b/mocks/eventsmocks/stream.go @@ -0,0 +1,141 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package eventsmocks + +import ( + context "context" + + apitypes "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + + fftypes "github.com/hyperledger/firefly-common/pkg/fftypes" + + mock "github.com/stretchr/testify/mock" +) + +// Stream is an autogenerated mock type for the Stream type +type Stream struct { + mock.Mock +} + +// AddOrUpdateListener provides a mock function with given fields: ctx, id, updates, reset +func (_m *Stream) AddOrUpdateListener(ctx context.Context, id *fftypes.UUID, updates *apitypes.Listener, reset bool) (*apitypes.Listener, error) { + ret := _m.Called(ctx, id, updates, reset) + + var r0 *apitypes.Listener + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, *apitypes.Listener, bool) *apitypes.Listener); ok { + r0 = rf(ctx, id, updates, reset) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*apitypes.Listener) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, *apitypes.Listener, bool) error); ok { + r1 = rf(ctx, id, updates, reset) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Delete provides a mock function with given fields: ctx +func (_m *Stream) Delete(ctx context.Context) error { + ret := _m.Called(ctx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveListener provides a mock function with given fields: ctx, id +func (_m *Stream) RemoveListener(ctx context.Context, id *fftypes.UUID) error { + ret := _m.Called(ctx, id) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Spec provides a mock function with given fields: +func (_m *Stream) Spec() *apitypes.EventStream { + ret := _m.Called() + + var r0 *apitypes.EventStream + if rf, ok := ret.Get(0).(func() *apitypes.EventStream); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*apitypes.EventStream) + } + } + + return r0 +} + +// Start provides a mock function with given fields: ctx +func (_m *Stream) Start(ctx context.Context) error { + ret := _m.Called(ctx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Status provides a mock function with given fields: +func (_m *Stream) Status() apitypes.EventStreamStatus { + ret := _m.Called() + + var r0 apitypes.EventStreamStatus + if rf, ok := ret.Get(0).(func() apitypes.EventStreamStatus); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(apitypes.EventStreamStatus) + } + + return r0 +} + +// Stop provides a mock function with given fields: ctx +func (_m *Stream) Stop(ctx context.Context) error { + ret := _m.Called(ctx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// UpdateSpec provides a mock function with given fields: ctx, updates +func (_m *Stream) UpdateSpec(ctx context.Context, updates *apitypes.EventStream) error { + ret := _m.Called(ctx, updates) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *apitypes.EventStream) error); ok { + r0 = rf(ctx, updates) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/mocks/ffcapimocks/api.go b/mocks/ffcapimocks/api.go index 64d37f5b..aa4a30a4 100644 --- a/mocks/ffcapimocks/api.go +++ b/mocks/ffcapimocks/api.go @@ -5,7 +5,7 @@ package ffcapimocks import ( context "context" - ffcapi "github.com/hyperledger/firefly-common/pkg/ffcapi" + ffcapi "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" mock "github.com/stretchr/testify/mock" ) @@ -14,28 +14,28 @@ type API struct { mock.Mock } -// CreateBlockListener provides a mock function with given fields: ctx, req -func (_m *API) CreateBlockListener(ctx context.Context, req *ffcapi.CreateBlockListenerRequest) (*ffcapi.CreateBlockListenerResponse, ffcapi.ErrorReason, error) { +// BlockInfoByHash provides a mock function with given fields: ctx, req +func (_m *API) BlockInfoByHash(ctx context.Context, req *ffcapi.BlockInfoByHashRequest) (*ffcapi.BlockInfoByHashResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) - var r0 *ffcapi.CreateBlockListenerResponse - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.CreateBlockListenerRequest) *ffcapi.CreateBlockListenerResponse); ok { + var r0 *ffcapi.BlockInfoByHashResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.BlockInfoByHashRequest) *ffcapi.BlockInfoByHashResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*ffcapi.CreateBlockListenerResponse) + r0 = ret.Get(0).(*ffcapi.BlockInfoByHashResponse) } } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.CreateBlockListenerRequest) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.BlockInfoByHashRequest) ffcapi.ErrorReason); ok { r1 = rf(ctx, req) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.CreateBlockListenerRequest) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.BlockInfoByHashRequest) error); ok { r2 = rf(ctx, req) } else { r2 = ret.Error(2) @@ -44,28 +44,28 @@ func (_m *API) CreateBlockListener(ctx context.Context, req *ffcapi.CreateBlockL return r0, r1, r2 } -// ExecQuery provides a mock function with given fields: ctx, req -func (_m *API) ExecQuery(ctx context.Context, req *ffcapi.ExecQueryRequest) (*ffcapi.ExecQueryResponse, ffcapi.ErrorReason, error) { +// BlockInfoByNumber provides a mock function with given fields: ctx, req +func (_m *API) BlockInfoByNumber(ctx context.Context, req *ffcapi.BlockInfoByNumberRequest) (*ffcapi.BlockInfoByNumberResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) - var r0 *ffcapi.ExecQueryResponse - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.ExecQueryRequest) *ffcapi.ExecQueryResponse); ok { + var r0 *ffcapi.BlockInfoByNumberResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.BlockInfoByNumberRequest) *ffcapi.BlockInfoByNumberResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*ffcapi.ExecQueryResponse) + r0 = ret.Get(0).(*ffcapi.BlockInfoByNumberResponse) } } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.ExecQueryRequest) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.BlockInfoByNumberRequest) ffcapi.ErrorReason); ok { r1 = rf(ctx, req) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.ExecQueryRequest) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.BlockInfoByNumberRequest) error); ok { r2 = rf(ctx, req) } else { r2 = ret.Error(2) @@ -74,28 +74,28 @@ func (_m *API) ExecQuery(ctx context.Context, req *ffcapi.ExecQueryRequest) (*ff return r0, r1, r2 } -// GetBlockInfoByHash provides a mock function with given fields: ctx, req -func (_m *API) GetBlockInfoByHash(ctx context.Context, req *ffcapi.GetBlockInfoByHashRequest) (*ffcapi.GetBlockInfoByHashResponse, ffcapi.ErrorReason, error) { +// DeployContractPrepare provides a mock function with given fields: ctx, req +func (_m *API) DeployContractPrepare(ctx context.Context, req *ffcapi.ContractDeployPrepareRequest) (*ffcapi.TransactionPrepareResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) - var r0 *ffcapi.GetBlockInfoByHashResponse - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.GetBlockInfoByHashRequest) *ffcapi.GetBlockInfoByHashResponse); ok { + var r0 *ffcapi.TransactionPrepareResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.ContractDeployPrepareRequest) *ffcapi.TransactionPrepareResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*ffcapi.GetBlockInfoByHashResponse) + r0 = ret.Get(0).(*ffcapi.TransactionPrepareResponse) } } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.GetBlockInfoByHashRequest) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.ContractDeployPrepareRequest) ffcapi.ErrorReason); ok { r1 = rf(ctx, req) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.GetBlockInfoByHashRequest) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.ContractDeployPrepareRequest) error); ok { r2 = rf(ctx, req) } else { r2 = ret.Error(2) @@ -104,28 +104,28 @@ func (_m *API) GetBlockInfoByHash(ctx context.Context, req *ffcapi.GetBlockInfoB return r0, r1, r2 } -// GetBlockInfoByNumber provides a mock function with given fields: ctx, req -func (_m *API) GetBlockInfoByNumber(ctx context.Context, req *ffcapi.GetBlockInfoByNumberRequest) (*ffcapi.GetBlockInfoByNumberResponse, ffcapi.ErrorReason, error) { +// EventListenerAdd provides a mock function with given fields: ctx, req +func (_m *API) EventListenerAdd(ctx context.Context, req *ffcapi.EventListenerAddRequest) (*ffcapi.EventListenerAddResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) - var r0 *ffcapi.GetBlockInfoByNumberResponse - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.GetBlockInfoByNumberRequest) *ffcapi.GetBlockInfoByNumberResponse); ok { + var r0 *ffcapi.EventListenerAddResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.EventListenerAddRequest) *ffcapi.EventListenerAddResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*ffcapi.GetBlockInfoByNumberResponse) + r0 = ret.Get(0).(*ffcapi.EventListenerAddResponse) } } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.GetBlockInfoByNumberRequest) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.EventListenerAddRequest) ffcapi.ErrorReason); ok { r1 = rf(ctx, req) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.GetBlockInfoByNumberRequest) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.EventListenerAddRequest) error); ok { r2 = rf(ctx, req) } else { r2 = ret.Error(2) @@ -134,28 +134,28 @@ func (_m *API) GetBlockInfoByNumber(ctx context.Context, req *ffcapi.GetBlockInf return r0, r1, r2 } -// GetGasPrice provides a mock function with given fields: ctx, req -func (_m *API) GetGasPrice(ctx context.Context, req *ffcapi.GetGasPriceRequest) (*ffcapi.GetGasPriceResponse, ffcapi.ErrorReason, error) { +// EventListenerHWM provides a mock function with given fields: ctx, req +func (_m *API) EventListenerHWM(ctx context.Context, req *ffcapi.EventListenerHWMRequest) (*ffcapi.EventListenerHWMResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) - var r0 *ffcapi.GetGasPriceResponse - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.GetGasPriceRequest) *ffcapi.GetGasPriceResponse); ok { + var r0 *ffcapi.EventListenerHWMResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.EventListenerHWMRequest) *ffcapi.EventListenerHWMResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*ffcapi.GetGasPriceResponse) + r0 = ret.Get(0).(*ffcapi.EventListenerHWMResponse) } } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.GetGasPriceRequest) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.EventListenerHWMRequest) ffcapi.ErrorReason); ok { r1 = rf(ctx, req) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.GetGasPriceRequest) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.EventListenerHWMRequest) error); ok { r2 = rf(ctx, req) } else { r2 = ret.Error(2) @@ -164,28 +164,28 @@ func (_m *API) GetGasPrice(ctx context.Context, req *ffcapi.GetGasPriceRequest) return r0, r1, r2 } -// GetNewBlockHashes provides a mock function with given fields: ctx, req -func (_m *API) GetNewBlockHashes(ctx context.Context, req *ffcapi.GetNewBlockHashesRequest) (*ffcapi.GetNewBlockHashesResponse, ffcapi.ErrorReason, error) { +// EventListenerRemove provides a mock function with given fields: ctx, req +func (_m *API) EventListenerRemove(ctx context.Context, req *ffcapi.EventListenerRemoveRequest) (*ffcapi.EventListenerRemoveResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) - var r0 *ffcapi.GetNewBlockHashesResponse - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.GetNewBlockHashesRequest) *ffcapi.GetNewBlockHashesResponse); ok { + var r0 *ffcapi.EventListenerRemoveResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.EventListenerRemoveRequest) *ffcapi.EventListenerRemoveResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*ffcapi.GetNewBlockHashesResponse) + r0 = ret.Get(0).(*ffcapi.EventListenerRemoveResponse) } } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.GetNewBlockHashesRequest) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.EventListenerRemoveRequest) ffcapi.ErrorReason); ok { r1 = rf(ctx, req) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.GetNewBlockHashesRequest) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.EventListenerRemoveRequest) error); ok { r2 = rf(ctx, req) } else { r2 = ret.Error(2) @@ -194,28 +194,28 @@ func (_m *API) GetNewBlockHashes(ctx context.Context, req *ffcapi.GetNewBlockHas return r0, r1, r2 } -// GetNextNonce provides a mock function with given fields: ctx, req -func (_m *API) GetNextNonce(ctx context.Context, req *ffcapi.GetNextNonceRequest) (*ffcapi.GetNextNonceResponse, ffcapi.ErrorReason, error) { +// EventListenerVerifyOptions provides a mock function with given fields: ctx, req +func (_m *API) EventListenerVerifyOptions(ctx context.Context, req *ffcapi.EventListenerVerifyOptionsRequest) (*ffcapi.EventListenerVerifyOptionsResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) - var r0 *ffcapi.GetNextNonceResponse - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.GetNextNonceRequest) *ffcapi.GetNextNonceResponse); ok { + var r0 *ffcapi.EventListenerVerifyOptionsResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.EventListenerVerifyOptionsRequest) *ffcapi.EventListenerVerifyOptionsResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*ffcapi.GetNextNonceResponse) + r0 = ret.Get(0).(*ffcapi.EventListenerVerifyOptionsResponse) } } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.GetNextNonceRequest) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.EventListenerVerifyOptionsRequest) ffcapi.ErrorReason); ok { r1 = rf(ctx, req) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.GetNextNonceRequest) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.EventListenerVerifyOptionsRequest) error); ok { r2 = rf(ctx, req) } else { r2 = ret.Error(2) @@ -224,28 +224,224 @@ func (_m *API) GetNextNonce(ctx context.Context, req *ffcapi.GetNextNonceRequest return r0, r1, r2 } -// GetReceipt provides a mock function with given fields: ctx, req -func (_m *API) GetReceipt(ctx context.Context, req *ffcapi.GetReceiptRequest) (*ffcapi.GetReceiptResponse, ffcapi.ErrorReason, error) { +// EventStreamNewCheckpointStruct provides a mock function with given fields: +func (_m *API) EventStreamNewCheckpointStruct() ffcapi.EventListenerCheckpoint { + ret := _m.Called() + + var r0 ffcapi.EventListenerCheckpoint + if rf, ok := ret.Get(0).(func() ffcapi.EventListenerCheckpoint); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(ffcapi.EventListenerCheckpoint) + } + } + + return r0 +} + +// EventStreamStart provides a mock function with given fields: ctx, req +func (_m *API) EventStreamStart(ctx context.Context, req *ffcapi.EventStreamStartRequest) (*ffcapi.EventStreamStartResponse, ffcapi.ErrorReason, error) { + ret := _m.Called(ctx, req) + + var r0 *ffcapi.EventStreamStartResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.EventStreamStartRequest) *ffcapi.EventStreamStartResponse); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ffcapi.EventStreamStartResponse) + } + } + + var r1 ffcapi.ErrorReason + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.EventStreamStartRequest) ffcapi.ErrorReason); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Get(1).(ffcapi.ErrorReason) + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.EventStreamStartRequest) error); ok { + r2 = rf(ctx, req) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// EventStreamStopped provides a mock function with given fields: ctx, req +func (_m *API) EventStreamStopped(ctx context.Context, req *ffcapi.EventStreamStoppedRequest) (*ffcapi.EventStreamStoppedResponse, ffcapi.ErrorReason, error) { + ret := _m.Called(ctx, req) + + var r0 *ffcapi.EventStreamStoppedResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.EventStreamStoppedRequest) *ffcapi.EventStreamStoppedResponse); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ffcapi.EventStreamStoppedResponse) + } + } + + var r1 ffcapi.ErrorReason + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.EventStreamStoppedRequest) ffcapi.ErrorReason); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Get(1).(ffcapi.ErrorReason) + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.EventStreamStoppedRequest) error); ok { + r2 = rf(ctx, req) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// GasPriceEstimate provides a mock function with given fields: ctx, req +func (_m *API) GasPriceEstimate(ctx context.Context, req *ffcapi.GasPriceEstimateRequest) (*ffcapi.GasPriceEstimateResponse, ffcapi.ErrorReason, error) { + ret := _m.Called(ctx, req) + + var r0 *ffcapi.GasPriceEstimateResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.GasPriceEstimateRequest) *ffcapi.GasPriceEstimateResponse); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ffcapi.GasPriceEstimateResponse) + } + } + + var r1 ffcapi.ErrorReason + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.GasPriceEstimateRequest) ffcapi.ErrorReason); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Get(1).(ffcapi.ErrorReason) + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.GasPriceEstimateRequest) error); ok { + r2 = rf(ctx, req) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// NewBlockListener provides a mock function with given fields: ctx, req +func (_m *API) NewBlockListener(ctx context.Context, req *ffcapi.NewBlockListenerRequest) (*ffcapi.NewBlockListenerResponse, ffcapi.ErrorReason, error) { + ret := _m.Called(ctx, req) + + var r0 *ffcapi.NewBlockListenerResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.NewBlockListenerRequest) *ffcapi.NewBlockListenerResponse); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ffcapi.NewBlockListenerResponse) + } + } + + var r1 ffcapi.ErrorReason + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.NewBlockListenerRequest) ffcapi.ErrorReason); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Get(1).(ffcapi.ErrorReason) + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.NewBlockListenerRequest) error); ok { + r2 = rf(ctx, req) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// NextNonceForSigner provides a mock function with given fields: ctx, req +func (_m *API) NextNonceForSigner(ctx context.Context, req *ffcapi.NextNonceForSignerRequest) (*ffcapi.NextNonceForSignerResponse, ffcapi.ErrorReason, error) { + ret := _m.Called(ctx, req) + + var r0 *ffcapi.NextNonceForSignerResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.NextNonceForSignerRequest) *ffcapi.NextNonceForSignerResponse); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ffcapi.NextNonceForSignerResponse) + } + } + + var r1 ffcapi.ErrorReason + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.NextNonceForSignerRequest) ffcapi.ErrorReason); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Get(1).(ffcapi.ErrorReason) + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.NextNonceForSignerRequest) error); ok { + r2 = rf(ctx, req) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// QueryInvoke provides a mock function with given fields: ctx, req +func (_m *API) QueryInvoke(ctx context.Context, req *ffcapi.QueryInvokeRequest) (*ffcapi.QueryInvokeResponse, ffcapi.ErrorReason, error) { + ret := _m.Called(ctx, req) + + var r0 *ffcapi.QueryInvokeResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.QueryInvokeRequest) *ffcapi.QueryInvokeResponse); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ffcapi.QueryInvokeResponse) + } + } + + var r1 ffcapi.ErrorReason + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.QueryInvokeRequest) ffcapi.ErrorReason); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Get(1).(ffcapi.ErrorReason) + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.QueryInvokeRequest) error); ok { + r2 = rf(ctx, req) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// TransactionPrepare provides a mock function with given fields: ctx, req +func (_m *API) TransactionPrepare(ctx context.Context, req *ffcapi.TransactionPrepareRequest) (*ffcapi.TransactionPrepareResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) - var r0 *ffcapi.GetReceiptResponse - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.GetReceiptRequest) *ffcapi.GetReceiptResponse); ok { + var r0 *ffcapi.TransactionPrepareResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.TransactionPrepareRequest) *ffcapi.TransactionPrepareResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*ffcapi.GetReceiptResponse) + r0 = ret.Get(0).(*ffcapi.TransactionPrepareResponse) } } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.GetReceiptRequest) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.TransactionPrepareRequest) ffcapi.ErrorReason); ok { r1 = rf(ctx, req) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.GetReceiptRequest) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.TransactionPrepareRequest) error); ok { r2 = rf(ctx, req) } else { r2 = ret.Error(2) @@ -254,28 +450,28 @@ func (_m *API) GetReceipt(ctx context.Context, req *ffcapi.GetReceiptRequest) (* return r0, r1, r2 } -// PrepareTransaction provides a mock function with given fields: ctx, req -func (_m *API) PrepareTransaction(ctx context.Context, req *ffcapi.PrepareTransactionRequest) (*ffcapi.PrepareTransactionResponse, ffcapi.ErrorReason, error) { +// TransactionReceipt provides a mock function with given fields: ctx, req +func (_m *API) TransactionReceipt(ctx context.Context, req *ffcapi.TransactionReceiptRequest) (*ffcapi.TransactionReceiptResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) - var r0 *ffcapi.PrepareTransactionResponse - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.PrepareTransactionRequest) *ffcapi.PrepareTransactionResponse); ok { + var r0 *ffcapi.TransactionReceiptResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.TransactionReceiptRequest) *ffcapi.TransactionReceiptResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*ffcapi.PrepareTransactionResponse) + r0 = ret.Get(0).(*ffcapi.TransactionReceiptResponse) } } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.PrepareTransactionRequest) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.TransactionReceiptRequest) ffcapi.ErrorReason); ok { r1 = rf(ctx, req) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.PrepareTransactionRequest) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.TransactionReceiptRequest) error); ok { r2 = rf(ctx, req) } else { r2 = ret.Error(2) @@ -284,28 +480,28 @@ func (_m *API) PrepareTransaction(ctx context.Context, req *ffcapi.PrepareTransa return r0, r1, r2 } -// SendTransaction provides a mock function with given fields: ctx, req -func (_m *API) SendTransaction(ctx context.Context, req *ffcapi.SendTransactionRequest) (*ffcapi.SendTransactionResponse, ffcapi.ErrorReason, error) { +// TransactionSend provides a mock function with given fields: ctx, req +func (_m *API) TransactionSend(ctx context.Context, req *ffcapi.TransactionSendRequest) (*ffcapi.TransactionSendResponse, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, req) - var r0 *ffcapi.SendTransactionResponse - if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.SendTransactionRequest) *ffcapi.SendTransactionResponse); ok { + var r0 *ffcapi.TransactionSendResponse + if rf, ok := ret.Get(0).(func(context.Context, *ffcapi.TransactionSendRequest) *ffcapi.TransactionSendResponse); ok { r0 = rf(ctx, req) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*ffcapi.SendTransactionResponse) + r0 = ret.Get(0).(*ffcapi.TransactionSendResponse) } } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.SendTransactionRequest) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, *ffcapi.TransactionSendRequest) ffcapi.ErrorReason); ok { r1 = rf(ctx, req) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.SendTransactionRequest) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, *ffcapi.TransactionSendRequest) error); ok { r2 = rf(ctx, req) } else { r2 = ret.Error(2) diff --git a/mocks/managermocks/manager.go b/mocks/managermocks/manager.go deleted file mode 100644 index e4cfd0b3..00000000 --- a/mocks/managermocks/manager.go +++ /dev/null @@ -1,43 +0,0 @@ -// Code generated by mockery v1.0.0. DO NOT EDIT. - -package managermocks - -import mock "github.com/stretchr/testify/mock" - -// Manager is an autogenerated mock type for the Manager type -type Manager struct { - mock.Mock -} - -// Start provides a mock function with given fields: -func (_m *Manager) Start() 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 -} - -// Stop provides a mock function with given fields: -func (_m *Manager) Stop() { - _m.Called() -} - -// WaitStop provides a mock function with given fields: -func (_m *Manager) WaitStop() error { - ret := _m.Called() - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 -} diff --git a/mocks/persistencemocks/persistence.go b/mocks/persistencemocks/persistence.go new file mode 100644 index 00000000..309fa3dd --- /dev/null +++ b/mocks/persistencemocks/persistence.go @@ -0,0 +1,390 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package persistencemocks + +import ( + context "context" + + apitypes "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + + fftypes "github.com/hyperledger/firefly-common/pkg/fftypes" + + mock "github.com/stretchr/testify/mock" + + persistence "github.com/hyperledger/firefly-transaction-manager/internal/persistence" +) + +// Persistence is an autogenerated mock type for the Persistence type +type Persistence struct { + mock.Mock +} + +// Close provides a mock function with given fields: ctx +func (_m *Persistence) Close(ctx context.Context) { + _m.Called(ctx) +} + +// DeleteCheckpoint provides a mock function with given fields: ctx, streamID +func (_m *Persistence) DeleteCheckpoint(ctx context.Context, streamID *fftypes.UUID) error { + ret := _m.Called(ctx, streamID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) error); ok { + r0 = rf(ctx, streamID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteListener provides a mock function with given fields: ctx, listenerID +func (_m *Persistence) DeleteListener(ctx context.Context, listenerID *fftypes.UUID) error { + ret := _m.Called(ctx, listenerID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) error); ok { + r0 = rf(ctx, listenerID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteStream provides a mock function with given fields: ctx, streamID +func (_m *Persistence) DeleteStream(ctx context.Context, streamID *fftypes.UUID) error { + ret := _m.Called(ctx, streamID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) error); ok { + r0 = rf(ctx, streamID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteTransaction provides a mock function with given fields: ctx, txID +func (_m *Persistence) DeleteTransaction(ctx context.Context, txID string) error { + ret := _m.Called(ctx, txID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, txID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetCheckpoint provides a mock function with given fields: ctx, streamID +func (_m *Persistence) GetCheckpoint(ctx context.Context, streamID *fftypes.UUID) (*apitypes.EventStreamCheckpoint, error) { + ret := _m.Called(ctx, streamID) + + var r0 *apitypes.EventStreamCheckpoint + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) *apitypes.EventStreamCheckpoint); ok { + r0 = rf(ctx, streamID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*apitypes.EventStreamCheckpoint) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID) error); ok { + r1 = rf(ctx, streamID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetListener provides a mock function with given fields: ctx, listenerID +func (_m *Persistence) GetListener(ctx context.Context, listenerID *fftypes.UUID) (*apitypes.Listener, error) { + ret := _m.Called(ctx, listenerID) + + var r0 *apitypes.Listener + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) *apitypes.Listener); ok { + r0 = rf(ctx, listenerID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*apitypes.Listener) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID) error); ok { + r1 = rf(ctx, listenerID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStream provides a mock function with given fields: ctx, streamID +func (_m *Persistence) GetStream(ctx context.Context, streamID *fftypes.UUID) (*apitypes.EventStream, error) { + ret := _m.Called(ctx, streamID) + + var r0 *apitypes.EventStream + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID) *apitypes.EventStream); ok { + r0 = rf(ctx, streamID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*apitypes.EventStream) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID) error); ok { + r1 = rf(ctx, streamID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTransactionByID provides a mock function with given fields: ctx, txID +func (_m *Persistence) GetTransactionByID(ctx context.Context, txID string) (*apitypes.ManagedTX, error) { + ret := _m.Called(ctx, txID) + + var r0 *apitypes.ManagedTX + if rf, ok := ret.Get(0).(func(context.Context, string) *apitypes.ManagedTX); ok { + r0 = rf(ctx, txID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*apitypes.ManagedTX) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, txID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTransactionByNonce provides a mock function with given fields: ctx, signer, nonce +func (_m *Persistence) GetTransactionByNonce(ctx context.Context, signer string, nonce *fftypes.FFBigInt) (*apitypes.ManagedTX, error) { + ret := _m.Called(ctx, signer, nonce) + + var r0 *apitypes.ManagedTX + if rf, ok := ret.Get(0).(func(context.Context, string, *fftypes.FFBigInt) *apitypes.ManagedTX); ok { + r0 = rf(ctx, signer, nonce) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*apitypes.ManagedTX) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, *fftypes.FFBigInt) error); ok { + r1 = rf(ctx, signer, nonce) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListListeners provides a mock function with given fields: ctx, after, limit, dir +func (_m *Persistence) ListListeners(ctx context.Context, after *fftypes.UUID, limit int, dir persistence.SortDirection) ([]*apitypes.Listener, error) { + ret := _m.Called(ctx, after, limit, dir) + + var r0 []*apitypes.Listener + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int, persistence.SortDirection) []*apitypes.Listener); ok { + r0 = rf(ctx, after, limit, dir) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*apitypes.Listener) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, int, persistence.SortDirection) error); ok { + r1 = rf(ctx, after, limit, dir) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListStreamListeners provides a mock function with given fields: ctx, after, limit, dir, streamID +func (_m *Persistence) ListStreamListeners(ctx context.Context, after *fftypes.UUID, limit int, dir persistence.SortDirection, streamID *fftypes.UUID) ([]*apitypes.Listener, error) { + ret := _m.Called(ctx, after, limit, dir, streamID) + + var r0 []*apitypes.Listener + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int, persistence.SortDirection, *fftypes.UUID) []*apitypes.Listener); ok { + r0 = rf(ctx, after, limit, dir, streamID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*apitypes.Listener) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, int, persistence.SortDirection, *fftypes.UUID) error); ok { + r1 = rf(ctx, after, limit, dir, streamID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListStreams provides a mock function with given fields: ctx, after, limit, dir +func (_m *Persistence) ListStreams(ctx context.Context, after *fftypes.UUID, limit int, dir persistence.SortDirection) ([]*apitypes.EventStream, error) { + ret := _m.Called(ctx, after, limit, dir) + + var r0 []*apitypes.EventStream + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int, persistence.SortDirection) []*apitypes.EventStream); ok { + r0 = rf(ctx, after, limit, dir) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*apitypes.EventStream) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, int, persistence.SortDirection) error); ok { + r1 = rf(ctx, after, limit, dir) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListTransactionsByCreateTime provides a mock function with given fields: ctx, after, limit, dir +func (_m *Persistence) ListTransactionsByCreateTime(ctx context.Context, after *apitypes.ManagedTX, limit int, dir persistence.SortDirection) ([]*apitypes.ManagedTX, error) { + ret := _m.Called(ctx, after, limit, dir) + + var r0 []*apitypes.ManagedTX + if rf, ok := ret.Get(0).(func(context.Context, *apitypes.ManagedTX, int, persistence.SortDirection) []*apitypes.ManagedTX); ok { + r0 = rf(ctx, after, limit, dir) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*apitypes.ManagedTX) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *apitypes.ManagedTX, int, persistence.SortDirection) error); ok { + r1 = rf(ctx, after, limit, dir) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListTransactionsByNonce provides a mock function with given fields: ctx, signer, after, limit, dir +func (_m *Persistence) ListTransactionsByNonce(ctx context.Context, signer string, after *fftypes.FFBigInt, limit int, dir persistence.SortDirection) ([]*apitypes.ManagedTX, error) { + ret := _m.Called(ctx, signer, after, limit, dir) + + var r0 []*apitypes.ManagedTX + if rf, ok := ret.Get(0).(func(context.Context, string, *fftypes.FFBigInt, int, persistence.SortDirection) []*apitypes.ManagedTX); ok { + r0 = rf(ctx, signer, after, limit, dir) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*apitypes.ManagedTX) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, *fftypes.FFBigInt, int, persistence.SortDirection) error); ok { + r1 = rf(ctx, signer, after, limit, dir) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListTransactionsPending provides a mock function with given fields: ctx, after, limit, dir +func (_m *Persistence) ListTransactionsPending(ctx context.Context, after *fftypes.UUID, limit int, dir persistence.SortDirection) ([]*apitypes.ManagedTX, error) { + ret := _m.Called(ctx, after, limit, dir) + + var r0 []*apitypes.ManagedTX + if rf, ok := ret.Get(0).(func(context.Context, *fftypes.UUID, int, persistence.SortDirection) []*apitypes.ManagedTX); ok { + r0 = rf(ctx, after, limit, dir) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*apitypes.ManagedTX) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *fftypes.UUID, int, persistence.SortDirection) error); ok { + r1 = rf(ctx, after, limit, dir) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// WriteCheckpoint provides a mock function with given fields: ctx, checkpoint +func (_m *Persistence) WriteCheckpoint(ctx context.Context, checkpoint *apitypes.EventStreamCheckpoint) error { + ret := _m.Called(ctx, checkpoint) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *apitypes.EventStreamCheckpoint) error); ok { + r0 = rf(ctx, checkpoint) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// WriteListener provides a mock function with given fields: ctx, spec +func (_m *Persistence) WriteListener(ctx context.Context, spec *apitypes.Listener) error { + ret := _m.Called(ctx, spec) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *apitypes.Listener) error); ok { + r0 = rf(ctx, spec) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// WriteStream provides a mock function with given fields: ctx, spec +func (_m *Persistence) WriteStream(ctx context.Context, spec *apitypes.EventStream) error { + ret := _m.Called(ctx, spec) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *apitypes.EventStream) error); ok { + r0 = rf(ctx, spec) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// WriteTransaction provides a mock function with given fields: ctx, tx, new +func (_m *Persistence) WriteTransaction(ctx context.Context, tx *apitypes.ManagedTX, new bool) error { + ret := _m.Called(ctx, tx, new) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *apitypes.ManagedTX, bool) error); ok { + r0 = rf(ctx, tx, new) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/mocks/policyenginemocks/policy_engine.go b/mocks/policyenginemocks/policy_engine.go index 26fcd79e..063bb917 100644 --- a/mocks/policyenginemocks/policy_engine.go +++ b/mocks/policyenginemocks/policy_engine.go @@ -5,8 +5,9 @@ package policyenginemocks import ( context "context" - ffcapi "github.com/hyperledger/firefly-common/pkg/ffcapi" - fftm "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + apitypes "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + + ffcapi "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" mock "github.com/stretchr/testify/mock" ) @@ -17,25 +18,25 @@ type PolicyEngine struct { } // Execute provides a mock function with given fields: ctx, cAPI, mtx -func (_m *PolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx *fftm.ManagedTXOutput) (bool, ffcapi.ErrorReason, error) { +func (_m *PolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx *apitypes.ManagedTX) (bool, ffcapi.ErrorReason, error) { ret := _m.Called(ctx, cAPI, mtx) var r0 bool - if rf, ok := ret.Get(0).(func(context.Context, ffcapi.API, *fftm.ManagedTXOutput) bool); ok { + if rf, ok := ret.Get(0).(func(context.Context, ffcapi.API, *apitypes.ManagedTX) bool); ok { r0 = rf(ctx, cAPI, mtx) } else { r0 = ret.Get(0).(bool) } var r1 ffcapi.ErrorReason - if rf, ok := ret.Get(1).(func(context.Context, ffcapi.API, *fftm.ManagedTXOutput) ffcapi.ErrorReason); ok { + if rf, ok := ret.Get(1).(func(context.Context, ffcapi.API, *apitypes.ManagedTX) ffcapi.ErrorReason); ok { r1 = rf(ctx, cAPI, mtx) } else { r1 = ret.Get(1).(ffcapi.ErrorReason) } var r2 error - if rf, ok := ret.Get(2).(func(context.Context, ffcapi.API, *fftm.ManagedTXOutput) error); ok { + if rf, ok := ret.Get(2).(func(context.Context, ffcapi.API, *apitypes.ManagedTX) error); ok { r2 = rf(ctx, cAPI, mtx) } else { r2 = ret.Error(2) diff --git a/mocks/wsmocks/web_socket_channels.go b/mocks/wsmocks/web_socket_channels.go new file mode 100644 index 00000000..593f108e --- /dev/null +++ b/mocks/wsmocks/web_socket_channels.go @@ -0,0 +1,49 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package wsmocks + +import mock "github.com/stretchr/testify/mock" + +// WebSocketChannels is an autogenerated mock type for the WebSocketChannels type +type WebSocketChannels struct { + mock.Mock +} + +// GetChannels provides a mock function with given fields: topic +func (_m *WebSocketChannels) GetChannels(topic string) (chan<- interface{}, chan<- interface{}, <-chan error) { + ret := _m.Called(topic) + + var r0 chan<- interface{} + if rf, ok := ret.Get(0).(func(string) chan<- interface{}); ok { + r0 = rf(topic) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(chan<- interface{}) + } + } + + var r1 chan<- interface{} + if rf, ok := ret.Get(1).(func(string) chan<- interface{}); ok { + r1 = rf(topic) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(chan<- interface{}) + } + } + + var r2 <-chan error + if rf, ok := ret.Get(2).(func(string) <-chan error); ok { + r2 = rf(topic) + } else { + if ret.Get(2) != nil { + r2 = ret.Get(2).(<-chan error) + } + } + + return r0, r1, r2 +} + +// SendReply provides a mock function with given fields: message +func (_m *WebSocketChannels) SendReply(message interface{}) { + _m.Called(message) +} diff --git a/pkg/apitypes/api_types.go b/pkg/apitypes/api_types.go new file mode 100644 index 00000000..d247ea7f --- /dev/null +++ b/pkg/apitypes/api_types.go @@ -0,0 +1,257 @@ +// 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 apitypes + +import ( + "bytes" + "encoding/json" + "reflect" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/jsonmap" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" +) + +type DistributionMode = fftypes.FFEnum + +var ( + DistributionModeBroadcast = fftypes.FFEnumValue("distmode", "broadcast") + DistributionModeLoadBalance = fftypes.FFEnumValue("distmode", "load_balance") +) + +type EventStreamType = fftypes.FFEnum + +var ( + EventStreamTypeWebhook = fftypes.FFEnumValue("estype", "webhook") + EventStreamTypeWebSocket = fftypes.FFEnumValue("estype", "websocket") +) + +type ErrorHandlingType = fftypes.FFEnum + +var ( + ErrorHandlingTypeBlock = fftypes.FFEnumValue("ehtype", "block") + ErrorHandlingTypeSkip = fftypes.FFEnumValue("ehtype", "skip") +) + +type EventStream struct { + ID *fftypes.UUID `ffstruct:"eventstream" json:"id"` + Created *fftypes.FFTime `ffstruct:"eventstream" json:"created"` + Updated *fftypes.FFTime `ffstruct:"eventstream" json:"updated"` + Name *string `ffstruct:"eventstream" json:"name,omitempty"` + Suspended *bool `ffstruct:"eventstream" json:"suspended,omitempty"` + Type *EventStreamType `ffstruct:"eventstream" json:"type,omitempty" ffenum:"estype"` + + ErrorHandling *ErrorHandlingType `ffstruct:"eventstream" json:"errorHandling"` + BatchSize *uint64 `ffstruct:"eventstream" json:"batchSize"` + BatchTimeout *fftypes.FFDuration `ffstruct:"eventstream" json:"batchTimeout"` + RetryTimeout *fftypes.FFDuration `ffstruct:"eventstream" json:"retryTimeout"` + BlockedRetryDelay *fftypes.FFDuration `ffstruct:"eventstream" json:"blockedRetryDelay"` + + EthCompatBatchTimeoutMS *uint64 `ffstruct:"eventstream" json:"batchTimeoutMS,omitempty"` // input only, for backwards compatibility + EthCompatRetryTimeoutSec *uint64 `ffstruct:"eventstream" json:"retryTimeoutSec,omitempty"` // input only, for backwards compatibility + EthCompatBlockedRetryDelaySec *uint64 `ffstruct:"eventstream" json:"blockedRetryDelaySec,omitempty"` // input only, for backwards compatibility + + Webhook *WebhookConfig `ffstruct:"eventstream" json:"webhook,omitempty"` + WebSocket *WebSocketConfig `ffstruct:"eventstream" json:"websocket,omitempty"` +} + +type EventStreamStatus string + +const ( + EventStreamStatusStarted EventStreamStatus = "started" + EventStreamStatusStopping EventStreamStatus = "stopping" + EventStreamStatusStopped EventStreamStatus = "stopped" + EventStreamStatusDeleted EventStreamStatus = "deleted" +) + +type EventStreamWithStatus struct { + EventStream + Status EventStreamStatus `ffstruct:"eventstream" json:"status"` +} + +type EventStreamCheckpoint struct { + StreamID *fftypes.UUID `json:"streamId"` + Time *fftypes.FFTime `json:"time"` + Listeners map[fftypes.UUID]json.RawMessage `json:"listeners"` +} + +type WebhookConfig struct { + URL *string `ffstruct:"whconfig" json:"url,omitempty"` + Headers map[string]string `ffstruct:"whconfig" json:"headers,omitempty"` + TLSkipHostVerify *bool `ffstruct:"whconfig" json:"tlsSkipHostVerify,omitempty"` + RequestTimeout *fftypes.FFDuration `ffstruct:"whconfig" json:"requestTimeout,omitempty"` + EthCompatRequestTimeoutSec *int64 `ffstruct:"whconfig" json:"requestTimeoutSec,omitempty"` // input only, for backwards compatibility +} + +type WebSocketConfig struct { + DistributionMode *DistributionMode `ffstruct:"wsconfig" json:"distributionMode,omitempty"` + Topic *string `ffstruct:"wsconfig" json:"topic,omitempty"` +} + +type Listener struct { + ID *fftypes.UUID `ffstruct:"listener" json:"id,omitempty"` + Created *fftypes.FFTime `ffstruct:"listener" json:"created"` + Updated *fftypes.FFTime `ffstruct:"listener" json:"updated"` + Name *string `ffstruct:"listener" json:"name"` + StreamID *fftypes.UUID `ffstruct:"listener" json:"stream" ffexcludeoutput:"true"` + EthCompatAddress *string `ffstruct:"listener" json:"address,omitempty"` + EthCompatEvent *fftypes.JSONAny `ffstruct:"listener" json:"event,omitempty"` + EthCompatMethods *fftypes.JSONAny `ffstruct:"listener" json:"methods,omitempty"` + Filters []fftypes.JSONAny `ffstruct:"listener" json:"filters"` + Options *fftypes.JSONAny `ffstruct:"listener" json:"options"` + Signature string `ffstruct:"listener" json:"signature,omitempty" ffexcludeinput:"true"` + FromBlock *string `ffstruct:"listener" json:"fromBlock,omitempty"` +} + +// CheckUpdateString helper merges supplied configuration, with a base, and applies a default if unset +func CheckUpdateString(changed bool, merged **string, old *string, new *string, defValue string) bool { + if new != nil { + *merged = new + } else { + *merged = old + } + if *merged == nil { + v := defValue + *merged = &v + return true + } + return changed || old == nil || *old != **merged +} + +// CheckUpdateBool helper merges supplied configuration, with a base, and applies a default if unset +func CheckUpdateBool(changed bool, merged **bool, old *bool, new *bool, defValue bool) bool { + if new != nil { + *merged = new + } else { + *merged = old + } + if *merged == nil { + v := defValue + *merged = &v + return true + } + return changed || old == nil || *old != **merged +} + +// CheckUpdateUint64 helper merges supplied configuration, with a base, and applies a default if unset +func CheckUpdateUint64(changed bool, merged **uint64, old *uint64, new *uint64, defValue int64) bool { + if new != nil { + *merged = new + } else { + *merged = old + } + if *merged == nil { + v := uint64(defValue) + *merged = &v + return true + } + return changed || old == nil || *old != **merged +} + +// CheckUpdateDuration helper merges supplied configuration, with a base, and applies a default if unset +func CheckUpdateDuration(changed bool, merged **fftypes.FFDuration, old *fftypes.FFDuration, new *fftypes.FFDuration, defValue fftypes.FFDuration) bool { + if new != nil { + *merged = new + } else { + *merged = old + } + if *merged == nil { + v := defValue + *merged = &v + return true + } + return changed || old == nil || *old != **merged +} + +// CheckUpdateEnum helper merges supplied configuration, with a base, and applies a default if unset +func CheckUpdateEnum(changed bool, merged **fftypes.FFEnum, old *fftypes.FFEnum, new *fftypes.FFEnum, defValue fftypes.FFEnum) bool { + if new != nil { + *merged = new + } else { + *merged = old + } + if *merged == nil { + v := defValue + *merged = &v + return true + } + return changed || old == nil || *old != **merged +} + +// CheckUpdateStringMap helper merges supplied configuration, with a base, and applies a default if unset +func CheckUpdateStringMap(changed bool, merged *map[string]string, old map[string]string, new map[string]string) bool { + if new != nil { + *merged = new + changed = changed || (old == nil) + } else { + *merged = old + return false // new was nil, we cannot have changed + } + if changed { + return true + } + // We need to compare otherwise + jsonOld, _ := json.Marshal(old) + jsonNew, _ := json.Marshal(new) + return !bytes.Equal(jsonOld, jsonNew) +} + +type EventContext struct { + StreamID *fftypes.UUID `json:"streamId"` // the ID of the event stream for this event + EthCompatSubID *fftypes.UUID `json:"subId"` // ID of the listener - EthCompat "subscription" naming + ListenerName string `json:"listenerName"` // name of the listener +} + +// EventWithContext is what is delivered +// There is custom serialization to flatten the whole structure, so all the custom `info` fields from the +// connector are alongside the required context fields. +// The `data` is kept separate +type EventWithContext struct { + StandardContext EventContext + ffcapi.Event +} + +func (e *EventWithContext) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}) + if e.Info != nil { + jsonmap.AddJSONFieldsToMap(reflect.ValueOf(e.Info), m) + } + jsonmap.AddJSONFieldsToMap(reflect.ValueOf(&e.ID), m) + jsonmap.AddJSONFieldsToMap(reflect.ValueOf(&e.StandardContext), m) + m["data"] = e.Data + return json.Marshal(m) +} + +// Note on unmarshal info will be a map with all the fields (except "data") +func (e *EventWithContext) UnmarshalJSON(b []byte) error { + var m fftypes.JSONObject + err := json.Unmarshal(b, &m) + if err == nil && m != nil { + e.Info = m + data := m["data"] + delete(m, "data") + if data != nil { + b, _ := json.Marshal(&data) + e.Data = fftypes.JSONAnyPtrBytes(b) + } + err = json.Unmarshal(b, &e.ID) + if err == nil { + err = json.Unmarshal(b, &e.StandardContext) + } + } + return err +} diff --git a/pkg/apitypes/api_types_test.go b/pkg/apitypes/api_types_test.go new file mode 100644 index 00000000..f6e04c07 --- /dev/null +++ b/pkg/apitypes/api_types_test.go @@ -0,0 +1,254 @@ +// 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 apitypes + +import ( + "encoding/json" + "testing" + "time" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" +) + +func TestCheckUpdateString(t *testing.T) { + var val1 = "val1" + var val2 = "val2" + var pVal3 *string + + changed := CheckUpdateString(false, &pVal3, nil, nil, "defVal") + assert.Equal(t, "defVal", *pVal3) // the default won + assert.True(t, changed) + + changed = CheckUpdateString(false, &pVal3, &val1, &val2, "differentDefault") + assert.Equal(t, "val2", *pVal3) // val2 won + assert.True(t, changed) + + changed = CheckUpdateString(true, &pVal3, &val2, &val2, "differentDefault") + assert.Equal(t, "val2", *pVal3) + assert.True(t, changed) // because it was already changed + + changed = CheckUpdateString(false, &pVal3, &val2, &val2, "differentDefault") + assert.Equal(t, "val2", *pVal3) + assert.False(t, changed) // the value hasn't changed + + changed = CheckUpdateString(false, &pVal3, &val1, nil, "differentDefault") + assert.Equal(t, "val1", *pVal3) // val1 won + assert.False(t, changed) // which was the current value +} + +func TestCheckUpdateBool(t *testing.T) { + var val1 = true + var val2 = false + var pVal3 *bool + + changed := CheckUpdateBool(false, &pVal3, nil, nil, true) + assert.Equal(t, true, *pVal3) // the default won + assert.True(t, changed) + + changed = CheckUpdateBool(false, &pVal3, &val1, &val2, false) + assert.Equal(t, false, *pVal3) // val2 won + assert.True(t, changed) + + changed = CheckUpdateBool(true, &pVal3, &val2, &val2, false) + assert.Equal(t, false, *pVal3) + assert.True(t, changed) // because it was already changed + + changed = CheckUpdateBool(false, &pVal3, &val2, &val2, false) + assert.Equal(t, false, *pVal3) + assert.False(t, changed) // the value hasn't changed + + changed = CheckUpdateBool(false, &pVal3, &val1, nil, false) + assert.Equal(t, true, *pVal3) // val1 won + assert.False(t, changed) // which was the current value +} + +func TestCheckUpdateInt64(t *testing.T) { + var val1 uint64 = 1111 + var val2 uint64 = 2222 + var pVal3 *uint64 + + changed := CheckUpdateUint64(false, &pVal3, nil, nil, 3333) + assert.Equal(t, uint64(3333), *pVal3) // the default won + assert.True(t, changed) + + changed = CheckUpdateUint64(false, &pVal3, &val1, &val2, 4444) + assert.Equal(t, uint64(2222), *pVal3) // val2 won + assert.True(t, changed) + + changed = CheckUpdateUint64(true, &pVal3, &val2, &val2, 4444) + assert.Equal(t, uint64(2222), *pVal3) + assert.True(t, changed) // because it was already changed + + changed = CheckUpdateUint64(false, &pVal3, &val2, &val2, 4444) + assert.Equal(t, uint64(2222), *pVal3) + assert.False(t, changed) // the value hasn't changed + + changed = CheckUpdateUint64(false, &pVal3, &val1, nil, 4444) + assert.Equal(t, uint64(1111), *pVal3) // val1 won + assert.False(t, changed) // which was the current value +} + +func TestCheckUpdateDuration(t *testing.T) { + var val1 fftypes.FFDuration = fftypes.FFDuration(1111 * time.Second) + var val2 fftypes.FFDuration = fftypes.FFDuration(2222 * time.Second) + var pVal3 *fftypes.FFDuration + + changed := CheckUpdateDuration(false, &pVal3, nil, nil, fftypes.FFDuration(3333*time.Second)) + assert.Equal(t, fftypes.FFDuration(3333*time.Second), *pVal3) // the default won + assert.True(t, changed) + + changed = CheckUpdateDuration(false, &pVal3, &val1, &val2, fftypes.FFDuration(4444*time.Second)) + assert.Equal(t, fftypes.FFDuration(2222*time.Second), *pVal3) // val2 won + assert.True(t, changed) + + changed = CheckUpdateDuration(true, &pVal3, &val2, &val2, fftypes.FFDuration(4444*time.Second)) + assert.Equal(t, fftypes.FFDuration(2222*time.Second), *pVal3) + assert.True(t, changed) // because it was already changed + + changed = CheckUpdateDuration(false, &pVal3, &val2, &val2, fftypes.FFDuration(4444*time.Second)) + assert.Equal(t, fftypes.FFDuration(2222*time.Second), *pVal3) + assert.False(t, changed) // the value hasn't changed + + changed = CheckUpdateDuration(false, &pVal3, &val1, nil, fftypes.FFDuration(4444*time.Second)) + assert.Equal(t, fftypes.FFDuration(1111*time.Second), *pVal3) // val1 won + assert.False(t, changed) // which was the current value +} + +func TestCheckUpdateEnum(t *testing.T) { + var val1 fftypes.FFEnum = fftypes.FFEnum("val1") + var val2 fftypes.FFEnum = fftypes.FFEnum("val2") + var pVal3 *fftypes.FFEnum + + changed := CheckUpdateEnum(false, &pVal3, nil, nil, fftypes.FFEnum("def1")) + assert.Equal(t, fftypes.FFEnum("def1"), *pVal3) // the default won + assert.True(t, changed) + + changed = CheckUpdateEnum(false, &pVal3, &val1, &val2, fftypes.FFEnum("def2")) + assert.Equal(t, fftypes.FFEnum("val2"), *pVal3) // val2 won + assert.True(t, changed) + + changed = CheckUpdateEnum(true, &pVal3, &val2, &val2, fftypes.FFEnum("def2")) + assert.Equal(t, fftypes.FFEnum("val2"), *pVal3) + assert.True(t, changed) // because it was already changed + + changed = CheckUpdateEnum(false, &pVal3, &val2, &val2, fftypes.FFEnum("def2")) + assert.Equal(t, fftypes.FFEnum("val2"), *pVal3) + assert.False(t, changed) // the value hasn't changed + + changed = CheckUpdateEnum(false, &pVal3, &val1, nil, fftypes.FFEnum("def2")) + assert.Equal(t, fftypes.FFEnum("val1"), *pVal3) // val1 won + assert.False(t, changed) // which was the current value +} + +func TestCheckUpdateStringMap(t *testing.T) { + val1 := map[string]string{"key1": "val1"} + val2 := map[string]string{"key2": "val2"} + var pVal3 map[string]string + + changed := CheckUpdateStringMap(false, &pVal3, val1, val2) + assert.Equal(t, map[string]string{"key2": "val2"}, pVal3) // val2 won + assert.True(t, changed) + + changed = CheckUpdateStringMap(true, &pVal3, val2, val2) + assert.Equal(t, map[string]string{"key2": "val2"}, pVal3) + assert.True(t, changed) // because it was already changed + + changed = CheckUpdateStringMap(false, &pVal3, val2, val2) + assert.Equal(t, map[string]string{"key2": "val2"}, pVal3) + assert.False(t, changed) // the value hasn't changed + + changed = CheckUpdateStringMap(false, &pVal3, val1, nil) + assert.Equal(t, map[string]string{"key1": "val1"}, pVal3) // val1 won + assert.False(t, changed) // which was the current value +} + +func TestMarshalUnmarshalEventOK(t *testing.T) { + + type customInfo struct { + InfoKey1 string `json:"key1"` + } + + e := &EventWithContext{ + StandardContext: EventContext{ + StreamID: NewULID(), + ListenerName: "listener1", + EthCompatSubID: NewULID(), + }, + Event: ffcapi.Event{ + ID: ffcapi.EventID{ + ListenerID: fftypes.NewUUID(), + BlockHash: "0x12345", + BlockNumber: 12345, + TransactionHash: "0x23456", + TransactionIndex: 10, + LogIndex: 1, + Signature: "ev()", + }, + Info: &customInfo{ + InfoKey1: "val1", + }, + Data: fftypes.JSONAnyPtr(`{"dk1":"dv1"}`), + }, + } + + b, err := json.Marshal(&e) + assert.NoError(t, err) + assert.JSONEq(t, `{ + "blockHash":"0x12345", + "blockNumber":"12345", + "data": {"dk1":"dv1"}, + "key1":"val1", + "listenerId":"`+e.ID.ListenerID.String()+`", + "listenerName":"listener1", + "logIndex":"1", + "signature":"ev()", + "subId":"`+e.StandardContext.EthCompatSubID.String()+`", + "streamId":"`+e.StandardContext.StreamID.String()+`", + "transactionHash":"0x23456", + "transactionIndex":"10" + }`, string(b)) + + var e2 *EventWithContext + err = json.Unmarshal(b, &e2) + assert.NoError(t, err) + + assert.Equal(t, e.ID.ListenerID, e2.ID.ListenerID) + assert.Equal(t, e.StandardContext.StreamID, e2.StandardContext.StreamID) + assert.Equal(t, e.Data, e2.Data) + assert.Equal(t, "val1", e2.Info.(fftypes.JSONObject).GetString("key1")) + +} + +func TestMarshalUnmarshalEmptyInfoOk(t *testing.T) { + + e := &EventWithContext{} + + _, err := json.Marshal(&e) + assert.NoError(t, err) + +} + +func TestUnmarshalFail(t *testing.T) { + + e := &EventWithContext{} + + err := json.Unmarshal([]byte(`!bad JSON`), &e) + assert.Error(t, err) + +} diff --git a/pkg/apitypes/base_request.go b/pkg/apitypes/base_request.go new file mode 100644 index 00000000..20c0f5d4 --- /dev/null +++ b/pkg/apitypes/base_request.go @@ -0,0 +1,51 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apitypes + +import "encoding/json" + +// BaseRequest is the common headers to all requests, and captures the full input payload for later decoding to a specific type +type BaseRequest struct { + headerDecoder + fullPayload []byte +} + +type headerDecoder struct { + Headers RequestHeaders `json:"headers"` +} + +func (br *BaseRequest) UnmarshalJSON(data []byte) error { + br.fullPayload = data + return json.Unmarshal(data, &br.headerDecoder) +} + +func (br *BaseRequest) UnmarshalTo(o interface{}) error { + return json.Unmarshal(br.fullPayload, &o) +} + +type RequestHeaders struct { + ID string `ffstruct:"fftmrequest" json:"id"` + Type RequestType `json:"type"` +} + +type RequestType string + +const ( + RequestTypeSendTransaction RequestType = "SendTransaction" + RequestTypeQuery RequestType = "Query" + RequestTypeDeploy RequestType = "DeployContract" +) diff --git a/pkg/apitypes/base_request_test.go b/pkg/apitypes/base_request_test.go new file mode 100644 index 00000000..05e4713a --- /dev/null +++ b/pkg/apitypes/base_request_test.go @@ -0,0 +1,58 @@ +// 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 apitypes + +import ( + "encoding/json" + "testing" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" +) + +func TestBaseRequestDecoding(t *testing.T) { + + sampleRequest := &TransactionRequest{ + Headers: RequestHeaders{ + Type: RequestTypeSendTransaction, + ID: fftypes.NewUUID().String(), + }, + TransactionInput: ffcapi.TransactionInput{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x12345", + }, + }, + } + + j, err := json.Marshal(&sampleRequest) + assert.NoError(t, err) + + var br BaseRequest + err = json.Unmarshal(j, &br) + assert.NoError(t, err) + + assert.Equal(t, RequestTypeSendTransaction, br.Headers.Type) + assert.Equal(t, sampleRequest.Headers.ID, br.Headers.ID) + + var receivedRequest TransactionRequest + err = br.UnmarshalTo(&receivedRequest) + assert.NoError(t, err) + + assert.Equal(t, "0x12345", receivedRequest.TransactionInput.From) + +} diff --git a/pkg/apitypes/managed_tx.go b/pkg/apitypes/managed_tx.go new file mode 100644 index 00000000..88d69f04 --- /dev/null +++ b/pkg/apitypes/managed_tx.go @@ -0,0 +1,99 @@ +// 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 apitypes + +import ( + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" +) + +// TxStatus is the current status of a transaction +type TxStatus string + +const ( + // TxStatusPending indicates the operation has been submitted, but is not yet confirmed as successful or failed + TxStatusPending TxStatus = "Pending" + // TxStatusSucceeded the infrastructure runtime has returned success for the operation + TxStatusSucceeded TxStatus = "Succeeded" + // TxStatusFailed happens when an error is reported by the infrastructure runtime + TxStatusFailed TxStatus = "Failed" +) + +type ManagedTXError struct { + Time *fftypes.FFTime `json:"time"` + Error string `json:"error,omitempty"` + Mapped ffcapi.ErrorReason `json:"mapped,omitempty"` +} + +// ManagedTX is the structure stored for each new transaction request, using the external ID of the operation +// +// Indexing: +// Multiple index collection are stored for the managed transactions, to allow them to be managed including: +// +// - Nonce allocation: this is a critical index, and why cleanup is so important (mentioned below). +// We use this index to determine the next nonce to assign to a given signing key. +// - Created time: a timestamp ordered index for the transactions for convenient ordering. +// the key includes the ID of the TX for uniqueness. +// - Pending sequence: An entry in this index only exists while the transaction is pending, and is +// ordered by a UUIDv1 sequence allocated to each entry. +// +// Index cleanup after partial write: +// - All indexes are stored before the TX itself. +// - When listing back entries, the persistence layer will automatically clean up indexes if the underlying +// TX they refer to is not available. For this reason the index records are written first. +type ManagedTX struct { + ID string `json:"id"` + Created *fftypes.FFTime `json:"created"` + Updated *fftypes.FFTime `json:"updated"` + Status TxStatus `json:"status"` + SequenceID *fftypes.UUID `json:"sequenceId"` + Nonce *fftypes.FFBigInt `json:"nonce"` + Gas *fftypes.FFBigInt `json:"gas"` + TransactionHeaders ffcapi.TransactionHeaders `json:"transactionHeaders"` + TransactionData string `json:"transactionData"` + TransactionHash string `json:"transactionHash,omitempty"` + GasPrice *fftypes.JSONAny `json:"gasPrice"` + PolicyInfo *fftypes.JSONAny `json:"policyInfo"` + FirstSubmit *fftypes.FFTime `json:"firstSubmit,omitempty"` + LastSubmit *fftypes.FFTime `json:"lastSubmit,omitempty"` + Receipt *ffcapi.TransactionReceiptResponse `json:"receipt,omitempty"` + ErrorMessage string `json:"errorMessage,omitempty"` + ErrorHistory []*ManagedTXError `json:"errorHistory"` + Confirmations []confirmations.BlockInfo `json:"confirmations,omitempty"` +} + +type ReplyType string + +const ( + TransactionUpdate ReplyType = "TransactionUpdate" + TransactionUpdateSuccess ReplyType = "TransactionSuccess" + TransactionUpdateFailure ReplyType = "TransactionFailure" +) + +type ReplyHeaders struct { + RequestID string `json:"requestId"` + Type ReplyType `json:"type"` +} + +// TransactionUpdateReply add a "headers" structure that allows a processor of websocket +// replies/updates to filter on a standard structure to know how to process the message. +// Extensible to update update types in the future. +type TransactionUpdateReply struct { + Headers ReplyHeaders `json:"headers"` + ManagedTX +} diff --git a/cmd/config.go b/pkg/apitypes/query_request.go similarity index 57% rename from cmd/config.go rename to pkg/apitypes/query_request.go index 7cae3ea1..50668df8 100644 --- a/cmd/config.go +++ b/pkg/apitypes/query_request.go @@ -14,27 +14,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -package cmd +package apitypes import ( - "context" - "fmt" - - "github.com/hyperledger/firefly-common/pkg/config" - "github.com/spf13/cobra" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) -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 +// QueryRequest is the request payload to send to perform a synchronous query against the blockchain state +type QueryRequest struct { + Headers RequestHeaders `json:"headers"` + ffcapi.TransactionInput } + +// QueryResponse is the response payload for a query +type QueryResponse ffcapi.QueryInvokeResponse diff --git a/pkg/fftm/tx_request.go b/pkg/apitypes/tx_request.go similarity index 60% rename from pkg/fftm/tx_request.go rename to pkg/apitypes/tx_request.go index 8895571a..9da83fa5 100644 --- a/pkg/fftm/tx_request.go +++ b/pkg/apitypes/tx_request.go @@ -14,27 +14,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -package fftm +package apitypes import ( - "github.com/hyperledger/firefly-common/pkg/ffcapi" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) -// TransactionRequest is the external interface into sending transactions to the front-side of Transaction Manager -// Note this is a deliberate match for the EthConnect subset that is supported by FireFly core +// TransactionRequest is the payload sent to initiate a new transaction type TransactionRequest struct { Headers RequestHeaders `json:"headers"` ffcapi.TransactionInput } -type RequestHeaders struct { - ID string `json:"id"` - Type RequestType `json:"type"` +// ContractDeployRequest is the payload sent to initiate a new transaction +type ContractDeployRequest struct { + Headers RequestHeaders `json:"headers"` + ffcapi.ContractDeployPrepareRequest } - -type RequestType string - -const ( - RequestTypeSendTransaction = "SendTransaction" - RequestTypeQuery = "Query" -) diff --git a/pkg/apitypes/ulid.go b/pkg/apitypes/ulid.go new file mode 100644 index 00000000..72ae1219 --- /dev/null +++ b/pkg/apitypes/ulid.go @@ -0,0 +1,40 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apitypes + +import ( + "crypto/rand" + "time" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/oklog/ulid/v2" +) + +var ulidReader = &ulid.LockedMonotonicReader{ + MonotonicReader: &ulid.MonotonicEntropy{ + Reader: rand.Reader, + }, +} + +// NewULID returns a Universally Unique Lexicographically Sortable Identifier (ULID). +// For consistency we impersonate the formatting of a UUID, so they can be used +// interchangeably. +// This can be used in database tables to ensure monotonic increasing identifiers. +func NewULID() *fftypes.UUID { + u := ulid.MustNew(ulid.Timestamp(time.Now()), ulidReader) + return (*fftypes.UUID)(&u) +} diff --git a/cmd/config_test.go b/pkg/apitypes/ulid_test.go similarity index 80% rename from cmd/config_test.go rename to pkg/apitypes/ulid_test.go index 8ccdd1bf..d9c6560c 100644 --- a/cmd/config_test.go +++ b/pkg/apitypes/ulid_test.go @@ -14,17 +14,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -package cmd +package apitypes import ( + "strings" "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) +func TestULID(t *testing.T) { + u1 := NewULID() + u2 := NewULID() + assert.Negative(t, strings.Compare(u1.String(), u2.String())) } diff --git a/pkg/ffcapi/api.go b/pkg/ffcapi/api.go new file mode 100644 index 00000000..3458b319 --- /dev/null +++ b/pkg/ffcapi/api.go @@ -0,0 +1,207 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ffcapi + +import ( + "context" + "fmt" + + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +// API is the interface to the blockchain specific connector, from the FFTM server and policy engine. +// +// The functions follow a consistent pattern of request/response objects, to allow extensibility of the +// inputs/outputs with minimal code change to existing connector implementations. +type API interface { + + // BlockInfoByHash gets block information using the hash of the block + BlockInfoByHash(ctx context.Context, req *BlockInfoByHashRequest) (*BlockInfoByHashResponse, ErrorReason, error) + + // BlockInfoByNumber gets block information from the specified position (block number/index) in the canonical chain currently known to the local node + BlockInfoByNumber(ctx context.Context, req *BlockInfoByNumberRequest) (*BlockInfoByNumberResponse, ErrorReason, error) + + // NextNonceForSigner is used when there are no outstanding transactions for a given signing identity, to determine the next nonce to use for submission of a transaction + NextNonceForSigner(ctx context.Context, req *NextNonceForSignerRequest) (*NextNonceForSignerResponse, ErrorReason, error) + + // GasPriceEstimate provides a blockchain specific gas price estimate + GasPriceEstimate(ctx context.Context, req *GasPriceEstimateRequest) (*GasPriceEstimateResponse, ErrorReason, error) + + // QueryInvoke executes a method on a blockchain smart contract, which might execute Smart Contract code, but does not affect the blockchain state. + QueryInvoke(ctx context.Context, req *QueryInvokeRequest) (*QueryInvokeResponse, ErrorReason, error) + + // TransactionReceipt queries to see if a receipt is available for a given transaction hash + TransactionReceipt(ctx context.Context, req *TransactionReceiptRequest) (*TransactionReceiptResponse, ErrorReason, error) + + // TransactionPrepare validates transaction inputs against the supplied schema/ABI and performs any binary serialization required (prior to signing) to encode a transaction from JSON into the native blockchain format + TransactionPrepare(ctx context.Context, req *TransactionPrepareRequest) (*TransactionPrepareResponse, ErrorReason, error) + + // TransactionSend combines a previously prepared encoded transaction, with a current gas price, and submits it to the transaction pool of the blockchain for mining + TransactionSend(ctx context.Context, req *TransactionSendRequest) (*TransactionSendResponse, ErrorReason, error) + + // DeployContractPrepare + DeployContractPrepare(ctx context.Context, req *ContractDeployPrepareRequest) (*TransactionPrepareResponse, ErrorReason, error) + + // EventStreamStart starts an event stream with an initial set of listeners (which might be empty), a channel to deliver events, and a context that will close to stop the stream + EventStreamStart(ctx context.Context, req *EventStreamStartRequest) (*EventStreamStartResponse, ErrorReason, error) + + // EventStreamStopped informs a connector that an event stream has been requested to stop, and the context has been cancelled. So the state associated with it can be removed (and a future start of the same ID can be performed) + EventStreamStopped(ctx context.Context, req *EventStreamStoppedRequest) (*EventStreamStoppedResponse, ErrorReason, error) + + // EventListenerVerifyOptions validates the configuration options for a listener, applying any defaults needed by the connector, and returning the update options for FFTM to persist + EventListenerVerifyOptions(ctx context.Context, req *EventListenerVerifyOptionsRequest) (*EventListenerVerifyOptionsResponse, ErrorReason, error) + + // EventListenerAdd begins/resumes listening on set of events that must be consistently ordered. Blockchain specific signatures of the events are included, along with initial conditions (initial block number etc.), and the last stored checkpoint (if any) + EventListenerAdd(ctx context.Context, req *EventListenerAddRequest) (*EventListenerAddResponse, ErrorReason, error) + + // EventListenerRemove ends listening on a set of events previous started + EventListenerRemove(ctx context.Context, req *EventListenerRemoveRequest) (*EventListenerRemoveResponse, ErrorReason, error) + + // EventListenerHWM queries the current high water mark checkpoint for a listener. Called at regular intervals when there are no events in flight for a listener, to ensure checkpoint are written regularly even when there is no activity + EventListenerHWM(ctx context.Context, req *EventListenerHWMRequest) (*EventListenerHWMResponse, ErrorReason, error) + + // EventStreamNewCheckpointStruct used during checkpoint restore, to get the specific into which to restore the JSON bytes + EventStreamNewCheckpointStruct() EventListenerCheckpoint + + // NewBlockListener creates a new block listener, decoupled from an event stream + NewBlockListener(ctx context.Context, req *NewBlockListenerRequest) (*NewBlockListenerResponse, ErrorReason, error) +} + +type BlockHashEvent struct { + BlockHashes []string `json:"blockHash"` // zero or more hashes (can be nil) + GapPotential bool `json:"gapPotential,omitempty"` // when true, the caller cannot be sure if blocks have been missed (use on reconnect of a websocket for example) +} + +// EventID are the set of required fields an FFCAPI compatible connector needs to map to the underlying blockchain constructs, to uniquely identify an event +type EventID struct { + ListenerID *fftypes.UUID `json:"listenerId"` // The listener for the event + Signature string `json:"signature"` // The signature of this specific event (noting a listener might filter on multiple events) + BlockHash string `json:"blockHash"` // String representation of the block, which will change if any transaction info in the block changes + BlockNumber fftypes.FFuint64 `json:"blockNumber"` // A numeric identifier for the block + TransactionHash string `json:"transactionHash"` // The transaction + TransactionIndex fftypes.FFuint64 `json:"transactionIndex"` // Index within the block of the transaction that emitted the event + LogIndex fftypes.FFuint64 `json:"logIndex"` // Index within the transaction of this emitted event log + Timestamp *fftypes.FFTime `json:"timestamp,omitempty"` // The on-chain timestamp +} + +// Event is a blockchain event that matches one of the started listeners, +// and is the structure passed from the connector to FFTM +// The implementation is responsible for ensuring all events on a listener are +// ordered on to this channel in the exact sequence from the blockchain. +type Event struct { + ID EventID // standard fields provided by the connector + Info interface{} // extra custom fields from the connector - can be any JSON serializable struct + Data *fftypes.JSONAny // data +} + +func (e *Event) String() string { + return e.ID.String() +} + +// EventListenerCheckpoint is the interface that a checkpoint must implement, basically to make it sortable. +// The checkpoint must also be JSON serializable +type EventListenerCheckpoint interface { + LessThan(b EventListenerCheckpoint) bool +} + +// String is unique in all cases for an event, by combining the protocol ID with the listener ID and block hash +func (eid *EventID) String() string { + return fmt.Sprintf("%s/B=%s/L=%s", eid.ProtocolID(), eid.BlockHash, eid.ListenerID) +} + +// ProtocolID represents the unique (once finality is reached) sortable position within the blockchain +func (eid *EventID) ProtocolID() string { + return fmt.Sprintf("%.12d/%.6d/%.6d", eid.BlockNumber, eid.TransactionIndex, eid.LogIndex) +} + +// Events array has a natural sort order of the block/txIndex/logIndex +type Events []*Event + +func (es Events) Len() int { return len(es) } +func (es Events) Swap(i, j int) { es[i], es[j] = es[j], es[i] } +func (es Events) Less(i, j int) bool { return evLess(es[i], es[j]) } + +// ListenerEvents array has a natural sort order of the event +type ListenerEvents []*ListenerEvent + +func (lu ListenerEvents) Len() int { return len(lu) } +func (lu ListenerEvents) Swap(i, j int) { lu[i], lu[j] = lu[j], lu[i] } +func (lu ListenerEvents) Less(i, j int) bool { return evLess(lu[i].Event, lu[j].Event) } + +func evLess(eI *Event, eJ *Event) bool { + return eI.ID.BlockNumber < eJ.ID.BlockNumber || + ((eI.ID.BlockNumber == eJ.ID.BlockNumber) && + ((eI.ID.TransactionIndex < eJ.ID.TransactionIndex) || + ((eI.ID.TransactionIndex == eJ.ID.TransactionIndex) && (eI.ID.LogIndex < eJ.ID.LogIndex)))) +} + +// ListenerEvent is an event+checkpoint for a particular listener, and is the object delivered over the event stream channel when +// a new event is detected for delivery to the confirmation manager. +type ListenerEvent struct { + Checkpoint EventListenerCheckpoint `json:"checkpoint"` // the checkpoint information associated with the event, must be non-nil if the event is not removed + Event *Event `json:"event"` // the event - for removed events, can only have the EventID fields set (to generate the protocol ID) + Removed bool `json:"removed,omitempty"` // when true, this is an explicit cancellation of a previous event +} + +// ErrorReason are a set of standard error conditions that a blockchain connector can return +// from execution, that affect the action of the transaction manager to the response. +// It is important that error mapping is performed for each of these classification +type ErrorReason string + +const ( + // ErrorReasonInvalidInputs transaction inputs could not be parsed by the connector according to the interface (nothing was sent to the blockchain) + ErrorReasonInvalidInputs ErrorReason = "invalid_inputs" + // ErrorReasonTransactionReverted on-chain execution (only expected to be returned when the connector is doing gas estimation, or executing a query) + ErrorReasonTransactionReverted ErrorReason = "transaction_reverted" + // ErrorReasonNonceTooLow on transaction submission, if the nonce has already been used for a transaction that has made it into a block on the canonical chain known to the local node + ErrorReasonNonceTooLow ErrorReason = "nonce_too_low" + // ErrorReasonTransactionUnderpriced if the transaction is rejected due to too low gas price. Either because it was too low according to the minimum configured on the node, or because it's a rescue transaction without a price bump. + ErrorReasonTransactionUnderpriced ErrorReason = "transaction_underpriced" + // ErrorReasonInsufficientFunds if the transaction is rejected due to not having enough of the underlying network coin (ether etc.) in your wallet + ErrorReasonInsufficientFunds ErrorReason = "insufficient_funds" + // ErrorReasonNotFound if the requested object (block/receipt etc.) was not found + ErrorReasonNotFound ErrorReason = "not_found" + // ErrorKnownTransaction if the exact transaction is already known + ErrorKnownTransaction ErrorReason = "known_transaction" +) + +// TransactionInput is a standardized set of parameters that describe a transaction submission to a blockchain. +// For convenience, ths structure is compatible with the EthConnect `TransactionSend` structure, for the subset of usage made by FireFly core / Tokens connectors. +// - Numeric values such as nonce/gas/gasPrice, are all passed as string encoded Base 10 integers +// - From/To are passed as strings, and are pass-through for FFTM from the values it receives from FireFly core after signing key resolution +// - The interface is a structure describing the method to invoke. The `variant` in the header tells you how to decode it. For variant=evm it will be an ABI method definition +// - The supplied value is passed through for each input parameter. It could be any JSON type (simple number/boolean/string, or complex object/array). The blockchain connection is responsible for serializing these according to the rules in the interface. +type TransactionInput struct { + TransactionHeaders + Method *fftypes.JSONAny `json:"method"` + Params []*fftypes.JSONAny `json:"params"` +} + +type TransactionHeaders struct { + From string `json:"from,omitempty"` + To string `json:"to,omitempty"` + Nonce *fftypes.FFBigInt `json:"nonce,omitempty"` + Gas *fftypes.FFBigInt `json:"gas,omitempty"` + Value *fftypes.FFBigInt `json:"value,omitempty"` +} + +type BlockInfo struct { + BlockNumber *fftypes.FFBigInt `json:"blockNumber"` + BlockHash string `json:"blockHash"` + ParentHash string `json:"parentHash"` + TransactionHashes []string `json:"transactionHashes"` +} diff --git a/pkg/ffcapi/api_test.go b/pkg/ffcapi/api_test.go new file mode 100644 index 00000000..2cf29d64 --- /dev/null +++ b/pkg/ffcapi/api_test.go @@ -0,0 +1,57 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ffcapi + +import ( + "crypto/rand" + "math/big" + "sort" + "strings" + "testing" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/stretchr/testify/assert" +) + +func TestSortEvents(t *testing.T) { + + events := make(Events, 10000) + listenerUpdates := make(ListenerEvents, len(events)) + for i := 0; i < 10000; i++ { + b, _ := rand.Int(rand.Reader, big.NewInt(1000)) + t, _ := rand.Int(rand.Reader, big.NewInt(10)) + l, _ := rand.Int(rand.Reader, big.NewInt(10)) + events[i] = &Event{ + ID: EventID{ + BlockNumber: fftypes.FFuint64(b.Uint64()), + TransactionIndex: fftypes.FFuint64(t.Uint64()), + LogIndex: fftypes.FFuint64(l.Uint64()), + }, + } + listenerUpdates[i] = &ListenerEvent{ + Event: events[i], + } + } + sort.Sort(events) + sort.Sort(listenerUpdates) + + for i := 1; i < len(events); i++ { + assert.LessOrEqual(t, strings.Compare(events[i-1].ID.ProtocolID(), events[i].ID.ProtocolID()), 0) + assert.LessOrEqual(t, strings.Compare(events[i-1].String(), events[i].String()), 0) + assert.LessOrEqual(t, strings.Compare(listenerUpdates[i-1].Event.ID.ProtocolID(), listenerUpdates[i].Event.ID.ProtocolID()), 0) + } +} diff --git a/pkg/ffcapi/block_info_by_hash.go b/pkg/ffcapi/block_info_by_hash.go new file mode 100644 index 00000000..7adb02b8 --- /dev/null +++ b/pkg/ffcapi/block_info_by_hash.go @@ -0,0 +1,25 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ffcapi + +type BlockInfoByHashRequest struct { + BlockHash string `json:"blockHash"` +} + +type BlockInfoByHashResponse struct { + BlockInfo +} diff --git a/pkg/ffcapi/block_info_by_number.go b/pkg/ffcapi/block_info_by_number.go new file mode 100644 index 00000000..d9c26112 --- /dev/null +++ b/pkg/ffcapi/block_info_by_number.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 ffcapi + +import ( + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +type BlockInfoByNumberRequest struct { + BlockNumber *fftypes.FFBigInt `json:"blockNumber"` + ExpectedParentHash string `json:"expectedParentHash"` // If set then a mismatched parent hash should be considered a cache miss (if the connector does caching) +} + +type BlockInfoByNumberResponse struct { + BlockInfo +} diff --git a/pkg/ffcapi/contract_deploy_prepare.go b/pkg/ffcapi/contract_deploy_prepare.go new file mode 100644 index 00000000..cde62ebd --- /dev/null +++ b/pkg/ffcapi/contract_deploy_prepare.go @@ -0,0 +1,28 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ffcapi + +import ( + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +type ContractDeployPrepareRequest struct { + TransactionHeaders + Definition *fftypes.JSONAny `json:"definition"` // such as an ABI for EVM + Contract *fftypes.JSONAny `json:"contract"` // such as the Bytecode for EVM + Params []*fftypes.JSONAny `json:"params"` // such as the inputs to the constructor for EVM +} diff --git a/pkg/ffcapi/event_listener_add.go b/pkg/ffcapi/event_listener_add.go new file mode 100644 index 00000000..dea671d2 --- /dev/null +++ b/pkg/ffcapi/event_listener_add.go @@ -0,0 +1,52 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ffcapi + +import ( + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +const ( + FromBlockEarliest = "earliest" + FromBlockLatest = "latest" +) + +type EventListenerOptions struct { + FromBlock string // The instruction for the first block to index from (when there is no previous checkpoint). Special "earliest" and "latest" strings should be supported as well as blockchain specific block ID (like a decimal number etc.) + Filters []fftypes.JSONAny // The blockchain specific list of filters. The top-level array is an OR list. The semantics within each entry is defined by the blockchain + Options *fftypes.JSONAny // Blockchain specific set of options, such as the first block to detect events from (can be null) +} + +type EventListenerVerifyOptionsRequest struct { + EventListenerOptions +} + +type EventListenerVerifyOptionsResponse struct { + ResolvedSignature string + ResolvedOptions fftypes.JSONAny +} + +type EventListenerAddRequest struct { + EventListenerOptions + ListenerID *fftypes.UUID // Unique UUID for the event listener, that should be included in each event + StreamID *fftypes.UUID // The event stream (previously started) to which events should be delivered + Name string // Descriptive name of the listener, provided by the user, or defaulted to the signature. Not guaranteed to be unique. Should be included in the event info + Checkpoint EventListenerCheckpoint // The last persisted checkpoint for this event stream +} + +type EventListenerAddResponse struct { +} diff --git a/pkg/ffcapi/event_listener_hwm.go b/pkg/ffcapi/event_listener_hwm.go new file mode 100644 index 00000000..2a0c8d2f --- /dev/null +++ b/pkg/ffcapi/event_listener_hwm.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 ffcapi + +import ( + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +type EventListenerHWMRequest struct { + StreamID *fftypes.UUID `json:"streamId"` + ListenerID *fftypes.UUID `json:"listenerId"` +} + +type EventListenerHWMResponse struct { + Checkpoint EventListenerCheckpoint `json:"checkpoint"` +} diff --git a/pkg/ffcapi/event_listener_remove.go b/pkg/ffcapi/event_listener_remove.go new file mode 100644 index 00000000..dc26298d --- /dev/null +++ b/pkg/ffcapi/event_listener_remove.go @@ -0,0 +1,29 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ffcapi + +import ( + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +type EventListenerRemoveRequest struct { + StreamID *fftypes.UUID `json:"streamId"` + ListenerID *fftypes.UUID `json:"listenerId"` +} + +type EventListenerRemoveResponse struct { +} diff --git a/pkg/ffcapi/event_stream_start.go b/pkg/ffcapi/event_stream_start.go new file mode 100644 index 00000000..96f0590f --- /dev/null +++ b/pkg/ffcapi/event_stream_start.go @@ -0,0 +1,34 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ffcapi + +import ( + "context" + + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +type EventStreamStartRequest struct { + ID *fftypes.UUID // UUID of the stream, which we be referenced in any future add/remove listener requests + StreamContext context.Context // Context that will be cancelled when the event stream needs to stop - no further events will be consumed after this, so all pushes to the stream should select on the done channel too + EventStream chan<- *ListenerEvent // The event stream to push events to as they are detected, and checkpoints regularly even if there are no events - remember to select on Done as well when pushing events + BlockListener chan<- *BlockHashEvent // The connector should push new blocks to every stream, marking if it's possible blocks were missed (due to reconnect). The stream guarantees to always consume from this channel, until the stream context closes. + InitialListeners []*EventListenerAddRequest // Initial list of event listeners to start with the stream - allows these to be started concurrently +} + +type EventStreamStartResponse struct { +} diff --git a/pkg/ffcapi/event_stream_stopped.go b/pkg/ffcapi/event_stream_stopped.go new file mode 100644 index 00000000..1165e80b --- /dev/null +++ b/pkg/ffcapi/event_stream_stopped.go @@ -0,0 +1,28 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ffcapi + +import ( + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +type EventStreamStoppedRequest struct { + ID *fftypes.UUID // UUID of the stream, which we be referenced in any future add/remove listener requests +} + +type EventStreamStoppedResponse struct { +} diff --git a/fftm/main.go b/pkg/ffcapi/gas_price_estimate.go similarity index 74% rename from fftm/main.go rename to pkg/ffcapi/gas_price_estimate.go index 80b54974..06b7d59d 100644 --- a/fftm/main.go +++ b/pkg/ffcapi/gas_price_estimate.go @@ -14,19 +14,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -package main +package ffcapi -import ( - "fmt" - "os" +import "github.com/hyperledger/firefly-common/pkg/fftypes" - "github.com/hyperledger/firefly-transaction-manager/cmd" -) +type GasPriceEstimateRequest struct { +} -func main() { - if err := cmd.Execute(); err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err) - os.Exit(1) - } - os.Exit(0) +type GasPriceEstimateResponse struct { + GasPrice *fftypes.JSONAny `json:"gasPrice"` } diff --git a/pkg/ffcapi/method_call.go b/pkg/ffcapi/method_call.go new file mode 100644 index 00000000..613496f3 --- /dev/null +++ b/pkg/ffcapi/method_call.go @@ -0,0 +1,36 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ffcapi + +import ( + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +// QueryInvokeRequest requests execution of a smart contract method in order to either: +// 1) Query state +// 2) Attempt to extract the revert reason from an on-chain failure to execute a transaction +// +// See the list of standard error reasons that should be returned for situations that can be +// detected by the back-end connector. +type QueryInvokeRequest struct { + TransactionInput + BlockNumber *fftypes.FFBigInt `json:"blockNumber,omitempty"` +} + +type QueryInvokeResponse struct { + Outputs *fftypes.JSONAny `json:"outputs"` // The data output from the method call - can be array or object structure +} diff --git a/pkg/ffcapi/new_block_listener.go b/pkg/ffcapi/new_block_listener.go new file mode 100644 index 00000000..4ee68db7 --- /dev/null +++ b/pkg/ffcapi/new_block_listener.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 ffcapi + +import ( + "context" + + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +type NewBlockListenerRequest struct { + ID *fftypes.UUID // unique identifier for this listener + ListenerContext context.Context // Context that will be cancelled when the listener needs to stop - no further events will be consumed after this, so all pushes to the listener should select on the done channel too + BlockListener chan<- *BlockHashEvent // The connector should push new blocks to every listener, marking if it's possible blocks were missed (due to reconnect). The listener guarantees to always consume from this channel, until the listener context closes. +} + +type NewBlockListenerResponse struct { +} diff --git a/pkg/ffcapi/next_nonce_for_signer.go b/pkg/ffcapi/next_nonce_for_signer.go new file mode 100644 index 00000000..9c3181e5 --- /dev/null +++ b/pkg/ffcapi/next_nonce_for_signer.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 ffcapi + +import ( + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +// NextNonceForSignerRequest used to do a query for the next nonce to use for a +// given signing identity. This is only used when there are no pending +// operations outstanding for this signer known to the transaction manager. +type NextNonceForSignerRequest struct { + Signer string `json:"signer"` +} + +type NextNonceForSignerResponse struct { + Nonce *fftypes.FFBigInt `json:"nonce"` +} diff --git a/pkg/ffcapi/transaction_prepare.go b/pkg/ffcapi/transaction_prepare.go new file mode 100644 index 00000000..a9552d90 --- /dev/null +++ b/pkg/ffcapi/transaction_prepare.go @@ -0,0 +1,48 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ffcapi + +import ( + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +// TransactionPrepareRequest is used to prepare a set of JSON formatted developer friendly +// inputs, into a raw transaction ready for submission to the blockchain. +// +// The connector is responsible for encoding the transaction ready for submission, +// and returning the hash for the transaction as well as a string serialization of +// the pre-signed raw transaction in a format of its own choosing (hex etc.). +// The hash is expected to be a function of: +// - the method signature +// - the signing identity +// - the nonce +// - the particular blockchain the transaction is submitted to +// - the input parameters +// +// If "gas" is not supplied, the connector is expected to perform gas estimation +// prior to generating the payload. +// +// See the list of standard error reasons that should be returned for situations that can be +// detected by the back-end connector. +type TransactionPrepareRequest struct { + TransactionInput +} + +type TransactionPrepareResponse struct { + Gas *fftypes.FFBigInt `json:"gas"` + TransactionData string `json:"transactionData"` +} diff --git a/pkg/ffcapi/transaction_receipt.go b/pkg/ffcapi/transaction_receipt.go new file mode 100644 index 00000000..ac64d05d --- /dev/null +++ b/pkg/ffcapi/transaction_receipt.go @@ -0,0 +1,33 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ffcapi + +import ( + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +type TransactionReceiptRequest struct { + TransactionHash string `json:"transactionHash"` +} + +type TransactionReceiptResponse struct { + BlockNumber *fftypes.FFBigInt `json:"blockNumber"` + TransactionIndex *fftypes.FFBigInt `json:"transactionIndex"` + BlockHash string `json:"blockHash"` + Success bool `json:"success"` + ExtraInfo *fftypes.JSONAny `json:"extraInfo"` +} diff --git a/pkg/ffcapi/transaction_send.go b/pkg/ffcapi/transaction_send.go new file mode 100644 index 00000000..c83f68df --- /dev/null +++ b/pkg/ffcapi/transaction_send.go @@ -0,0 +1,34 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ffcapi + +import ( + "github.com/hyperledger/firefly-common/pkg/fftypes" +) + +// TransactionSendRequest is used to send a transaction to the blockchain. +// The connector is responsible for adding it to the transaction pool of the blockchain, +// noting the transaction hash has already been calculated in the prepare step previously. +type TransactionSendRequest struct { + GasPrice *fftypes.JSONAny `json:"gasPrice,omitempty"` // can be a simple string/number, or a complex object - contract is between policy engine and blockchain connector + TransactionHeaders + TransactionData string `json:"transactionData"` +} + +type TransactionSendResponse struct { + TransactionHash string `json:"transactionHash"` +} diff --git a/pkg/fftm/api.go b/pkg/fftm/api.go new file mode 100644 index 00000000..7c453ffd --- /dev/null +++ b/pkg/fftm/api.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 fftm + +import ( + "encoding/json" + "net/http" + + "github.com/ghodss/yaml" + "github.com/gorilla/mux" + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" +) + +func (m *manager) router() *mux.Router { + mux := mux.NewRouter() + hf := ffapi.HandlerFactory{ + DefaultRequestTimeout: config.GetDuration(tmconfig.APIDefaultRequestTimeout), + MaxTimeout: config.GetDuration(tmconfig.APIMaxRequestTimeout), + } + routes := m.routes() + for _, r := range routes { + mux.Path(r.Path).Methods(r.Method).Handler(hf.RouteHandler(r)) + } + mux.Path("/api").Methods(http.MethodGet).Handler(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + url := req.URL.String() + "/spec.yaml" + handler := hf.APIWrapper(hf.SwaggerUIHandler(url)) + handler(res, req) + })) + mux.Path("/api/spec.yaml").Methods(http.MethodGet).Handler(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + u := req.URL + u.Path = "" + swaggerGen := ffapi.NewSwaggerGen(&ffapi.Options{ + BaseURL: u.String(), + }) + doc := swaggerGen.Generate(req.Context(), routes) + res.Header().Add("Content-Type", "application/x-yaml") + b, _ := yaml.Marshal(&doc) + _, _ = res.Write(b) + })) + mux.Path("/api/spec.json").Methods(http.MethodGet).Handler(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + u := req.URL + u.Path = "" + swaggerGen := ffapi.NewSwaggerGen(&ffapi.Options{ + BaseURL: u.String(), + }) + doc := swaggerGen.Generate(req.Context(), routes) + res.Header().Add("Content-Type", "application/json") + b, _ := json.Marshal(&doc) + _, _ = res.Write(b) + })) + + mux.HandleFunc("/ws", m.wsServer.Handler) + + mux.NotFoundHandler = hf.APIWrapper(func(res http.ResponseWriter, req *http.Request) (status int, err error) { + return 404, i18n.NewError(req.Context(), i18n.Msg404NotFound) + }) + return mux +} + +func (m *manager) runAPIServer() { + m.apiServer.ServeHTTP(m.ctx) +} diff --git a/pkg/fftm/api_test.go b/pkg/fftm/api_test.go new file mode 100644 index 00000000..d71e70b3 --- /dev/null +++ b/pkg/fftm/api_test.go @@ -0,0 +1,452 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "fmt" + "strings" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" + "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +const sampleSendTX = `{ + "headers": { + "id": "ns1:904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "SendTransaction" + }, + "from": "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", + "to": "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771", + "gas": 1000000, + "method": { + "inputs": [ + { + "internalType":" uint256", + "name": "x", + "type": "uint256" + } + ], + "name":"set", + "outputs":[], + "stateMutability":"nonpayable", + "type":"function" + }, + "params": [ + { + "value": 4276993775, + "type": "uint256" + } + ] +}` + +const sampleDeployTX = `{ + "headers": { + "id": "ns1:904F177C-C790-4B01-BDF4-F2B4E52E607E", + "type": "DeployContract" + }, + "from": "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8", + "gas": 1000000, + "contract": "0xfeedbeef", + "definition": [{ + "inputs": [ + { + "internalType":" uint256", + "name": "x", + "type": "uint256" + } + ], + "type":"constructor" + }], + "params": [ + { + "value": 4276993775, + "type": "uint256" + } + ] +}` + +func TestSendTransactionE2E(t *testing.T) { + + txSent := make(chan struct{}) + + url, m, cancel := newTestManager(t) + defer cancel() + + mFFC := m.connector.(*ffcapimocks.API) + + mFFC.On("NextNonceForSigner", mock.Anything, mock.MatchedBy(func(nonceReq *ffcapi.NextNonceForSignerRequest) bool { + return "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8" == nonceReq.Signer + })).Return(&ffcapi.NextNonceForSignerResponse{ + Nonce: fftypes.NewFFBigInt(12345), + }, ffcapi.ErrorReason(""), nil) + + mFFC.On("TransactionPrepare", mock.Anything, mock.MatchedBy(func(prepTX *ffcapi.TransactionPrepareRequest) bool { + return "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8" == prepTX.From && + "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771" == prepTX.To && + uint64(1000000) == prepTX.Gas.Uint64() && + "set" == prepTX.Method.JSONObject().GetString("name") && + 1 == len(prepTX.Params) && + "4276993775" == prepTX.Params[0].JSONObject().GetString("value") && + "4276993775" == prepTX.Params[0].JSONObject().GetString("value") + })).Return(&ffcapi.TransactionPrepareResponse{ + TransactionData: "RAW_UNSIGNED_BYTES", + Gas: fftypes.NewFFBigInt(2000000), // gas estimate simulation + }, ffcapi.ErrorReason(""), nil) + + mFFC.On("TransactionSend", mock.Anything, mock.MatchedBy(func(sendTX *ffcapi.TransactionSendRequest) bool { + matches := "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8" == sendTX.From && + "0xe1a078b9e2b145d0a7387f09277c6ae1d9470771" == sendTX.To && + uint64(2000000) == sendTX.Gas.Uint64() && + `223344556677` == sendTX.GasPrice.String() && + "RAW_UNSIGNED_BYTES" == sendTX.TransactionData + if matches { + // We're at end of job for this test + close(txSent) + } + return matches + })).Return(&ffcapi.TransactionSendResponse{ + TransactionHash: "0x106215b9c0c9372e3f541beff0cdc3cd061a26f69f3808e28fd139a1abc9d345", + }, ffcapi.ErrorReason(""), nil) + + mc := m.confirmations.(*confirmationsmocks.Manager) + mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { + return n.NotificationType == confirmations.NewTransaction + })).Return(nil) + + m.Start() + + req := strings.NewReader(sampleSendTX) + res, err := resty.New().R(). + SetBody(req). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 202, res.StatusCode()) + + <-txSent + +} + +func TestDeployTransactionE2E(t *testing.T) { + + txSent := make(chan struct{}) + + url, m, cancel := newTestManager(t) + defer cancel() + + mFFC := m.connector.(*ffcapimocks.API) + + mFFC.On("NextNonceForSigner", mock.Anything, mock.MatchedBy(func(nonceReq *ffcapi.NextNonceForSignerRequest) bool { + return "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8" == nonceReq.Signer + })).Return(&ffcapi.NextNonceForSignerResponse{ + Nonce: fftypes.NewFFBigInt(12345), + }, ffcapi.ErrorReason(""), nil) + + mFFC.On("DeployContractPrepare", mock.Anything, mock.MatchedBy(func(prepTX *ffcapi.ContractDeployPrepareRequest) bool { + return "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8" == prepTX.From && + `constructor` == prepTX.Definition.JSONObjectArray()[0].GetString("type") && + `"0xfeedbeef"` == prepTX.Contract.String() && + uint64(1000000) == prepTX.Gas.Uint64() && + 1 == len(prepTX.Params) && + "4276993775" == prepTX.Params[0].JSONObject().GetString("value") && + "4276993775" == prepTX.Params[0].JSONObject().GetString("value") + })).Return(&ffcapi.TransactionPrepareResponse{ + TransactionData: "RAW_UNSIGNED_BYTES", + Gas: fftypes.NewFFBigInt(2000000), // gas estimate simulation + }, ffcapi.ErrorReason(""), nil) + + mFFC.On("TransactionSend", mock.Anything, mock.MatchedBy(func(sendTX *ffcapi.TransactionSendRequest) bool { + matches := "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8" == sendTX.From && + uint64(2000000) == sendTX.Gas.Uint64() && + `223344556677` == sendTX.GasPrice.String() && + "RAW_UNSIGNED_BYTES" == sendTX.TransactionData + if matches { + // We're at end of job for this test + close(txSent) + } + return matches + })).Return(&ffcapi.TransactionSendResponse{ + TransactionHash: "0x106215b9c0c9372e3f541beff0cdc3cd061a26f69f3808e28fd139a1abc9d345", + }, ffcapi.ErrorReason(""), nil) + + mc := m.confirmations.(*confirmationsmocks.Manager) + mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { + return n.NotificationType == confirmations.NewTransaction + })).Return(nil) + + m.Start() + + req := strings.NewReader(sampleDeployTX) + res, err := resty.New().R(). + SetBody(req). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 202, res.StatusCode()) + + <-txSent + +} + +func TestSendInvalidRequestBadTXType(t *testing.T) { + + url, m, cancel := newTestManager(t) + defer cancel() + m.Start() + + req := strings.NewReader(`{ + "headers": { + "type": "SendTransaction" + }, + "from": { + "Not": "a string" + } + }`) + var errRes fftypes.RESTError + res, err := resty.New().R(). + SetBody(req). + SetError(&errRes). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 400, res.StatusCode()) + assert.Regexp(t, "FF21022", errRes.Error) +} + +func TestSendInvalidDeployBadTXType(t *testing.T) { + + url, m, cancel := newTestManager(t) + defer cancel() + m.Start() + + req := strings.NewReader(`{ + "headers": { + "type": "DeployContract" + }, + "from": { + "Not": "a string" + } + }`) + var errRes fftypes.RESTError + res, err := resty.New().R(). + SetBody(req). + SetError(&errRes). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 400, res.StatusCode()) + assert.Regexp(t, "FF21022", errRes.Error) +} + +func TestSwaggerEndpoints(t *testing.T) { + + url, m, cancel := newTestManager(t) + defer cancel() + m.Start() + + res, err := resty.New().R().SetDoNotParseResponse(true).Get(url + "/api/spec.json") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + res, err = resty.New().R().SetDoNotParseResponse(true).Get(url + "/api/spec.yaml") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + res, err = resty.New().R().SetDoNotParseResponse(true).Get(url + "/api") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) +} + +func TestSendInvalidRequestWrongType(t *testing.T) { + + url, m, cancel := newTestManager(t) + defer cancel() + m.Start() + + req := strings.NewReader(`{ + "headers": { + "id": "ns1:` + fftypes.NewUUID().String() + `", + "type": "wrong" + } + }`) + var errRes fftypes.RESTError + res, err := resty.New().R(). + SetBody(req). + SetError(&errRes). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 400, res.StatusCode()) + assert.Regexp(t, "FF21023", errRes.Error) +} + +func TestSendTransactionPrepareFail(t *testing.T) { + + url, m, cancel := newTestManager(t) + defer cancel() + + mFFC := m.connector.(*ffcapimocks.API) + + mFFC.On("NextNonceForSigner", mock.Anything, mock.MatchedBy(func(nonceReq *ffcapi.NextNonceForSignerRequest) bool { + return "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8" == nonceReq.Signer + })).Return(&ffcapi.NextNonceForSignerResponse{ + Nonce: fftypes.NewFFBigInt(12345), + }, ffcapi.ErrorReason(""), nil) + + mFFC.On("TransactionPrepare", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + m.Start() + + req := strings.NewReader(sampleSendTX) + res, err := resty.New().R(). + SetBody(req). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 500, res.StatusCode()) + +} + +func TestDeployContractPrepareFail(t *testing.T) { + + url, m, cancel := newTestManager(t) + defer cancel() + + mFFC := m.connector.(*ffcapimocks.API) + + mFFC.On("NextNonceForSigner", mock.Anything, mock.MatchedBy(func(nonceReq *ffcapi.NextNonceForSignerRequest) bool { + return "0xb480F96c0a3d6E9e9a263e4665a39bFa6c4d01E8" == nonceReq.Signer + })).Return(&ffcapi.NextNonceForSignerResponse{ + Nonce: fftypes.NewFFBigInt(12345), + }, ffcapi.ErrorReason(""), nil) + + mFFC.On("DeployContractPrepare", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + m.Start() + + req := strings.NewReader(sampleDeployTX) + res, err := resty.New().R(). + SetBody(req). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 500, res.StatusCode()) + +} + +func TestQueryOK(t *testing.T) { + + url, m, cancel := newTestManager(t) + defer cancel() + m.Start() + + mca := m.connector.(*ffcapimocks.API) + mca.On("QueryInvoke", mock.Anything, mock.MatchedBy(func(req *ffcapi.QueryInvokeRequest) bool { + return req.Method.String() == `"some method details"` + })).Return(&ffcapi.QueryInvokeResponse{ + Outputs: fftypes.JSONAnyPtr(`"some output data"`), + }, ffcapi.ErrorReason(""), nil) + + var queryRes string + res, err := resty.New().R(). + SetBody(&apitypes.QueryRequest{ + Headers: apitypes.RequestHeaders{ + ID: fftypes.NewUUID().String(), + Type: apitypes.RequestTypeQuery, + }, + TransactionInput: ffcapi.TransactionInput{ + Method: fftypes.JSONAnyPtr(`"some method details"`), + }, + }). + SetResult(&queryRes). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 202, res.StatusCode()) + + assert.Equal(t, `some output data`, queryRes) + + mca.AssertExpectations(t) + +} + +func TestQueryFail(t *testing.T) { + + url, m, cancel := newTestManager(t) + defer cancel() + m.Start() + + mca := m.connector.(*ffcapimocks.API) + mca.On("QueryInvoke", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + res, err := resty.New().R(). + SetBody(&apitypes.QueryRequest{ + Headers: apitypes.RequestHeaders{ + ID: fftypes.NewUUID().String(), + Type: apitypes.RequestTypeQuery, + }, + TransactionInput: ffcapi.TransactionInput{ + Method: fftypes.JSONAnyPtr(`"some method details"`), + }, + }). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 500, res.StatusCode()) + + mca.AssertExpectations(t) + +} + +func TestQueryBadRequest(t *testing.T) { + + url, m, cancel := newTestManager(t) + defer cancel() + m.Start() + + var errRes fftypes.RESTError + res, err := resty.New().R(). + SetBody(`{ + "headers": { + "id": "`+fftypes.NewUUID().String()+`", + "type": "Query" + }, + "params": "not an array" + }`, + ). + SetHeader("content-type", "application/json"). + SetError(&errRes). + Post(url) + assert.NoError(t, err) + assert.Equal(t, 400, res.StatusCode()) + assert.Regexp(t, "FF21022", errRes.Error) + +} + +func TestNotFound(t *testing.T) { + + url, m, cancel := newTestManager(t) + defer cancel() + m.Start() + + var errRes fftypes.RESTError + res, err := resty.New().R(). + SetError(&errRes). + Post(url + "/not found") + assert.NoError(t, err) + assert.Equal(t, 404, res.StatusCode()) + assert.Regexp(t, "FF00167", errRes.Error) +} diff --git a/cmd/config_docs_generate_test.go b/pkg/fftm/config_docs_generate_test.go similarity index 92% rename from cmd/config_docs_generate_test.go rename to pkg/fftm/config_docs_generate_test.go index 5c7098cc..0a9f0f95 100644 --- a/cmd/config_docs_generate_test.go +++ b/pkg/fftm/config_docs_generate_test.go @@ -17,7 +17,7 @@ //go:build docs // +build docs -package cmd +package fftm import ( "context" @@ -31,8 +31,8 @@ import ( func TestGenerateConfigDocs(t *testing.T) { // Initialize config of all plugins - initConfig() - f, err := os.Create(filepath.Join("..", "config.md")) + 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) diff --git a/cmd/config_docs_test.go b/pkg/fftm/config_docs_test.go similarity index 92% rename from cmd/config_docs_test.go rename to pkg/fftm/config_docs_test.go index 52adc184..4299413d 100644 --- a/cmd/config_docs_test.go +++ b/pkg/fftm/config_docs_test.go @@ -17,7 +17,7 @@ //go:build !docs // +build !docs -package cmd +package fftm import ( "context" @@ -32,15 +32,15 @@ import ( func TestConfigDocsUpToDate(t *testing.T) { // Initialize config of all plugins - initConfig() + InitConfig() generatedConfig, err := config.GenerateConfigMarkdown(context.Background(), config.GetKnownKeys()) assert.NoError(t, err) - configOnDisk, err := os.ReadFile(filepath.Join("..", "config.md")) + 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`?") + 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 reference`?") } diff --git a/pkg/fftm/managed_tx.go b/pkg/fftm/managed_tx.go deleted file mode 100644 index 8b34ca1a..00000000 --- a/pkg/fftm/managed_tx.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright © 2022 Kaleido, Inc. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package fftm - -import ( - "github.com/hyperledger/firefly-common/pkg/ffcapi" - "github.com/hyperledger/firefly-common/pkg/fftypes" - "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" -) - -type ManagedTXError struct { - Time *fftypes.FFTime `json:"time"` - Error string `json:"error,omitempty"` - Mapped ffcapi.ErrorReason `json:"mapped,omitempty"` -} - -// ManagedTXOutput is the structure stored into the operation in FireFly, that the policy -// engine can use to apply policy, and apply updates to -type ManagedTXOutput struct { - FFTMName string `json:"fftmName"` - ID string `json:"id"` - Nonce *fftypes.FFBigInt `json:"nonce"` - Gas *fftypes.FFBigInt `json:"gas"` - TransactionHash string `json:"transactionHash,omitempty"` - TransactionData string `json:"transactionData,omitempty"` - GasPrice *fftypes.JSONAny `json:"gasPrice"` - PolicyInfo *fftypes.JSONAny `json:"policyInfo"` - FirstSubmit *fftypes.FFTime `json:"firstSubmit,omitempty"` - LastSubmit *fftypes.FFTime `json:"lastSubmit,omitempty"` - Request *TransactionRequest `json:"request,omitempty"` - Receipt *ffcapi.GetReceiptResponse `json:"receipt,omitempty"` - ErrorHistory []*ManagedTXError `json:"errorHistory"` - Confirmations []confirmations.BlockInfo `json:"confirmations,omitempty"` -} diff --git a/pkg/fftm/manager.go b/pkg/fftm/manager.go new file mode 100644 index 00000000..35a006e4 --- /dev/null +++ b/pkg/fftm/manager.go @@ -0,0 +1,193 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "context" + "sync" + "time" + + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/httpserver" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-common/pkg/retry" + "github.com/hyperledger/firefly-transaction-manager/internal/blocklistener" + "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" + "github.com/hyperledger/firefly-transaction-manager/internal/events" + "github.com/hyperledger/firefly-transaction-manager/internal/persistence" + "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/internal/ws" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" + "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines" +) + +type Manager interface { + Start() error + Close() +} + +type manager struct { + ctx context.Context + cancelCtx func() + retry *retry.Retry + connector ffcapi.API + confirmations confirmations.Manager + policyEngine policyengine.PolicyEngine + apiServer httpserver.HTTPServer + wsServer ws.WebSocketServer + persistence persistence.Persistence + inflightStale chan bool + inflightUpdate chan bool + inflight []*pendingState + + mux sync.Mutex + lockedNonces map[string]*lockedNonce + eventStreams map[fftypes.UUID]events.Stream + streamsByName map[string]*fftypes.UUID + policyLoopDone chan struct{} + blockListenerDone chan struct{} + started bool + apiServerDone chan error + + policyLoopInterval time.Duration + nonceStateTimeout time.Duration + errorHistoryCount int + maxInFlight int +} + +func InitConfig() { + tmconfig.Reset() + events.InitDefaults() +} + +func NewManager(ctx context.Context, connector ffcapi.API) (Manager, error) { + var err error + m := newManager(ctx, connector) + if err = m.initServices(ctx); err != nil { + return nil, err + } + if err = m.initPersistence(ctx); err != nil { + return nil, err + } + return m, nil +} + +func newManager(ctx context.Context, connector ffcapi.API) *manager { + m := &manager{ + connector: connector, + lockedNonces: make(map[string]*lockedNonce), + apiServerDone: make(chan error), + eventStreams: make(map[fftypes.UUID]events.Stream), + streamsByName: make(map[string]*fftypes.UUID), + + policyLoopInterval: config.GetDuration(tmconfig.PolicyLoopInterval), + errorHistoryCount: config.GetInt(tmconfig.TransactionsErrorHistoryCount), + maxInFlight: config.GetInt(tmconfig.TransactionsMaxInFlight), + nonceStateTimeout: config.GetDuration(tmconfig.TransactionsNonceStateTimeout), + inflightStale: make(chan bool, 1), + inflightUpdate: make(chan bool, 1), + retry: &retry.Retry{ + InitialDelay: config.GetDuration(tmconfig.PolicyLoopRetryInitDelay), + MaximumDelay: config.GetDuration(tmconfig.PolicyLoopRetryMaxDelay), + Factor: config.GetFloat64(tmconfig.PolicyLoopRetryFactor), + }, + } + m.ctx, m.cancelCtx = context.WithCancel(ctx) + return m +} + +type pendingState struct { + mtx *apitypes.ManagedTX + lastPolicyCycle time.Time + confirmed bool + remove bool + trackingTransactionHash string +} + +func (m *manager) initServices(ctx context.Context) (err error) { + m.confirmations = confirmations.NewBlockConfirmationManager(ctx, m.connector, "receipts") + m.policyEngine, err = policyengines.NewPolicyEngine(ctx, tmconfig.PolicyEngineBaseConfig, config.GetString(tmconfig.PolicyEngineName)) + if err != nil { + return err + } + m.wsServer = ws.NewWebSocketServer(ctx) + m.apiServer, err = httpserver.NewHTTPServer(ctx, "api", m.router(), m.apiServerDone, tmconfig.APIConfig, tmconfig.CorsConfig) + if err != nil { + return err + } + return nil +} + +func (m *manager) initPersistence(ctx context.Context) (err error) { + pType := config.GetString(tmconfig.PersistenceType) + switch pType { + case "leveldb": + if m.persistence, err = persistence.NewLevelDBPersistence(ctx); err != nil { + return i18n.NewError(ctx, tmmsgs.MsgPersistenceInitFail, pType, err) + } + return nil + default: + return i18n.NewError(ctx, tmmsgs.MsgUnknownPersistence, pType) + } +} + +func (m *manager) Start() error { + if err := m.restoreStreams(); err != nil { + return err + } + + blReq := &ffcapi.NewBlockListenerRequest{ListenerContext: m.ctx, ID: fftypes.NewUUID()} + blReq.BlockListener, m.blockListenerDone = blocklistener.BufferChannel(m.ctx, m.confirmations) + _, _, err := m.connector.NewBlockListener(m.ctx, blReq) + if err != nil { + return err + } + + go m.runAPIServer() + m.policyLoopDone = make(chan struct{}) + m.markInflightStale() + go m.policyLoop() + go m.confirmations.Start() + + m.started = true + return nil +} + +func (m *manager) Close() { + m.cancelCtx() + if m.started { + m.started = false + <-m.apiServerDone + <-m.policyLoopDone + <-m.blockListenerDone + + streams := []events.Stream{} + m.mux.Lock() + for _, s := range m.eventStreams { + streams = append(streams, s) + } + m.mux.Unlock() + for _, s := range streams { + _ = s.Stop(m.ctx) + } + } + m.persistence.Close(m.ctx) +} diff --git a/pkg/fftm/manager_test.go b/pkg/fftm/manager_test.go new file mode 100644 index 00000000..9c457d01 --- /dev/null +++ b/pkg/fftm/manager_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 fftm + +import ( + "context" + "fmt" + "io/ioutil" + "net" + "os" + "strings" + "testing" + + "github.com/hyperledger/firefly-common/pkg/config" + "github.com/hyperledger/firefly-common/pkg/httpserver" + "github.com/hyperledger/firefly-transaction-manager/internal/persistence" + "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" + "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/mocks/persistencemocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines" + "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines/simple" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +const testManagerName = "unittest" + +func strPtr(s string) *string { return &s } + +func testManagerCommonInit(t *testing.T) string { + InitConfig() + policyengines.RegisterEngine(&simple.PolicyEngineFactory{}) + tmconfig.PolicyEngineBaseConfig.SubSection("simple").SubSection(simple.GasOracleConfig).Set(simple.GasOracleMode, simple.GasOracleModeDisabled) + + ln, err := net.Listen("tcp", "127.0.0.1:0") + assert.NoError(t, err) + managerPort := strings.Split(ln.Addr().String(), ":")[1] + ln.Close() + tmconfig.APIConfig.Set(httpserver.HTTPConfPort, managerPort) + tmconfig.APIConfig.Set(httpserver.HTTPConfAddress, "127.0.0.1") + + config.Set(tmconfig.PolicyLoopInterval, "1ns") + tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "223344556677") + + return fmt.Sprintf("http://127.0.0.1:%s", managerPort) +} + +func newTestManager(t *testing.T) (string, *manager, func()) { + + url := testManagerCommonInit(t) + + dir, err := ioutil.TempDir("", "ldb_*") + assert.NoError(t, err) + config.Set(tmconfig.PersistenceLevelDBPath, dir) + + mca := &ffcapimocks.API{} + mca.On("NewBlockListener", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), nil).Maybe() + mm, err := NewManager(context.Background(), mca) + assert.NoError(t, err) + + m := mm.(*manager) + mcm := &confirmationsmocks.Manager{} + mcm.On("Start").Return().Maybe() + m.confirmations = mcm + + return url, + m, + func() { + m.Close() + os.RemoveAll(dir) + } + +} + +func newTestManagerMockPersistence(t *testing.T) (string, *manager, func()) { + + url := testManagerCommonInit(t) + + m := newManager(context.Background(), &ffcapimocks.API{}) + mp := &persistencemocks.Persistence{} + mp.On("Close", mock.Anything).Return(nil).Maybe() + m.persistence = mp + + err := m.initServices(context.Background()) + assert.NoError(t, err) + + return url, m, func() { + m.Close() + } +} + +func TestNewManagerBadHttpConfig(t *testing.T) { + + tmconfig.Reset() + tmconfig.APIConfig.Set(httpserver.HTTPConfAddress, "::::") + + policyengines.RegisterEngine(&simple.PolicyEngineFactory{}) + tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "223344556677") + + _, err := NewManager(context.Background(), nil) + assert.Regexp(t, "FF00151", err) + +} + +func TestNewManagerBadLevelDBConfig(t *testing.T) { + + tmpFile, err := ioutil.TempFile("", "ut-*") + assert.NoError(t, err) + defer os.Remove(tmpFile.Name()) + + tmconfig.Reset() + config.Set(tmconfig.PersistenceLevelDBPath, tmpFile.Name) + tmconfig.APIConfig.Set(httpserver.HTTPConfPort, "0") + + policyengines.RegisterEngine(&simple.PolicyEngineFactory{}) + tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "223344556677") + + _, err = NewManager(context.Background(), nil) + assert.Regexp(t, "FF21049", err) + +} + +func TestNewManagerBadPersistenceConfig(t *testing.T) { + + tmconfig.Reset() + config.Set(tmconfig.PersistenceType, "wrong") + tmconfig.APIConfig.Set(httpserver.HTTPConfPort, "0") + + policyengines.RegisterEngine(&simple.PolicyEngineFactory{}) + tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "223344556677") + + _, err := NewManager(context.Background(), nil) + assert.Regexp(t, "FF21043", err) + +} + +func TestNewManagerBadPolicyEngine(t *testing.T) { + + tmconfig.Reset() + config.Set(tmconfig.PolicyEngineName, "wrong") + + _, err := NewManager(context.Background(), nil) + assert.Regexp(t, "FF21019", err) + +} + +func TestAddErrorMessageMax(t *testing.T) { + + _, m, close := newTestManagerMockPersistence(t) + defer close() + + m.errorHistoryCount = 2 + mtx := &apitypes.ManagedTX{} + m.addError(mtx, ffcapi.ErrorReasonTransactionUnderpriced, fmt.Errorf("snap")) + m.addError(mtx, ffcapi.ErrorReasonTransactionUnderpriced, fmt.Errorf("crackle")) + m.addError(mtx, ffcapi.ErrorReasonTransactionUnderpriced, fmt.Errorf("pop")) + assert.Len(t, mtx.ErrorHistory, 2) + assert.Equal(t, "pop", mtx.ErrorHistory[0].Error) + assert.Equal(t, "crackle", mtx.ErrorHistory[1].Error) + +} + +func TestStartRestoreFail(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("ListStreams", mock.Anything, mock.Anything, startupPaginationLimit, persistence.SortDirectionAscending). + Return(nil, fmt.Errorf("pop")) + + err := m.Start() + assert.Regexp(t, "pop", err) +} + +func TestStartBlockListenerFail(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("ListStreams", mock.Anything, mock.Anything, startupPaginationLimit, persistence.SortDirectionAscending).Return(nil, nil) + + mca := m.connector.(*ffcapimocks.API) + mca.On("NewBlockListener", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + err := m.Start() + assert.Regexp(t, "pop", err) + +} diff --git a/internal/manager/nonces.go b/pkg/fftm/nonces.go similarity index 51% rename from internal/manager/nonces.go rename to pkg/fftm/nonces.go index e5210a5d..2f379117 100644 --- a/internal/manager/nonces.go +++ b/pkg/fftm/nonces.go @@ -14,14 +14,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package manager +package fftm import ( "context" + "time" - "github.com/hyperledger/firefly-common/pkg/ffcapi" "github.com/hyperledger/firefly-common/pkg/log" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/hyperledger/firefly-transaction-manager/internal/persistence" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) type lockedNonce struct { @@ -30,17 +32,15 @@ type lockedNonce struct { signer string unlocked chan struct{} nonce uint64 - spent *fftm.ManagedTXOutput + spent *apitypes.ManagedTX } // complete must be called for any lockedNonce returned from a successful assignAndLockNonce call func (ln *lockedNonce) complete(ctx context.Context) { if ln.spent != nil { log.L(ctx).Debugf("Next nonce %d for signer %s spent", ln.nonce, ln.signer) - ln.m.trackManaged(ln.spent) } else { log.L(ctx).Debugf("Returning next nonce %d for signer %s unspent", ln.nonce, ln.signer) - // Do not } ln.m.mux.Lock() delete(ln.m.lockedNonces, ln.signer) @@ -63,16 +63,6 @@ func (m *manager) assignAndLockNonce(ctx context.Context, nsOpID, signer string) unlocked: make(chan struct{}), } m.lockedNonces[signer] = locked - // We might know the highest nonce straight away - nextNonce, nonceCached := m.nextNonces[signer] - if nonceCached { - locked.nonce = nextNonce - log.L(ctx).Debugf("Locking next nonce %d from cache for signer %s", locked.nonce, signer) - // We can return the nonce to use without any query - m.mux.Unlock() - return locked, nil - } - // Otherwise, defer a lookup to outside of the mutex doLookup = true } m.mux.Unlock() @@ -84,20 +74,54 @@ func (m *manager) assignAndLockNonce(ctx context.Context, nsOpID, signer string) } else if doLookup { // We have to ensure we either successfully return a nonce, // or otherwise we unlock when we send the error - nextNonceRes, _, err := m.connectorAPI.GetNextNonce(ctx, &ffcapi.GetNextNonceRequest{ - Signer: signer, - }) + nextNonce, err := m.calcNextNonce(ctx, signer) if err != nil { - close(locked.unlocked) + locked.complete(ctx) return nil, err } - nextNonce := nextNonceRes.Nonce.Uint64() - m.mux.Lock() - m.nextNonces[signer] = nextNonce locked.nonce = nextNonce - m.mux.Unlock() return locked, nil } } } + +func (m *manager) calcNextNonce(ctx context.Context, signer string) (uint64, error) { + + // First we check our DB to find the last nonce we used for this address. + // Note we are within the nonce-lock in assignAndLockNonce for this signer, so we can be sure we're the + // only routine attempting this right now. + var lastTxn *apitypes.ManagedTX + txns, err := m.persistence.ListTransactionsByNonce(ctx, signer, nil, 1, persistence.SortDirectionDescending) + if err != nil { + return 0, err + } + if len(txns) > 0 { + lastTxn = txns[0] + if time.Since(*lastTxn.Created.Time()) < m.nonceStateTimeout { + nextNonce := lastTxn.Nonce.Uint64() + 1 + log.L(ctx).Debugf("Allocating next nonce '%s' / '%d' after TX '%s' (status=%s)", signer, nextNonce, lastTxn.ID, lastTxn.Status) + return nextNonce, nil + } + } + + // If we don't have a fresh answer in our state store, then ask the node. + nextNonceRes, _, err := m.connector.NextNonceForSigner(ctx, &ffcapi.NextNonceForSignerRequest{ + Signer: signer, + }) + if err != nil { + return 0, err + } + nextNonce := nextNonceRes.Nonce.Uint64() + + // If we had a stale answer in our state store, make sure this isn't re-used. + // This is important in case we have transactions that have expired from the TX pool of nodes, but we still have them + // in our state store. So basically whichever is further forwards of our state store and the node answer wins. + if lastTxn != nil && nextNonce <= lastTxn.Nonce.Uint64() { + log.L(ctx).Debugf("Node TX pool next nonce '%s' / '%d' is not ahead of '%d' in TX '%s' (status=%s)", signer, nextNonce, lastTxn.Nonce.Uint64(), lastTxn.ID, lastTxn.Status) + nextNonce = lastTxn.Nonce.Uint64() + 1 + } + + return nextNonce, nil + +} diff --git a/pkg/fftm/nonces_test.go b/pkg/fftm/nonces_test.go new file mode 100644 index 00000000..7712a282 --- /dev/null +++ b/pkg/fftm/nonces_test.go @@ -0,0 +1,186 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/mocks/persistencemocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestNonceStaleStateContention(t *testing.T) { + + _, m, cancel := newTestManager(t) + defer cancel() + + // Write a stale record to persistence + oldTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) + err := m.persistence.WriteTransaction(m.ctx, &apitypes.ManagedTX{ + ID: "stale1", + Created: &oldTime, + Status: apitypes.TxStatusSucceeded, + SequenceID: apitypes.NewULID(), + Nonce: fftypes.NewFFBigInt(1000), // old nonce + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x12345", + }, + }, true) + assert.NoError(t, err) + + mFFC := m.connector.(*ffcapimocks.API) + + mFFC.On("NextNonceForSigner", mock.Anything, mock.MatchedBy(func(nonceReq *ffcapi.NextNonceForSignerRequest) bool { + return "0x12345" == nonceReq.Signer + })).Return(&ffcapi.NextNonceForSignerResponse{ + Nonce: fftypes.NewFFBigInt(1111), + }, ffcapi.ErrorReason(""), nil) + + locked1 := make(chan struct{}) + done1 := make(chan struct{}) + done2 := make(chan struct{}) + + go func() { + defer close(done1) + + ln, err := m.assignAndLockNonce(context.Background(), "ns1:"+fftypes.NewUUID().String(), "0x12345") + assert.NoError(t, err) + assert.Equal(t, uint64(1111), ln.nonce) + close(locked1) + + time.Sleep(1 * time.Millisecond) + ln.spent = &apitypes.ManagedTX{ + ID: "ns1:" + fftypes.NewUUID().String(), + Created: &oldTime, + Nonce: fftypes.NewFFBigInt(int64(ln.nonce)), + Status: apitypes.TxStatusPending, + SequenceID: apitypes.NewULID(), + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x12345", + }, + } + err = m.persistence.WriteTransaction(m.ctx, ln.spent, true) + assert.NoError(t, err) + ln.complete(context.Background()) + }() + + go func() { + defer close(done2) + + <-locked1 + ln, err := m.assignAndLockNonce(context.Background(), "ns2:"+fftypes.NewUUID().String(), "0x12345") + assert.NoError(t, err) + + assert.Equal(t, uint64(1112), ln.nonce) + + ln.complete(context.Background()) + + }() + + <-done1 + <-done2 + +} + +func TestNonceListError(t *testing.T) { + + _, m, close := newTestManagerMockPersistence(t) + defer close() + + mFFC := m.connector.(*ffcapimocks.API) + mFFC.On("TransactionPrepare", mock.Anything, mock.Anything).Return(&ffcapi.TransactionPrepareResponse{ + TransactionData: "RAW_UNSIGNED_BYTES", + Gas: fftypes.NewFFBigInt(2000000), // gas estimate simulation + }, ffcapi.ErrorReason(""), nil) + + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("ListTransactionsByNonce", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(nil, fmt.Errorf("pop")) + + _, err := m.sendManagedTransaction(context.Background(), &apitypes.TransactionRequest{ + TransactionInput: ffcapi.TransactionInput{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x12345", + }, + }, + }) + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) + mFFC.AssertExpectations(t) + +} + +func TestNonceListStaleThenQueryFail(t *testing.T) { + + _, m, close := newTestManagerMockPersistence(t) + defer close() + + mp := m.persistence.(*persistencemocks.Persistence) + old := fftypes.FFTime(time.Now().Add(-10000 * time.Hour)) + mp.On("ListTransactionsByNonce", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return([]*apitypes.ManagedTX{ + {ID: "id12345", Created: &old, Status: apitypes.TxStatusSucceeded, Nonce: fftypes.NewFFBigInt(1000)}, + }, nil) + + mFFC := m.connector.(*ffcapimocks.API) + mFFC.On("TransactionPrepare", mock.Anything, mock.Anything).Return(&ffcapi.TransactionPrepareResponse{ + TransactionData: "RAW_UNSIGNED_BYTES", + Gas: fftypes.NewFFBigInt(2000000), // gas estimate simulation + }, ffcapi.ErrorReason(""), nil) + mFFC.On("NextNonceForSigner", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + _, err := m.sendManagedTransaction(context.Background(), &apitypes.TransactionRequest{ + TransactionInput: ffcapi.TransactionInput{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x12345", + }, + }, + }) + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) + mFFC.AssertExpectations(t) + +} + +func TestNonceListNotStale(t *testing.T) { + + _, m, close := newTestManagerMockPersistence(t) + defer close() + m.nonceStateTimeout = 1 * time.Hour + + mp := m.persistence.(*persistencemocks.Persistence) + + mp.On("ListTransactionsByNonce", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return([]*apitypes.ManagedTX{ + {ID: "id12345", Created: fftypes.Now(), Status: apitypes.TxStatusSucceeded, Nonce: fftypes.NewFFBigInt(1000)}, + }, nil) + + n, err := m.calcNextNonce(context.Background(), "0x12345") + assert.NoError(t, err) + assert.Equal(t, uint64(1001), n) + +} diff --git a/pkg/fftm/policyloop.go b/pkg/fftm/policyloop.go new file mode 100644 index 00000000..411b4450 --- /dev/null +++ b/pkg/fftm/policyloop.go @@ -0,0 +1,272 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "context" + "time" + + "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-transaction-manager/internal/confirmations" + "github.com/hyperledger/firefly-transaction-manager/internal/persistence" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" +) + +func (m *manager) policyLoop() { + defer close(m.policyLoopDone) + ctx := log.WithLogField(m.ctx, "role", "policyloop") + + for { + timer := time.NewTimer(m.policyLoopInterval) + select { + case <-m.inflightUpdate: + m.policyLoopCycle(ctx, false) + case <-m.inflightStale: + m.policyLoopCycle(ctx, true) + case <-timer.C: + m.policyLoopCycle(ctx, false) + case <-ctx.Done(): + log.L(ctx).Infof("Receipt poller exiting") + return + } + } +} + +func (m *manager) markInflightStale() { + select { + case m.inflightStale <- true: + default: + } +} + +func (m *manager) markInflightUpdate() { + select { + case m.inflightUpdate <- true: + default: + } +} + +func (m *manager) updateInflightSet(ctx context.Context) bool { + + oldInflight := m.inflight + m.inflight = make([]*pendingState, 0, len(oldInflight)) + + // Run through removing those that are removed + for _, p := range oldInflight { + if !p.remove { + m.inflight = append(m.inflight, p) + } + } + + // If we are not at maximum, then query if there are more candidates now + spaces := m.maxInFlight - len(m.inflight) + if spaces > 0 { + var after *fftypes.UUID + if len(m.inflight) > 0 { + after = m.inflight[len(m.inflight)-1].mtx.SequenceID + } + var additional []*apitypes.ManagedTX + // We retry the get from persistence indefinitely (until the context cancels) + err := m.retry.Do(ctx, "get pending transactions", func(attempt int) (retry bool, err error) { + additional, err = m.persistence.ListTransactionsPending(ctx, after, spaces, persistence.SortDirectionAscending) + return true, err + }) + if err != nil { + log.L(ctx).Infof("Policy loop context cancelled while retrying") + return false + } + for _, mtx := range additional { + m.inflight = append(m.inflight, &pendingState{mtx: mtx}) + } + newLen := len(m.inflight) + if newLen > 0 { + log.L(ctx).Debugf("Inflight set updated len=%d head-seq=%s tail-seq=%s old-tail=%s", len(m.inflight), m.inflight[0].mtx.SequenceID, m.inflight[newLen-1].mtx.SequenceID, after) + } + } + return true + +} + +func (m *manager) policyLoopCycle(ctx context.Context, inflightStale bool) { + + if inflightStale { + if !m.updateInflightSet(ctx) { + return + } + } + + // Go through executing the policy engine against them + for _, pending := range m.inflight { + err := m.execPolicy(ctx, pending) + if err != nil { + log.L(ctx).Errorf("Failed policy cycle transaction=%s operation=%s: %s", pending.mtx.TransactionHash, pending.mtx.ID, err) + } + } + +} + +func (m *manager) addError(mtx *apitypes.ManagedTX, reason ffcapi.ErrorReason, err error) { + newLen := len(mtx.ErrorHistory) + 1 + if newLen > m.errorHistoryCount { + newLen = m.errorHistoryCount + } + oldHistory := mtx.ErrorHistory + mtx.ErrorHistory = make([]*apitypes.ManagedTXError, newLen) + latestError := &apitypes.ManagedTXError{ + Time: fftypes.Now(), + Mapped: reason, + Error: err.Error(), + } + mtx.ErrorMessage = latestError.Error + mtx.ErrorHistory[0] = latestError + for i := 1; i < newLen; i++ { + mtx.ErrorHistory[i] = oldHistory[i-1] + } +} + +func (m *manager) execPolicy(ctx context.Context, pending *pendingState) (err error) { + + var updated bool + completed := false + + // Check whether this has been confirmed by the confirmation manager + m.mux.Lock() + mtx := pending.mtx + confirmed := pending.confirmed + m.mux.Unlock() + + switch { + case confirmed: + updated = true + completed = true + if mtx.Receipt.Success { + mtx.Status = apitypes.TxStatusSucceeded + mtx.ErrorMessage = "" + } else { + mtx.Status = apitypes.TxStatusFailed + mtx.ErrorMessage = i18n.NewError(ctx, tmmsgs.MsgTransactionFailed).Error() + } + + default: + // We get woken for lots of reasons to go through the policy loop, but we only want + // to drive the policy engine at regular intervals. + // So we track the last time we ran the policy engine against each pending item. + if time.Since(pending.lastPolicyCycle) > m.policyLoopInterval { + // Pass the state to the pluggable policy engine to potentially perform more actions against it, + // such as submitting for the first time, or raising the gas etc. + var reason ffcapi.ErrorReason + updated, reason, err = m.policyEngine.Execute(ctx, m.connector, pending.mtx) + if err != nil { + log.L(ctx).Errorf("Policy engine returned error for transaction %s reason=%s: %s", mtx.ID, reason, err) + m.addError(mtx, reason, err) + } else { + if mtx.FirstSubmit != nil && pending.trackingTransactionHash != mtx.TransactionHash { + // If now submitted, add to confirmations manager for receipt checking + m.trackSubmittedTransaction(ctx, pending) + } + pending.lastPolicyCycle = time.Now() + } + } + } + + if updated || err != nil { + mtx.Updated = fftypes.Now() + err := m.persistence.WriteTransaction(ctx, mtx, false) + if err != nil { + log.L(ctx).Errorf("Failed to update transaction %s (status=%s): %s", mtx.ID, mtx.Status, err) + return err + } + if completed { + pending.remove = true // for the next time round the loop + m.markInflightStale() + } + m.sendWSReply(mtx) + } + + return nil +} + +func (m *manager) sendWSReply(mtx *apitypes.ManagedTX) { + wsr := &apitypes.TransactionUpdateReply{ + ManagedTX: *mtx, + Headers: apitypes.ReplyHeaders{ + RequestID: mtx.ID, + }, + } + switch mtx.Status { + case apitypes.TxStatusSucceeded: + wsr.Headers.Type = apitypes.TransactionUpdateSuccess + case apitypes.TxStatusFailed: + wsr.Headers.Type = apitypes.TransactionUpdateFailure + default: + wsr.Headers.Type = apitypes.TransactionUpdate + } + // Notify on the websocket - this is best-effort (there is no subscription/acknowledgement) + m.wsServer.SendReply(wsr) +} + +func (m *manager) trackSubmittedTransaction(ctx context.Context, pending *pendingState) { + var err error + + // Clear any old transaction hash + if pending.trackingTransactionHash != "" { + err = m.confirmations.Notify(&confirmations.Notification{ + NotificationType: confirmations.RemovedTransaction, + Transaction: &confirmations.TransactionInfo{ + TransactionHash: pending.trackingTransactionHash, + }, + }) + } + + // Notify of the new + if err == nil { + err = m.confirmations.Notify(&confirmations.Notification{ + NotificationType: confirmations.NewTransaction, + Transaction: &confirmations.TransactionInfo{ + TransactionHash: pending.mtx.TransactionHash, + Receipt: func(ctx context.Context, receipt *ffcapi.TransactionReceiptResponse) { + // Will be picked up on the next policy loop cycle - guaranteed to occur before Confirmed + m.mux.Lock() + pending.mtx.Receipt = receipt + m.mux.Unlock() + log.L(m.ctx).Debugf("Receipt received for transaction %s at nonce %s / %d - hash: %s", pending.mtx.ID, pending.mtx.TransactionHeaders.From, pending.mtx.Nonce.Int64(), pending.mtx.TransactionHash) + m.markInflightUpdate() + }, + Confirmed: func(ctx context.Context, confirmations []confirmations.BlockInfo) { + // Will be picked up on the next policy loop cycle + m.mux.Lock() + pending.confirmed = true + pending.mtx.Confirmations = confirmations + m.mux.Unlock() + log.L(m.ctx).Debugf("Confirmed transaction %s at nonce %s / %d - hash: %s", pending.mtx.ID, pending.mtx.TransactionHeaders.From, pending.mtx.Nonce.Int64(), pending.mtx.TransactionHash) + m.markInflightUpdate() + }, + }, + }) + } + + // Only reason for error here should be a cancelled context + if err != nil { + log.L(ctx).Infof("Error detected notifying confirmation manager: %s", err) + } else { + pending.trackingTransactionHash = pending.mtx.TransactionHash + } +} diff --git a/pkg/fftm/policyloop_test.go b/pkg/fftm/policyloop_test.go new file mode 100644 index 00000000..16e2882a --- /dev/null +++ b/pkg/fftm/policyloop_test.go @@ -0,0 +1,366 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/internal/confirmations" + "github.com/hyperledger/firefly-transaction-manager/internal/persistence" + "github.com/hyperledger/firefly-transaction-manager/mocks/confirmationsmocks" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/mocks/persistencemocks" + "github.com/hyperledger/firefly-transaction-manager/mocks/policyenginemocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func sendSampleTX(t *testing.T, m *manager, signer string, nonce int64) *apitypes.ManagedTX { + + txInput := ffcapi.TransactionInput{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: signer, + }, + } + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("NextNonceForSigner", m.ctx, &ffcapi.NextNonceForSignerRequest{ + Signer: signer, + }).Return(&ffcapi.NextNonceForSignerResponse{ + Nonce: fftypes.NewFFBigInt(nonce), + }, ffcapi.ErrorReason(""), nil).Once() + mfc.On("TransactionPrepare", m.ctx, &ffcapi.TransactionPrepareRequest{ + TransactionInput: txInput, + }).Return(&ffcapi.TransactionPrepareResponse{ + Gas: fftypes.NewFFBigInt(100000), + TransactionData: "0xabce1234", + }, ffcapi.ErrorReason(""), nil).Once() + + mtx, err := m.sendManagedTransaction(m.ctx, &apitypes.TransactionRequest{ + TransactionInput: txInput, + }) + assert.NoError(t, err) + return mtx +} + +func TestPolicyLoopE2EOk(t *testing.T) { + + _, m, cancel := newTestManager(t) + defer cancel() + + mtx := sendSampleTX(t, m, "0xaaaaa", 12345) + txHash := "0x" + fftypes.NewRandB32().String() + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("TransactionSend", m.ctx, mock.MatchedBy(func(r *ffcapi.TransactionSendRequest) bool { + return r.Nonce.Equals(fftypes.NewFFBigInt(12345)) + })).Return(&ffcapi.TransactionSendResponse{ + TransactionHash: txHash, + }, ffcapi.ErrorReason(""), nil) + + mc := m.confirmations.(*confirmationsmocks.Manager) + mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { + return n.NotificationType == confirmations.NewTransaction + })).Run(func(args mock.Arguments) { + n := args[0].(*confirmations.Notification) + n.Transaction.Receipt(context.Background(), &ffcapi.TransactionReceiptResponse{ + BlockNumber: fftypes.NewFFBigInt(12345), + TransactionIndex: fftypes.NewFFBigInt(10), + BlockHash: fftypes.NewRandB32().String(), + Success: true, + }) + n.Transaction.Confirmed(context.Background(), []confirmations.BlockInfo{}) + }).Return(nil) + + // Run the policy once to do the send + <-m.inflightStale // from sending the TX + m.policyLoopCycle(m.ctx, true) + assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) + assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) + + // A second time will mark it complete for flush + m.policyLoopCycle(m.ctx, false) + + <-m.inflightStale // policy loop should have marked us stale, to clean up the TX + m.policyLoopCycle(m.ctx, true) + assert.Empty(t, m.inflight) + + // Check the update is persisted + rtx, err := m.persistence.GetTransactionByID(m.ctx, mtx.ID) + assert.NoError(t, err) + assert.Equal(t, apitypes.TxStatusSucceeded, rtx.Status) + + mc.AssertExpectations(t) + mfc.AssertExpectations(t) +} + +func TestPolicyLoopE2EReverted(t *testing.T) { + + _, m, cancel := newTestManager(t) + defer cancel() + + mtx := sendSampleTX(t, m, "0xaaaaa", 12345) + txHash := "0x" + fftypes.NewRandB32().String() + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("TransactionSend", m.ctx, mock.MatchedBy(func(r *ffcapi.TransactionSendRequest) bool { + return r.Nonce.Equals(fftypes.NewFFBigInt(12345)) + })).Return(&ffcapi.TransactionSendResponse{ + TransactionHash: txHash, + }, ffcapi.ErrorReason(""), nil) + + mc := m.confirmations.(*confirmationsmocks.Manager) + mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { + return n.NotificationType == confirmations.NewTransaction + })).Run(func(args mock.Arguments) { + n := args[0].(*confirmations.Notification) + n.Transaction.Receipt(context.Background(), &ffcapi.TransactionReceiptResponse{ + BlockNumber: fftypes.NewFFBigInt(12345), + TransactionIndex: fftypes.NewFFBigInt(10), + BlockHash: fftypes.NewRandB32().String(), + Success: false, + }) + n.Transaction.Confirmed(context.Background(), []confirmations.BlockInfo{}) + }).Return(nil) + + // Run the policy once to do the send + <-m.inflightStale // from sending the TX + m.policyLoopCycle(m.ctx, true) + assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) + assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) + + // A second time will mark it complete for flush + m.policyLoopCycle(m.ctx, false) + + <-m.inflightStale // policy loop should have marked us stale, to clean up the TX + m.policyLoopCycle(m.ctx, true) + assert.Empty(t, m.inflight) + + // Check the update is persisted + rtx, err := m.persistence.GetTransactionByID(m.ctx, mtx.ID) + assert.NoError(t, err) + assert.Equal(t, apitypes.TxStatusFailed, rtx.Status) + + mc.AssertExpectations(t) + mfc.AssertExpectations(t) +} + +func TestPolicyLoopResubmitNewTXID(t *testing.T) { + + _, m, cancel := newTestManager(t) + defer cancel() + + mtx := sendSampleTX(t, m, "0xaaaaa", 12345) + txHash1 := "0x" + fftypes.NewRandB32().String() + txHash2 := "0x" + fftypes.NewRandB32().String() + + mfc := m.connector.(*ffcapimocks.API) + + mfc.On("TransactionPrepare", mock.Anything, mock.Anything).Return(&ffcapi.TransactionPrepareResponse{ + Gas: fftypes.NewFFBigInt(12345), + TransactionData: "0x12345", + }, ffcapi.ErrorReason(""), nil) + + mfc.On("TransactionSend", mock.Anything, mock.Anything).Return(&ffcapi.TransactionSendResponse{ + TransactionHash: txHash1, + }, ffcapi.ErrorReason(""), nil).Once() + mfc.On("TransactionSend", mock.Anything, mock.Anything).Return(&ffcapi.TransactionSendResponse{ + TransactionHash: txHash2, + }, ffcapi.ErrorReason(""), nil).Once() + + mc := m.confirmations.(*confirmationsmocks.Manager) + mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { + // First we get notified to add the old TX hash + return n.NotificationType == confirmations.NewTransaction && + n.Transaction.TransactionHash == txHash1 + })).Return(nil) + mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { + // Then we get notified to remove the old TX hash + return n.NotificationType == confirmations.RemovedTransaction && + n.Transaction.TransactionHash == txHash1 + })).Return(nil) + mc.On("Notify", mock.MatchedBy(func(n *confirmations.Notification) bool { + // Then we get the new TX hash, which we confirm + return n.NotificationType == confirmations.NewTransaction && + n.Transaction.TransactionHash == txHash2 + })).Run(func(args mock.Arguments) { + n := args[0].(*confirmations.Notification) + n.Transaction.Receipt(context.Background(), &ffcapi.TransactionReceiptResponse{ + BlockNumber: fftypes.NewFFBigInt(12345), + TransactionIndex: fftypes.NewFFBigInt(10), + BlockHash: fftypes.NewRandB32().String(), + Success: true, + }) + n.Transaction.Confirmed(context.Background(), []confirmations.BlockInfo{}) + }).Return(nil) + + // Run the policy once to do the send with the first hash + <-m.inflightStale // from sending the TX + m.policyLoopCycle(m.ctx, true) + assert.Len(t, m.inflight, 1) + assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) + assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) + + // Run again to confirm it does not change anything, when the state is the same + m.policyLoopCycle(m.ctx, true) + assert.Len(t, m.inflight, 1) + assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) + assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) + + // Reset the transaction so the policy manager resubmits it + m.inflight[0].mtx.FirstSubmit = nil + m.policyLoopCycle(m.ctx, false) + assert.Equal(t, mtx.ID, m.inflight[0].mtx.ID) + assert.Equal(t, apitypes.TxStatusPending, m.inflight[0].mtx.Status) + + mc.AssertExpectations(t) + mfc.AssertExpectations(t) +} + +func TestNotifyConfirmationMgrFail(t *testing.T) { + + _, m, cancel := newTestManager(t) + defer cancel() + + _ = sendSampleTX(t, m, "0xaaaaa", 12345) + txHash := "0x" + fftypes.NewRandB32().String() + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("TransactionSend", mock.Anything, mock.Anything).Return(&ffcapi.TransactionSendResponse{ + TransactionHash: txHash, + }, ffcapi.ErrorReason(""), nil).Once() + + mc := m.confirmations.(*confirmationsmocks.Manager) + mc.On("Notify", mock.Anything).Return(fmt.Errorf("pop")) + + m.policyLoopCycle(m.ctx, true) + + mc.AssertExpectations(t) + mfc.AssertExpectations(t) + +} + +func TestInflightSetListFailCancel(t *testing.T) { + + _, m, close := newTestManagerMockPersistence(t) + close() + + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("ListTransactionsPending", m.ctx, (*fftypes.UUID)(nil), m.maxInFlight, persistence.SortDirectionAscending). + Return(nil, fmt.Errorf("pop")) + + m.policyLoopCycle(m.ctx, true) + + mp.AssertExpectations(t) + +} + +func TestPolicyLoopUpdateFail(t *testing.T) { + + _, m, close := newTestManagerMockPersistence(t) + defer close() + + m.inflight = []*pendingState{ + { + confirmed: true, + mtx: &apitypes.ManagedTX{ + ID: fmt.Sprintf("ns1/%s", fftypes.NewUUID()), + Created: fftypes.Now(), + SequenceID: apitypes.NewULID(), + Nonce: fftypes.NewFFBigInt(1000), + Status: apitypes.TxStatusSucceeded, + FirstSubmit: fftypes.Now(), + Receipt: &ffcapi.TransactionReceiptResponse{}, + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x12345", + }, + }, + }, + } + + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("WriteTransaction", m.ctx, mock.Anything, false).Return(fmt.Errorf("pop")) + mp.On("Close", mock.Anything).Return(nil).Maybe() + + m.policyLoopCycle(m.ctx, false) + + mp.AssertExpectations(t) + +} + +func TestPolicyEngineFailStaleThenUpdated(t *testing.T) { + + _, m, cancel := newTestManager(t) + defer cancel() + m.policyLoopInterval = 1 * time.Hour + + mpe := &policyenginemocks.PolicyEngine{} + m.policyEngine = mpe + done1 := make(chan struct{}) + mpe.On("Execute", mock.Anything, mock.Anything, mock.Anything). + Return(false, ffcapi.ErrorReason(""), fmt.Errorf("pop")). + Once(). + Run(func(args mock.Arguments) { + close(done1) + m.markInflightUpdate() + }) + + done2 := make(chan struct{}) + mpe.On("Execute", mock.Anything, mock.Anything, mock.Anything). + Return(false, ffcapi.ErrorReason(""), fmt.Errorf("pop")). + Once(). + Run(func(args mock.Arguments) { + close(done2) + }) + + _ = sendSampleTX(t, m, "0xaaaaa", 12345) + + m.Start() + + <-done1 + + <-done2 + + mpe.AssertExpectations(t) + +} + +func TestMarkInflightStaleDoesNotBlock(t *testing.T) { + + _, m, cancel := newTestManager(t) + defer cancel() + + m.markInflightStale() + m.markInflightStale() + +} + +func TestMarkInflightUpdateDoesNotBlock(t *testing.T) { + + _, m, cancel := newTestManager(t) + defer cancel() + + m.markInflightUpdate() + m.markInflightUpdate() + +} diff --git a/pkg/fftm/route__root_command.go b/pkg/fftm/route__root_command.go new file mode 100644 index 00000000..d4d910f1 --- /dev/null +++ b/pkg/fftm/route__root_command.go @@ -0,0 +1,106 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "context" + "net/http" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" +) + +var postRootCommand = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "postRootCommand", + Path: "/", + Method: http.MethodPost, + PathParams: nil, + QueryParams: nil, + Description: tmmsgs.APIEndpointPostRoot, + JSONInputValue: func() interface{} { return &apitypes.BaseRequest{} }, + JSONInputSchema: func(_ context.Context, schemaGen ffapi.SchemaGenerator) (*openapi3.SchemaRef, error) { + schemas := []*openapi3.SchemaRef{} + txRequest, err := schemaGen(&apitypes.TransactionRequest{}) + if err == nil { + schemas = append(schemas, txRequest) + } + deployRequest, err := schemaGen(&apitypes.ContractDeployRequest{}) + if err == nil { + schemas = append(schemas, deployRequest) + } + queryRequest, err := schemaGen(&apitypes.QueryRequest{}) + if err == nil { + schemas = append(schemas, queryRequest) + } + return &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + AnyOf: schemas, + }, + }, err + }, + JSONOutputSchema: func(ctx context.Context, schemaGen ffapi.SchemaGenerator) (*openapi3.SchemaRef, error) { + managedTX, _ := schemaGen(&apitypes.QueryRequest{}) + return &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + AnyOf: openapi3.SchemaRefs{ + {Value: &openapi3.Schema{ + Description: i18n.Expand(ctx, tmmsgs.APIEndpointDeleteEventStream), + }}, + managedTX, + }, + }, + }, nil + }, + JSONOutputCodes: []int{http.StatusAccepted}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + baseReq := r.Input.(*apitypes.BaseRequest) + switch baseReq.Headers.Type { + case apitypes.RequestTypeSendTransaction: + var tReq apitypes.TransactionRequest + if err = baseReq.UnmarshalTo(&tReq); err != nil { + return nil, i18n.NewError(r.Req.Context(), tmmsgs.MsgInvalidRequestErr, baseReq.Headers.Type, err) + } + return m.sendManagedTransaction(r.Req.Context(), &tReq) + case apitypes.RequestTypeDeploy: + var tReq apitypes.ContractDeployRequest + if err = baseReq.UnmarshalTo(&tReq); err != nil { + return nil, i18n.NewError(r.Req.Context(), tmmsgs.MsgInvalidRequestErr, baseReq.Headers.Type, err) + } + return m.sendManagedContractDeployment(r.Req.Context(), &tReq) + case apitypes.RequestTypeQuery: + var tReq apitypes.QueryRequest + if err = baseReq.UnmarshalTo(&tReq); err != nil { + return nil, i18n.NewError(r.Req.Context(), tmmsgs.MsgInvalidRequestErr, baseReq.Headers.Type, err) + } + res, _, err := m.connector.QueryInvoke(r.Req.Context(), &ffcapi.QueryInvokeRequest{ + TransactionInput: tReq.TransactionInput, + }) + if err != nil { + return nil, err + } + return res.Outputs, nil + default: + return nil, i18n.NewError(r.Req.Context(), tmmsgs.MsgUnsupportedRequestType, baseReq.Headers.Type) + } + }, + } +} diff --git a/pkg/fftm/route_delete_eventstream.go b/pkg/fftm/route_delete_eventstream.go new file mode 100644 index 00000000..841a037c --- /dev/null +++ b/pkg/fftm/route_delete_eventstream.go @@ -0,0 +1,44 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" +) + +var deleteEventStream = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "deleteEventStream", + Path: "/eventstreams/{streamId}", + Method: http.MethodDelete, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointDeleteEventStream, + JSONInputValue: nil, + JSONOutputValue: nil, + JSONOutputCodes: []int{http.StatusNoContent}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + err = m.deleteStream(r.Req.Context(), r.PP["streamId"]) + return nil, err + }, + } +} diff --git a/pkg/fftm/route_delete_eventstream_listener.go b/pkg/fftm/route_delete_eventstream_listener.go new file mode 100644 index 00000000..7dc890d8 --- /dev/null +++ b/pkg/fftm/route_delete_eventstream_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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var deleteEventStreamListener = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "deleteEventStreamListener", + Path: "/eventstreams/{streamId}/listeners/{listenerId}", + Method: http.MethodDelete, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + {Name: "listenerId", Description: tmmsgs.APIParamListenerID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointDeleteEventStreamListener, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusNoContent}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return nil, m.deleteListener(r.Req.Context(), r.PP["streamId"], r.PP["listenerId"]) + }, + } +} diff --git a/pkg/fftm/route_delete_eventstream_listener_test.go b/pkg/fftm/route_delete_eventstream_listener_test.go new file mode 100644 index 00000000..2b630c64 --- /dev/null +++ b/pkg/fftm/route_delete_eventstream_listener_test.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 fftm + +import ( + "fmt" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestDeleteEventStreamListener(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Create some listeners + var l1 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + assert.NoError(t, err) + + // Then get it + res, err = resty.New().R().Delete(fmt.Sprintf("%s/eventstreams/%s/listeners/%s", url, es1.ID, l1.ID)) + assert.NoError(t, err) + assert.Equal(t, 204, res.StatusCode()) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/route_delete_eventstream_test.go b/pkg/fftm/route_delete_eventstream_test.go new file mode 100644 index 00000000..c2200703 --- /dev/null +++ b/pkg/fftm/route_delete_eventstream_test.go @@ -0,0 +1,62 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestDeleteEventStream(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + err := m.Start() + assert.NoError(t, err) + + // Create stream + var es apitypes.EventStream + res, err := resty.New().R(). + SetBody(&apitypes.EventStream{ + Name: strPtr("my event stream"), + }). + SetResult(&es). + Post(url + "/eventstreams") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + // Then delete it + res, err = resty.New().R(). + SetResult(&es). + Delete(url + "/eventstreams/" + es.ID.String()) + assert.NoError(t, err) + assert.Equal(t, 204, res.StatusCode()) + + assert.Nil(t, m.eventStreams[(*es.ID)]) + +} diff --git a/pkg/fftm/route_delete_subscription.go b/pkg/fftm/route_delete_subscription.go new file mode 100644 index 00000000..fd097996 --- /dev/null +++ b/pkg/fftm/route_delete_subscription.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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var deleteSubscription = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "deleteSubscription", + Path: "/subscriptions/{listenerId}", + Deprecated: true, // in favor of "/eventstreams/{streamId}/listeners/{listenerId}" + Method: http.MethodDelete, + PathParams: []*ffapi.PathParam{ + {Name: "listenerId", Description: tmmsgs.APIParamListenerID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointDeleteSubscription, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusNoContent}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return nil, m.deleteListener(r.Req.Context(), "" /* no streamId on this path */, r.PP["listenerId"]) + }, + } +} diff --git a/pkg/fftm/route_delete_subscription_test.go b/pkg/fftm/route_delete_subscription_test.go new file mode 100644 index 00000000..9214b85f --- /dev/null +++ b/pkg/fftm/route_delete_subscription_test.go @@ -0,0 +1,62 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestDeleteSubscription(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Create some listeners + var l1 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + assert.NoError(t, err) + + // Then get it + res, err = resty.New().R().Delete(url + "/subscriptions/" + l1.ID.String()) + assert.NoError(t, err) + assert.Equal(t, 204, res.StatusCode()) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/route_get_eventstream.go b/pkg/fftm/route_get_eventstream.go new file mode 100644 index 00000000..19694c98 --- /dev/null +++ b/pkg/fftm/route_get_eventstream.go @@ -0,0 +1,44 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var getEventStream = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "getEventStream", + Path: "/eventstreams/{streamId}", + Method: http.MethodGet, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointGetEventStream, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return &apitypes.EventStreamWithStatus{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.getStream(r.Req.Context(), r.PP["streamId"]) + }, + } +} diff --git a/pkg/fftm/route_get_eventstream_listener.go b/pkg/fftm/route_get_eventstream_listener.go new file mode 100644 index 00000000..2e80ea76 --- /dev/null +++ b/pkg/fftm/route_get_eventstream_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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var getEventStreamListener = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "getEventStreamListener", + Path: "/eventstreams/{streamId}/listeners/{listenerId}", + Method: http.MethodGet, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + {Name: "listenerId", Description: tmmsgs.APIParamListenerID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointGetEventStreamListener, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.getListener(r.Req.Context(), r.PP["streamId"], r.PP["listenerId"]) + }, + } +} diff --git a/pkg/fftm/route_get_eventstream_listener_test.go b/pkg/fftm/route_get_eventstream_listener_test.go new file mode 100644 index 00000000..a750b8dc --- /dev/null +++ b/pkg/fftm/route_get_eventstream_listener_test.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 fftm + +import ( + "fmt" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetEventStreamsListener(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Create some listeners + var l1 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + assert.NoError(t, err) + + // Then get it + var listener apitypes.Listener + res, err = resty.New().R(). + SetResult(&listener). + Get(fmt.Sprintf("%s/eventstreams/%s/listeners/%s", url, es1.ID, l1.ID)) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + assert.Equal(t, l1.ID, listener.ID) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/route_get_eventstream_listeners.go b/pkg/fftm/route_get_eventstream_listeners.go new file mode 100644 index 00000000..52c94c78 --- /dev/null +++ b/pkg/fftm/route_get_eventstream_listeners.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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var getEventStreamListeners = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "getEventStreamListeners", + Path: "/eventstreams/{streamId}/listeners", + Method: http.MethodGet, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + }, + QueryParams: []*ffapi.QueryParam{ + {Name: "limit", Description: tmmsgs.APIParamLimit}, + {Name: "after", Description: tmmsgs.APIParamAfter}, + }, + Description: tmmsgs.APIEndpointGetEventStreamListeners, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return []*apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.getStreamListeners(r.Req.Context(), r.QP["after"], r.QP["limit"], r.PP["streamId"]) + }, + } +} diff --git a/pkg/fftm/route_get_eventstream_listeners_test.go b/pkg/fftm/route_get_eventstream_listeners_test.go new file mode 100644 index 00000000..5e2eaf34 --- /dev/null +++ b/pkg/fftm/route_get_eventstream_listeners_test.go @@ -0,0 +1,76 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "fmt" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetEventStreamListeners(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1, es2 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + res, err = resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream2")}).SetResult(&es2).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Create some listeners + var l1, l2, l3 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + assert.NoError(t, err) + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener2"), StreamID: es2.ID}).SetResult(&l2).Post(url + "/subscriptions") + assert.NoError(t, err) + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener3"), StreamID: es1.ID}).SetResult(&l3).Post(url + "/subscriptions") + assert.NoError(t, err) + + // Then get it + var listeners []*apitypes.Listener + res, err = resty.New().R(). + SetResult(&listeners). + Get(fmt.Sprintf("%s/eventstreams/%s/listeners?limit=1&after=%s", url, es1.ID, l2.ID)) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + assert.Len(t, listeners, 1) + assert.Equal(t, l1.ID, listeners[0].ID) + assert.Equal(t, es1.ID, listeners[0].StreamID) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/route_get_eventstream_test.go b/pkg/fftm/route_get_eventstream_test.go new file mode 100644 index 00000000..580465df --- /dev/null +++ b/pkg/fftm/route_get_eventstream_test.go @@ -0,0 +1,64 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetEventStream(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + err := m.Start() + assert.NoError(t, err) + + // Create stream + var es apitypes.EventStream + res, err := resty.New().R(). + SetBody(&apitypes.EventStream{ + Name: strPtr("my event stream"), + }). + SetResult(&es). + Post(url + "/eventstreams") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + // Then get it + var ess apitypes.EventStreamWithStatus + res, err = resty.New().R(). + SetResult(&ess). + Get(url + "/eventstreams/" + es.ID.String()) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + assert.Equal(t, es.ID, ess.ID) + assert.Equal(t, apitypes.EventStreamStatusStarted, ess.Status) + +} diff --git a/pkg/fftm/route_get_eventstreams.go b/pkg/fftm/route_get_eventstreams.go new file mode 100644 index 00000000..0cb27503 --- /dev/null +++ b/pkg/fftm/route_get_eventstreams.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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var getEventStreams = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "getEventStreams", + Path: "/eventstreams", + Method: http.MethodGet, + PathParams: nil, + QueryParams: []*ffapi.QueryParam{ + {Name: "limit", Description: tmmsgs.APIParamLimit}, + {Name: "after", Description: tmmsgs.APIParamAfter}, + }, + Description: tmmsgs.APIEndpointGetEventStreams, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return []*apitypes.EventStream{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.getStreams(r.Req.Context(), r.QP["after"], r.QP["limit"]) + }, + } +} diff --git a/pkg/fftm/route_get_eventstreams_test.go b/pkg/fftm/route_get_eventstreams_test.go new file mode 100644 index 00000000..7f10a3b0 --- /dev/null +++ b/pkg/fftm/route_get_eventstreams_test.go @@ -0,0 +1,62 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetEventStreams(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + err := m.Start() + assert.NoError(t, err) + + // Create 3 streams + var es1, es2, es3 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + res, err = resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream2")}).SetResult(&es2).Post(url + "/eventstreams") + assert.NoError(t, err) + res, err = resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream3")}).SetResult(&es3).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Then get it + var ess []*apitypes.EventStream + res, err = resty.New().R(). + SetResult(&ess). + Get(url + "/eventstreams?limit=1&after=" + es2.ID.String()) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + assert.Len(t, ess, 1) + assert.Equal(t, es1.ID, ess[0].ID) + +} diff --git a/pkg/fftm/route_get_subscription.go b/pkg/fftm/route_get_subscription.go new file mode 100644 index 00000000..68d47e05 --- /dev/null +++ b/pkg/fftm/route_get_subscription.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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var getSubscription = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "getSubscription", + Path: "/subscriptions/{listenerId}", + Deprecated: true, // in favor of "/eventstreams/{streamId}/listeners/{listenerId}" + Method: http.MethodGet, + PathParams: []*ffapi.PathParam{ + {Name: "listenerId", Description: tmmsgs.APIParamListenerID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointGetSubscription, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.getListener(r.Req.Context(), "" /* no streamId on this path */, r.PP["listenerId"]) + }, + } +} diff --git a/pkg/fftm/route_get_subscription_test.go b/pkg/fftm/route_get_subscription_test.go new file mode 100644 index 00000000..88fd8a98 --- /dev/null +++ b/pkg/fftm/route_get_subscription_test.go @@ -0,0 +1,67 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetSubscription(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Create some listeners + var l1 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + assert.NoError(t, err) + + // Then get it + var listener apitypes.Listener + res, err = resty.New().R(). + SetResult(&listener). + Get(url + "/subscriptions/" + l1.ID.String()) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + assert.Equal(t, l1.ID, listener.ID) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/route_get_subscriptions.go b/pkg/fftm/route_get_subscriptions.go new file mode 100644 index 00000000..b3bdca3f --- /dev/null +++ b/pkg/fftm/route_get_subscriptions.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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var getSubscriptions = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "getSubscriptions", + Path: "/subscriptions", + Deprecated: true, // in favor of "/eventstreams/{id}/listeners" + Method: http.MethodGet, + PathParams: nil, + QueryParams: []*ffapi.QueryParam{ + {Name: "limit", Description: tmmsgs.APIParamLimit}, + {Name: "after", Description: tmmsgs.APIParamAfter}, + }, + Description: tmmsgs.APIEndpointGetSubscriptions, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return []*apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.getListeners(r.Req.Context(), r.QP["after"], r.QP["limit"]) + }, + } +} diff --git a/pkg/fftm/route_get_subscriptions_test.go b/pkg/fftm/route_get_subscriptions_test.go new file mode 100644 index 00000000..fdb85f97 --- /dev/null +++ b/pkg/fftm/route_get_subscriptions_test.go @@ -0,0 +1,71 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetSubscriptions(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Create some listeners + var l1, l2 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + assert.NoError(t, err) + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener2"), StreamID: es1.ID}).SetResult(&l2).Post(url + "/subscriptions") + assert.NoError(t, err) + + // Then get it + var listeners []*apitypes.Listener + res, err = resty.New().R(). + SetResult(&listeners). + Get(url + "/subscriptions?limit=1&after=" + l2.ID.String()) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + assert.Len(t, listeners, 1) + assert.Equal(t, l1.ID, listeners[0].ID) + assert.Equal(t, es1.ID, listeners[0].StreamID) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/route_get_transaction.go b/pkg/fftm/route_get_transaction.go new file mode 100644 index 00000000..2125abd3 --- /dev/null +++ b/pkg/fftm/route_get_transaction.go @@ -0,0 +1,44 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var getTransaction = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "getTransaction", + Path: "/transactions/{transactionId}", + Method: http.MethodGet, + PathParams: []*ffapi.PathParam{ + {Name: "transactionId", Description: tmmsgs.APIParamTransactionID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointGetSubscriptions, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return &apitypes.ManagedTX{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.getTransactionByID(r.Req.Context(), r.PP["transactionId"]) + }, + } +} diff --git a/pkg/fftm/route_get_transaction_test.go b/pkg/fftm/route_get_transaction_test.go new file mode 100644 index 00000000..0fda2662 --- /dev/null +++ b/pkg/fftm/route_get_transaction_test.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 fftm + +import ( + "fmt" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/stretchr/testify/assert" +) + +func TestGetTransaction(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + noopPolicyEngine(m) + + err := m.Start() + assert.NoError(t, err) + + txIn := newTestTxn(t, m, "0xaaaaa", 10001, apitypes.TxStatusSucceeded) + + var txOut *apitypes.ManagedTX + res, err := resty.New().R(). + SetResult(&txOut). + Get(fmt.Sprintf("%s/transactions/%s", url, txIn.ID)) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + assert.Equal(t, *txIn, *txOut) + +} diff --git a/pkg/fftm/route_get_transactions.go b/pkg/fftm/route_get_transactions.go new file mode 100644 index 00000000..e85b1839 --- /dev/null +++ b/pkg/fftm/route_get_transactions.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 fftm + +import ( + "net/http" + "strings" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var getTransactions = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "getTransactions", + Path: "/transactions", + Method: http.MethodGet, + PathParams: nil, + QueryParams: []*ffapi.QueryParam{ + {Name: "limit", Description: tmmsgs.APIParamLimit}, + {Name: "after", Description: tmmsgs.APIParamAfter}, + {Name: "signer", Description: tmmsgs.APIParamTXSigner}, + {Name: "pending", Description: tmmsgs.APIParamTXPending, IsBool: true}, + {Name: "direction", Description: tmmsgs.APIParamSortDirection}, + }, + Description: tmmsgs.APIEndpointGetSubscriptions, + JSONInputValue: nil, + JSONOutputValue: func() interface{} { return []*apitypes.ManagedTX{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.getTransactions(r.Req.Context(), r.QP["after"], r.QP["limit"], r.QP["signer"], strings.EqualFold(r.QP["pending"], "true"), r.QP["direction"]) + }, + } +} diff --git a/pkg/fftm/route_get_transactions_test.go b/pkg/fftm/route_get_transactions_test.go new file mode 100644 index 00000000..b2fd9924 --- /dev/null +++ b/pkg/fftm/route_get_transactions_test.go @@ -0,0 +1,110 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "context" + "fmt" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/mocks/policyenginemocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func newTestTxn(t *testing.T, m *manager, signer string, nonce int64, status apitypes.TxStatus) *apitypes.ManagedTX { + tx := &apitypes.ManagedTX{ + ID: fmt.Sprintf("ns1:%s", fftypes.NewUUID()), + Created: fftypes.Now(), + SequenceID: apitypes.NewULID(), + Nonce: fftypes.NewFFBigInt(nonce), + Status: status, + TransactionHeaders: ffcapi.TransactionHeaders{ + From: signer, + }, + } + err := m.persistence.WriteTransaction(context.Background(), tx, true) + assert.NoError(t, err) + return tx +} + +func noopPolicyEngine(m *manager) { + mpe := &policyenginemocks.PolicyEngine{} + m.policyEngine = mpe + mpe.On("Execute", mock.Anything, mock.Anything, mock.Anything).Return(false, ffcapi.ErrorReason(""), nil).Maybe() +} + +func TestGetTransactions(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + noopPolicyEngine(m) + + err := m.Start() + assert.NoError(t, err) + + // Create a few persisted transaction directly in the persistence + s1t1 := newTestTxn(t, m, "0xaaaaa", 10001, apitypes.TxStatusSucceeded) + s2t1 := newTestTxn(t, m, "0xbbbbb", 10001, apitypes.TxStatusPending) + s1t2 := newTestTxn(t, m, "0xaaaaa", 10002, apitypes.TxStatusFailed) + s1t3 := newTestTxn(t, m, "0xaaaaa", 10003, apitypes.TxStatusPending) + + // Get with no filtering (not reverse order) + var transactions []*apitypes.ManagedTX + res, err := resty.New().R(). + SetResult(&transactions). + Get(url + "/transactions?direction=asc") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + assert.Len(t, transactions, 4) + assert.Equal(t, s1t1.ID, transactions[0].ID) + assert.Equal(t, s2t1.ID, transactions[1].ID) + assert.Equal(t, s1t2.ID, transactions[2].ID) + assert.Equal(t, s1t3.ID, transactions[3].ID) + + // Test pagination on default sort/filter + res, err = resty.New().R(). + SetResult(&transactions). + Get(url + "/transactions?limit=1&after=" + s1t2.ID) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + assert.Len(t, transactions, 1) + assert.Equal(t, s2t1.ID, transactions[0].ID) + + // Test pagination on nonce filter + res, err = resty.New().R(). + SetResult(&transactions). + Get(url + "/transactions?signer=0xaaaaa&after=" + s1t2.ID) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + assert.Len(t, transactions, 1) + assert.Equal(t, s1t1.ID, transactions[0].ID) + + // Test pagination on pending filter + res, err = resty.New().R(). + SetResult(&transactions). + Get(url + "/transactions?pending&after=" + s1t3.ID) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + assert.Len(t, transactions, 1) + assert.Equal(t, s2t1.ID, transactions[0].ID) + +} diff --git a/pkg/fftm/route_patch_eventstream.go b/pkg/fftm/route_patch_eventstream.go new file mode 100644 index 00000000..8ebb395e --- /dev/null +++ b/pkg/fftm/route_patch_eventstream.go @@ -0,0 +1,44 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var patchEventStream = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "patchEventStream", + Path: "/eventstreams/{streamId}", + Method: http.MethodPatch, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointPatchEventStream, + JSONInputValue: func() interface{} { return &apitypes.EventStream{} }, + JSONOutputValue: func() interface{} { return &apitypes.EventStream{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.updateStream(r.Req.Context(), r.PP["streamId"], r.Input.(*apitypes.EventStream)) + }, + } +} diff --git a/pkg/fftm/route_patch_eventstream_listener.go b/pkg/fftm/route_patch_eventstream_listener.go new file mode 100644 index 00000000..76302cc4 --- /dev/null +++ b/pkg/fftm/route_patch_eventstream_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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var patchEventStreamListener = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "patchEventStreamListener", + Path: "/eventstreams/{streamId}/listeners/{listenerId}", + Method: http.MethodPatch, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + {Name: "listenerId", Description: tmmsgs.APIParamListenerID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointPatchEventStreamListener, + JSONInputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.updateExistingListener(r.Req.Context(), r.PP["streamId"], r.PP["listenerId"], r.Input.(*apitypes.Listener), false) + }, + } +} diff --git a/pkg/fftm/route_patch_eventstream_listener_test.go b/pkg/fftm/route_patch_eventstream_listener_test.go new file mode 100644 index 00000000..74076573 --- /dev/null +++ b/pkg/fftm/route_patch_eventstream_listener_test.go @@ -0,0 +1,72 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "fmt" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPatchEventStreamListener(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Create some listeners + var l1 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + assert.NoError(t, err) + + // Then get it + var listener apitypes.Listener + res, err = resty.New().R(). + SetBody(&apitypes.Listener{ + Name: strPtr("listener1a"), + }). + SetResult(&listener). + Patch(fmt.Sprintf("%s/eventstreams/%s/listeners/%s", url, es1.ID, l1.ID)) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + assert.Equal(t, l1.ID, listener.ID) + assert.Equal(t, "listener1a", *listener.Name) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/route_patch_eventstream_test.go b/pkg/fftm/route_patch_eventstream_test.go new file mode 100644 index 00000000..7d5dc2f1 --- /dev/null +++ b/pkg/fftm/route_patch_eventstream_test.go @@ -0,0 +1,67 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPatchEventStream(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + err := m.Start() + assert.NoError(t, err) + + // Create stream + var es apitypes.EventStream + res, err := resty.New().R(). + SetBody(&apitypes.EventStream{ + Name: strPtr("my event stream"), + }). + SetResult(&es). + Post(url + "/eventstreams") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + // Then update it + res, err = resty.New().R(). + SetBody(&apitypes.EventStream{ + Name: strPtr("my renamed event stream"), + }). + SetResult(&es). + Patch(url + "/eventstreams/" + es.ID.String()) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + assert.NotNil(t, es.ID) + assert.NotNil(t, es.Created) + assert.NotEqual(t, es.Created, es.Updated) + assert.Equal(t, "my renamed event stream", *es.Name) + +} diff --git a/pkg/fftm/route_patch_subscription.go b/pkg/fftm/route_patch_subscription.go new file mode 100644 index 00000000..9ae7ce22 --- /dev/null +++ b/pkg/fftm/route_patch_subscription.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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var patchSubscription = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "patchSubscription", + Path: "/subscriptions/{listenerId}", + Deprecated: true, // in favor of "/eventstreams/{streamId}/listeners/{listenerId}" + Method: http.MethodPatch, + PathParams: []*ffapi.PathParam{ + {Name: "listenerId", Description: tmmsgs.APIParamListenerID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointPatchSubscription, + JSONInputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.updateExistingListener(r.Req.Context(), "" /* no streamId on this path */, r.PP["listenerId"], r.Input.(*apitypes.Listener), false) + }, + } +} diff --git a/pkg/fftm/route_patch_subscription_test.go b/pkg/fftm/route_patch_subscription_test.go new file mode 100644 index 00000000..47b56146 --- /dev/null +++ b/pkg/fftm/route_patch_subscription_test.go @@ -0,0 +1,71 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPatchSubscription(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Create some listeners + var l1 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + assert.NoError(t, err) + + // Then get it + var listener apitypes.Listener + res, err = resty.New().R(). + SetBody(&apitypes.Listener{ + Name: strPtr("listener1a"), + }). + SetResult(&listener). + Patch(url + "/subscriptions/" + l1.ID.String()) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + assert.Equal(t, l1.ID, listener.ID) + assert.Equal(t, "listener1a", *listener.Name) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/route_post_eventstream.go b/pkg/fftm/route_post_eventstream.go new file mode 100644 index 00000000..c41d70cb --- /dev/null +++ b/pkg/fftm/route_post_eventstream.go @@ -0,0 +1,42 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var postEventStream = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "postEventStream", + Path: "/eventstreams", + Method: http.MethodPost, + PathParams: nil, + QueryParams: nil, + Description: tmmsgs.APIEndpointPostEventStream, + JSONInputValue: func() interface{} { return &apitypes.EventStream{} }, + JSONOutputValue: func() interface{} { return &apitypes.EventStream{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.createAndStoreNewStream(r.Req.Context(), r.Input.(*apitypes.EventStream)) + }, + } +} diff --git a/pkg/fftm/route_post_eventstream_listener_reset.go b/pkg/fftm/route_post_eventstream_listener_reset.go new file mode 100644 index 00000000..39741977 --- /dev/null +++ b/pkg/fftm/route_post_eventstream_listener_reset.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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var postEventStreamListenerReset = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "postEventStreamListenerReset", + Path: "/eventstreams/{streamId}/listeners/{listenerId}/reset", + Method: http.MethodPost, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + {Name: "listenerId", Description: tmmsgs.APIParamListenerID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointPostEventStreamListenerReset, + JSONInputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.updateExistingListener(r.Req.Context(), r.PP["streamId"], r.PP["listenerId"], r.Input.(*apitypes.Listener), true) + }, + } +} diff --git a/pkg/fftm/route_post_eventstream_listener_reset_test.go b/pkg/fftm/route_post_eventstream_listener_reset_test.go new file mode 100644 index 00000000..77273ea9 --- /dev/null +++ b/pkg/fftm/route_post_eventstream_listener_reset_test.go @@ -0,0 +1,72 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "fmt" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPostEventStreamListenerReset(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Create some listeners + var l1 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + assert.NoError(t, err) + + // Then get it + var listener apitypes.Listener + res, err = resty.New().R(). + SetBody(&apitypes.Listener{ + Name: strPtr("listener1a"), + }). + SetResult(&listener). + Post(fmt.Sprintf("%s/eventstreams/%s/listeners/%s/reset", url, es1.ID, l1.ID)) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + assert.Equal(t, l1.ID, listener.ID) + assert.Equal(t, "listener1a", *listener.Name) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/route_post_eventstream_listeners.go b/pkg/fftm/route_post_eventstream_listeners.go new file mode 100644 index 00000000..c5e03f08 --- /dev/null +++ b/pkg/fftm/route_post_eventstream_listeners.go @@ -0,0 +1,44 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var postEventStreamListeners = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "postEventStreamListeners", + Path: "/eventstreams/{streamId}/listeners", + Method: http.MethodPost, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointPostEventStreamListener, + JSONInputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.createAndStoreNewStreamListener(r.Req.Context(), r.PP["streamId"], r.Input.(*apitypes.Listener)) + }, + } +} diff --git a/pkg/fftm/route_post_eventstream_listeners_test.go b/pkg/fftm/route_post_eventstream_listeners_test.go new file mode 100644 index 00000000..f40343c5 --- /dev/null +++ b/pkg/fftm/route_post_eventstream_listeners_test.go @@ -0,0 +1,60 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "fmt" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPostEventStreamListeners(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + // Create a listener + var l1 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(fmt.Sprintf("%s/eventstreams/%s/listeners", url, es1.ID)) + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/route_post_eventstream_resume.go b/pkg/fftm/route_post_eventstream_resume.go new file mode 100644 index 00000000..ce9c2931 --- /dev/null +++ b/pkg/fftm/route_post_eventstream_resume.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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var postEventStreamResume = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "postEventStreamResume", + Path: "/eventstreams/{streamId}/resume", + Method: http.MethodPost, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointPostEventStreamResume, + JSONInputValue: func() interface{} { return struct{}{} }, // empty input + JSONOutputValue: func() interface{} { return struct{}{} }, // empty output + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + falsy := false + _, err = m.updateStream(r.Req.Context(), r.PP["streamId"], &apitypes.EventStream{ + Suspended: &falsy, + }) + return &struct{}{}, err + }, + } +} diff --git a/pkg/fftm/route_post_eventstream_resume_test.go b/pkg/fftm/route_post_eventstream_resume_test.go new file mode 100644 index 00000000..886161bd --- /dev/null +++ b/pkg/fftm/route_post_eventstream_resume_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 fftm + +import ( + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPostEventStreamResume(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + err := m.Start() + assert.NoError(t, err) + + // Create stream + var es apitypes.EventStream + truthy := true + res, err := resty.New().R(). + SetBody(&apitypes.EventStream{ + Name: strPtr("my event stream"), + Suspended: &truthy, + }). + SetResult(&es). + Post(url + "/eventstreams") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + // Then suspend it + res, err = resty.New().R(). + SetBody(&struct{}{}). + SetResult(&es). + Post(url + "/eventstreams/" + es.ID.String() + "/resume") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + assert.Equal(t, apitypes.EventStreamStatusStarted, m.eventStreams[(*es.ID)].Status()) + +} diff --git a/pkg/fftm/route_post_eventstream_suspend.go b/pkg/fftm/route_post_eventstream_suspend.go new file mode 100644 index 00000000..3551ee42 --- /dev/null +++ b/pkg/fftm/route_post_eventstream_suspend.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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var postEventStreamSuspend = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "postEventStreamSuspend", + Path: "/eventstreams/{streamId}/suspend", + Method: http.MethodPost, + PathParams: []*ffapi.PathParam{ + {Name: "streamId", Description: tmmsgs.APIParamStreamID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointPostEventStreamSuspend, + JSONInputValue: func() interface{} { return struct{}{} }, // empty input + JSONOutputValue: func() interface{} { return struct{}{} }, // empty output + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + truthy := true + _, err = m.updateStream(r.Req.Context(), r.PP["streamId"], &apitypes.EventStream{ + Suspended: &truthy, + }) + return &struct{}{}, err + }, + } +} diff --git a/pkg/fftm/route_post_eventstream_suspend_test.go b/pkg/fftm/route_post_eventstream_suspend_test.go new file mode 100644 index 00000000..dc70c438 --- /dev/null +++ b/pkg/fftm/route_post_eventstream_suspend_test.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 fftm + +import ( + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPostEventStreamSuspend(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + err := m.Start() + assert.NoError(t, err) + + // Create stream + var es apitypes.EventStream + res, err := resty.New().R(). + SetBody(&apitypes.EventStream{ + Name: strPtr("my event stream"), + }). + SetResult(&es). + Post(url + "/eventstreams") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + // Then suspend it + res, err = resty.New().R(). + SetBody(&struct{}{}). + SetResult(&es). + Post(url + "/eventstreams/" + es.ID.String() + "/suspend") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + assert.Equal(t, apitypes.EventStreamStatusStopped, m.eventStreams[(*es.ID)].Status()) + +} diff --git a/pkg/fftm/route_post_eventstream_test.go b/pkg/fftm/route_post_eventstream_test.go new file mode 100644 index 00000000..24c24863 --- /dev/null +++ b/pkg/fftm/route_post_eventstream_test.go @@ -0,0 +1,55 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPostNewEventStream(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + err := m.Start() + assert.NoError(t, err) + + var es apitypes.EventStream + res, err := resty.New().R(). + SetBody(&apitypes.EventStream{ + Name: strPtr("my event stream"), + }). + SetResult(&es). + Post(url + "/eventstreams") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + assert.NotNil(t, es.ID) + assert.NotNil(t, es.Created) + assert.Equal(t, es.Created, es.Updated) + +} diff --git a/pkg/fftm/route_post_subscription_reset.go b/pkg/fftm/route_post_subscription_reset.go new file mode 100644 index 00000000..d6f82f91 --- /dev/null +++ b/pkg/fftm/route_post_subscription_reset.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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var postSubscriptionReset = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "postSubscriptionReset", + Path: "/subscriptions/{listenerId}/reset", + Deprecated: true, // in favor of "/eventstreams/{streamId}/listeners/{listenerId}" + Method: http.MethodPost, + PathParams: []*ffapi.PathParam{ + {Name: "listenerId", Description: tmmsgs.APIParamListenerID}, + }, + QueryParams: nil, + Description: tmmsgs.APIEndpointPostSubscriptionReset, + JSONInputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.updateExistingListener(r.Req.Context(), "" /* no streamId on this path */, r.PP["listenerId"], r.Input.(*apitypes.Listener), true) + }, + } +} diff --git a/pkg/fftm/route_post_subscription_reset_test.go b/pkg/fftm/route_post_subscription_reset_test.go new file mode 100644 index 00000000..cf764071 --- /dev/null +++ b/pkg/fftm/route_post_subscription_reset_test.go @@ -0,0 +1,71 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPostSubscriptionReset(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + + // Create some listeners + var l1 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + assert.NoError(t, err) + + // Then get it + var listener apitypes.Listener + res, err = resty.New().R(). + SetBody(&apitypes.Listener{ + Name: strPtr("listener1a"), + }). + SetResult(&listener). + Post(url + "/subscriptions/" + l1.ID.String() + "/reset") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + assert.Equal(t, l1.ID, listener.ID) + assert.Equal(t, "listener1a", *listener.Name) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/route_post_subscriptions.go b/pkg/fftm/route_post_subscriptions.go new file mode 100644 index 00000000..313a5fd9 --- /dev/null +++ b/pkg/fftm/route_post_subscriptions.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 fftm + +import ( + "net/http" + + "github.com/hyperledger/firefly-common/pkg/ffapi" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +var postSubscriptions = func(m *manager) *ffapi.Route { + return &ffapi.Route{ + Name: "postSubscriptions", + Path: "/subscriptions", + Deprecated: true, // in favor of "/eventstreams/{id}/listeners" + Method: http.MethodPost, + PathParams: nil, + QueryParams: nil, + Description: tmmsgs.APIEndpointPostSubscriptions, + JSONInputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputValue: func() interface{} { return &apitypes.Listener{} }, + JSONOutputCodes: []int{http.StatusOK}, + JSONHandler: func(r *ffapi.APIRequest) (output interface{}, err error) { + return m.createAndStoreNewListener(r.Req.Context(), r.Input.(*apitypes.Listener)) + }, + } +} diff --git a/pkg/fftm/route_post_subscriptions_test.go b/pkg/fftm/route_post_subscriptions_test.go new file mode 100644 index 00000000..eca6b0a5 --- /dev/null +++ b/pkg/fftm/route_post_subscriptions_test.go @@ -0,0 +1,59 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "testing" + + "github.com/go-resty/resty/v2" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestPostSubscriptions(t *testing.T) { + + url, m, done := newTestManager(t) + defer done() + + err := m.Start() + assert.NoError(t, err) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerAddResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + // Create a stream + var es1 apitypes.EventStream + res, err := resty.New().R().SetBody(&apitypes.EventStream{Name: strPtr("stream1")}).SetResult(&es1).Post(url + "/eventstreams") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + // Create a listener + var l1 apitypes.Listener + res, err = resty.New().R().SetBody(&apitypes.Listener{Name: strPtr("listener1"), StreamID: es1.ID}).SetResult(&l1).Post(url + "/subscriptions") + assert.NoError(t, err) + assert.Equal(t, 200, res.StatusCode()) + + mfc.AssertExpectations(t) + +} diff --git a/pkg/fftm/routes.go b/pkg/fftm/routes.go new file mode 100644 index 00000000..04f57582 --- /dev/null +++ b/pkg/fftm/routes.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 fftm + +import "github.com/hyperledger/firefly-common/pkg/ffapi" + +func (m *manager) routes() []*ffapi.Route { + return []*ffapi.Route{ + deleteEventStream(m), + deleteEventStreamListener(m), + deleteSubscription(m), + getEventStream(m), + getEventStreamListener(m), + getEventStreamListeners(m), + getEventStreams(m), + getSubscription(m), + getSubscriptions(m), + getTransaction(m), + getTransactions(m), + patchEventStream(m), + patchEventStreamListener(m), + patchSubscription(m), + postEventStream(m), + postEventStreamListenerReset(m), + postEventStreamListeners(m), + postEventStreamResume(m), + postEventStreamSuspend(m), + postRootCommand(m), + postSubscriptionReset(m), + postSubscriptions(m), + } +} diff --git a/pkg/fftm/send_tx.go b/pkg/fftm/send_tx.go new file mode 100644 index 00000000..6d6dc8c0 --- /dev/null +++ b/pkg/fftm/send_tx.go @@ -0,0 +1,106 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "context" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" +) + +func (m *manager) sendManagedTransaction(ctx context.Context, request *apitypes.TransactionRequest) (*apitypes.ManagedTX, error) { + + // Prepare the transaction, which will mean we have a transaction that should be submittable. + // If we fail at this stage, we don't need to write any state as we are sure we haven't submitted + // anything to the blockchain itself. + prepared, _, err := m.connector.TransactionPrepare(ctx, &ffcapi.TransactionPrepareRequest{ + TransactionInput: request.TransactionInput, + }) + if err != nil { + return nil, err + } + + return m.submitPreparedTX(ctx, request.Headers.ID, &request.TransactionHeaders, prepared.Gas, prepared.TransactionData) +} + +func (m *manager) sendManagedContractDeployment(ctx context.Context, request *apitypes.ContractDeployRequest) (*apitypes.ManagedTX, error) { + + // Prepare the transaction, which will mean we have a transaction that should be submittable. + // If we fail at this stage, we don't need to write any state as we are sure we haven't submitted + // anything to the blockchain itself. + prepared, _, err := m.connector.DeployContractPrepare(ctx, &request.ContractDeployPrepareRequest) + if err != nil { + return nil, err + } + + return m.submitPreparedTX(ctx, request.Headers.ID, &request.TransactionHeaders, prepared.Gas, prepared.TransactionData) +} + +func (m *manager) submitPreparedTX(ctx context.Context, txID string, txHeaders *ffcapi.TransactionHeaders, gas *fftypes.FFBigInt, transactionData string) (*apitypes.ManagedTX, error) { + + // The request ID is the primary ID, and should be supplied by the user for idempotence + if txID == "" { + txID = fftypes.NewUUID().String() + } + + // First job is to assign the next nonce to this request. + // We block any further sends on this nonce until we've got this one successfully into the node, or + // fail deterministically in a way that allows us to return it. + lockedNonce, err := m.assignAndLockNonce(ctx, txID, txHeaders.From) + if err != nil { + return nil, err + } + // We will call markSpent() once we reach the point the nonce has been used + defer lockedNonce.complete(ctx) + + // Sequencing ID is always generated by us - so we have a deterministic order of transactions + // Note: We must allocate this within the nonce lock, to ensure that the nonce sequence and the + // global transaction sequence line up. + seqID := apitypes.NewULID() + + // Next we update FireFly core with the pre-submitted record pending record, with the allocated nonce. + // From this point on, we will guide this transaction through to submission. + // We return an "ack" at this point, and dispatch the work of getting the transaction submitted + // to the background worker. + now := fftypes.Now() + mtx := &apitypes.ManagedTX{ + ID: txID, // on input the request ID must be the namespaced operation ID + Created: now, + Updated: now, + SequenceID: seqID, + Nonce: fftypes.NewFFBigInt(int64(lockedNonce.nonce)), + Gas: gas, + TransactionHeaders: *txHeaders, + TransactionData: transactionData, + Status: apitypes.TxStatusPending, + } + + if err = m.persistence.WriteTransaction(m.ctx, mtx, true); err != nil { + return nil, err + } + log.L(m.ctx).Infof("Tracking transaction %s at nonce %s / %d", mtx.ID, mtx.TransactionHeaders.From, mtx.Nonce.Int64()) + m.markInflightStale() + + // Ok - we've spent it. The rest of the processing will be triggered off of lockedNonce + // completion adding this transaction to the pool (and/or the change event that comes in from + // FireFly core from the update to the transaction) + lockedNonce.spent = mtx + return mtx, nil +} diff --git a/pkg/fftm/send_tx_test.go b/pkg/fftm/send_tx_test.go new file mode 100644 index 00000000..dbbba75e --- /dev/null +++ b/pkg/fftm/send_tx_test.go @@ -0,0 +1,51 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/mocks/persistencemocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestSendTXPersistFail(t *testing.T) { + + _, m, close := newTestManagerMockPersistence(t) + defer close() + + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("ListTransactionsByNonce", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return([]*apitypes.ManagedTX{ + {ID: "id12345", Created: fftypes.Now(), Status: apitypes.TxStatusSucceeded, Nonce: fftypes.NewFFBigInt(1000)}, + }, nil) + mp.On("WriteTransaction", m.ctx, mock.Anything, true).Return(fmt.Errorf("pop")) + + var txReq *ffcapi.TransactionSendRequest + err := json.Unmarshal([]byte(sampleSendTX), &txReq) + assert.NoError(t, err) + + _, err = m.submitPreparedTX(m.ctx, "id1", &txReq.TransactionHeaders, fftypes.NewFFBigInt(12345), "0x123456") + assert.Regexp(t, "pop", err) + +} diff --git a/pkg/fftm/stream_management.go b/pkg/fftm/stream_management.go new file mode 100644 index 00000000..4f8b0e88 --- /dev/null +++ b/pkg/fftm/stream_management.go @@ -0,0 +1,408 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "context" + "encoding/json" + "strconv" + + "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-transaction-manager/internal/events" + "github.com/hyperledger/firefly-transaction-manager/internal/persistence" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +const ( + startupPaginationLimit = 25 +) + +func (m *manager) restoreStreams() error { + var lastInPage *fftypes.UUID + for { + streamDefs, err := m.persistence.ListStreams(m.ctx, lastInPage, startupPaginationLimit, persistence.SortDirectionAscending) + if err != nil { + return err + } + if len(streamDefs) == 0 { + break + } + for _, def := range streamDefs { + lastInPage = def.ID + streamListeners, err := m.persistence.ListStreamListeners(m.ctx, nil, 0, persistence.SortDirectionAscending, def.ID) + if err != nil { + return err + } + // check to see if it's already started + if m.eventStreams[*def.ID] == nil { + closeoutName, err := m.reserveStreamName(m.ctx, *def.Name, def.ID) + var s events.Stream + if err == nil { + s, err = m.addRuntimeStream(def, streamListeners) + } + if err == nil && !*def.Suspended { + err = s.Start(m.ctx) + } + if err != nil { + return err + } + closeoutName(err == nil) + } + } + } + return nil +} + +func (m *manager) deleteAllStreamListeners(ctx context.Context, streamID *fftypes.UUID) error { + var lastInPage *fftypes.UUID + for { + listenerDefs, err := m.persistence.ListStreamListeners(ctx, lastInPage, startupPaginationLimit, persistence.SortDirectionAscending, streamID) + if err != nil { + return err + } + if len(listenerDefs) == 0 { + break + } + for _, def := range listenerDefs { + lastInPage = def.ID + if err := m.persistence.DeleteListener(ctx, def.ID); err != nil { + return err + } + } + } + return nil +} + +func (m *manager) addRuntimeStream(def *apitypes.EventStream, listeners []*apitypes.Listener) (events.Stream, error) { + s, err := events.NewEventStream(m.ctx, def, m.connector, m.persistence, m.wsServer, listeners) + if err != nil { + return nil, err + } + spec := s.Spec() + m.mux.Lock() + m.eventStreams[*spec.ID] = s + m.mux.Unlock() + return s, nil +} + +func (m *manager) deleteStream(ctx context.Context, idStr string) error { + id, err := fftypes.ParseUUID(ctx, idStr) + if err != nil { + return err + } + m.mux.Lock() + s := m.eventStreams[*id] + delete(m.eventStreams, *id) + if s != nil { + delete(m.streamsByName, *s.Spec().Name) + } + m.mux.Unlock() + if err := m.deleteAllStreamListeners(ctx, id); err != nil { + return err + } + if err := m.persistence.DeleteStream(ctx, id); err != nil { + return err + } + if s != nil { + return s.Delete(ctx) + } + return nil +} + +func (m *manager) reserveStreamName(ctx context.Context, name string, id *fftypes.UUID) (func(bool), error) { + m.mux.Lock() + defer m.mux.Unlock() + + oldName := "" + s := m.eventStreams[*id] + if s != nil { + oldName = *s.Spec().Name + } + existing := m.streamsByName[name] + if existing != nil { + if !existing.Equals(id) { + return nil, i18n.NewError(ctx, tmmsgs.MsgDuplicateStreamName, name, existing) + } + } + m.streamsByName[name] = id + + return func(succeeded bool) { + // Release the name on failure, but only if it wasn't existing + if !succeeded && (existing == nil) { + m.mux.Lock() + delete(m.streamsByName, name) + m.mux.Unlock() + } else if succeeded && oldName != name { + // Delete the old name on success + delete(m.streamsByName, oldName) + } + }, nil +} + +func (m *manager) createAndStoreNewStream(ctx context.Context, def *apitypes.EventStream) (*apitypes.EventStream, error) { + def.ID = apitypes.NewULID() + def.Created = nil // set to updated time by events.NewEventStream + if def.Name == nil || *def.Name == "" { + return nil, i18n.NewError(ctx, tmmsgs.MsgMissingName) + } + + stored := false + closeoutName, err := m.reserveStreamName(ctx, *def.Name, def.ID) + if err != nil { + return nil, err + } + defer func() { closeoutName(stored) }() + + s, err := m.addRuntimeStream(def, nil /* no listeners when a new stream is first created */) + if err != nil { + return nil, err + } + spec := s.Spec() + err = m.persistence.WriteStream(ctx, spec) + if err != nil { + m.mux.Lock() + delete(m.eventStreams, *def.ID) + m.mux.Unlock() + err1 := s.Delete(ctx) + log.L(ctx).Infof("Cleaned up runtime stream after write failed (err?=%v)", err1) + return nil, err + } + stored = true + if !*spec.Suspended { + return spec, s.Start(ctx) + } + return spec, nil +} + +func (m *manager) createAndStoreNewStreamListener(ctx context.Context, idStr string, def *apitypes.Listener) (*apitypes.Listener, error) { + streamID, err := fftypes.ParseUUID(ctx, idStr) + if err != nil { + return nil, err + } + def.StreamID = streamID + return m.createAndStoreNewListener(ctx, def) +} + +func (m *manager) createAndStoreNewListener(ctx context.Context, def *apitypes.Listener) (*apitypes.Listener, error) { + return m.createOrUpdateListener(ctx, apitypes.NewULID(), def, false) +} + +func (m *manager) updateExistingListener(ctx context.Context, streamIDStr, listenerIDStr string, updates *apitypes.Listener, reset bool) (*apitypes.Listener, error) { + l, err := m.getListener(ctx, streamIDStr, listenerIDStr) // Verify the listener exists in storage + if err != nil { + return nil, err + } + updates.StreamID = l.StreamID + return m.createOrUpdateListener(ctx, l.ID, updates, reset) +} + +func (m *manager) createOrUpdateListener(ctx context.Context, id *fftypes.UUID, newOrUpdates *apitypes.Listener, reset bool) (*apitypes.Listener, error) { + if err := mergeEthCompatMethods(ctx, newOrUpdates); err != nil { + return nil, err + } + var s events.Stream + if newOrUpdates.StreamID != nil { + m.mux.Lock() + s = m.eventStreams[*newOrUpdates.StreamID] + m.mux.Unlock() + } + if s == nil { + return nil, i18n.NewError(ctx, tmmsgs.MsgStreamNotFound, newOrUpdates.StreamID) + } + def, err := s.AddOrUpdateListener(ctx, id, newOrUpdates, reset) + if err != nil { + return nil, err + } + if err := m.persistence.WriteListener(ctx, def); err != nil { + err1 := s.RemoveListener(ctx, def.ID) + log.L(ctx).Infof("Cleaned up runtime listener after write failed (err?=%v)", err1) + return nil, err + } + return def, nil +} + +func (m *manager) deleteListener(ctx context.Context, streamIDStr, listenerIDStr string) error { + spec, err := m.getListener(ctx, streamIDStr, listenerIDStr) // Verify the listener exists in storage + if err != nil { + return err + } + m.mux.Lock() + s := m.eventStreams[*spec.StreamID] + m.mux.Unlock() + if s == nil { + return i18n.NewError(ctx, tmmsgs.MsgStreamNotFound, spec.StreamID) + } + if err := s.RemoveListener(ctx, spec.ID); err != nil { + return err + } + return m.persistence.DeleteListener(ctx, spec.ID) +} + +func (m *manager) updateStream(ctx context.Context, idStr string, updates *apitypes.EventStream) (*apitypes.EventStream, error) { + id, err := fftypes.ParseUUID(ctx, idStr) + if err != nil { + return nil, err + } + m.mux.Lock() + s := m.eventStreams[*id] + m.mux.Unlock() + if s == nil { + return nil, i18n.NewError(ctx, tmmsgs.MsgStreamNotFound, id) + } + + nameChanged := false + if updates.Name != nil && *updates.Name != "" { + closeoutName, err := m.reserveStreamName(ctx, *updates.Name, id) + if err != nil { + return nil, err + } + defer func() { closeoutName(nameChanged) }() + } + + err = s.UpdateSpec(ctx, updates) + if err != nil { + return nil, err + } + spec := s.Spec() + err = m.persistence.WriteStream(ctx, spec) + if err != nil { + return nil, err + } + nameChanged = true + + // We might need to start or stop + if *spec.Suspended && s.Status() != apitypes.EventStreamStatusStopped { + return nil, s.Stop(ctx) + } else if !*spec.Suspended && s.Status() != apitypes.EventStreamStatusStarted { + return nil, s.Start(ctx) + } + return spec, nil +} + +func (m *manager) getStream(ctx context.Context, idStr string) (*apitypes.EventStreamWithStatus, error) { + id, err := fftypes.ParseUUID(ctx, idStr) + if err != nil { + return nil, err + } + m.mux.Lock() + s := m.eventStreams[*id] + m.mux.Unlock() + if s == nil { + return nil, i18n.NewError(ctx, tmmsgs.MsgStreamNotFound, idStr) + } + return &apitypes.EventStreamWithStatus{ + EventStream: *s.Spec(), + Status: s.Status(), + }, nil +} + +func (m *manager) parseLimit(ctx context.Context, limitStr string) (limit int, err error) { + if limitStr != "" { + if limit, err = strconv.Atoi(limitStr); err != nil { + return -1, i18n.NewError(ctx, tmmsgs.MsgInvalidLimit, limitStr, err) + } + } + return limit, nil +} + +func (m *manager) parseAfterAndLimit(ctx context.Context, afterStr, limitStr string) (after *fftypes.UUID, limit int, err error) { + if limit, err = m.parseLimit(ctx, limitStr); err != nil { + return nil, -1, i18n.NewError(ctx, tmmsgs.MsgInvalidLimit, limitStr, err) + } + if afterStr != "" { + if after, err = fftypes.ParseUUID(ctx, afterStr); err != nil { + return nil, -1, err + } + } + return after, limit, nil +} + +func (m *manager) getStreams(ctx context.Context, afterStr, limitStr string) (streams []*apitypes.EventStream, err error) { + after, limit, err := m.parseAfterAndLimit(ctx, afterStr, limitStr) + if err != nil { + return nil, err + } + return m.persistence.ListStreams(ctx, after, limit, persistence.SortDirectionDescending) +} + +func (m *manager) getListener(ctx context.Context, streamIDStr, listenerIDStr string) (spec *apitypes.Listener, err error) { + var streamID *fftypes.UUID + if streamIDStr != "" { + streamID, err = fftypes.ParseUUID(ctx, streamIDStr) + if err != nil { + return nil, err + } + + } + listenerID, err := fftypes.ParseUUID(ctx, listenerIDStr) + if err != nil { + return nil, err + } + spec, err = m.persistence.GetListener(ctx, listenerID) + if err != nil { + return nil, err + } + // Check we found the listener, and it's owned by the correct stream ID (if we're on a path that specifies a stream ID) + if spec == nil || (streamID != nil && !streamID.Equals(spec.StreamID)) { + return nil, i18n.NewError(ctx, tmmsgs.MsgListenerNotFound, listenerID) + } + return spec, nil +} + +func (m *manager) getListeners(ctx context.Context, afterStr, limitStr string) (streams []*apitypes.Listener, err error) { + after, limit, err := m.parseAfterAndLimit(ctx, afterStr, limitStr) + if err != nil { + return nil, err + } + return m.persistence.ListListeners(ctx, after, limit, persistence.SortDirectionDescending) +} + +func (m *manager) getStreamListeners(ctx context.Context, afterStr, limitStr, idStr string) (streams []*apitypes.Listener, err error) { + after, limit, err := m.parseAfterAndLimit(ctx, afterStr, limitStr) + if err != nil { + return nil, err + } + id, err := fftypes.ParseUUID(ctx, idStr) + if err != nil { + return nil, err + } + return m.persistence.ListStreamListeners(ctx, after, limit, persistence.SortDirectionDescending, id) +} + +func mergeEthCompatMethods(ctx context.Context, listener *apitypes.Listener) error { + if listener.EthCompatMethods != nil { + if listener.Options == nil { + listener.Options = fftypes.JSONAnyPtr("{}") + } + var optionsMap map[string]interface{} + if err := listener.Options.Unmarshal(ctx, &optionsMap); err != nil { + return err + } + var methodList []interface{} + if err := listener.EthCompatMethods.Unmarshal(ctx, &methodList); err != nil { + return err + } + optionsMap["methods"] = methodList + b, _ := json.Marshal(optionsMap) + listener.Options = fftypes.JSONAnyPtrBytes(b) + listener.EthCompatMethods = nil + } + return nil +} diff --git a/pkg/fftm/stream_management_test.go b/pkg/fftm/stream_management_test.go new file mode 100644 index 00000000..8a60c80a --- /dev/null +++ b/pkg/fftm/stream_management_test.go @@ -0,0 +1,658 @@ +// Copyright © 2022 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fftm + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/internal/persistence" + "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" + "github.com/hyperledger/firefly-transaction-manager/mocks/persistencemocks" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestRestoreStreamsAndListenersOK(t *testing.T) { + + _, m, done := newTestManager(t) + defer done() + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + falsy := false + + es1 := &apitypes.EventStream{ID: apitypes.NewULID(), Name: strPtr("stream1"), Suspended: &falsy} + err := m.persistence.WriteStream(m.ctx, es1) + assert.NoError(t, err) + + e1l1 := &apitypes.Listener{ID: apitypes.NewULID(), Name: strPtr("listener1"), StreamID: es1.ID} + err = m.persistence.WriteListener(m.ctx, e1l1) + assert.NoError(t, err) + + e1l2 := &apitypes.Listener{ID: apitypes.NewULID(), Name: strPtr("listener2"), StreamID: es1.ID} + err = m.persistence.WriteListener(m.ctx, e1l2) + assert.NoError(t, err) + + es2 := &apitypes.EventStream{ID: apitypes.NewULID(), Name: strPtr("stream2"), Suspended: &falsy} + err = m.persistence.WriteStream(m.ctx, es2) + assert.NoError(t, err) + + e2l1 := &apitypes.Listener{ID: apitypes.NewULID(), Name: strPtr("listener3"), StreamID: es2.ID} + err = m.persistence.WriteListener(m.ctx, e2l1) + assert.NoError(t, err) + + err = m.Start() + assert.NoError(t, err) + + assert.Equal(t, es1.ID, m.streamsByName["stream1"]) + assert.Equal(t, es2.ID, m.streamsByName["stream2"]) + + mfc.AssertExpectations(t) + +} + +func TestRestoreStreamsReadFailed(t *testing.T) { + + _, m, close := newTestManagerMockPersistence(t) + defer close() + + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("ListStreams", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending).Return(nil, fmt.Errorf("pop")) + + err := m.restoreStreams() + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) +} + +func TestRestoreListenersReadFailed(t *testing.T) { + + _, m, close := newTestManagerMockPersistence(t) + defer close() + + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("ListStreams", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending).Return([]*apitypes.EventStream{ + {ID: fftypes.NewUUID()}, + }, nil) + mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), 0, persistence.SortDirectionAscending, mock.Anything).Return(nil, fmt.Errorf("pop")) + + err := m.restoreStreams() + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) +} + +func TestRestoreStreamsValidateFail(t *testing.T) { + + _, m, done := newTestManager(t) + defer done() + + falsy := false + es1 := &apitypes.EventStream{ID: apitypes.NewULID(), Name: strPtr(""), Suspended: &falsy} + err := m.persistence.WriteStream(m.ctx, es1) + assert.NoError(t, err) + + err = m.restoreStreams() + assert.Regexp(t, "FF21028", err) + +} + +func TestRestoreListenersStartFail(t *testing.T) { + + _, m, done := newTestManager(t) + defer done() + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + falsy := false + es1 := &apitypes.EventStream{ID: apitypes.NewULID(), Name: strPtr("stream1"), Suspended: &falsy} + err := m.persistence.WriteStream(m.ctx, es1) + assert.NoError(t, err) + + e1l1 := &apitypes.Listener{ID: apitypes.NewULID(), Name: strPtr("listener1"), StreamID: es1.ID} + err = m.persistence.WriteListener(m.ctx, e1l1) + assert.NoError(t, err) + + err = m.restoreStreams() + assert.Regexp(t, "pop", err) + + mfc.AssertExpectations(t) + +} + +func TestDeleteStartedListener(t *testing.T) { + + _, m, done := newTestManager(t) + defer done() + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventStreamStopped", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStoppedResponse{}, ffcapi.ErrorReason(""), nil).Maybe() + + falsy := false + es1 := &apitypes.EventStream{ID: apitypes.NewULID(), Name: strPtr("stream1"), Suspended: &falsy} + err := m.persistence.WriteStream(m.ctx, es1) + assert.NoError(t, err) + + e1l1 := &apitypes.Listener{ID: apitypes.NewULID(), Name: strPtr("listener1"), StreamID: es1.ID} + err = m.persistence.WriteListener(m.ctx, e1l1) + assert.NoError(t, err) + + err = m.Start() + assert.NoError(t, err) + + err = m.deleteStream(m.ctx, es1.ID.String()) + assert.NoError(t, err) + + mfc.AssertExpectations(t) + +} + +func TestDeleteStartedListenerFail(t *testing.T) { + + _, m, close := newTestManagerMockPersistence(t) + defer close() + + esID := apitypes.NewULID() + lID := apitypes.NewULID() + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending, esID).Return([]*apitypes.Listener{ + {ID: lID, StreamID: esID}, + }, nil) + mp.On("DeleteListener", m.ctx, lID).Return(fmt.Errorf("pop")) + + err := m.deleteAllStreamListeners(m.ctx, esID) + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) +} + +func TestDeleteStreamBadID(t *testing.T) { + + _, m, close := newTestManagerMockPersistence(t) + defer close() + + err := m.deleteStream(m.ctx, "Bad ID") + assert.Regexp(t, "FF00138", err) + +} + +func TestDeleteStreamListenerPersistenceFail(t *testing.T) { + + _, m, close := newTestManagerMockPersistence(t) + defer close() + + esID := apitypes.NewULID() + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending, esID).Return(nil, fmt.Errorf("pop")) + + err := m.deleteStream(m.ctx, esID.String()) + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) +} + +func TestDeleteStreamPersistenceFail(t *testing.T) { + + _, m, close := newTestManagerMockPersistence(t) + defer close() + + esID := apitypes.NewULID() + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending, esID).Return([]*apitypes.Listener{}, nil) + mp.On("DeleteStream", m.ctx, esID).Return(fmt.Errorf("pop")) + + err := m.deleteStream(m.ctx, esID.String()) + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) +} + +func TestDeleteStreamNotInitialized(t *testing.T) { + + _, m, close := newTestManagerMockPersistence(t) + defer close() + + esID := apitypes.NewULID() + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("ListStreamListeners", m.ctx, (*fftypes.UUID)(nil), startupPaginationLimit, persistence.SortDirectionAscending, esID).Return([]*apitypes.Listener{}, nil) + mp.On("DeleteStream", m.ctx, esID).Return(nil) + + err := m.deleteStream(m.ctx, esID.String()) + assert.NoError(t, err) + + mp.AssertExpectations(t) +} + +func TestCreateRenameStreamNameReservation(t *testing.T) { + + _, m, close := newTestManagerMockPersistence(t) + defer close() + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("WriteStream", m.ctx, mock.Anything).Return(fmt.Errorf("temporary")).Once() + mp.On("DeleteCheckpoint", m.ctx, mock.Anything).Return(fmt.Errorf("temporary")).Once() + mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) + mp.On("GetCheckpoint", m.ctx, mock.Anything).Return(nil, nil) + + // Reject missing name + _, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{}) + assert.Regexp(t, "FF21028", err) + + // Attempt to start and encounter a temporary error + _, err = m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("Name1")}) + assert.Regexp(t, "temporary", err) + + // Ensure we still allow use of the name after the glitch is fixed + es1, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("Name1")}) + assert.NoError(t, err) + + // Ensure we can't create another stream of same name + _, err = m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("Name1")}) + assert.Regexp(t, "FF21047", err) + + // Create a second stream to test clash on rename + es2, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("Name2")}) + assert.NoError(t, err) + + // Check for clash + _, err = m.updateStream(m.ctx, es1.ID.String(), &apitypes.EventStream{Name: strPtr("Name2")}) + assert.Regexp(t, "FF21047", err) + + // Check for no-op rename to self + _, err = m.updateStream(m.ctx, es2.ID.String(), &apitypes.EventStream{Name: strPtr("Name2")}) + assert.NoError(t, err) + + mp.AssertExpectations(t) +} + +func TestCreateStreamValidateFail(t *testing.T) { + + _, m, close := newTestManagerMockPersistence(t) + defer close() + + wrongType := apitypes.DistributionMode("wrong") + _, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1"), Type: &wrongType}) + assert.Regexp(t, "FF21029", err) + +} + +func TestCreateAndStoreNewStreamListenerBadID(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + _, err := m.createAndStoreNewStreamListener(m.ctx, "bad", nil) + assert.Regexp(t, "FF00138", err) +} + +func TestUpdateExistingListenerNotFound(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("GetListener", m.ctx, mock.Anything).Return(nil, nil) + + _, err := m.updateExistingListener(m.ctx, apitypes.NewULID().String(), apitypes.NewULID().String(), &apitypes.Listener{}, false) + assert.Regexp(t, "FF21046", err) + + mp.AssertExpectations(t) +} + +func TestCreateOrUpdateListenerNotFound(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + _, err := m.createOrUpdateListener(m.ctx, apitypes.NewULID(), &apitypes.Listener{StreamID: apitypes.NewULID()}, false) + assert.Regexp(t, "FF21045", err) + +} + +func TestCreateOrUpdateListenerFail(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) + mp.On("GetCheckpoint", m.ctx, mock.Anything).Return(nil, nil) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) + + _, err = m.createOrUpdateListener(m.ctx, apitypes.NewULID(), &apitypes.Listener{StreamID: es.ID}, false) + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) +} + +func TestCreateOrUpdateListenerFailMergeEthCompatMethods(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) + mp.On("GetCheckpoint", m.ctx, mock.Anything).Return(nil, nil) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) + + l := &apitypes.Listener{ + StreamID: es.ID, + EthCompatMethods: fftypes.JSONAnyPtr(`{}`), + } + + _, err = m.createOrUpdateListener(m.ctx, apitypes.NewULID(), l, false) + assert.Error(t, err) + + mp.AssertExpectations(t) +} + +func TestCreateOrUpdateListenerWriteFail(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) + mp.On("WriteListener", m.ctx, mock.Anything).Return(fmt.Errorf("pop")) + mp.On("GetCheckpoint", m.ctx, mock.Anything).Return(nil, nil) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerRemoveResponse{}, ffcapi.ErrorReason(""), nil) + + es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) + + _, err = m.createOrUpdateListener(m.ctx, apitypes.NewULID(), &apitypes.Listener{StreamID: es.ID}, false) + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) +} + +func TestDeleteListenerBadID(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + err := m.deleteListener(m.ctx, "bad ID", "bad ID") + assert.Regexp(t, "FF00138", err) + +} + +func TestDeleteListenerStreamNotFound(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + l1 := &apitypes.Listener{ID: apitypes.NewULID(), StreamID: apitypes.NewULID()} + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("GetListener", m.ctx, mock.Anything).Return(l1, nil) + + err := m.deleteListener(m.ctx, l1.StreamID.String(), l1.ID.String()) + assert.Regexp(t, "FF21045", err) + + mp.AssertExpectations(t) + +} + +func TestDeleteListenerFail(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) + mp.On("WriteListener", m.ctx, mock.Anything).Return(nil) + mp.On("GetCheckpoint", m.ctx, mock.Anything).Return(nil, nil) + + mfc := m.connector.(*ffcapimocks.API) + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerVerifyOptions", mock.Anything, mock.Anything).Return(&ffcapi.EventListenerVerifyOptionsResponse{}, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerAdd", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), nil) + mfc.On("EventListenerRemove", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) + + es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) + + l1, err := m.createOrUpdateListener(m.ctx, apitypes.NewULID(), &apitypes.Listener{StreamID: es.ID}, false) + assert.NoError(t, err) + + mp.On("GetListener", m.ctx, mock.Anything).Return(l1, nil) + + err = m.deleteListener(m.ctx, l1.StreamID.String(), l1.ID.String()) + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) + +} + +func TestUpdateStreamBadID(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + _, err := m.updateStream(m.ctx, "bad ID", &apitypes.EventStream{}) + assert.Regexp(t, "FF00138", err) + +} + +func TestUpdateStreamNotFound(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + _, err := m.updateStream(m.ctx, apitypes.NewULID().String(), &apitypes.EventStream{}) + assert.Regexp(t, "FF21045", err) + +} + +func TestUpdateStreamBadChanges(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + mfc := m.connector.(*ffcapimocks.API) + mp := m.persistence.(*persistencemocks.Persistence) + + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + + mp.On("WriteStream", m.ctx, mock.Anything).Return(nil) + mp.On("GetCheckpoint", m.ctx, mock.Anything).Return(nil, nil) + + es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) + + wrongType := apitypes.DistributionMode("wrong") + _, err = m.updateStream(m.ctx, es.ID.String(), &apitypes.EventStream{Type: &wrongType}) + assert.Regexp(t, "FF21029", err) + +} + +func TestUpdateStreamWriteFail(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + mfc := m.connector.(*ffcapimocks.API) + mp := m.persistence.(*persistencemocks.Persistence) + + mfc.On("EventStreamStart", mock.Anything, mock.Anything).Return(&ffcapi.EventStreamStartResponse{}, ffcapi.ErrorReason(""), nil) + mp.On("WriteStream", m.ctx, mock.Anything).Return(nil).Once() + mp.On("WriteStream", m.ctx, mock.Anything).Return(fmt.Errorf("pop")) + mp.On("GetCheckpoint", m.ctx, mock.Anything).Return(nil, nil) + + es, err := m.createAndStoreNewStream(m.ctx, &apitypes.EventStream{Name: strPtr("stream1")}) + + _, err = m.updateStream(m.ctx, es.ID.String(), &apitypes.EventStream{}) + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) + +} + +func TestGetStreamBadID(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + _, err := m.getStream(m.ctx, "bad ID") + assert.Regexp(t, "FF00138", err) + +} + +func TestGetStreamNotFound(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + _, err := m.getStream(m.ctx, apitypes.NewULID().String()) + assert.Regexp(t, "FF21045", err) + +} + +func TestGetStreamsBadLimit(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + _, err := m.getStreams(m.ctx, "", "wrong") + assert.Regexp(t, "FF21044", err) + +} + +func TestGetListenerBadAfter(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + _, err := m.getListeners(m.ctx, "!bad UUID", "") + assert.Regexp(t, "FF00138", err) + +} + +func TestGetListenerBadStreamID(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + _, err := m.getListener(m.ctx, "bad ID", apitypes.NewULID().String()) + assert.Regexp(t, "FF00138", err) + +} + +func TestGetListenerBadListenerID(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + _, err := m.getListener(m.ctx, apitypes.NewULID().String(), "bad ID") + assert.Regexp(t, "FF00138", err) + +} + +func TestGetListenerLookupErr(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("GetListener", m.ctx, mock.Anything).Return(nil, fmt.Errorf("pop")) + + _, err := m.getListener(m.ctx, apitypes.NewULID().String(), apitypes.NewULID().String()) + assert.Regexp(t, "pop", err) + + mp.AssertExpectations(t) + +} + +func TestGetListenerNotFound(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("GetListener", m.ctx, mock.Anything).Return(nil, nil) + + _, err := m.getListener(m.ctx, apitypes.NewULID().String(), apitypes.NewULID().String()) + assert.Regexp(t, "FF21046", err) + + mp.AssertExpectations(t) + +} + +func TestGetStreamListenersBadLimit(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + _, err := m.getStreamListeners(m.ctx, "", "!bad limit", apitypes.NewULID().String()) + assert.Regexp(t, "FF21044", err) + +} + +func TestGetStreamListenersBadStreamID(t *testing.T) { + _, m, close := newTestManagerMockPersistence(t) + defer close() + + _, err := m.getStreamListeners(m.ctx, "", "", "bad ID") + assert.Regexp(t, "FF00138", err) + +} + +func TestMergeEthCompatMethods(t *testing.T) { + l := &apitypes.Listener{ + EthCompatMethods: fftypes.JSONAnyPtr(`[{"method1": "awesomeMethod"}]`), + Options: fftypes.JSONAnyPtr(`{"otherOption": "otherValue"}`), + } + err := mergeEthCompatMethods(context.Background(), l) + assert.NoError(t, err) + b, err := json.Marshal(l.Options) + assert.NoError(t, err) + assert.JSONEq(t, `{"methods": [{"method1":"awesomeMethod"}], "otherOption":"otherValue"}`, string(b)) + assert.Nil(t, l.EthCompatMethods) + + l = &apitypes.Listener{ + EthCompatMethods: fftypes.JSONAnyPtr(`[{"method1": "awesomeMethod"}]`), + Options: nil, + } + err = mergeEthCompatMethods(context.Background(), l) + assert.NoError(t, err) + b, err = json.Marshal(l.Options) + assert.NoError(t, err) + assert.JSONEq(t, `{"methods": [{"method1":"awesomeMethod"}]}`, string(b)) + assert.Nil(t, l.EthCompatMethods) +} + +func TestMergeEthCompatMethodsFail(t *testing.T) { + l := &apitypes.Listener{ + EthCompatMethods: fftypes.JSONAnyPtr(`[{"method1": "awesomeMethod"}`), + Options: fftypes.JSONAnyPtr(`{"otherOption": "otherValue"}`), + } + err := mergeEthCompatMethods(context.Background(), l) + assert.Error(t, err) + + l = &apitypes.Listener{ + EthCompatMethods: fftypes.JSONAnyPtr(`[{"method1": "awesomeMethod"}]`), + Options: fftypes.JSONAnyPtr(`{"otherOption": "otherValue"`), + } + err = mergeEthCompatMethods(context.Background(), l) + assert.Error(t, err) +} diff --git a/pkg/fftm/transaction_management.go b/pkg/fftm/transaction_management.go new file mode 100644 index 00000000..fa897318 --- /dev/null +++ b/pkg/fftm/transaction_management.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 fftm + +import ( + "context" + "strings" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-transaction-manager/internal/persistence" + "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" +) + +func (m *manager) getTransactionByID(ctx context.Context, txID string) (transaction *apitypes.ManagedTX, err error) { + return m.persistence.GetTransactionByID(ctx, txID) +} + +func (m *manager) getTransactions(ctx context.Context, afterStr, limitStr, signer string, pending bool, dirString string) (transactions []*apitypes.ManagedTX, err error) { + limit, err := m.parseLimit(ctx, limitStr) + if err != nil { + return nil, err + } + var dir persistence.SortDirection + switch strings.ToLower(dirString) { + case "", "desc", "descending": + dir = persistence.SortDirectionDescending // descending is default + case "asc", "ascending": + dir = persistence.SortDirectionAscending + default: + return nil, i18n.NewError(ctx, tmmsgs.MsgInvalidSortDirection, dirString) + } + var afterTx *apitypes.ManagedTX + if afterStr != "" { + // Get the transaction, as we need this to exist to pick the right field depending on the index that's been chosen + afterTx, err = m.persistence.GetTransactionByID(ctx, afterStr) + if err != nil { + return nil, err + } + if afterTx == nil { + return nil, i18n.NewError(ctx, tmmsgs.MsgPaginationErrTxNotFound, afterStr) + } + } + switch { + case signer != "" && pending: + return nil, i18n.NewError(ctx, tmmsgs.MsgTXConflictSignerPending) + case signer != "": + var afterNonce *fftypes.FFBigInt + if afterTx != nil { + afterNonce = afterTx.Nonce + } + return m.persistence.ListTransactionsByNonce(ctx, signer, afterNonce, limit, dir) + case pending: + var afterSequence *fftypes.UUID + if afterTx != nil { + afterSequence = afterTx.SequenceID + } + return m.persistence.ListTransactionsPending(ctx, afterSequence, limit, dir) + default: + return m.persistence.ListTransactionsByCreateTime(ctx, afterTx, limit, dir) + } + +} diff --git a/pkg/fftm/transaction_management_test.go b/pkg/fftm/transaction_management_test.go new file mode 100644 index 00000000..f3b7e952 --- /dev/null +++ b/pkg/fftm/transaction_management_test.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 fftm + +import ( + "fmt" + "testing" + + "github.com/hyperledger/firefly-transaction-manager/mocks/persistencemocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetTransactionsErrors(t *testing.T) { + + _, m, close := newTestManagerMockPersistence(t) + defer close() + + mp := m.persistence.(*persistencemocks.Persistence) + mp.On("GetTransactionByID", m.ctx, mock.Anything).Return(nil, fmt.Errorf("pop")).Once() + mp.On("GetTransactionByID", m.ctx, mock.Anything).Return(nil, nil).Once() + mp.On("Close", mock.Anything).Return(nil).Maybe() + + _, err := m.getTransactions(m.ctx, "", "bad limit", "", false, "") + assert.Regexp(t, "FF21044", err) + + _, err = m.getTransactions(m.ctx, "", "", "", false, "wrong") + assert.Regexp(t, "FF21064", err) + + _, err = m.getTransactions(m.ctx, "", "", "cannot be specified with pending", true, "") + assert.Regexp(t, "FF21063", err) + + _, err = m.getTransactions(m.ctx, "after-causes-failure", "", "", false, "") + assert.Regexp(t, "pop", err) + + _, err = m.getTransactions(m.ctx, "after-not-found", "", "", false, "") + assert.Regexp(t, "FF21062", err) + +} diff --git a/pkg/policyengine/policyengine.go b/pkg/policyengine/policyengine.go index e5fa467e..96478358 100644 --- a/pkg/policyengine/policyengine.go +++ b/pkg/policyengine/policyengine.go @@ -19,10 +19,10 @@ package policyengine import ( "context" - "github.com/hyperledger/firefly-common/pkg/ffcapi" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) type PolicyEngine interface { - Execute(ctx context.Context, cAPI ffcapi.API, mtx *fftm.ManagedTXOutput) (updated bool, reason ffcapi.ErrorReason, err error) + Execute(ctx context.Context, cAPI ffcapi.API, mtx *apitypes.ManagedTX) (updated bool, reason ffcapi.ErrorReason, err error) } diff --git a/internal/policyengines/registry.go b/pkg/policyengines/registry.go similarity index 70% rename from internal/policyengines/registry.go rename to pkg/policyengines/registry.go index 8c0656f2..f375f9e2 100644 --- a/internal/policyengines/registry.go +++ b/pkg/policyengines/registry.go @@ -21,29 +21,30 @@ import ( "github.com/hyperledger/firefly-common/pkg/config" "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" ) var policyEngines = make(map[string]Factory) -func NewPolicyEngine(ctx context.Context, basePrefix config.Prefix, name string) (policyengine.PolicyEngine, error) { +func NewPolicyEngine(ctx context.Context, baseConfig config.Section, name string) (policyengine.PolicyEngine, error) { factory, ok := policyEngines[name] if !ok { return nil, i18n.NewError(ctx, tmmsgs.MsgPolicyEngineNotRegistered, name) } - return factory.NewPolicyEngine(ctx, basePrefix.SubPrefix(name)) + return factory.NewPolicyEngine(ctx, baseConfig.SubSection(name)) } type Factory interface { Name() string - InitPrefix(prefix config.Prefix) - NewPolicyEngine(ctx context.Context, prefix config.Prefix) (policyengine.PolicyEngine, error) + InitConfig(conf config.Section) + NewPolicyEngine(ctx context.Context, conf config.Section) (policyengine.PolicyEngine, error) } -func RegisterEngine(basePrefix config.Prefix, factory Factory) string { +func RegisterEngine(factory Factory) string { name := factory.Name() policyEngines[name] = factory - factory.InitPrefix(basePrefix.SubPrefix(name)) + factory.InitConfig(tmconfig.PolicyEngineBaseConfig.SubSection(name)) return name } diff --git a/internal/policyengines/registry_test.go b/pkg/policyengines/registry_test.go similarity index 78% rename from internal/policyengines/registry_test.go rename to pkg/policyengines/registry_test.go index 567103d1..e50d82d2 100644 --- a/internal/policyengines/registry_test.go +++ b/pkg/policyengines/registry_test.go @@ -20,22 +20,22 @@ import ( "context" "testing" - "github.com/hyperledger/firefly-transaction-manager/internal/policyengines/simple" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" + "github.com/hyperledger/firefly-transaction-manager/pkg/policyengines/simple" "github.com/stretchr/testify/assert" ) func TestRegistry(t *testing.T) { tmconfig.Reset() - RegisterEngine(tmconfig.PolicyEngineBasePrefix, &simple.PolicyEngineFactory{}) + RegisterEngine(&simple.PolicyEngineFactory{}) - tmconfig.PolicyEngineBasePrefix.SubPrefix("simple").Set(simple.FixedGasPrice, "12345") - p, err := NewPolicyEngine(context.Background(), tmconfig.PolicyEngineBasePrefix, "simple") + tmconfig.PolicyEngineBaseConfig.SubSection("simple").Set(simple.FixedGasPrice, "12345") + p, err := NewPolicyEngine(context.Background(), tmconfig.PolicyEngineBaseConfig, "simple") assert.NotNil(t, p) assert.NoError(t, err) - p, err = NewPolicyEngine(context.Background(), tmconfig.PolicyEngineBasePrefix, "bob") + p, err = NewPolicyEngine(context.Background(), tmconfig.PolicyEngineBaseConfig, "bob") assert.Nil(t, p) assert.Regexp(t, "FF21019", err) diff --git a/internal/policyengines/simple/config.go b/pkg/policyengines/simple/config.go similarity index 54% rename from internal/policyengines/simple/config.go rename to pkg/policyengines/simple/config.go index 4dcfcbd9..4e3d2fc0 100644 --- a/internal/policyengines/simple/config.go +++ b/pkg/policyengines/simple/config.go @@ -24,9 +24,9 @@ import ( ) const ( - FixedGasPrice = "fixedGasPrice" // when not using a gas station - will be treated as a raw JSON string, so can be numeric 123, or string "123", or object {"maxPriorityFeePerGas":123}) - WarnInterval = "warnInterval" // warnings will be written to the log at this interval if mining has not occurred - GasOraclePrefix = "gasOracle" + FixedGasPrice = "fixedGasPrice" // when not using a gas station - will be treated as a raw JSON string, so can be numeric 123, or string "123", or object {"maxPriorityFeePerGas":123}) + ResubmitInterval = "resubmitInterval" // warnings will be written to the log at this interval if mining has not occurred, and the TX will be resubmitted + GasOracleConfig = "gasOracle" GasOracleMode = "mode" GasOracleMethod = "method" GasOracleTemplate = "template" @@ -40,21 +40,21 @@ const ( ) const ( - defaultWarnInterval = "15m" + defaultResubmitInterval = "5m" defaultGasOracleQueryInterval = "5m" defaultGasOracleMethod = http.MethodGet - defaultGasOracleMode = GasOracleModeDisabled + defaultGasOracleMode = GasOracleModeConnector ) -func (f *PolicyEngineFactory) InitPrefix(prefix config.Prefix) { - prefix.AddKnownKey(FixedGasPrice) - prefix.AddKnownKey(WarnInterval, defaultWarnInterval) +func (f *PolicyEngineFactory) InitConfig(conf config.Section) { + conf.AddKnownKey(FixedGasPrice) + conf.AddKnownKey(ResubmitInterval, defaultResubmitInterval) - gasOraclePrefix := prefix.SubPrefix(GasOraclePrefix) - ffresty.InitPrefix(gasOraclePrefix) - gasOraclePrefix.AddKnownKey(GasOracleMethod, defaultGasOracleMethod) - gasOraclePrefix.AddKnownKey(GasOracleMode, defaultGasOracleMode) - gasOraclePrefix.AddKnownKey(GasOracleQueryInterval, defaultGasOracleQueryInterval) - gasOraclePrefix.AddKnownKey(GasOracleTemplate) + gasOracleConfig := conf.SubSection(GasOracleConfig) + ffresty.InitConfig(gasOracleConfig) + gasOracleConfig.AddKnownKey(GasOracleMethod, defaultGasOracleMethod) + gasOracleConfig.AddKnownKey(GasOracleMode, defaultGasOracleMode) + gasOracleConfig.AddKnownKey(GasOracleQueryInterval, defaultGasOracleQueryInterval) + gasOracleConfig.AddKnownKey(GasOracleTemplate) } diff --git a/internal/policyengines/simple/simple_policy_engine.go b/pkg/policyengines/simple/simple_policy_engine.go similarity index 61% rename from internal/policyengines/simple/simple_policy_engine.go rename to pkg/policyengines/simple/simple_policy_engine.go index b05a86e9..3bd09583 100644 --- a/internal/policyengines/simple/simple_policy_engine.go +++ b/pkg/policyengines/simple/simple_policy_engine.go @@ -25,13 +25,13 @@ import ( "github.com/go-resty/resty/v2" "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/fftypes" "github.com/hyperledger/firefly-common/pkg/i18n" "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-transaction-manager/internal/tmmsgs" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/hyperledger/firefly-transaction-manager/pkg/policyengine" ) @@ -45,22 +45,22 @@ func (f *PolicyEngineFactory) Name() string { // - It uses a public gas estimation // - It submits the transaction once // - It logs errors transactions breach certain configured thresholds of staleness -func (f *PolicyEngineFactory) NewPolicyEngine(ctx context.Context, prefix config.Prefix) (pe policyengine.PolicyEngine, err error) { - gasOraclePrefix := prefix.SubPrefix(GasOraclePrefix) +func (f *PolicyEngineFactory) NewPolicyEngine(ctx context.Context, conf config.Section) (pe policyengine.PolicyEngine, err error) { + gasOracleConfig := conf.SubSection(GasOracleConfig) p := &simplePolicyEngine{ - warnInterval: prefix.GetDuration(WarnInterval), - fixedGasPrice: fftypes.JSONAnyPtr(prefix.GetString(FixedGasPrice)), + resubmitInterval: conf.GetDuration(ResubmitInterval), + fixedGasPrice: fftypes.JSONAnyPtr(conf.GetString(FixedGasPrice)), - gasOracleMethod: gasOraclePrefix.GetString(GasOracleMethod), - gasOracleQueryInterval: gasOraclePrefix.GetDuration(GasOracleQueryInterval), - gasOracleMode: gasOraclePrefix.GetString(GasOracleMode), + gasOracleMethod: gasOracleConfig.GetString(GasOracleMethod), + gasOracleQueryInterval: gasOracleConfig.GetDuration(GasOracleQueryInterval), + gasOracleMode: gasOracleConfig.GetString(GasOracleMode), } switch p.gasOracleMode { case GasOracleModeConnector: // No initialization required case GasOracleModeRESTAPI: - p.gasOracleClient = ffresty.New(ctx, gasOraclePrefix) - templateString := gasOraclePrefix.GetString(GasOracleTemplate) + p.gasOracleClient = ffresty.New(ctx, gasOracleConfig) + templateString := gasOracleConfig.GetString(GasOracleTemplate) if templateString == "" { return nil, i18n.NewError(ctx, tmmsgs.MsgMissingGOTemplate) } @@ -77,8 +77,8 @@ func (f *PolicyEngineFactory) NewPolicyEngine(ctx context.Context, prefix config } type simplePolicyEngine struct { - fixedGasPrice *fftypes.JSONAny - warnInterval time.Duration + fixedGasPrice *fftypes.JSONAny + resubmitInterval time.Duration gasOracleMode string gasOracleClient *resty.Client @@ -94,7 +94,7 @@ type simplePolicyInfo struct { } // withPolicyInfo is a convenience helper to run some logic that accesses/updates our policy section -func (p *simplePolicyEngine) withPolicyInfo(ctx context.Context, mtx *fftm.ManagedTXOutput, fn func(info *simplePolicyInfo) (updated bool, reason ffcapi.ErrorReason, err error)) (updated bool, reason ffcapi.ErrorReason, err error) { +func (p *simplePolicyEngine) withPolicyInfo(ctx context.Context, mtx *apitypes.ManagedTX, fn func(info *simplePolicyInfo) (updated bool, reason ffcapi.ErrorReason, err error)) (updated bool, reason ffcapi.ErrorReason, err error) { var info simplePolicyInfo infoBytes := []byte(mtx.PolicyInfo.String()) if len(infoBytes) > 0 { @@ -111,31 +111,54 @@ func (p *simplePolicyEngine) withPolicyInfo(ctx context.Context, mtx *fftm.Manag return updated, reason, err } -func (p *simplePolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx *fftm.ManagedTXOutput) (updated bool, reason ffcapi.ErrorReason, err error) { +func (p *simplePolicyEngine) submitTX(ctx context.Context, cAPI ffcapi.API, mtx *apitypes.ManagedTX) (reason ffcapi.ErrorReason, err error) { + sendTX := &ffcapi.TransactionSendRequest{ + TransactionHeaders: mtx.TransactionHeaders, + GasPrice: mtx.GasPrice, + TransactionData: mtx.TransactionData, + } + sendTX.TransactionHeaders.Nonce = (*fftypes.FFBigInt)(mtx.Nonce.Int()) + sendTX.TransactionHeaders.Gas = (*fftypes.FFBigInt)(mtx.Gas.Int()) + log.L(ctx).Debugf("Sending transaction %s at nonce %s / %d (lastSubmit=%s)", mtx.ID, mtx.TransactionHeaders.From, mtx.Nonce.Int64(), mtx.LastSubmit) + res, reason, err := cAPI.TransactionSend(ctx, sendTX) + if err == nil { + mtx.TransactionHash = res.TransactionHash + mtx.LastSubmit = fftypes.Now() + } else { + // We have some simple rules for handling reasons from the connector, which could be enhanced by extending the connector. + switch reason { + case ffcapi.ErrorKnownTransaction, ffcapi.ErrorReasonNonceTooLow: + // If we already have a transaction hash, this is fine - we just return as if we submitted it + if mtx.TransactionHash != "" { + log.L(ctx).Debugf("Transaction %s at nonce %s / %d known with hash: %s (%s)", mtx.ID, mtx.TransactionHeaders.From, mtx.Nonce.Int64(), mtx.TransactionHash, err) + return "", nil + } + // TODO: to cover the edge case where we had a timeout or other failure during the initial TransactionSend, we need to + // be able to re-calculate the hash that we would expect for the transaction. + // This would require a new FFCAPI API to calculate that hash, which requires the connector to perform the signing + // without submission to the node. For example using `eth_signTransaction` for EVM JSON/RPC. + return reason, err + default: + return reason, err + } + } + log.L(ctx).Infof("Transaction %s at nonce %s / %d submitted. Hash: %s", mtx.ID, mtx.TransactionHeaders.From, mtx.Nonce.Int64(), mtx.TransactionHash) + return "", nil +} + +func (p *simplePolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx *apitypes.ManagedTX) (updated bool, reason ffcapi.ErrorReason, err error) { // Simple policy engine only submits once. if mtx.FirstSubmit == nil { - + // Only calculate gas price here in the simple policy engine mtx.GasPrice, err = p.getGasPrice(ctx, cAPI) if err != nil { return false, "", err } - sendTX := &ffcapi.SendTransactionRequest{ - TransactionHeaders: mtx.Request.TransactionHeaders, - GasPrice: mtx.GasPrice, - TransactionData: mtx.TransactionData, + // Submit the first time + if reason, err := p.submitTX(ctx, cAPI, mtx); err != nil { + return true, reason, err } - sendTX.TransactionHeaders.Nonce = (*fftypes.FFBigInt)(mtx.Nonce.Int()) - sendTX.TransactionHeaders.Gas = (*fftypes.FFBigInt)(mtx.Gas.Int()) - log.L(ctx).Infof("Sending transaction: %+v", sendTX) - res, reason, err := cAPI.SendTransaction(ctx, sendTX) - if err != nil { - // A more sophisticated policy engine would consider the reason here, and potentially adjust the transaction for future attempts - return false, reason, err - } - log.L(ctx).Infof("Transaction hash=%s", res.TransactionHash) - mtx.TransactionHash = res.TransactionHash - mtx.FirstSubmit = fftypes.Now() - mtx.LastSubmit = mtx.FirstSubmit + mtx.FirstSubmit = mtx.LastSubmit return true, "", nil } else if mtx.Receipt == nil { @@ -149,10 +172,16 @@ func (p *simplePolicyEngine) Execute(ctx context.Context, cAPI ffcapi.API, mtx * lastWarnTime = mtx.FirstSubmit } now := fftypes.Now() - if now.Time().Sub(*lastWarnTime.Time()) > p.warnInterval { + if now.Time().Sub(*lastWarnTime.Time()) > p.resubmitInterval { secsSinceSubmit := float64(now.Time().Sub(*mtx.FirstSubmit.Time())) / float64(time.Second) - log.L(ctx).Warnf("Transaction %s (op=%s) has not been mined after %.2fs", mtx.TransactionHash, mtx.ID, secsSinceSubmit) + log.L(ctx).Infof("Transaction %s at nonce %s / %d has not been mined after %.2fs", mtx.ID, mtx.TransactionHeaders.From, mtx.Nonce.Int64(), secsSinceSubmit) info.LastWarnTime = now + // We do a resubmit at this point - as it might no longer be in the TX pool + if reason, err := p.submitTX(ctx, cAPI, mtx); err != nil { + if reason != ffcapi.ErrorKnownTransaction { + return true, reason, err + } + } return true, "", nil } return false, "", nil @@ -181,7 +210,7 @@ func (p *simplePolicyEngine) getGasPrice(ctx context.Context, cAPI ffcapi.API) ( return p.gasOracleQueryValue, nil case GasOracleModeConnector: // Call the connector - res, _, err := cAPI.GetGasPrice(ctx, &ffcapi.GetGasPriceRequest{}) + res, _, err := cAPI.GasPriceEstimate(ctx, &ffcapi.GasPriceEstimateRequest{}) if err != nil { return nil, err } @@ -189,7 +218,7 @@ func (p *simplePolicyEngine) getGasPrice(ctx context.Context, cAPI ffcapi.API) ( p.gasOracleLastQueryTime = fftypes.Now() return p.gasOracleQueryValue, nil default: - // Disabled - rust a fixed value + // Disabled - just a fixed value return p.fixedGasPrice, nil } } diff --git a/internal/policyengines/simple/simple_policy_engine_test.go b/pkg/policyengines/simple/simple_policy_engine_test.go similarity index 55% rename from internal/policyengines/simple/simple_policy_engine_test.go rename to pkg/policyengines/simple/simple_policy_engine_test.go index 5313e041..6e1135f8 100644 --- a/internal/policyengines/simple/simple_policy_engine_test.go +++ b/pkg/policyengines/simple/simple_policy_engine_test.go @@ -25,61 +25,57 @@ import ( "time" "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/fftypes" "github.com/hyperledger/firefly-transaction-manager/internal/tmconfig" "github.com/hyperledger/firefly-transaction-manager/mocks/ffcapimocks" - "github.com/hyperledger/firefly-transaction-manager/pkg/fftm" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) -func newTestPolicyEngineFactory(t *testing.T) (*PolicyEngineFactory, config.Prefix) { +func newTestPolicyEngineFactory(t *testing.T) (*PolicyEngineFactory, config.Section) { tmconfig.Reset() - prefix := config.NewPluginConfig("unittest.simple") + conf := config.RootSection("unittest.simple") f := &PolicyEngineFactory{} - f.InitPrefix(prefix) + f.InitConfig(conf) assert.Equal(t, "simple", f.Name()) - return f, prefix + return f, conf } func TestMissingGasConfig(t *testing.T) { - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeDisabled) - _, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeDisabled) + _, err := f.NewPolicyEngine(context.Background(), conf) assert.Regexp(t, "FF21020", err) } func TestFixedGasPriceOK(t *testing.T) { - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeDisabled) - prefix.Set(FixedGasPrice, `{ + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeDisabled) + conf.Set(FixedGasPrice, `{ "maxPriorityFee":32.146027800733336, "maxFee":32.14602781673334 }`) - p, err := f.NewPolicyEngine(context.Background(), prefix) + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + mtx := &apitypes.ManagedTX{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionHash: "0x12345", TransactionData: "SOME_RAW_TX_BYTES", } mockFFCAPI := &ffcapimocks.API{} - mockFFCAPI.On("SendTransaction", mock.Anything, mock.MatchedBy(func(req *ffcapi.SendTransactionRequest) bool { + mockFFCAPI.On("TransactionSend", mock.Anything, mock.MatchedBy(func(req *ffcapi.TransactionSendRequest) bool { return req.GasPrice.JSONObject().GetString("maxPriorityFee") == "32.146027800733336" && req.GasPrice.JSONObject().GetString("maxFee") == "32.14602781673334" && req.From == "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712" && req.TransactionData == "SOME_RAW_TX_BYTES" - })).Return(&ffcapi.SendTransactionResponse{ + })).Return(&ffcapi.TransactionSendResponse{ TransactionHash: "0x12345", }, ffcapi.ErrorReason(""), nil) @@ -122,32 +118,28 @@ func TestGasOracleSendOK(t *testing.T) { }`)) })) - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeRESTAPI) - prefix.SubPrefix(GasOraclePrefix).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleTemplate, `{"unit":"gwei","value":{{ .standard.maxPriorityFee }}}`) - p, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeRESTAPI) + conf.SubSection(GasOracleConfig).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) + conf.SubSection(GasOracleConfig).Set(GasOracleTemplate, `{"unit":"gwei","value":{{ .standard.maxPriorityFee }}}`) + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + mtx := &apitypes.ManagedTX{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionHash: "0x12345", TransactionData: "SOME_RAW_TX_BYTES", } mockFFCAPI := &ffcapimocks.API{} - mockFFCAPI.On("SendTransaction", mock.Anything, mock.MatchedBy(func(req *ffcapi.SendTransactionRequest) bool { + mockFFCAPI.On("TransactionSend", mock.Anything, mock.MatchedBy(func(req *ffcapi.TransactionSendRequest) bool { return req.GasPrice.JSONObject().GetString("unit") == "gwei" && req.GasPrice.JSONObject().GetString("value") == "32.146027800733336" && req.From == "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712" && req.TransactionData == "SOME_RAW_TX_BYTES" - })).Return(&ffcapi.SendTransactionResponse{ + })).Return(&ffcapi.TransactionSendResponse{ TransactionHash: "0x12345", }, ffcapi.ErrorReason(""), nil) @@ -171,31 +163,27 @@ func TestGasOracleSendOK(t *testing.T) { func TestConnectorGasOracleSendOK(t *testing.T) { - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeConnector) - p, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeConnector) + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + mtx := &apitypes.ManagedTX{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionHash: "0x12345", TransactionData: "SOME_RAW_TX_BYTES", } mockFFCAPI := &ffcapimocks.API{} - mockFFCAPI.On("GetGasPrice", mock.Anything, mock.Anything).Return(&ffcapi.GetGasPriceResponse{ + mockFFCAPI.On("GasPriceEstimate", mock.Anything, mock.Anything).Return(&ffcapi.GasPriceEstimateResponse{ GasPrice: fftypes.JSONAnyPtr(`"12345"`), }, ffcapi.ErrorReason(""), nil).Once() - mockFFCAPI.On("SendTransaction", mock.Anything, mock.MatchedBy(func(req *ffcapi.SendTransactionRequest) bool { + mockFFCAPI.On("TransactionSend", mock.Anything, mock.MatchedBy(func(req *ffcapi.TransactionSendRequest) bool { return req.From == "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712" && req.TransactionData == "SOME_RAW_TX_BYTES" - })).Return(&ffcapi.SendTransactionResponse{ + })).Return(&ffcapi.TransactionSendResponse{ TransactionHash: "0x12345", }, ffcapi.ErrorReason(""), nil) @@ -218,25 +206,21 @@ func TestConnectorGasOracleSendOK(t *testing.T) { func TestConnectorGasOracleFail(t *testing.T) { - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeConnector) - p, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeConnector) + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + mtx := &apitypes.ManagedTX{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionHash: "0x12345", TransactionData: "SOME_RAW_TX_BYTES", } mockFFCAPI := &ffcapimocks.API{} - mockFFCAPI.On("GetGasPrice", mock.Anything, mock.Anything).Return(&ffcapi.GetGasPriceResponse{ + mockFFCAPI.On("GasPriceEstimate", mock.Anything, mock.Anything).Return(&ffcapi.GasPriceEstimateResponse{ GasPrice: fftypes.JSONAnyPtr(`"12345"`), }, ffcapi.ErrorReason(""), fmt.Errorf("pop")) @@ -257,20 +241,16 @@ func TestGasOracleSendFail(t *testing.T) { })) defer server.Close() - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeRESTAPI) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleTemplate, "{{ . }}") - prefix.SubPrefix(GasOraclePrefix).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) - p, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeRESTAPI) + conf.SubSection(GasOracleConfig).Set(GasOracleTemplate, "{{ . }}") + conf.SubSection(GasOracleConfig).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + mtx := &apitypes.ManagedTX{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionData: "SOME_RAW_TX_BYTES", } @@ -287,10 +267,10 @@ func TestGasOracleMissingTemplate(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) server.Close() - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeRESTAPI) - prefix.SubPrefix(GasOraclePrefix).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) - _, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeRESTAPI) + conf.SubSection(GasOracleConfig).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) + _, err := f.NewPolicyEngine(context.Background(), conf) assert.Regexp(t, "FF21024", err) } @@ -300,11 +280,11 @@ func TestGasOracleBadTemplate(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) server.Close() - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeRESTAPI) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleTemplate, "{{ !!! wrong") - prefix.SubPrefix(GasOraclePrefix).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) - _, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeRESTAPI) + conf.SubSection(GasOracleConfig).Set(GasOracleTemplate, "{{ !!! wrong") + conf.SubSection(GasOracleConfig).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) + _, err := f.NewPolicyEngine(context.Background(), conf) assert.Regexp(t, "FF21025", err) } @@ -317,20 +297,16 @@ func TestGasOracleTemplateExecuteFail(t *testing.T) { })) defer server.Close() - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeRESTAPI) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleTemplate, "{{ .wrong.thing | len }}") - prefix.SubPrefix(GasOraclePrefix).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) - p, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeRESTAPI) + conf.SubSection(GasOracleConfig).Set(GasOracleTemplate, "{{ .wrong.thing | len }}") + conf.SubSection(GasOracleConfig).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + mtx := &apitypes.ManagedTX{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionData: "SOME_RAW_TX_BYTES", } @@ -347,20 +323,16 @@ func TestGasOracleNonJSON(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) server.Close() - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeRESTAPI) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleTemplate, "{{ . }}") - prefix.SubPrefix(GasOraclePrefix).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) - p, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeRESTAPI) + conf.SubSection(GasOracleConfig).Set(GasOracleTemplate, "{{ . }}") + conf.SubSection(GasOracleConfig).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + mtx := &apitypes.ManagedTX{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionData: "SOME_RAW_TX_BYTES", } @@ -380,26 +352,22 @@ func TestTXSendFail(t *testing.T) { })) defer server.Close() - f, prefix := newTestPolicyEngineFactory(t) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleMode, GasOracleModeRESTAPI) - prefix.SubPrefix(GasOraclePrefix).Set(GasOracleTemplate, "{{ . }}") - prefix.SubPrefix(GasOraclePrefix).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) - p, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.SubSection(GasOracleConfig).Set(GasOracleMode, GasOracleModeRESTAPI) + conf.SubSection(GasOracleConfig).Set(GasOracleTemplate, "{{ . }}") + conf.SubSection(GasOracleConfig).Set(ffresty.HTTPConfigURL, fmt.Sprintf("http://%s", server.Listener.Addr())) + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + mtx := &apitypes.ManagedTX{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionData: "SOME_RAW_TX_BYTES", } mockFFCAPI := &ffcapimocks.API{} - mockFFCAPI.On("SendTransaction", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonInvalidInputs, fmt.Errorf("pop")) + mockFFCAPI.On("TransactionSend", mock.Anything, mock.Anything).Return(nil, ffcapi.ErrorReasonInvalidInputs, fmt.Errorf("pop")) ctx := context.Background() _, _, err = p.Execute(ctx, mockFFCAPI, mtx) assert.Regexp(t, "pop", err) @@ -407,26 +375,24 @@ func TestTXSendFail(t *testing.T) { } func TestWarnStaleWarningCannotParse(t *testing.T) { - f, prefix := newTestPolicyEngineFactory(t) - prefix.Set(FixedGasPrice, `12345`) - p, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.Set(FixedGasPrice, `12345`) + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) - mtx := &fftm.ManagedTXOutput{ + mtx := &apitypes.ManagedTX{ TransactionData: "SOME_RAW_TX_BYTES", FirstSubmit: &submitTime, PolicyInfo: fftypes.JSONAnyPtr("!not json!"), - Request: &fftm.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, } mockFFCAPI := &ffcapimocks.API{} + mockFFCAPI.On("TransactionSend", mock.Anything, mock.Anything). + Return(nil, ffcapi.ErrorKnownTransaction, fmt.Errorf("Known transaction")) ctx := context.Background() updated, _, err := p.Execute(ctx, mockFFCAPI, mtx) @@ -437,21 +403,17 @@ func TestWarnStaleWarningCannotParse(t *testing.T) { mockFFCAPI.AssertExpectations(t) } -func TestWarnStaleAdditionalWarning(t *testing.T) { - f, prefix := newTestPolicyEngineFactory(t) - prefix.Set(FixedGasPrice, `12345`) - p, err := f.NewPolicyEngine(context.Background(), prefix) +func TestWarnStaleAdditionalWarningResubmitFail(t *testing.T) { + f, conf := newTestPolicyEngineFactory(t) + conf.Set(FixedGasPrice, `12345`) + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) lastWarning := fftypes.FFTime(time.Now().Add(-50 * time.Hour)) - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + mtx := &apitypes.ManagedTX{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionData: "SOME_RAW_TX_BYTES", FirstSubmit: &submitTime, @@ -459,10 +421,12 @@ func TestWarnStaleAdditionalWarning(t *testing.T) { } mockFFCAPI := &ffcapimocks.API{} + mockFFCAPI.On("TransactionSend", mock.Anything, mock.Anything). + Return(nil, ffcapi.ErrorReason(""), fmt.Errorf("pop")) ctx := context.Background() updated, reason, err := p.Execute(ctx, mockFFCAPI, mtx) - assert.NoError(t, err) + assert.Regexp(t, "pop", err) assert.Empty(t, reason) assert.True(t, updated) assert.NotEmpty(t, mtx.PolicyInfo.JSONObject().GetString("lastWarnTime")) @@ -471,21 +435,17 @@ func TestWarnStaleAdditionalWarning(t *testing.T) { } func TestWarnStaleNoWarning(t *testing.T) { - f, prefix := newTestPolicyEngineFactory(t) - prefix.Set(FixedGasPrice, `12345`) - prefix.Set(WarnInterval, "100s") - p, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.Set(FixedGasPrice, `12345`) + conf.Set(ResubmitInterval, "100s") + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) submitTime := fftypes.FFTime(time.Now().Add(-100 * time.Hour)) lastWarning := fftypes.Now() - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + mtx := &apitypes.ManagedTX{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionData: "SOME_RAW_TX_BYTES", FirstSubmit: &submitTime, @@ -504,24 +464,20 @@ func TestWarnStaleNoWarning(t *testing.T) { } func TestNoOpWithReceipt(t *testing.T) { - f, prefix := newTestPolicyEngineFactory(t) - prefix.Set(FixedGasPrice, `12345`) - prefix.Set(WarnInterval, "100s") - p, err := f.NewPolicyEngine(context.Background(), prefix) + f, conf := newTestPolicyEngineFactory(t) + conf.Set(FixedGasPrice, `12345`) + conf.Set(ResubmitInterval, "100s") + p, err := f.NewPolicyEngine(context.Background(), conf) assert.NoError(t, err) submitTime := fftypes.Now() - mtx := &fftm.ManagedTXOutput{ - Request: &fftm.TransactionRequest{ - TransactionInput: ffcapi.TransactionInput{ - TransactionHeaders: ffcapi.TransactionHeaders{ - From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", - }, - }, + mtx := &apitypes.ManagedTX{ + TransactionHeaders: ffcapi.TransactionHeaders{ + From: "0x6b7cfa4cf9709d3b3f5f7c22de123d2e16aee712", }, TransactionData: "SOME_RAW_TX_BYTES", FirstSubmit: submitTime, - Receipt: &ffcapi.GetReceiptResponse{ + Receipt: &ffcapi.TransactionReceiptResponse{ BlockHash: "0x39e2664effa5ad0651c35f1fe3b4c4b90492b1955fee731c2e9fb4d6518de114", }, }