├── .github
├── CODEOWNERS
├── assets
│ ├── tx-submit-api-20241120.png
│ └── tx-submit-api-logo.png
├── dependabot.yml
└── workflows
│ ├── ci-docker.yml
│ ├── conventional-commits.yml
│ ├── go-test.yml
│ ├── golangci-lint.yml
│ └── publish.yml
├── .gitignore
├── .golangci.yml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── cmd
└── tx-submit-api
│ └── main.go
├── config.yaml.example
├── docs
├── docs.go
├── iohk.yaml
├── swagger.json
└── swagger.yaml
├── go.mod
├── go.sum
├── internal
├── api
│ ├── api.go
│ └── static
│ │ ├── index.html
│ │ └── txsubmit-logo.png
├── config
│ └── config.go
├── logging
│ └── logging.go
└── version
│ └── version.go
└── submit
└── tx.go
/.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 |
--------------------------------------------------------------------------------
/.github/assets/tx-submit-api-20241120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blinklabs-io/tx-submit-api/c41d7825b7c9864805ccaf3cee7252a43edd7cb4/.github/assets/tx-submit-api-20241120.png
--------------------------------------------------------------------------------
/.github/assets/tx-submit-api-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blinklabs-io/tx-submit-api/c41d7825b7c9864805ccaf3cee7252a43edd7cb4/.github/assets/tx-submit-api-logo.png
--------------------------------------------------------------------------------
/.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/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@v4
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 |
--------------------------------------------------------------------------------
/.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@v4
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.23.x, 1.24.x]
20 | platform: [ubuntu-latest]
21 | runs-on: ${{ matrix.platform }}
22 | steps:
23 | - uses: actions/checkout@v4
24 | - uses: actions/setup-go@v5
25 | with:
26 | go-version: ${{ matrix.go-version }}
27 | - name: go-test
28 | run: go test ./...
29 |
--------------------------------------------------------------------------------
/.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@v4
19 | - uses: actions/setup-go@v5
20 | with:
21 | go-version: 1.23.x
22 | - name: golangci-lint
23 | uses: golangci/golangci-lint-action@v8
24 |
--------------------------------------------------------------------------------
/.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@v7
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@v4
61 | with:
62 | fetch-depth: '0'
63 | - uses: actions/setup-go@v5
64 | with:
65 | go-version: 1.23.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@v2
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@v4
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@v2
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@v2
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@v4
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@v7
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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 | - noctx
29 | - perfsprint
30 | - prealloc
31 | - protogetter
32 | - reassign
33 | - recvcheck
34 | - rowserrcheck
35 | - spancheck
36 | - sqlclosecheck
37 | - testifylint
38 | - unparam
39 | - usestdlibvars
40 | - whitespace
41 | - zerologlint
42 | disable:
43 | - depguard
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 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ghcr.io/blinklabs-io/go:1.24.2-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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # tx-submit-api
2 |
3 |
4 |

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

