├── openapi
├── .openapi-generator
│ ├── VERSION
│ └── FILES
├── .travis.yml
├── go.mod
├── .gitignore
├── go.sum
├── .openapi-generator-ignore
├── test
│ └── api_default_test.go
├── response.go
├── git_push.sh
├── api
│ └── openapi.yaml
├── README.md
├── docs
│ └── DefaultAPI.md
├── configuration.go
├── utils.go
├── api_default.go
└── client.go
├── openapi-config.yml
├── .github
├── assets
│ ├── tx-submit-api-logo.png
│ └── tx-submit-api-20241120.png
├── CODEOWNERS
├── workflows
│ ├── conventional-commits.yml
│ ├── go-test.yml
│ ├── nilaway.yml
│ ├── golangci-lint.yml
│ ├── ci-docker.yml
│ └── publish.yml
└── dependabot.yml
├── internal
├── api
│ ├── static
│ │ ├── txsubmit-logo.png
│ │ └── index.html
│ └── api.go
├── version
│ └── version.go
├── logging
│ ├── gin.go
│ └── logging.go
└── config
│ └── config.go
├── Dockerfile
├── openapi.sh
├── .gitignore
├── Makefile
├── .golangci.yml
├── docs
├── iohk.yaml
├── swagger.yaml
├── swagger.json
└── docs.go
├── config.yaml.example
├── cmd
└── tx-submit-api
│ └── main.go
├── submit
└── tx.go
├── go.mod
├── README.md
├── LICENSE
└── go.sum
/openapi/.openapi-generator/VERSION:
--------------------------------------------------------------------------------
1 | 7.14.0
2 |
--------------------------------------------------------------------------------
/openapi-config.yml:
--------------------------------------------------------------------------------
1 | enumClassPrefix: true
2 | generateInterfaces: true
3 | structPrefix: true
4 | isGoSubmodule: true
5 |
--------------------------------------------------------------------------------
/openapi/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 |
3 | install:
4 | - go get -d -v .
5 |
6 | script:
7 | - go build -v ./
8 |
9 |
--------------------------------------------------------------------------------
/.github/assets/tx-submit-api-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blinklabs-io/tx-submit-api/HEAD/.github/assets/tx-submit-api-logo.png
--------------------------------------------------------------------------------
/internal/api/static/txsubmit-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blinklabs-io/tx-submit-api/HEAD/internal/api/static/txsubmit-logo.png
--------------------------------------------------------------------------------
/.github/assets/tx-submit-api-20241120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blinklabs-io/tx-submit-api/HEAD/.github/assets/tx-submit-api-20241120.png
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Blink Labs
2 | #
3 | * @blinklabs-io/core
4 | *.md @blinklabs-io/core @blinklabs-io/docs @blinklabs-io/pms
5 | LICENSE @blinklabs-io/core @blinklabs-io/pms
6 |
--------------------------------------------------------------------------------
/openapi/.openapi-generator/FILES:
--------------------------------------------------------------------------------
1 | .gitignore
2 | .travis.yml
3 | README.md
4 | api/openapi.yaml
5 | api_default.go
6 | client.go
7 | configuration.go
8 | docs/DefaultAPI.md
9 | git_push.sh
10 | go.mod
11 | go.sum
12 | response.go
13 | utils.go
14 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ghcr.io/blinklabs-io/go:1.25.5-1 AS build
2 |
3 | WORKDIR /code
4 | COPY . .
5 | RUN make build
6 |
7 | FROM cgr.dev/chainguard/glibc-dynamic AS tx-submit-api
8 | COPY --from=build /code/tx-submit-api /bin/
9 | USER root
10 | ENTRYPOINT ["tx-submit-api"]
11 |
--------------------------------------------------------------------------------
/openapi.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | docker run --rm -v "${PWD}:/local" openapitools/openapi-generator-cli generate -i /local/docs/swagger.yaml --git-user-id blinklabs-io --git-repo-id tx-submit-api -g go -o /local/openapi -c /local/openapi-config.yml
4 | make format golines
5 | cd openapi && go mod tidy
6 |
--------------------------------------------------------------------------------
/openapi/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/blinklabs-io/tx-submit-api/openapi
2 |
3 | go 1.18
4 |
5 | require github.com/stretchr/testify v1.10.0
6 |
7 | require (
8 | github.com/davecgh/go-spew v1.1.1 // indirect
9 | github.com/pmezard/go-difflib v1.0.0 // indirect
10 | gopkg.in/yaml.v3 v3.0.1 // indirect
11 | )
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories (remove the comment below to include it)
15 | # vendor/
16 |
17 | # Binary
18 | /tx-submit-api
19 |
--------------------------------------------------------------------------------
/openapi/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 |
6 | # Folders
7 | _obj
8 | _test
9 |
10 | # Architecture specific extensions/prefixes
11 | *.[568vq]
12 | [568vq].out
13 |
14 | *.cgo1.go
15 | *.cgo2.c
16 | _cgo_defun.c
17 | _cgo_gotypes.go
18 | _cgo_export.*
19 |
20 | _testmain.go
21 |
22 | *.exe
23 | *.test
24 | *.prof
25 |
--------------------------------------------------------------------------------
/.github/workflows/conventional-commits.yml:
--------------------------------------------------------------------------------
1 | # The below is pulled from upstream and slightly modified
2 | # https://github.com/webiny/action-conventional-commits/blob/master/README.md#usage
3 |
4 | name: Conventional Commits
5 |
6 | on:
7 | pull_request:
8 |
9 | jobs:
10 | build:
11 | name: Conventional Commits
12 | runs-on: ubuntu-latest
13 | permissions:
14 | contents: read
15 | steps:
16 | - uses: actions/checkout@v6.0.1
17 | - uses: webiny/action-conventional-commits@v1.3.0
18 |
--------------------------------------------------------------------------------
/.github/workflows/go-test.yml:
--------------------------------------------------------------------------------
1 | name: go-test
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 | branches:
8 | - main
9 | pull_request:
10 |
11 | permissions:
12 | contents: read
13 |
14 | jobs:
15 | go-test:
16 | name: go-test
17 | strategy:
18 | matrix:
19 | go-version: [1.24.x, 1.25.x]
20 | platform: [ubuntu-latest]
21 | runs-on: ${{ matrix.platform }}
22 | steps:
23 | - uses: actions/checkout@v6.0.1
24 | - uses: actions/setup-go@v6
25 | with:
26 | go-version: ${{ matrix.go-version }}
27 | - name: go-test
28 | run: go test ./...
29 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "docker"
9 | directory: "/"
10 | schedule:
11 | interval: "weekly"
12 | - package-ecosystem: "github-actions"
13 | directory: "/"
14 | schedule:
15 | interval: "weekly"
16 | - package-ecosystem: "gomod"
17 | directory: "/"
18 | schedule:
19 | interval: "weekly"
20 |
--------------------------------------------------------------------------------
/.github/workflows/nilaway.yml:
--------------------------------------------------------------------------------
1 | name: nilaway
2 | on:
3 | push:
4 | tags:
5 | - v*
6 | branches:
7 | - main
8 | pull_request:
9 |
10 | permissions:
11 | contents: read
12 |
13 | jobs:
14 | nilaway:
15 | name: nilaway
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5.0.1 https://github.com/actions/checkout/releases/tag/v5.0.1
19 | - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 https://github.com/actions/setup-go/releases/tag/v6.0.0
20 | with:
21 | go-version: 1.25.x
22 | - name: install nilaway
23 | run: go install go.uber.org/nilaway/cmd/nilaway@latest
24 | - name: run nilaway
25 | run: nilaway ./...
26 |
--------------------------------------------------------------------------------
/.github/workflows/golangci-lint.yml:
--------------------------------------------------------------------------------
1 | name: golangci-lint
2 | on:
3 | push:
4 | tags:
5 | - v*
6 | branches:
7 | - main
8 | pull_request:
9 |
10 | permissions:
11 | contents: read
12 |
13 | jobs:
14 | golangci:
15 | name: lint
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5.0.1 https://github.com/actions/checkout/releases/tag/v5.0.1
19 | - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 https://github.com/actions/setup-go/releases/tag/v6.0.0
20 | with:
21 | go-version: 1.25.x
22 | - name: golangci-lint
23 | uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 https://github.com/golangci/golangci-lint-action/releases/tag/v9.2.0
24 |
--------------------------------------------------------------------------------
/openapi/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
6 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
11 |
--------------------------------------------------------------------------------
/internal/version/version.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 Blink Labs Software
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package version
16 |
17 | import (
18 | "fmt"
19 | )
20 |
21 | // These are populated at build time
22 | var (
23 | Version string
24 | CommitHash string
25 | )
26 |
27 | func GetVersionString() string {
28 | if Version != "" {
29 | return fmt.Sprintf("%s (commit %s)", Version, CommitHash)
30 | } else {
31 | return fmt.Sprintf("devel (commit %s)", CommitHash)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/.github/workflows/ci-docker.yml:
--------------------------------------------------------------------------------
1 | name: Docker CI
2 |
3 | on:
4 | pull_request:
5 | branches: ['main']
6 | paths: ['Dockerfile','cmd/**','docs/**','internal/**','go.*','.github/workflows/ci-docker.yml']
7 |
8 | env:
9 | REGISTRY: ghcr.io
10 | IMAGE_NAME: blinklabs-io/tx-submit-api
11 |
12 | permissions:
13 | contents: read
14 |
15 | jobs:
16 | docker:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: actions/checkout@v6.0.1
20 | with:
21 | fetch-depth: '0'
22 | - name: qemu
23 | uses: docker/setup-qemu-action@v3
24 | - uses: docker/setup-buildx-action@v3
25 | - id: meta
26 | uses: docker/metadata-action@v5
27 | with:
28 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
29 | - name: build
30 | uses: docker/build-push-action@v6
31 | with:
32 | context: .
33 | push: false
34 | ### TODO: test multiple platforms
35 | # platforms: linux/amd64,linux/arm64
36 | tags: ${{ steps.meta.outputs.tags }}
37 | labels: ${{ steps.meta.outputs.labels }}
38 |
--------------------------------------------------------------------------------
/openapi/.openapi-generator-ignore:
--------------------------------------------------------------------------------
1 | # OpenAPI Generator Ignore
2 | # Generated by openapi-generator https://github.com/openapitools/openapi-generator
3 |
4 | # Use this file to prevent files from being overwritten by the generator.
5 | # The patterns follow closely to .gitignore or .dockerignore.
6 |
7 | # As an example, the C# client generator defines ApiClient.cs.
8 | # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
9 | #ApiClient.cs
10 |
11 | # You can match any string of characters against a directory, file or extension with a single asterisk (*):
12 | #foo/*/qux
13 | # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
14 |
15 | # You can recursively match patterns against a directory, file or extension with a double asterisk (**):
16 | #foo/**/qux
17 | # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
18 |
19 | # You can also negate patterns with an exclamation (!).
20 | # For example, you can ignore all files in a docs folder with the file extension .md:
21 | #docs/*.md
22 | # Then explicitly reverse the ignore rule for a single file:
23 | #!docs/README.md
24 |
--------------------------------------------------------------------------------
/openapi/test/api_default_test.go:
--------------------------------------------------------------------------------
1 | /*
2 | tx-submit-api
3 |
4 | Testing DefaultAPIService
5 |
6 | */
7 |
8 | // Code generated by OpenAPI Generator (https://openapi-generator.tech);
9 |
10 | package openapi
11 |
12 | import (
13 | "context"
14 | openapiclient "github.com/blinklabs-io/tx-submit-api/openapi"
15 | "github.com/stretchr/testify/assert"
16 | "github.com/stretchr/testify/require"
17 | "testing"
18 | )
19 |
20 | func Test_openapi_DefaultAPIService(t *testing.T) {
21 |
22 | configuration := openapiclient.NewConfiguration()
23 | apiClient := openapiclient.NewAPIClient(configuration)
24 |
25 | t.Run("Test DefaultAPIService ApiHastxTxHashGet", func(t *testing.T) {
26 |
27 | t.Skip("skip test") // remove to run test
28 |
29 | var txHash string
30 |
31 | resp, httpRes, err := apiClient.DefaultAPI.ApiHastxTxHashGet(context.Background(), txHash).
32 | Execute()
33 |
34 | require.Nil(t, err)
35 | require.NotNil(t, resp)
36 | assert.Equal(t, 200, httpRes.StatusCode)
37 |
38 | })
39 |
40 | t.Run("Test DefaultAPIService ApiSubmitTxPost", func(t *testing.T) {
41 |
42 | t.Skip("skip test") // remove to run test
43 |
44 | resp, httpRes, err := apiClient.DefaultAPI.ApiSubmitTxPost(context.Background()).
45 | Execute()
46 |
47 | require.Nil(t, err)
48 | require.NotNil(t, resp)
49 | assert.Equal(t, 200, httpRes.StatusCode)
50 |
51 | })
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | BINARY=tx-submit-api
2 |
3 | # Determine root directory
4 | ROOT_DIR=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
5 |
6 | # Gather all .go files for use in dependencies below
7 | GO_FILES=$(shell find $(ROOT_DIR) -name '*.go')
8 |
9 | # Extract Go module name from go.mod
10 | GOMODULE=$(shell grep ^module $(ROOT_DIR)/go.mod | awk '{ print $$2 }')
11 |
12 | # Set version strings based on git tag and current ref
13 | GO_LDFLAGS=-ldflags "-s -w -X '$(GOMODULE)/internal/version.Version=$(shell git describe --tags --exact-match 2>/dev/null)' -X '$(GOMODULE)/internal/version.CommitHash=$(shell git rev-parse --short HEAD)'"
14 |
15 | # Alias for building program binary
16 | build: $(BINARY)
17 |
18 | mod-tidy:
19 | go mod tidy
20 |
21 | # Build our program binary
22 | # Depends on GO_FILES to determine when rebuild is needed
23 | $(BINARY): mod-tidy $(GO_FILES)
24 | CGO_ENABLED=0 go build \
25 | $(GO_LDFLAGS) \
26 | -o $(BINARY) \
27 | ./cmd/$(BINARY)
28 |
29 | .PHONY: build clean image mod-tidy
30 |
31 | clean:
32 | rm -f $(BINARY)
33 |
34 | format: mod-tidy
35 | go fmt ./...
36 | gofmt -s -w $(GO_FILES)
37 |
38 | golines:
39 | golines -w --ignore-generated --chain-split-dots --max-len=80 --reformat-tags .
40 |
41 | swagger:
42 | swag f -g api.go -d internal/api
43 | swag i -g api.go -d internal/api
44 |
45 | test: mod-tidy
46 | go test -v -race ./...
47 |
48 | # Build docker image
49 | image: build
50 | docker build -t $(BINARY) .
51 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | run:
3 | issues-exit-code: 1
4 | tests: false
5 | linters:
6 | enable:
7 | - asasalint
8 | - asciicheck
9 | - bidichk
10 | - bodyclose
11 | - contextcheck
12 | - copyloopvar
13 | - durationcheck
14 | - errchkjson
15 | - errorlint
16 | - exhaustive
17 | - fatcontext
18 | - gocheckcompilerdirectives
19 | - gochecksumtype
20 | - gomodguard
21 | - gosec
22 | - gosmopolitan
23 | - loggercheck
24 | - makezero
25 | - musttag
26 | - nilerr
27 | - nilnesserr
28 | - perfsprint
29 | - prealloc
30 | - protogetter
31 | - reassign
32 | - recvcheck
33 | - rowserrcheck
34 | - spancheck
35 | - sqlclosecheck
36 | - testifylint
37 | - unparam
38 | - usestdlibvars
39 | - whitespace
40 | - zerologlint
41 | disable:
42 | - depguard
43 | - noctx
44 | exclusions:
45 | generated: lax
46 | presets:
47 | - comments
48 | - common-false-positives
49 | - legacy
50 | - std-error-handling
51 | paths:
52 | - docs
53 | - third_party$
54 | - builtin$
55 | - examples$
56 | issues:
57 | max-issues-per-linter: 0
58 | max-same-issues: 0
59 | formatters:
60 | enable:
61 | - gci
62 | - gofmt
63 | - gofumpt
64 | - goimports
65 | exclusions:
66 | generated: lax
67 | paths:
68 | - docs
69 | - third_party$
70 | - builtin$
71 | - examples$
72 |
--------------------------------------------------------------------------------
/openapi/response.go:
--------------------------------------------------------------------------------
1 | /*
2 | tx-submit-api
3 |
4 | Cardano Transaction Submit API
5 |
6 | API version: v0
7 | Contact: support@blinklabs.io
8 | */
9 |
10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
11 |
12 | package openapi
13 |
14 | import (
15 | "net/http"
16 | )
17 |
18 | // APIResponse stores the API response returned by the server.
19 | type APIResponse struct {
20 | *http.Response `json:"-"`
21 | Message string `json:"message,omitempty"`
22 | // Operation is the name of the OpenAPI operation.
23 | Operation string `json:"operation,omitempty"`
24 | // RequestURL is the request URL. This value is always available, even if the
25 | // embedded *http.Response is nil.
26 | RequestURL string `json:"url,omitempty"`
27 | // Method is the HTTP method used for the request. This value is always
28 | // available, even if the embedded *http.Response is nil.
29 | Method string `json:"method,omitempty"`
30 | // Payload holds the contents of the response body (which may be nil or empty).
31 | // This is provided here as the raw response.Body() reader will have already
32 | // been drained.
33 | Payload []byte `json:"-"`
34 | }
35 |
36 | // NewAPIResponse returns a new APIResponse object.
37 | func NewAPIResponse(r *http.Response) *APIResponse {
38 |
39 | response := &APIResponse{Response: r}
40 | return response
41 | }
42 |
43 | // NewAPIResponseWithError returns a new APIResponse object with the provided error message.
44 | func NewAPIResponseWithError(errorMessage string) *APIResponse {
45 |
46 | response := &APIResponse{Message: errorMessage}
47 | return response
48 | }
49 |
--------------------------------------------------------------------------------
/internal/logging/gin.go:
--------------------------------------------------------------------------------
1 | package logging
2 |
3 | import (
4 | "log/slog"
5 | "net/http"
6 | "runtime/debug"
7 | "time"
8 |
9 | "github.com/gin-gonic/gin"
10 | )
11 |
12 | // GinLogger returns a middleware that logs HTTP requests with slog.
13 | func GinLogger(logger *slog.Logger, skipPaths []string) gin.HandlerFunc {
14 | skip := make(map[string]struct{}, len(skipPaths))
15 | for _, path := range skipPaths {
16 | skip[path] = struct{}{}
17 | }
18 |
19 | return func(c *gin.Context) {
20 | start := time.Now()
21 | path := c.Request.URL.Path
22 | raw := c.Request.URL.RawQuery
23 |
24 | c.Next()
25 |
26 | if _, ok := skip[path]; ok {
27 | return
28 | }
29 |
30 | latency := time.Since(start)
31 | entry := []any{
32 | "status", c.Writer.Status(),
33 | "method", c.Request.Method,
34 | "path", path,
35 | "query", raw,
36 | "ip", c.ClientIP(),
37 | "userAgent", c.Request.UserAgent(),
38 | "latency", latency.String(),
39 | "size", c.Writer.Size(),
40 | }
41 | if len(c.Errors) > 0 {
42 | entry = append(entry, "errors", c.Errors.String())
43 | }
44 |
45 | logger.Info("request completed", entry...)
46 | }
47 | }
48 |
49 | // GinRecovery recovers from panics and logs them.
50 | func GinRecovery(logger *slog.Logger, includeStack bool) gin.HandlerFunc {
51 | return func(c *gin.Context) {
52 | defer func() {
53 | if err := recover(); err != nil {
54 | fields := []any{"err", err}
55 | if includeStack {
56 | fields = append(fields, "stack", string(debug.Stack()))
57 | }
58 | logger.Error("panic recovered", fields...)
59 | c.AbortWithStatus(http.StatusInternalServerError)
60 | }
61 | }()
62 | c.Next()
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/docs/iohk.yaml:
--------------------------------------------------------------------------------
1 | swagger: '2.0'
2 | schemes: ["http"]
3 | host: localhost
4 | basePath: /
5 | info:
6 | title: cardano-submit-api
7 | version: 3.1.0
8 | license:
9 | name: Apache-2.0
10 | url: https://github.com/input-output-hk/cardano-rest/blob/master/submit-api/LICENSE
11 | description: |
12 |

13 |
14 | x-tagGroups: []
15 |
16 | definitions: {}
17 |
18 | paths:
19 | /api/submit/tx:
20 | post:
21 | operationId: postTransaction
22 | summary: Submit Tx
23 | description: Submit an already serialized transaction to the network.
24 | parameters:
25 | - in: header
26 | name: Content-Type
27 | required: true
28 | type: string
29 | enum: ["application/cbor"]
30 |
31 | x-code-samples:
32 | - lang: "Shell"
33 | label: "cURL"
34 | source: |
35 | # Assuming `data` is a raw binary serialized transaction on the file-system.
36 | curl -X POST \
37 | --header "Content-Type: application/cbor" \
38 | --data-binary @data http://localhost:8101/api/submit/tx
39 | produces:
40 | - "application/json"
41 | responses:
42 | 202:
43 | description: Ok
44 | schema:
45 | description: The transaction id.
46 | type: string
47 | format: hex
48 | minLength: 64
49 | maxLength: 64
50 | example: 92bcd06b25dfbd89b578d536b4d3b7dd269b7c2aa206ed518012cffe0444d67f
51 |
52 | 400:
53 | description: Bad Request
54 | schema:
55 | type: string
56 | description: An error message.
57 |
--------------------------------------------------------------------------------
/openapi/git_push.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
3 | #
4 | # Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com"
5 |
6 | git_user_id=$1
7 | git_repo_id=$2
8 | release_note=$3
9 | git_host=$4
10 |
11 | if [ "$git_host" = "" ]; then
12 | git_host="github.com"
13 | echo "[INFO] No command line input provided. Set \$git_host to $git_host"
14 | fi
15 |
16 | if [ "$git_user_id" = "" ]; then
17 | git_user_id="blinklabs-io"
18 | echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id"
19 | fi
20 |
21 | if [ "$git_repo_id" = "" ]; then
22 | git_repo_id="tx-submit-api"
23 | echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id"
24 | fi
25 |
26 | if [ "$release_note" = "" ]; then
27 | release_note="Minor update"
28 | echo "[INFO] No command line input provided. Set \$release_note to $release_note"
29 | fi
30 |
31 | # Initialize the local directory as a Git repository
32 | git init
33 |
34 | # Adds the files in the local repository and stages them for commit.
35 | git add .
36 |
37 | # Commits the tracked changes and prepares them to be pushed to a remote repository.
38 | git commit -m "$release_note"
39 |
40 | # Sets the new remote
41 | git_remote=$(git remote)
42 | if [ "$git_remote" = "" ]; then # git remote not defined
43 |
44 | if [ "$GIT_TOKEN" = "" ]; then
45 | echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment."
46 | git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git
47 | else
48 | git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git
49 | fi
50 |
51 | fi
52 |
53 | git pull origin master
54 |
55 | # Pushes (Forces) the changes in the local repository up to the remote repository
56 | echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git"
57 | git push origin master 2>&1 | grep -v 'To https'
58 |
--------------------------------------------------------------------------------
/docs/swagger.yaml:
--------------------------------------------------------------------------------
1 | basePath: /
2 | info:
3 | contact:
4 | email: support@blinklabs.io
5 | name: Blink Labs Software
6 | url: https://blinklabs.io
7 | description: Cardano Transaction Submit API
8 | license:
9 | name: Apache 2.0
10 | url: http://www.apache.org/licenses/LICENSE-2.0.html
11 | title: tx-submit-api
12 | version: v0
13 | paths:
14 | /api/hastx/{tx_hash}:
15 | get:
16 | description: Determine if a given transaction ID exists in the node mempool.
17 | parameters:
18 | - description: Transaction Hash
19 | in: path
20 | name: tx_hash
21 | required: true
22 | type: string
23 | produces:
24 | - application/json
25 | responses:
26 | "200":
27 | description: Ok
28 | schema:
29 | type: string
30 | "400":
31 | description: Bad Request
32 | schema:
33 | type: string
34 | "404":
35 | description: Not Found
36 | schema:
37 | type: string
38 | "415":
39 | description: Unsupported Media Type
40 | schema:
41 | type: string
42 | "500":
43 | description: Server Error
44 | schema:
45 | type: string
46 | summary: HasTx
47 | /api/submit/tx:
48 | post:
49 | description: Submit an already serialized transaction to the network.
50 | parameters:
51 | - description: Content type
52 | enum:
53 | - application/cbor
54 | in: header
55 | name: Content-Type
56 | required: true
57 | type: string
58 | produces:
59 | - application/json
60 | responses:
61 | "202":
62 | description: Ok
63 | schema:
64 | type: string
65 | "400":
66 | description: Bad Request
67 | schema:
68 | type: string
69 | "415":
70 | description: Unsupported Media Type
71 | schema:
72 | type: string
73 | "500":
74 | description: Server Error
75 | schema:
76 | type: string
77 | summary: Submit Tx
78 | swagger: "2.0"
79 |
--------------------------------------------------------------------------------
/internal/logging/logging.go:
--------------------------------------------------------------------------------
1 | // Copyright 2023 Blink Labs Software
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package logging
16 |
17 | import (
18 | "fmt"
19 | "log"
20 | "log/slog"
21 | "os"
22 | "strings"
23 | "time"
24 |
25 | "github.com/blinklabs-io/tx-submit-api/internal/config"
26 | )
27 |
28 | var (
29 | globalLogger *slog.Logger
30 | accessLogger *slog.Logger
31 | )
32 |
33 | func Setup(cfg *config.LoggingConfig) {
34 | level, err := parseLevel(cfg.Level)
35 | if err != nil {
36 | log.Fatalf("error configuring logger: %s", err)
37 | }
38 |
39 | opts := &slog.HandlerOptions{
40 | Level: level,
41 | ReplaceAttr: func(groups []string, attr slog.Attr) slog.Attr {
42 | if attr.Key == slog.TimeKey {
43 | attr.Key = "timestamp"
44 | attr.Value = slog.StringValue(
45 | attr.Value.Time().Format(time.RFC3339),
46 | )
47 | }
48 | return attr
49 | },
50 | }
51 |
52 | handler := slog.NewJSONHandler(os.Stdout, opts)
53 | globalLogger = slog.New(handler)
54 | accessLogger = globalLogger.With(slog.String("type", "access"))
55 | }
56 |
57 | func GetLogger() *slog.Logger {
58 | return globalLogger
59 | }
60 |
61 | func GetAccessLogger() *slog.Logger {
62 | return accessLogger
63 | }
64 |
65 | func parseLevel(level string) (slog.Leveler, error) {
66 | if level == "" {
67 | return slog.LevelInfo, nil
68 | }
69 | switch strings.ToLower(level) {
70 | case "debug":
71 | return slog.LevelDebug, nil
72 | case "info":
73 | return slog.LevelInfo, nil
74 | case "warn", "warning":
75 | return slog.LevelWarn, nil
76 | case "error":
77 | return slog.LevelError, nil
78 | default:
79 | return nil, fmt.Errorf("invalid log level: %s", level)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/openapi/api/openapi.yaml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.1
2 | info:
3 | contact:
4 | email: support@blinklabs.io
5 | name: Blink Labs Software
6 | url: https://blinklabs.io
7 | description: Cardano Transaction Submit API
8 | license:
9 | name: Apache 2.0
10 | url: http://www.apache.org/licenses/LICENSE-2.0.html
11 | title: tx-submit-api
12 | version: v0
13 | servers:
14 | - url: /
15 | paths:
16 | /api/hastx/{tx_hash}:
17 | get:
18 | description: Determine if a given transaction ID exists in the node mempool.
19 | parameters:
20 | - description: Transaction Hash
21 | in: path
22 | name: tx_hash
23 | required: true
24 | schema:
25 | type: string
26 | responses:
27 | "200":
28 | content:
29 | application/json:
30 | schema:
31 | type: string
32 | description: Ok
33 | "400":
34 | content:
35 | application/json:
36 | schema:
37 | type: string
38 | description: Bad Request
39 | "404":
40 | content:
41 | application/json:
42 | schema:
43 | type: string
44 | description: Not Found
45 | "415":
46 | content:
47 | application/json:
48 | schema:
49 | type: string
50 | description: Unsupported Media Type
51 | "500":
52 | content:
53 | application/json:
54 | schema:
55 | type: string
56 | description: Server Error
57 | summary: HasTx
58 | /api/submit/tx:
59 | post:
60 | description: Submit an already serialized transaction to the network.
61 | parameters:
62 | - description: Content type
63 | in: header
64 | name: Content-Type
65 | required: true
66 | schema:
67 | enum:
68 | - application/cbor
69 | type: string
70 | responses:
71 | "202":
72 | content:
73 | application/json:
74 | schema:
75 | type: string
76 | description: Ok
77 | "400":
78 | content:
79 | application/json:
80 | schema:
81 | type: string
82 | description: Bad Request
83 | "415":
84 | content:
85 | application/json:
86 | schema:
87 | type: string
88 | description: Unsupported Media Type
89 | "500":
90 | content:
91 | application/json:
92 | schema:
93 | type: string
94 | description: Server Error
95 | summary: Submit Tx
96 | components:
97 | schemas: {}
98 | x-original-swagger-version: "2.0"
99 |
--------------------------------------------------------------------------------
/config.yaml.example:
--------------------------------------------------------------------------------
1 | ---
2 | # Example config file for tx-submit-api
3 | # The values shown below correspond to the in-code defaults
4 |
5 | logging:
6 | # Logging level
7 | #
8 | # This can also be set via the LOGGING_LEVEL environment variable
9 | level: info
10 |
11 | # Health checks
12 | #
13 | # This can also be set via the LOGGING_HEALTHCHECKS environment variable
14 | healthchecks: false
15 |
16 | api:
17 | # Listen address for the API
18 | #
19 | # This can also be set via the API_LISTEN_ADDRESS environment variable
20 | address:
21 |
22 | # Listen port for the API
23 | #
24 | # This can also be set via the API_LISTEN_PORT environment variable
25 | port: 8090
26 |
27 | metrics:
28 | # Listen address for the metrics endpoint
29 | #
30 | # This can also be set via the METRICS_LISTEN_ADDRESS environment variable
31 | address:
32 |
33 | # Listen port for the metrics endpoint
34 | #
35 | # This can also be set via the METRICS_LISTEN_PORT environment variable
36 | port: 8081
37 |
38 | # The debug endpoint provides access to pprof for debugging purposes. This is
39 | # disabled by default, but it can be enabled by setting the port to a non-zero
40 | # value
41 | debug:
42 | # Listen address for the debug endpoint
43 | #
44 | # This can also be set via the DEBUG_ADDRESS environment variable
45 | address: localhost
46 |
47 | # Listen port for the debug endpoint
48 | #
49 | # This can also be set via the DEBUG_PORT environment variable
50 | port: 0
51 |
52 | node:
53 | # Named Cardano network for cardano-node
54 | #
55 | # This is a short-cut to select the NetworkMagic and can be used to
56 | # select mainnet, preprod, or preview networks.
57 | #
58 | # This can also be set via the CARDANO_NETWORK environment variable
59 | network: mainnet
60 |
61 | # NetworkMagic for network for cardano-node
62 | #
63 | # This selects the correct network for operation and can be configured to
64 | # any network, not just the named networks.
65 | #
66 | # This can also be set via the CARDANO_NODE_NETWORK_MAGIC environment variable
67 | networkMagic:
68 |
69 | # Path to UNIX socket file for cardano-node
70 | #
71 | # This can also be set via the CARDANO_NODE_SOCKET_PATH environment variable
72 | socketPath:
73 |
74 | # Address/port for cardano-node
75 | #
76 | # This requires that you be running socat or similar to create a bridge
77 | # between TCP and the UNIX socket.
78 | #
79 | # These can also be set via the CARDANO_NODE_SOCKET_TCP_HOST and
80 | # CARDANO_NODE_SOCKET_TCP_PORT environment variables
81 | address:
82 | port:
83 |
84 | # Skip checking connection to cardano-node
85 | #
86 | # On startup, we connect to the configured cardano-node and exit on failure.
87 | #
88 | # Setting this to true will skip this check.
89 | skipCheck:
90 |
91 | # Timeout for connections to cardano-node
92 | #
93 | # This can also be set via the CARDANO_NODE_SOCKET_TIMEOUT environment
94 | # variable
95 | timeout:
96 |
--------------------------------------------------------------------------------
/cmd/tx-submit-api/main.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Blink Labs Software
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package main
16 |
17 | import (
18 | "flag"
19 | "fmt"
20 | "net/http"
21 | _ "net/http/pprof" // #nosec G108
22 | "os"
23 | "time"
24 |
25 | "github.com/blinklabs-io/tx-submit-api/internal/api"
26 | "github.com/blinklabs-io/tx-submit-api/internal/config"
27 | "github.com/blinklabs-io/tx-submit-api/internal/logging"
28 | "github.com/blinklabs-io/tx-submit-api/internal/version"
29 | "go.uber.org/automaxprocs/maxprocs"
30 | )
31 |
32 | var cmdlineFlags struct {
33 | configFile string
34 | }
35 |
36 | func logPrintf(format string, v ...any) {
37 | logging.GetLogger().Info(fmt.Sprintf(format, v...))
38 | }
39 |
40 | func main() {
41 | flag.StringVar(
42 | &cmdlineFlags.configFile,
43 | "config",
44 | "",
45 | "path to config file to load",
46 | )
47 | flag.Parse()
48 |
49 | // Load config
50 | cfg, err := config.Load(cmdlineFlags.configFile)
51 | if err != nil {
52 | fmt.Printf("Failed to load config: %s\n", err)
53 | os.Exit(1)
54 | }
55 |
56 | // Configure logging
57 | logging.Setup(&cfg.Logging)
58 | logger := logging.GetLogger()
59 |
60 | logger.Info("starting tx-submit-api", "version", version.GetVersionString())
61 |
62 | // Configure max processes with our logger wrapper, toss undo func
63 | _, err = maxprocs.Set(maxprocs.Logger(logPrintf))
64 | if err != nil {
65 | // If we hit this, something really wrong happened
66 | logger.Error("maxprocs setup failed", "err", err)
67 | os.Exit(1)
68 | }
69 |
70 | // Start debug listener
71 | if cfg.Debug.ListenPort > 0 {
72 | logger.Info(
73 | "starting debug listener",
74 | "address", cfg.Debug.ListenAddress,
75 | "port", cfg.Debug.ListenPort,
76 | )
77 | go func() {
78 | debugger := &http.Server{
79 | Addr: fmt.Sprintf(
80 | "%s:%d",
81 | cfg.Debug.ListenAddress,
82 | cfg.Debug.ListenPort,
83 | ),
84 | ReadHeaderTimeout: 60 * time.Second,
85 | }
86 | err := debugger.ListenAndServe()
87 | if err != nil {
88 | logger.Error("failed to start debug listener", "err", err)
89 | os.Exit(1)
90 | }
91 | }()
92 | }
93 |
94 | // Start API listener
95 | if err := api.Start(cfg); err != nil {
96 | logger.Error("failed to start API", "err", err)
97 | os.Exit(1)
98 | }
99 |
100 | // Wait forever
101 | select {}
102 | }
103 |
--------------------------------------------------------------------------------
/submit/tx.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Blink Labs Software
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package submit
16 |
17 | import (
18 | "errors"
19 | "fmt"
20 | "math"
21 | "time"
22 |
23 | ouroboros "github.com/blinklabs-io/gouroboros"
24 | "github.com/blinklabs-io/gouroboros/ledger"
25 | "github.com/blinklabs-io/gouroboros/protocol/localtxsubmission"
26 | )
27 |
28 | type Config struct {
29 | ErrorChan chan error
30 | Network string
31 | NetworkMagic uint32
32 | NodeAddress string
33 | NodePort uint
34 | SocketPath string
35 | Timeout uint
36 | }
37 |
38 | func SubmitTx(cfg *Config, txRawBytes []byte) (string, error) {
39 | // Fail fast if timeout is too large
40 | if cfg.Timeout > math.MaxInt64 {
41 | return "", errors.New("given timeout too large")
42 | }
43 | // Determine transaction type (era)
44 | txType, err := ledger.DetermineTransactionType(txRawBytes)
45 | if err != nil {
46 | return "", fmt.Errorf(
47 | "could not parse transaction to determine type: %w",
48 | err,
49 | )
50 | }
51 | tx, err := ledger.NewTransactionFromCbor(txType, txRawBytes)
52 | if err != nil {
53 | return "", fmt.Errorf("failed to parse transaction CBOR: %w", err)
54 | }
55 |
56 | err = cfg.populateNetworkMagic()
57 | if err != nil {
58 | return "", fmt.Errorf("failed to populate networkMagic: %w", err)
59 | }
60 |
61 | // Connect to cardano-node and submit TX using Ouroboros LocalTxSubmission
62 | oConn, err := ouroboros.NewConnection(
63 | ouroboros.WithNetworkMagic(uint32(cfg.NetworkMagic)),
64 | ouroboros.WithErrorChan(cfg.ErrorChan),
65 | ouroboros.WithNodeToNode(false),
66 | ouroboros.WithLocalTxSubmissionConfig(
67 | localtxsubmission.NewConfig(
68 | localtxsubmission.WithTimeout(
69 | time.Duration(cfg.Timeout)*time.Second,
70 | ),
71 | ),
72 | ),
73 | )
74 | if err != nil {
75 | return "", fmt.Errorf("failure creating Ouroboros connection: %w", err)
76 | }
77 | if cfg.NodeAddress != "" && cfg.NodePort > 0 {
78 | if err := oConn.Dial("tcp", fmt.Sprintf("%s:%d", cfg.NodeAddress, cfg.NodePort)); err != nil {
79 | return "", fmt.Errorf("failure connecting to node via TCP: %w", err)
80 | }
81 | } else {
82 | if err := oConn.Dial("unix", cfg.SocketPath); err != nil {
83 | return "", fmt.Errorf("failure connecting to node via UNIX socket: %w", err)
84 | }
85 | }
86 | defer func() {
87 | // Close Ouroboros connection
88 | oConn.Close()
89 | }()
90 | // Submit the transaction
91 | // #nosec G115
92 | if err := oConn.LocalTxSubmission().Client.SubmitTx(uint16(txType), txRawBytes); err != nil {
93 | return "", fmt.Errorf("%s", err.Error())
94 | }
95 | return tx.Hash().String(), nil
96 | }
97 |
98 | func (c *Config) populateNetworkMagic() error {
99 | if c.NetworkMagic == 0 {
100 | if c.Network != "" {
101 | network, ok := ouroboros.NetworkByName(c.Network)
102 | if !ok {
103 | return fmt.Errorf("unknown network: %s", c.Network)
104 | }
105 | c.NetworkMagic = uint32(network.NetworkMagic)
106 | return nil
107 | }
108 | }
109 | return nil
110 | }
111 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/blinklabs-io/tx-submit-api
2 |
3 | go 1.24.0
4 |
5 | toolchain go1.24.4
6 |
7 | require (
8 | github.com/blinklabs-io/gouroboros v0.143.0
9 | github.com/fxamacker/cbor/v2 v2.9.0
10 | github.com/gin-contrib/cors v1.7.6
11 | github.com/gin-gonic/gin v1.11.0
12 | github.com/kelseyhightower/envconfig v1.4.0
13 | github.com/penglongli/gin-metrics v0.1.13
14 | github.com/swaggo/files v1.0.1
15 | github.com/swaggo/gin-swagger v1.6.1
16 | github.com/swaggo/swag v1.16.6
17 | go.uber.org/automaxprocs v1.6.0
18 | gopkg.in/yaml.v2 v2.4.0
19 | )
20 |
21 | require (
22 | filippo.io/edwards25519 v1.1.0 // indirect
23 | github.com/KyleBanks/depth v1.2.1 // indirect
24 | github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect
25 | github.com/beorn7/perks v1.0.1 // indirect
26 | github.com/bits-and-blooms/bitset v1.20.0 // indirect
27 | github.com/blinklabs-io/plutigo v0.0.16 // indirect
28 | github.com/btcsuite/btcd/btcec/v2 v2.3.6 // indirect
29 | github.com/btcsuite/btcd/btcutil v1.1.6 // indirect
30 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
31 | github.com/bytedance/sonic v1.14.0 // indirect
32 | github.com/bytedance/sonic/loader v0.3.0 // indirect
33 | github.com/cespare/xxhash/v2 v2.3.0 // indirect
34 | github.com/cloudwego/base64x v0.1.6 // indirect
35 | github.com/consensys/gnark-crypto v0.19.2 // indirect
36 | github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
37 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
38 | github.com/ethereum/go-ethereum v1.16.7 // indirect
39 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect
40 | github.com/gin-contrib/sse v1.1.0 // indirect
41 | github.com/go-openapi/jsonpointer v0.19.5 // indirect
42 | github.com/go-openapi/jsonreference v0.20.0 // indirect
43 | github.com/go-openapi/spec v0.20.7 // indirect
44 | github.com/go-openapi/swag v0.22.3 // indirect
45 | github.com/go-playground/locales v0.14.1 // indirect
46 | github.com/go-playground/universal-translator v0.18.1 // indirect
47 | github.com/go-playground/validator/v10 v10.27.0 // indirect
48 | github.com/goccy/go-json v0.10.5 // indirect
49 | github.com/goccy/go-yaml v1.18.0 // indirect
50 | github.com/holiman/uint256 v1.3.2 // indirect
51 | github.com/jinzhu/copier v0.4.0 // indirect
52 | github.com/josharian/intern v1.0.0 // indirect
53 | github.com/json-iterator/go v1.1.12 // indirect
54 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect
55 | github.com/leodido/go-urn v1.4.0 // indirect
56 | github.com/mailru/easyjson v0.7.7 // indirect
57 | github.com/mattn/go-isatty v0.0.20 // indirect
58 | github.com/minio/sha256-simd v1.0.1 // indirect
59 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
60 | github.com/modern-go/reflect2 v1.0.2 // indirect
61 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect
62 | github.com/pkg/errors v0.9.1 // indirect
63 | github.com/prometheus/client_golang v1.19.1 // indirect
64 | github.com/prometheus/client_model v0.6.1 // indirect
65 | github.com/prometheus/common v0.52.2 // indirect
66 | github.com/prometheus/procfs v0.13.0 // indirect
67 | github.com/quic-go/qpack v0.6.0 // indirect
68 | github.com/quic-go/quic-go v0.57.0 // indirect
69 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
70 | github.com/ugorji/go/codec v1.3.0 // indirect
71 | github.com/utxorpc/go-codegen v0.18.1 // indirect
72 | github.com/x448/float16 v0.8.4 // indirect
73 | golang.org/x/arch v0.20.0 // indirect
74 | golang.org/x/crypto v0.45.0 // indirect
75 | golang.org/x/mod v0.29.0 // indirect
76 | golang.org/x/net v0.47.0 // indirect
77 | golang.org/x/sync v0.18.0 // indirect
78 | golang.org/x/sys v0.38.0 // indirect
79 | golang.org/x/text v0.31.0 // indirect
80 | golang.org/x/tools v0.38.0 // indirect
81 | google.golang.org/protobuf v1.36.10 // indirect
82 | gopkg.in/yaml.v3 v3.0.1 // indirect
83 | )
84 |
--------------------------------------------------------------------------------
/openapi/README.md:
--------------------------------------------------------------------------------
1 | # Go API client for openapi
2 |
3 | Cardano Transaction Submit API
4 |
5 | ## Overview
6 | This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [OpenAPI-spec](https://www.openapis.org/) from a remote server, you can easily generate an API client.
7 |
8 | - API version: v0
9 | - Package version: 1.0.0
10 | - Generator version: 7.14.0
11 | - Build package: org.openapitools.codegen.languages.GoClientCodegen
12 | For more information, please visit [https://blinklabs.io](https://blinklabs.io)
13 |
14 | ## Installation
15 |
16 | Install the following dependencies:
17 |
18 | ```sh
19 | go get github.com/stretchr/testify/assert
20 | go get golang.org/x/net/context
21 | ```
22 |
23 | Put the package under your project folder and add the following in import:
24 |
25 | ```go
26 | import openapi "github.com/blinklabs-io/tx-submit-api/openapi"
27 | ```
28 |
29 | To use a proxy, set the environment variable `HTTP_PROXY`:
30 |
31 | ```go
32 | os.Setenv("HTTP_PROXY", "http://proxy_name:proxy_port")
33 | ```
34 |
35 | ## Configuration of Server URL
36 |
37 | Default configuration comes with `Servers` field that contains server objects as defined in the OpenAPI specification.
38 |
39 | ### Select Server Configuration
40 |
41 | For using other server than the one defined on index 0 set context value `openapi.ContextServerIndex` of type `int`.
42 |
43 | ```go
44 | ctx := context.WithValue(context.Background(), openapi.ContextServerIndex, 1)
45 | ```
46 |
47 | ### Templated Server URL
48 |
49 | Templated server URL is formatted using default variables from configuration or from context value `openapi.ContextServerVariables` of type `map[string]string`.
50 |
51 | ```go
52 | ctx := context.WithValue(context.Background(), openapi.ContextServerVariables, map[string]string{
53 | "basePath": "v2",
54 | })
55 | ```
56 |
57 | Note, enum values are always validated and all unused variables are silently ignored.
58 |
59 | ### URLs Configuration per Operation
60 |
61 | Each operation can use different server URL defined using `OperationServers` map in the `Configuration`.
62 | An operation is uniquely identified by `"{classname}Service.{nickname}"` string.
63 | Similar rules for overriding default operation server index and variables applies by using `openapi.ContextOperationServerIndices` and `openapi.ContextOperationServerVariables` context maps.
64 |
65 | ```go
66 | ctx := context.WithValue(context.Background(), openapi.ContextOperationServerIndices, map[string]int{
67 | "{classname}Service.{nickname}": 2,
68 | })
69 | ctx = context.WithValue(context.Background(), openapi.ContextOperationServerVariables, map[string]map[string]string{
70 | "{classname}Service.{nickname}": {
71 | "port": "8443",
72 | },
73 | })
74 | ```
75 |
76 | ## Documentation for API Endpoints
77 |
78 | All URIs are relative to *http://localhost*
79 |
80 | Class | Method | HTTP request | Description
81 | ------------ | ------------- | ------------- | -------------
82 | *DefaultAPI* | [**ApiHastxTxHashGet**](docs/DefaultAPI.md#apihastxtxhashget) | **Get** /api/hastx/{tx_hash} | HasTx
83 | *DefaultAPI* | [**ApiSubmitTxPost**](docs/DefaultAPI.md#apisubmittxpost) | **Post** /api/submit/tx | Submit Tx
84 |
85 |
86 | ## Documentation For Models
87 |
88 |
89 |
90 | ## Documentation For Authorization
91 |
92 | Endpoints do not require authorization.
93 |
94 |
95 | ## Documentation for Utility Methods
96 |
97 | Due to the fact that model structure members are all pointers, this package contains
98 | a number of utility functions to easily obtain pointers to values of basic types.
99 | Each of these functions takes a value of the given basic type and returns a pointer to it:
100 |
101 | * `PtrBool`
102 | * `PtrInt`
103 | * `PtrInt32`
104 | * `PtrInt64`
105 | * `PtrFloat`
106 | * `PtrFloat32`
107 | * `PtrFloat64`
108 | * `PtrString`
109 | * `PtrTime`
110 |
111 | ## Author
112 |
113 | support@blinklabs.io
114 |
115 |
--------------------------------------------------------------------------------
/openapi/docs/DefaultAPI.md:
--------------------------------------------------------------------------------
1 | # \DefaultAPI
2 |
3 | All URIs are relative to *http://localhost*
4 |
5 | Method | HTTP request | Description
6 | ------------- | ------------- | -------------
7 | [**ApiHastxTxHashGet**](DefaultAPI.md#ApiHastxTxHashGet) | **Get** /api/hastx/{tx_hash} | HasTx
8 | [**ApiSubmitTxPost**](DefaultAPI.md#ApiSubmitTxPost) | **Post** /api/submit/tx | Submit Tx
9 |
10 |
11 |
12 | ## ApiHastxTxHashGet
13 |
14 | > string ApiHastxTxHashGet(ctx, txHash).Execute()
15 |
16 | HasTx
17 |
18 |
19 |
20 | ### Example
21 |
22 | ```go
23 | package main
24 |
25 | import (
26 | "context"
27 | "fmt"
28 | "os"
29 | openapiclient "github.com/blinklabs-io/tx-submit-api/openapi"
30 | )
31 |
32 | func main() {
33 | txHash := "txHash_example" // string | Transaction Hash
34 |
35 | configuration := openapiclient.NewConfiguration()
36 | apiClient := openapiclient.NewAPIClient(configuration)
37 | resp, r, err := apiClient.DefaultAPI.ApiHastxTxHashGet(context.Background(), txHash).Execute()
38 | if err != nil {
39 | fmt.Fprintf(os.Stderr, "Error when calling `DefaultAPI.ApiHastxTxHashGet``: %v\n", err)
40 | fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r)
41 | }
42 | // response from `ApiHastxTxHashGet`: string
43 | fmt.Fprintf(os.Stdout, "Response from `DefaultAPI.ApiHastxTxHashGet`: %v\n", resp)
44 | }
45 | ```
46 |
47 | ### Path Parameters
48 |
49 |
50 | Name | Type | Description | Notes
51 | ------------- | ------------- | ------------- | -------------
52 | **ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc.
53 | **txHash** | **string** | Transaction Hash |
54 |
55 | ### Other Parameters
56 |
57 | Other parameters are passed through a pointer to a apiApiHastxTxHashGetRequest struct via the builder pattern
58 |
59 |
60 | Name | Type | Description | Notes
61 | ------------- | ------------- | ------------- | -------------
62 |
63 |
64 | ### Return type
65 |
66 | **string**
67 |
68 | ### Authorization
69 |
70 | No authorization required
71 |
72 | ### HTTP request headers
73 |
74 | - **Content-Type**: Not defined
75 | - **Accept**: application/json
76 |
77 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints)
78 | [[Back to Model list]](../README.md#documentation-for-models)
79 | [[Back to README]](../README.md)
80 |
81 |
82 | ## ApiSubmitTxPost
83 |
84 | > string ApiSubmitTxPost(ctx).ContentType(contentType).Execute()
85 |
86 | Submit Tx
87 |
88 |
89 |
90 | ### Example
91 |
92 | ```go
93 | package main
94 |
95 | import (
96 | "context"
97 | "fmt"
98 | "os"
99 | openapiclient "github.com/blinklabs-io/tx-submit-api/openapi"
100 | )
101 |
102 | func main() {
103 | contentType := "contentType_example" // string | Content type
104 |
105 | configuration := openapiclient.NewConfiguration()
106 | apiClient := openapiclient.NewAPIClient(configuration)
107 | resp, r, err := apiClient.DefaultAPI.ApiSubmitTxPost(context.Background()).ContentType(contentType).Execute()
108 | if err != nil {
109 | fmt.Fprintf(os.Stderr, "Error when calling `DefaultAPI.ApiSubmitTxPost``: %v\n", err)
110 | fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r)
111 | }
112 | // response from `ApiSubmitTxPost`: string
113 | fmt.Fprintf(os.Stdout, "Response from `DefaultAPI.ApiSubmitTxPost`: %v\n", resp)
114 | }
115 | ```
116 |
117 | ### Path Parameters
118 |
119 |
120 |
121 | ### Other Parameters
122 |
123 | Other parameters are passed through a pointer to a apiApiSubmitTxPostRequest struct via the builder pattern
124 |
125 |
126 | Name | Type | Description | Notes
127 | ------------- | ------------- | ------------- | -------------
128 | **contentType** | **string** | Content type |
129 |
130 | ### Return type
131 |
132 | **string**
133 |
134 | ### Authorization
135 |
136 | No authorization required
137 |
138 | ### HTTP request headers
139 |
140 | - **Content-Type**: Not defined
141 | - **Accept**: application/json
142 |
143 | [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints)
144 | [[Back to Model list]](../README.md#documentation-for-models)
145 | [[Back to README]](../README.md)
146 |
147 |
--------------------------------------------------------------------------------
/docs/swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "description": "Cardano Transaction Submit API",
5 | "title": "tx-submit-api",
6 | "contact": {
7 | "name": "Blink Labs Software",
8 | "url": "https://blinklabs.io",
9 | "email": "support@blinklabs.io"
10 | },
11 | "license": {
12 | "name": "Apache 2.0",
13 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
14 | },
15 | "version": "v0"
16 | },
17 | "basePath": "/",
18 | "paths": {
19 | "/api/hastx/{tx_hash}": {
20 | "get": {
21 | "description": "Determine if a given transaction ID exists in the node mempool.",
22 | "produces": [
23 | "application/json"
24 | ],
25 | "summary": "HasTx",
26 | "parameters": [
27 | {
28 | "type": "string",
29 | "description": "Transaction Hash",
30 | "name": "tx_hash",
31 | "in": "path",
32 | "required": true
33 | }
34 | ],
35 | "responses": {
36 | "200": {
37 | "description": "Ok",
38 | "schema": {
39 | "type": "string"
40 | }
41 | },
42 | "400": {
43 | "description": "Bad Request",
44 | "schema": {
45 | "type": "string"
46 | }
47 | },
48 | "404": {
49 | "description": "Not Found",
50 | "schema": {
51 | "type": "string"
52 | }
53 | },
54 | "415": {
55 | "description": "Unsupported Media Type",
56 | "schema": {
57 | "type": "string"
58 | }
59 | },
60 | "500": {
61 | "description": "Server Error",
62 | "schema": {
63 | "type": "string"
64 | }
65 | }
66 | }
67 | }
68 | },
69 | "/api/submit/tx": {
70 | "post": {
71 | "description": "Submit an already serialized transaction to the network.",
72 | "produces": [
73 | "application/json"
74 | ],
75 | "summary": "Submit Tx",
76 | "parameters": [
77 | {
78 | "enum": [
79 | "application/cbor"
80 | ],
81 | "type": "string",
82 | "description": "Content type",
83 | "name": "Content-Type",
84 | "in": "header",
85 | "required": true
86 | }
87 | ],
88 | "responses": {
89 | "202": {
90 | "description": "Ok",
91 | "schema": {
92 | "type": "string"
93 | }
94 | },
95 | "400": {
96 | "description": "Bad Request",
97 | "schema": {
98 | "type": "string"
99 | }
100 | },
101 | "415": {
102 | "description": "Unsupported Media Type",
103 | "schema": {
104 | "type": "string"
105 | }
106 | },
107 | "500": {
108 | "description": "Server Error",
109 | "schema": {
110 | "type": "string"
111 | }
112 | }
113 | }
114 | }
115 | }
116 | }
117 | }
--------------------------------------------------------------------------------
/docs/docs.go:
--------------------------------------------------------------------------------
1 | // Code generated by swaggo/swag. DO NOT EDIT.
2 |
3 | package docs
4 |
5 | import "github.com/swaggo/swag"
6 |
7 | const docTemplate = `{
8 | "schemes": {{ marshal .Schemes }},
9 | "swagger": "2.0",
10 | "info": {
11 | "description": "{{escape .Description}}",
12 | "title": "{{.Title}}",
13 | "contact": {
14 | "name": "Blink Labs Software",
15 | "url": "https://blinklabs.io",
16 | "email": "support@blinklabs.io"
17 | },
18 | "license": {
19 | "name": "Apache 2.0",
20 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
21 | },
22 | "version": "{{.Version}}"
23 | },
24 | "host": "{{.Host}}",
25 | "basePath": "{{.BasePath}}",
26 | "paths": {
27 | "/api/hastx/{tx_hash}": {
28 | "get": {
29 | "description": "Determine if a given transaction ID exists in the node mempool.",
30 | "produces": [
31 | "application/json"
32 | ],
33 | "summary": "HasTx",
34 | "parameters": [
35 | {
36 | "type": "string",
37 | "description": "Transaction Hash",
38 | "name": "tx_hash",
39 | "in": "path",
40 | "required": true
41 | }
42 | ],
43 | "responses": {
44 | "200": {
45 | "description": "Ok",
46 | "schema": {
47 | "type": "string"
48 | }
49 | },
50 | "400": {
51 | "description": "Bad Request",
52 | "schema": {
53 | "type": "string"
54 | }
55 | },
56 | "404": {
57 | "description": "Not Found",
58 | "schema": {
59 | "type": "string"
60 | }
61 | },
62 | "415": {
63 | "description": "Unsupported Media Type",
64 | "schema": {
65 | "type": "string"
66 | }
67 | },
68 | "500": {
69 | "description": "Server Error",
70 | "schema": {
71 | "type": "string"
72 | }
73 | }
74 | }
75 | }
76 | },
77 | "/api/submit/tx": {
78 | "post": {
79 | "description": "Submit an already serialized transaction to the network.",
80 | "produces": [
81 | "application/json"
82 | ],
83 | "summary": "Submit Tx",
84 | "parameters": [
85 | {
86 | "enum": [
87 | "application/cbor"
88 | ],
89 | "type": "string",
90 | "description": "Content type",
91 | "name": "Content-Type",
92 | "in": "header",
93 | "required": true
94 | }
95 | ],
96 | "responses": {
97 | "202": {
98 | "description": "Ok",
99 | "schema": {
100 | "type": "string"
101 | }
102 | },
103 | "400": {
104 | "description": "Bad Request",
105 | "schema": {
106 | "type": "string"
107 | }
108 | },
109 | "415": {
110 | "description": "Unsupported Media Type",
111 | "schema": {
112 | "type": "string"
113 | }
114 | },
115 | "500": {
116 | "description": "Server Error",
117 | "schema": {
118 | "type": "string"
119 | }
120 | }
121 | }
122 | }
123 | }
124 | }
125 | }`
126 |
127 | // SwaggerInfo holds exported Swagger Info so clients can modify it
128 | var SwaggerInfo = &swag.Spec{
129 | Version: "v0",
130 | Host: "",
131 | BasePath: "/",
132 | Schemes: []string{},
133 | Title: "tx-submit-api",
134 | Description: "Cardano Transaction Submit API",
135 | InfoInstanceName: "swagger",
136 | SwaggerTemplate: docTemplate,
137 | }
138 |
139 | func init() {
140 | swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
141 | }
142 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # tx-submit-api
2 |
3 |
4 |

5 |
6 |
7 | Transaction Submission API for Cardano
8 |
9 | A simple HTTP API which accepts a CBOR encoded Cardano transaction as a
10 | payload body and submits it to a Cardano full node using the Ouroboros
11 | LocalTxSubmission Node-to-Client (NtC) protocol.
12 |
13 | ## Usage
14 |
15 | The recommended method of using this application is via the published
16 | container images, coupled with Blink Labs container images for the Cardano
17 | Node.
18 |
19 | ```
20 | docker run -p 8090:8090 ghcr.io/blinklabs-io/tx-submit-api
21 | ```
22 |
23 | Binaries can be executed directly and are available from
24 | [Releases](https://github.com/blinklabs-io/tx-submit-api/releases).
25 |
26 | ```
27 | ./tx-submit-api
28 | ```
29 |
30 | ### Configuration
31 |
32 | Configuration can be done using either a `config.yaml` file or setting
33 | environment variables. Our recommendation is environment variables to adhere
34 | to the 12-factor application philisophy.
35 |
36 | #### Environment variables
37 |
38 | Configuration via environment variables can be broken into two sets of
39 | variables. The first set controls the behavior of the application, while the
40 | second set controls the connection to the Cardano node instance.
41 |
42 | Application configuration:
43 | - `API_LISTEN_ADDRESS` - Address to bind for API calls, all addresses if empty
44 | (default: empty)
45 | - `API_LISTEN_PORT` - Port to bind for API calls (default: 8090)
46 | - `DEBUG_ADDRESS` - Address to bind for pprof debugging (default: localhost)
47 | - `DEBUG_PORT` - Port to bind for pprof debugging, disabled if 0 (default: 0)
48 | - `LOGGING_HEALTHCHECKS` - Log requests to `/healthcheck` endpoint (default: false)
49 | - `LOGGING_LEVEL` - Logging level for log output (default: info)
50 | - `METRICS_LISTEN_ADDRESS` - Address to bind for Prometheus format metrics, all
51 | addresses if empty (default: empty)
52 | - `METRICS_LISTEN_PORT` - Port to bind for metrics (default: 8081)
53 | - `TLS_CERT_FILE_PATH` - SSL certificate to use, requires `TLS_KEY_FILE_PATH`
54 | (default: empty)
55 | - `TLS_KEY_FILE_PATH` - SSL certificate key to use (default: empty)
56 |
57 | Connection to the Cardano node can be performed using specific named network
58 | shortcuts for known network magic configurations. Supported named networks are:
59 |
60 | - mainnet
61 | - preprod
62 | - preview
63 | - testnet
64 |
65 | You can set the network to an empty value and provide your own network magic to
66 | connect to unlisted networks.
67 |
68 | TCP connection to a Cardano node without using an intermediary like SOCAT is
69 | possible using the node address and port. It is up to you to expose the node's
70 | NtC communication socket over TCP. TCP connections are preferred over socket
71 | within the application.
72 |
73 | Cardano node configuration:
74 | - `CARDANO_NETWORK` - Use a named Cardano network (default: mainnet)
75 | - `CARDANO_NODE_NETWORK_MAGIC` - Cardano network magic (default: automatically
76 | determined from named network)
77 | - `CARDANO_NODE_SKIP_CHECK` - Skip the connection test to Cardano Node on start
78 | (default: false)
79 | - `CARDANO_NODE_SOCKET_PATH` - Socket path to Cardano node NtC via UNIX socket
80 | (default: /node-ipc/node.socket)
81 | - `CARDANO_NODE_SOCKET_TCP_HOST` - Address to Cardano node NtC via TCP
82 | (default: unset)
83 | - `CARDANO_NODE_SOCKET_TCP_PORT` - Port to Cardano node NtC via TCP (default:
84 | unset)
85 | - `CARDANO_NODE_SOCKET_TIMEOUT` - Sets a timeout in seconds for waiting on
86 | requests to the Cardano node (default: 30)
87 |
88 | ### Connecting to a cardano-node
89 |
90 | You can connect to either a cardano-node running locally on the host or a
91 | container running either `inputoutput/cardano-node` or
92 | `blinklabs-io/cardano-node` by mapping in the correct paths and setting the
93 | environment variables or configuration options to match.
94 |
95 | #### Together with blinklabs-io/cardano-node in Docker
96 |
97 | Use Docker to run both cardano-node and tx-submit-api with Docker
98 | volumes for blockchain storage and node-ipc.
99 |
100 | ```
101 | # Start node
102 | docker run --detach \
103 | --name cardano-node \
104 | -v node-data:/opt/cardano/data \
105 | -v node-ipc:/opt/cardano/ipc \
106 | -p 3001:3001 \
107 | ghcr.io/blinklabs-io/cardano-node run
108 |
109 | # Start submit-api
110 | docker run --detach \
111 | --name tx-submit-api
112 | -v node-ipc:/node-ipc \
113 | -p 8090:8090 \
114 | ghcr.io/blinklabs-io/tx-submit-api
115 | ```
116 |
117 | #### Using a local cardano-node
118 |
119 | Use the local path when mapping the node-ipc volume into the container to use
120 | a local cardano-node.
121 |
122 | ```
123 | # Start submit-api
124 | docker run --detach \
125 | --name tx-submit-api \
126 | -v /opt/cardano/ipc:/node-ipc \
127 | -p 8090:8090 \
128 | ghcr.io/blinklabs-io/tx-submit-api
129 | ```
130 |
131 | ### Sending transactions
132 |
133 | This implementation shares an API spec with IOHK's Haskell implementation. The
134 | same instructions apply. Follow the steps to
135 | [build and submit a transaction](https://github.com/input-output-hk/cardano-node/tree/master/cardano-submit-api#build-and-submit-a-transaction)
136 |
137 | ```
138 | # Submit a binary tx.signed.cbor signed CBOR encoded transaction binary file
139 | curl -X POST \
140 | --header "Content-Type: application/cbor" \
141 | --data-binary @tx.signed.cbor \
142 | http://localhost:8090/api/submit/tx
143 | ```
144 |
145 | ### Metrics UI
146 |
147 | There is a metrics web user interface running on the service's API port.
148 |
149 | Connect to [http://localhost:8090/ui/](http://localhost:8090/ui/) with your
150 | browser to view it.
151 |
152 |
153 |

154 |
155 |
156 | ## Development
157 |
158 | There is a Makefile to provide some simple helpers.
159 |
160 | Run from checkout:
161 | ```
162 | go run .
163 | ```
164 |
165 | Create a binary:
166 | ```
167 | make
168 | ```
169 |
170 | Create a docker image:
171 | ```
172 | make image
173 | ```
174 |
--------------------------------------------------------------------------------
/internal/config/config.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Blink Labs Software
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package config
16 |
17 | import (
18 | "errors"
19 | "fmt"
20 | "os"
21 |
22 | ouroboros "github.com/blinklabs-io/gouroboros"
23 | "github.com/kelseyhightower/envconfig"
24 | "gopkg.in/yaml.v2"
25 | )
26 |
27 | type Config struct {
28 | Logging LoggingConfig `yaml:"logging"`
29 | Api ApiConfig `yaml:"api"`
30 | Metrics MetricsConfig `yaml:"metrics"`
31 | Debug DebugConfig `yaml:"debug"`
32 | Node NodeConfig `yaml:"node"`
33 | Tls TlsConfig `yaml:"tls"`
34 | }
35 |
36 | type LoggingConfig struct {
37 | Healthchecks bool `yaml:"healthchecks" envconfig:"LOGGING_HEALTHCHECKS"`
38 | Level string `yaml:"level" envconfig:"LOGGING_LEVEL"`
39 | }
40 |
41 | type ApiConfig struct {
42 | ListenAddress string `yaml:"address" envconfig:"API_LISTEN_ADDRESS"`
43 | ListenPort uint `yaml:"port" envconfig:"API_LISTEN_PORT"`
44 | }
45 |
46 | type DebugConfig struct {
47 | ListenAddress string `yaml:"address" envconfig:"DEBUG_ADDRESS"`
48 | ListenPort uint `yaml:"port" envconfig:"DEBUG_PORT"`
49 | }
50 |
51 | type MetricsConfig struct {
52 | ListenAddress string `yaml:"address" envconfig:"METRICS_LISTEN_ADDRESS"`
53 | ListenPort uint `yaml:"port" envconfig:"METRICS_LISTEN_PORT"`
54 | }
55 |
56 | type NodeConfig struct {
57 | Network string `yaml:"network" envconfig:"CARDANO_NETWORK"`
58 | NetworkMagic uint32 `yaml:"networkMagic" envconfig:"CARDANO_NODE_NETWORK_MAGIC"`
59 | Address string `yaml:"address" envconfig:"CARDANO_NODE_SOCKET_TCP_HOST"`
60 | Port uint `yaml:"port" envconfig:"CARDANO_NODE_SOCKET_TCP_PORT"`
61 | SkipCheck bool `yaml:"skipCheck" envconfig:"CARDANO_NODE_SKIP_CHECK"`
62 | SocketPath string `yaml:"socketPath" envconfig:"CARDANO_NODE_SOCKET_PATH"`
63 | Timeout uint `yaml:"timeout" envconfig:"CARDANO_NODE_SOCKET_TIMEOUT"`
64 | }
65 |
66 | type TlsConfig struct {
67 | CertFilePath string `yaml:"certFilePath" envconfig:"TLS_CERT_FILE_PATH"`
68 | KeyFilePath string `yaml:"keyFilePath" envconfig:"TLS_KEY_FILE_PATH"`
69 | }
70 |
71 | // Singleton config instance with default values
72 | var globalConfig = &Config{
73 | Logging: LoggingConfig{
74 | Level: "info",
75 | Healthchecks: false,
76 | },
77 | Api: ApiConfig{
78 | ListenAddress: "0.0.0.0",
79 | ListenPort: 8090,
80 | },
81 | Debug: DebugConfig{
82 | ListenAddress: "localhost",
83 | ListenPort: 0,
84 | },
85 | Metrics: MetricsConfig{
86 | ListenAddress: "",
87 | ListenPort: 8081,
88 | },
89 | Node: NodeConfig{
90 | Network: "mainnet",
91 | SocketPath: "/node-ipc/node.socket",
92 | Timeout: 30,
93 | },
94 | }
95 |
96 | func Load(configFile string) (*Config, error) {
97 | // Load config file as YAML if provided
98 | if configFile != "" {
99 | buf, err := os.ReadFile(configFile)
100 | if err != nil {
101 | return nil, fmt.Errorf("error reading config file: %w", err)
102 | }
103 | err = yaml.Unmarshal(buf, globalConfig)
104 | if err != nil {
105 | return nil, fmt.Errorf("error parsing config file: %w", err)
106 | }
107 | }
108 | // Load config values from environment variables
109 | // We use "dummy" as the app name here to (mostly) prevent picking up env
110 | // vars that we hadn't explicitly specified in annotations above
111 | err := envconfig.Process("dummy", globalConfig)
112 | if err != nil {
113 | return nil, fmt.Errorf("error processing environment: %w", err)
114 | }
115 | if err := globalConfig.populateNetworkMagic(); err != nil {
116 | return nil, err
117 | }
118 | if err := globalConfig.checkNode(); err != nil {
119 | return nil, err
120 | }
121 | return globalConfig, nil
122 | }
123 |
124 | // Return global config instance
125 | func GetConfig() *Config {
126 | return globalConfig
127 | }
128 |
129 | func (c *Config) populateNetworkMagic() error {
130 | if c.Node.NetworkMagic == 0 {
131 | if c.Node.Network != "" {
132 | network, ok := ouroboros.NetworkByName(c.Node.Network)
133 | if !ok {
134 | return fmt.Errorf("unknown network: %s", c.Node.Network)
135 | }
136 | c.Node.NetworkMagic = uint32(network.NetworkMagic)
137 | return nil
138 | }
139 | }
140 | return nil
141 | }
142 |
143 | func (c *Config) checkNode() error {
144 | if c.Node.SkipCheck {
145 | return nil
146 | }
147 | // Connect to cardano-node
148 | oConn, err := ouroboros.NewConnection(
149 | ouroboros.WithNetworkMagic(uint32(c.Node.NetworkMagic)),
150 | ouroboros.WithNodeToNode(false),
151 | )
152 | if err != nil {
153 | return fmt.Errorf("failure creating Ouroboros connection: %w", err)
154 | }
155 |
156 | if c.Node.Address != "" && c.Node.Port > 0 {
157 | // Connect to TCP port
158 | if err := oConn.Dial("tcp", fmt.Sprintf("%s:%d", c.Node.Address, c.Node.Port)); err != nil {
159 | return fmt.Errorf("failure connecting to node via TCP: %w", err)
160 | }
161 | } else if c.Node.SocketPath != "" {
162 | // Check that node socket path exists
163 | if _, err := os.Stat(c.Node.SocketPath); err != nil {
164 | if os.IsNotExist(err) {
165 | return fmt.Errorf("node socket path does not exist: %s", c.Node.SocketPath)
166 | } else {
167 | return fmt.Errorf("unknown error checking if node socket path exists: %w", err)
168 | }
169 | }
170 | if err := oConn.Dial("unix", c.Node.SocketPath); err != nil {
171 | return fmt.Errorf("failure connecting to node via UNIX socket: %w", err)
172 | }
173 | } else {
174 | return errors.New("you must specify either the UNIX socket path or the address/port for your cardano-node")
175 | }
176 | // Close Ouroboros connection
177 | oConn.Close()
178 | return nil
179 | }
180 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: publish
2 |
3 | on:
4 | push:
5 | branches: ['main']
6 | tags:
7 | - 'v*.*.*'
8 |
9 | concurrency: ${{ github.ref }}
10 |
11 | jobs:
12 | create-draft-release:
13 | runs-on: ubuntu-latest
14 | permissions:
15 | contents: write
16 | outputs:
17 | RELEASE_ID: ${{ steps.create-release.outputs.result }}
18 | steps:
19 | - run: "echo \"RELEASE_TAG=${GITHUB_REF#refs/tags/}\" >> $GITHUB_ENV"
20 | - uses: actions/github-script@v8
21 | id: create-release
22 | if: startsWith(github.ref, 'refs/tags/')
23 | with:
24 | github-token: ${{ secrets.GITHUB_TOKEN }}
25 | result-encoding: string
26 | script: |
27 | try {
28 | const response = await github.rest.repos.createRelease({
29 | draft: true,
30 | generate_release_notes: true,
31 | name: process.env.RELEASE_TAG,
32 | owner: context.repo.owner,
33 | prerelease: false,
34 | repo: context.repo.repo,
35 | tag_name: process.env.RELEASE_TAG,
36 | });
37 |
38 | return response.data.id;
39 | } catch (error) {
40 | core.setFailed(error.message);
41 | }
42 |
43 | build-binaries:
44 | strategy:
45 | matrix:
46 | os: [linux, darwin, freebsd, windows]
47 | arch: [amd64, arm64]
48 | runs-on: ubuntu-latest
49 | needs: [create-draft-release]
50 | permissions:
51 | actions: write
52 | attestations: write
53 | checks: write
54 | contents: write
55 | id-token: write
56 | packages: write
57 | statuses: write
58 | steps:
59 | - run: "echo \"RELEASE_TAG=${GITHUB_REF#refs/tags/}\" >> $GITHUB_ENV"
60 | - uses: actions/checkout@v6.0.1
61 | with:
62 | fetch-depth: '0'
63 | - uses: actions/setup-go@v6
64 | with:
65 | go-version: 1.25.x
66 | - name: Build binary
67 | run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} make build
68 | - name: Upload release asset
69 | if: startsWith(github.ref, 'refs/tags/')
70 | run: |
71 | _filename=tx-submit-api-${{ env.RELEASE_TAG }}-${{ matrix.os }}-${{ matrix.arch }}
72 | if [[ ${{ matrix.os }} == windows ]]; then
73 | _filename=${_filename}.exe
74 | fi
75 | cp tx-submit-api ${_filename}
76 | curl \
77 | -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
78 | -H "Content-Type: application/octet-stream" \
79 | --data-binary @${_filename} \
80 | https://uploads.github.com/repos/${{ github.repository_owner }}/tx-submit-api/releases/${{ needs.create-draft-release.outputs.RELEASE_ID }}/assets?name=${_filename}
81 | - name: Attest binary
82 | uses: actions/attest-build-provenance@v3
83 | with:
84 | subject-path: 'tx-submit-api'
85 |
86 | build-images:
87 | runs-on: ubuntu-latest
88 | needs: [create-draft-release]
89 | permissions:
90 | actions: write
91 | attestations: write
92 | checks: write
93 | contents: write
94 | id-token: write
95 | packages: write
96 | statuses: write
97 | steps:
98 | - run: "echo \"RELEASE_TAG=${GITHUB_REF#refs/tags/}\" >> $GITHUB_ENV"
99 | - uses: actions/checkout@v6.0.1
100 | with:
101 | fetch-depth: '0'
102 | - name: Set up QEMU
103 | uses: docker/setup-qemu-action@v3
104 | - name: Set up Docker Buildx
105 | uses: docker/setup-buildx-action@v3
106 | - name: Login to Docker Hub
107 | uses: docker/login-action@v3
108 | with:
109 | username: blinklabs
110 | password: ${{ secrets.DOCKER_PASSWORD }} # uses token
111 | - name: Login to GHCR
112 | uses: docker/login-action@v3
113 | with:
114 | username: ${{ github.repository_owner }}
115 | password: ${{ secrets.GITHUB_TOKEN }}
116 | registry: ghcr.io
117 | - id: meta
118 | uses: docker/metadata-action@v5
119 | with:
120 | images: |
121 | blinklabs/tx-submit-api
122 | ghcr.io/${{ github.repository }}
123 | tags: |
124 | # Only version, no revision
125 | type=match,pattern=v(.*)-(.*),group=1
126 | # branch
127 | type=ref,event=branch
128 | # semver
129 | type=semver,pattern={{version}}
130 | - name: Build images
131 | uses: docker/build-push-action@v6
132 | id: push
133 | with:
134 | outputs: "type=registry,push=true"
135 | platforms: linux/amd64,linux/arm64
136 | tags: ${{ steps.meta.outputs.tags }}
137 | labels: ${{ steps.meta.outputs.labels }}
138 | - name: Attest Docker Hub image
139 | uses: actions/attest-build-provenance@v3
140 | with:
141 | subject-name: index.docker.io/blinklabs/tx-submit-api
142 | subject-digest: ${{ steps.push.outputs.digest }}
143 | push-to-registry: true
144 | - name: Attest GHCR image
145 | uses: actions/attest-build-provenance@v3
146 | with:
147 | subject-name: ghcr.io/${{ github.repository }}
148 | subject-digest: ${{ steps.push.outputs.digest }}
149 | push-to-registry: true
150 | # Update Docker Hub from README
151 | - name: Docker Hub Description
152 | uses: peter-evans/dockerhub-description@v5
153 | with:
154 | username: blinklabs
155 | password: ${{ secrets.DOCKER_PASSWORD }}
156 | repository: blinklabs/tx-submit-api
157 | readme-filepath: ./README.md
158 | short-description: "A Go implementation of the Cardano Submit API service"
159 |
160 | finalize-release:
161 | runs-on: ubuntu-latest
162 | permissions:
163 | contents: write
164 | needs: [create-draft-release, build-binaries, build-images]
165 | steps:
166 | - uses: actions/github-script@v8
167 | if: startsWith(github.ref, 'refs/tags/')
168 | with:
169 | github-token: ${{ secrets.GITHUB_TOKEN }}
170 | script: |
171 | try {
172 | await github.rest.repos.updateRelease({
173 | owner: context.repo.owner,
174 | repo: context.repo.repo,
175 | release_id: ${{ needs.create-draft-release.outputs.RELEASE_ID }},
176 | draft: false,
177 | });
178 | } catch (error) {
179 | core.setFailed(error.message);
180 | }
181 |
--------------------------------------------------------------------------------
/openapi/configuration.go:
--------------------------------------------------------------------------------
1 | /*
2 | tx-submit-api
3 |
4 | Cardano Transaction Submit API
5 |
6 | API version: v0
7 | Contact: support@blinklabs.io
8 | */
9 |
10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
11 |
12 | package openapi
13 |
14 | import (
15 | "context"
16 | "fmt"
17 | "net/http"
18 | "strings"
19 | )
20 |
21 | // contextKeys are used to identify the type of value in the context.
22 | // Since these are string, it is possible to get a short description of the
23 | // context key for logging and debugging using key.String().
24 |
25 | type contextKey string
26 |
27 | func (c contextKey) String() string {
28 | return "auth " + string(c)
29 | }
30 |
31 | var (
32 | // ContextServerIndex uses a server configuration from the index.
33 | ContextServerIndex = contextKey("serverIndex")
34 |
35 | // ContextOperationServerIndices uses a server configuration from the index mapping.
36 | ContextOperationServerIndices = contextKey("serverOperationIndices")
37 |
38 | // ContextServerVariables overrides a server configuration variables.
39 | ContextServerVariables = contextKey("serverVariables")
40 |
41 | // ContextOperationServerVariables overrides a server configuration variables using operation specific values.
42 | ContextOperationServerVariables = contextKey("serverOperationVariables")
43 | )
44 |
45 | // BasicAuth provides basic http authentication to a request passed via context using ContextBasicAuth
46 | type BasicAuth struct {
47 | UserName string `json:"userName,omitempty"`
48 | Password string `json:"password,omitempty"`
49 | }
50 |
51 | // APIKey provides API key based authentication to a request passed via context using ContextAPIKey
52 | type APIKey struct {
53 | Key string
54 | Prefix string
55 | }
56 |
57 | // ServerVariable stores the information about a server variable
58 | type ServerVariable struct {
59 | Description string
60 | DefaultValue string
61 | EnumValues []string
62 | }
63 |
64 | // ServerConfiguration stores the information about a server
65 | type ServerConfiguration struct {
66 | URL string
67 | Description string
68 | Variables map[string]ServerVariable
69 | }
70 |
71 | // ServerConfigurations stores multiple ServerConfiguration items
72 | type ServerConfigurations []ServerConfiguration
73 |
74 | // Configuration stores the configuration of the API client
75 | type Configuration struct {
76 | Host string `json:"host,omitempty"`
77 | Scheme string `json:"scheme,omitempty"`
78 | DefaultHeader map[string]string `json:"defaultHeader,omitempty"`
79 | UserAgent string `json:"userAgent,omitempty"`
80 | Debug bool `json:"debug,omitempty"`
81 | Servers ServerConfigurations
82 | OperationServers map[string]ServerConfigurations
83 | HTTPClient *http.Client
84 | }
85 |
86 | // NewConfiguration returns a new Configuration object
87 | func NewConfiguration() *Configuration {
88 | cfg := &Configuration{
89 | DefaultHeader: make(map[string]string),
90 | UserAgent: "OpenAPI-Generator/1.0.0/go",
91 | Debug: false,
92 | Servers: ServerConfigurations{
93 | {
94 | URL: "",
95 | Description: "No description provided",
96 | },
97 | },
98 | OperationServers: map[string]ServerConfigurations{},
99 | }
100 | return cfg
101 | }
102 |
103 | // AddDefaultHeader adds a new HTTP header to the default header in the request
104 | func (c *Configuration) AddDefaultHeader(key string, value string) {
105 | c.DefaultHeader[key] = value
106 | }
107 |
108 | // URL formats template on a index using given variables
109 | func (sc ServerConfigurations) URL(
110 | index int,
111 | variables map[string]string,
112 | ) (string, error) {
113 | if index < 0 || len(sc) <= index {
114 | return "", fmt.Errorf("index %v out of range %v", index, len(sc)-1)
115 | }
116 | server := sc[index]
117 | url := server.URL
118 |
119 | // go through variables and replace placeholders
120 | for name, variable := range server.Variables {
121 | if value, ok := variables[name]; ok {
122 | found := bool(len(variable.EnumValues) == 0)
123 | for _, enumValue := range variable.EnumValues {
124 | if value == enumValue {
125 | found = true
126 | }
127 | }
128 | if !found {
129 | return "", fmt.Errorf(
130 | "the variable %s in the server URL has invalid value %v. Must be %v",
131 | name,
132 | value,
133 | variable.EnumValues,
134 | )
135 | }
136 | url = strings.Replace(url, "{"+name+"}", value, -1)
137 | } else {
138 | url = strings.Replace(url, "{"+name+"}", variable.DefaultValue, -1)
139 | }
140 | }
141 | return url, nil
142 | }
143 |
144 | // ServerURL returns URL based on server settings
145 | func (c *Configuration) ServerURL(
146 | index int,
147 | variables map[string]string,
148 | ) (string, error) {
149 | return c.Servers.URL(index, variables)
150 | }
151 |
152 | func getServerIndex(ctx context.Context) (int, error) {
153 | si := ctx.Value(ContextServerIndex)
154 | if si != nil {
155 | if index, ok := si.(int); ok {
156 | return index, nil
157 | }
158 | return 0, reportError("Invalid type %T should be int", si)
159 | }
160 | return 0, nil
161 | }
162 |
163 | func getServerOperationIndex(
164 | ctx context.Context,
165 | endpoint string,
166 | ) (int, error) {
167 | osi := ctx.Value(ContextOperationServerIndices)
168 | if osi != nil {
169 | if operationIndices, ok := osi.(map[string]int); !ok {
170 | return 0, reportError(
171 | "Invalid type %T should be map[string]int",
172 | osi,
173 | )
174 | } else {
175 | index, ok := operationIndices[endpoint]
176 | if ok {
177 | return index, nil
178 | }
179 | }
180 | }
181 | return getServerIndex(ctx)
182 | }
183 |
184 | func getServerVariables(ctx context.Context) (map[string]string, error) {
185 | sv := ctx.Value(ContextServerVariables)
186 | if sv != nil {
187 | if variables, ok := sv.(map[string]string); ok {
188 | return variables, nil
189 | }
190 | return nil, reportError(
191 | "ctx value of ContextServerVariables has invalid type %T should be map[string]string",
192 | sv,
193 | )
194 | }
195 | return nil, nil
196 | }
197 |
198 | func getServerOperationVariables(
199 | ctx context.Context,
200 | endpoint string,
201 | ) (map[string]string, error) {
202 | osv := ctx.Value(ContextOperationServerVariables)
203 | if osv != nil {
204 | if operationVariables, ok := osv.(map[string]map[string]string); !ok {
205 | return nil, reportError(
206 | "ctx value of ContextOperationServerVariables has invalid type %T should be map[string]map[string]string",
207 | osv,
208 | )
209 | } else {
210 | variables, ok := operationVariables[endpoint]
211 | if ok {
212 | return variables, nil
213 | }
214 | }
215 | }
216 | return getServerVariables(ctx)
217 | }
218 |
219 | // ServerURLWithContext returns a new server URL given an endpoint
220 | func (c *Configuration) ServerURLWithContext(
221 | ctx context.Context,
222 | endpoint string,
223 | ) (string, error) {
224 | sc, ok := c.OperationServers[endpoint]
225 | if !ok {
226 | sc = c.Servers
227 | }
228 |
229 | if ctx == nil {
230 | return sc.URL(0, nil)
231 | }
232 |
233 | index, err := getServerOperationIndex(ctx, endpoint)
234 | if err != nil {
235 | return "", err
236 | }
237 |
238 | variables, err := getServerOperationVariables(ctx, endpoint)
239 | if err != nil {
240 | return "", err
241 | }
242 |
243 | return sc.URL(index, variables)
244 | }
245 |
--------------------------------------------------------------------------------
/openapi/utils.go:
--------------------------------------------------------------------------------
1 | /*
2 | tx-submit-api
3 |
4 | Cardano Transaction Submit API
5 |
6 | API version: v0
7 | Contact: support@blinklabs.io
8 | */
9 |
10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
11 |
12 | package openapi
13 |
14 | import (
15 | "bytes"
16 | "encoding/json"
17 | "fmt"
18 | "reflect"
19 | "time"
20 | )
21 |
22 | // PtrBool is a helper routine that returns a pointer to given boolean value.
23 | func PtrBool(v bool) *bool { return &v }
24 |
25 | // PtrInt is a helper routine that returns a pointer to given integer value.
26 | func PtrInt(v int) *int { return &v }
27 |
28 | // PtrInt32 is a helper routine that returns a pointer to given integer value.
29 | func PtrInt32(v int32) *int32 { return &v }
30 |
31 | // PtrInt64 is a helper routine that returns a pointer to given integer value.
32 | func PtrInt64(v int64) *int64 { return &v }
33 |
34 | // PtrFloat32 is a helper routine that returns a pointer to given float value.
35 | func PtrFloat32(v float32) *float32 { return &v }
36 |
37 | // PtrFloat64 is a helper routine that returns a pointer to given float value.
38 | func PtrFloat64(v float64) *float64 { return &v }
39 |
40 | // PtrString is a helper routine that returns a pointer to given string value.
41 | func PtrString(v string) *string { return &v }
42 |
43 | // PtrTime is helper routine that returns a pointer to given Time value.
44 | func PtrTime(v time.Time) *time.Time { return &v }
45 |
46 | type NullableBool struct {
47 | value *bool
48 | isSet bool
49 | }
50 |
51 | func (v NullableBool) Get() *bool {
52 | return v.value
53 | }
54 |
55 | func (v *NullableBool) Set(val *bool) {
56 | v.value = val
57 | v.isSet = true
58 | }
59 |
60 | func (v NullableBool) IsSet() bool {
61 | return v.isSet
62 | }
63 |
64 | func (v *NullableBool) Unset() {
65 | v.value = nil
66 | v.isSet = false
67 | }
68 |
69 | func NewNullableBool(val *bool) *NullableBool {
70 | return &NullableBool{value: val, isSet: true}
71 | }
72 |
73 | func (v NullableBool) MarshalJSON() ([]byte, error) {
74 | return json.Marshal(v.value)
75 | }
76 |
77 | func (v *NullableBool) UnmarshalJSON(src []byte) error {
78 | v.isSet = true
79 | return json.Unmarshal(src, &v.value)
80 | }
81 |
82 | type NullableInt struct {
83 | value *int
84 | isSet bool
85 | }
86 |
87 | func (v NullableInt) Get() *int {
88 | return v.value
89 | }
90 |
91 | func (v *NullableInt) Set(val *int) {
92 | v.value = val
93 | v.isSet = true
94 | }
95 |
96 | func (v NullableInt) IsSet() bool {
97 | return v.isSet
98 | }
99 |
100 | func (v *NullableInt) Unset() {
101 | v.value = nil
102 | v.isSet = false
103 | }
104 |
105 | func NewNullableInt(val *int) *NullableInt {
106 | return &NullableInt{value: val, isSet: true}
107 | }
108 |
109 | func (v NullableInt) MarshalJSON() ([]byte, error) {
110 | return json.Marshal(v.value)
111 | }
112 |
113 | func (v *NullableInt) UnmarshalJSON(src []byte) error {
114 | v.isSet = true
115 | return json.Unmarshal(src, &v.value)
116 | }
117 |
118 | type NullableInt32 struct {
119 | value *int32
120 | isSet bool
121 | }
122 |
123 | func (v NullableInt32) Get() *int32 {
124 | return v.value
125 | }
126 |
127 | func (v *NullableInt32) Set(val *int32) {
128 | v.value = val
129 | v.isSet = true
130 | }
131 |
132 | func (v NullableInt32) IsSet() bool {
133 | return v.isSet
134 | }
135 |
136 | func (v *NullableInt32) Unset() {
137 | v.value = nil
138 | v.isSet = false
139 | }
140 |
141 | func NewNullableInt32(val *int32) *NullableInt32 {
142 | return &NullableInt32{value: val, isSet: true}
143 | }
144 |
145 | func (v NullableInt32) MarshalJSON() ([]byte, error) {
146 | return json.Marshal(v.value)
147 | }
148 |
149 | func (v *NullableInt32) UnmarshalJSON(src []byte) error {
150 | v.isSet = true
151 | return json.Unmarshal(src, &v.value)
152 | }
153 |
154 | type NullableInt64 struct {
155 | value *int64
156 | isSet bool
157 | }
158 |
159 | func (v NullableInt64) Get() *int64 {
160 | return v.value
161 | }
162 |
163 | func (v *NullableInt64) Set(val *int64) {
164 | v.value = val
165 | v.isSet = true
166 | }
167 |
168 | func (v NullableInt64) IsSet() bool {
169 | return v.isSet
170 | }
171 |
172 | func (v *NullableInt64) Unset() {
173 | v.value = nil
174 | v.isSet = false
175 | }
176 |
177 | func NewNullableInt64(val *int64) *NullableInt64 {
178 | return &NullableInt64{value: val, isSet: true}
179 | }
180 |
181 | func (v NullableInt64) MarshalJSON() ([]byte, error) {
182 | return json.Marshal(v.value)
183 | }
184 |
185 | func (v *NullableInt64) UnmarshalJSON(src []byte) error {
186 | v.isSet = true
187 | return json.Unmarshal(src, &v.value)
188 | }
189 |
190 | type NullableFloat32 struct {
191 | value *float32
192 | isSet bool
193 | }
194 |
195 | func (v NullableFloat32) Get() *float32 {
196 | return v.value
197 | }
198 |
199 | func (v *NullableFloat32) Set(val *float32) {
200 | v.value = val
201 | v.isSet = true
202 | }
203 |
204 | func (v NullableFloat32) IsSet() bool {
205 | return v.isSet
206 | }
207 |
208 | func (v *NullableFloat32) Unset() {
209 | v.value = nil
210 | v.isSet = false
211 | }
212 |
213 | func NewNullableFloat32(val *float32) *NullableFloat32 {
214 | return &NullableFloat32{value: val, isSet: true}
215 | }
216 |
217 | func (v NullableFloat32) MarshalJSON() ([]byte, error) {
218 | return json.Marshal(v.value)
219 | }
220 |
221 | func (v *NullableFloat32) UnmarshalJSON(src []byte) error {
222 | v.isSet = true
223 | return json.Unmarshal(src, &v.value)
224 | }
225 |
226 | type NullableFloat64 struct {
227 | value *float64
228 | isSet bool
229 | }
230 |
231 | func (v NullableFloat64) Get() *float64 {
232 | return v.value
233 | }
234 |
235 | func (v *NullableFloat64) Set(val *float64) {
236 | v.value = val
237 | v.isSet = true
238 | }
239 |
240 | func (v NullableFloat64) IsSet() bool {
241 | return v.isSet
242 | }
243 |
244 | func (v *NullableFloat64) Unset() {
245 | v.value = nil
246 | v.isSet = false
247 | }
248 |
249 | func NewNullableFloat64(val *float64) *NullableFloat64 {
250 | return &NullableFloat64{value: val, isSet: true}
251 | }
252 |
253 | func (v NullableFloat64) MarshalJSON() ([]byte, error) {
254 | return json.Marshal(v.value)
255 | }
256 |
257 | func (v *NullableFloat64) UnmarshalJSON(src []byte) error {
258 | v.isSet = true
259 | return json.Unmarshal(src, &v.value)
260 | }
261 |
262 | type NullableString struct {
263 | value *string
264 | isSet bool
265 | }
266 |
267 | func (v NullableString) Get() *string {
268 | return v.value
269 | }
270 |
271 | func (v *NullableString) Set(val *string) {
272 | v.value = val
273 | v.isSet = true
274 | }
275 |
276 | func (v NullableString) IsSet() bool {
277 | return v.isSet
278 | }
279 |
280 | func (v *NullableString) Unset() {
281 | v.value = nil
282 | v.isSet = false
283 | }
284 |
285 | func NewNullableString(val *string) *NullableString {
286 | return &NullableString{value: val, isSet: true}
287 | }
288 |
289 | func (v NullableString) MarshalJSON() ([]byte, error) {
290 | return json.Marshal(v.value)
291 | }
292 |
293 | func (v *NullableString) UnmarshalJSON(src []byte) error {
294 | v.isSet = true
295 | return json.Unmarshal(src, &v.value)
296 | }
297 |
298 | type NullableTime struct {
299 | value *time.Time
300 | isSet bool
301 | }
302 |
303 | func (v NullableTime) Get() *time.Time {
304 | return v.value
305 | }
306 |
307 | func (v *NullableTime) Set(val *time.Time) {
308 | v.value = val
309 | v.isSet = true
310 | }
311 |
312 | func (v NullableTime) IsSet() bool {
313 | return v.isSet
314 | }
315 |
316 | func (v *NullableTime) Unset() {
317 | v.value = nil
318 | v.isSet = false
319 | }
320 |
321 | func NewNullableTime(val *time.Time) *NullableTime {
322 | return &NullableTime{value: val, isSet: true}
323 | }
324 |
325 | func (v NullableTime) MarshalJSON() ([]byte, error) {
326 | return json.Marshal(v.value)
327 | }
328 |
329 | func (v *NullableTime) UnmarshalJSON(src []byte) error {
330 | v.isSet = true
331 | return json.Unmarshal(src, &v.value)
332 | }
333 |
334 | // IsNil checks if an input is nil
335 | func IsNil(i interface{}) bool {
336 | if i == nil {
337 | return true
338 | }
339 | switch reflect.TypeOf(i).Kind() {
340 | case reflect.Chan,
341 | reflect.Func,
342 | reflect.Map,
343 | reflect.Ptr,
344 | reflect.UnsafePointer,
345 | reflect.Interface,
346 | reflect.Slice:
347 | return reflect.ValueOf(i).IsNil()
348 | case reflect.Array:
349 | return reflect.ValueOf(i).IsZero()
350 | }
351 | return false
352 | }
353 |
354 | type MappedNullable interface {
355 | ToMap() (map[string]interface{}, error)
356 | }
357 |
358 | // A wrapper for strict JSON decoding
359 | func newStrictDecoder(data []byte) *json.Decoder {
360 | dec := json.NewDecoder(bytes.NewBuffer(data))
361 | dec.DisallowUnknownFields()
362 | return dec
363 | }
364 |
365 | // Prevent trying to import "fmt"
366 | func reportError(format string, a ...interface{}) error {
367 | return fmt.Errorf(format, a...)
368 | }
369 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2022 CloudStruct, LLC.
190 | Copyright 2023 Blink Labs Software
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/internal/api/api.go:
--------------------------------------------------------------------------------
1 | // Copyright 2025 Blink Labs Software
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package api
16 |
17 | import (
18 | "embed"
19 | "errors"
20 | "fmt"
21 | "io"
22 | "io/fs"
23 | "net/http"
24 |
25 | ouroboros "github.com/blinklabs-io/gouroboros"
26 | "github.com/blinklabs-io/gouroboros/protocol/localtxsubmission"
27 | _ "github.com/blinklabs-io/tx-submit-api/docs" // docs is generated by Swag CLI
28 | "github.com/blinklabs-io/tx-submit-api/internal/config"
29 | "github.com/blinklabs-io/tx-submit-api/internal/logging"
30 | "github.com/blinklabs-io/tx-submit-api/submit"
31 | "github.com/fxamacker/cbor/v2"
32 | cors "github.com/gin-contrib/cors"
33 | "github.com/gin-gonic/gin"
34 | "github.com/penglongli/gin-metrics/ginmetrics"
35 | swaggerFiles "github.com/swaggo/files" // swagger embed files
36 | ginSwagger "github.com/swaggo/gin-swagger" // gin-swagger middleware
37 | )
38 |
39 | //go:embed static
40 | var staticFS embed.FS
41 |
42 | // @title tx-submit-api
43 | // @version v0
44 | // @description Cardano Transaction Submit API
45 | // @BasePath /
46 | // @contact.name Blink Labs Software
47 | // @contact.url https://blinklabs.io
48 | // @contact.email support@blinklabs.io
49 | //
50 | // @license.name Apache 2.0
51 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html
52 | func Start(cfg *config.Config) error {
53 | // Standard logging
54 | logger := logging.GetLogger()
55 | if cfg.Tls.CertFilePath != "" && cfg.Tls.KeyFilePath != "" {
56 | logger.Info(
57 | "starting API TLS listener",
58 | "address", cfg.Api.ListenAddress,
59 | "port", cfg.Api.ListenPort,
60 | )
61 | } else {
62 | logger.Info(
63 | "starting API listener",
64 | "address", cfg.Api.ListenAddress,
65 | "port", cfg.Api.ListenPort,
66 | )
67 | }
68 | // Disable gin debug and color output
69 | gin.SetMode(gin.ReleaseMode)
70 | gin.DisableConsoleColor()
71 |
72 | // Configure API router
73 | router := gin.New()
74 | // Catch panics and return a 500
75 | router.Use(gin.Recovery())
76 | // Configure CORS
77 | corsConfig := cors.DefaultConfig()
78 | corsConfig.AllowAllOrigins = true
79 | corsConfig.AllowHeaders = []string{
80 | "hx-current-url",
81 | "hx-request",
82 | "hx-target",
83 | "hx-trigger",
84 | }
85 | router.Use(cors.New(corsConfig))
86 | // Access logging
87 | accessLogger := logging.GetAccessLogger()
88 | skipPaths := []string{}
89 | if cfg.Logging.Healthchecks {
90 | skipPaths = append(skipPaths, "/healthcheck")
91 | logger.Info("disabling access logs for /healthcheck")
92 | }
93 | router.Use(logging.GinLogger(accessLogger, skipPaths))
94 | router.Use(logging.GinRecovery(accessLogger, true))
95 |
96 | // Configure static route
97 | fsys, err := fs.Sub(staticFS, "static")
98 | if err != nil {
99 | return err
100 | }
101 | router.StaticFS("/ui", http.FS(fsys))
102 | // Redirect from root
103 | router.GET("/", func(c *gin.Context) {
104 | c.Request.URL.Path = "/ui"
105 | router.HandleContext(c)
106 | })
107 |
108 | // Create a healthcheck (before metrics so it's not instrumented)
109 | router.GET("/healthcheck", handleHealthcheck)
110 | // Create a swagger endpoint (not instrumented)
111 | router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
112 |
113 | // Metrics
114 | metricsRouter := gin.New()
115 | // Configure CORS
116 | metricsRouter.Use(cors.New(corsConfig))
117 | metrics := ginmetrics.GetMonitor()
118 | // Set metrics path
119 | metrics.SetMetricPath("/")
120 | // Set metrics router
121 | metrics.Expose(metricsRouter)
122 | // Use metrics middleware without exposing path in main app router
123 | metrics.SetMetricPath("/metrics")
124 | metrics.Use(router)
125 |
126 | // Custom metrics
127 | failureMetric := &ginmetrics.Metric{
128 | // This is a Gauge because input-output-hk's is a gauge
129 | Type: ginmetrics.Gauge,
130 | Name: "tx_submit_fail_count",
131 | Description: "transactions failed",
132 | Labels: nil,
133 | }
134 | submittedMetric := &ginmetrics.Metric{
135 | // This is a Gauge because input-output-hk's is a gauge
136 | Type: ginmetrics.Gauge,
137 | Name: "tx_submit_count",
138 | Description: "transactions submitted",
139 | Labels: nil,
140 | }
141 | // Add to global monitor object
142 | _ = ginmetrics.GetMonitor().AddMetric(failureMetric)
143 | _ = ginmetrics.GetMonitor().AddMetric(submittedMetric)
144 | // Initialize metrics
145 | _ = ginmetrics.GetMonitor().
146 | GetMetric("tx_submit_fail_count").
147 | SetGaugeValue(nil, 0.0)
148 | _ = ginmetrics.GetMonitor().
149 | GetMetric("tx_submit_count").
150 | SetGaugeValue(nil, 0.0)
151 |
152 | // Start metrics listener
153 | go func() {
154 | // TODO: return error if we cannot initialize metrics
155 | logger.Info("starting metrics listener",
156 | "address", cfg.Metrics.ListenAddress,
157 | "port", cfg.Metrics.ListenPort)
158 | _ = metricsRouter.Run(fmt.Sprintf("%s:%d",
159 | cfg.Metrics.ListenAddress,
160 | cfg.Metrics.ListenPort))
161 | }()
162 |
163 | // Configure API routes
164 | router.POST("/api/submit/tx", handleSubmitTx)
165 | router.GET("/api/hastx/:tx_hash", handleHasTx)
166 |
167 | // Start API listener
168 | if cfg.Tls.CertFilePath != "" && cfg.Tls.KeyFilePath != "" {
169 | return router.RunTLS(
170 | fmt.Sprintf("%s:%d", cfg.Api.ListenAddress, cfg.Api.ListenPort),
171 | cfg.Tls.CertFilePath,
172 | cfg.Tls.KeyFilePath,
173 | )
174 | } else {
175 | return router.Run(fmt.Sprintf("%s:%d",
176 | cfg.Api.ListenAddress,
177 | cfg.Api.ListenPort))
178 | }
179 | }
180 |
181 | func handleHealthcheck(c *gin.Context) {
182 | // TODO: add some actual health checking here
183 | c.JSON(200, gin.H{"failed": false})
184 | }
185 |
186 | // Path parameters for GET requests
187 | type TxHashPathParams struct {
188 | TxHash string `uri:"tx_hash" binding:"required"` // Transaction hash
189 | }
190 |
191 | // handleHasTx godoc
192 | //
193 | // @Summary HasTx
194 | // @Description Determine if a given transaction ID exists in the node mempool.
195 | // @Produce json
196 | // @Param tx_hash path string true "Transaction Hash"
197 | // @Success 200 {object} string "Ok"
198 | // @Failure 400 {object} string "Bad Request"
199 | // @Failure 404 {object} string "Not Found"
200 | // @Failure 415 {object} string "Unsupported Media Type"
201 | // @Failure 500 {object} string "Server Error"
202 | // @Router /api/hastx/{tx_hash} [get]
203 | func handleHasTx(c *gin.Context) {
204 | // First, initialize our configuration and loggers
205 | cfg := config.GetConfig()
206 | logger := logging.GetLogger()
207 |
208 | var uriParams TxHashPathParams
209 | if err := c.ShouldBindUri(&uriParams); err != nil {
210 | logger.Error("failed to bind transaction hash from path", "err", err)
211 | c.JSON(400, fmt.Sprintf("invalid transaction hash: %s", err))
212 | return
213 | }
214 |
215 | txHash := uriParams.TxHash
216 | // convert to cbor bytes
217 | cborData, err := cbor.Marshal(txHash)
218 | if err != nil {
219 | logger.Error("failed to encode transaction hash to CBOR", "err", err)
220 | c.JSON(
221 | 400,
222 | fmt.Sprintf("failed to encode transaction hash to CBOR: %s", err),
223 | )
224 | return
225 | }
226 |
227 | // Connect to cardano-node and check for transaction
228 | oConn, err := ouroboros.NewConnection(
229 | ouroboros.WithNetworkMagic(uint32(cfg.Node.NetworkMagic)),
230 | ouroboros.WithNodeToNode(false),
231 | )
232 | if err != nil {
233 | logger.Error("failure creating Ouroboros connection", "err", err)
234 | c.JSON(500, "failure communicating with node")
235 | return
236 | }
237 | if cfg.Node.Address != "" && cfg.Node.Port > 0 {
238 | if err := oConn.Dial("tcp", fmt.Sprintf("%s:%d", cfg.Node.Address, cfg.Node.Port)); err != nil {
239 | logger.Error("failure connecting to node via TCP", "err", err)
240 | c.JSON(500, "failure communicating with node")
241 | return
242 | }
243 | } else {
244 | if err := oConn.Dial("unix", cfg.Node.SocketPath); err != nil {
245 | logger.Error("failure connecting to node via UNIX socket", "err", err)
246 | c.JSON(500, "failure communicating with node")
247 | return
248 | }
249 | }
250 | defer func() {
251 | // Close Ouroboros connection
252 | oConn.Close()
253 | }()
254 | hasTx, err := oConn.LocalTxMonitor().Client.HasTx(cborData)
255 | if err != nil {
256 | logger.Error("failure getting transaction", "err", err)
257 | c.JSON(500, fmt.Sprintf("failure getting transaction: %s", err))
258 | return
259 | }
260 | if !hasTx {
261 | c.JSON(404, "transaction not found in mempool")
262 | return
263 | }
264 | c.JSON(200, "transaction found in mempool")
265 | }
266 |
267 | // handleSubmitTx godoc
268 | //
269 | // @Summary Submit Tx
270 | // @Description Submit an already serialized transaction to the network.
271 | // @Produce json
272 | // @Param Content-Type header string true "Content type" Enums(application/cbor)
273 | // @Success 202 {object} string "Ok"
274 | // @Failure 400 {object} string "Bad Request"
275 | // @Failure 415 {object} string "Unsupported Media Type"
276 | // @Failure 500 {object} string "Server Error"
277 | // @Router /api/submit/tx [post]
278 | func handleSubmitTx(c *gin.Context) {
279 | // First, initialize our configuration and loggers
280 | cfg := config.GetConfig()
281 | logger := logging.GetLogger()
282 | // Check our headers for content-type
283 | if c.ContentType() != "application/cbor" {
284 | // Log the error, return an error to the user, and increment failed count
285 | logger.Error("invalid request body, should be application/cbor")
286 | c.JSON(415, "invalid request body, should be application/cbor")
287 | _ = ginmetrics.GetMonitor().GetMetric("tx_submit_fail_count").Inc(nil)
288 | return
289 | }
290 | // Read raw transaction bytes from the request body and store in a byte array
291 | txRawBytes, err := io.ReadAll(c.Request.Body)
292 | if err != nil {
293 | // Log the error, return an error to the user, and increment failed count
294 | logger.Error("failed to read request body", "err", err)
295 | c.JSON(500, "failed to read request body")
296 | _ = ginmetrics.GetMonitor().GetMetric("tx_submit_fail_count").Inc(nil)
297 | return
298 | }
299 | // Close request body after read
300 | if c.Request.Body != nil {
301 | if err := c.Request.Body.Close(); err != nil {
302 | logger.Error("failed to close request body", "err", err)
303 | }
304 | }
305 | // Send TX
306 | errorChan := make(chan error)
307 | submitConfig := &submit.Config{
308 | ErrorChan: errorChan,
309 | NetworkMagic: cfg.Node.NetworkMagic,
310 | NodeAddress: cfg.Node.Address,
311 | NodePort: cfg.Node.Port,
312 | SocketPath: cfg.Node.SocketPath,
313 | Timeout: cfg.Node.Timeout,
314 | }
315 | txHash, err := submit.SubmitTx(submitConfig, txRawBytes)
316 | if err != nil {
317 | if c.GetHeader("Accept") == "application/cbor" {
318 | var txRejectErr *localtxsubmission.TransactionRejectedError
319 | if errors.As(err, &txRejectErr) {
320 | c.Data(400, "application/cbor", txRejectErr.ReasonCbor)
321 | } else {
322 | c.Data(500, "application/cbor", []byte{})
323 | }
324 | } else {
325 | if err.Error() != "" {
326 | c.JSON(400, err.Error())
327 | } else {
328 | c.JSON(400, fmt.Sprintf("%s", err))
329 | }
330 | }
331 | _ = ginmetrics.GetMonitor().GetMetric("tx_submit_fail_count").Inc(nil)
332 | return
333 | }
334 | // Start async error handler
335 | go func() {
336 | err, ok := <-errorChan
337 | if ok {
338 | logger.Error("failure communicating with node", "err", err)
339 | c.JSON(500, "failure communicating with node")
340 | _ = ginmetrics.GetMonitor().
341 | GetMetric("tx_submit_fail_count").
342 | Inc(nil)
343 | }
344 | }()
345 | // Return transaction ID
346 | c.JSON(202, txHash)
347 | // Increment custom metric
348 | _ = ginmetrics.GetMonitor().GetMetric("tx_submit_count").Inc(nil)
349 | }
350 |
--------------------------------------------------------------------------------
/openapi/api_default.go:
--------------------------------------------------------------------------------
1 | /*
2 | tx-submit-api
3 |
4 | Cardano Transaction Submit API
5 |
6 | API version: v0
7 | Contact: support@blinklabs.io
8 | */
9 |
10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
11 |
12 | package openapi
13 |
14 | import (
15 | "bytes"
16 | "context"
17 | "io"
18 | "net/http"
19 | "net/url"
20 | "strings"
21 | )
22 |
23 | type DefaultAPI interface {
24 |
25 | /*
26 | ApiHastxTxHashGet HasTx
27 |
28 | Determine if a given transaction ID exists in the node mempool.
29 |
30 | @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
31 | @param txHash Transaction Hash
32 | @return DefaultAPIApiHastxTxHashGetRequest
33 | */
34 | ApiHastxTxHashGet(
35 | ctx context.Context,
36 | txHash string,
37 | ) DefaultAPIApiHastxTxHashGetRequest
38 |
39 | // ApiHastxTxHashGetExecute executes the request
40 | // @return string
41 | ApiHastxTxHashGetExecute(
42 | r DefaultAPIApiHastxTxHashGetRequest,
43 | ) (string, *http.Response, error)
44 |
45 | /*
46 | ApiSubmitTxPost Submit Tx
47 |
48 | Submit an already serialized transaction to the network.
49 |
50 | @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
51 | @return DefaultAPIApiSubmitTxPostRequest
52 | */
53 | ApiSubmitTxPost(ctx context.Context) DefaultAPIApiSubmitTxPostRequest
54 |
55 | // ApiSubmitTxPostExecute executes the request
56 | // @return string
57 | ApiSubmitTxPostExecute(
58 | r DefaultAPIApiSubmitTxPostRequest,
59 | ) (string, *http.Response, error)
60 | }
61 |
62 | // DefaultAPIService DefaultAPI service
63 | type DefaultAPIService service
64 |
65 | type DefaultAPIApiHastxTxHashGetRequest struct {
66 | ctx context.Context
67 | ApiService DefaultAPI
68 | txHash string
69 | }
70 |
71 | func (r DefaultAPIApiHastxTxHashGetRequest) Execute() (string, *http.Response, error) {
72 | return r.ApiService.ApiHastxTxHashGetExecute(r)
73 | }
74 |
75 | /*
76 | ApiHastxTxHashGet HasTx
77 |
78 | Determine if a given transaction ID exists in the node mempool.
79 |
80 | @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
81 | @param txHash Transaction Hash
82 | @return DefaultAPIApiHastxTxHashGetRequest
83 | */
84 | func (a *DefaultAPIService) ApiHastxTxHashGet(
85 | ctx context.Context,
86 | txHash string,
87 | ) DefaultAPIApiHastxTxHashGetRequest {
88 | return DefaultAPIApiHastxTxHashGetRequest{
89 | ApiService: a,
90 | ctx: ctx,
91 | txHash: txHash,
92 | }
93 | }
94 |
95 | // Execute executes the request
96 | //
97 | // @return string
98 | func (a *DefaultAPIService) ApiHastxTxHashGetExecute(
99 | r DefaultAPIApiHastxTxHashGetRequest,
100 | ) (string, *http.Response, error) {
101 | var (
102 | localVarHTTPMethod = http.MethodGet
103 | localVarPostBody interface{}
104 | formFiles []formFile
105 | localVarReturnValue string
106 | )
107 |
108 | localBasePath, err := a.client.cfg.ServerURLWithContext(
109 | r.ctx,
110 | "DefaultAPIService.ApiHastxTxHashGet",
111 | )
112 | if err != nil {
113 | return localVarReturnValue, nil, &GenericOpenAPIError{
114 | error: err.Error(),
115 | }
116 | }
117 |
118 | localVarPath := localBasePath + "/api/hastx/{tx_hash}"
119 | localVarPath = strings.Replace(
120 | localVarPath,
121 | "{"+"tx_hash"+"}",
122 | url.PathEscape(parameterValueToString(r.txHash, "txHash")),
123 | -1,
124 | )
125 |
126 | localVarHeaderParams := make(map[string]string)
127 | localVarQueryParams := url.Values{}
128 | localVarFormParams := url.Values{}
129 |
130 | // to determine the Content-Type header
131 | localVarHTTPContentTypes := []string{}
132 |
133 | // set Content-Type header
134 | localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
135 | if localVarHTTPContentType != "" {
136 | localVarHeaderParams["Content-Type"] = localVarHTTPContentType
137 | }
138 |
139 | // to determine the Accept header
140 | localVarHTTPHeaderAccepts := []string{"application/json"}
141 |
142 | // set Accept header
143 | localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
144 | if localVarHTTPHeaderAccept != "" {
145 | localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
146 | }
147 | req, err := a.client.prepareRequest(
148 | r.ctx,
149 | localVarPath,
150 | localVarHTTPMethod,
151 | localVarPostBody,
152 | localVarHeaderParams,
153 | localVarQueryParams,
154 | localVarFormParams,
155 | formFiles,
156 | )
157 | if err != nil {
158 | return localVarReturnValue, nil, err
159 | }
160 |
161 | localVarHTTPResponse, err := a.client.callAPI(req)
162 | if err != nil || localVarHTTPResponse == nil {
163 | return localVarReturnValue, localVarHTTPResponse, err
164 | }
165 |
166 | localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
167 | localVarHTTPResponse.Body.Close()
168 | localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
169 | if err != nil {
170 | return localVarReturnValue, localVarHTTPResponse, err
171 | }
172 |
173 | if localVarHTTPResponse.StatusCode >= 300 {
174 | newErr := &GenericOpenAPIError{
175 | body: localVarBody,
176 | error: localVarHTTPResponse.Status,
177 | }
178 | if localVarHTTPResponse.StatusCode == 400 {
179 | var v string
180 | err = a.client.decode(
181 | &v,
182 | localVarBody,
183 | localVarHTTPResponse.Header.Get("Content-Type"),
184 | )
185 | if err != nil {
186 | newErr.error = err.Error()
187 | return localVarReturnValue, localVarHTTPResponse, newErr
188 | }
189 | newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
190 | newErr.model = v
191 | return localVarReturnValue, localVarHTTPResponse, newErr
192 | }
193 | if localVarHTTPResponse.StatusCode == 404 {
194 | var v string
195 | err = a.client.decode(
196 | &v,
197 | localVarBody,
198 | localVarHTTPResponse.Header.Get("Content-Type"),
199 | )
200 | if err != nil {
201 | newErr.error = err.Error()
202 | return localVarReturnValue, localVarHTTPResponse, newErr
203 | }
204 | newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
205 | newErr.model = v
206 | return localVarReturnValue, localVarHTTPResponse, newErr
207 | }
208 | if localVarHTTPResponse.StatusCode == 415 {
209 | var v string
210 | err = a.client.decode(
211 | &v,
212 | localVarBody,
213 | localVarHTTPResponse.Header.Get("Content-Type"),
214 | )
215 | if err != nil {
216 | newErr.error = err.Error()
217 | return localVarReturnValue, localVarHTTPResponse, newErr
218 | }
219 | newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
220 | newErr.model = v
221 | return localVarReturnValue, localVarHTTPResponse, newErr
222 | }
223 | if localVarHTTPResponse.StatusCode == 500 {
224 | var v string
225 | err = a.client.decode(
226 | &v,
227 | localVarBody,
228 | localVarHTTPResponse.Header.Get("Content-Type"),
229 | )
230 | if err != nil {
231 | newErr.error = err.Error()
232 | return localVarReturnValue, localVarHTTPResponse, newErr
233 | }
234 | newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
235 | newErr.model = v
236 | }
237 | return localVarReturnValue, localVarHTTPResponse, newErr
238 | }
239 |
240 | err = a.client.decode(
241 | &localVarReturnValue,
242 | localVarBody,
243 | localVarHTTPResponse.Header.Get("Content-Type"),
244 | )
245 | if err != nil {
246 | newErr := &GenericOpenAPIError{
247 | body: localVarBody,
248 | error: err.Error(),
249 | }
250 | return localVarReturnValue, localVarHTTPResponse, newErr
251 | }
252 |
253 | return localVarReturnValue, localVarHTTPResponse, nil
254 | }
255 |
256 | type DefaultAPIApiSubmitTxPostRequest struct {
257 | ctx context.Context
258 | ApiService DefaultAPI
259 | contentType *string
260 | }
261 |
262 | // Content type
263 | func (r DefaultAPIApiSubmitTxPostRequest) ContentType(
264 | contentType string,
265 | ) DefaultAPIApiSubmitTxPostRequest {
266 | r.contentType = &contentType
267 | return r
268 | }
269 |
270 | func (r DefaultAPIApiSubmitTxPostRequest) Execute() (string, *http.Response, error) {
271 | return r.ApiService.ApiSubmitTxPostExecute(r)
272 | }
273 |
274 | /*
275 | ApiSubmitTxPost Submit Tx
276 |
277 | Submit an already serialized transaction to the network.
278 |
279 | @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
280 | @return DefaultAPIApiSubmitTxPostRequest
281 | */
282 | func (a *DefaultAPIService) ApiSubmitTxPost(
283 | ctx context.Context,
284 | ) DefaultAPIApiSubmitTxPostRequest {
285 | return DefaultAPIApiSubmitTxPostRequest{
286 | ApiService: a,
287 | ctx: ctx,
288 | }
289 | }
290 |
291 | // Execute executes the request
292 | //
293 | // @return string
294 | func (a *DefaultAPIService) ApiSubmitTxPostExecute(
295 | r DefaultAPIApiSubmitTxPostRequest,
296 | ) (string, *http.Response, error) {
297 | var (
298 | localVarHTTPMethod = http.MethodPost
299 | localVarPostBody interface{}
300 | formFiles []formFile
301 | localVarReturnValue string
302 | )
303 |
304 | localBasePath, err := a.client.cfg.ServerURLWithContext(
305 | r.ctx,
306 | "DefaultAPIService.ApiSubmitTxPost",
307 | )
308 | if err != nil {
309 | return localVarReturnValue, nil, &GenericOpenAPIError{
310 | error: err.Error(),
311 | }
312 | }
313 |
314 | localVarPath := localBasePath + "/api/submit/tx"
315 |
316 | localVarHeaderParams := make(map[string]string)
317 | localVarQueryParams := url.Values{}
318 | localVarFormParams := url.Values{}
319 | if r.contentType == nil {
320 | return localVarReturnValue, nil, reportError(
321 | "contentType is required and must be specified",
322 | )
323 | }
324 |
325 | // to determine the Content-Type header
326 | localVarHTTPContentTypes := []string{}
327 |
328 | // set Content-Type header
329 | localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
330 | if localVarHTTPContentType != "" {
331 | localVarHeaderParams["Content-Type"] = localVarHTTPContentType
332 | }
333 |
334 | // to determine the Accept header
335 | localVarHTTPHeaderAccepts := []string{"application/json"}
336 |
337 | // set Accept header
338 | localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
339 | if localVarHTTPHeaderAccept != "" {
340 | localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
341 | }
342 | parameterAddToHeaderOrQuery(
343 | localVarHeaderParams,
344 | "Content-Type",
345 | r.contentType,
346 | "",
347 | "",
348 | )
349 | req, err := a.client.prepareRequest(
350 | r.ctx,
351 | localVarPath,
352 | localVarHTTPMethod,
353 | localVarPostBody,
354 | localVarHeaderParams,
355 | localVarQueryParams,
356 | localVarFormParams,
357 | formFiles,
358 | )
359 | if err != nil {
360 | return localVarReturnValue, nil, err
361 | }
362 |
363 | localVarHTTPResponse, err := a.client.callAPI(req)
364 | if err != nil || localVarHTTPResponse == nil {
365 | return localVarReturnValue, localVarHTTPResponse, err
366 | }
367 |
368 | localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
369 | localVarHTTPResponse.Body.Close()
370 | localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
371 | if err != nil {
372 | return localVarReturnValue, localVarHTTPResponse, err
373 | }
374 |
375 | if localVarHTTPResponse.StatusCode >= 300 {
376 | newErr := &GenericOpenAPIError{
377 | body: localVarBody,
378 | error: localVarHTTPResponse.Status,
379 | }
380 | if localVarHTTPResponse.StatusCode == 400 {
381 | var v string
382 | err = a.client.decode(
383 | &v,
384 | localVarBody,
385 | localVarHTTPResponse.Header.Get("Content-Type"),
386 | )
387 | if err != nil {
388 | newErr.error = err.Error()
389 | return localVarReturnValue, localVarHTTPResponse, newErr
390 | }
391 | newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
392 | newErr.model = v
393 | return localVarReturnValue, localVarHTTPResponse, newErr
394 | }
395 | if localVarHTTPResponse.StatusCode == 415 {
396 | var v string
397 | err = a.client.decode(
398 | &v,
399 | localVarBody,
400 | localVarHTTPResponse.Header.Get("Content-Type"),
401 | )
402 | if err != nil {
403 | newErr.error = err.Error()
404 | return localVarReturnValue, localVarHTTPResponse, newErr
405 | }
406 | newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
407 | newErr.model = v
408 | return localVarReturnValue, localVarHTTPResponse, newErr
409 | }
410 | if localVarHTTPResponse.StatusCode == 500 {
411 | var v string
412 | err = a.client.decode(
413 | &v,
414 | localVarBody,
415 | localVarHTTPResponse.Header.Get("Content-Type"),
416 | )
417 | if err != nil {
418 | newErr.error = err.Error()
419 | return localVarReturnValue, localVarHTTPResponse, newErr
420 | }
421 | newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
422 | newErr.model = v
423 | }
424 | return localVarReturnValue, localVarHTTPResponse, newErr
425 | }
426 |
427 | err = a.client.decode(
428 | &localVarReturnValue,
429 | localVarBody,
430 | localVarHTTPResponse.Header.Get("Content-Type"),
431 | )
432 | if err != nil {
433 | newErr := &GenericOpenAPIError{
434 | body: localVarBody,
435 | error: err.Error(),
436 | }
437 | return localVarReturnValue, localVarHTTPResponse, newErr
438 | }
439 |
440 | return localVarReturnValue, localVarHTTPResponse, nil
441 | }
442 |
--------------------------------------------------------------------------------
/internal/api/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Metrics Dashboard
7 |
8 |
9 |
10 |
25 |
37 |
38 |
41 |
42 |
43 | tx-submit-api metrics
44 |
45 |
48 |
56 |
57 |
58 |
59 | Blink Labs Software: tx-submit-api
60 |
61 |
68 | Loading metrics...
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
456 |
457 |
458 |
--------------------------------------------------------------------------------
/openapi/client.go:
--------------------------------------------------------------------------------
1 | /*
2 | tx-submit-api
3 |
4 | Cardano Transaction Submit API
5 |
6 | API version: v0
7 | Contact: support@blinklabs.io
8 | */
9 |
10 | // Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT.
11 |
12 | package openapi
13 |
14 | import (
15 | "bytes"
16 | "context"
17 | "encoding/json"
18 | "encoding/xml"
19 | "errors"
20 | "fmt"
21 | "io"
22 | "log"
23 | "mime/multipart"
24 | "net/http"
25 | "net/http/httputil"
26 | "net/url"
27 | "os"
28 | "path/filepath"
29 | "reflect"
30 | "regexp"
31 | "strconv"
32 | "strings"
33 | "time"
34 | "unicode/utf8"
35 | )
36 |
37 | var (
38 | JsonCheck = regexp.MustCompile(
39 | `(?i:(?:application|text)/(?:[^;]+\+)?json)`,
40 | )
41 | XmlCheck = regexp.MustCompile(
42 | `(?i:(?:application|text)/(?:[^;]+\+)?xml)`,
43 | )
44 | queryParamSplit = regexp.MustCompile(`(^|&)([^&]+)`)
45 | queryDescape = strings.NewReplacer("%5B", "[", "%5D", "]")
46 | )
47 |
48 | // APIClient manages communication with the tx-submit-api API vv0
49 | // In most cases there should be only one, shared, APIClient.
50 | type APIClient struct {
51 | cfg *Configuration
52 | common service // Reuse a single struct instead of allocating one for each service on the heap.
53 |
54 | // API Services
55 |
56 | DefaultAPI DefaultAPI
57 | }
58 |
59 | type service struct {
60 | client *APIClient
61 | }
62 |
63 | // NewAPIClient creates a new API client. Requires a userAgent string describing your application.
64 | // optionally a custom http.Client to allow for advanced features such as caching.
65 | func NewAPIClient(cfg *Configuration) *APIClient {
66 | if cfg.HTTPClient == nil {
67 | cfg.HTTPClient = http.DefaultClient
68 | }
69 |
70 | c := &APIClient{}
71 | c.cfg = cfg
72 | c.common.client = c
73 |
74 | // API Services
75 | c.DefaultAPI = (*DefaultAPIService)(&c.common)
76 |
77 | return c
78 | }
79 |
80 | func atoi(in string) (int, error) {
81 | return strconv.Atoi(in)
82 | }
83 |
84 | // selectHeaderContentType select a content type from the available list.
85 | func selectHeaderContentType(contentTypes []string) string {
86 | if len(contentTypes) == 0 {
87 | return ""
88 | }
89 | if contains(contentTypes, "application/json") {
90 | return "application/json"
91 | }
92 | return contentTypes[0] // use the first content type specified in 'consumes'
93 | }
94 |
95 | // selectHeaderAccept join all accept types and return
96 | func selectHeaderAccept(accepts []string) string {
97 | if len(accepts) == 0 {
98 | return ""
99 | }
100 |
101 | if contains(accepts, "application/json") {
102 | return "application/json"
103 | }
104 |
105 | return strings.Join(accepts, ",")
106 | }
107 |
108 | // contains is a case insensitive match, finding needle in a haystack
109 | func contains(haystack []string, needle string) bool {
110 | for _, a := range haystack {
111 | if strings.EqualFold(a, needle) {
112 | return true
113 | }
114 | }
115 | return false
116 | }
117 |
118 | // Verify optional parameters are of the correct type.
119 | func typeCheckParameter(obj interface{}, expected string, name string) error {
120 | // Make sure there is an object.
121 | if obj == nil {
122 | return nil
123 | }
124 |
125 | // Check the type is as expected.
126 | if reflect.TypeOf(obj).String() != expected {
127 | return fmt.Errorf(
128 | "expected %s to be of type %s but received %s",
129 | name,
130 | expected,
131 | reflect.TypeOf(obj).String(),
132 | )
133 | }
134 | return nil
135 | }
136 |
137 | func parameterValueToString(obj interface{}, key string) string {
138 | if reflect.TypeOf(obj).Kind() != reflect.Ptr {
139 | if actualObj, ok := obj.(interface{ GetActualInstanceValue() interface{} }); ok {
140 | return fmt.Sprintf("%v", actualObj.GetActualInstanceValue())
141 | }
142 |
143 | return fmt.Sprintf("%v", obj)
144 | }
145 | var param, ok = obj.(MappedNullable)
146 | if !ok {
147 | return ""
148 | }
149 | dataMap, err := param.ToMap()
150 | if err != nil {
151 | return ""
152 | }
153 | return fmt.Sprintf("%v", dataMap[key])
154 | }
155 |
156 | // parameterAddToHeaderOrQuery adds the provided object to the request header or url query
157 | // supporting deep object syntax
158 | func parameterAddToHeaderOrQuery(
159 | headerOrQueryParams interface{},
160 | keyPrefix string,
161 | obj interface{},
162 | style string,
163 | collectionType string,
164 | ) {
165 | var v = reflect.ValueOf(obj)
166 | var value = ""
167 | if v == reflect.ValueOf(nil) {
168 | value = "null"
169 | } else {
170 | switch v.Kind() {
171 | case reflect.Invalid:
172 | value = "invalid"
173 |
174 | case reflect.Struct:
175 | if t, ok := obj.(MappedNullable); ok {
176 | dataMap, err := t.ToMap()
177 | if err != nil {
178 | return
179 | }
180 | parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefix, dataMap, style, collectionType)
181 | return
182 | }
183 | if t, ok := obj.(time.Time); ok {
184 | parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefix, t.Format(time.RFC3339Nano), style, collectionType)
185 | return
186 | }
187 | value = v.Type().String() + " value"
188 | case reflect.Slice:
189 | var indValue = reflect.ValueOf(obj)
190 | if indValue == reflect.ValueOf(nil) {
191 | return
192 | }
193 | var lenIndValue = indValue.Len()
194 | for i := 0; i < lenIndValue; i++ {
195 | var arrayValue = indValue.Index(i)
196 | var keyPrefixForCollectionType = keyPrefix
197 | if style == "deepObject" {
198 | keyPrefixForCollectionType = keyPrefix + "[" + strconv.Itoa(i) + "]"
199 | }
200 | parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefixForCollectionType, arrayValue.Interface(), style, collectionType)
201 | }
202 | return
203 |
204 | case reflect.Map:
205 | var indValue = reflect.ValueOf(obj)
206 | if indValue == reflect.ValueOf(nil) {
207 | return
208 | }
209 | iter := indValue.MapRange()
210 | for iter.Next() {
211 | k, v := iter.Key(), iter.Value()
212 | parameterAddToHeaderOrQuery(headerOrQueryParams, fmt.Sprintf("%s[%s]", keyPrefix, k.String()), v.Interface(), style, collectionType)
213 | }
214 | return
215 |
216 | case reflect.Interface:
217 | fallthrough
218 | case reflect.Ptr:
219 | parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefix, v.Elem().Interface(), style, collectionType)
220 | return
221 |
222 | case reflect.Int, reflect.Int8, reflect.Int16,
223 | reflect.Int32, reflect.Int64:
224 | value = strconv.FormatInt(v.Int(), 10)
225 | case reflect.Uint, reflect.Uint8, reflect.Uint16,
226 | reflect.Uint32, reflect.Uint64, reflect.Uintptr:
227 | value = strconv.FormatUint(v.Uint(), 10)
228 | case reflect.Float32, reflect.Float64:
229 | value = strconv.FormatFloat(v.Float(), 'g', -1, 32)
230 | case reflect.Bool:
231 | value = strconv.FormatBool(v.Bool())
232 | case reflect.String:
233 | value = v.String()
234 | default:
235 | value = v.Type().String() + " value"
236 | }
237 | }
238 |
239 | switch valuesMap := headerOrQueryParams.(type) {
240 | case url.Values:
241 | if collectionType == "csv" && valuesMap.Get(keyPrefix) != "" {
242 | valuesMap.Set(keyPrefix, valuesMap.Get(keyPrefix)+","+value)
243 | } else {
244 | valuesMap.Add(keyPrefix, value)
245 | }
246 | break
247 | case map[string]string:
248 | valuesMap[keyPrefix] = value
249 | break
250 | }
251 | }
252 |
253 | // helper for converting interface{} parameters to json strings
254 | func parameterToJson(obj interface{}) (string, error) {
255 | jsonBuf, err := json.Marshal(obj)
256 | if err != nil {
257 | return "", err
258 | }
259 | return string(jsonBuf), err
260 | }
261 |
262 | // callAPI do the request.
263 | func (c *APIClient) callAPI(request *http.Request) (*http.Response, error) {
264 | if c.cfg.Debug {
265 | dump, err := httputil.DumpRequestOut(request, true)
266 | if err != nil {
267 | return nil, err
268 | }
269 | log.Printf("\n%s\n", string(dump))
270 | }
271 |
272 | resp, err := c.cfg.HTTPClient.Do(request)
273 | if err != nil {
274 | return resp, err
275 | }
276 |
277 | if c.cfg.Debug {
278 | dump, err := httputil.DumpResponse(resp, true)
279 | if err != nil {
280 | return resp, err
281 | }
282 | log.Printf("\n%s\n", string(dump))
283 | }
284 | return resp, err
285 | }
286 |
287 | // Allow modification of underlying config for alternate implementations and testing
288 | // Caution: modifying the configuration while live can cause data races and potentially unwanted behavior
289 | func (c *APIClient) GetConfig() *Configuration {
290 | return c.cfg
291 | }
292 |
293 | type formFile struct {
294 | fileBytes []byte
295 | fileName string
296 | formFileName string
297 | }
298 |
299 | // prepareRequest build the request
300 | func (c *APIClient) prepareRequest(
301 | ctx context.Context,
302 | path string, method string,
303 | postBody interface{},
304 | headerParams map[string]string,
305 | queryParams url.Values,
306 | formParams url.Values,
307 | formFiles []formFile) (localVarRequest *http.Request, err error) {
308 |
309 | var body *bytes.Buffer
310 |
311 | // Detect postBody type and post.
312 | if postBody != nil {
313 | contentType := headerParams["Content-Type"]
314 | if contentType == "" {
315 | contentType = detectContentType(postBody)
316 | headerParams["Content-Type"] = contentType
317 | }
318 |
319 | body, err = setBody(postBody, contentType)
320 | if err != nil {
321 | return nil, err
322 | }
323 | }
324 |
325 | // add form parameters and file if available.
326 | if strings.HasPrefix(headerParams["Content-Type"], "multipart/form-data") &&
327 | len(formParams) > 0 ||
328 | (len(formFiles) > 0) {
329 | if body != nil {
330 | return nil, errors.New(
331 | "Cannot specify postBody and multipart form at the same time.",
332 | )
333 | }
334 | body = &bytes.Buffer{}
335 | w := multipart.NewWriter(body)
336 |
337 | for k, v := range formParams {
338 | for _, iv := range v {
339 | if strings.HasPrefix(k, "@") { // file
340 | err = addFile(w, k[1:], iv)
341 | if err != nil {
342 | return nil, err
343 | }
344 | } else { // form value
345 | w.WriteField(k, iv)
346 | }
347 | }
348 | }
349 | for _, formFile := range formFiles {
350 | if len(formFile.fileBytes) > 0 && formFile.fileName != "" {
351 | w.Boundary()
352 | part, err := w.CreateFormFile(
353 | formFile.formFileName,
354 | filepath.Base(formFile.fileName),
355 | )
356 | if err != nil {
357 | return nil, err
358 | }
359 | _, err = part.Write(formFile.fileBytes)
360 | if err != nil {
361 | return nil, err
362 | }
363 | }
364 | }
365 |
366 | // Set the Boundary in the Content-Type
367 | headerParams["Content-Type"] = w.FormDataContentType()
368 |
369 | // Set Content-Length
370 | headerParams["Content-Length"] = fmt.Sprintf("%d", body.Len())
371 | w.Close()
372 | }
373 |
374 | if strings.HasPrefix(
375 | headerParams["Content-Type"],
376 | "application/x-www-form-urlencoded",
377 | ) &&
378 | len(formParams) > 0 {
379 | if body != nil {
380 | return nil, errors.New(
381 | "Cannot specify postBody and x-www-form-urlencoded form at the same time.",
382 | )
383 | }
384 | body = &bytes.Buffer{}
385 | body.WriteString(formParams.Encode())
386 | // Set Content-Length
387 | headerParams["Content-Length"] = fmt.Sprintf("%d", body.Len())
388 | }
389 |
390 | // Setup path and query parameters
391 | url, err := url.Parse(path)
392 | if err != nil {
393 | return nil, err
394 | }
395 |
396 | // Override request host, if applicable
397 | if c.cfg.Host != "" {
398 | url.Host = c.cfg.Host
399 | }
400 |
401 | // Override request scheme, if applicable
402 | if c.cfg.Scheme != "" {
403 | url.Scheme = c.cfg.Scheme
404 | }
405 |
406 | // Adding Query Param
407 | query := url.Query()
408 | for k, v := range queryParams {
409 | for _, iv := range v {
410 | query.Add(k, iv)
411 | }
412 | }
413 |
414 | // Encode the parameters.
415 | url.RawQuery = queryParamSplit.ReplaceAllStringFunc(
416 | query.Encode(),
417 | func(s string) string {
418 | pieces := strings.Split(s, "=")
419 | pieces[0] = queryDescape.Replace(pieces[0])
420 | return strings.Join(pieces, "=")
421 | },
422 | )
423 |
424 | // Generate a new request
425 | if body != nil {
426 | localVarRequest, err = http.NewRequest(method, url.String(), body)
427 | } else {
428 | localVarRequest, err = http.NewRequest(method, url.String(), nil)
429 | }
430 | if err != nil {
431 | return nil, err
432 | }
433 |
434 | // add header parameters, if any
435 | if len(headerParams) > 0 {
436 | headers := http.Header{}
437 | for h, v := range headerParams {
438 | headers[h] = []string{v}
439 | }
440 | localVarRequest.Header = headers
441 | }
442 |
443 | // Add the user agent to the request.
444 | localVarRequest.Header.Add("User-Agent", c.cfg.UserAgent)
445 |
446 | if ctx != nil {
447 | // add context to the request
448 | localVarRequest = localVarRequest.WithContext(ctx)
449 |
450 | // Walk through any authentication.
451 |
452 | }
453 |
454 | for header, value := range c.cfg.DefaultHeader {
455 | localVarRequest.Header.Add(header, value)
456 | }
457 | return localVarRequest, nil
458 | }
459 |
460 | func (c *APIClient) decode(
461 | v interface{},
462 | b []byte,
463 | contentType string,
464 | ) (err error) {
465 | if len(b) == 0 {
466 | return nil
467 | }
468 | if s, ok := v.(*string); ok {
469 | *s = string(b)
470 | return nil
471 | }
472 | if f, ok := v.(*os.File); ok {
473 | f, err = os.CreateTemp("", "HttpClientFile")
474 | if err != nil {
475 | return
476 | }
477 | _, err = f.Write(b)
478 | if err != nil {
479 | return
480 | }
481 | _, err = f.Seek(0, io.SeekStart)
482 | return
483 | }
484 | if f, ok := v.(**os.File); ok {
485 | *f, err = os.CreateTemp("", "HttpClientFile")
486 | if err != nil {
487 | return
488 | }
489 | _, err = (*f).Write(b)
490 | if err != nil {
491 | return
492 | }
493 | _, err = (*f).Seek(0, io.SeekStart)
494 | return
495 | }
496 | if XmlCheck.MatchString(contentType) {
497 | if err = xml.Unmarshal(b, v); err != nil {
498 | return err
499 | }
500 | return nil
501 | }
502 | if JsonCheck.MatchString(contentType) {
503 | if actualObj, ok := v.(interface{ GetActualInstance() interface{} }); ok { // oneOf, anyOf schemas
504 | if unmarshalObj, ok := actualObj.(interface{ UnmarshalJSON([]byte) error }); ok { // make sure it has UnmarshalJSON defined
505 | if err = unmarshalObj.UnmarshalJSON(b); err != nil {
506 | return err
507 | }
508 | } else {
509 | return errors.New("Unknown type with GetActualInstance but no unmarshalObj.UnmarshalJSON defined")
510 | }
511 | } else if err = json.Unmarshal(b, v); err != nil { // simple model
512 | return err
513 | }
514 | return nil
515 | }
516 | return errors.New("undefined response type")
517 | }
518 |
519 | // Add a file to the multipart request
520 | func addFile(w *multipart.Writer, fieldName, path string) error {
521 | file, err := os.Open(filepath.Clean(path))
522 | if err != nil {
523 | return err
524 | }
525 | err = file.Close()
526 | if err != nil {
527 | return err
528 | }
529 |
530 | part, err := w.CreateFormFile(fieldName, filepath.Base(path))
531 | if err != nil {
532 | return err
533 | }
534 | _, err = io.Copy(part, file)
535 |
536 | return err
537 | }
538 |
539 | // Set request body from an interface{}
540 | func setBody(
541 | body interface{},
542 | contentType string,
543 | ) (bodyBuf *bytes.Buffer, err error) {
544 | if bodyBuf == nil {
545 | bodyBuf = &bytes.Buffer{}
546 | }
547 |
548 | if reader, ok := body.(io.Reader); ok {
549 | _, err = bodyBuf.ReadFrom(reader)
550 | } else if fp, ok := body.(*os.File); ok {
551 | _, err = bodyBuf.ReadFrom(fp)
552 | } else if b, ok := body.([]byte); ok {
553 | _, err = bodyBuf.Write(b)
554 | } else if s, ok := body.(string); ok {
555 | _, err = bodyBuf.WriteString(s)
556 | } else if s, ok := body.(*string); ok {
557 | _, err = bodyBuf.WriteString(*s)
558 | } else if JsonCheck.MatchString(contentType) {
559 | err = json.NewEncoder(bodyBuf).Encode(body)
560 | } else if XmlCheck.MatchString(contentType) {
561 | var bs []byte
562 | bs, err = xml.Marshal(body)
563 | if err == nil {
564 | bodyBuf.Write(bs)
565 | }
566 | }
567 |
568 | if err != nil {
569 | return nil, err
570 | }
571 |
572 | if bodyBuf.Len() == 0 {
573 | err = fmt.Errorf("invalid body type %s\n", contentType)
574 | return nil, err
575 | }
576 | return bodyBuf, nil
577 | }
578 |
579 | // detectContentType method is used to figure out `Request.Body` content type for request header
580 | func detectContentType(body interface{}) string {
581 | contentType := "text/plain; charset=utf-8"
582 | kind := reflect.TypeOf(body).Kind()
583 |
584 | switch kind {
585 | case reflect.Struct, reflect.Map, reflect.Ptr:
586 | contentType = "application/json; charset=utf-8"
587 | case reflect.String:
588 | contentType = "text/plain; charset=utf-8"
589 | default:
590 | if b, ok := body.([]byte); ok {
591 | contentType = http.DetectContentType(b)
592 | } else if kind == reflect.Slice {
593 | contentType = "application/json; charset=utf-8"
594 | }
595 | }
596 |
597 | return contentType
598 | }
599 |
600 | // Ripped from https://github.com/gregjones/httpcache/blob/master/httpcache.go
601 | type cacheControl map[string]string
602 |
603 | func parseCacheControl(headers http.Header) cacheControl {
604 | cc := cacheControl{}
605 | ccHeader := headers.Get("Cache-Control")
606 | for _, part := range strings.Split(ccHeader, ",") {
607 | part = strings.Trim(part, " ")
608 | if part == "" {
609 | continue
610 | }
611 | if strings.ContainsRune(part, '=') {
612 | keyval := strings.Split(part, "=")
613 | cc[strings.Trim(keyval[0], " ")] = strings.Trim(keyval[1], ",")
614 | } else {
615 | cc[part] = ""
616 | }
617 | }
618 | return cc
619 | }
620 |
621 | // CacheExpires helper function to determine remaining time before repeating a request.
622 | func CacheExpires(r *http.Response) time.Time {
623 | // Figure out when the cache expires.
624 | var expires time.Time
625 | now, err := time.Parse(time.RFC1123, r.Header.Get("date"))
626 | if err != nil {
627 | return time.Now()
628 | }
629 | respCacheControl := parseCacheControl(r.Header)
630 |
631 | if maxAge, ok := respCacheControl["max-age"]; ok {
632 | lifetime, err := time.ParseDuration(maxAge + "s")
633 | if err != nil {
634 | expires = now
635 | } else {
636 | expires = now.Add(lifetime)
637 | }
638 | } else {
639 | expiresHeader := r.Header.Get("Expires")
640 | if expiresHeader != "" {
641 | expires, err = time.Parse(time.RFC1123, expiresHeader)
642 | if err != nil {
643 | expires = now
644 | }
645 | }
646 | }
647 | return expires
648 | }
649 |
650 | func strlen(s string) int {
651 | return utf8.RuneCountInString(s)
652 | }
653 |
654 | // GenericOpenAPIError Provides access to the body, error and model on returned errors.
655 | type GenericOpenAPIError struct {
656 | body []byte
657 | error string
658 | model interface{}
659 | }
660 |
661 | // Error returns non-empty string if there was an error.
662 | func (e GenericOpenAPIError) Error() string {
663 | return e.error
664 | }
665 |
666 | // Body returns the raw bytes of the response
667 | func (e GenericOpenAPIError) Body() []byte {
668 | return e.body
669 | }
670 |
671 | // Model returns the unpacked model of the error
672 | func (e GenericOpenAPIError) Model() interface{} {
673 | return e.model
674 | }
675 |
676 | // format error message using title and detail when model implements rfc7807
677 | func formatErrorMessage(status string, v interface{}) string {
678 | str := ""
679 | metaValue := reflect.ValueOf(v).Elem()
680 |
681 | if metaValue.Kind() == reflect.Struct {
682 | field := metaValue.FieldByName("Title")
683 | if field != (reflect.Value{}) {
684 | str = fmt.Sprintf("%s", field.Interface())
685 | }
686 |
687 | field = metaValue.FieldByName("Detail")
688 | if field != (reflect.Value{}) {
689 | str = fmt.Sprintf("%s (%s)", str, field.Interface())
690 | }
691 | }
692 |
693 | return strings.TrimSpace(fmt.Sprintf("%s %s", status, str))
694 | }
695 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
3 | github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
4 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
5 | github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU=
6 | github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
7 | github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
8 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
9 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
10 | github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU=
11 | github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
12 | github.com/blinklabs-io/gouroboros v0.143.0 h1:ZkMNPEBC4mvTQM/6zt3QHDvpGsbdrBhT1+DahkswheU=
13 | github.com/blinklabs-io/gouroboros v0.143.0/go.mod h1:aAJ6T0RGZQdLNu5deS54Wx6P30Bm3j9TE18XBXeZPQs=
14 | github.com/blinklabs-io/ouroboros-mock v0.4.0 h1:ppOcTMnC/2f5rYYSlvNqcGfAQOIpMCSDUrNh9K6a4mY=
15 | github.com/blinklabs-io/ouroboros-mock v0.4.0/go.mod h1:e+Kck8bmdOuaN7IfkbVvbqAVlskXNXB95oHI3YlFG5g=
16 | github.com/blinklabs-io/plutigo v0.0.16 h1:3+1eOby9ckoDXb+8ObCBQ4BGbHZ6z7cxGFDR4cbS+7Y=
17 | github.com/blinklabs-io/plutigo v0.0.16/go.mod h1:aT3mJAh1s8JJ8C42ygd8OyGDQZ8f+w8Uge2+C/9BLug=
18 | github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
19 | github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
20 | github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
21 | github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg=
22 | github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
23 | github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
24 | github.com/btcsuite/btcd/btcec/v2 v2.3.6 h1:IzlsEr9olcSRKB/n7c4351F3xHKxS2lma+1UFGCYd4E=
25 | github.com/btcsuite/btcd/btcec/v2 v2.3.6/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ=
26 | github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
27 | github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
28 | github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
29 | github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c=
30 | github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE=
31 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
32 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
33 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
34 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
35 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
36 | github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
37 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
38 | github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
39 | github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
40 | github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
41 | github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
42 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
43 | github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
44 | github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
45 | github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
46 | github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
47 | github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
48 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
49 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
50 | github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
51 | github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
52 | github.com/consensys/gnark-crypto v0.19.2 h1:qrEAIXq3T4egxqiliFFoNrepkIWVEeIYwt3UL0fvS80=
53 | github.com/consensys/gnark-crypto v0.19.2/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0=
54 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
55 | github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
56 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
57 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
58 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
59 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
60 | github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
61 | github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
62 | github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
63 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
64 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
65 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
66 | github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
67 | github.com/ethereum/go-ethereum v1.16.7 h1:qeM4TvbrWK0UC0tgkZ7NiRsmBGwsjqc64BHo20U59UQ=
68 | github.com/ethereum/go-ethereum v1.16.7/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk=
69 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
70 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
71 | github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
72 | github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
73 | github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
74 | github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
75 | github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
76 | github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
77 | github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
78 | github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
79 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
80 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
81 | github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
82 | github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
83 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
84 | github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
85 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
86 | github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
87 | github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
88 | github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI=
89 | github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
90 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
91 | github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
92 | github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
93 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
94 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
95 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
96 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
97 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
98 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
99 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
100 | github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
101 | github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
102 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
103 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
104 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
105 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
106 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
107 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
108 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
109 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
110 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
111 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
112 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
113 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
114 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
115 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
116 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
117 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
118 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
119 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
120 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
121 | github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
122 | github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
123 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
124 | github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
125 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
126 | github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
127 | github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
128 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
129 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
130 | github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
131 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
132 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
133 | github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
134 | github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
135 | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
136 | github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
137 | github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
138 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
139 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
140 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
141 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
142 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
143 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
144 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
145 | github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
146 | github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
147 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
148 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
149 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
150 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
151 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
152 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
153 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
154 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
155 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
156 | github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
157 | github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
158 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
159 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
160 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
161 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
162 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
163 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
164 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
165 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
166 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
167 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
168 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
169 | github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
170 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
171 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
172 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
173 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
174 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
175 | github.com/penglongli/gin-metrics v0.1.13 h1:a1wyrXcbUVxL5w4c2TSv+9kyQA9qM1o23h0V6SdSHgQ=
176 | github.com/penglongli/gin-metrics v0.1.13/go.mod h1:VEmSyx/9TwUG50IsPCgjMKOUuGO74V2lmkLZ6x1Dlko=
177 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
178 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
179 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
180 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
181 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
182 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
183 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
184 | github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
185 | github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
186 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
187 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
188 | github.com/prometheus/common v0.52.2 h1:LW8Vk7BccEdONfrJBDffQGRtpSzi5CQaRZGtboOO2ck=
189 | github.com/prometheus/common v0.52.2/go.mod h1:lrWtQx+iDfn2mbH5GUzlH9TSHyfZpHkSiG1W7y3sF2Q=
190 | github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
191 | github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
192 | github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
193 | github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
194 | github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE=
195 | github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
196 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
197 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
198 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
199 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
200 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
201 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
202 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
203 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
204 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
205 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
206 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
207 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
208 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
209 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
210 | github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
211 | github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
212 | github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
213 | github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
214 | github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
215 | github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
216 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
217 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
218 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
219 | github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
220 | github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
221 | github.com/utxorpc/go-codegen v0.18.1 h1:2eenzXCkqvB2+g8MCq70MBR6koWs9CeTihZ0AqUvLDY=
222 | github.com/utxorpc/go-codegen v0.18.1/go.mod h1:DFij3zIGDM39BYCuzrz1rSuO3kTIIiHglWV0043wQxo=
223 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
224 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
225 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
226 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
227 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
228 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
229 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
230 | go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
231 | go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
232 | golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
233 | golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
234 | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
235 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
236 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
237 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
238 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
239 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
240 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
241 | golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
242 | golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
243 | golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
244 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
245 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
246 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
247 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
248 | golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
249 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
250 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
251 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
252 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
253 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
254 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
255 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
256 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
257 | golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
258 | golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
259 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
260 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
261 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
262 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
263 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
264 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
265 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
266 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
267 | golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
268 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
269 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
270 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
271 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
272 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
273 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
274 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
275 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
276 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
277 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
278 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
279 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
280 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
281 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
282 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
283 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
284 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
285 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
286 | golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
287 | golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
288 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
289 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
290 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
291 | golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
292 | golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
293 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
294 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
295 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
296 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
297 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
298 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
299 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
300 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
301 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
302 | google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
303 | google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
304 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
305 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
306 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
307 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
308 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
309 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
310 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
311 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
312 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
313 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
314 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
315 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
316 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
317 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
318 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
319 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
320 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
321 |
--------------------------------------------------------------------------------