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