├── 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 | Tx Submit API 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 | Tx Submit API 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 |
49 |
50 |
51 |
52 |
53 |
54 |
Blink Labs Software
55 |
56 |
57 |
58 |
59 | Blink Labs Software: tx-submit-api 60 |
61 | 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 | --------------------------------------------------------------------------------