154 |
155 |
156 | ## Development
157 |
158 | There is a Makefile to provide some simple helpers.
159 |
160 | Run from checkout:
161 | ```
162 | go run .
163 | ```
164 |
165 | Create a binary:
166 | ```
167 | make
168 | ```
169 |
170 | Create a docker image:
171 | ```
172 | make image
173 | ```
174 |
--------------------------------------------------------------------------------
/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().Infof(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 | // Sync logger on exit
60 | defer func() {
61 | if err := logger.Sync(); err != nil {
62 | // We don't actually care about the error here, but we have to do something
63 | // to appease the linter
64 | return
65 | }
66 | }()
67 |
68 | logger.Infof("starting tx-submit-api %s", version.GetVersionString())
69 |
70 | // Configure max processes with our logger wrapper, toss undo func
71 | _, err = maxprocs.Set(maxprocs.Logger(logPrintf))
72 | if err != nil {
73 | // If we hit this, something really wrong happened
74 | logger.Errorf(err.Error())
75 | os.Exit(1)
76 | }
77 |
78 | // Start debug listener
79 | if cfg.Debug.ListenPort > 0 {
80 | logger.Infof(
81 | "starting debug listener on %s:%d",
82 | cfg.Debug.ListenAddress,
83 | cfg.Debug.ListenPort,
84 | )
85 | go func() {
86 | debugger := &http.Server{
87 | Addr: fmt.Sprintf(
88 | "%s:%d",
89 | cfg.Debug.ListenAddress,
90 | cfg.Debug.ListenPort,
91 | ),
92 | ReadHeaderTimeout: 60 * time.Second,
93 | }
94 | err := debugger.ListenAndServe()
95 | if err != nil {
96 | logger.Fatalf("failed to start debug listener: %s", err)
97 | }
98 | }()
99 | }
100 |
101 | // Start API listener
102 | if err := api.Start(cfg); err != nil {
103 | logger.Fatalf("failed to start API: %s", err)
104 | }
105 |
106 | // Wait forever
107 | select {}
108 | }
109 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/blinklabs-io/tx-submit-api
2 |
3 | go 1.23.6
4 |
5 | toolchain go1.24.1
6 |
7 | require (
8 | github.com/blinklabs-io/gouroboros v0.121.0
9 | github.com/fxamacker/cbor/v2 v2.8.0
10 | github.com/gin-contrib/cors v1.7.5
11 | github.com/gin-contrib/zap v1.1.5
12 | github.com/gin-gonic/gin v1.10.1
13 | github.com/kelseyhightower/envconfig v1.4.0
14 | github.com/penglongli/gin-metrics v0.1.13
15 | github.com/swaggo/files v1.0.1
16 | github.com/swaggo/gin-swagger v1.6.0
17 | github.com/swaggo/swag v1.16.4
18 | go.uber.org/automaxprocs v1.6.0
19 | go.uber.org/zap v1.27.0
20 | gopkg.in/yaml.v2 v2.4.0
21 | )
22 |
23 | require (
24 | filippo.io/edwards25519 v1.1.0 // indirect
25 | github.com/KyleBanks/depth v1.2.1 // indirect
26 | github.com/beorn7/perks v1.0.1 // indirect
27 | github.com/bits-and-blooms/bitset v1.8.0 // indirect
28 | github.com/btcsuite/btcd/btcutil v1.1.6 // indirect
29 | github.com/bytedance/sonic v1.13.2 // indirect
30 | github.com/bytedance/sonic/loader v0.2.4 // indirect
31 | github.com/cespare/xxhash/v2 v2.3.0 // indirect
32 | github.com/cloudwego/base64x v0.1.5 // indirect
33 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect
34 | github.com/gin-contrib/sse v1.0.0 // indirect
35 | github.com/go-openapi/jsonpointer v0.19.5 // indirect
36 | github.com/go-openapi/jsonreference v0.20.0 // indirect
37 | github.com/go-openapi/spec v0.20.7 // indirect
38 | github.com/go-openapi/swag v0.22.3 // indirect
39 | github.com/go-playground/locales v0.14.1 // indirect
40 | github.com/go-playground/universal-translator v0.18.1 // indirect
41 | github.com/go-playground/validator/v10 v10.26.0 // indirect
42 | github.com/goccy/go-json v0.10.5 // indirect
43 | github.com/jinzhu/copier v0.4.0 // indirect
44 | github.com/josharian/intern v1.0.0 // indirect
45 | github.com/json-iterator/go v1.1.12 // indirect
46 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect
47 | github.com/leodido/go-urn v1.4.0 // indirect
48 | github.com/mailru/easyjson v0.7.7 // indirect
49 | github.com/mattn/go-isatty v0.0.20 // indirect
50 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
51 | github.com/modern-go/reflect2 v1.0.2 // indirect
52 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect
53 | github.com/pkg/errors v0.9.1 // indirect
54 | github.com/prometheus/client_golang v1.19.0 // indirect
55 | github.com/prometheus/client_model v0.6.1 // indirect
56 | github.com/prometheus/common v0.52.2 // indirect
57 | github.com/prometheus/procfs v0.13.0 // indirect
58 | github.com/rogpeppe/go-internal v1.12.0 // indirect
59 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
60 | github.com/ugorji/go/codec v1.2.12 // indirect
61 | github.com/utxorpc/go-codegen v0.16.0 // indirect
62 | github.com/x448/float16 v0.8.4 // indirect
63 | go.uber.org/multierr v1.11.0 // indirect
64 | golang.org/x/arch v0.15.0 // indirect
65 | golang.org/x/crypto v0.38.0 // indirect
66 | golang.org/x/net v0.38.0 // indirect
67 | golang.org/x/sys v0.33.0 // indirect
68 | golang.org/x/text v0.25.0 // indirect
69 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
70 | google.golang.org/protobuf v1.36.6 // indirect
71 | gopkg.in/yaml.v3 v3.0.1 // indirect
72 | )
73 |
--------------------------------------------------------------------------------
/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/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
6 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
7 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
8 | github.com/bits-and-blooms/bitset v1.8.0 h1:FD+XqgOZDUxxZ8hzoBFuV9+cGWY9CslN6d5MS5JVb4c=
9 | github.com/bits-and-blooms/bitset v1.8.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
10 | github.com/blinklabs-io/gouroboros v0.121.0 h1:Hb8amqG2dOztE1r5cuZAUUKSUzusDuffdGgSSh+qIfs=
11 | github.com/blinklabs-io/gouroboros v0.121.0/go.mod h1:hAJS7mv7dYMbjXujmr6X8pJIzbYvDQIoQo10orJiOuo=
12 | github.com/blinklabs-io/ouroboros-mock v0.3.8 h1:+DAt2rx0ouZUxee5DBMgZq3I1+ZdxFSHG9g3tYl/FKU=
13 | github.com/blinklabs-io/ouroboros-mock v0.3.8/go.mod h1:UwQIf4KqZwO13P9d90fbi3UL/X7JaJfeEbqk+bEeFQA=
14 | github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
15 | github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
16 | github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
17 | github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg=
18 | github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
19 | github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
20 | github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
21 | github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
22 | github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
23 | github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c=
24 | github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE=
25 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
26 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
27 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
28 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
29 | github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
30 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
31 | github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
32 | github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
33 | github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
34 | github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
35 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
36 | github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
37 | github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
38 | github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
39 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
40 | github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
41 | github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
42 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
43 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
44 | github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
45 | github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
46 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
47 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
48 | github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
49 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
50 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
51 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
52 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
53 | github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
54 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
55 | github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
56 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
57 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
58 | github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
59 | github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
60 | github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
61 | github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
62 | github.com/gin-contrib/cors v1.7.5 h1:cXC9SmofOrRg0w9PigwGlHG3ztswH6bqq4vJVXnvYMk=
63 | github.com/gin-contrib/cors v1.7.5/go.mod h1:4q3yi7xBEDDWKapjT2o1V7mScKDDr8k+jZ0fSquGoy0=
64 | github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
65 | github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
66 | github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
67 | github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
68 | github.com/gin-contrib/zap v1.1.5 h1:qKwhWb4DQgPriCl1AHLLob6hav/KUIctKXIjTmWIN3I=
69 | github.com/gin-contrib/zap v1.1.5/go.mod h1:lAchUtGz9M2K6xDr1rwtczyDrThmSx6c9F384T45iOE=
70 | github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
71 | github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
72 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
73 | github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
74 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
75 | github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
76 | github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
77 | github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI=
78 | github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
79 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
80 | github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
81 | github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
82 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
83 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
84 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
85 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
86 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
87 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
88 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
89 | github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
90 | github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
91 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
92 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
93 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
94 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
95 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
96 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
97 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
98 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
99 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
100 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
101 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
102 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
103 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
104 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
105 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
106 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
107 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
108 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
109 | github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
110 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
111 | github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
112 | github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
113 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
114 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
115 | github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
116 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
117 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
118 | github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
119 | github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
120 | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
121 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
122 | github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
123 | github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
124 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
125 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
126 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
127 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
128 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
129 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
130 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
131 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
132 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
133 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
134 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
135 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
136 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
137 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
138 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
139 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
140 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
141 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
142 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
143 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
144 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
145 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
146 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
147 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
148 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
149 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
150 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
151 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
152 | github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
153 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
154 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
155 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
156 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
157 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
158 | github.com/penglongli/gin-metrics v0.1.13 h1:a1wyrXcbUVxL5w4c2TSv+9kyQA9qM1o23h0V6SdSHgQ=
159 | github.com/penglongli/gin-metrics v0.1.13/go.mod h1:VEmSyx/9TwUG50IsPCgjMKOUuGO74V2lmkLZ6x1Dlko=
160 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
161 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
162 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
163 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
164 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
165 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
166 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
167 | github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
168 | github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
169 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
170 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
171 | github.com/prometheus/common v0.52.2 h1:LW8Vk7BccEdONfrJBDffQGRtpSzi5CQaRZGtboOO2ck=
172 | github.com/prometheus/common v0.52.2/go.mod h1:lrWtQx+iDfn2mbH5GUzlH9TSHyfZpHkSiG1W7y3sF2Q=
173 | github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
174 | github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
175 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
176 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
177 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
178 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
179 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
180 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
181 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
182 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
183 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
184 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
185 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
186 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
187 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
188 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
189 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
190 | github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
191 | github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
192 | github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
193 | github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
194 | github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
195 | github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
196 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
197 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
198 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
199 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
200 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
201 | github.com/utxorpc/go-codegen v0.16.0 h1:jPTyKtv2OI6Ms7U/goAYbaP6axAZ39vRmoWdjO/rkeM=
202 | github.com/utxorpc/go-codegen v0.16.0/go.mod h1:2Nwq1md4HEcO2guvTpH45slGHO2aGRbiXKx73FM65ow=
203 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
204 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
205 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
206 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
207 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
208 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
209 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
210 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
211 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
212 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
213 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
214 | golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
215 | golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
216 | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
217 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
218 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
219 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
220 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
221 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
222 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
223 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
224 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
225 | golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
226 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
227 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
228 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
229 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
230 | golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
231 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
232 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
233 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
234 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
235 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
236 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
237 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
238 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
239 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
240 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
241 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
242 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
243 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
244 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
245 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
246 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
247 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
248 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
249 | golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
250 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
251 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
252 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
253 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
254 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
255 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
256 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
257 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
258 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
259 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
260 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
261 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
262 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
263 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
264 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
265 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
266 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
267 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
268 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
269 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
270 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
271 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
272 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
273 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
274 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
275 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
276 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
277 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
278 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
279 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
280 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
281 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
282 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
283 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
284 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
285 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
286 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
287 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
288 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
289 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
290 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
291 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
292 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
293 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
294 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
295 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
296 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
297 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
298 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
299 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
300 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
301 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
302 |
--------------------------------------------------------------------------------
/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 | "time"
25 |
26 | ouroboros "github.com/blinklabs-io/gouroboros"
27 | "github.com/blinklabs-io/gouroboros/protocol/localtxsubmission"
28 | _ "github.com/blinklabs-io/tx-submit-api/docs" // docs is generated by Swag CLI
29 | "github.com/blinklabs-io/tx-submit-api/internal/config"
30 | "github.com/blinklabs-io/tx-submit-api/internal/logging"
31 | "github.com/blinklabs-io/tx-submit-api/submit"
32 | "github.com/fxamacker/cbor/v2"
33 | cors "github.com/gin-contrib/cors"
34 | ginzap "github.com/gin-contrib/zap"
35 | "github.com/gin-gonic/gin"
36 | "github.com/penglongli/gin-metrics/ginmetrics"
37 | swaggerFiles "github.com/swaggo/files" // swagger embed files
38 | ginSwagger "github.com/swaggo/gin-swagger" // gin-swagger middleware
39 | )
40 |
41 | //go:embed static
42 | var staticFS embed.FS
43 |
44 | // @title tx-submit-api
45 | // @version v0
46 | // @description Cardano Transaction Submit API
47 | // @BasePath /
48 | // @contact.name Blink Labs Software
49 | // @contact.url https://blinklabs.io
50 | // @contact.email support@blinklabs.io
51 | //
52 | // @license.name Apache 2.0
53 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html
54 | func Start(cfg *config.Config) error {
55 | // Standard logging
56 | logger := logging.GetLogger()
57 | if cfg.Tls.CertFilePath != "" && cfg.Tls.KeyFilePath != "" {
58 | logger.Infof(
59 | "starting API TLS listener on %s:%d",
60 | cfg.Api.ListenAddress,
61 | cfg.Api.ListenPort,
62 | )
63 | } else {
64 | logger.Infof(
65 | "starting API listener on %s:%d",
66 | cfg.Api.ListenAddress,
67 | cfg.Api.ListenPort,
68 | )
69 | }
70 | // Disable gin debug and color output
71 | gin.SetMode(gin.ReleaseMode)
72 | gin.DisableConsoleColor()
73 |
74 | // Configure API router
75 | router := gin.New()
76 | // Catch panics and return a 500
77 | router.Use(gin.Recovery())
78 | // Configure CORS
79 | corsConfig := cors.DefaultConfig()
80 | corsConfig.AllowAllOrigins = true
81 | corsConfig.AllowHeaders = []string{
82 | "hx-current-url",
83 | "hx-request",
84 | "hx-target",
85 | "hx-trigger",
86 | }
87 | router.Use(cors.New(corsConfig))
88 | // Access logging
89 | accessLogger := logging.GetAccessLogger()
90 | skipPaths := []string{}
91 | if cfg.Logging.Healthchecks {
92 | skipPaths = append(skipPaths, "/healthcheck")
93 | logger.Infof("disabling access logs for /healthcheck")
94 | }
95 | router.Use(ginzap.GinzapWithConfig(accessLogger, &ginzap.Config{
96 | TimeFormat: time.RFC3339,
97 | UTC: true,
98 | SkipPaths: skipPaths,
99 | }))
100 | router.Use(ginzap.RecoveryWithZap(accessLogger, true))
101 |
102 | // Configure static route
103 | fsys, err := fs.Sub(staticFS, "static")
104 | if err != nil {
105 | return err
106 | }
107 | router.StaticFS("/ui", http.FS(fsys))
108 | // Redirect from root
109 | router.GET("/", func(c *gin.Context) {
110 | c.Request.URL.Path = "/ui"
111 | router.HandleContext(c)
112 | })
113 |
114 | // Create a healthcheck (before metrics so it's not instrumented)
115 | router.GET("/healthcheck", handleHealthcheck)
116 | // Create a swagger endpoint (not instrumented)
117 | router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
118 |
119 | // Metrics
120 | metricsRouter := gin.New()
121 | // Configure CORS
122 | metricsRouter.Use(cors.New(corsConfig))
123 | metrics := ginmetrics.GetMonitor()
124 | // Set metrics path
125 | metrics.SetMetricPath("/")
126 | // Set metrics router
127 | metrics.Expose(metricsRouter)
128 | // Use metrics middleware without exposing path in main app router
129 | metrics.SetMetricPath("/metrics")
130 | metrics.Use(router)
131 |
132 | // Custom metrics
133 | failureMetric := &ginmetrics.Metric{
134 | // This is a Gauge because input-output-hk's is a gauge
135 | Type: ginmetrics.Gauge,
136 | Name: "tx_submit_fail_count",
137 | Description: "transactions failed",
138 | Labels: nil,
139 | }
140 | submittedMetric := &ginmetrics.Metric{
141 | // This is a Gauge because input-output-hk's is a gauge
142 | Type: ginmetrics.Gauge,
143 | Name: "tx_submit_count",
144 | Description: "transactions submitted",
145 | Labels: nil,
146 | }
147 | // Add to global monitor object
148 | _ = ginmetrics.GetMonitor().AddMetric(failureMetric)
149 | _ = ginmetrics.GetMonitor().AddMetric(submittedMetric)
150 | // Initialize metrics
151 | _ = ginmetrics.GetMonitor().
152 | GetMetric("tx_submit_fail_count").
153 | SetGaugeValue(nil, 0.0)
154 | _ = ginmetrics.GetMonitor().
155 | GetMetric("tx_submit_count").
156 | SetGaugeValue(nil, 0.0)
157 |
158 | // Start metrics listener
159 | go func() {
160 | // TODO: return error if we cannot initialize metrics
161 | logger.Infof("starting metrics listener on %s:%d",
162 | cfg.Metrics.ListenAddress,
163 | cfg.Metrics.ListenPort)
164 | _ = metricsRouter.Run(fmt.Sprintf("%s:%d",
165 | cfg.Metrics.ListenAddress,
166 | cfg.Metrics.ListenPort))
167 | }()
168 |
169 | // Configure API routes
170 | router.POST("/api/submit/tx", handleSubmitTx)
171 | router.GET("/api/hastx/:tx_hash", handleHasTx)
172 |
173 | // Start API listener
174 | if cfg.Tls.CertFilePath != "" && cfg.Tls.KeyFilePath != "" {
175 | return router.RunTLS(
176 | fmt.Sprintf("%s:%d", cfg.Api.ListenAddress, cfg.Api.ListenPort),
177 | cfg.Tls.CertFilePath,
178 | cfg.Tls.KeyFilePath,
179 | )
180 | } else {
181 | return router.Run(fmt.Sprintf("%s:%d",
182 | cfg.Api.ListenAddress,
183 | cfg.Api.ListenPort))
184 | }
185 | }
186 |
187 | func handleHealthcheck(c *gin.Context) {
188 | // TODO: add some actual health checking here
189 | c.JSON(200, gin.H{"failed": false})
190 | }
191 |
192 | // Path parameters for GET requests
193 | type TxHashPathParams struct {
194 | TxHash string `uri:"tx_hash" binding:"required"` // Transaction hash
195 | }
196 |
197 | // handleHasTx godoc
198 | //
199 | // @Summary HasTx
200 | // @Description Determine if a given transaction ID exists in the node mempool.
201 | // @Produce json
202 | // @Param tx_hash path string true "Transaction Hash"
203 | // @Success 200 {object} string "Ok"
204 | // @Failure 400 {object} string "Bad Request"
205 | // @Failure 404 {object} string "Not Found"
206 | // @Failure 415 {object} string "Unsupported Media Type"
207 | // @Failure 500 {object} string "Server Error"
208 | // @Router /api/hastx/{tx_hash} [get]
209 | func handleHasTx(c *gin.Context) {
210 | // First, initialize our configuration and loggers
211 | cfg := config.GetConfig()
212 | logger := logging.GetLogger()
213 |
214 | var uriParams TxHashPathParams
215 | if err := c.ShouldBindUri(&uriParams); err != nil {
216 | logger.Errorf("failed to bind transaction hash from path: %s", err)
217 | c.JSON(400, fmt.Sprintf("invalid transaction hash: %s", err))
218 | return
219 | }
220 |
221 | txHash := uriParams.TxHash
222 | // convert to cbor bytes
223 | cborData, err := cbor.Marshal(txHash)
224 | if err != nil {
225 | logger.Errorf("failed to encode transaction hash to CBOR: %s", err)
226 | c.JSON(
227 | 400,
228 | fmt.Sprintf("failed to encode transaction hash to CBOR: %s", err),
229 | )
230 | return
231 | }
232 |
233 | // Connect to cardano-node and check for transaction
234 | errorChan := make(chan error)
235 | oConn, err := ouroboros.NewConnection(
236 | ouroboros.WithNetworkMagic(uint32(cfg.Node.NetworkMagic)),
237 | ouroboros.WithErrorChan(errorChan),
238 | ouroboros.WithNodeToNode(false),
239 | )
240 | if err != nil {
241 | logger.Errorf("failure creating Ouroboros connection: %s", err)
242 | c.JSON(500, "failure communicating with node")
243 | return
244 | }
245 | if cfg.Node.Address != "" && cfg.Node.Port > 0 {
246 | if err := oConn.Dial("tcp", fmt.Sprintf("%s:%d", cfg.Node.Address, cfg.Node.Port)); err != nil {
247 | logger.Errorf("failure connecting to node via TCP: %s", err)
248 | c.JSON(500, "failure communicating with node")
249 | return
250 | }
251 | } else {
252 | if err := oConn.Dial("unix", cfg.Node.SocketPath); err != nil {
253 | logger.Errorf("failure connecting to node via UNIX socket: %s", err)
254 | c.JSON(500, "failure communicating with node")
255 | return
256 | }
257 | }
258 | // Start async error handler
259 | go func() {
260 | err, ok := <-errorChan
261 | if ok {
262 | logger.Errorf("failure communicating with node: %s", err)
263 | c.JSON(500, "failure communicating with node")
264 | }
265 | }()
266 | defer func() {
267 | // Close Ouroboros connection
268 | oConn.Close()
269 | }()
270 | hasTx, err := oConn.LocalTxMonitor().Client.HasTx(cborData)
271 | if err != nil {
272 | logger.Errorf("failure getting transaction: %s", err)
273 | c.JSON(500, fmt.Sprintf("failure getting transaction: %s", err))
274 | }
275 | if !hasTx {
276 | c.JSON(404, "transaction not found in mempool")
277 | return
278 | }
279 | c.JSON(200, "transaction found in mempool")
280 | }
281 |
282 | // handleSubmitTx godoc
283 | //
284 | // @Summary Submit Tx
285 | // @Description Submit an already serialized transaction to the network.
286 | // @Produce json
287 | // @Param Content-Type header string true "Content type" Enums(application/cbor)
288 | // @Success 202 {object} string "Ok"
289 | // @Failure 400 {object} string "Bad Request"
290 | // @Failure 415 {object} string "Unsupported Media Type"
291 | // @Failure 500 {object} string "Server Error"
292 | // @Router /api/submit/tx [post]
293 | func handleSubmitTx(c *gin.Context) {
294 | // First, initialize our configuration and loggers
295 | cfg := config.GetConfig()
296 | logger := logging.GetLogger()
297 | // Check our headers for content-type
298 | if c.ContentType() != "application/cbor" {
299 | // Log the error, return an error to the user, and increment failed count
300 | logger.Errorf("invalid request body, should be application/cbor")
301 | c.JSON(415, "invalid request body, should be application/cbor")
302 | _ = ginmetrics.GetMonitor().GetMetric("tx_submit_fail_count").Inc(nil)
303 | return
304 | }
305 | // Read raw transaction bytes from the request body and store in a byte array
306 | txRawBytes, err := io.ReadAll(c.Request.Body)
307 | if err != nil {
308 | // Log the error, return an error to the user, and increment failed count
309 | logger.Errorf("failed to read request body: %s", err)
310 | c.JSON(500, "failed to read request body")
311 | _ = ginmetrics.GetMonitor().GetMetric("tx_submit_fail_count").Inc(nil)
312 | return
313 | }
314 | // Close request body after read
315 | if c.Request.Body != nil {
316 | if err := c.Request.Body.Close(); err != nil {
317 | logger.Errorf("failed to close request body: %s", err)
318 | }
319 | }
320 | // Send TX
321 | errorChan := make(chan error)
322 | submitConfig := &submit.Config{
323 | ErrorChan: errorChan,
324 | NetworkMagic: cfg.Node.NetworkMagic,
325 | NodeAddress: cfg.Node.Address,
326 | NodePort: cfg.Node.Port,
327 | SocketPath: cfg.Node.SocketPath,
328 | Timeout: cfg.Node.Timeout,
329 | }
330 | txHash, err := submit.SubmitTx(submitConfig, txRawBytes)
331 | if err != nil {
332 | if c.GetHeader("Accept") == "application/cbor" {
333 | var txRejectErr *localtxsubmission.TransactionRejectedError
334 | if errors.As(err, &txRejectErr) {
335 | c.Data(400, "application/cbor", txRejectErr.ReasonCbor)
336 | } else {
337 | c.Data(500, "application/cbor", []byte{})
338 | }
339 | } else {
340 | if err.Error() != "" {
341 | c.JSON(400, err.Error())
342 | } else {
343 | c.JSON(400, fmt.Sprintf("%s", err))
344 | }
345 | }
346 | _ = ginmetrics.GetMonitor().GetMetric("tx_submit_fail_count").Inc(nil)
347 | return
348 | }
349 | // Start async error handler
350 | go func() {
351 | err, ok := <-errorChan
352 | if ok {
353 | logger.Errorf("failure communicating with node: %s", err)
354 | c.JSON(500, "failure communicating with node")
355 | _ = ginmetrics.GetMonitor().
356 | GetMetric("tx_submit_fail_count").
357 | Inc(nil)
358 | }
359 | }()
360 | // Return transaction ID
361 | c.JSON(202, txHash)
362 | // Increment custom metric
363 | _ = ginmetrics.GetMonitor().GetMetric("tx_submit_count").Inc(nil)
364 | }
365 |
--------------------------------------------------------------------------------
/internal/api/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Metrics Dashboard
7 |
8 |
9 |
10 |
25 |
37 |
38 |
41 |
42 |
43 | tx-submit-api metrics
44 |
45 |
48 |
56 |
57 |
58 |
59 | Blink Labs Software: tx-submit-api
60 |
61 |
68 | Loading metrics...
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
456 |
457 |
458 |
--------------------------------------------------------------------------------
/internal/api/static/txsubmit-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blinklabs-io/tx-submit-api/c41d7825b7c9864805ccaf3cee7252a43edd7cb4/internal/api/static/txsubmit-logo.png
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | "log"
19 | "time"
20 |
21 | "github.com/blinklabs-io/tx-submit-api/internal/config"
22 | "go.uber.org/zap"
23 | "go.uber.org/zap/zapcore"
24 | )
25 |
26 | type Logger = zap.SugaredLogger
27 |
28 | var globalLogger *Logger
29 |
30 | func Setup(cfg *config.LoggingConfig) {
31 | // Build our custom logging config
32 | loggerConfig := zap.NewProductionConfig()
33 | // Change timestamp key name
34 | loggerConfig.EncoderConfig.TimeKey = "timestamp"
35 | // Use a human readable time format
36 | loggerConfig.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout(
37 | time.RFC3339,
38 | )
39 |
40 | // Set level
41 | if cfg.Level != "" {
42 | level, err := zapcore.ParseLevel(cfg.Level)
43 | if err != nil {
44 | log.Fatalf("error configuring logger: %s", err)
45 | }
46 | loggerConfig.Level.SetLevel(level)
47 | }
48 |
49 | // Create the logger
50 | l, err := loggerConfig.Build()
51 | if err != nil {
52 | log.Fatal(err)
53 | }
54 |
55 | // Store the "sugared" version of the logger
56 | globalLogger = l.Sugar()
57 | }
58 |
59 | func GetLogger() *zap.SugaredLogger {
60 | return globalLogger
61 | }
62 |
63 | func GetDesugaredLogger() *zap.Logger {
64 | return globalLogger.Desugar()
65 | }
66 |
67 | func GetAccessLogger() *zap.Logger {
68 | return globalLogger.Desugar().
69 | With(zap.String("type", "access")).
70 | WithOptions(zap.WithCaller(false))
71 | }
72 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------