├── .dockerignore ├── .github ├── CODEOWNERS ├── 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 ├── assets ├── bluefin-logo.png └── genesis │ ├── mainnet.json │ └── preview.json ├── cmd └── bluefin │ └── main.go ├── go.mod ├── go.sum └── internal ├── config ├── config.go ├── networks.go └── profiles.go ├── indexer └── indexer.go ├── logging └── logging.go ├── miner ├── miner.go ├── miner_test.go └── worker.go ├── storage ├── storage.go └── trie.go ├── tx ├── context.go └── tx.go ├── version └── version.go └── wallet └── wallet.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .github/ 3 | .bluefin/ 4 | Dockerfile 5 | bluefin 6 | README.md 7 | -------------------------------------------------------------------------------- /.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/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 | GHCR_IMAGE_NAME: ghcr.io/blinklabs-io/bluefin 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | docker: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 https://github.com/actions/checkout/releases/tag/v4.2.2 19 | with: 20 | fetch-depth: '0' 21 | - name: qemu 22 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 https://github.com/docker/setup-qemu-action/releases/tag/v3.6.0 23 | - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 https://github.com/docker/setup-buildx-action/releases/tag/v3.10.0 24 | - id: meta 25 | uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 https://github.com/docker/metadata-action/releases/tag/v5.7.0 26 | with: 27 | images: ${{ env.GHCR_IMAGE_NAME }} 28 | - name: build 29 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 https://github.com/docker/build-push-action/releases/tag/v6.18.0 30 | with: 31 | context: . 32 | push: false 33 | ### TODO: test multiple platforms 34 | # platforms: linux/amd64,linux/arm64 35 | tags: ${{ steps.meta.outputs.tags }} 36 | labels: ${{ steps.meta.outputs.labels }} 37 | -------------------------------------------------------------------------------- /.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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 https://github.com/actions/checkout/releases/tag/v4.2.2 17 | - uses: webiny/action-conventional-commits@8bc41ff4e7d423d56fa4905f6ff79209a78776c7 # v1.3.0 https://github.com/webiny/action-conventional-commits/releases/tag/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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 https://github.com/actions/checkout/releases/tag/v4.2.2 24 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 https://github.com/actions/setup-go/releases/tag/v5.5.0 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 https://github.com/actions/checkout/releases/tag/v4.2.2 19 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 https://github.com/actions/setup-go/releases/tag/v5.5.0 20 | with: 21 | go-version: 1.23.x 22 | - name: golangci-lint 23 | uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 https://github.com/golangci/golangci-lint-action/releases/tag/v8.0.0 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@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 https://github.com/actions/github-script/releases/tag/v7.0.1 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 | include: 47 | - runner: macos-latest 48 | os: darwin 49 | arch: arm64 50 | - runner: ubuntu-latest 51 | os: freebsd 52 | arch: amd64 53 | - runner: ubuntu-latest 54 | os: freebsd 55 | arch: arm64 56 | - runner: ubuntu-latest 57 | os: linux 58 | arch: amd64 59 | - runner: ubuntu-latest 60 | os: linux 61 | arch: arm64 62 | - runner: ubuntu-latest 63 | os: windows 64 | arch: amd64 65 | - runner: ubuntu-latest 66 | os: windows 67 | arch: arm64 68 | runs-on: ${{ matrix.runner }} 69 | needs: [create-draft-release] 70 | permissions: 71 | actions: write 72 | attestations: write 73 | checks: write 74 | contents: write 75 | id-token: write 76 | packages: write 77 | statuses: write 78 | steps: 79 | - run: "echo \"RELEASE_TAG=${GITHUB_REF#refs/tags/}\" >> $GITHUB_ENV" 80 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 https://github.com/actions/checkout/releases/tag/v4.2.2 81 | with: 82 | fetch-depth: '0' 83 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 https://github.com/actions/setup-go/releases/tag/v5.5.0 84 | with: 85 | go-version: 1.23.x 86 | - name: Build binary 87 | run: GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} make build 88 | 89 | # Sign Windows build 90 | - name: Set up Java 91 | uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 https://github.com/actions/setup-java/releases/tag/v4.7.1 92 | if: ${{ startsWith(github.ref, 'refs/tags/') && matrix.os == 'windows' }} 93 | with: 94 | java-version: 17 95 | distribution: 'temurin' 96 | - id: 'auth' 97 | name: Authenticate with Google Cloud 98 | if: ${{ startsWith(github.ref, 'refs/tags/') && matrix.os == 'windows' }} 99 | uses: 'google-github-actions/auth@v2' 100 | with: 101 | credentials_json: '${{ secrets.CERTIFICATE_SA_CREDENTIALS }}' 102 | - name: Set up Cloud SDK 103 | if: ${{ startsWith(github.ref, 'refs/tags/') && matrix.os == 'windows' }} 104 | uses: 'google-github-actions/setup-gcloud@v2' 105 | - name: Sign windows binary 106 | if: ${{ startsWith(github.ref, 'refs/tags/') && matrix.os == 'windows' }} 107 | run: | 108 | echo "Downloading jsign.jar" 109 | curl -L -o jsign.jar https://github.com/ebourg/jsign/releases/download/6.0/jsign-6.0.jar 110 | echo "Verifying jsign.jar checksum" 111 | echo '05ca18d4ab7b8c2183289b5378d32860f0ea0f3bdab1f1b8cae5894fb225fa8a jsign.jar' | sha256sum -c 112 | echo "${{ secrets.CERTIFICATE_CHAIN }}" | base64 --decode > codesign-chain.pem 113 | set +x 114 | _filename=bluefin 115 | ACCESS_TOKEN=$(gcloud auth print-access-token) 116 | echo "::add-mask::$ACCESS_TOKEN" 117 | java -jar jsign.jar \ 118 | --storetype ${{ secrets.CERTIFICATE_STORE_TYPE }} \ 119 | --storepass "$ACCESS_TOKEN" \ 120 | --keystore ${{ secrets.CERTIFICATE_KEYSTORE }} \ 121 | --alias ${{ secrets.CERTIFICATE_KEY_NAME }} \ 122 | --certfile codesign-chain.pem \ 123 | --tsmode RFC3161 \ 124 | --tsaurl http://timestamp.globalsign.com/tsa/r6advanced1 \ 125 | ${_filename} 126 | unset ACCESS_TOKEN 127 | set -x 128 | echo "Signed Windows binary: ${_filename}" 129 | echo "Cleaning up certificate chain" 130 | rm -f codesign-chain.pem 131 | 132 | # Sign MacOS build 133 | 134 | - name: Create .app package and sign macos binary 135 | if: ${{ startsWith(github.ref, 'refs/tags/') && matrix.os == 'darwin' }} 136 | run: | 137 | echo "Decoding and importing Apple certificate..." 138 | echo -n "${{ secrets.APPLE_CERTIFICATE }}" | base64 --decode -o apple_certificate.p12 139 | security create-keychain -p "${{ secrets.APPLE_KEYCHAIN_PASSWORD }}" build.keychain 140 | security default-keychain -s build.keychain 141 | security set-keychain-settings -lut 21600 build.keychain 142 | security unlock-keychain -p "${{ secrets.APPLE_KEYCHAIN_PASSWORD }}" build.keychain 143 | security import apple_certificate.p12 -k build.keychain -P "${{ secrets.APPLE_CERTIFICATE_PASSWORD }}" -T /usr/bin/codesign 144 | security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "${{ secrets.APPLE_KEYCHAIN_PASSWORD }}" build.keychain 145 | echo "Packaging bluefin..." 146 | mkdir -p Bluefin.app/Contents/MacOS 147 | mkdir -p Bluefin.app/Contents/Resources 148 | cp bluefin Bluefin.app/Contents/MacOS/bluefin 149 | chmod +x Bluefin.app/Contents/MacOS/bluefin 150 | cat < Bluefin.app/Contents/Info.plist 151 | 152 | 153 | 154 | 155 | CFBundleExecutable 156 | bluefin 157 | CFBundleIdentifier 158 | com.blinklabssoftware.bluefin 159 | CFBundleName 160 | Bluefin 161 | CFBundleVersion 162 | ${{ env.RELEASE_TAG }} 163 | CFBundleShortVersionString 164 | ${{ env.RELEASE_TAG }} 165 | 166 | 167 | EOF 168 | /usr/bin/codesign --force -s "Developer ID Application: Blink Labs Software (${{ secrets.APPLE_TEAM_ID }})" --options runtime Bluefin.app -v 169 | xcrun notarytool store-credentials "notarytool-profile" --apple-id "${{ secrets.APPLE_ID }}" --team-id "${{ secrets.APPLE_TEAM_ID }}" --password "${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}" 170 | ditto -c -k --keepParent "Bluefin.app" "notarization.zip" 171 | xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait 172 | xcrun stapler staple "Bluefin.app" 173 | - name: Upload release asset 174 | if: startsWith(github.ref, 'refs/tags/') 175 | run: | 176 | _filename=bluefin-${{ env.RELEASE_TAG }}-${{ matrix.os }}-${{ matrix.arch }} 177 | if [[ ${{ matrix.os }} == windows ]]; then 178 | _filename=${_filename}.exe 179 | fi 180 | if [[ "${{ matrix.os }}" == "windows" || "${{ matrix.os }}" == "linux" || "${{ matrix.os }}" == "freebsd" ]]; then 181 | cp bluefin ${_filename} 182 | fi 183 | if [[ "${{ matrix.os }}" == "darwin" ]]; then 184 | _filename=bluefin-${{ env.RELEASE_TAG }}-${{ matrix.os }}-${{ matrix.arch }}.zip 185 | zip -r ${_filename} Bluefin.app 186 | fi 187 | curl \ 188 | -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ 189 | -H "Content-Type: application/octet-stream" \ 190 | --data-binary @${_filename} \ 191 | https://uploads.github.com/repos/${{ github.repository_owner }}/bluefin/releases/${{ needs.create-draft-release.outputs.RELEASE_ID }}/assets?name=${_filename} 192 | 193 | - name: Attest binary 194 | uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0 https://github.com/actions/attest-build-provenance/releases/tag/v2.3.0 195 | with: 196 | subject-path: 'bluefin' 197 | 198 | build-images: 199 | runs-on: ubuntu-latest 200 | needs: [create-draft-release] 201 | permissions: 202 | actions: write 203 | attestations: write 204 | checks: write 205 | contents: write 206 | id-token: write 207 | packages: write 208 | statuses: write 209 | steps: 210 | - run: "echo \"RELEASE_TAG=${GITHUB_REF#refs/tags/}\" >> $GITHUB_ENV" 211 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 https://github.com/actions/checkout/releases/tag/v4.2.2 212 | with: 213 | fetch-depth: '0' 214 | - name: Set up QEMU 215 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 https://github.com/docker/setup-qemu-action/releases/tag/v3.6.0 216 | - name: Set up Docker Buildx 217 | uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 https://github.com/docker/setup-buildx-action/releases/tag/v3.10.0 218 | - name: Login to Docker Hub 219 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 https://github.com/docker/login-action/releases/tag/v3.4.0 220 | with: 221 | username: blinklabs 222 | password: ${{ secrets.DOCKER_PASSWORD }} # uses token 223 | - name: Login to GHCR 224 | uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 https://github.com/docker/login-action/releases/tag/v3.4.0 225 | with: 226 | username: ${{ github.repository_owner }} 227 | password: ${{ secrets.GITHUB_TOKEN }} 228 | registry: ghcr.io 229 | - id: meta 230 | uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 https://github.com/docker/metadata-action/releases/tag/v5.7.0 231 | with: 232 | images: | 233 | blinklabs/bluefin 234 | ghcr.io/${{ github.repository }} 235 | tags: | 236 | # Only version, no revision 237 | type=match,pattern=v(.*)-(.*),group=1 238 | # branch 239 | type=ref,event=branch 240 | # semver 241 | type=semver,pattern={{version}} 242 | - name: Build images 243 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 https://github.com/docker/build-push-action/releases/tag/v6.18.0 244 | id: push 245 | with: 246 | outputs: "type=registry,push=true" 247 | platforms: linux/amd64,linux/arm64 248 | tags: ${{ steps.meta.outputs.tags }} 249 | labels: ${{ steps.meta.outputs.labels }} 250 | - name: Attest Docker Hub image 251 | uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0 https://github.com/actions/attest-build-provenance/releases/tag/v2.3.0 252 | with: 253 | subject-name: index.docker.io/blinklabs/bluefin 254 | subject-digest: ${{ steps.push.outputs.digest }} 255 | push-to-registry: true 256 | - name: Attest GHCR image 257 | uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0 https://github.com/actions/attest-build-provenance/releases/tag/v2.3.0 258 | with: 259 | subject-name: ghcr.io/${{ github.repository }} 260 | subject-digest: ${{ steps.push.outputs.digest }} 261 | push-to-registry: true 262 | # Update Docker Hub from README 263 | - name: Docker Hub Description 264 | uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4.0.2 https://github.com/peter-evans/dockerhub-description/releases/tag/v4.0.2 265 | with: 266 | username: blinklabs 267 | password: ${{ secrets.DOCKER_PASSWORD }} 268 | repository: blinklabs/bluefin 269 | readme-filepath: ./README.md 270 | short-description: "A $TUNA miner, written in Go" 271 | 272 | finalize-release: 273 | runs-on: ubuntu-latest 274 | permissions: 275 | contents: write 276 | needs: [create-draft-release, build-binaries, build-images] 277 | steps: 278 | - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 https://github.com/actions/github-script/releases/tag/v7.0.1 279 | if: startsWith(github.ref, 'refs/tags/') 280 | with: 281 | github-token: ${{ secrets.GITHUB_TOKEN }} 282 | script: | 283 | try { 284 | await github.rest.repos.updateRelease({ 285 | owner: context.repo.owner, 286 | repo: context.repo.repo, 287 | release_id: ${{ needs.create-draft-release.outputs.RELEASE_ID }}, 288 | draft: false, 289 | }); 290 | } catch (error) { 291 | core.setFailed(error.message); 292 | } 293 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | /bluefin 24 | 25 | # seed phrase... DO NOT REMOVE 26 | seed.txt 27 | 28 | # Storage dir for local testing 29 | /.bluefin 30 | -------------------------------------------------------------------------------- /.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 bluefin 8 | COPY --from=build /code/bluefin /bin/ 9 | # Create data dir owned by container user and use it as default dir 10 | VOLUME /data 11 | WORKDIR /data 12 | ENTRYPOINT ["bluefin"] 13 | -------------------------------------------------------------------------------- /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 2023 Blink Labs Software 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Determine root directory 2 | ROOT_DIR=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 3 | 4 | # Gather all .go files for use in dependencies below 5 | GO_FILES=$(shell find $(ROOT_DIR) -name '*.go') 6 | 7 | # Gather list of expected binaries 8 | BINARIES=$(shell cd $(ROOT_DIR)/cmd && ls -1 | grep -v ^common) 9 | 10 | # Extract Go module name from go.mod 11 | GOMODULE=$(shell grep ^module $(ROOT_DIR)/go.mod | awk '{ print $$2 }') 12 | 13 | # Set version strings based on git tag and current ref 14 | 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)'" 15 | 16 | .PHONY: build mod-tidy clean test 17 | 18 | # Alias for building program binary 19 | build: $(BINARIES) 20 | 21 | mod-tidy: 22 | # Needed to fetch new dependencies and add them to go.mod 23 | go mod tidy 24 | 25 | clean: 26 | rm -f $(BINARIES) 27 | 28 | format: mod-tidy 29 | go fmt ./... 30 | gofmt -s -w $(GO_FILES) 31 | 32 | golines: 33 | golines -w --ignore-generated --chain-split-dots --max-len=80 --reformat-tags . 34 | 35 | test: mod-tidy 36 | go test -v -race ./... 37 | 38 | bench: mod-tidy 39 | go test -v -bench=. ./... 40 | 41 | # Build our program binaries 42 | # Depends on GO_FILES to determine when rebuild is needed 43 | $(BINARIES): mod-tidy $(GO_FILES) 44 | CGO_ENABLED=0 go build \ 45 | $(GO_LDFLAGS) \ 46 | -o $(@) \ 47 | ./cmd/$(@) 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bluefin 2 | 3 |
4 | bluefin Logo 5 |
6 | 7 | A $TUNA miner, written in Go 8 | 9 | ## Running the miner 10 | 11 | Bluefin is self-contained and runs with no external dependencies. You can run it via 12 | the [Docker images](https://ghcr.io/blinklabs-io/bluefin) or binaries from the 13 | [releases page](https://github.com/blinklabs-io/bluefin/releases). 14 | 15 | Bluefin is designed to take its configuration from environment variables. All examples below 16 | show running the bluefin binary directly from the shell and will need to be adapted for use 17 | with Docker. 18 | 19 | When run with no configuration, bluefin defaults to mining TUNA v1 on `mainnet`. It will generate a new 20 | wallet and write the seed phrase to the `seed.txt` file in the current directory. 21 | 22 | ``` 23 | $ ./bluefin 24 | ... 25 | {"level":"info","timestamp":"2024-07-04T20:13:53-05:00","caller":"wallet/wallet.go:62","msg":"wrote generated mnemonic to seed.txt"} 26 | {"level":"info","timestamp":"2024-07-04T20:13:53-05:00","caller":"bluefin/main.go:73","msg":"loaded mnemonic for address: addr1..."} 27 | {"level":"info","timestamp":"2024-07-04T20:13:53-05:00","caller":"bluefin/main.go:79","msg":"starting indexer on mainnet"} 28 | ``` 29 | 30 | You can use the `NETWORK` and `PROFILE` environment variables to change the mode that bluefin operates in. 31 | For example, to mine TUNA v2 on `preview`: 32 | 33 | ``` 34 | $ NETWORK=preview PROFILE=tuna-v2 ./bluefin 35 | ``` 36 | 37 | If you want to provide your own wallet seed phrase, you can set the `MNEMONIC` environment variable or create the `seed.txt` file before 38 | running bluefin. 39 | 40 | ### Seeding the wallet 41 | 42 | If allowing bluefin to generate a new wallet, you will need to seed the wallet with some initial funds using the wallet address 43 | logged at startup. If the wallet already exists, you may need to send funds back to your own wallet so that they're visible to bluefin. 44 | The wallet will need at least 2 available UTxOs, one to cover TX fees, and another of at least 5 (t)ADA to use as collateral. 45 | 46 | ### Submitting TXs 47 | 48 | By default, bluefin will use the NtN (node-to-node) TxSubmission protocol to submit transactions directly to the Cardano network. 49 | This method has the downside of not providing any feedback if a transaction fails. You can use the `SUBMIT_URL` environment variable 50 | to specify the URL for a submit API to use instead, which will provide feedback about any transaction validation issues. 51 | 52 | ### Clearing the local data 53 | 54 | Bluefin stores its local data in `.bluefin/` in the current directory. If you run into a problem that requires clearing the data, you can 55 | delete this data and bluefin will re-sync from scratch. 56 | 57 | ## Development / Building 58 | 59 | This requires Go 1.19 or better is installed. You also need `make`. 60 | 61 | ```bash 62 | # Build 63 | make 64 | # Run 65 | ./bluefin 66 | ``` 67 | 68 | You can also run the code without building a binary, first 69 | ```bash 70 | go run ./cmd/bluefin 71 | ``` 72 | 73 | ## WE WANT YOU!!! 74 | 75 | We're looking for people to join this project and help get it off the ground. 76 | 77 | Discussion is on Discord at https://discord.gg/5fPRZnX4qW 78 | -------------------------------------------------------------------------------- /assets/bluefin-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blinklabs-io/bluefin/603611abb1d7cb18c7129bcf118b2d799e2f72c4/assets/bluefin-logo.png -------------------------------------------------------------------------------- /assets/genesis/mainnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "validator": "590f86590f830100003323232323232323232323222253330083370e9000180380089929998049919299980599b87480080084c8c8c8c94ccc03ccdc3a4000601c0022646464646464646464646464646464646464646464646464646464a66605466e1d2002302900313232533302c3370e900118158048991929998172999817199817002a504a22a66605c012266e24cdc0801800a4181f82a294052809929998179981280e919baf3302c302e001480000ac4c8c8c94ccc0d4c0e00084c8c8c8c8c94ccc0dccdc3a4008606c00226464a6660726464a66607c608200426464a66607a66e3c009221096c6f72642074756e610013370e00290010a50375a607c0046eb8c0f000458c0fc004c8c94ccc0eccdc3a4004002297adef6c601323756608200260720046072002646600200203a44a66607c0022980103d87a8000132323232533303f3371e05e004266e95200033043374c00297ae0133006006003375660800066eb8c0f8008c108008c10000454ccc0e4c8c8c94ccc0fcc1080084c8c8c8c94ccc100cdc7802245001325333044304700213232533304353330433371e00a066266e1c005200214a0266e3c009221096c6f72642074756e610014a06eb4c110008dd718210008b182280089929998221823802099192999821a99982199b8f00703313370e00290010a5013371e004911096c6f72642074756e610014a06eb4c110008dd718210008b18228019bab3041004375c607e0066eacc0fc010dd7181e8018b18200009820003181f0028991919baf0010033374a90001981f26010100003303e37520166607c9810105003303e4c10319ffff003303e4c10100003303e37500186607c9810100003303e4c10180004bd7019299981d19b87480000044c8c8c8c8c8c8c8c8c8c8c8c8c8c8c8c94ccc134c140008526163758609c002609c004609800260980046eb4c128004c128008dd6982400098240011bad30460013046002375a608800260880046eb8c108004c108008dd69820000981c0010b181c0008b0b181e800981a8008b181d800981d8011bab303900130390013030001163036001323300100101c22533303500114bd7009919299981a19baf3303030323303030320024800120003374a90011981c1ba90244bd7009981c00119802002000899802002000981c801181b8009919191801000980080111b9200137660542c66e00cdc199b810030014801000458dd6981900098150048b1bad30300013028003163370e900118151baa302e001302e002302c00130240053370e900118131baa302a001302a00230280013020003302600130260023024001301c002323300100100622533302200114bd6f7b630099191919299981199b8f489000021003133027337606ea4008dd3000998030030019bab3024003375c6044004604c0046048002604200260420026040002603e0046eacc074004c074004c070008dd6180d000980d000980c8011bac3017001300f005375c602a002601a0022c6026002602600460220026012008264646464a66601e66e1d2000300e00113232323300837586601c602000c9000119baf3300f30113300f30113300f301100148009200048000008cdd2a40046602a6ea40052f5c06eb8c054004c03400458c04c004c04c008c044004c02401088c8cc00400400c894ccc04400452809919299980818028010a51133004004001301500230130013008003149858c94ccc024cdc3a40000022a666018600e0062930b0a99980499b874800800454ccc030c01c00c5261616300700213223232533300c32323232323232323232323232323232323232533301f3370e9001180f0008991919191919191919191919191919191919191919299981a19b8748008c0cc0044c8c8c8c94ccc0ecc0f80084c8c8c8c8c8c8c8c8c8c8c8c8c8c8c8c8c94ccc124cdc3a4004609000626464a66609666e1d2002304a0091323232533304e533304e33304e0064a094454ccc1380284cdc499b8100400248303f0545280a50132323232325333053533305333710084002294454ccc14ccdc3800821099b8800204014a0264a6660a866e1cc8c8c94ccc15ccdc3a4004002290000991bad305d001305500230550013253330563370e90010008a6103d87a800013232323300100100222533305d00114c103d87a8000132323232533305e3371e911096c6f72642074756e610000213374a9000198311ba80014bd700998030030019bad305f003375c60ba00460c200460be0026eacc170004c150008c150004cc00408807d2002132325333059305c002132323232533305a533305a3371e00891010454554e410013370e006002294054ccc168c8c8c94ccc180c18c0084c8c8c8c94ccc184cdc7802245001325333065306800213232533306453330643371e00a05e266e1c005200214a0266e3c009221096c6f72642074756e610014a06eb4c194008dd718318008b183300089929998329834002099192999832299983219b8f00702f13370e00290010a5013371e004911096c6f72642074756e610014a06eb4c194008dd718318008b18330019bab3062004375c60c00066eacc180010dd7182f0018b18308009830810982f8100a99982d19b8748010c1640784c8c94ccc170cdc3a400060b6002264646464646464646464646464646464a6660de60e4004264a6660daa6660daa6660da66e1ccdc3030241803e9000099b8848000180528099191919191919299983a19b87001013153330743370e004022266e1d200000f14a02940dd6983a8011bad3073001333300505e060002001375a60e40046eb4c1c00054ccc1b94ccc1b8cdc4a401066e0d2080a0c88109001133710900019b8648202832204240045280a5ef6c601010100010104001533306e533306e33712900419b8300148202832204244cdc42400066e180052080a0c8810914a0297bdb181010400010101001337606ea0005301051a48190800003370266e001600801584c94ccc1b8cdc382e8068a99983719b8705b00b13370e002012294052819b81337000b00400ac2a6660da66e1c01808054ccc1b54ccc1b4cdc399b80060480080404cdc780700f0a501533306d337126e34dd98022410010266ebcdd3999999111119199980080082c8018011111191919299983ca99983c99b8800100b14a22a6660f266e1c02c0044cdc40050010a501533307c00613307d00c3307d00c33330070074bd70001000899299983e80089983f0069983f006999980400425eb8000c0084c8cc1fc038cc1fc038cccc02402400401000cc20004004c1fc0184c8cccc00400401c0180148888c8c8c94ccc200054ccc20004cdc40008090a5115333080013370e024002266e200440085280a99984180803099842008099999803803a5eb800080044c8cc21404050cccc02002000400c008c218040184018dd69840808011bad307f0013333011002001480092004375a60f40046eb4c1e0004cccc028008005200248020dd480f00d80e02d02e1ba7002161616162222323253330723370e66e0c009208080084800054ccc1c8cdc4a40f800a297bdb18103191000000102183e001337606ea0008dd419b800054800854ccc1c8cdc42400066e0c00520808008153330723371200a90020a5ef6c6010319ffff00010102001337606ea0cdc1800a40406ea0cdc0802a4004266ec0dd40009ba800533706002901019b833370466e08011202000200116375860e000260e000460dc00260dc0046eb4c1b0004c1b0008dd6983500098350011bad30680013068002375a60cc00260cc0046eb8c190004c190008dd69831000982d0008b1830000982c00f0b0b0b299982c99b88480e80045200013370690406457d0129991919180080091299982e99b89480280044cdc1240806600400466e04005200a13003001300100122533305b3371200290000a4004266e08cc008008cdc0800a4004900200099b8304b482834464dd6982c8011bae305700116305a001323253330563370e90010008a5eb7bdb1804c8dd5982e000982a001182a0009980081380f8b11191980080080191299982d0008a6103d87a8000132323232533305b3371e00e004266e9520003305f374c00297ae0133006006003375660b80066eb8c168008c178008c17000458dd6982a0011bad3052001323253330523370e66e180092004480004c8cdd81ba8001375000666e00cdc119b8e0030014820010cdc700199b80001480084c8cdd81ba8001375000666e00cdc019b823371c00600290402019b823371c00666e00005200248080cdc199b8e0033370000290022404066e0c005200432330010014800088c94ccc14ccdc3800a4000266e012004330030033370000490010a99982999b880014808052002148000cdc70018009919191801000980080111b92001376600266e952000330523752086660a46ea0104cc148dd481f998291ba803d330523750076660a46ea00e52f5c02c66e00cdc199b8100300148010004dd6982880098248048b1bad304f0013047003163370e900118249baa304d001304d002304b00130430053370e900118229baa304900130490023047001303f003304500130450023043001303b011304100130410023756607e002607e002606c0022c6078002646600200202444a666076002297ae013232533303a3375e6606c6070004900000509981f00119802002000899802002000981f801181e8009bae303a0013032001163302f303100348000dd5981b800981b801181a800981680099299981799b8748000c0b80044c8c8cc0b4c0bc00520023035001302d00116323300100100d22533303300114c0103d87a80001323253330323375e6605c60600049000009099ba548000cc0d80092f5c0266008008002606e004606a002646600200200c44a666064002297adef6c6013232323253330333371e911000021003133037337606ea4008dd3000998030030019bab3034003375c6064004606c0046068002606200260620026060002605e0046eacc0b4004c0b4004c0b0008dd61815000981500098148011bac3027001301f0053025001301d0011630230013023002302100130190123758603e002603e002603c0046eb4c070004c070008dd6980d000980d0011bad30180013018002375a602c002602c0046eb8c050004c050008dd6980900098050030a4c2c6eb800cc94ccc02ccdc3a4000002264646464646464646464646464646464a66603c60420042930b1bac301f001301f002301d001301d002375a603600260360046eb4c064004c064008dd6980b800980b8011bad30150013015002375c602600260260046eb4c044004c02401458c024010c034c018004cc0040052000222233330073370e0020060184666600a00a66e000112002300e001002002230053754002460066ea80055cd2ab9d5573caae7d5d02ba157449812bd8799fd8799f582021fc8e4f33ca92e38f78f9bbd84cef1c037b15a86665ddba4528c7ecbc60ac90ff00ff0001", 3 | "validatorHash": "279f842c33eed9054b9e3c70cd6a3b32298259c24b78b895cb41d91a", 4 | "validatorAddress": "addr1wynelppvx0hdjp2tnc78pnt28veznqjecf9h3wy4edqajxsg7hwsc", 5 | "boostrapHash": "e4390b57fd759b5961107b931dca6d826cb2c272f0f711e266df48d0afc3a441", 6 | "datum": "d8799f005820e4390b57fd759b5961107b931dca6d826cb2c272f0f711e266df48d0afc3a4410519ffff001b0000018a3350a9d80080ff", 7 | "outRef": { 8 | "txHash": "21fc8e4f33ca92e38f78f9bbd84cef1c037b15a86665ddba4528c7ecbc60ac90", 9 | "index": 0 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /assets/genesis/preview.json: -------------------------------------------------------------------------------- 1 | { 2 | "validator": "590f86590f830100003323232323232323232323222253330083370e9000180380089929998049919299980599b87480080084c8c8c8c94ccc03ccdc3a4000601c0022646464646464646464646464646464646464646464646464646464a66605466e1d2002302900313232533302c3370e900118158048991929998172999817199817002a504a22a66605c012266e24cdc0801800a4181f82a294052809929998179981280e919baf3302c302e001480000ac4c8c8c94ccc0d4c0e00084c8c8c8c8c94ccc0dccdc3a4008606c00226464a6660726464a66607c608200426464a66607a66e3c009221096c6f72642074756e610013370e00290010a50375a607c0046eb8c0f000458c0fc004c8c94ccc0eccdc3a4004002297adef6c601323756608200260720046072002646600200203a44a66607c0022980103d87a8000132323232533303f3371e05e004266e95200033043374c00297ae0133006006003375660800066eb8c0f8008c108008c10000454ccc0e4c8c8c94ccc0fcc1080084c8c8c8c94ccc100cdc7802245001325333044304700213232533304353330433371e00a066266e1c005200214a0266e3c009221096c6f72642074756e610014a06eb4c110008dd718210008b182280089929998221823802099192999821a99982199b8f00703313370e00290010a5013371e004911096c6f72642074756e610014a06eb4c110008dd718210008b18228019bab3041004375c607e0066eacc0fc010dd7181e8018b18200009820003181f0028991919baf0010033374a90001981f26010100003303e37520166607c9810105003303e4c10319ffff003303e4c10100003303e37500186607c9810100003303e4c10180004bd7019299981d19b87480000044c8c8c8c8c8c8c8c8c8c8c8c8c8c8c8c94ccc134c140008526163758609c002609c004609800260980046eb4c128004c128008dd6982400098240011bad30460013046002375a608800260880046eb8c108004c108008dd69820000981c0010b181c0008b0b181e800981a8008b181d800981d8011bab303900130390013030001163036001323300100101c22533303500114bd7009919299981a19baf3303030323303030320024800120003374a90011981c1ba90244bd7009981c00119802002000899802002000981c801181b8009919191801000980080111b9200137660542c66e00cdc199b810030014801000458dd6981900098150048b1bad30300013028003163370e900118151baa302e001302e002302c00130240053370e900118131baa302a001302a00230280013020003302600130260023024001301c002323300100100622533302200114bd6f7b630099191919299981199b8f489000021003133027337606ea4008dd3000998030030019bab3024003375c6044004604c0046048002604200260420026040002603e0046eacc074004c074004c070008dd6180d000980d000980c8011bac3017001300f005375c602a002601a0022c6026002602600460220026012008264646464a66601e66e1d2000300e00113232323300837586601c602000c9000119baf3300f30113300f30113300f301100148009200048000008cdd2a40046602a6ea40052f5c06eb8c054004c03400458c04c004c04c008c044004c02401088c8cc00400400c894ccc04400452809919299980818028010a51133004004001301500230130013008003149858c94ccc024cdc3a40000022a666018600e0062930b0a99980499b874800800454ccc030c01c00c5261616300700213223232533300c32323232323232323232323232323232323232533301f3370e9001180f0008991919191919191919191919191919191919191919299981a19b8748008c0cc0044c8c8c8c94ccc0ecc0f80084c8c8c8c8c8c8c8c8c8c8c8c8c8c8c8c8c94ccc124cdc3a4004609000626464a66609666e1d2002304a0091323232533304e533304e33304e0064a094454ccc1380284cdc499b8100400248303f0545280a50132323232325333053533305333710084002294454ccc14ccdc3800821099b8800204014a0264a6660a866e1cc8c8c94ccc15ccdc3a4004002290000991bad305d001305500230550013253330563370e90010008a6103d87a800013232323300100100222533305d00114c103d87a8000132323232533305e3371e911096c6f72642074756e610000213374a9000198311ba80014bd700998030030019bad305f003375c60ba00460c200460be0026eacc170004c150008c150004cc00408807d2002132325333059305c002132323232533305a533305a3371e00891010454554e410013370e006002294054ccc168c8c8c94ccc180c18c0084c8c8c8c94ccc184cdc7802245001325333065306800213232533306453330643371e00a05e266e1c005200214a0266e3c009221096c6f72642074756e610014a06eb4c194008dd718318008b183300089929998329834002099192999832299983219b8f00702f13370e00290010a5013371e004911096c6f72642074756e610014a06eb4c194008dd718318008b18330019bab3062004375c60c00066eacc180010dd7182f0018b18308009830810982f8100a99982d19b8748010c1640784c8c94ccc170cdc3a400060b6002264646464646464646464646464646464a6660de60e4004264a6660daa6660daa6660da66e1ccdc3030241803e9000099b8848000180528099191919191919299983a19b87001013153330743370e004022266e1d200000f14a02940dd6983a8011bad3073001333300505e060002001375a60e40046eb4c1c00054ccc1b94ccc1b8cdc4a401066e0d2080a0c88109001133710900019b8648202832204240045280a5ef6c601010100010104001533306e533306e33712900419b8300148202832204244cdc42400066e180052080a0c8810914a0297bdb181010400010101001337606ea0005301051a48190800003370266e001600801584c94ccc1b8cdc382e8068a99983719b8705b00b13370e002012294052819b81337000b00400ac2a6660da66e1c01808054ccc1b54ccc1b4cdc399b80060480080404cdc780700f0a501533306d337126e34dd98022410010266ebcdd3999999111119199980080082c8018011111191919299983ca99983c99b8800100b14a22a6660f266e1c02c0044cdc40050010a501533307c00613307d00c3307d00c33330070074bd70001000899299983e80089983f0069983f006999980400425eb8000c0084c8cc1fc038cc1fc038cccc02402400401000cc20004004c1fc0184c8cccc00400401c0180148888c8c8c94ccc200054ccc20004cdc40008090a5115333080013370e024002266e200440085280a99984180803099842008099999803803a5eb800080044c8cc21404050cccc02002000400c008c218040184018dd69840808011bad307f0013333011002001480092004375a60f40046eb4c1e0004cccc028008005200248020dd480f00d80e02d02e1ba7002161616162222323253330723370e66e0c009208080084800054ccc1c8cdc4a40f800a297bdb18103191000000102183e001337606ea0008dd419b800054800854ccc1c8cdc42400066e0c00520808008153330723371200a90020a5ef6c6010319ffff00010102001337606ea0cdc1800a40406ea0cdc0802a4004266ec0dd40009ba800533706002901019b833370466e08011202000200116375860e000260e000460dc00260dc0046eb4c1b0004c1b0008dd6983500098350011bad30680013068002375a60cc00260cc0046eb8c190004c190008dd69831000982d0008b1830000982c00f0b0b0b299982c99b88480e80045200013370690406457d0129991919180080091299982e99b89480280044cdc1240806600400466e04005200a13003001300100122533305b3371200290000a4004266e08cc008008cdc0800a4004900200099b8304b482834464dd6982c8011bae305700116305a001323253330563370e90010008a5eb7bdb1804c8dd5982e000982a001182a0009980081380f8b11191980080080191299982d0008a6103d87a8000132323232533305b3371e00e004266e9520003305f374c00297ae0133006006003375660b80066eb8c168008c178008c17000458dd6982a0011bad3052001323253330523370e66e180092004480004c8cdd81ba8001375000666e00cdc119b8e0030014820010cdc700199b80001480084c8cdd81ba8001375000666e00cdc019b823371c00600290402019b823371c00666e00005200248080cdc199b8e0033370000290022404066e0c005200432330010014800088c94ccc14ccdc3800a4000266e012004330030033370000490010a99982999b880014808052002148000cdc70018009919191801000980080111b92001376600266e952000330523752086660a46ea0104cc148dd481f998291ba803d330523750076660a46ea00e52f5c02c66e00cdc199b8100300148010004dd6982880098248048b1bad304f0013047003163370e900118249baa304d001304d002304b00130430053370e900118229baa304900130490023047001303f003304500130450023043001303b011304100130410023756607e002607e002606c0022c6078002646600200202444a666076002297ae013232533303a3375e6606c6070004900000509981f00119802002000899802002000981f801181e8009bae303a0013032001163302f303100348000dd5981b800981b801181a800981680099299981799b8748000c0b80044c8c8cc0b4c0bc00520023035001302d00116323300100100d22533303300114c0103d87a80001323253330323375e6605c60600049000009099ba548000cc0d80092f5c0266008008002606e004606a002646600200200c44a666064002297adef6c6013232323253330333371e911000021003133037337606ea4008dd3000998030030019bab3034003375c6064004606c0046068002606200260620026060002605e0046eacc0b4004c0b4004c0b0008dd61815000981500098148011bac3027001301f0053025001301d0011630230013023002302100130190123758603e002603e002603c0046eb4c070004c070008dd6980d000980d0011bad30180013018002375a602c002602c0046eb8c050004c050008dd6980900098050030a4c2c6eb800cc94ccc02ccdc3a4000002264646464646464646464646464646464a66603c60420042930b1bac301f001301f002301d001301d002375a603600260360046eb4c064004c064008dd6980b800980b8011bad30150013015002375c602600260260046eb4c044004c02401458c024010c034c018004cc0040052000222233330073370e0020060184666600a00a66e000112002300e001002002230053754002460066ea80055cd2ab9d5573caae7d5d02ba157449812bd8799fd8799f5820580c37415cf5b98da27f845ed853f2e4fda0034c1441c99eb3a7f333483ce99dff02ff0001", 3 | "validatorHash": "502fbfbdafc7ddada9c335bd1440781e5445d08bada77dc2032866a6", 4 | "validatorAddress": "addr_test1wpgzl0aa4lramtdfcv6m69zq0q09g3ws3wk6wlwzqv5xdfsdcf2qa", 5 | "boostrapHash": "2aa538d5e7849d0e102f86a0ce4498c51c099ef06a61cd59d881d9e44a6c8ef1", 6 | "datum": "d8799f0058202aa538d5e7849d0e102f86a0ce4498c51c099ef06a61cd59d881d9e44a6c8ef10519ffff001b0000018a32f933380080ff", 7 | "outRef": { 8 | "txHash": "580c37415cf5b98da27f845ed853f2e4fda0034c1441c99eb3a7f333483ce99d", 9 | "index": 2 10 | } 11 | } -------------------------------------------------------------------------------- /cmd/bluefin/main.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 main 16 | 17 | import ( 18 | "flag" 19 | "fmt" 20 | "log/slog" 21 | "os" 22 | 23 | "github.com/blinklabs-io/bluefin/internal/config" 24 | "github.com/blinklabs-io/bluefin/internal/indexer" 25 | "github.com/blinklabs-io/bluefin/internal/logging" 26 | "github.com/blinklabs-io/bluefin/internal/storage" 27 | "github.com/blinklabs-io/bluefin/internal/version" 28 | "github.com/blinklabs-io/bluefin/internal/wallet" 29 | "go.uber.org/automaxprocs/maxprocs" 30 | ) 31 | 32 | var cmdlineFlags struct { 33 | configFile string 34 | debug bool 35 | } 36 | 37 | func slogPrintf(format string, v ...any) { 38 | slog.Info(fmt.Sprintf(format, v...)) 39 | } 40 | 41 | func main() { 42 | flag.StringVar( 43 | &cmdlineFlags.configFile, 44 | "config", 45 | "", 46 | "path to config file to load", 47 | ) 48 | flag.BoolVar( 49 | &cmdlineFlags.debug, 50 | "debug", 51 | false, 52 | "enable debug logging", 53 | ) 54 | flag.Parse() 55 | 56 | // Load config 57 | cfg, err := config.Load(cmdlineFlags.configFile) 58 | if err != nil { 59 | fmt.Printf("Failed to load config: %s\n", err) 60 | os.Exit(1) 61 | } 62 | 63 | // Configure logger 64 | logging.Configure() 65 | logger := logging.GetLogger() 66 | slog.SetDefault(logger) 67 | 68 | slog.Info( 69 | fmt.Sprintf("bluefin %s started", version.GetVersionString()), 70 | ) 71 | 72 | // Configure max processes with our logger wrapper, toss undo func 73 | _, err = maxprocs.Set(maxprocs.Logger(slogPrintf)) 74 | if err != nil { 75 | // If we hit this, something really wrong happened 76 | slog.Error(err.Error()) 77 | os.Exit(1) 78 | } 79 | 80 | // Load storage 81 | if err := storage.GetStorage().Load(); err != nil { 82 | slog.Error( 83 | fmt.Sprintf("failed to load storage: %s", err), 84 | ) 85 | os.Exit(1) 86 | } 87 | 88 | // Setup wallet 89 | wallet.Setup() 90 | bursa := wallet.GetWallet() 91 | slog.Info( 92 | "loaded mnemonic for address: " + bursa.PaymentAddress, 93 | ) 94 | 95 | // Fake Tx 96 | // tx.SendTx([]byte("foo")) 97 | 98 | // Start indexer 99 | slog.Info( 100 | "starting indexer on " + cfg.Network, 101 | ) 102 | if err := indexer.GetIndexer().Start(); err != nil { 103 | slog.Error( 104 | fmt.Sprintf("failed to start indexer: %s", err), 105 | ) 106 | os.Exit(1) 107 | } 108 | 109 | // Wait forever 110 | select {} 111 | } 112 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/blinklabs-io/bluefin 2 | 3 | go 1.23.6 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/Salvionied/apollo v1.1.1 9 | github.com/blinklabs-io/adder v0.30.1 10 | github.com/blinklabs-io/bursa v0.11.0 11 | github.com/blinklabs-io/cardano-models v0.5.1 12 | github.com/blinklabs-io/gouroboros v0.121.0 13 | github.com/blinklabs-io/merkle-patricia-forestry v0.1.3 14 | github.com/dgraph-io/badger/v4 v4.7.0 15 | github.com/kelseyhightower/envconfig v1.4.0 16 | github.com/minio/sha256-simd v1.0.1 17 | go.uber.org/automaxprocs v1.6.0 18 | golang.org/x/crypto v0.38.0 19 | gopkg.in/yaml.v2 v2.4.0 20 | ) 21 | 22 | // XXX: uncomment when testing local changes to bursa 23 | // replace github.com/blinklabs-io/bursa => ../bursa 24 | 25 | // XXX: uncomment when testing local changes to gouroboros 26 | // replace github.com/blinklabs-io/gouroboros => ../gouroboros 27 | 28 | // XXX: uncomment when testing local changes to adder 29 | // replace github.com/blinklabs-io/adder => ../adder 30 | 31 | // XXX: uncomment when testing local changes to cardano-models 32 | // replace github.com/blinklabs-io/cardano-models => ../cardano-models 33 | 34 | require ( 35 | filippo.io/edwards25519 v1.1.0 // indirect 36 | github.com/SundaeSwap-finance/kugo v1.2.0 // indirect 37 | github.com/SundaeSwap-finance/ogmigo/v6 v6.0.1 // indirect 38 | github.com/aws/aws-sdk-go v1.55.6 // indirect 39 | github.com/btcsuite/btcd/btcutil v1.1.6 // indirect 40 | github.com/buger/jsonparser v1.1.1 // indirect 41 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 42 | github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect 43 | github.com/dustin/go-humanize v1.0.1 // indirect 44 | github.com/fivebinaries/go-cardano-serialization v0.0.0-20220907134105-ec9b85086588 // indirect 45 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect 46 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 47 | github.com/go-logr/logr v1.4.2 // indirect 48 | github.com/go-logr/stdr v1.2.2 // indirect 49 | github.com/go-playground/locales v0.14.1 // indirect 50 | github.com/go-playground/universal-translator v0.18.1 // indirect 51 | github.com/go-playground/validator/v10 v10.26.0 // indirect 52 | github.com/google/flatbuffers v25.2.10+incompatible // indirect 53 | github.com/gorilla/websocket v1.5.3 // indirect 54 | github.com/jinzhu/copier v0.4.0 // indirect 55 | github.com/jmespath/go-jmespath v0.4.0 // indirect 56 | github.com/klauspost/compress v1.18.0 // indirect 57 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 58 | github.com/leodido/go-urn v1.4.0 // indirect 59 | github.com/maestro-org/go-sdk v1.2.1 // indirect 60 | github.com/tyler-smith/go-bip39 v1.1.0 // indirect 61 | github.com/utxorpc/go-codegen v0.16.0 // indirect 62 | github.com/x448/float16 v0.8.4 // indirect 63 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 64 | go.opentelemetry.io/otel v1.35.0 // indirect 65 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 66 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 67 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect 68 | golang.org/x/net v0.38.0 // indirect 69 | golang.org/x/sync v0.14.0 // indirect 70 | golang.org/x/sys v0.33.0 // indirect 71 | golang.org/x/text v0.25.0 // indirect 72 | google.golang.org/protobuf v1.36.6 // indirect 73 | ) 74 | -------------------------------------------------------------------------------- /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/Salvionied/apollo v1.1.1 h1:8Iw4m9bwLbDjebQbbkqNlfvyrjS0Oy3bRZuo2ra0Ifg= 4 | github.com/Salvionied/apollo v1.1.1/go.mod h1:v1jv3hFtZziqAk87yCJf7RxTa6F+HqfhKkHxcxHLYxw= 5 | github.com/SundaeSwap-finance/kugo v1.2.0 h1:55bPIMWx0CHlMBeiAePSfVLjdlZXwa/Mr5zVg/3z18w= 6 | github.com/SundaeSwap-finance/kugo v1.2.0/go.mod h1:ZPi5S5z1/OCYYznub01glx2tlyvvjltImspmEPCVPa0= 7 | github.com/SundaeSwap-finance/ogmigo/v6 v6.0.1 h1:Rx4tIx6zE4wTO5rslKJXgvi5dOlQzyU/3vxHGPiJUNE= 8 | github.com/SundaeSwap-finance/ogmigo/v6 v6.0.1/go.mod h1:llbznyK+r6E/KCyVyWoGYafWZyAPqAVYnyOlY+BIQVg= 9 | github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= 10 | github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= 11 | github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= 12 | github.com/blinklabs-io/adder v0.30.1 h1:Cc1fUjDDGl0VMk5eUaVqa8+vxwF4KmGxubnY2rk/grU= 13 | github.com/blinklabs-io/adder v0.30.1/go.mod h1:IycfpHYEXP6YV8SzDZLIEGTfLNcxX/Tqltfk2OrMqnY= 14 | github.com/blinklabs-io/bursa v0.11.0 h1:TKgDSqAvL8TO35avT13zNn+EymswedIj/0l4nMjz0rw= 15 | github.com/blinklabs-io/bursa v0.11.0/go.mod h1:iRMqaLl1kj8CxqRwCplTD1YaRRT/doZMOg6OUvs7gxU= 16 | github.com/blinklabs-io/cardano-models v0.5.1 h1:dPe5gT8vwmYUbc+rgofO9WS8otbgAKTo5fpMJhLPLIE= 17 | github.com/blinklabs-io/cardano-models v0.5.1/go.mod h1:8z20pRjrS/0nUO3r0HEKKP3cYstwbyfIWUsYKv+ud3s= 18 | github.com/blinklabs-io/gouroboros v0.121.0 h1:Hb8amqG2dOztE1r5cuZAUUKSUzusDuffdGgSSh+qIfs= 19 | github.com/blinklabs-io/gouroboros v0.121.0/go.mod h1:hAJS7mv7dYMbjXujmr6X8pJIzbYvDQIoQo10orJiOuo= 20 | github.com/blinklabs-io/merkle-patricia-forestry v0.1.3 h1:KF3NCojSCJGwNaMSoV+EjYFYGTo5c22CC7nyBNJz9wU= 21 | github.com/blinklabs-io/merkle-patricia-forestry v0.1.3/go.mod h1:RozVrsdYPG/iLKXen91X/3eKvQnqaWP8naU7XnrihlU= 22 | github.com/blinklabs-io/ouroboros-mock v0.3.8 h1:+DAt2rx0ouZUxee5DBMgZq3I1+ZdxFSHG9g3tYl/FKU= 23 | github.com/blinklabs-io/ouroboros-mock v0.3.8/go.mod h1:UwQIf4KqZwO13P9d90fbi3UL/X7JaJfeEbqk+bEeFQA= 24 | github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= 25 | github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= 26 | github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= 27 | github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= 28 | github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= 29 | github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= 30 | github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= 31 | github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= 32 | github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= 33 | github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c= 34 | github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= 35 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 36 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 37 | github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= 38 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= 39 | github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= 40 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= 41 | github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= 42 | github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= 43 | github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 44 | github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= 45 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= 46 | github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= 47 | github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= 48 | github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= 49 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 50 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 51 | github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 52 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 53 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 54 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 55 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 56 | github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= 57 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= 58 | github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= 59 | github.com/dgraph-io/badger/v4 v4.7.0 h1:Q+J8HApYAY7UMpL8d9owqiB+odzEc0zn/aqOD9jhc6Y= 60 | github.com/dgraph-io/badger/v4 v4.7.0/go.mod h1:He7TzG3YBy3j4f5baj5B7Zl2XyfNe5bl4Udl0aPemVA= 61 | github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= 62 | github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= 63 | github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= 64 | github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 65 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 66 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 67 | github.com/fivebinaries/go-cardano-serialization v0.0.0-20220907134105-ec9b85086588 h1:TrEpycmnOvLc6jsJyD+mEWqJbrqi2UGD1HMawMAkpi8= 68 | github.com/fivebinaries/go-cardano-serialization v0.0.0-20220907134105-ec9b85086588/go.mod h1:tLkxhM4oeOACL9BB0lkpdiGrANPcrpqsydCQrAfZUbw= 69 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 70 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 71 | github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= 72 | github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 73 | github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= 74 | github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 75 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 76 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 77 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 78 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 79 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 80 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 81 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 82 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 83 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 84 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 85 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 86 | github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= 87 | github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 88 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 89 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 90 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 91 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 92 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 93 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 94 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 95 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 96 | github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= 97 | github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 98 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 99 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 100 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 101 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 102 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 103 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 104 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 105 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 106 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 107 | github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 108 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 109 | github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= 110 | github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= 111 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 112 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 113 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 114 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 115 | github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= 116 | github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= 117 | github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= 118 | github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= 119 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 120 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 121 | github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= 122 | github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 123 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 124 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 125 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 126 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 127 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 128 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 129 | github.com/maestro-org/go-sdk v1.2.1 h1:8bmYSfO7hI7u9UR68VsfCZz74tO2hJSzOJTxoSwm7QQ= 130 | github.com/maestro-org/go-sdk v1.2.1/go.mod h1:EYaRwFT8nkwFzZsN6xK256j+r7ASUUn9p44RlaqYjE8= 131 | github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 132 | github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 133 | github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1aMrNhpgxHmow66XQtm0aQLY0AE= 134 | github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= 135 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 136 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 137 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 138 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 139 | github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 140 | github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 141 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 142 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 143 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 144 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 145 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 146 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 147 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 148 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 149 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 150 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 151 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 152 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 153 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 154 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 155 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 156 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 157 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 158 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 159 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 160 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= 161 | github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= 162 | github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= 163 | github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= 164 | github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= 165 | github.com/utxorpc/go-codegen v0.16.0 h1:jPTyKtv2OI6Ms7U/goAYbaP6axAZ39vRmoWdjO/rkeM= 166 | github.com/utxorpc/go-codegen v0.16.0/go.mod h1:2Nwq1md4HEcO2guvTpH45slGHO2aGRbiXKx73FM65ow= 167 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 168 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 169 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 170 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 171 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 172 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 173 | go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 174 | go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 175 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 176 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 177 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 178 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 179 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 180 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 181 | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 182 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 183 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 184 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 185 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 186 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= 187 | golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= 188 | golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 189 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 190 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 191 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 192 | golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 193 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 194 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 195 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 196 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 197 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 198 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 199 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 200 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 201 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 202 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 203 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 204 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 205 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 206 | golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 207 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 208 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 209 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 210 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 211 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 212 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 213 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 214 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 215 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 216 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 217 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 218 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 219 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 220 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 221 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 222 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 223 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 224 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 225 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 226 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 227 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 228 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 229 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 230 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 231 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 232 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 233 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 234 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 235 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 236 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 237 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 238 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 239 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 240 | -------------------------------------------------------------------------------- /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 | "runtime" 22 | 23 | "github.com/blinklabs-io/bluefin/internal/version" 24 | ouroboros "github.com/blinklabs-io/gouroboros" 25 | "github.com/kelseyhightower/envconfig" 26 | "gopkg.in/yaml.v2" 27 | ) 28 | 29 | type Config struct { 30 | Logging LoggingConfig `yaml:"logging"` 31 | Storage StorageConfig `yaml:"storage"` 32 | Indexer IndexerConfig `yaml:"indexer"` 33 | Submit SubmitConfig `yaml:"submit"` 34 | Wallet WalletConfig `yaml:"wallet"` 35 | Miner MinerConfig `yaml:"miner"` 36 | Metrics MetricsConfig `yaml:"metrics"` 37 | Debug DebugConfig `yaml:"debug"` 38 | Profile string `yaml:"profile" envconfig:"PROFILE"` 39 | Network string `yaml:"network" envconfig:"NETWORK"` 40 | NetworkMagic uint32 41 | } 42 | 43 | type LoggingConfig struct { 44 | Debug bool `yaml:"debug" envconfig:"LOGGING_DEBUG"` 45 | } 46 | 47 | type IndexerConfig struct { 48 | Address string `yaml:"address" envconfig:"INDEXER_TCP_ADDRESS"` 49 | SocketPath string `yaml:"socketPath" envconfig:"INDEXER_SOCKET_PATH"` 50 | ScriptAddress string `yaml:"scriptAddress" envconfig:"INDEXER_SCRIPT_ADDRESS"` 51 | InterceptHash string `yaml:"interceptHash" envconfig:"INDEXER_INTERCEPT_HASH"` 52 | InterceptSlot uint64 `yaml:"interceptSlot" envconfig:"INDEXER_INTERCEPT_SLOT"` 53 | } 54 | 55 | type SubmitConfig struct { 56 | Address string `yaml:"address" envconfig:"SUBMIT_TCP_ADDRESS"` 57 | SocketPath string `yaml:"socketPath" envconfig:"SUBMIT_SOCKET_PATH"` 58 | Url string `yaml:"url" envconfig:"SUBMIT_URL"` 59 | BlockFrostProjectID string `yaml:"blockFrostProjectID" envconfig:"SUBMIT_BLOCKFROST_PROJECT_ID"` 60 | } 61 | 62 | type StorageConfig struct { 63 | Directory string `yaml:"dir" envconfig:"STORAGE_DIR"` 64 | } 65 | 66 | type WalletConfig struct { 67 | Mnemonic string `yaml:"mnemonic" envconfig:"MNEMONIC"` 68 | } 69 | 70 | type MinerConfig struct { 71 | WorkerCount int `yaml:"workers" envconfig:"WORKER_COUNT"` 72 | HashRateInterval int `yaml:"hashRateInterval" envconfig:"HASH_RATE_INTERVAL"` 73 | Message string `yaml:"message" envconfig:"MINER_MESSAGE"` 74 | } 75 | 76 | type MetricsConfig struct { 77 | ListenAddress string `yaml:"address" envconfig:"METRICS_LISTEN_ADDRESS"` 78 | ListenPort uint `yaml:"port" envconfig:"METRICS_LISTEN_PORT"` 79 | } 80 | 81 | type DebugConfig struct { 82 | ListenAddress string `yaml:"address" envconfig:"DEBUG_ADDRESS"` 83 | ListenPort uint `yaml:"port" envconfig:"DEBUG_PORT"` 84 | } 85 | 86 | // Singleton config instance with default values 87 | var globalConfig = &Config{ 88 | Debug: DebugConfig{ 89 | ListenAddress: "localhost", 90 | ListenPort: 0, 91 | }, 92 | Metrics: MetricsConfig{ 93 | ListenAddress: "", 94 | ListenPort: 8081, 95 | }, 96 | Storage: StorageConfig{ 97 | // TODO: pick a better location 98 | Directory: "./.bluefin", 99 | }, 100 | // The default worker config is somewhat conservative: worker count is set 101 | // to half of the available logical CPUs 102 | Miner: MinerConfig{ 103 | WorkerCount: max(1, runtime.NumCPU()/2), 104 | HashRateInterval: 60, 105 | Message: fmt.Sprintf( 106 | "Bluefin %s by Blink Labs", 107 | version.GetVersionString(), 108 | ), 109 | }, 110 | Network: "mainnet", 111 | Profile: "tuna-v2", 112 | } 113 | 114 | func Load(configFile string) (*Config, error) { 115 | // Load config file as YAML if provided 116 | if configFile != "" { 117 | buf, err := os.ReadFile(configFile) 118 | if err != nil { 119 | return nil, fmt.Errorf("error reading config file: %w", err) 120 | } 121 | err = yaml.Unmarshal(buf, globalConfig) 122 | if err != nil { 123 | return nil, fmt.Errorf("error parsing config file: %w", err) 124 | } 125 | } 126 | // Load config values from environment variables 127 | // We use "dummy" as the app name here to (mostly) prevent picking up env 128 | // vars that we hadn't explicitly specified in annotations above 129 | err := envconfig.Process("dummy", globalConfig) 130 | if err != nil { 131 | return nil, fmt.Errorf("error processing environment: %w", err) 132 | } 133 | // Populate network magic value 134 | if err := globalConfig.populateNetworkMagic(); err != nil { 135 | return nil, err 136 | } 137 | // Check specified profile 138 | if err := globalConfig.validateProfile(); err != nil { 139 | return nil, err 140 | } 141 | // Populate our Indexer startup 142 | if err := globalConfig.populateIndexer(); err != nil { 143 | return nil, err 144 | } 145 | return globalConfig, nil 146 | } 147 | 148 | // GetConfig returns the global config instance 149 | func GetConfig() *Config { 150 | return globalConfig 151 | } 152 | 153 | func (c *Config) populateNetworkMagic() error { 154 | network, ok := ouroboros.NetworkByName(c.Network) 155 | if !ok { 156 | return fmt.Errorf("unknown network: %s", c.Network) 157 | } 158 | c.NetworkMagic = network.NetworkMagic 159 | return nil 160 | } 161 | 162 | func (c *Config) validateProfile() error { 163 | if _, ok := Profiles[c.Network]; !ok { 164 | return fmt.Errorf("no profiles defined for network %s", c.Network) 165 | } 166 | if _, ok := Profiles[c.Network][c.Profile]; !ok { 167 | return fmt.Errorf( 168 | "no profile %s defined for network %s", 169 | c.Profile, 170 | c.Network, 171 | ) 172 | } 173 | return nil 174 | } 175 | 176 | func (c *Config) populateIndexer() error { 177 | profile, ok := Profiles[c.Network][c.Profile] 178 | if !ok { 179 | return errors.New("failed indexer init") 180 | } 181 | c.Indexer.InterceptHash = profile.InterceptHash 182 | c.Indexer.InterceptSlot = profile.InterceptSlot 183 | c.Indexer.ScriptAddress = profile.ScriptAddress 184 | return nil 185 | } 186 | -------------------------------------------------------------------------------- /internal/config/networks.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Network struct { 4 | ShelleyOffsetSlot uint64 5 | ShelleyOffsetTime int64 6 | } 7 | 8 | var Networks = map[string]Network{ 9 | "preview": { 10 | ShelleyOffsetSlot: 0, 11 | ShelleyOffsetTime: 1666656000, 12 | }, 13 | "mainnet": { 14 | ShelleyOffsetSlot: 4924800, 15 | ShelleyOffsetTime: 1596491091, 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /internal/indexer/indexer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 indexer 16 | 17 | import ( 18 | "encoding/hex" 19 | "fmt" 20 | "log/slog" 21 | "os" 22 | "time" 23 | 24 | "github.com/blinklabs-io/adder/event" 25 | filter_event "github.com/blinklabs-io/adder/filter/event" 26 | input_chainsync "github.com/blinklabs-io/adder/input/chainsync" 27 | output_embedded "github.com/blinklabs-io/adder/output/embedded" 28 | "github.com/blinklabs-io/adder/pipeline" 29 | "github.com/blinklabs-io/bluefin/internal/config" 30 | "github.com/blinklabs-io/bluefin/internal/logging" 31 | "github.com/blinklabs-io/bluefin/internal/miner" 32 | "github.com/blinklabs-io/bluefin/internal/storage" 33 | "github.com/blinklabs-io/bluefin/internal/wallet" 34 | models "github.com/blinklabs-io/cardano-models" 35 | "github.com/blinklabs-io/gouroboros/cbor" 36 | "github.com/blinklabs-io/gouroboros/ledger" 37 | ocommon "github.com/blinklabs-io/gouroboros/protocol/common" 38 | ) 39 | 40 | const ( 41 | syncStatusLogInterval = 30 * time.Second 42 | rollbackSlots = 50 * 20 // 50 blocks with a 20s average between 43 | ) 44 | 45 | type Indexer struct { 46 | pipeline *pipeline.Pipeline 47 | cursorSlot uint64 48 | cursorHash string 49 | tipSlot uint64 50 | tipHash string 51 | tipReached bool 52 | syncLogTimer *time.Timer 53 | lastBlockData any 54 | } 55 | 56 | // Singleton indexer instance 57 | var globalIndexer = &Indexer{} 58 | 59 | func (i *Indexer) Start() error { 60 | cfg := config.GetConfig() 61 | profileCfg := config.GetProfile() 62 | // Load saved block data 63 | var lastBlockDataBytes cbor.RawMessage 64 | if err := storage.GetStorage().GetBlockData(&(lastBlockDataBytes)); err != nil { 65 | return err 66 | } 67 | if profileCfg.UseTunaV1 { 68 | var tmpBlockData models.TunaV1State 69 | if len(lastBlockDataBytes) > 0 { 70 | if _, err := cbor.Decode(lastBlockDataBytes, &tmpBlockData); err != nil { 71 | return fmt.Errorf("failed to parse last block data: %w", err) 72 | } 73 | } 74 | i.lastBlockData = tmpBlockData 75 | } else { 76 | var tmpBlockData models.TunaV2State 77 | if len(lastBlockDataBytes) > 0 { 78 | if _, err := cbor.Decode(lastBlockDataBytes, &tmpBlockData); err != nil { 79 | return fmt.Errorf("failed to parse last block data: %w", err) 80 | } 81 | } 82 | i.lastBlockData = tmpBlockData 83 | } 84 | // Create pipeline 85 | i.pipeline = pipeline.New() 86 | // Configure pipeline input 87 | inputOpts := []input_chainsync.ChainSyncOptionFunc{ 88 | input_chainsync.WithBulkMode(true), 89 | input_chainsync.WithAutoReconnect(true), 90 | input_chainsync.WithLogger(logging.GetLogger()), 91 | input_chainsync.WithStatusUpdateFunc(i.updateStatus), 92 | input_chainsync.WithNetwork(cfg.Network), 93 | } 94 | if cfg.Indexer.Address != "" { 95 | inputOpts = append( 96 | inputOpts, 97 | input_chainsync.WithAddress(cfg.Indexer.Address), 98 | ) 99 | } 100 | cursorSlotNumber, cursorBlockHash, err := storage.GetStorage().GetCursor() 101 | if err != nil { 102 | return err 103 | } 104 | if cursorSlotNumber > 0 { 105 | slog.Info( 106 | fmt.Sprintf( 107 | "found previous chainsync cursor: %d, %s", 108 | cursorSlotNumber, 109 | cursorBlockHash, 110 | ), 111 | ) 112 | hashBytes, err := hex.DecodeString(cursorBlockHash) 113 | if err != nil { 114 | return err 115 | } 116 | inputOpts = append( 117 | inputOpts, 118 | input_chainsync.WithIntersectPoints( 119 | []ocommon.Point{ 120 | { 121 | Hash: hashBytes, 122 | Slot: cursorSlotNumber, 123 | }, 124 | }, 125 | ), 126 | ) 127 | } else if cfg.Indexer.InterceptHash != "" && cfg.Indexer.InterceptSlot > 0 { 128 | hashBytes, err := hex.DecodeString(cfg.Indexer.InterceptHash) 129 | if err != nil { 130 | return err 131 | } 132 | inputOpts = append( 133 | inputOpts, 134 | input_chainsync.WithIntersectPoints( 135 | []ocommon.Point{ 136 | { 137 | Hash: hashBytes, 138 | Slot: cfg.Indexer.InterceptSlot, 139 | }, 140 | }, 141 | ), 142 | ) 143 | } 144 | input := input_chainsync.New( 145 | inputOpts..., 146 | ) 147 | i.pipeline.AddInput(input) 148 | // Configure pipeline filters 149 | // We only care about transaction events 150 | filterEvent := filter_event.New( 151 | filter_event.WithTypes( 152 | []string{"chainsync.transaction", "chainsync.rollback"}, 153 | ), 154 | ) 155 | i.pipeline.AddFilter(filterEvent) 156 | // Configure pipeline output 157 | output := output_embedded.New( 158 | output_embedded.WithCallbackFunc(i.handleEvent), 159 | ) 160 | i.pipeline.AddOutput(output) 161 | // Start pipeline 162 | if err := i.pipeline.Start(); err != nil { 163 | slog.Error( 164 | fmt.Sprintf("failed to start pipeline: %s\n", err), 165 | ) 166 | os.Exit(1) 167 | } 168 | // Start error handler 169 | go func() { 170 | err, ok := <-i.pipeline.ErrorChan() 171 | if ok { 172 | slog.Error( 173 | fmt.Sprintf("pipeline failed: %s\n", err), 174 | ) 175 | os.Exit(1) 176 | } 177 | }() 178 | // Schedule periodic catch-up sync log messages 179 | i.scheduleSyncStatusLog() 180 | return nil 181 | } 182 | 183 | func (i *Indexer) handleEvent(evt event.Event) error { 184 | switch evt.Payload.(type) { 185 | case input_chainsync.RollbackEvent: 186 | return i.handleEventRollback(evt) 187 | case input_chainsync.TransactionEvent: 188 | return i.handleEventTransaction(evt) 189 | default: 190 | return fmt.Errorf("unknown event payload type: %T", evt.Payload) 191 | } 192 | } 193 | 194 | func (i *Indexer) handleEventRollback(evt event.Event) error { 195 | store := storage.GetStorage() 196 | eventRollback := evt.Payload.(input_chainsync.RollbackEvent) 197 | store.Lock() 198 | defer store.Unlock() 199 | if err := store.Rollback(eventRollback.SlotNumber); err != nil { 200 | return err 201 | } 202 | slog.Info( 203 | fmt.Sprintf( 204 | "rolled back to %d.%s", 205 | eventRollback.SlotNumber, 206 | eventRollback.BlockHash, 207 | ), 208 | ) 209 | // Purge older deleted UTxOs 210 | if err := store.PurgeDeletedUtxos(eventRollback.SlotNumber - rollbackSlots); err != nil { 211 | slog.Warn( 212 | fmt.Sprintf("failed to purge deleted UTxOs: %s", err), 213 | ) 214 | } 215 | return nil 216 | } 217 | 218 | func (i *Indexer) handleEventTransaction(evt event.Event) error { 219 | cfg := config.GetConfig() 220 | profileCfg := config.GetProfile() 221 | bursa := wallet.GetWallet() 222 | store := storage.GetStorage() 223 | eventTx := evt.Payload.(input_chainsync.TransactionEvent) 224 | eventCtx := evt.Context.(input_chainsync.TransactionContext) 225 | store.Lock() 226 | defer store.Unlock() 227 | // Delete used UTXOs 228 | for _, txInput := range eventTx.Transaction.Consumed() { 229 | // We don't have a ledger DB to know where the TX inputs came from, so we just try deleting them for our known addresses 230 | for _, tmpAddress := range []string{cfg.Indexer.ScriptAddress, bursa.PaymentAddress} { 231 | if err := store.RemoveUtxo(tmpAddress, txInput.Id().String(), txInput.Index(), eventCtx.SlotNumber); err != nil { 232 | return err 233 | } 234 | } 235 | } 236 | // Check for TUNA mints 237 | var tunaMintCount int64 238 | var tunaPolicyId string 239 | if profileCfg.UseTunaV1 { 240 | tunaPolicyId = profileCfg.ValidatorHash 241 | } else { 242 | tunaPolicyId = profileCfg.MintValidatorHash 243 | } 244 | tunaPolicyIdHex, err := hex.DecodeString(tunaPolicyId) 245 | if err != nil { 246 | return err 247 | } 248 | mints := eventTx.Transaction.AssetMint() 249 | if mints != nil { 250 | tunaMintCount = mints.Asset( 251 | ledger.Blake2b224(tunaPolicyIdHex), 252 | []byte("TUNA"), 253 | ) 254 | } 255 | // Process produced UTxOs 256 | startMiner := false 257 | for _, utxo := range eventTx.Transaction.Produced() { 258 | // Check for reference inputs 259 | for _, refInput := range profileCfg.ScriptRefInputs { 260 | if refInput.TxId == eventCtx.TransactionHash && 261 | refInput.OutputIdx == utxo.Id.Index() { 262 | // Record script ref UTxO 263 | if err := store.AddUtxo( 264 | "script_ref", 265 | eventCtx.TransactionHash, 266 | utxo.Id.Index(), 267 | utxo.Output.Cbor(), 268 | eventCtx.SlotNumber, 269 | ); err != nil { 270 | return err 271 | } 272 | } 273 | } 274 | outputAddress := utxo.Output.Address().String() 275 | // Ignore outputs to addresses that we don't care about 276 | if outputAddress != cfg.Indexer.ScriptAddress && 277 | outputAddress != bursa.PaymentAddress { 278 | continue 279 | } 280 | // Write UTXO to storage 281 | if err := store.AddUtxo( 282 | outputAddress, 283 | eventCtx.TransactionHash, 284 | utxo.Id.Index(), 285 | utxo.Output.Cbor(), 286 | eventCtx.SlotNumber, 287 | ); err != nil { 288 | return err 289 | } 290 | // Show message when receiving freshly minted TUNA 291 | if outputAddress == bursa.PaymentAddress { 292 | if tunaMintCount > 0 { 293 | if utxo.Output.Assets() != nil { 294 | outputTunaCount := utxo.Output.Assets().Asset( 295 | ledger.Blake2b224(tunaPolicyIdHex), 296 | []byte("TUNA"), 297 | ) 298 | if outputTunaCount > 0 { 299 | slog.Info( 300 | fmt.Sprintf("minted %d TUNA!", tunaMintCount), 301 | ) 302 | } 303 | } 304 | } 305 | } 306 | // Handle datum for script address 307 | if outputAddress == cfg.Indexer.ScriptAddress { 308 | datum := utxo.Output.Datum() 309 | if datum != nil { 310 | if _, err := datum.Decode(); err != nil { 311 | slog.Warn( 312 | fmt.Sprintf( 313 | "error decoding TX (%s) output datum: %s", 314 | eventCtx.TransactionHash, 315 | err, 316 | ), 317 | ) 318 | return err 319 | } 320 | if profileCfg.UseTunaV1 { 321 | var blockData models.TunaV1State 322 | if _, err := cbor.Decode(datum.Cbor(), &blockData); err != nil { 323 | slog.Warn( 324 | fmt.Sprintf( 325 | "error decoding TX (%s) output datum: %s", 326 | eventCtx.TransactionHash, 327 | err, 328 | ), 329 | ) 330 | return err 331 | } 332 | i.lastBlockData = blockData 333 | var tmpExtra any 334 | switch v := blockData.Extra.(type) { 335 | case []byte: 336 | tmpExtra = string(v) 337 | default: 338 | tmpExtra = v 339 | } 340 | slog.Info( 341 | fmt.Sprintf( 342 | "found updated datum: block number: %d, hash: %x, leading zeros: %d, difficulty number: %d, epoch time: %d, real time now: %d, extra: %v", 343 | blockData.BlockNumber, 344 | blockData.CurrentHash, 345 | blockData.LeadingZeros, 346 | blockData.DifficultyNumber, 347 | blockData.EpochTime, 348 | blockData.RealTimeNow, 349 | tmpExtra, 350 | ), 351 | ) 352 | } else { 353 | var blockData models.TunaV2State 354 | if _, err := cbor.Decode(datum.Cbor(), &blockData); err != nil { 355 | slog.Warn( 356 | fmt.Sprintf( 357 | "error decoding TX (%s) output datum: %s", 358 | eventCtx.TransactionHash, 359 | err, 360 | ), 361 | ) 362 | return err 363 | } 364 | i.lastBlockData = blockData 365 | // Update trie 366 | trie := store.Trie() 367 | trie.Lock() 368 | trieKey := trie.HashKey(blockData.CurrentHash) 369 | if err := trie.Update(trieKey, blockData.CurrentHash, eventCtx.SlotNumber); err != nil { 370 | trie.Unlock() 371 | return err 372 | } 373 | trie.Unlock() 374 | slog.Info( 375 | fmt.Sprintf( 376 | "found updated datum: block number: %d, hash: %x, leading zeros: %d, difficulty number: %d, epoch time: %d, current POSIX time: %d, merkle root = %x", 377 | blockData.BlockNumber, 378 | blockData.CurrentHash, 379 | blockData.LeadingZeros, 380 | blockData.DifficultyNumber, 381 | blockData.EpochTime, 382 | blockData.CurrentPosixTime, 383 | blockData.MerkleRoot, 384 | ), 385 | ) 386 | } 387 | 388 | if err := store.UpdateBlockData(&(i.lastBlockData)); err != nil { 389 | return err 390 | } 391 | 392 | if i.tipReached { 393 | startMiner = true 394 | } 395 | } 396 | } 397 | } 398 | // Purge older deleted UTxOs 399 | if i.tipReached { 400 | if err := store.PurgeDeletedUtxos(eventCtx.SlotNumber - rollbackSlots); err != nil { 401 | slog.Warn( 402 | fmt.Sprintf("failed to purge deleted UTxOs: %s", err), 403 | ) 404 | } 405 | } 406 | // (Re)start miner if we got a new datum 407 | if startMiner { 408 | miner.GetManager().Stop() 409 | miner.GetManager().Start(i.lastBlockData) 410 | } 411 | return nil 412 | } 413 | 414 | func (i *Indexer) scheduleSyncStatusLog() { 415 | i.syncLogTimer = time.AfterFunc(syncStatusLogInterval, i.syncStatusLog) 416 | } 417 | 418 | func (i *Indexer) syncStatusLog() { 419 | slog.Info( 420 | fmt.Sprintf( 421 | "catch-up sync in progress: at %d.%s (current tip slot is %d)", 422 | i.cursorSlot, 423 | i.cursorHash, 424 | i.tipSlot, 425 | ), 426 | ) 427 | i.scheduleSyncStatusLog() 428 | } 429 | 430 | func (i *Indexer) updateStatus(status input_chainsync.ChainSyncStatus) { 431 | // Check if we've hit chain tip 432 | if !i.tipReached && status.TipReached { 433 | if i.syncLogTimer != nil { 434 | i.syncLogTimer.Stop() 435 | } 436 | i.tipReached = true 437 | miner.GetManager().Start(i.lastBlockData) 438 | } 439 | i.cursorSlot = status.SlotNumber 440 | i.cursorHash = status.BlockHash 441 | i.tipSlot = status.TipSlotNumber 442 | i.tipHash = status.TipBlockHash 443 | if err := storage.GetStorage().UpdateCursor(status.SlotNumber, status.BlockHash); err != nil { 444 | slog.Error( 445 | fmt.Sprintf("failed to update cursor: %s", err), 446 | ) 447 | } 448 | } 449 | 450 | // GetIndexer returns the global indexer instance 451 | func GetIndexer() *Indexer { 452 | return globalIndexer 453 | } 454 | -------------------------------------------------------------------------------- /internal/logging/logging.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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/slog" 19 | "os" 20 | "time" 21 | 22 | "github.com/blinklabs-io/bluefin/internal/config" 23 | ) 24 | 25 | var globalLogger *slog.Logger 26 | 27 | func Configure() { 28 | cfg := config.GetConfig() 29 | var level slog.Level 30 | if cfg.Logging.Debug { 31 | level = slog.LevelDebug 32 | } else { 33 | level = slog.LevelInfo 34 | } 35 | 36 | handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 37 | ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { 38 | if a.Key == slog.TimeKey { 39 | // Format the time attribute to use RFC3339 or your custom format 40 | // Rename the time key to timestamp 41 | return slog.String( 42 | "timestamp", 43 | a.Value.Time().Format(time.RFC3339), 44 | ) 45 | } 46 | return a 47 | }, 48 | Level: level, 49 | }) 50 | globalLogger = slog.New(handler).With("component", "main") 51 | } 52 | 53 | func GetLogger() *slog.Logger { 54 | if globalLogger == nil { 55 | Configure() 56 | } 57 | return globalLogger 58 | } 59 | -------------------------------------------------------------------------------- /internal/miner/miner.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 miner 16 | 17 | import ( 18 | "crypto/rand" 19 | "fmt" 20 | "log/slog" 21 | "sync" 22 | "sync/atomic" 23 | "time" 24 | 25 | "github.com/blinklabs-io/bluefin/internal/config" 26 | "github.com/blinklabs-io/bluefin/internal/storage" 27 | "github.com/blinklabs-io/bluefin/internal/wallet" 28 | models "github.com/blinklabs-io/cardano-models" 29 | "github.com/blinklabs-io/gouroboros/cbor" 30 | "github.com/minio/sha256-simd" 31 | ) 32 | 33 | const ( 34 | DefaultEpochNumber = 2016 35 | DefaultEpochTarget = 1_209_600_000 36 | ) 37 | 38 | type Miner struct { 39 | Config *config.Config 40 | waitGroup *sync.WaitGroup 41 | resultChan chan Result 42 | doneChan chan any 43 | blockData any 44 | state TargetState 45 | hashCounter *atomic.Uint64 46 | nonceCount uint8 47 | } 48 | 49 | type TargetState interface { 50 | MarshalCBOR() ([]byte, error) 51 | SetNonce([16]byte) 52 | GetNonce() [16]byte 53 | } 54 | 55 | type TargetStateV1 struct { 56 | Nonce [16]byte 57 | BlockNumber int64 58 | CurrentHash []byte 59 | LeadingZeros int64 60 | DifficultyNumber int64 61 | EpochTime int64 62 | } 63 | 64 | func (t *TargetStateV1) SetNonce(nonce [16]byte) { 65 | t.Nonce = nonce 66 | } 67 | 68 | func (t *TargetStateV1) GetNonce() [16]byte { 69 | return t.Nonce 70 | } 71 | 72 | func (state *TargetStateV1) MarshalCBOR() ([]byte, error) { 73 | tmp := cbor.NewConstructor( 74 | 0, 75 | cbor.IndefLengthList{ 76 | state.Nonce, 77 | state.BlockNumber, 78 | state.CurrentHash, 79 | state.LeadingZeros, 80 | state.DifficultyNumber, 81 | state.EpochTime, 82 | }, 83 | ) 84 | return cbor.Encode(&tmp) 85 | } 86 | 87 | type TargetStateV2 struct { 88 | Nonce [16]byte 89 | MinerCredHash []byte 90 | EpochTime int64 91 | BlockNumber int64 92 | CurrentHash []byte 93 | LeadingZeros int64 94 | DifficultyNumber int64 95 | cachedCbor []byte 96 | } 97 | 98 | func (t *TargetStateV2) SetNonce(nonce [16]byte) { 99 | t.Nonce = nonce 100 | } 101 | 102 | func (t *TargetStateV2) GetNonce() [16]byte { 103 | return t.Nonce 104 | } 105 | 106 | func (state *TargetStateV2) MarshalCBOR() ([]byte, error) { 107 | // Use cached CBOR to generate new CBOR more quickly 108 | if state.cachedCbor != nil { 109 | // Replace nonce value in cached CBOR with current nonce 110 | for i := range 16 { 111 | state.cachedCbor[4+i] = state.Nonce[i] 112 | } 113 | return state.cachedCbor, nil 114 | } 115 | // There are different ways we can order the fields for V2, so we need to check which 116 | profileCfg := config.GetProfile() 117 | var indefList cbor.IndefLengthList 118 | if profileCfg.TunaV2OldTargetStateOrder { 119 | indefList = cbor.IndefLengthList{ 120 | state.Nonce, 121 | state.MinerCredHash, 122 | state.EpochTime, 123 | state.BlockNumber, 124 | state.CurrentHash, 125 | state.LeadingZeros, 126 | state.DifficultyNumber, 127 | } 128 | } else { 129 | indefList = cbor.IndefLengthList{ 130 | state.Nonce, 131 | state.MinerCredHash, 132 | state.BlockNumber, 133 | state.CurrentHash, 134 | state.LeadingZeros, 135 | state.DifficultyNumber, 136 | state.EpochTime, 137 | } 138 | } 139 | tmp := cbor.NewConstructor( 140 | 0, 141 | indefList, 142 | ) 143 | cborData, err := cbor.Encode(&tmp) 144 | if err != nil { 145 | return nil, err 146 | } 147 | state.cachedCbor = make([]byte, len(cborData)) 148 | copy(state.cachedCbor, cborData) 149 | return cborData, nil 150 | } 151 | 152 | type DifficultyMetrics struct { 153 | LeadingZeros int64 154 | DifficultyNumber int64 155 | } 156 | 157 | type DifficultyAdjustment struct { 158 | Numerator int64 159 | Denominator int64 160 | } 161 | 162 | type Result struct { 163 | BlockData any 164 | Nonce [16]byte 165 | } 166 | 167 | func New( 168 | waitGroup *sync.WaitGroup, 169 | resultChan chan Result, 170 | doneChan chan any, 171 | blockData any, 172 | hashCounter *atomic.Uint64, 173 | ) *Miner { 174 | return &Miner{ 175 | Config: config.GetConfig(), 176 | waitGroup: waitGroup, 177 | resultChan: resultChan, 178 | doneChan: doneChan, 179 | blockData: blockData, 180 | hashCounter: hashCounter, 181 | } 182 | } 183 | 184 | func (m *Miner) Start() { 185 | defer m.waitGroup.Done() 186 | 187 | cfg := config.GetConfig() 188 | profileCfg := config.GetProfile() 189 | 190 | if profileCfg.UseTunaV1 { 191 | // Create initial state from block data 192 | blockData := m.blockData.(models.TunaV1State) 193 | m.state = &TargetStateV1{ 194 | Nonce: randomNonce(), 195 | BlockNumber: blockData.BlockNumber, 196 | CurrentHash: blockData.CurrentHash, 197 | LeadingZeros: blockData.LeadingZeros, 198 | DifficultyNumber: blockData.DifficultyNumber, 199 | EpochTime: blockData.EpochTime, 200 | } 201 | } else { 202 | // Build miner credential 203 | userPkh := wallet.PaymentKeyHash() 204 | minerCredential := cbor.NewConstructor( 205 | 0, 206 | cbor.IndefLengthList{ 207 | userPkh, 208 | []byte(cfg.Miner.Message), 209 | }, 210 | ) 211 | minerCredCbor, err := cbor.Encode(&minerCredential) 212 | if err != nil { 213 | panic(err) 214 | } 215 | // NOTE: we happen to use the same hash mechanism for our trie keys, so we 216 | // can reuse that hashing function here for convenience 217 | minerCredHash := storage.GetStorage().Trie().HashKey(minerCredCbor) 218 | // Create initial state from block data 219 | blockData := m.blockData.(models.TunaV2State) 220 | m.state = &TargetStateV2{ 221 | Nonce: randomNonce(), 222 | MinerCredHash: minerCredHash, 223 | EpochTime: blockData.EpochTime, 224 | BlockNumber: blockData.BlockNumber, 225 | CurrentHash: blockData.CurrentHash, 226 | LeadingZeros: blockData.LeadingZeros, 227 | DifficultyNumber: blockData.DifficultyNumber, 228 | } 229 | } 230 | 231 | targetHash := m.calculateHash() 232 | 233 | // Check for shutdown 234 | select { 235 | case <-m.doneChan: 236 | return 237 | default: 238 | break 239 | } 240 | 241 | realTimeNow := time.Now().Unix()*1000 - 60000 242 | 243 | var epochTime int64 244 | var blockDataBlockNumber int64 245 | var difficultyNumber int64 246 | var leadingZeros int64 247 | if profileCfg.UseTunaV1 { 248 | blockData := m.blockData.(models.TunaV1State) 249 | epochTime = blockData.EpochTime + 90000 + realTimeNow - blockData.RealTimeNow 250 | blockDataBlockNumber = blockData.BlockNumber 251 | difficultyNumber = blockData.DifficultyNumber 252 | leadingZeros = blockData.LeadingZeros 253 | } else { 254 | blockData := m.blockData.(models.TunaV2State) 255 | epochTime = blockData.EpochTime + 90000 + realTimeNow - blockData.CurrentPosixTime 256 | blockDataBlockNumber = blockData.BlockNumber 257 | difficultyNumber = blockData.DifficultyNumber 258 | leadingZeros = blockData.LeadingZeros 259 | } 260 | 261 | // Adjust difficulty on epoch boundary 262 | epochNumber := DefaultEpochNumber 263 | if profileCfg.EpochNumber > 0 { 264 | epochNumber = profileCfg.EpochNumber 265 | } 266 | epochTarget := DefaultEpochTarget 267 | if profileCfg.EpochTarget > 0 { 268 | epochTarget = profileCfg.EpochTarget 269 | } 270 | if blockDataBlockNumber > 0 && 271 | blockDataBlockNumber%int64(epochNumber) == 0 { 272 | adjustment := getDifficultyAdjustment(epochTime, int64(epochTarget)) 273 | epochTime = 0 274 | newDifficulty := calculateDifficultyNumber( 275 | DifficultyMetrics{ 276 | DifficultyNumber: difficultyNumber, 277 | LeadingZeros: leadingZeros, 278 | }, 279 | adjustment, 280 | ) 281 | difficultyNumber = newDifficulty.DifficultyNumber 282 | leadingZeros = newDifficulty.LeadingZeros 283 | } 284 | 285 | // Construct the new block data 286 | var postDatum any 287 | if profileCfg.UseTunaV1 { 288 | blockData := m.blockData.(models.TunaV1State) 289 | difficulty := getDifficulty([]byte(targetHash)) 290 | currentInterlink := calculateInterlink( 291 | targetHash, 292 | difficulty, 293 | m.getCurrentDifficulty(), 294 | blockData.Interlink, 295 | ) 296 | postDatum = models.TunaV1State{ 297 | BlockNumber: blockData.BlockNumber + 1, 298 | CurrentHash: targetHash, 299 | LeadingZeros: leadingZeros, 300 | DifficultyNumber: difficultyNumber, 301 | EpochTime: epochTime, 302 | RealTimeNow: 90000 + realTimeNow, 303 | Extra: []byte(cfg.Miner.Message), 304 | Interlink: currentInterlink, 305 | } 306 | } else { 307 | blockData := m.blockData.(models.TunaV2State) 308 | // Temporarily add new target hash to trie to calculate new block's merkle root hash 309 | trie := storage.GetStorage().Trie() 310 | trie.Lock() 311 | tmpHashKey := storage.HashValue(targetHash).Bytes() 312 | if err := trie.Update(tmpHashKey, targetHash, 0); err != nil { 313 | panic(fmt.Sprintf("failed to update storage for trie: %s", err)) 314 | } 315 | postDatum = models.TunaV2State{ 316 | BlockNumber: blockData.BlockNumber + 1, 317 | CurrentHash: targetHash, 318 | LeadingZeros: leadingZeros, 319 | DifficultyNumber: difficultyNumber, 320 | EpochTime: epochTime, 321 | CurrentPosixTime: 90000 + realTimeNow, 322 | MerkleRoot: trie.Hash(), 323 | } 324 | // Remove item from trie until it comes in via the indexer 325 | _ = trie.Delete(tmpHashKey) 326 | trie.Unlock() 327 | } 328 | 329 | // Check for shutdown 330 | select { 331 | case <-m.doneChan: 332 | return 333 | default: 334 | break 335 | } 336 | 337 | // Return the result 338 | m.resultChan <- Result{BlockData: postDatum, Nonce: m.state.GetNonce()} 339 | } 340 | 341 | func randomNonce() [16]byte { 342 | var ret [16]byte 343 | // This will never return an error 344 | _, _ = rand.Read(ret[:]) 345 | return ret 346 | } 347 | 348 | func (m *Miner) calculateHash() []byte { 349 | var tmpLeadingZeros int64 350 | var tmpDifficultyNumber int64 351 | switch v := m.blockData.(type) { 352 | case models.TunaV1State: 353 | tmpLeadingZeros = v.LeadingZeros 354 | tmpDifficultyNumber = v.DifficultyNumber 355 | case models.TunaV2State: 356 | tmpLeadingZeros = v.LeadingZeros 357 | tmpDifficultyNumber = v.DifficultyNumber 358 | default: 359 | panic("unknown state model type") 360 | } 361 | for { 362 | // Check for shutdown 363 | select { 364 | case <-m.doneChan: 365 | return nil 366 | default: 367 | break 368 | } 369 | stateBytes, err := m.state.MarshalCBOR() 370 | if err != nil { 371 | slog.Error(err.Error()) 372 | return nil 373 | } 374 | 375 | // Hash it once 376 | hasher := sha256.New() 377 | hasher.Write(stateBytes) 378 | hash := hasher.Sum(nil) 379 | 380 | // And hash it again 381 | hasher2 := sha256.New() 382 | hasher2.Write(hash) 383 | hash2 := hasher2.Sum(nil) 384 | 385 | // Increment hash counter 386 | m.hashCounter.Add(1) 387 | 388 | // Get the difficulty metrics for the hash 389 | metrics := getDifficulty(hash2) 390 | 391 | // Check the condition 392 | if metrics.LeadingZeros > tmpLeadingZeros || 393 | (metrics.LeadingZeros == tmpLeadingZeros && metrics.DifficultyNumber < tmpDifficultyNumber) { 394 | return hash2 395 | } 396 | 397 | // Generate a new random nonce when nonceCount rolls over, and increment bytes in existing nonce otherwise 398 | if m.nonceCount == 0 { 399 | m.state.SetNonce(randomNonce()) 400 | } else { 401 | nonce := m.state.GetNonce() 402 | // Increment each byte of the nonce 403 | for j := range 16 { 404 | nonce[j]++ 405 | } 406 | m.state.SetNonce(nonce) 407 | } 408 | m.nonceCount++ 409 | } 410 | } 411 | 412 | func (m *Miner) getCurrentDifficulty() DifficultyMetrics { 413 | var tmpLeadingZeros int64 414 | var tmpDifficultyNumber int64 415 | switch v := m.blockData.(type) { 416 | case models.TunaV1State: 417 | tmpLeadingZeros = v.LeadingZeros 418 | tmpDifficultyNumber = v.DifficultyNumber 419 | case models.TunaV2State: 420 | tmpLeadingZeros = v.LeadingZeros 421 | tmpDifficultyNumber = v.DifficultyNumber 422 | default: 423 | panic("unknown state model type") 424 | } 425 | return DifficultyMetrics{ 426 | LeadingZeros: tmpLeadingZeros, 427 | DifficultyNumber: tmpDifficultyNumber, 428 | } 429 | } 430 | 431 | func getDifficulty(hash []byte) DifficultyMetrics { 432 | var leadingZeros int64 433 | var difficultyNumber int64 434 | for indx, chr := range hash { 435 | if chr != 0 { 436 | // 437 | if (chr & 0x0F) == chr { 438 | leadingZeros += 1 439 | difficultyNumber += int64(chr) * 4096 440 | difficultyNumber += int64(hash[indx+1]) * 16 441 | difficultyNumber += int64(hash[indx+2]) / 16 442 | return DifficultyMetrics{ 443 | LeadingZeros: leadingZeros, 444 | DifficultyNumber: difficultyNumber, 445 | } 446 | } else { 447 | difficultyNumber += int64(chr) * 256 448 | difficultyNumber += int64(hash[indx+1]) 449 | return DifficultyMetrics{ 450 | LeadingZeros: leadingZeros, 451 | DifficultyNumber: difficultyNumber, 452 | } 453 | } 454 | } else { 455 | leadingZeros += 2 456 | } 457 | } 458 | return DifficultyMetrics{ 459 | LeadingZeros: 32, 460 | DifficultyNumber: 0, 461 | } 462 | } 463 | 464 | func calculateInterlink( 465 | currentHash []byte, 466 | newDifficulty DifficultyMetrics, 467 | origDifficulty DifficultyMetrics, 468 | currentInterlink [][]byte, 469 | ) [][]byte { 470 | interlink := currentInterlink 471 | 472 | origHalf := halfDifficultyNumber(origDifficulty) 473 | currentIndex := 0 474 | 475 | for origHalf.LeadingZeros < newDifficulty.LeadingZeros || (origHalf.LeadingZeros == newDifficulty.LeadingZeros && origHalf.DifficultyNumber > newDifficulty.DifficultyNumber) { 476 | if currentIndex < len(interlink) { 477 | interlink[currentIndex] = currentHash 478 | } else { 479 | interlink = append(interlink, currentHash) 480 | } 481 | 482 | origHalf = halfDifficultyNumber(origHalf) 483 | currentIndex++ 484 | } 485 | 486 | return interlink 487 | } 488 | 489 | func halfDifficultyNumber(metrics DifficultyMetrics) DifficultyMetrics { 490 | newA := metrics.DifficultyNumber / 2 491 | if newA < 4096 { 492 | return DifficultyMetrics{ 493 | LeadingZeros: metrics.LeadingZeros + 1, 494 | DifficultyNumber: newA * 16, 495 | } 496 | } else { 497 | return DifficultyMetrics{ 498 | LeadingZeros: metrics.LeadingZeros, 499 | DifficultyNumber: newA, 500 | } 501 | } 502 | } 503 | 504 | func getDifficultyAdjustment( 505 | totalEpochTime int64, 506 | epochTarget int64, 507 | ) DifficultyAdjustment { 508 | if epochTarget/totalEpochTime >= 4 && epochTarget%totalEpochTime > 0 { 509 | return DifficultyAdjustment{ 510 | Numerator: 1, 511 | Denominator: 4, 512 | } 513 | } else if totalEpochTime/epochTarget >= 4 && totalEpochTime%epochTarget > 0 { 514 | return DifficultyAdjustment{ 515 | Numerator: 4, 516 | Denominator: 1, 517 | } 518 | } else { 519 | return DifficultyAdjustment{ 520 | Numerator: totalEpochTime, 521 | Denominator: epochTarget, 522 | } 523 | } 524 | } 525 | 526 | func calculateDifficultyNumber( 527 | diffMetrics DifficultyMetrics, 528 | diffAdjustment DifficultyAdjustment, 529 | ) DifficultyMetrics { 530 | newPaddedDifficulty := (diffMetrics.DifficultyNumber * 16 * diffAdjustment.Numerator) / diffAdjustment.Denominator 531 | newDifficulty := newPaddedDifficulty / 16 532 | if newPaddedDifficulty/65536 == 0 { 533 | if diffMetrics.LeadingZeros >= 62 { 534 | return DifficultyMetrics{ 535 | DifficultyNumber: 4096, 536 | LeadingZeros: 62, 537 | } 538 | } else { 539 | return DifficultyMetrics{ 540 | DifficultyNumber: newPaddedDifficulty, 541 | LeadingZeros: diffMetrics.LeadingZeros + 1, 542 | } 543 | } 544 | } else if newDifficulty/65536 > 0 { 545 | if diffMetrics.LeadingZeros <= 2 { 546 | return DifficultyMetrics{ 547 | DifficultyNumber: 65535, 548 | LeadingZeros: 2, 549 | } 550 | } else { 551 | return DifficultyMetrics{ 552 | DifficultyNumber: newDifficulty / 16, 553 | LeadingZeros: diffMetrics.LeadingZeros - 1, 554 | } 555 | } 556 | } else { 557 | return DifficultyMetrics{ 558 | DifficultyNumber: newDifficulty, 559 | LeadingZeros: diffMetrics.LeadingZeros, 560 | } 561 | } 562 | } 563 | -------------------------------------------------------------------------------- /internal/miner/miner_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 miner 16 | 17 | import ( 18 | "crypto/sha256" 19 | "hash" 20 | "testing" 21 | 22 | sha256_simd "github.com/minio/sha256-simd" 23 | ) 24 | 25 | // BenchmarkRandomNonce tests the performance of generating a completely new random nonce 26 | // each iteration 27 | func BenchmarkRandomNonce(b *testing.B) { 28 | for i := 0; i < b.N; i++ { 29 | randomNonce() 30 | } 31 | } 32 | 33 | // BenchmarkRandomNonceAndIncrement tests the performance of generating a random nonce 34 | // and incrementing each byte 255 times 35 | func BenchmarkRandomNonceAndIncrement(b *testing.B) { 36 | var tmpNonce [16]byte 37 | for i := 0; i < b.N; i++ { 38 | if i%256 == 0 { 39 | // Generate random nonce for first and every 256 iterations 40 | tmpNonce = randomNonce() 41 | } else { 42 | // Increment each byte of the last nonce 43 | for j := range 16 { 44 | tmpNonce[j] = byte(uint8(tmpNonce[j]) + 1) 45 | } 46 | } 47 | } 48 | } 49 | 50 | // BenchmarkSha256Builtin tests the performance of crypto/sha256 51 | func BenchmarkSha256Builtin(b *testing.B) { 52 | var hasher hash.Hash 53 | tmpNonce := randomNonce() 54 | for i := 0; i < b.N; i++ { 55 | hasher = sha256.New() 56 | hasher.Write(tmpNonce[:]) 57 | hasher.Sum(nil) 58 | } 59 | } 60 | 61 | // BenchmarkSha256Simd tests the performance github.com/minio/sha256-simd 62 | func BenchmarkSha256Simd(b *testing.B) { 63 | var hasher hash.Hash 64 | tmpNonce := randomNonce() 65 | for i := 0; i < b.N; i++ { 66 | hasher = sha256_simd.New() 67 | hasher.Write(tmpNonce[:]) 68 | hasher.Sum(nil) 69 | } 70 | } 71 | 72 | // BenchmarkTargetStateCborMarshal tests marshaling TargetStateV2 to CBOR 73 | func BenchmarkTargetStateCborMarshal(b *testing.B) { 74 | tmpHash := [32]byte{} 75 | tmpState := TargetStateV2{ 76 | Nonce: randomNonce(), 77 | MinerCredHash: tmpHash[:], 78 | EpochTime: 999999, 79 | BlockNumber: 999999, 80 | CurrentHash: tmpHash[:], 81 | LeadingZeros: 4, 82 | DifficultyNumber: 65535, 83 | } 84 | for i := 0; i < b.N; i++ { 85 | _, _ = tmpState.MarshalCBOR() 86 | } 87 | } 88 | 89 | // BenchmarkTargetStateCborMarshalNoCache tests marshaling TargetStateV2 to CBOR without caching 90 | func BenchmarkTargetStateCborMarshalNoCache(b *testing.B) { 91 | tmpHash := [32]byte{} 92 | tmpState := TargetStateV2{ 93 | Nonce: randomNonce(), 94 | MinerCredHash: tmpHash[:], 95 | EpochTime: 999999, 96 | BlockNumber: 999999, 97 | CurrentHash: tmpHash[:], 98 | LeadingZeros: 4, 99 | DifficultyNumber: 65535, 100 | } 101 | for i := 0; i < b.N; i++ { 102 | _, _ = tmpState.MarshalCBOR() 103 | // Wipe out CBOR cache 104 | tmpState.cachedCbor = nil 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /internal/miner/worker.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 miner 16 | 17 | import ( 18 | "fmt" 19 | "log/slog" 20 | "sync" 21 | "sync/atomic" 22 | "time" 23 | 24 | "github.com/blinklabs-io/bluefin/internal/config" 25 | "github.com/blinklabs-io/bluefin/internal/tx" 26 | ) 27 | 28 | const ( 29 | restartTimeout = 2 * time.Minute 30 | ) 31 | 32 | type Manager struct { 33 | workerWaitGroup sync.WaitGroup 34 | doneChan chan any 35 | resultChan chan Result 36 | started bool 37 | startMutex sync.Mutex 38 | stopMutex sync.Mutex 39 | hashCounter *atomic.Uint64 40 | hashLogTimer *time.Timer 41 | hashLogLastCount uint64 42 | restartTimer *time.Timer 43 | lastBlockData any 44 | } 45 | 46 | var globalManager = &Manager{} 47 | 48 | func (m *Manager) Reset() { 49 | m.workerWaitGroup = sync.WaitGroup{} 50 | m.doneChan = make(chan any) 51 | m.resultChan = make(chan Result, config.GetConfig().Miner.WorkerCount) 52 | } 53 | 54 | func (m *Manager) Stop() { 55 | m.stopMutex.Lock() 56 | defer m.stopMutex.Unlock() 57 | if !m.started { 58 | return 59 | } 60 | if m.hashLogTimer != nil { 61 | m.hashLogTimer.Stop() 62 | } 63 | close(m.doneChan) 64 | m.workerWaitGroup.Wait() 65 | close(m.resultChan) 66 | m.started = false 67 | slog.Info("stopped workers") 68 | // Start timer to restart miner 69 | m.restartTimer = time.AfterFunc( 70 | restartTimeout, 71 | func() { 72 | slog.Warn( 73 | fmt.Sprintf( 74 | "restarting miner automatically after %s timeout", 75 | restartTimeout, 76 | ), 77 | ) 78 | m.Start(m.lastBlockData) 79 | }, 80 | ) 81 | } 82 | 83 | func (m *Manager) Start(blockData any) { 84 | m.startMutex.Lock() 85 | defer m.startMutex.Unlock() 86 | if m.started { 87 | return 88 | } 89 | m.lastBlockData = blockData 90 | // Cancel any restart timer 91 | if m.restartTimer != nil { 92 | m.restartTimer.Stop() 93 | } 94 | cfg := config.GetConfig() 95 | // Start hash rate log timer 96 | m.hashCounter = &atomic.Uint64{} 97 | m.scheduleHashRateLog() 98 | // Start workers 99 | m.Reset() 100 | slog.Info( 101 | fmt.Sprintf("starting %d workers", cfg.Miner.WorkerCount), 102 | ) 103 | for range cfg.Miner.WorkerCount { 104 | miner := New( 105 | &(m.workerWaitGroup), 106 | m.resultChan, 107 | m.doneChan, 108 | blockData, 109 | m.hashCounter, 110 | ) 111 | m.workerWaitGroup.Add(1) 112 | go miner.Start() 113 | } 114 | // Wait for result 115 | go func() { 116 | select { 117 | case <-m.doneChan: 118 | return 119 | case result := <-m.resultChan: 120 | // Stop workers until our result makes it on-chain 121 | m.Stop() 122 | // Build and submit the TX 123 | if err := tx.SendTx(result.BlockData, result.Nonce); err != nil { 124 | slog.Error( 125 | fmt.Sprintf("failed to submit TX: %s", err), 126 | ) 127 | } 128 | } 129 | }() 130 | m.started = true 131 | } 132 | 133 | func (m *Manager) scheduleHashRateLog() { 134 | cfg := config.GetConfig() 135 | m.hashLogTimer = time.AfterFunc( 136 | time.Duration(cfg.Miner.HashRateInterval)*time.Second, 137 | m.hashRateLog, 138 | ) 139 | } 140 | 141 | func (m *Manager) hashRateLog() { 142 | cfg := config.GetConfig() 143 | hashCount := m.hashCounter.Load() 144 | // Handle counter rollover 145 | if hashCount < m.hashLogLastCount { 146 | m.hashLogLastCount = 0 147 | m.scheduleHashRateLog() 148 | return 149 | } 150 | hashCountDiff := hashCount - m.hashLogLastCount 151 | m.hashLogLastCount = hashCount 152 | secondDivisor := uint64(cfg.Miner.HashRateInterval) // #nosec G115 153 | hashCountPerSec := hashCountDiff / secondDivisor 154 | slog.Info( 155 | fmt.Sprintf("hash rate: %d/s", hashCountPerSec), 156 | ) 157 | m.scheduleHashRateLog() 158 | } 159 | 160 | func GetManager() *Manager { 161 | return globalManager 162 | } 163 | -------------------------------------------------------------------------------- /internal/storage/storage.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 storage 16 | 17 | import ( 18 | "encoding/hex" 19 | "errors" 20 | "fmt" 21 | "log/slog" 22 | "math" 23 | "strconv" 24 | "strings" 25 | "sync" 26 | 27 | "github.com/blinklabs-io/bluefin/internal/config" 28 | "github.com/blinklabs-io/gouroboros/cbor" 29 | "github.com/dgraph-io/badger/v4" 30 | ) 31 | 32 | const ( 33 | chainsyncCursorKey = "chainsync_cursor" 34 | fingerprintKey = "config_fingerprint" 35 | minerBlockDataKey = "miner_block_data" 36 | ) 37 | 38 | type Storage struct { 39 | sync.Mutex 40 | db *badger.DB 41 | trie *Trie 42 | } 43 | 44 | var globalStorage = &Storage{} 45 | 46 | func (s *Storage) Load() error { 47 | cfg := config.GetConfig() 48 | badgerOpts := badger.DefaultOptions(cfg.Storage.Directory). 49 | WithLogger(NewBadgerLogger()). 50 | // The default INFO logging is a bit verbose 51 | WithLoggingLevel(badger.WARNING) 52 | db, err := badger.Open(badgerOpts) 53 | // TODO: setup automatic GC for Badger 54 | if err != nil { 55 | return err 56 | } 57 | s.db = db 58 | // defer db.Close() 59 | if err := s.compareFingerprint(); err != nil { 60 | return err 61 | } 62 | // Populate trie 63 | trie, err := NewTrie(s.db, cfg.Profile) 64 | if err != nil { 65 | return err 66 | } 67 | s.trie = trie 68 | return nil 69 | } 70 | 71 | func (s *Storage) compareFingerprint() error { 72 | cfg := config.GetConfig() 73 | fingerprint := fmt.Sprintf( 74 | "network=%s,profile=%s", 75 | cfg.Network, 76 | cfg.Profile, 77 | ) 78 | err := s.db.Update(func(txn *badger.Txn) error { 79 | item, err := txn.Get([]byte(fingerprintKey)) 80 | if err != nil { 81 | if errors.Is(err, badger.ErrKeyNotFound) { 82 | if err := txn.Set([]byte(fingerprintKey), []byte(fingerprint)); err != nil { 83 | return err 84 | } 85 | return nil 86 | } else { 87 | return err 88 | } 89 | } 90 | err = item.Value(func(v []byte) error { 91 | if string(v) != fingerprint { 92 | return fmt.Errorf( 93 | "config fingerprint in DB doesn't match current config: %s", 94 | v, 95 | ) 96 | } 97 | return nil 98 | }) 99 | if err != nil { 100 | return err 101 | } 102 | return nil 103 | }) 104 | if err != nil { 105 | return err 106 | } 107 | return nil 108 | } 109 | 110 | func (s *Storage) Trie() *Trie { 111 | return s.trie 112 | } 113 | 114 | func (s *Storage) UpdateCursor(slotNumber uint64, blockHash string) error { 115 | err := s.db.Update(func(txn *badger.Txn) error { 116 | val := fmt.Sprintf("%d,%s", slotNumber, blockHash) 117 | if err := txn.Set([]byte(chainsyncCursorKey), []byte(val)); err != nil { 118 | return err 119 | } 120 | return nil 121 | }) 122 | return err 123 | } 124 | 125 | func (s *Storage) GetCursor() (uint64, string, error) { 126 | var slotNumber uint64 127 | var blockHash string 128 | err := s.db.View(func(txn *badger.Txn) error { 129 | item, err := txn.Get([]byte(chainsyncCursorKey)) 130 | if err != nil { 131 | return err 132 | } 133 | err = item.Value(func(v []byte) error { 134 | var err error 135 | cursorParts := strings.Split(string(v), ",") 136 | slotNumber, err = strconv.ParseUint(cursorParts[0], 10, 64) 137 | if err != nil { 138 | return err 139 | } 140 | blockHash = cursorParts[1] 141 | return nil 142 | }) 143 | if err != nil { 144 | return err 145 | } 146 | return nil 147 | }) 148 | if errors.Is(err, badger.ErrKeyNotFound) { 149 | return 0, "", nil 150 | } 151 | return slotNumber, blockHash, err 152 | } 153 | 154 | func (s *Storage) UpdateBlockData(blockData any) error { 155 | blockDataCbor, err := cbor.Encode(blockData) 156 | if err != nil { 157 | return err 158 | } 159 | err = s.db.Update(func(txn *badger.Txn) error { 160 | if err := txn.Set([]byte(minerBlockDataKey), blockDataCbor); err != nil { 161 | return err 162 | } 163 | return nil 164 | }) 165 | return err 166 | } 167 | 168 | func (s *Storage) GetBlockData(dest any) error { 169 | err := s.db.View(func(txn *badger.Txn) error { 170 | item, err := txn.Get([]byte(minerBlockDataKey)) 171 | if err != nil { 172 | return err 173 | } 174 | err = item.Value(func(v []byte) error { 175 | if _, err := cbor.Decode(v, dest); err != nil { 176 | return err 177 | } 178 | return nil 179 | }) 180 | return err 181 | }) 182 | if err != nil { 183 | if errors.Is(err, badger.ErrKeyNotFound) { 184 | return nil 185 | } 186 | return err 187 | } 188 | return nil 189 | } 190 | 191 | func (s *Storage) AddUtxo( 192 | address string, 193 | txId string, 194 | txOutIdx uint32, 195 | txOutBytes []byte, 196 | slot uint64, 197 | ) error { 198 | if slot > math.MaxInt { 199 | return errors.New("slot number int overflow") 200 | } 201 | keyUtxo := fmt.Sprintf("utxo_%s_%s.%d", address, txId, txOutIdx) 202 | keyAdded := keyUtxo + `_added` 203 | err := s.db.Update(func(txn *badger.Txn) error { 204 | // Wrap TX output in UTxO structure to make it easier to consume later 205 | txIdBytes, err := hex.DecodeString(txId) 206 | if err != nil { 207 | return err 208 | } 209 | // Create temp UTxO structure 210 | utxoTmp := []any{ 211 | // Transaction output reference 212 | []any{ 213 | txIdBytes, 214 | uint32(txOutIdx), 215 | }, 216 | // Transaction output CBOR 217 | cbor.RawMessage(txOutBytes), 218 | } 219 | // Convert to CBOR 220 | cborBytes, err := cbor.Encode(&utxoTmp) 221 | if err != nil { 222 | return err 223 | } 224 | if err := txn.Set([]byte(keyUtxo), cborBytes); err != nil { 225 | return err 226 | } 227 | // Set "added" key to provided slot number 228 | if err := txn.Set( 229 | []byte(keyAdded), 230 | []byte( 231 | // Convert slot to string for storage 232 | strconv.Itoa(int(slot)), // #nosec G115 233 | ), 234 | ); err != nil { 235 | return err 236 | } 237 | return nil 238 | }) 239 | return err 240 | } 241 | 242 | func (s *Storage) RemoveUtxo( 243 | address string, 244 | txId string, 245 | utxoIdx uint32, 246 | slot uint64, 247 | ) error { 248 | if slot > math.MaxInt { 249 | return errors.New("slot number int overflow") 250 | } 251 | keyUtxo := fmt.Sprintf("utxo_%s_%s.%d", address, txId, utxoIdx) 252 | keyDeleted := keyUtxo + `_deleted` 253 | err := s.db.Update(func(txn *badger.Txn) error { 254 | // Check if UTxO exists at all 255 | if _, err := txn.Get([]byte(keyUtxo)); err != nil { 256 | return err 257 | } 258 | // Set "deleted" key to provided slot number 259 | if err := txn.Set( 260 | []byte(keyDeleted), 261 | []byte( 262 | // Convert slot to string for storage 263 | strconv.Itoa(int(slot)), // #nosec G115 264 | ), 265 | ); err != nil { 266 | return err 267 | } 268 | return nil 269 | }) 270 | if err != nil { 271 | if errors.Is(err, badger.ErrKeyNotFound) { 272 | return nil 273 | } 274 | return err 275 | } 276 | return nil 277 | } 278 | 279 | func (s *Storage) GetScriptRefUtxo(txId string, outputIdx int) ([]byte, error) { 280 | var ret []byte 281 | key := fmt.Appendf(nil, "utxo_script_ref_%s.%d", txId, outputIdx) 282 | err := s.db.View(func(txn *badger.Txn) error { 283 | item, err := txn.Get(key) 284 | if err != nil { 285 | return err 286 | } 287 | ret, err = item.ValueCopy(nil) 288 | return err 289 | }) 290 | if err != nil { 291 | return nil, err 292 | } 293 | return ret, nil 294 | } 295 | 296 | func (s *Storage) GetUtxos(address string) ([][]byte, error) { 297 | var ret [][]byte 298 | keyPrefix := fmt.Appendf(nil, "utxo_%s_", address) 299 | err := s.db.View(func(txn *badger.Txn) error { 300 | it := txn.NewIterator(badger.DefaultIteratorOptions) 301 | defer it.Close() 302 | for it.Seek(keyPrefix); it.ValidForPrefix(keyPrefix); it.Next() { 303 | item := it.Item() 304 | key := item.Key() 305 | // Ignore "added" and "deleted" metadata keys when iterating 306 | if strings.HasSuffix(string(key), `_deleted`) || 307 | strings.HasSuffix(string(key), `_added`) { 308 | continue 309 | } 310 | // Ignore "deleted" UTxOs 311 | keyDeleted := string(key) + `_deleted` 312 | if _, err := txn.Get([]byte(keyDeleted)); !errors.Is( 313 | err, 314 | badger.ErrKeyNotFound, 315 | ) { 316 | continue 317 | } 318 | val, err := item.ValueCopy(nil) 319 | if err != nil { 320 | return err 321 | } 322 | ret = append(ret, val) 323 | } 324 | return nil 325 | }) 326 | if err != nil { 327 | return nil, err 328 | } 329 | if len(ret) == 0 { 330 | return nil, nil 331 | } 332 | return ret, nil 333 | } 334 | 335 | func (s *Storage) Rollback(slot uint64) error { 336 | keyPrefix := []byte(`utxo_`) 337 | var deleteKeys [][]byte 338 | err := s.db.Update(func(txn *badger.Txn) error { 339 | it := txn.NewIterator(badger.DefaultIteratorOptions) 340 | defer it.Close() 341 | for it.Seek(keyPrefix); it.ValidForPrefix(keyPrefix); it.Next() { 342 | item := it.Item() 343 | key := item.KeyCopy(nil) 344 | // Ignore "added" and "deleted" metadata keys when iterating 345 | if strings.HasSuffix(string(key), `_deleted`) || 346 | strings.HasSuffix(string(key), `_added`) { 347 | continue 348 | } 349 | // Restore UTxOs deleted after rollback slot 350 | keyDeleted := string(key) + `_deleted` 351 | delItem, err := txn.Get([]byte(keyDeleted)) 352 | if err != nil && !errors.Is(err, badger.ErrKeyNotFound) { 353 | return err 354 | } 355 | if !errors.Is(err, badger.ErrKeyNotFound) { 356 | delVal, err := delItem.ValueCopy(nil) 357 | if err != nil { 358 | return err 359 | } 360 | delSlot, err := strconv.ParseUint(string(delVal), 10, 64) 361 | if err != nil { 362 | return err 363 | } 364 | if delSlot > slot { 365 | slog.Debug( 366 | fmt.Sprintf( 367 | "deleting key %s ('deleted' slot %d) to restore deleted UTxO", 368 | keyDeleted, 369 | delSlot, 370 | ), 371 | ) 372 | deleteKeys = append(deleteKeys, []byte(keyDeleted)) 373 | } 374 | } 375 | // Remove UTxOs added after rollback slot 376 | keyAdded := string(key) + `_added` 377 | addItem, err := txn.Get([]byte(keyAdded)) 378 | if err != nil && !errors.Is(err, badger.ErrKeyNotFound) { 379 | return err 380 | } 381 | if !errors.Is(err, badger.ErrKeyNotFound) { 382 | addVal, err := addItem.ValueCopy(nil) 383 | if err != nil { 384 | return err 385 | } 386 | addSlot, err := strconv.ParseUint(string(addVal), 10, 64) 387 | if err != nil { 388 | return err 389 | } 390 | if addSlot > slot { 391 | slog.Debug( 392 | fmt.Sprintf( 393 | "deleting keys %s ('added' slot %d) and %s to remove rolled-back UTxO", 394 | key, 395 | addSlot, 396 | keyAdded, 397 | ), 398 | ) 399 | deleteKeys = append( 400 | deleteKeys, 401 | key, 402 | []byte(keyAdded), 403 | ) 404 | } 405 | } 406 | } 407 | // We delete the keys outside of the iterator, because apparently you can't delete 408 | // the current key when iterating 409 | for _, key := range deleteKeys { 410 | if err := txn.Delete([]byte(key)); err != nil { 411 | return err 412 | } 413 | } 414 | return nil 415 | }) 416 | // Remove rolled-back hashes from trie 417 | if err := s.trie.Rollback(slot); err != nil { 418 | return err 419 | } 420 | return err 421 | } 422 | 423 | func (s *Storage) PurgeDeletedUtxos(beforeSlot uint64) error { 424 | keyPrefix := []byte(`utxo_`) 425 | var deleteKeys [][]byte 426 | err := s.db.Update(func(txn *badger.Txn) error { 427 | it := txn.NewIterator(badger.DefaultIteratorOptions) 428 | defer it.Close() 429 | for it.Seek(keyPrefix); it.ValidForPrefix(keyPrefix); it.Next() { 430 | item := it.Item() 431 | key := item.KeyCopy(nil) 432 | // Ignore "added" and "deleted" metadata keys when iterating 433 | if strings.HasSuffix(string(key), `_deleted`) || 434 | strings.HasSuffix(string(key), `_added`) { 435 | continue 436 | } 437 | // Check for "deleted" key 438 | keyDeleted := string(key) + `_deleted` 439 | delItem, err := txn.Get([]byte(keyDeleted)) 440 | if err != nil { 441 | if errors.Is(err, badger.ErrKeyNotFound) { 442 | continue 443 | } 444 | return err 445 | } 446 | delVal, err := delItem.ValueCopy(nil) 447 | if err != nil { 448 | return err 449 | } 450 | delSlot, err := strconv.ParseUint(string(delVal), 10, 64) 451 | if err != nil { 452 | return err 453 | } 454 | if delSlot < beforeSlot { 455 | deleteKeys = append( 456 | deleteKeys, 457 | // UTxO key 458 | key, 459 | // UTxO "added" key 460 | []byte(string(key)+`_added`), 461 | // UTxO "deleted" key 462 | []byte(string(key)+`_deleted`), 463 | ) 464 | } 465 | } 466 | // We delete the keys outside of the iterator, because apparently you can't delete 467 | // the current key when iterating 468 | for _, key := range deleteKeys { 469 | if err := txn.Delete([]byte(key)); err != nil { 470 | // Leave the rest for the next run if we hit the max transaction size 471 | if errors.Is(err, badger.ErrTxnTooBig) { 472 | slog.Debug( 473 | "purge deleted UTxOs: badger transaction too large, leaving remainder until next run", 474 | ) 475 | break 476 | } 477 | return err 478 | } 479 | slog.Debug( 480 | fmt.Sprintf("purged deleted UTxO key: %s", key), 481 | ) 482 | } 483 | return nil 484 | }) 485 | return err 486 | } 487 | 488 | func GetStorage() *Storage { 489 | return globalStorage 490 | } 491 | 492 | // BadgerLogger is a wrapper type to give our logger the expected interface 493 | type BadgerLogger struct{} 494 | 495 | func NewBadgerLogger() *BadgerLogger { 496 | return &BadgerLogger{} 497 | } 498 | 499 | func (b *BadgerLogger) Infof(msg string, args ...any) { 500 | slog.Info( 501 | fmt.Sprintf(msg, args...), 502 | ) 503 | } 504 | 505 | func (b *BadgerLogger) Warningf(msg string, args ...any) { 506 | slog.Warn( 507 | fmt.Sprintf(msg, args...), 508 | ) 509 | } 510 | 511 | func (b *BadgerLogger) Debugf(msg string, args ...any) { 512 | slog.Debug( 513 | fmt.Sprintf(msg, args...), 514 | ) 515 | } 516 | 517 | func (b *BadgerLogger) Errorf(msg string, args ...any) { 518 | slog.Error( 519 | fmt.Sprintf(msg, args...), 520 | ) 521 | } 522 | -------------------------------------------------------------------------------- /internal/storage/trie.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 storage 16 | 17 | import ( 18 | "encoding/hex" 19 | "errors" 20 | "fmt" 21 | "math" 22 | "strconv" 23 | "strings" 24 | "sync" 25 | 26 | "github.com/blinklabs-io/bluefin/internal/config" 27 | mpf "github.com/blinklabs-io/merkle-patricia-forestry" 28 | "github.com/dgraph-io/badger/v4" 29 | "golang.org/x/crypto/blake2b" 30 | ) 31 | 32 | type Trie struct { 33 | sync.Mutex 34 | db *badger.DB 35 | trie *mpf.Trie 36 | keyPrefix []byte 37 | } 38 | 39 | func NewTrie(db *badger.DB, keyPrefix string) (*Trie, error) { 40 | t := &Trie{ 41 | db: db, 42 | trie: mpf.NewTrie(), 43 | keyPrefix: []byte(keyPrefix), 44 | } 45 | if err := t.load(); err != nil { 46 | return nil, err 47 | } 48 | return t, nil 49 | } 50 | 51 | func (t *Trie) load() error { 52 | // Load seed keys 53 | profile := config.GetProfile() 54 | for _, seedHash := range profile.SeedHashes { 55 | seedHashBytes, err := hex.DecodeString(seedHash) 56 | if err != nil { 57 | return err 58 | } 59 | trieKey := t.HashKey(seedHashBytes) 60 | if err := t.Update(trieKey, seedHashBytes, 0); err != nil { 61 | return err 62 | } 63 | } 64 | // Load values from storage 65 | dbKeyPrefix := t.dbKeyPrefix(nil) 66 | err := t.db.View(func(txn *badger.Txn) error { 67 | it := txn.NewIterator(badger.DefaultIteratorOptions) 68 | defer it.Close() 69 | for it.Seek(dbKeyPrefix); it.ValidForPrefix(dbKeyPrefix); it.Next() { 70 | item := it.Item() 71 | val, err := item.ValueCopy(nil) 72 | if err != nil { 73 | return err 74 | } 75 | // Insert key/value into trie 76 | tmpKey := strings.TrimPrefix( 77 | string(item.Key()), 78 | string(dbKeyPrefix), 79 | ) 80 | t.trie.Set([]byte(tmpKey), val) 81 | } 82 | return nil 83 | }) 84 | return err 85 | } 86 | 87 | func (t *Trie) Update(key []byte, val []byte, slot uint64) error { 88 | if slot > math.MaxInt { 89 | return errors.New("slot number int overflow") 90 | } 91 | // Update trie 92 | t.trie.Set(key, val) 93 | // Update storage 94 | dbKey := t.dbKeyPrefix(key) 95 | err := t.db.Update(func(txn *badger.Txn) error { 96 | if err := txn.Set(dbKey, val); err != nil { 97 | return err 98 | } 99 | // Set "added" key to provided slot number 100 | keyAdded := `meta_` + string(dbKey) + `_added` 101 | if err := txn.Set( 102 | []byte(keyAdded), 103 | []byte( 104 | // Convert slot to string for storage 105 | strconv.Itoa(int(slot)), // #nosec G115 106 | ), 107 | ); err != nil { 108 | return err 109 | } 110 | return nil 111 | }) 112 | return err 113 | } 114 | 115 | func (t *Trie) Delete(key []byte) error { 116 | // Update trie 117 | if err := t.trie.Delete(key); err != nil { 118 | return err 119 | } 120 | // Update storage 121 | dbKey := t.dbKeyPrefix(key) 122 | err := t.db.Update(func(txn *badger.Txn) error { 123 | if err := txn.Delete(dbKey); err != nil { 124 | return err 125 | } 126 | // Delete "added" key 127 | keyAdded := `meta_` + string(dbKey) + `_added` 128 | if err := txn.Delete([]byte(keyAdded)); err != nil { 129 | if !errors.Is(err, badger.ErrKeyNotFound) { 130 | return err 131 | } 132 | } 133 | return nil 134 | }) 135 | return err 136 | } 137 | 138 | func (t *Trie) Rollback(slot uint64) error { 139 | if slot > math.MaxInt64 { 140 | return errors.New("slot number int overflow") 141 | } 142 | dbKeyPrefix := t.dbKeyPrefix(nil) 143 | err := t.db.Update(func(txn *badger.Txn) error { 144 | it := txn.NewIterator(badger.DefaultIteratorOptions) 145 | defer it.Close() 146 | for it.Seek(dbKeyPrefix); it.ValidForPrefix(dbKeyPrefix); it.Next() { 147 | item := it.Item() 148 | key := item.Key() 149 | keyAdded := `meta_` + string(key) + `_added` 150 | addItem, err := txn.Get([]byte(keyAdded)) 151 | if err != nil { 152 | if errors.Is(err, badger.ErrKeyNotFound) { 153 | continue 154 | } 155 | return err 156 | } 157 | addVal, err := addItem.ValueCopy(nil) 158 | if err != nil { 159 | return err 160 | } 161 | addSlot, err := strconv.Atoi(string(addVal)) 162 | if err != nil { 163 | return err 164 | } 165 | // #nosec G115 166 | if addSlot > int(slot) { 167 | // Delete rolled-back hashes from trie 168 | tmpKey := strings.TrimPrefix( 169 | string(item.Key()), 170 | string(dbKeyPrefix), 171 | ) 172 | if err := t.Delete([]byte(tmpKey)); err != nil { 173 | return err 174 | } 175 | } 176 | } 177 | return nil 178 | }) 179 | return err 180 | } 181 | 182 | func (t *Trie) Hash() []byte { 183 | return t.trie.Hash().Bytes() 184 | } 185 | 186 | func (t *Trie) Prove(key []byte) (*mpf.Proof, error) { 187 | return t.trie.Prove(key) 188 | } 189 | 190 | // HashKey returns a blake2b-256 hash for use in key values 191 | func (t *Trie) HashKey(key []byte) []byte { 192 | tmpHash, err := blake2b.New256(nil) 193 | if err != nil { 194 | // This should never happen 195 | panic(err.Error()) 196 | } 197 | tmpHash.Write(key) 198 | trieKey := tmpHash.Sum(nil) 199 | return trieKey 200 | } 201 | 202 | func (t *Trie) dbKeyPrefix(key []byte) []byte { 203 | return fmt.Appendf(nil, 204 | "trie_%s_%s", 205 | t.keyPrefix, 206 | key, 207 | ) 208 | } 209 | 210 | func HashValue(val []byte) mpf.Hash { 211 | return mpf.HashValue(val) 212 | } 213 | -------------------------------------------------------------------------------- /internal/tx/context.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 tx 16 | 17 | import ( 18 | "fmt" 19 | "log/slog" 20 | 21 | "github.com/Salvionied/apollo/serialization/UTxO" 22 | "github.com/Salvionied/apollo/txBuilding/Backend/Base" 23 | "github.com/Salvionied/apollo/txBuilding/Backend/FixedChainContext" 24 | "github.com/blinklabs-io/bluefin/internal/storage" 25 | "github.com/blinklabs-io/gouroboros/cbor" 26 | ) 27 | 28 | // CustomChainContext allows Apollo to lookup script ref UTxOs from our storage 29 | type CustomChainContext struct { 30 | Base.ChainContext 31 | } 32 | 33 | func NewCustomChainContext() CustomChainContext { 34 | return CustomChainContext{ 35 | ChainContext: FixedChainContext.InitFixedChainContext(), 36 | } 37 | } 38 | 39 | func (c CustomChainContext) GetUtxoFromRef( 40 | txHash string, 41 | txIndex int, 42 | ) (*UTxO.UTxO, error) { 43 | var ret UTxO.UTxO 44 | store := storage.GetStorage() 45 | store.Lock() 46 | utxoBytes, err := store.GetScriptRefUtxo(txHash, txIndex) 47 | if err != nil { 48 | slog.Error( 49 | fmt.Sprintf("failed to get script ref UTxO: %s", err), 50 | ) 51 | slog.Warn( 52 | "NOTE: this probably means that you need to remove your .bluefin directory to re-sync from scratch", 53 | ) 54 | return nil, err 55 | } 56 | store.Unlock() 57 | if _, err := cbor.Decode(utxoBytes, &ret); err != nil { 58 | slog.Error( 59 | fmt.Sprintf("failed to decode script ref UTxO bytes: %s", err), 60 | ) 61 | return nil, err 62 | } 63 | return &ret, nil 64 | } 65 | -------------------------------------------------------------------------------- /internal/tx/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 tx 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "encoding/hex" 21 | "errors" 22 | "fmt" 23 | "io" 24 | "log/slog" 25 | "math/big" 26 | "net" 27 | "net/http" 28 | "os" 29 | "sync" 30 | "time" 31 | 32 | "github.com/Salvionied/apollo" 33 | "github.com/Salvionied/apollo/serialization" 34 | serAddress "github.com/Salvionied/apollo/serialization/Address" 35 | "github.com/Salvionied/apollo/serialization/AssetName" 36 | "github.com/Salvionied/apollo/serialization/Key" 37 | "github.com/Salvionied/apollo/serialization/PlutusData" 38 | "github.com/Salvionied/apollo/serialization/Policy" 39 | "github.com/Salvionied/apollo/serialization/Redeemer" 40 | "github.com/Salvionied/apollo/serialization/UTxO" 41 | "github.com/blinklabs-io/bluefin/internal/config" 42 | "github.com/blinklabs-io/bluefin/internal/storage" 43 | "github.com/blinklabs-io/bluefin/internal/wallet" 44 | models "github.com/blinklabs-io/cardano-models" 45 | ouroboros "github.com/blinklabs-io/gouroboros" 46 | "github.com/blinklabs-io/gouroboros/cbor" 47 | "github.com/blinklabs-io/gouroboros/protocol/txsubmission" 48 | "golang.org/x/crypto/blake2b" 49 | ) 50 | 51 | var ( 52 | ntnTxBytes []byte 53 | ntnTxHash [32]byte 54 | ntnSentTx bool 55 | ntnMutex sync.Mutex 56 | doneChan chan any 57 | ) 58 | 59 | func SendTx(blockData any, nonce [16]byte) error { 60 | txBytes, err := createTx(blockData, nonce) 61 | if err != nil { 62 | return err 63 | } 64 | txId, err := submitTx(txBytes) 65 | if err != nil { 66 | return err 67 | } 68 | slog.Info( 69 | "successfully submitted TX " + txId, 70 | ) 71 | return nil 72 | } 73 | 74 | func createTx(blockData any, nonce [16]byte) ([]byte, error) { 75 | cfg := config.GetConfig() 76 | bursa := wallet.GetWallet() 77 | store := storage.GetStorage() 78 | 79 | profileCfg := config.GetProfile() 80 | 81 | validatorHash := profileCfg.ValidatorHash 82 | validatorHashBytes, err := hex.DecodeString(validatorHash) 83 | if err != nil { 84 | return nil, err 85 | } 86 | mintValidatorHash := profileCfg.MintValidatorHash 87 | 88 | postDatum := PlutusData.PlutusData{ 89 | PlutusDataType: PlutusData.PlutusBytes, 90 | TagNr: 0, 91 | Value: blockData, 92 | } 93 | 94 | contractAddress, _ := serAddress.DecodeAddress(cfg.Indexer.ScriptAddress) 95 | myAddress, _ := serAddress.DecodeAddress(bursa.PaymentAddress) 96 | cc := NewCustomChainContext() 97 | apollob := apollo.New(&cc) 98 | apollob, err = apollob. 99 | SetWalletFromBech32(bursa.PaymentAddress). 100 | SetWalletAsChangeAddress() 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | // Gather input UTxOs from our wallet 106 | store.Lock() 107 | utxosBytes, err := store.GetUtxos(bursa.PaymentAddress) 108 | if err != nil { 109 | store.Unlock() 110 | return nil, err 111 | } 112 | store.Unlock() 113 | utxos := []UTxO.UTxO{} 114 | var tunaPolicyId *Policy.PolicyId 115 | if profileCfg.UseTunaV1 { 116 | var err error 117 | tunaPolicyId, err = Policy.New(validatorHash) 118 | if err != nil { 119 | return nil, err 120 | } 121 | } else { 122 | var err error 123 | tunaPolicyId, err = Policy.New(mintValidatorHash) 124 | if err != nil { 125 | return nil, err 126 | } 127 | } 128 | var tunaCount int64 129 | for _, utxoBytes := range utxosBytes { 130 | utxo := UTxO.UTxO{} 131 | if _, err := cbor.Decode(utxoBytes, &utxo); err != nil { 132 | return nil, err 133 | } 134 | // Record the number of TUNA in inputs to use in outputs 135 | assets := utxo.Output.GetValue().GetAssets() 136 | if assets != nil { 137 | tunaCount += assets.GetByPolicyAndId( 138 | *tunaPolicyId, 139 | AssetName.NewAssetNameFromString("TUNA"), 140 | ) 141 | } 142 | utxos = append(utxos, utxo) 143 | } 144 | 145 | // Gather UTxO(s) for script 146 | store.Lock() 147 | scriptUtxosBytes, err := store.GetUtxos(cfg.Indexer.ScriptAddress) 148 | if err != nil { 149 | store.Unlock() 150 | return nil, err 151 | } 152 | store.Unlock() 153 | scriptUtxos := []UTxO.UTxO{} 154 | for _, utxoBytes := range scriptUtxosBytes { 155 | var utxo UTxO.UTxO 156 | if _, err := cbor.Decode(utxoBytes, &utxo); err != nil { 157 | return nil, err 158 | } 159 | scriptUtxos = append(scriptUtxos, utxo) 160 | } 161 | // There should only ever be 1 UTxO for the script address 162 | if len(scriptUtxos) > 1 { 163 | slog.Warn( 164 | fmt.Sprintf( 165 | "found unexpected UTxO(s) at script address (%s), expected 1 and found %d", 166 | cfg.Indexer.ScriptAddress, 167 | len(scriptUtxos), 168 | ), 169 | ) 170 | } 171 | if len(scriptUtxos) == 0 { 172 | return nil, errors.New("no script UTxOs found") 173 | } 174 | validatorOutRef := scriptUtxos[0] 175 | 176 | var blockDataRealTimeNow int64 177 | var blockDataBlockNumber int64 178 | var blockDataHash []byte 179 | if profileCfg.UseTunaV1 { 180 | tmpBlockData := blockData.(models.TunaV1State) 181 | blockDataRealTimeNow = tmpBlockData.RealTimeNow 182 | } else { 183 | tmpBlockData := blockData.(models.TunaV2State) 184 | blockDataRealTimeNow = tmpBlockData.CurrentPosixTime 185 | blockDataBlockNumber = tmpBlockData.BlockNumber 186 | blockDataHash = tmpBlockData.CurrentHash 187 | } 188 | 189 | // Determine validity start/end slot based on datum 190 | datumSlot := unixTimeToSlot(blockDataRealTimeNow / 1000) 191 | 192 | apollob = apollob.AddLoadedUTxOs(utxos...). 193 | SetValidityStart(int64(datumSlot - 90)). 194 | SetTtl(int64(datumSlot + 90)) // #nosec G115 195 | 196 | if profileCfg.UseTunaV1 { 197 | apollob = apollob. 198 | PayToAddress( 199 | myAddress, 2000000, apollo.NewUnit(validatorHash, "TUNA", int(tunaCount+5000000000)), 200 | ). 201 | PayToContract( 202 | contractAddress, &postDatum, int(validatorOutRef.Output.PostAlonzo.Amount.Am.Coin), true, apollo.NewUnit(validatorHash, "lord tuna", 1), 203 | ). 204 | MintAssetsWithRedeemer( 205 | apollo.NewUnit(validatorHash, "TUNA", 5000000000), 206 | Redeemer.Redeemer{ 207 | Tag: Redeemer.MINT, 208 | Index: 0, 209 | // NOTE: these values are estimated 210 | ExUnits: Redeemer.ExecutionUnits{ 211 | Mem: 80_000, 212 | Steps: 30_000_000, 213 | }, 214 | Data: PlutusData.PlutusData{ 215 | PlutusDataType: PlutusData.PlutusArray, 216 | TagNr: 121, 217 | Value: PlutusData.PlutusIndefArray{}, 218 | }, 219 | }, 220 | ). 221 | CollectFrom( 222 | validatorOutRef, 223 | Redeemer.Redeemer{ 224 | Tag: Redeemer.SPEND, 225 | // NOTE: these values are estimated 226 | ExUnits: Redeemer.ExecutionUnits{ 227 | Mem: 450_000, 228 | Steps: 200_000_000, 229 | }, 230 | Data: PlutusData.PlutusData{ 231 | PlutusDataType: PlutusData.PlutusArray, 232 | TagNr: 122, 233 | Value: PlutusData.PlutusIndefArray{ 234 | PlutusData.PlutusData{ 235 | PlutusDataType: PlutusData.PlutusBytes, 236 | Value: nonce, 237 | }, 238 | }, 239 | }, 240 | }, 241 | ) 242 | } else { 243 | // Build miner credential 244 | userPkh := wallet.PaymentKeyHash() 245 | minerCredential := cbor.NewConstructor( 246 | 0, 247 | cbor.IndefLengthList{ 248 | userPkh, 249 | []byte(cfg.Miner.Message), 250 | }, 251 | ) 252 | // Convert old and new block numbers to byte representation for use in token names 253 | oldBlockNumberBytes := big.NewInt(int64(blockDataBlockNumber - 1)).Bytes() 254 | newBlockNumberBytes := big.NewInt(int64(blockDataBlockNumber)).Bytes() 255 | // Temporarily add new target hash to trie to calculate merkle proof 256 | trie := storage.GetStorage().Trie() 257 | trie.Lock() 258 | tmpHashKey := storage.HashValue(blockDataHash).Bytes() 259 | if err := trie.Update(tmpHashKey, blockDataHash, 0); err != nil { 260 | trie.Unlock() 261 | return nil, err 262 | } 263 | proof, err := trie.Prove(tmpHashKey) 264 | if err != nil { 265 | trie.Unlock() 266 | return nil, err 267 | } 268 | // Remove item from trie until it comes in via the indexer 269 | _ = trie.Delete(tmpHashKey) 270 | trie.Unlock() 271 | minerRedeemer := Redeemer.Redeemer{ 272 | Tag: Redeemer.SPEND, 273 | // NOTE: these values are estimated 274 | ExUnits: Redeemer.ExecutionUnits{ 275 | Mem: 1_500_000, 276 | Steps: 550_000_000, 277 | }, 278 | Data: PlutusData.PlutusData{ 279 | PlutusDataType: PlutusData.PlutusBytes, 280 | TagNr: 0, 281 | Value: cbor.NewConstructor( 282 | 0, 283 | cbor.IndefLengthList{ 284 | nonce, 285 | minerCredential, 286 | proof, 287 | }, 288 | ), 289 | }, 290 | } 291 | mintRedeemer := Redeemer.Redeemer{ 292 | Tag: Redeemer.MINT, 293 | // NOTE: these values are estimated 294 | ExUnits: Redeemer.ExecutionUnits{ 295 | Mem: 280_000, 296 | Steps: 130_000_000, 297 | }, 298 | Data: PlutusData.PlutusData{ 299 | PlutusDataType: PlutusData.PlutusBytes, 300 | TagNr: 0, 301 | Value: cbor.NewConstructor( 302 | 1, 303 | cbor.IndefLengthList{ 304 | cbor.NewConstructor( 305 | 0, 306 | cbor.IndefLengthList{ 307 | cbor.NewConstructor( 308 | 0, 309 | cbor.IndefLengthList{ 310 | validatorOutRef.Input.TransactionId, 311 | }, 312 | ), 313 | validatorOutRef.Input.Index, 314 | }, 315 | ), 316 | blockDataBlockNumber - 1, 317 | }, 318 | ), 319 | }, 320 | } 321 | apollob = apollob. 322 | PayToAddress( 323 | myAddress, 2000000, apollo.NewUnit(mintValidatorHash, "TUNA", int(tunaCount+5000000000)), 324 | ). 325 | PayToContract( 326 | contractAddress, 327 | &postDatum, 328 | int(validatorOutRef.Output.PostAlonzo.Amount.Am.Coin), 329 | true, 330 | apollo.NewUnit(mintValidatorHash, "TUNA"+string(validatorHashBytes), 1), 331 | apollo.NewUnit(mintValidatorHash, "COUNTER"+string(newBlockNumberBytes), 1), 332 | ). 333 | CollectFrom( 334 | validatorOutRef, 335 | minerRedeemer, 336 | ). 337 | MintAssetsWithRedeemer( 338 | apollo.NewUnit(mintValidatorHash, "TUNA", 5000000000), 339 | mintRedeemer, 340 | ). 341 | MintAssetsWithRedeemer( 342 | apollo.NewUnit(mintValidatorHash, "COUNTER"+string(newBlockNumberBytes), 1), 343 | mintRedeemer, 344 | ). 345 | MintAssetsWithRedeemer( 346 | apollo.NewUnit(mintValidatorHash, "COUNTER"+string(oldBlockNumberBytes), -1), 347 | mintRedeemer, 348 | ). 349 | AddRequiredSigner( 350 | serialization.PubKeyHash(userPkh), 351 | ) 352 | } 353 | if len(profileCfg.ScriptRefInputs) > 0 { 354 | // Use script reference input(s) 355 | for _, refInput := range profileCfg.ScriptRefInputs { 356 | apollob = apollob.AddReferenceInput( 357 | refInput.TxId, 358 | int(refInput.OutputIdx), 359 | ) 360 | } 361 | } else { 362 | // Include the script with the TX 363 | validatorScriptBytes, err := hex.DecodeString(profileCfg.ValidatorScript) 364 | if err != nil { 365 | return nil, err 366 | } 367 | apollob = apollob.AttachV2Script(PlutusData.PlutusV2Script(validatorScriptBytes)) 368 | } 369 | 370 | // Disable ExUnits estimation, since it doesn't work with the backend we use 371 | apollob = apollob.DisableExecutionUnitsEstimation() 372 | 373 | tx, err := apollob.Complete() 374 | if err != nil { 375 | return nil, err 376 | } 377 | 378 | vKeyBytes, err := hex.DecodeString(bursa.PaymentVKey.CborHex) 379 | if err != nil { 380 | return nil, err 381 | } 382 | sKeyBytes, err := hex.DecodeString(bursa.PaymentSKey.CborHex) 383 | if err != nil { 384 | return nil, err 385 | } 386 | // Strip off leading 2 bytes as shortcut for CBOR decoding to unwrap bytes 387 | vKeyBytes = vKeyBytes[2:] 388 | sKeyBytes = sKeyBytes[2:] 389 | vkey := Key.VerificationKey{Payload: vKeyBytes} 390 | skey := Key.SigningKey{Payload: sKeyBytes} 391 | tx, err = tx.SignWithSkey(vkey, skey) 392 | if err != nil { 393 | return nil, err 394 | } 395 | txBytes, err := tx.GetTx().Bytes() 396 | if err != nil { 397 | return nil, err 398 | } 399 | slog.Debug( 400 | fmt.Sprintf("TX bytes: %x", txBytes), 401 | ) 402 | return txBytes, nil 403 | } 404 | 405 | func unixTimeToSlot(unixTime int64) uint64 { 406 | cfg := config.GetConfig() 407 | networkCfg := config.Networks[cfg.Network] 408 | if unixTime < 0 { 409 | panic("you have traveled backward in time") 410 | } 411 | // #nosec G115 412 | return networkCfg.ShelleyOffsetSlot + uint64( 413 | unixTime-networkCfg.ShelleyOffsetTime, 414 | ) 415 | } 416 | 417 | func submitTx(txRawBytes []byte) (string, error) { 418 | cfg := config.GetConfig() 419 | if cfg.Submit.Address != "" { 420 | return submitTxNtN(txRawBytes) 421 | } else if cfg.Submit.SocketPath != "" { 422 | return submitTxNtC(txRawBytes) 423 | } else if cfg.Submit.Url != "" { 424 | return submitTxApi(txRawBytes) 425 | } else { 426 | // Populate address info from indexer network 427 | network, ok := ouroboros.NetworkByName(cfg.Network) 428 | if !ok { 429 | slog.Error( 430 | "unknown network: " + cfg.Network, 431 | ) 432 | os.Exit(1) 433 | } 434 | if len(network.BootstrapPeers) == 0 { 435 | slog.Error( 436 | "no known peers for network: " + cfg.Network, 437 | ) 438 | os.Exit(1) 439 | } 440 | peer := network.BootstrapPeers[0] 441 | cfg.Submit.Address = fmt.Sprintf("%s:%d", peer.Address, peer.Port) 442 | return submitTxNtN(txRawBytes) 443 | } 444 | } 445 | 446 | func submitTxNtN(txRawBytes []byte) (string, error) { 447 | ntnMutex.Lock() 448 | defer ntnMutex.Unlock() 449 | cfg := config.GetConfig() 450 | 451 | // Record TX bytes in global for use in handler functions 452 | ntnTxBytes = txRawBytes[:] 453 | ntnSentTx = false 454 | 455 | // Generate TX hash 456 | // Unwrap raw transaction bytes into a CBOR array 457 | txUnwrap := []cbor.RawMessage{} 458 | if _, err := cbor.Decode(txRawBytes, &txUnwrap); err != nil { 459 | slog.Error( 460 | fmt.Sprintf("failed to unwrap transaction CBOR: %s", err), 461 | ) 462 | return "", fmt.Errorf("failed to unwrap transaction CBOR: %w", err) 463 | } 464 | // index 0 is the transaction body 465 | // Store index 0 (transaction body) as byte array 466 | txBody := txUnwrap[0] 467 | // Convert the body into a blake2b256 hash string 468 | ntnTxHash = blake2b.Sum256(txBody) 469 | 470 | // Create connection 471 | conn, err := createClientConnection(cfg.Submit.Address) 472 | if err != nil { 473 | return "", err 474 | } 475 | o, err := ouroboros.New( 476 | ouroboros.WithConnection(conn), 477 | ouroboros.WithNetworkMagic(cfg.NetworkMagic), 478 | ouroboros.WithNodeToNode(true), 479 | ouroboros.WithKeepAlive(true), 480 | ouroboros.WithTxSubmissionConfig( 481 | txsubmission.NewConfig( 482 | txsubmission.WithRequestTxIdsFunc(handleRequestTxIds), 483 | txsubmission.WithRequestTxsFunc(handleRequestTxs), 484 | ), 485 | ), 486 | ) 487 | if err != nil { 488 | return "", err 489 | } 490 | // Capture errors 491 | doneChan = make(chan any) 492 | go func() { 493 | err, ok := <-o.ErrorChan() 494 | if ok { 495 | select { 496 | case <-doneChan: 497 | return 498 | default: 499 | } 500 | close(doneChan) 501 | slog.Error( 502 | fmt.Sprintf("async error submitting TX via NtN: %s", err), 503 | ) 504 | } 505 | }() 506 | // Start txSubmission loop 507 | o.TxSubmission().Client.Init() 508 | <-doneChan 509 | // Sleep 1s to allow time for TX to enter remote mempool before closing connection 510 | time.Sleep(1 * time.Second) 511 | 512 | if err := o.Close(); err != nil { 513 | return "", fmt.Errorf("failed to close connection: %w", err) 514 | } 515 | 516 | return hex.EncodeToString(ntnTxHash[:]), nil 517 | } 518 | 519 | func submitTxNtC(txRawBytes []byte) (string, error) { 520 | // TODO 521 | return "", nil 522 | } 523 | 524 | func submitTxApi(txRawBytes []byte) (string, error) { 525 | cfg := config.GetConfig() 526 | reqBody := bytes.NewBuffer(txRawBytes) 527 | ctx := context.Background() 528 | req, err := http.NewRequestWithContext( 529 | ctx, 530 | http.MethodPost, 531 | cfg.Submit.Url, 532 | reqBody, 533 | ) 534 | if err != nil { 535 | return "", fmt.Errorf("failed to create request: %w", err) 536 | } 537 | req.Header.Add("Content-Type", "application/cbor") 538 | if cfg.Submit.BlockFrostProjectID != "" { 539 | req.Header.Add("project_id", cfg.Submit.BlockFrostProjectID) 540 | } 541 | client := &http.Client{Timeout: 5 * time.Minute} 542 | resp, err := client.Do(req) 543 | if err != nil { 544 | return "", fmt.Errorf( 545 | "failed to send request: %s: %w", 546 | cfg.Submit.Url, 547 | err, 548 | ) 549 | } 550 | if resp == nil { 551 | return "", fmt.Errorf( 552 | "failed parsing empty response from: %s", 553 | cfg.Submit.Url, 554 | ) 555 | } 556 | // We have to read the entire response body and close it to prevent a memory leak 557 | respBody, err := io.ReadAll(resp.Body) 558 | if err != nil { 559 | return "", fmt.Errorf("failed to read response body: %w", err) 560 | } 561 | defer resp.Body.Close() 562 | 563 | if resp.StatusCode == http.StatusAccepted { 564 | return string(respBody), nil 565 | } else { 566 | return "", fmt.Errorf("failed to submit TX to API: %s: %d: %s", cfg.Submit.Url, resp.StatusCode, respBody) 567 | } 568 | } 569 | 570 | func createClientConnection(nodeAddress string) (net.Conn, error) { 571 | var err error 572 | var conn net.Conn 573 | var dialProto string 574 | var dialAddress string 575 | dialProto = "tcp" 576 | dialAddress = nodeAddress 577 | 578 | conn, err = net.Dial(dialProto, dialAddress) 579 | if err != nil { 580 | return nil, fmt.Errorf("connection failed: %w", err) 581 | } 582 | return conn, nil 583 | } 584 | 585 | func handleRequestTxIds( 586 | ctx txsubmission.CallbackContext, 587 | blocking bool, 588 | ack uint16, 589 | req uint16, 590 | ) ([]txsubmission.TxIdAndSize, error) { 591 | // Shutdown if we've already sent the TX 592 | if ntnSentTx { 593 | select { 594 | case <-doneChan: 595 | return nil, nil 596 | default: 597 | } 598 | close(doneChan) 599 | // This prevents creating an async error while waiting for shutdown 600 | time.Sleep(2 * time.Second) 601 | return nil, nil 602 | } 603 | ret := []txsubmission.TxIdAndSize{ 604 | { 605 | TxId: txsubmission.TxId{ 606 | EraId: 5, 607 | TxId: ntnTxHash, 608 | }, 609 | Size: uint32(len(ntnTxBytes)), // #nosec G115 610 | }, 611 | } 612 | return ret, nil 613 | } 614 | 615 | func handleRequestTxs( 616 | ctx txsubmission.CallbackContext, 617 | txIds []txsubmission.TxId, 618 | ) ([]txsubmission.TxBody, error) { 619 | ret := []txsubmission.TxBody{ 620 | { 621 | EraId: 5, 622 | TxBody: ntnTxBytes, 623 | }, 624 | } 625 | ntnSentTx = true 626 | return ret, nil 627 | } 628 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /internal/wallet/wallet.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 wallet 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "log/slog" 21 | "os" 22 | "path" 23 | 24 | "github.com/blinklabs-io/bluefin/internal/config" 25 | "github.com/blinklabs-io/bursa" 26 | "golang.org/x/crypto/blake2b" 27 | ) 28 | 29 | var globalWallet = &bursa.Wallet{} 30 | 31 | func Setup() { 32 | // Setup wallet 33 | cfg := config.GetConfig() 34 | // TODO: check storage for mnemonic 35 | mnemonic := cfg.Wallet.Mnemonic 36 | if mnemonic == "" { 37 | pwd, err := os.Getwd() 38 | if err != nil { 39 | panic(err.Error()) 40 | } 41 | seedPath := path.Join( 42 | pwd, 43 | "seed.txt", 44 | ) 45 | // Read seed.txt if it exists 46 | if data, err := os.ReadFile(seedPath); err == nil { 47 | slog.Info( 48 | "read mnemonic from " + seedPath, 49 | ) 50 | mnemonic = string(data) 51 | } else if errors.Is(err, os.ErrNotExist) { 52 | mnemonic, err = bursa.NewMnemonic() 53 | if err != nil { 54 | panic(err) 55 | } 56 | // Write seed.txt 57 | // WARNING: this will clobber existing files 58 | f, err := os.Create(seedPath) 59 | if err != nil { 60 | panic(err) 61 | } 62 | l, err := f.WriteString(mnemonic) 63 | slog.Debug( 64 | fmt.Sprintf("wrote %d bytes to seed.txt", l), 65 | ) 66 | if err != nil { 67 | f.Close() 68 | panic(err) 69 | } 70 | err = f.Close() 71 | if err != nil { 72 | panic(err) 73 | } 74 | slog.Info( 75 | "wrote generated mnemonic to " + seedPath, 76 | ) 77 | // TODO: write mnemonic to storage 78 | } else { 79 | panic(err) 80 | } 81 | } 82 | wallet, err := bursa.NewWallet( 83 | mnemonic, 84 | cfg.Network, 85 | "", 0, 0, 0, 0, 86 | ) 87 | if err != nil { 88 | panic(err) 89 | } 90 | globalWallet = wallet 91 | } 92 | 93 | func GetWallet() *bursa.Wallet { 94 | return globalWallet 95 | } 96 | 97 | func PaymentKeyHash() []byte { 98 | rootKey, err := bursa.GetRootKeyFromMnemonic(globalWallet.Mnemonic, "") 99 | if err != nil { 100 | panic(err) 101 | } 102 | userPkh := bursa.GetPaymentKey(bursa.GetAccountKey(rootKey, 0), 0). 103 | Public(). 104 | PublicKey() 105 | tmpHasher, err := blake2b.New(28, nil) 106 | if err != nil { 107 | panic(err) 108 | } 109 | tmpHasher.Write(userPkh) 110 | hash := tmpHasher.Sum(nil) 111 | return hash 112 | } 113 | --------------------------------------------------------------------------------