├── .github ├── CODE_OF_CONDUCT.md ├── CODE_STANDARDS.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── IMAGES │ └── github-share-image.png ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── PULL_REQUEST_TEMPLATE │ └── general.md ├── SECURITY.md ├── dependabot.yml ├── labels.yml ├── mergify.yml └── workflows │ ├── codeql-analysis.yml │ ├── release.yml │ ├── run-tests.yml │ └── sync-labels.yml ├── .gitignore ├── .gitpod.yml ├── .golangci.yml ├── .goreleaser.yml ├── .make ├── common.mk └── go.mk ├── LICENSE ├── Makefile ├── README.md ├── address.go ├── address_test.go ├── bitcoin.go ├── codecov.yml ├── encryption.go ├── encryption_test.go ├── errors.go ├── examples ├── address_from_private_key │ └── address_from_private_key.go ├── address_from_wif │ └── address_from_wif.go ├── calculate_fee_for_tx │ └── calculate_fee_for_tx.go ├── create_pubkey │ └── create_pubkey.go ├── create_tx │ └── create_tx.go ├── create_tx_using_wif │ └── create_tx_using_wif.go ├── create_tx_with_change │ └── create_tx_with_change.go ├── create_wif │ └── create_wif.go ├── decrypt_with_private_key │ └── decrypt_with_private_key.go ├── encrypt_shared_keys │ └── encrypt_shared_keys.go ├── encrypt_with_private_key │ └── encrypt_with_private_key.go ├── generate_hd_key │ └── generate_hd_key.go ├── get_address_from_hd_key │ └── get_address_from_hd_key.go ├── get_addresses_for_path │ └── get_addresses_for_path.go ├── get_extended_public_key │ └── get_extended_public_key.go ├── get_hd_key_from_xpub │ └── get_hd_key_from_xpub.go ├── get_private_key_for_path │ └── get_private_key_for_path.go ├── get_public_keys_for_path │ └── get_public_keys_for_path.go ├── private_key_to_wif │ └── private_key_to_wif.go ├── script_from_address │ └── script_from_address.go ├── sign_message │ └── sign_message.go ├── tx_from_hex │ └── tx_from_hex.go ├── verify_signature │ └── verify_signature.go ├── verify_signature_der │ └── verify_signature_der.go ├── wif_from_string │ └── wif_from_string.go └── wif_to_private_key │ └── wif_to_private_key.go ├── go.mod ├── go.sum ├── hd_key.go ├── hd_key_test.go ├── private_key.go ├── private_key_test.go ├── pubkey.go ├── pubkey_test.go ├── script.go ├── script_test.go ├── sign.go ├── sign_test.go ├── transaction.go ├── transaction_test.go ├── verify.go └── verify_test.go /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Merit 2 | 3 | 1. The project creators, lead developers, core team, constitute 4 | the managing members of the project and have final say in every decision 5 | of the project, technical or otherwise, including overruling previous decisions. 6 | There are no limitations to this decisional power. 7 | 8 | 2. Contributions are an expected result of your membership on the project. 9 | Don't expect others to do your work or help you with your work forever. 10 | 11 | 3. All members have the same opportunities to seek any challenge they want 12 | within the project. 13 | 14 | 4. Authority or position in the project will be proportional 15 | to the accrued contribution. Seniority must be earned. 16 | 17 | 5. Software is evolutive: the better implementations must supersede lesser 18 | implementations. Technical advantage is the primary evaluation metric. 19 | 20 | 6. This is a space for technical prowess; topics outside of the project 21 | will not be tolerated. 22 | 23 | 7. Non technical conflicts will be discussed in a separate space. Disruption 24 | of the project will not be allowed. 25 | 26 | 8. Individual characteristics, including but not limited to, 27 | body, sex, sexual preference, race, language, religion, nationality, 28 | or political preferences are irrelevant in the scope of the project and 29 | will not be taken into account concerning your value or that of your contribution 30 | to the project. 31 | 32 | 9. Discuss or debate the idea, not the person. 33 | 34 | 10. There is no room for ambiguity: Ambiguity will be met with questioning; 35 | further ambiguity will be met with silence. It is the responsibility 36 | of the originator to provide requested context. 37 | 38 | 11. If something is illegal outside the scope of the project, it is illegal 39 | in the scope of the project. This Code of Merit does not take precedence over 40 | governing law. 41 | 42 | 12. This Code of Merit governs the technical procedures of the project not the 43 | activities outside of it. 44 | 45 | 13. Participation on the project equates to agreement of this Code of Merit. 46 | 47 | 14. No objectives beyond the stated objectives of this project are relevant 48 | to the project. Any intent to deviate the project from its original purpose 49 | of existence will constitute grounds for remedial action which may include 50 | expulsion from the project. 51 | 52 | This document is the Code of Merit (`http://code-of-merit.org`), version 1.0. -------------------------------------------------------------------------------- /.github/CODE_STANDARDS.md: -------------------------------------------------------------------------------- 1 | # Code Standards 2 | 3 | This project uses the following code standards and specifications from: 4 | - [effective go](https://golang.org/doc/effective_go.html) 5 | - [go benchmarks](https://golang.org/pkg/testing/#hdr-Benchmarks) 6 | - [go examples](https://golang.org/pkg/testing/#hdr-Examples) 7 | - [go tests](https://golang.org/pkg/testing/) 8 | - [godoc](https://godoc.org/golang.org/x/tools/cmd/godoc) 9 | - [gofmt](https://golang.org/cmd/gofmt/) 10 | - [golangci-lint](https://golangci-lint.run/) 11 | - [report card](https://goreportcard.com/) 12 | 13 | ### *effective go* standards 14 | View the [effective go](https://golang.org/doc/effective_go.html) standards documentation. 15 | 16 | ### *golangci-lint* specifications 17 | The package [golangci-lint](https://golangci-lint.run/usage/quick-start) runs several linters in one package/cmd. 18 | 19 | View the active linters in the [configuration file](../.golangci.yml). 20 | 21 | Install via macOS: 22 | ```shell 23 | brew install golangci-lint 24 | ``` 25 | 26 | Install via Linux and Windows: 27 | ```shell 28 | # binary will be $(go env GOPATH)/bin/golangci-lint 29 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.31.0 30 | golangci-lint --version 31 | ``` 32 | 33 | ### *godoc* specifications 34 | All code is written with documentation in mind. Follow the best practices with naming, examples and function descriptions. -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Please send a GitHub Pull Request to with a clear list of what you've done (read more about [pull requests](http://help.github.com/pull-requests/)). The more tests the merrier. Please follow the coding conventions (below) and make sure all of your commits are atomic (one feature per commit). 4 | 5 | ## Testing 6 | 7 | All tests follow the standard Go testing pattern. 8 | - [Go Tests](https://golang.org/pkg/testing/) 9 | - [Go Examples](https://golang.org/pkg/testing/#hdr-Examples) 10 | - [Go Benchmarks](https://golang.org/pkg/testing/#hdr-Benchmarks) 11 | 12 | ## Coding conventions 13 | 14 | This project follows [effective Go standards](https://golang.org/doc/effective_go.html) and uses additional convention tools: 15 | - [godoc](https://godoc.org/golang.org/x/tools/cmd/godoc) 16 | - [golangci-lint](https://golangci-lint.run/) 17 | - [GoReportCard.com](https://goreportcard.com/) -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: BitcoinSchema 4 | custom: https://gobitcoinsv.com/#sponsor?utm_source=github&utm_medium=sponsor-link&utm_campaign=go-bitcoin&utm_term=go-bitcoin&utm_content=go-bitcoin -------------------------------------------------------------------------------- /.github/IMAGES/github-share-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitcoinSchema/go-bitcoin/98962b405ddc8f8257099c5fff3c6341068e90d4/.github/IMAGES/github-share-image.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve this project 4 | labels: bug-p3 5 | assignees: mrz1836 6 | 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Additional context** 23 | Add any other context about the problem here. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | labels: idea 5 | assignees: mrz1836 6 | 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: 'General template for a question ' 4 | labels: question 5 | assignees: mrz1836 6 | 7 | --- 8 | 9 | **What's your question?** 10 | A clear and concise question using references to specific regions of code if applicable. 11 | 12 | **Additional context** 13 | Add any other context or information. 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/general.md: -------------------------------------------------------------------------------- 1 | Fixes # 2 | 3 | ## Proposed Changes 4 | 5 | - 6 | - 7 | - 8 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported & Maintained Versions 4 | 5 | | Version | Supported | 6 | |---------|--------------------| 7 | | 0.x.x | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Individuals or organizations that are experiencing a product security issue are strongly encouraged to contact the [project maintainers](mailto:security@bitcoinschema.org). 12 | We welcome reports from independent researchers, industry organizations, vendors, customers, and other sources concerned with our project security. 13 | The minimal data needed for reporting a security issue is a description of the potential vulnerability. -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Basic dependabot.yml to update gomod, GitHub Actions and Docker 2 | version: 2 3 | updates: 4 | # Maintain dependencies for the core library 5 | - package-ecosystem: "gomod" 6 | target-branch: "master" 7 | directory: "/" 8 | schedule: 9 | interval: "daily" 10 | time: "10:00" 11 | timezone: "UTC" 12 | reviewers: 13 | - "mrz1836" 14 | assignees: 15 | - "mrz1836" 16 | labels: 17 | - "chore" 18 | open-pull-requests-limit: 10 19 | 20 | # Maintain dependencies for GitHub Actions 21 | - package-ecosystem: "github-actions" 22 | target-branch: "master" 23 | directory: "/" 24 | schedule: 25 | interval: "weekly" 26 | day: "monday" 27 | reviewers: 28 | - "mrz1836" 29 | assignees: 30 | - "mrz1836" 31 | labels: 32 | - "chore" 33 | open-pull-requests-limit: 10 34 | 35 | # Maintain dependencies for the core library (deprecated V1) 36 | - package-ecosystem: "gomod" 37 | target-branch: "v1" 38 | directory: "/" 39 | schedule: 40 | interval: "weekly" 41 | time: "10:00" 42 | timezone: "UTC" 43 | reviewers: 44 | - "mrz1836" 45 | assignees: 46 | - "mrz1836" 47 | labels: 48 | - "chore" 49 | open-pull-requests-limit: 10 50 | 51 | # Maintain dependencies for GitHub Actions (deprecated V1) 52 | - package-ecosystem: "github-actions" 53 | target-branch: "v1" 54 | directory: "/" 55 | schedule: 56 | interval: "weekly" 57 | day: "monday" 58 | reviewers: 59 | - "mrz1836" 60 | assignees: 61 | - "mrz1836" 62 | labels: 63 | - "chore" 64 | open-pull-requests-limit: 10 65 | 66 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | - color: 0075ca 2 | description: "Improvements or additions to documentation" 3 | name: "documentation" 4 | - color: b23128 5 | description: "Highest rated bug or issue, affects all" 6 | name: "bug-P1" 7 | - color: de3d32 8 | description: "Medium rated bug, affects a few" 9 | name: "bug-P2" 10 | - color: f44336 11 | description: "Lowest rated bug, affects nearly none or low-impact" 12 | name: "bug-P3" 13 | - color: 0e8a16 14 | description: "Any new significant addition" 15 | name: "feature" 16 | - color: b60205 17 | description: "Urgent or important fix/patch" 18 | name: "hot-fix" 19 | - color: cccccc 20 | description: "Any idea, suggestion" 21 | name: "idea" 22 | - color: d4c5f9 23 | description: "Experimental - can break!" 24 | name: "prototype" 25 | - color: cc317c 26 | description: "Any question or concern" 27 | name: "question" 28 | - color: c2e0c6 29 | description: "Unit tests, mocking, integration testing" 30 | name: "test" 31 | - color: fbca04 32 | description: "Anything GUI related" 33 | name: "ui-ux" 34 | - color: 006b75 35 | description: "Simple dependency updates or version bumps" 36 | name: "chore" 37 | - color: 006b75 38 | description: "General updates" 39 | name: "update" 40 | - color: FFA500 41 | description: "Any significant refactoring" 42 | name: "refactor" 43 | - color: FEF2C0 44 | description: "Used for automatic merging" 45 | name: "automerge" 46 | - color: FBCA04 47 | description: "Used for denoting a WIP, stops auto-merge" 48 | name: "work-in-progress" 49 | - color: c2e0c6 50 | description: "Old, unused, stale" 51 | name: "stale" -------------------------------------------------------------------------------- /.github/mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | 3 | # =============================================================================== 4 | # DEPENDABOT 5 | # =============================================================================== 6 | 7 | - name: Automatic Merge for Dependabot Minor Version Pull Requests 8 | conditions: 9 | - -draft 10 | - author~=^dependabot(|-preview)\[bot\]$ 11 | - check-success='test (1.18.x, ubuntu-latest)' 12 | - check-success='Analyze (go)' 13 | - title~=^Bump [^\s]+ from ([\d]+)\..+ to \1\. 14 | actions: 15 | review: 16 | type: APPROVE 17 | message: Automatically approving dependabot pull request 18 | merge: 19 | method: merge 20 | - name: Alert on major version detection 21 | conditions: 22 | - author~=^dependabot(|-preview)\[bot\]$ 23 | - check-success='test (1.18.x, ubuntu-latest)' 24 | - check-success='Analyze (go)' 25 | - -title~=^Bump [^\s]+ from ([\d]+)\..+ to \1\. 26 | actions: 27 | comment: 28 | message: "⚠️ @mrz1836: this is a major version bump and requires your attention" 29 | 30 | # =============================================================================== 31 | # AUTOMATIC MERGE (APPROVALS) 32 | # =============================================================================== 33 | 34 | - name: Automatic Merge ⬇️ on Approval ✔ 35 | conditions: 36 | - "#approved-reviews-by>=1" 37 | - "#review-requested=0" 38 | - "#changes-requested-reviews-by=0" 39 | - check-success='test (1.18.x, ubuntu-latest)' 40 | - check-success='Analyze (go)' 41 | - -title~=(?i)wip 42 | - label!=work-in-progress 43 | - -draft 44 | actions: 45 | merge: 46 | method: merge 47 | 48 | # =============================================================================== 49 | # AUTHOR 50 | # =============================================================================== 51 | 52 | - name: Auto-Assign Author 53 | conditions: 54 | - "#assignee=0" 55 | actions: 56 | assign: 57 | users: [ "mrz1836" ] 58 | 59 | # =============================================================================== 60 | # ALERTS 61 | # =============================================================================== 62 | 63 | - name: Notify on merge 64 | conditions: 65 | - merged 66 | - label=automerge 67 | actions: 68 | comment: 69 | message: "✅ @{{author}}: **{{title}}** has been merged successfully." 70 | - name: Alert on merge conflict 71 | conditions: 72 | - conflict 73 | - label=automerge 74 | actions: 75 | comment: 76 | message: "🆘 @{{author}}: `{{head}}` has conflicts with `{{base}}` that must be resolved." 77 | - name: Alert on tests failure for automerge 78 | conditions: 79 | - label=automerge 80 | - status-failure=commit 81 | actions: 82 | comment: 83 | message: "🆘 @{{author}}: unable to merge due to CI failure." 84 | 85 | # =============================================================================== 86 | # LABELS 87 | # =============================================================================== 88 | # Automatically add labels when PRs match certain patterns 89 | # 90 | # NOTE: 91 | # - single quotes for regex to avoid accidental escapes 92 | # - Mergify leverages Python regular expressions to match rules. 93 | # 94 | # Semantic commit messages 95 | # - chore: updating grunt tasks etc.; no production code change 96 | # - docs: changes to the documentation 97 | # - feat: feature or story 98 | # - feature: new feature or story 99 | # - fix: bug fix for the user, not a fix to a build script 100 | # - idea: general idea or suggestion 101 | # - question: question regarding code 102 | # - test: test related changes 103 | # - wip: work in progress PR 104 | # =============================================================================== 105 | 106 | - name: Work in Progress 107 | conditions: 108 | - "head~=(?i)^wip" # if the PR branch starts with wip/ 109 | actions: 110 | label: 111 | add: ["work-in-progress"] 112 | - name: Hotfix label 113 | conditions: 114 | - "head~=(?i)^hotfix" # if the PR branch starts with hotfix/ 115 | actions: 116 | label: 117 | add: ["hot-fix"] 118 | - name: Bug / Fix label 119 | conditions: 120 | - "head~=(?i)^(bug)?fix" # if the PR branch starts with (bug)?fix/ 121 | actions: 122 | label: 123 | add: ["bug-P3"] 124 | - name: Documentation label 125 | conditions: 126 | - "head~=(?i)^docs" # if the PR branch starts with docs/ 127 | actions: 128 | label: 129 | add: ["documentation"] 130 | - name: Feature label 131 | conditions: 132 | - "head~=(?i)^feat(ure)?" # if the PR branch starts with feat(ure)?/ 133 | actions: 134 | label: 135 | add: ["feature"] 136 | - name: Chore label 137 | conditions: 138 | - "head~=(?i)^chore" # if the PR branch starts with chore/ 139 | actions: 140 | label: 141 | add: ["update"] 142 | - name: Question label 143 | conditions: 144 | - "head~=(?i)^question" # if the PR branch starts with question/ 145 | actions: 146 | label: 147 | add: ["question"] 148 | - name: Test label 149 | conditions: 150 | - "head~=(?i)^test" # if the PR branch starts with test/ 151 | actions: 152 | label: 153 | add: ["test"] 154 | - name: Idea label 155 | conditions: 156 | - "head~=(?i)^idea" # if the PR branch starts with idea/ 157 | actions: 158 | label: 159 | add: ["idea"] 160 | 161 | # =============================================================================== 162 | # CONTRIBUTORS 163 | # =============================================================================== 164 | 165 | - name: Welcome New Contributors 166 | conditions: 167 | - and: 168 | - author!=dependabot[bot] 169 | - author!=mergify[bot] 170 | - author!=mrz1836 171 | - author!=rohenaz 172 | - author!=galt-tr 173 | - author!=icellan 174 | actions: 175 | comment: 176 | message: Welcome to our open-source project! 💘 177 | 178 | # =============================================================================== 179 | # STALE BRANCHES 180 | # =============================================================================== 181 | 182 | - name: Close stale pull request 183 | conditions: 184 | - base=master 185 | - -closed 186 | - updated-at<21 days ago 187 | actions: 188 | close: 189 | message: | 190 | This pull request looks stale. Feel free to reopen it if you think it's a mistake. 191 | label: 192 | add: [ "stale" ] 193 | 194 | # =============================================================================== 195 | # BRANCHES 196 | # =============================================================================== 197 | 198 | - name: Delete head branch after merge 199 | conditions: 200 | - merged 201 | actions: 202 | delete_head_branch: 203 | 204 | # =============================================================================== 205 | # CONVENTION 206 | # =============================================================================== 207 | # https://www.conventionalcommits.org/en/v1.0.0/ 208 | # Premium feature only 209 | 210 | #- name: Conventional Commit 211 | # conditions: 212 | # - "title~=^(fix|feat|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\\(.+\\))?:" 213 | # actions: 214 | # post_check: 215 | # title: | 216 | # {% if check_succeed %} 217 | # Title follows Conventional Commit 218 | # {% else %} 219 | # Title does not follow Conventional Commit 220 | # {% endif %} 221 | # summary: | 222 | # {% if not check_succeed %} 223 | # Your pull request title must follow [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/). 224 | # {% endif %} -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | # schedule: 15 | # - cron: '0 23 * * 0' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['go'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can check out the head. 37 | fetch-depth: 2 38 | 39 | # If this run was triggered by a pull request event, then checkout 40 | # the head of the pull request instead of the merge commit. 41 | - run: git checkout HEAD^2 42 | if: ${{ github.event_name == 'pull_request' }} 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v3 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v3 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | # - run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v3 72 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # From: https://goreleaser.com/ci/actions/#usage 2 | name: release 3 | 4 | env: 5 | GO111MODULE: on 6 | 7 | on: 8 | push: 9 | tags: 10 | - '*' 11 | 12 | permissions: 13 | contents: write 14 | 15 | jobs: 16 | goreleaser: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | - name: Set up Go 24 | uses: actions/setup-go@v5 25 | with: 26 | go-version: 1.19 27 | - name: Run GoReleaser 28 | uses: goreleaser/goreleaser-action@v6.3.0 29 | with: 30 | distribution: goreleaser 31 | version: latest 32 | args: release --rm-dist --debug 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} 36 | - name: Syndicate to GoDocs 37 | run: make godocs -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-go-tests 2 | 3 | env: 4 | GO111MODULE: on 5 | 6 | on: 7 | pull_request: 8 | branches: 9 | - "*" 10 | push: 11 | branches: 12 | - "*" 13 | 14 | jobs: 15 | test: 16 | strategy: 17 | matrix: 18 | go-version: [ 1.18.x ] 19 | os: [ ubuntu-latest ] 20 | runs-on: ${{ matrix.os }} 21 | steps: 22 | - name: Install Go 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version: ${{ matrix.go-version }} 26 | - name: Checkout code 27 | uses: actions/checkout@v4 28 | - uses: actions/cache@v4 29 | with: 30 | path: | 31 | ~/go/pkg/mod # Module download cache 32 | ~/.cache/go-build # Build cache (Linux) 33 | ~/Library/Caches/go-build # Build cache (Mac) 34 | '%LocalAppData%\go-build' # Build cache (Windows) 35 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 36 | restore-keys: | 37 | ${{ runner.os }}-go- 38 | - name: Run linter and tests 39 | run: make test-ci 40 | - name: Update code coverage 41 | uses: codecov/codecov-action@v5.4.3 42 | with: 43 | token: ${{ secrets.CODECOV_TOKEN }} 44 | flags: unittests 45 | fail_ci_if_error: true # optional (default = false) 46 | verbose: true # optional (default = false) -------------------------------------------------------------------------------- /.github/workflows/sync-labels.yml: -------------------------------------------------------------------------------- 1 | # Workflow: https://github.com/micnncim/action-label-syncer 2 | # Export your labels: https://github.com/micnncim/label-exporter 3 | name: sync-labels 4 | on: 5 | push: 6 | branches: 7 | - master 8 | paths: 9 | - .github/labels.yml 10 | jobs: 11 | sync-labels: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: micnncim/action-label-syncer@v1.3.0 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | with: 19 | manifest: .github/labels.yml 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # OS files 15 | *.db 16 | *.DS_Store 17 | 18 | # Jetbrains 19 | .idea/ 20 | 21 | # Eclipse 22 | .project 23 | 24 | # Notes 25 | todo.md 26 | paymail-notes.md 27 | 28 | # Releases 29 | *.tar.gz 30 | 31 | # Generated binaries 32 | dist 33 | 34 | # Converage 35 | coverage.txt -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: go get && go build ./... 3 | command: go run 4 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # This file contains all available configuration options 2 | # with their default values. 3 | 4 | # options for analysis running 5 | run: 6 | # default concurrency is an available CPU number 7 | concurrency: 4 8 | 9 | # timeout for analysis, e.g. 30s, 5m, default is 1m 10 | timeout: 2m 11 | 12 | # exit code when at least one issue was found, default is 1 13 | issues-exit-code: 1 14 | 15 | # include test files or not, default is true 16 | tests: true 17 | 18 | # list of build tags, all linters use it. Default is empty list. 19 | build-tags: 20 | - mytag 21 | 22 | # which dirs to skip: issues from them won't be reported; 23 | # can use regexp here: generated.*, regexp is applied on full path; 24 | # default value is empty list, but default dirs are skipped independently 25 | # of this option's value (see skip-dirs-use-default). 26 | # "/" will be replaced by current OS file path separator to properly work 27 | # on Windows. 28 | skip-dirs: 29 | - .github 30 | - .make 31 | - dist 32 | 33 | # default is true. Enables skipping of directories: 34 | # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ 35 | skip-dirs-use-default: true 36 | 37 | # which files to skip: they will be analyzed, but issues from them 38 | # won't be reported. Default value is empty list, but there is 39 | # no need to include all autogenerated files, we confidently recognize 40 | # autogenerated files. If it's not please let us know. 41 | # "/" will be replaced by current OS file path separator to properly work 42 | # on Windows. 43 | skip-files: 44 | - ".*\\.my\\.go$" 45 | - lib/bad.go 46 | 47 | # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": 48 | # If invoked with -mod=readonly, the go command is disallowed from the implicit 49 | # automatic updating of go.mod described above. Instead, it fails when any changes 50 | # to go.mod are needed. This setting is most useful to check that go.mod does 51 | # not need updates, such as in a continuous integration and testing system. 52 | # If invoked with -mod=vendor, the go command assumes that the vendor 53 | # directory holds the correct copies of dependencies and ignores 54 | # the dependency descriptions in go.mod. 55 | #modules-download-mode: readonly|release|vendor 56 | 57 | # Allow multiple parallel golangci-lint instances running. 58 | # If false (default) - golangci-lint acquires file lock on start. 59 | allow-parallel-runners: false 60 | 61 | 62 | # output configuration options 63 | output: 64 | # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" 65 | format: colored-line-number 66 | 67 | # print lines of code with issue, default is true 68 | print-issued-lines: true 69 | 70 | # print linter name in the end of issue text, default is true 71 | print-linter-name: true 72 | 73 | # make issues output unique by line, default is true 74 | uniq-by-line: true 75 | 76 | # add a prefix to the output file references; default is no prefix 77 | path-prefix: "" 78 | 79 | 80 | # all available settings of specific linters 81 | linters-settings: 82 | dogsled: 83 | # checks assignments with too many blank identifiers; default is 2 84 | max-blank-identifiers: 2 85 | dupl: 86 | # tokens count to trigger issue, 150 by default 87 | threshold: 100 88 | errcheck: 89 | # report about not checking of errors in type assertions: `a := b.(MyStruct)`; 90 | # default is false: such cases aren't reported by default. 91 | check-type-assertions: false 92 | 93 | # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; 94 | # default is false: such cases aren't reported by default. 95 | check-blank: false 96 | 97 | # [deprecated] comma-separated list of pairs of the form pkg:regex 98 | # the regex is used to ignore names within pkg. (default "fmt:.*"). 99 | # see https://github.com/kisielk/errcheck#the-deprecated-method for details 100 | ignore: fmt:.*,io/ioutil:^Read.* 101 | 102 | # path to a file containing a list of functions to exclude from checking 103 | # see https://github.com/kisielk/errcheck#excluding-functions for details 104 | #exclude: /path/to/file.txt 105 | exhaustive: 106 | # indicates that switch statements are to be considered exhaustive if a 107 | # 'default' case is present, even if all enum members aren't listed in the 108 | # switch 109 | default-signifies-exhaustive: false 110 | funlen: 111 | lines: 60 112 | statements: 40 113 | gci: 114 | # put imports beginning with prefix after 3rd-party packages; 115 | # only support one prefix 116 | # if not set, use goimports.local-prefixes 117 | local-prefixes: github.com/org/project 118 | gocognit: 119 | # minimal code complexity to report, 30 by default (but we recommend 10-20) 120 | min-complexity: 10 121 | nestif: 122 | # minimal complexity of if statements to report, 5 by default 123 | min-complexity: 4 124 | goconst: 125 | # minimal length of string constant, 3 by default 126 | min-len: 3 127 | # minimal occurrences count to trigger, 3 by default 128 | min-occurrences: 3 129 | gocritic: 130 | # Which checks should be enabled; can't be combined with 'disabled-checks'; 131 | # See https://go-critic.github.io/overview#checks-overview 132 | # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run` 133 | # By default list of stable checks is used. 134 | #enabled-checks: 135 | # - rangeValCopy 136 | 137 | # Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty 138 | disabled-checks: 139 | - regexpMust 140 | 141 | # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. 142 | # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". 143 | enabled-tags: 144 | - performance 145 | disabled-tags: 146 | - experimental 147 | 148 | settings: # settings passed to gocritic 149 | captLocal: # must be valid enabled check name 150 | paramsOnly: true 151 | rangeValCopy: 152 | sizeThreshold: 32 153 | gocyclo: 154 | # minimal code complexity to report, 30 by default (but we recommend 10-20) 155 | min-complexity: 10 156 | godot: 157 | # check all top-level comments, not only declarations 158 | check-all: false 159 | godox: 160 | # report any comments starting with keywords, this is useful for TODO or FIXME comments that 161 | # might be left in the code accidentally and should be resolved before merging 162 | keywords: # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting 163 | - NOTE 164 | - OPTIMIZE # marks code that should be optimized before merging 165 | - HACK # marks hack-arounds that should be removed before merging 166 | gofmt: 167 | # simplify code: gofmt with `-s` option, true by default 168 | simplify: true 169 | goheader: 170 | values: 171 | const: 172 | # define here const type values in format k:v, for example: 173 | # YEAR: 2020 174 | # COMPANY: MY COMPANY 175 | regexp: 176 | # define here regexp type values, for example 177 | # AUTHOR: .*@mycompany\.com 178 | template: 179 | # put here copyright header template for source code files, for example: 180 | # {{ AUTHOR }} {{ COMPANY }} {{ YEAR }} 181 | # SPDX-License-Identifier: Apache-2.0 182 | # 183 | # Licensed under the Apache License, Version 2.0 (the "License"); 184 | # you may not use this file except in compliance with the License. 185 | # You may obtain a copy of the License at: 186 | # 187 | # http://www.apache.org/licenses/LICENSE-2.0 188 | # 189 | # Unless required by applicable law or agreed to in writing, software 190 | # distributed under the License is distributed on an "AS IS" BASIS, 191 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 192 | # See the License for the specific language governing permissions and 193 | # limitations under the License. 194 | template-path: 195 | # also, as alternative of directive 'template' you may put the path to file with the template source 196 | goimports: 197 | # put imports beginning with prefix after 3rd-party packages; 198 | # it's a comma-separated list of prefixes 199 | local-prefixes: github.com/org/project 200 | gomnd: 201 | settings: 202 | mnd: 203 | # the list of enabled checks, see https://github.com/tommy-muehle/go-mnd/#checks for description. 204 | checks: argument,case,condition,operation,return,assign 205 | govet: 206 | # report about shadowed variables 207 | check-shadowing: true 208 | 209 | # settings per analyzer 210 | settings: 211 | printf: # analyzer name, run `go tool vet help` to see all analyzers 212 | funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer 213 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof 214 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf 215 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf 216 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf 217 | 218 | # enable or disable analyzers by name 219 | enable: 220 | - atomicalign 221 | enable-all: false 222 | disable: 223 | #- shadow 224 | disable-all: false 225 | depguard: 226 | list-type: blacklist 227 | include-go-root: false 228 | packages: 229 | - github.com/sirupsen/logrus 230 | packages-with-error-message: 231 | # specify an error message to output when a blacklisted package is used 232 | - github.com/sirupsen/logrus: "logging is allowed only by logutils.Log" 233 | lll: 234 | # max line length, lines longer will be reported. Default is 120. 235 | # '\t' is counted as 1 character by default, and can be changed with the tab-width option 236 | line-length: 120 237 | # tab width in spaces. Default to 1. 238 | tab-width: 1 239 | misspell: 240 | # Correct spellings using locale preferences for US or UK. 241 | # Default is to use a neutral variety of English. 242 | # Setting locale to US will correct the British spelling of 'colour' to 'color'. 243 | locale: US 244 | ignore-words: 245 | - bsv 246 | - bitcoin 247 | nakedret: 248 | # make an issue if func has more lines of code than this setting, and it has naked returns; default is 30 249 | max-func-lines: 30 250 | prealloc: 251 | # XXX: we don't recommend using this linter before doing performance profiling. 252 | # For most programs usage of prealloc will be a premature optimization. 253 | 254 | # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. 255 | # True by default. 256 | simple: true 257 | range-loops: true # Report preallocation suggestions on range loops, true by default 258 | for-loops: false # Report preallocation suggestions on for loops, false by default 259 | nolintlint: 260 | # Enable to ensure that nolint directives are all used. Default is true. 261 | allow-unused: false 262 | # Disable to ensure that nolint directives don't have a leading space. Default is true. 263 | allow-leading-space: true 264 | # Exclude following linters from requiring an explanation. Default is []. 265 | allow-no-explanation: [] 266 | # Enable to require an explanation of nonzero length after each nolint directive. Default is false. 267 | require-explanation: true 268 | # Enable to require nolint directives to mention the specific linter being suppressed. Default is false. 269 | require-specific: true 270 | rowserrcheck: 271 | packages: 272 | - github.com/jmoiron/sqlx 273 | testpackage: 274 | # regexp pattern to skip files 275 | skip-regexp: (export|internal)_test\.go 276 | unparam: 277 | # Inspect exported functions, default is false. Set to true if no external program/library imports your code. 278 | # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: 279 | # if it's called for subdir of a project it can't find external interfaces. All text editor integrations 280 | # with golangci-lint call it on a directory with the changed file. 281 | check-exported: false 282 | unused: 283 | # treat code as a program (not a library) and report unused exported identifiers; default is false. 284 | # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: 285 | # if it's called for subdir of a project it can't find funcs usages. All text editor integrations 286 | # with golangci-lint call it on a directory with the changed file. 287 | check-exported: false 288 | whitespace: 289 | multi-if: false # Enforces newlines (or comments) after every multi-line if statement 290 | multi-func: false # Enforces newlines (or comments) after every multi-line function signature 291 | wsl: 292 | # If true append is only allowed to be cuddled if appending value is 293 | # matching variables, fields or types on the line above. Default is true. 294 | strict-append: true 295 | # Allow calls and assignments to be cuddled as long as the lines have any 296 | # matching variables, fields or types. Default is true. 297 | allow-assign-and-call: true 298 | # Allow multiline assignments to be cuddled. Default is true. 299 | allow-multiline-assign: true 300 | # Allow declarations (var) to be cuddled. 301 | allow-cuddle-declarations: true 302 | # Allow trailing comments in ending of blocks 303 | allow-trailing-comment: false 304 | # Force newlines in end of case at this limit (0 = never). 305 | force-case-trailing-whitespace: 0 306 | # Force cuddling of err checks with err var assignment 307 | force-err-cuddling: false 308 | # Allow leading comments to be separated with empty liens 309 | allow-separated-leading-comment: false 310 | gofumpt: 311 | # Choose whether to use the extra rules that are disabled 312 | # by default 313 | extra-rules: false 314 | 315 | # The custom section can be used to define linter plugins to be loaded at runtime. See README doc 316 | # for more info. 317 | custom: 318 | # Each custom linter should have a unique name. 319 | #example: 320 | # The path to the plugin *.so. Can be absolute or local. Required for each custom linter 321 | #path: /path/to/example.so 322 | # The description of the linter. Optional, just for documentation purposes. 323 | #description: This is an example usage of a plugin linter. 324 | # Intended to point to the repo location of the linter. Optional, just for documentation purposes. 325 | #original-url: github.com/golangci/example-linter 326 | 327 | linters: 328 | enable: 329 | - megacheck 330 | - govet 331 | - gosec 332 | - bodyclose 333 | - revive 334 | - unconvert 335 | - dupl 336 | - misspell 337 | - dogsled 338 | - prealloc 339 | - exportloopref 340 | - exhaustive 341 | - sqlclosecheck 342 | - nolintlint 343 | - gci 344 | - goconst 345 | - lll 346 | disable: 347 | - gocritic # use this for very opinionated linting 348 | - gochecknoglobals 349 | - whitespace 350 | - wsl 351 | - goerr113 352 | - godot 353 | - testpackage 354 | - nestif 355 | - nlreturn 356 | disable-all: false 357 | presets: 358 | - bugs 359 | - unused 360 | fast: false 361 | 362 | 363 | issues: 364 | # List of regexps of issue texts to exclude, empty list by default. 365 | # But independently of this option we use default exclude patterns, 366 | # it can be disabled by `exclude-use-default: false`. To list all 367 | # excluded by default patterns execute `golangci-lint run --help` 368 | exclude: 369 | - Using the variable on range scope .* in function literal 370 | 371 | # Excluding configuration per-path, per-linter, per-text and per-source 372 | exclude-rules: 373 | # Exclude some linters from running on tests files. 374 | - path: _test\.go 375 | linters: 376 | - gocyclo 377 | - errcheck 378 | - dupl 379 | - gosec 380 | - lll 381 | 382 | # Exclude known linters from partially hard-vendored code, 383 | # which is impossible to exclude via "nolint" comments. 384 | - path: internal/hmac/ 385 | text: "weak cryptographic primitive" 386 | linters: 387 | - gosec 388 | 389 | # Exclude some staticcheck messages 390 | - linters: 391 | - staticcheck 392 | text: "SA1019:" 393 | 394 | # Exclude lll issues for long lines with go:generate 395 | - linters: 396 | - lll 397 | source: "^//go:generate " 398 | 399 | # Independently of option `exclude` we use default exclude patterns, 400 | # it can be disabled by this option. To list all 401 | # excluded by default patterns execute `golangci-lint run --help`. 402 | # Default value for this option is true. 403 | exclude-use-default: false 404 | 405 | # The default value is false. If set to true exclude and exclude-rules 406 | # regular expressions become case-sensitive. 407 | exclude-case-sensitive: false 408 | 409 | # Maximum issues count per one linter. Set to 0 to disable. Default is 50. 410 | max-issues-per-linter: 0 411 | 412 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 413 | max-same-issues: 0 414 | 415 | # Show only new issues: if there are unstaged changes or untracked files, 416 | # only those changes are analyzed, else only changes in HEAD~ are analyzed. 417 | # It's a super-useful option for integration of golangci-lint into existing 418 | # large codebase. It's not practical to fix all existing issues at the moment 419 | # of integration: much better don't allow issues in new code. 420 | # Default is false. 421 | new: false 422 | 423 | # Show only new issues created after git revision `REV` 424 | new-from-rev: "" 425 | 426 | # Show only new issues created in git patch with set file path. 427 | #new-from-patch: path/to/patch/file 428 | 429 | severity: 430 | # Default value is empty string. 431 | # Set the default severity for issues. If severity rules are defined and the issues 432 | # do not match or no severity is provided to the rule this will be the default 433 | # severity applied. Severities should match the supported severity names of the 434 | # selected out format. 435 | # - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity 436 | # - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#severity 437 | # - GitHub: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message 438 | default-severity: error 439 | 440 | # The default value is false. 441 | # If set to true severity-rules regular expressions become case-sensitive. 442 | case-sensitive: false 443 | 444 | # Default value is empty list. 445 | # When a list of severity rules are provided, severity information will be added to lint 446 | # issues. Severity rules have the same filtering capability as exclude rules except you 447 | # are allowed to specify one matcher per severity rule. 448 | # Only affects out formats that support setting severity information. 449 | rules: 450 | - linters: 451 | - dupl 452 | severity: info -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Make sure to check the documentation at http://goreleaser.com 2 | # --------------------------- 3 | # General 4 | # --------------------------- 5 | before: 6 | hooks: 7 | - make all 8 | snapshot: 9 | name_template: "{{ .Tag }}" 10 | changelog: 11 | sort: asc 12 | filters: 13 | exclude: 14 | - '^.github:' 15 | - '^.vscode:' 16 | - '^test:' 17 | 18 | # --------------------------- 19 | # Publishers 20 | # --------------------------- 21 | #publishers: 22 | # - name: "Publish GoDocs" 23 | # cmd: make godocs 24 | 25 | # --------------------------- 26 | # Builder 27 | # --------------------------- 28 | build: 29 | skip: true 30 | 31 | # --------------------------- 32 | # GitHub Release 33 | # --------------------------- 34 | release: 35 | prerelease: false 36 | name_template: "Release v{{.Version}}" 37 | 38 | # --------------------------- 39 | # Announce 40 | # --------------------------- 41 | announce: 42 | 43 | # See more at: https://goreleaser.com/customization/announce/#slack 44 | slack: 45 | enabled: true 46 | message_template: '{{ .ProjectName }} {{ .Tag }} is out! Changelog: https://github.com/BitcoinSchema/{{ .ProjectName }}/releases/tag/{{ .Tag }}' 47 | channel: '#go' 48 | # username: '' 49 | # icon_emoji: '' 50 | # icon_url: '' 51 | 52 | # See more at: https://goreleaser.com/customization/announce/#twitter 53 | twitter: 54 | enabled: false 55 | message_template: '{{ .ProjectName }} {{ .Tag }} is out!' 56 | 57 | # See more at: https://goreleaser.com/customization/announce/#discord 58 | discord: 59 | enabled: false 60 | message_template: '{{ .ProjectName }} {{ .Tag }} is out!' 61 | # Defaults to `GoReleaser` 62 | author: '' 63 | # Defaults to `3888754` - the grey-ish from goreleaser 64 | color: '' 65 | # Defaults to `https://goreleaser.com/static/avatar.png` 66 | icon_url: '' 67 | 68 | # See more at: https://goreleaser.com/customization/announce/#reddit 69 | reddit: 70 | enabled: false 71 | # Application ID for Reddit Application 72 | application_id: "" 73 | # Username for your Reddit account 74 | username: "" 75 | # Defaults to `{{ .GitURL }}/releases/tag/{{ .Tag }}` 76 | # url_template: 'https://github.com/BitcoinSchema/{{ .ProjectName }}/releases/tag/{{ .Tag }}' 77 | # Defaults to `{{ .ProjectName }} {{ .Tag }} is out!` 78 | title_template: '{{ .ProjectName }} {{ .Tag }} is out!' 79 | -------------------------------------------------------------------------------- /.make/common.mk: -------------------------------------------------------------------------------- 1 | ## Default repository domain name 2 | ifndef GIT_DOMAIN 3 | override GIT_DOMAIN=github.com 4 | endif 5 | 6 | ## Set if defined (alias variable for ease of use) 7 | ifdef branch 8 | override REPO_BRANCH=$(branch) 9 | export REPO_BRANCH 10 | endif 11 | 12 | ## Do we have git available? 13 | HAS_GIT := $(shell command -v git 2> /dev/null) 14 | 15 | ifdef HAS_GIT 16 | ## Do we have a repo? 17 | HAS_REPO := $(shell git rev-parse --is-inside-work-tree 2> /dev/null) 18 | ifdef HAS_REPO 19 | ## Automatically detect the repo owner and repo name (for local use with Git) 20 | REPO_NAME=$(shell basename "$(shell git rev-parse --show-toplevel 2> /dev/null)") 21 | OWNER=$(shell git config --get remote.origin.url | sed 's/git@$(GIT_DOMAIN)://g' | sed 's/\/$(REPO_NAME).git//g') 22 | REPO_OWNER=$(shell echo $(OWNER) | tr A-Z a-z) 23 | VERSION_SHORT=$(shell git describe --tags --always --abbrev=0) 24 | export REPO_NAME, REPO_OWNER, VERSION_SHORT 25 | endif 26 | endif 27 | 28 | ## Set the distribution folder 29 | ifndef DISTRIBUTIONS_DIR 30 | override DISTRIBUTIONS_DIR=./dist 31 | endif 32 | export DISTRIBUTIONS_DIR 33 | 34 | .PHONY: diff 35 | diff: ## Show the git diff 36 | $(call print-target) 37 | git diff --exit-code 38 | RES=$$(git status --porcelain) ; if [ -n "$$RES" ]; then echo $$RES && exit 1 ; fi 39 | 40 | .PHONY: help 41 | help: ## Show this help message 42 | @egrep -h '^(.+)\:\ ##\ (.+)' ${MAKEFILE_LIST} | column -t -c 2 -s ':#' 43 | 44 | .PHONY: install-releaser 45 | install-releaser: ## Install the GoReleaser application 46 | @echo "installing GoReleaser..." 47 | @curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh 48 | 49 | .PHONY: release 50 | release:: ## Full production release (creates release in GitHub) 51 | @echo "releasing..." 52 | @test $(github_token) 53 | @export GITHUB_TOKEN=$(github_token) && goreleaser --rm-dist 54 | 55 | .PHONY: release-test 56 | release-test: ## Full production test release (everything except deploy) 57 | @echo "creating a release test..." 58 | @goreleaser --skip-publish --rm-dist 59 | 60 | .PHONY: release-snap 61 | release-snap: ## Test the full release (build binaries) 62 | @echo "creating a release snapshot..." 63 | @goreleaser --snapshot --skip-publish --rm-dist 64 | 65 | .PHONY: release-version 66 | replace-version: ## Replaces the version in HTML/JS (pre-deploy) 67 | @echo "replacing version..." 68 | @test $(version) 69 | @test "$(path)" 70 | @find $(path) -name "*.html" -type f -exec sed -i '' -e "s/{{version}}/$(version)/g" {} \; 71 | @find $(path) -name "*.js" -type f -exec sed -i '' -e "s/{{version}}/$(version)/g" {} \; 72 | 73 | .PHONY: tag 74 | tag: ## Generate a new tag and push (tag version=0.0.0) 75 | @echo "creating new tag..." 76 | @test $(version) 77 | @git tag -a v$(version) -m "Pending full release..." 78 | @git push origin v$(version) 79 | @git fetch --tags -f 80 | 81 | .PHONY: tag-remove 82 | tag-remove: ## Remove a tag if found (tag-remove version=0.0.0) 83 | @echo "removing tag..." 84 | @test $(version) 85 | @git tag -d v$(version) 86 | @git push --delete origin v$(version) 87 | @git fetch --tags 88 | 89 | .PHONY: tag-update 90 | tag-update: ## Update an existing tag to current commit (tag-update version=0.0.0) 91 | @echo "updating tag to new commit..." 92 | @test $(version) 93 | @git push --force origin HEAD:refs/tags/v$(version) 94 | @git fetch --tags -f 95 | 96 | .PHONY: update-releaser 97 | update-releaser: ## Update the goreleaser application 98 | @echo "updating GoReleaser application..." 99 | @$(MAKE) install-releaser 100 | -------------------------------------------------------------------------------- /.make/go.mk: -------------------------------------------------------------------------------- 1 | ## Default to the repo name if empty 2 | ifndef BINARY_NAME 3 | override BINARY_NAME=app 4 | endif 5 | 6 | ## Define the binary name 7 | ifdef CUSTOM_BINARY_NAME 8 | override BINARY_NAME=$(CUSTOM_BINARY_NAME) 9 | endif 10 | 11 | ## Set the binary release names 12 | DARWIN=$(BINARY_NAME)-darwin 13 | LINUX=$(BINARY_NAME)-linux 14 | WINDOWS=$(BINARY_NAME)-windows.exe 15 | 16 | ## Define the binary name 17 | TAGS= 18 | ifdef GO_BUILD_TAGS 19 | override TAGS=-tags $(GO_BUILD_TAGS) 20 | endif 21 | 22 | .PHONY: bench 23 | bench: ## Run all benchmarks in the Go application 24 | @echo "running benchmarks..." 25 | @go test -bench=. -benchmem $(TAGS) 26 | 27 | .PHONY: build-go 28 | build-go: ## Build the Go application (locally) 29 | @echo "building go app..." 30 | @go build -o bin/$(BINARY_NAME) $(TAGS) 31 | 32 | .PHONY: clean-mods 33 | clean-mods: ## Remove all the Go mod cache 34 | @echo "cleaning mods..." 35 | @go clean -modcache 36 | 37 | .PHONY: coverage 38 | coverage: ## Shows the test coverage 39 | @echo "creating coverage report..." 40 | @go test -coverprofile=coverage.out ./... $(TAGS) && go tool cover -func=coverage.out $(TAGS) 41 | 42 | .PHONY: generate 43 | generate: ## Runs the go generate command in the base of the repo 44 | @echo "generating files..." 45 | @go generate -v $(TAGS) 46 | 47 | .PHONY: godocs 48 | godocs: ## Sync the latest tag with GoDocs 49 | @echo "syndicating to GoDocs..." 50 | @test $(GIT_DOMAIN) 51 | @test $(REPO_OWNER) 52 | @test $(REPO_NAME) 53 | @test $(VERSION_SHORT) 54 | @curl https://proxy.golang.org/$(GIT_DOMAIN)/$(REPO_OWNER)/$(REPO_NAME)/@v/$(VERSION_SHORT).info 55 | 56 | .PHONY: install 57 | install: ## Install the application 58 | @echo "installing binary..." 59 | @go build -o $$GOPATH/bin/$(BINARY_NAME) $(TAGS) 60 | 61 | .PHONY: install-go 62 | install-go: ## Install the application (Using Native Go) 63 | @echo "installing package..." 64 | @go install $(GIT_DOMAIN)/$(REPO_OWNER)/$(REPO_NAME) $(TAGS) 65 | 66 | .PHONY: lint 67 | lint: ## Run the golangci-lint application (install if not found) 68 | @echo "installing golangci-lint..." 69 | @#Travis (has sudo) 70 | @if [ "$(shell command -v golangci-lint)" = "" ] && [ $(TRAVIS) ]; then curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.53.3 && sudo cp ./bin/golangci-lint $(go env GOPATH)/bin/; fi; 71 | @#AWS CodePipeline 72 | @if [ "$(shell command -v golangci-lint)" = "" ] && [ "$(CODEBUILD_BUILD_ID)" != "" ]; then curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.53.3; fi; 73 | @#GitHub Actions 74 | @if [ "$(shell command -v golangci-lint)" = "" ] && [ "$(GITHUB_WORKFLOW)" != "" ]; then curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sudo sh -s -- -b $(go env GOPATH)/bin v1.53.3; fi; 75 | @#Brew - MacOS 76 | @if [ "$(shell command -v golangci-lint)" = "" ] && [ "$(shell command -v brew)" != "" ]; then brew install golangci-lint; fi; 77 | @#MacOS Vanilla 78 | @if [ "$(shell command -v golangci-lint)" = "" ] && [ "$(shell command -v brew)" != "" ]; then curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- v1.53.3; fi; 79 | @echo "running golangci-lint..." 80 | @golangci-lint run --verbose 81 | 82 | .PHONY: test 83 | test: ## Runs lint and ALL tests 84 | @$(MAKE) lint 85 | @echo "running tests..." 86 | @go test ./... -v $(TAGS) 87 | 88 | .PHONY: test-unit 89 | test-unit: ## Runs tests and outputs coverage 90 | @echo "running unit tests..." 91 | @go test ./... -race -coverprofile=coverage.txt -covermode=atomic $(TAGS) 92 | 93 | .PHONY: test-short 94 | test-short: ## Runs vet, lint and tests (excludes integration tests) 95 | @$(MAKE) lint 96 | @echo "running tests (short)..." 97 | @go test ./... -v -test.short $(TAGS) 98 | 99 | .PHONY: test-ci 100 | test-ci: ## Runs all tests via CI (exports coverage) 101 | @$(MAKE) lint 102 | @echo "running tests (CI)..." 103 | @go test ./... -race -coverprofile=coverage.txt -covermode=atomic $(TAGS) 104 | 105 | .PHONY: test-ci-no-race 106 | test-ci-no-race: ## Runs all tests via CI (no race) (exports coverage) 107 | @$(MAKE) lint 108 | @echo "running tests (CI - no race)..." 109 | @go test ./... -coverprofile=coverage.txt -covermode=atomic $(TAGS) 110 | 111 | .PHONY: test-ci-short 112 | test-ci-short: ## Runs unit tests via CI (exports coverage) 113 | @$(MAKE) lint 114 | @echo "running tests (CI - unit tests only)..." 115 | @go test ./... -test.short -race -coverprofile=coverage.txt -covermode=atomic $(TAGS) 116 | 117 | .PHONY: test-no-lint 118 | test-no-lint: ## Runs just tests 119 | @echo "running tests..." 120 | @go test ./... -v $(TAGS) 121 | 122 | .PHONY: uninstall 123 | uninstall: ## Uninstall the application (and remove files) 124 | @echo "uninstalling go application..." 125 | @test $(BINARY_NAME) 126 | @test $(GIT_DOMAIN) 127 | @test $(REPO_OWNER) 128 | @test $(REPO_NAME) 129 | @go clean -i $(GIT_DOMAIN)/$(REPO_OWNER)/$(REPO_NAME) 130 | @rm -rf $$GOPATH/src/$(GIT_DOMAIN)/$(REPO_OWNER)/$(REPO_NAME) 131 | @rm -rf $$GOPATH/bin/$(BINARY_NAME) 132 | 133 | .PHONY: update 134 | update: ## Update all project dependencies 135 | @echo "updating dependencies..." 136 | @go get -u ./... && go mod tidy 137 | 138 | .PHONY: update-linter 139 | update-linter: ## Update the golangci-lint package (macOS only) 140 | @echo "upgrading golangci-lint..." 141 | @brew upgrade golangci-lint 142 | 143 | .PHONY: vet 144 | vet: ## Run the Go vet application 145 | @echo "running go vet..." 146 | @go vet -v ./... $(TAGS) 147 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 @BitcoinSchema 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Common makefile commands & variables between projects 2 | include .make/common.mk 3 | 4 | # Common Golang makefile commands & variables between projects 5 | include .make/go.mk 6 | 7 | ## Not defined? Use default repo name which is the application 8 | ifeq ($(REPO_NAME),) 9 | REPO_NAME="go-bitcoin" 10 | endif 11 | 12 | ## Not defined? Use default repo owner 13 | ifeq ($(REPO_OWNER),) 14 | REPO_OWNER="bitcoinschema" 15 | endif 16 | 17 | .PHONY: all 18 | all: ## Runs multiple commands 19 | @$(MAKE) test 20 | 21 | .PHONY: clean 22 | clean: ## Remove previous builds and any test cache data 23 | @go clean -cache -testcache -i -r 24 | @test $(DISTRIBUTIONS_DIR) 25 | @if [ -d $(DISTRIBUTIONS_DIR) ]; then rm -r $(DISTRIBUTIONS_DIR); fi 26 | 27 | .PHONY: release 28 | release:: ## Runs common.release then runs godocs 29 | @$(MAKE) godocs -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-bitcoin 2 | > A library for working with Bitcoin (BSV) transactions, addresses, keys, encryption, and more. 3 | 4 | [![Release](https://img.shields.io/github/release-pre/BitcoinSchema/go-bitcoin.svg?logo=github&style=flat&v=2)](https://github.com/BitcoinSchema/go-bitcoin/releases) 5 | [![Build Status](https://img.shields.io/github/actions/workflow/status/BitcoinSchema/go-bitcoin/run-tests.yml?branch=master&logo=github&v=2)](https://github.com/BitcoinSchema/go-bitcoin/actions) 6 | [![Report](https://goreportcard.com/badge/github.com/BitcoinSchema/go-bitcoin?style=flat&v=2)](https://goreportcard.com/report/github.com/BitcoinSchema/go-bitcoin) 7 | [![codecov](https://codecov.io/gh/BitcoinSchema/go-bitcoin/branch/master/graph/badge.svg?v=2)](https://codecov.io/gh/BitcoinSchema/go-bitcoin) 8 | [![Go](https://img.shields.io/github/go-mod/go-version/BitcoinSchema/go-bitcoin?v=2)](https://golang.org/) 9 |
10 | [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod&v=2)](https://gitpod.io/#https://github.com/BitcoinSchema/go-bitcoin) 11 | [![Mergify Status](https://img.shields.io/endpoint.svg?url=https://api.mergify.com/v1/badges/BitcoinSchema/go-bitcoin&style=flat&v=2)](https://mergify.io) 12 | [![Sponsor](https://img.shields.io/badge/sponsor-BitcoinSchema-181717.svg?logo=github&style=flat&v=2)](https://github.com/sponsors/BitcoinSchema) 13 | [![Donate](https://img.shields.io/badge/donate-bitcoin-ff9900.svg?logo=bitcoin&style=flat&v=2)](https://gobitcoinsv.com/#sponsor?utm_source=github&utm_medium=sponsor-link&utm_campaign=go-bitcoin&utm_term=go-bitcoin&utm_content=go-bitcoin) 14 | 15 |
16 | 17 | ## Table of Contents 18 | 19 | - [Installation](#installation) 20 | - [Documentation](#documentation) 21 | - [Examples & Tests](#examples--tests) 22 | - [Benchmarks](#benchmarks) 23 | - [Code Standards](#code-standards) 24 | - [Usage](#usage) 25 | - [Maintainers](#maintainers) 26 | - [Contributing](#contributing) 27 | - [License](#license) 28 | 29 |
30 | 31 | ## Installation 32 | 33 | **go-bitcoin** requires a [supported release of Go](https://golang.org/doc/devel/release.html#policy). 34 | 35 | ```shell script 36 | go get -u github.com/bitcoinschema/go-bitcoin/v2 37 | ``` 38 | 39 | > If you want to install the **deprecated V1**: 40 | ```shell script 41 | go get -u github.com/bitcoinschema/go-bitcoin@v1 42 | ``` 43 | 44 |
45 | 46 | ## Documentation 47 | 48 | View the generated [documentation](https://pkg.go.dev/github.com/bitcoinschema/go-bitcoin) 49 | 50 | [![GoDoc](https://godoc.org/github.com/bitcoinschema/go-bitcoin/?status.svg&style=flat)](https://pkg.go.dev/github.com/bitcoinschema/go-bitcoin) 51 | 52 | ### Features 53 | 54 | - **Addresses** 55 | - [Address from PrivateKey (bec.PrivateKey)](address.go) 56 | - [Address from Script](address.go) 57 | - **Encryption** 58 | - [Encrypt With Private Key](encryption.go) 59 | - [Decrypt With Private Key](encryption.go) 60 | - [Encrypt Shared](encryption.go) 61 | - **HD Keys** _(Master / xPub)_ 62 | - [Generate HD Keys](hd_key.go) 63 | - [Generate HD Key from string](hd_key.go) 64 | - [Get HD Key by Path](hd_key.go) 65 | - [Get PrivateKey by Path](hd_key.go) 66 | - [Get HD Child Key](hd_key.go) 67 | - [Get Addresses from HD Key](hd_key.go) 68 | - [Get XPub from HD Key](hd_key.go) 69 | - [Get HD Key from XPub](hd_key.go) 70 | - [Get PublicKeys for Path](hd_key.go) 71 | - [Get Addresses for Path](hd_key.go) 72 | - **PubKeys** 73 | - [Create PubKey from PrivateKey](pubkey.go) 74 | - [PubKey from String](pubkey.go) 75 | - **Private Keys** 76 | - [Create PrivateKey](private_key.go) 77 | - [Create WIF](private_key.go) 78 | - [PrivateKey (string) to Address (string)](address.go) 79 | - [PrivateKey from string](private_key.go) 80 | - [Generate Shared Keypair](private_key.go) 81 | - [Get Private and Public keys](private_key.go) 82 | - [WIF to PrivateKey](private_key.go) 83 | - [PrivateKey to WIF](private_key.go) 84 | - **Scripts** 85 | - [Script from Address](script.go) 86 | - **Signatures** 87 | - [Sign](sign.go) & [Verify a Bitcoin Message](verify.go) 88 | - [Verify a DER Signature](verify.go) 89 | - **Transactions** 90 | - [Calculate Fee](transaction.go) 91 | - [Create Tx](transaction.go) 92 | - [Create Tx using WIF](transaction.go) 93 | - [Create Tx with Change](transaction.go) 94 | - [Tx from Hex](transaction.go) 95 | 96 |
97 | Package Dependencies 98 |
99 | 100 | - [bitcoinsv/bsvd](https://github.com/bitcoinsv/bsvd) 101 | - [libsv/go-bk](https://github.com/libsv/go-bk) 102 | - [libsv/go-bt](https://github.com/libsv/go-bt) 103 |
104 | 105 |
106 | Library Deployment 107 |
108 | 109 | [goreleaser](https://github.com/goreleaser/goreleaser) for easy binary or library deployment to GitHub and can be installed via: `brew install goreleaser`. 110 | 111 | The [.goreleaser.yml](.goreleaser.yml) file is used to configure [goreleaser](https://github.com/goreleaser/goreleaser). 112 | 113 | Use `make release-snap` to create a snapshot version of the release, and finally `make release` to ship to production. 114 | 115 |
116 | 117 |
118 | Makefile Commands 119 |
120 | 121 | View all `makefile` commands 122 | 123 | ```shell script 124 | make help 125 | ``` 126 | 127 | List of all current commands: 128 | 129 | ```text 130 | all Runs multiple commands 131 | clean Remove previous builds and any test cache data 132 | clean-mods Remove all the Go mod cache 133 | coverage Shows the test coverage 134 | diff Show the git diff 135 | generate Runs the go generate command in the base of the repo 136 | godocs Sync the latest tag with GoDocs 137 | help Show this help message 138 | install Install the application 139 | install-go Install the application (Using Native Go) 140 | install-releaser Install the GoReleaser application 141 | lint Run the golangci-lint application (install if not found) 142 | release Full production release (creates release in GitHub) 143 | release Runs common.release then runs godocs 144 | release-snap Test the full release (build binaries) 145 | release-test Full production test release (everything except deploy) 146 | replace-version Replaces the version in HTML/JS (pre-deploy) 147 | tag Generate a new tag and push (tag version=0.0.0) 148 | tag-remove Remove a tag if found (tag-remove version=0.0.0) 149 | tag-update Update an existing tag to current commit (tag-update version=0.0.0) 150 | test Runs lint and ALL tests 151 | test-ci Runs all tests via CI (exports coverage) 152 | test-ci-no-race Runs all tests via CI (no race) (exports coverage) 153 | test-ci-short Runs unit tests via CI (exports coverage) 154 | test-no-lint Runs just tests 155 | test-short Runs vet, lint and tests (excludes integration tests) 156 | test-unit Runs tests and outputs coverage 157 | uninstall Uninstall the application (and remove files) 158 | update-linter Update the golangci-lint package (macOS only) 159 | vet Run the Go vet application 160 | ``` 161 | 162 |
163 | 164 |
165 | 166 | ## Examples & Tests 167 | All unit tests and [examples](examples) run via [GitHub Actions](https://github.com/BitcoinSchema/go-bitcoin/actions) and 168 | uses [Go version 1.18.x](https://golang.org/doc/go1.18). View the [configuration file](.github/workflows/run-tests.yml). 169 | 170 | Run all tests (including integration tests) 171 | 172 | ```shell script 173 | make test 174 | ``` 175 | 176 | Run tests (excluding integration tests) 177 | 178 | ```shell script 179 | make test-short 180 | ``` 181 | 182 |
183 | 184 | ## Benchmarks 185 | 186 | Run the Go benchmarks: 187 | 188 | ```shell script 189 | make bench 190 | ``` 191 | 192 |
193 | 194 | ## Code Standards 195 | 196 | Read more about this Go project's [code standards](.github/CODE_STANDARDS.md). 197 | 198 |
199 | 200 | ## Usage 201 | 202 | Checkout all the [examples](examples)! 203 | 204 |
205 | 206 | ## Maintainers 207 | 208 | | [MrZ](https://github.com/mrz1836) | [MrZ](https://github.com/rohenaz) | 209 | |:------------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------:| 210 | | [MrZ](https://github.com/mrz1836) | [Satchmo](https://github.com/rohenaz) | 211 | 212 |
213 | 214 | ## Contributing 215 | 216 | View the [contributing guidelines](.github/CONTRIBUTING.md) and follow the [code of conduct](.github/CODE_OF_CONDUCT.md). 217 | 218 | ### How can I help? 219 | 220 | All kinds of contributions are welcome :raised_hands:! 221 | The most basic way to show your support is to star :star2: the project, or to raise issues :speech_balloon:. 222 | You can also support this project by [becoming a sponsor on GitHub](https://github.com/sponsors/BitcoinSchema) :clap: 223 | or by making a [**bitcoin donation**](https://gobitcoinsv.com/#sponsor?utm_source=github&utm_medium=sponsor-link&utm_campaign=go-bitcoin&utm_term=go-bitcoin&utm_content=go-bitcoin) to ensure this journey continues indefinitely! :rocket: 224 | 225 | [![Stars](https://img.shields.io/github/stars/BitcoinSchema/go-bitcoin?label=Please%20like%20us&style=social)](https://github.com/BitcoinSchema/go-bitcoin/stargazers) 226 | 227 |
228 | 229 | ## License 230 | 231 | [![License](https://img.shields.io/github/license/BitcoinSchema/go-bitcoin.svg?style=flat&v=2)](LICENSE) 232 | -------------------------------------------------------------------------------- /address.go: -------------------------------------------------------------------------------- 1 | package bitcoin 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "fmt" 8 | 9 | "github.com/libsv/go-bk/bec" 10 | "github.com/libsv/go-bk/crypto" 11 | "github.com/libsv/go-bt/v2/bscript" 12 | ) 13 | 14 | // A25 is a type for a 25 byte (not base58 encoded) bitcoin address. 15 | type A25 [25]byte 16 | 17 | // DoubleSHA256 computes a double sha256 hash of the first 21 bytes of the 18 | // address. This is the one function shared with the other bitcoin RC task. 19 | // Returned is the full 32 byte sha256 hash. (The bitcoin checksum will be 20 | // the first four bytes of the slice.) 21 | func (a *A25) doubleSHA256() []byte { 22 | h := sha256.New() 23 | _, _ = h.Write(a[:21]) 24 | d := h.Sum([]byte{}) 25 | h = sha256.New() 26 | _, _ = h.Write(d) 27 | return h.Sum(d[:0]) 28 | } 29 | 30 | // Version returns the version byte of an A25 address 31 | func (a *A25) Version() byte { 32 | return a[0] 33 | } 34 | 35 | // EmbeddedChecksum returns the 4 checksum bytes of an A25 address 36 | func (a *A25) EmbeddedChecksum() (c [4]byte) { 37 | copy(c[:], a[21:]) 38 | return 39 | } 40 | 41 | // Tmpl and Set58 are adapted from the C solution. 42 | // Go has big integers but this technique seems better. 43 | var tmpl = []byte("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz") 44 | 45 | // Set58 takes a base58 encoded address and decodes it into the receiver. 46 | // Errors are returned if the argument is not valid base58 or if the decoded 47 | // value does not fit in the 25 byte address. The address is not otherwise 48 | // checked for validity. 49 | func (a *A25) Set58(s []byte) error { 50 | for _, s1 := range s { 51 | c := bytes.IndexByte(tmpl, s1) 52 | if c < 0 { 53 | return ErrBadCharacter 54 | } 55 | for j := 24; j >= 0; j-- { 56 | c += 58 * int(a[j]) 57 | a[j] = byte(c % 256) 58 | c /= 256 59 | } 60 | if c > 0 { 61 | return ErrTooLong 62 | } 63 | } 64 | return nil 65 | } 66 | 67 | // ComputeChecksum returns a four byte checksum computed from the first 21 68 | // bytes of the address. The embedded checksum is not updated. 69 | func (a *A25) ComputeChecksum() (c [4]byte) { 70 | copy(c[:], a.doubleSHA256()) 71 | return 72 | } 73 | 74 | // ValidA58 validates a base58 encoded bitcoin address. An address is valid 75 | // if it can be decoded into a 25 byte address, the version number is 0, 76 | // and the checksum validates. Return value ok will be true for valid 77 | // addresses. If ok is false, the address is invalid and the error value 78 | // may indicate why. 79 | func ValidA58(a58 []byte) (bool, error) { 80 | var a A25 81 | if err := a.Set58(a58); err != nil { 82 | return false, err 83 | } 84 | if a.Version() != 0 { 85 | return false, ErrNotVersion0 86 | } 87 | return a.EmbeddedChecksum() == a.ComputeChecksum(), nil 88 | } 89 | 90 | // GetAddressFromPrivateKey takes a bec private key and returns a Bitcoin address 91 | func GetAddressFromPrivateKey(privateKey *bec.PrivateKey, compressed bool) (string, error) { 92 | address, err := GetAddressFromPubKey(privateKey.PubKey(), compressed) 93 | if err != nil { 94 | return "", err 95 | } 96 | return address.AddressString, nil 97 | } 98 | 99 | // GetAddressFromPrivateKeyString takes a private key string and returns a Bitcoin address 100 | func GetAddressFromPrivateKeyString(privateKey string, compressed bool) (string, error) { 101 | rawKey, err := PrivateKeyFromString(privateKey) 102 | if err != nil { 103 | return "", err 104 | } 105 | var address *bscript.Address 106 | if address, err = GetAddressFromPubKey(rawKey.PubKey(), compressed); err != nil { 107 | return "", err 108 | } 109 | return address.AddressString, nil 110 | } 111 | 112 | // GetAddressFromPubKey gets a bscript.Address from a bec.PublicKey 113 | func GetAddressFromPubKey(publicKey *bec.PublicKey, compressed bool) (*bscript.Address, error) { 114 | if publicKey == nil { 115 | return nil, fmt.Errorf("publicKey cannot be nil") 116 | } else if publicKey.X == nil { 117 | return nil, fmt.Errorf("publicKey.X cannot be nil") 118 | } 119 | 120 | if !compressed { 121 | // go-bt/v2/bscript does not have a function that exports the uncompressed address 122 | // https://github.com/libsv/go-bt/blob/master/bscript/address.go#L98 123 | hash := crypto.Hash160(publicKey.SerialiseUncompressed()) 124 | bb := make([]byte, 1) 125 | //nolint: makezero // we need to set up the array with 1 126 | bb = append(bb, hash...) 127 | return &bscript.Address{ 128 | AddressString: bscript.Base58EncodeMissingChecksum(bb), 129 | PublicKeyHash: hex.EncodeToString(hash), 130 | }, nil 131 | } 132 | 133 | return bscript.NewAddressFromPublicKey(publicKey, true) 134 | } 135 | 136 | // GetAddressFromPubKeyString is a convenience function to use a hex string pubKey 137 | func GetAddressFromPubKeyString(pubKey string, compressed bool) (*bscript.Address, error) { 138 | rawPubKey, err := PubKeyFromString(pubKey) 139 | if err != nil { 140 | return nil, err 141 | } 142 | return GetAddressFromPubKey(rawPubKey, compressed) 143 | } 144 | 145 | // GetAddressFromScript will take an output script and extract a standard bitcoin address 146 | func GetAddressFromScript(script string) (string, error) { 147 | 148 | // No script? 149 | if len(script) == 0 { 150 | return "", ErrMissingScript 151 | } 152 | 153 | // Decode the hex string into bytes 154 | scriptBytes, err := hex.DecodeString(script) 155 | if err != nil { 156 | return "", err 157 | } 158 | 159 | // Extract the addresses from the script 160 | bScript := bscript.NewFromBytes(scriptBytes) 161 | var addresses []string 162 | addresses, err = bScript.Addresses() 163 | if err != nil { 164 | return "", err 165 | } 166 | 167 | // Missing an address? 168 | if len(addresses) == 0 { 169 | // This error case should not occur since the error above will occur when no address is found, 170 | // however we ensure that we have an address for the NewLegacyAddressPubKeyHash() below 171 | return "", fmt.Errorf("invalid output script, missing an address") 172 | } 173 | 174 | // Use the encoded version of the address 175 | return addresses[0], nil 176 | } 177 | -------------------------------------------------------------------------------- /address_test.go: -------------------------------------------------------------------------------- 1 | package bitcoin 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/libsv/go-bk/bec" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | // TestValidA58 will test the method ValidA58() 12 | func TestValidA58(t *testing.T) { 13 | 14 | t.Parallel() 15 | 16 | var tests = []struct { 17 | input string 18 | expectedValid bool 19 | expectedError bool 20 | }{ 21 | {"1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi2", true, false}, 22 | {"1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi", false, false}, 23 | {"1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi", false, true}, 24 | {"1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi", false, true}, 25 | {"1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi", false, true}, 26 | {"1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi", false, true}, 27 | {"1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi", false, true}, 28 | {"1KCEAmV", false, false}, 29 | {"", false, false}, 30 | {"0", false, true}, 31 | } 32 | 33 | for _, test := range tests { 34 | if valid, err := ValidA58([]byte(test.input)); err != nil && !test.expectedError { 35 | t.Fatalf("%s Failed: [%s] inputted and error not expected but got: %s", t.Name(), test.input, err.Error()) 36 | } else if err == nil && test.expectedError { 37 | t.Fatalf("%s Failed: [%s] inputted and error was expected", t.Name(), test.input) 38 | } else if valid && !test.expectedValid { 39 | t.Fatalf("%s Failed: [%s] inputted and was valid but should NOT be valid", t.Name(), test.input) 40 | } else if !valid && test.expectedValid { 41 | t.Fatalf("%s Failed: [%s] inputted and was invalid but should be valid", t.Name(), test.input) 42 | } 43 | } 44 | } 45 | 46 | // ExampleValidA58 example using ValidA58() 47 | func ExampleValidA58() { 48 | valid, err := ValidA58([]byte("1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi2")) 49 | if err != nil { 50 | fmt.Printf("error occurred: %s", err.Error()) 51 | return 52 | } else if !valid { 53 | fmt.Printf("address is not valid: %s", "1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi2") 54 | return 55 | } else { 56 | fmt.Printf("address is valid!") 57 | } 58 | // Output:address is valid! 59 | } 60 | 61 | // BenchmarkValidA58 benchmarks the method ValidA58() 62 | func BenchmarkValidA58(b *testing.B) { 63 | for i := 0; i < b.N; i++ { 64 | _, _ = ValidA58([]byte("1KCEAmVS6FFggtc7W9as7sEENvjt7DqMi2")) 65 | } 66 | } 67 | 68 | // TestGetAddressFromPrivateKey will test the method GetAddressFromPrivateKey() 69 | func TestGetAddressFromPrivateKey(t *testing.T) { 70 | t.Parallel() 71 | 72 | var tests = []struct { 73 | input string 74 | expectedAddress string 75 | compressed bool 76 | expectedError bool 77 | }{ 78 | {"0", "", true, true}, 79 | {"00000", "", true, true}, 80 | {"12345678", "1BHxe5Yw72oYoV8tFjySYrV9Y2JwMpAZEy", true, false}, 81 | {"54035dd4c7dda99ac473905a3d82", "1L5GmmuGeS3HwoEDv7zkWcheayXrRsurUm", true, false}, 82 | {"54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9", "13dnka5SaugRchayN84EED7a2E8dCNMLXQ", true, false}, 83 | {"54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd", "1DfGxKmgL3ETwUdNnXLBueEvNpjcDGcKgK", true, false}, 84 | } 85 | 86 | for _, test := range tests { 87 | if address, err := GetAddressFromPrivateKeyString(test.input, test.compressed); err != nil && !test.expectedError { 88 | t.Fatalf("%s Failed: [%s] inputted and error not expected but got: %s", t.Name(), test.input, err.Error()) 89 | } else if err == nil && test.expectedError { 90 | t.Fatalf("%s Failed: [%s] inputted and error was expected", t.Name(), test.input) 91 | } else if address != test.expectedAddress { 92 | t.Fatalf("%s Failed: [%s] inputted and [%s] expected, but got: %s", t.Name(), test.input, test.expectedAddress, address) 93 | } 94 | } 95 | } 96 | 97 | // TestGetAddressFromPrivateKeyCompression will test the method GetAddressFromPrivateKey() 98 | func TestGetAddressFromPrivateKeyCompression(t *testing.T) { 99 | 100 | privateKey, err := bec.NewPrivateKey(bec.S256()) 101 | assert.NoError(t, err) 102 | 103 | var addressUncompressed string 104 | addressUncompressed, err = GetAddressFromPrivateKey(privateKey, false) 105 | assert.NoError(t, err) 106 | 107 | var addressCompressed string 108 | addressCompressed, err = GetAddressFromPrivateKey(privateKey, true) 109 | assert.NoError(t, err) 110 | 111 | assert.NotEqual(t, addressCompressed, addressUncompressed) 112 | 113 | addressCompressed, err = GetAddressFromPrivateKey(&bec.PrivateKey{}, true) 114 | assert.Error(t, err) 115 | assert.Equal(t, "", addressCompressed) 116 | } 117 | 118 | // ExampleGetAddressFromPrivateKey example using GetAddressFromPrivateKey() 119 | func ExampleGetAddressFromPrivateKey() { 120 | address, err := GetAddressFromPrivateKeyString("54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd", true) 121 | if err != nil { 122 | fmt.Printf("error occurred: %s", err.Error()) 123 | return 124 | } 125 | fmt.Printf("address found: %s", address) 126 | // Output:address found: 1DfGxKmgL3ETwUdNnXLBueEvNpjcDGcKgK 127 | } 128 | 129 | // BenchmarkGetAddressFromPrivateKey benchmarks the method GetAddressFromPrivateKey() 130 | func BenchmarkGetAddressFromPrivateKey(b *testing.B) { 131 | key, _ := CreatePrivateKeyString() 132 | for i := 0; i < b.N; i++ { 133 | _, _ = GetAddressFromPrivateKeyString(key, true) 134 | } 135 | } 136 | 137 | // testGetPublicKeyFromPrivateKey is a helper method for tests 138 | func testGetPublicKeyFromPrivateKey(privateKey string) *bec.PublicKey { 139 | rawKey, err := PrivateKeyFromString(privateKey) 140 | if err != nil { 141 | return nil 142 | } 143 | return rawKey.PubKey() 144 | } 145 | 146 | // TestGetAddressFromPubKey will test the method GetAddressFromPubKey() 147 | func TestGetAddressFromPubKey(t *testing.T) { 148 | t.Parallel() 149 | 150 | var tests = []struct { 151 | input *bec.PublicKey 152 | expectedAddress string 153 | expectedNil bool 154 | expectedError bool 155 | }{ 156 | {&bec.PublicKey{}, "", true, true}, 157 | {testGetPublicKeyFromPrivateKey("54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd"), "1DfGxKmgL3ETwUdNnXLBueEvNpjcDGcKgK", false, false}, 158 | {testGetPublicKeyFromPrivateKey("000000"), "15wJjXvfQzo3SXqoWGbWZmNYND1Si4siqV", false, false}, 159 | {testGetPublicKeyFromPrivateKey("0"), "15wJjXvfQzo3SXqoWGbWZmNYND1Si4siqV", true, true}, 160 | } 161 | 162 | // todo: add more error cases of invalid *bec.PublicKey 163 | 164 | for _, test := range tests { 165 | if rawKey, err := GetAddressFromPubKey(test.input, true); err != nil && !test.expectedError { 166 | t.Fatalf("%s Failed: [%v] inputted and error not expected but got: %s", t.Name(), test.input, err.Error()) 167 | } else if err == nil && test.expectedError { 168 | t.Fatalf("%s Failed: [%v] inputted and error was expected", t.Name(), test.input) 169 | } else if rawKey == nil && !test.expectedNil { 170 | t.Fatalf("%s Failed: [%v] inputted and was nil but not expected", t.Name(), test.input) 171 | } else if rawKey != nil && test.expectedNil { 172 | t.Fatalf("%s Failed: [%v] inputted and was NOT nil but expected to be nil", t.Name(), test.input) 173 | } else if rawKey != nil && rawKey.AddressString != test.expectedAddress { 174 | t.Fatalf("%s Failed: [%v] inputted [%s] expected but failed comparison of addresses, got: %s", t.Name(), test.input, test.expectedAddress, rawKey.AddressString) 175 | } 176 | } 177 | } 178 | 179 | // ExampleGetAddressFromPubKey example using GetAddressFromPubKey() 180 | func ExampleGetAddressFromPubKey() { 181 | rawAddress, err := GetAddressFromPubKey(testGetPublicKeyFromPrivateKey("54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd"), true) 182 | if err != nil { 183 | fmt.Printf("error occurred: %s", err.Error()) 184 | return 185 | } 186 | fmt.Printf("address found: %s", rawAddress.AddressString) 187 | // Output:address found: 1DfGxKmgL3ETwUdNnXLBueEvNpjcDGcKgK 188 | } 189 | 190 | // BenchmarkGetAddressFromPubKey benchmarks the method GetAddressFromPubKey() 191 | func BenchmarkGetAddressFromPubKey(b *testing.B) { 192 | pubKey := testGetPublicKeyFromPrivateKey("54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd") 193 | for i := 0; i < b.N; i++ { 194 | _, _ = GetAddressFromPubKey(pubKey, true) 195 | } 196 | } 197 | 198 | // TestGetAddressFromScript will test the method GetAddressFromScript() 199 | func TestGetAddressFromScript(t *testing.T) { 200 | t.Parallel() 201 | 202 | var tests = []struct { 203 | inputScript string 204 | expectedAddress string 205 | expectedError bool 206 | }{ 207 | {"", "", true}, 208 | {"0", "", true}, 209 | {"76a9141a9d62736746f85ca872dc555ff51b1fed2471e288ac", "13Rj7G3pn2GgG8KE6SFXLc7dCJdLNnNK7M", false}, 210 | {"76a914b424110292f4ea2ac92beb9e83cf5e6f0fa2996388ac", "1HRVqUGDzpZSMVuNSZxJVaB9xjneEShfA7", false}, 211 | {"76a914b424110292f4ea2ac92beb9e83cf5e6f0fa2", "", true}, 212 | {"76a914b424110292f4ea2ac92beb9e83", "", true}, 213 | {"76a914b424110292f", "", true}, 214 | {"1HRVqUGDzpZSMVuNSZxJVaB9xjneEShfA7", "", true}, 215 | {"514104cc71eb30d653c0c3163990c47b976f3fb3f37cccdcbedb169a1dfef58bbfbfaff7d8a473e7e2e6d317b87bafe8bde97e3cf8f065dec022b51d11fcdd0d348ac4410461cbdcc5409fb4b4d42b51d33381354d80e550078cb532a34bfa2fcfdeb7d76519aecc62770f5b0e4ef8551946d8a540911abe3e7854a26f39f58b25c15342af52ae", "", true}, 216 | {"410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3", "", true}, 217 | {"47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901", "", true}, 218 | } 219 | 220 | for _, test := range tests { 221 | if address, err := GetAddressFromScript(test.inputScript); err != nil && !test.expectedError { 222 | t.Fatalf("%s Failed: [%v] inputted and error not expected but got: %s", t.Name(), test.inputScript, err.Error()) 223 | } else if err == nil && test.expectedError { 224 | t.Fatalf("%s Failed: [%v] inputted and error was expected", t.Name(), test.inputScript) 225 | } else if address != test.expectedAddress { 226 | t.Fatalf("%s Failed: [%v] inputted [%s] expected but failed comparison of addresses, got: %s", t.Name(), test.inputScript, test.expectedAddress, address) 227 | } 228 | } 229 | } 230 | 231 | // ExampleGetAddressFromScript example using GetAddressFromScript() 232 | func ExampleGetAddressFromScript() { 233 | address, err := GetAddressFromScript("76a914b424110292f4ea2ac92beb9e83cf5e6f0fa2996388ac") 234 | if err != nil { 235 | fmt.Printf("error occurred: %s", err.Error()) 236 | return 237 | } 238 | fmt.Printf("address found: %s", address) 239 | // Output:address found: 1HRVqUGDzpZSMVuNSZxJVaB9xjneEShfA7 240 | } 241 | 242 | // BenchmarkAddressFromScript benchmarks the method GetAddressFromScript() 243 | func BenchmarkGetAddressFromScript(b *testing.B) { 244 | for i := 0; i < b.N; i++ { 245 | _, _ = GetAddressFromScript("76a914b424110292f4ea2ac92beb9e83cf5e6f0fa2996388ac") 246 | } 247 | } 248 | 249 | // TestGetAddressFromPubKeyString will test the method GetAddressFromPubKeyString() 250 | func TestGetAddressFromPubKeyString(t *testing.T) { 251 | t.Parallel() 252 | 253 | var tests = []struct { 254 | input string 255 | expectedAddress string 256 | expectedNil bool 257 | expectedError bool 258 | }{ 259 | {"", "", true, true}, 260 | {"0", "", true, true}, 261 | {"03ce8a73eb5e4d45966d719ac3ceb431cd0ee203e6395357a167b9abebc4baeacf", "17HeHWVDqDqexLJ31aG4qtVMoX8pKMGSuJ", false, false}, 262 | {"0000", "", true, true}, 263 | } 264 | 265 | for _, test := range tests { 266 | if rawKey, err := GetAddressFromPubKeyString(test.input, true); err != nil && !test.expectedError { 267 | t.Fatalf("%s Failed: [%v] inputted and error not expected but got: %s", t.Name(), test.input, err.Error()) 268 | } else if err == nil && test.expectedError { 269 | t.Fatalf("%s Failed: [%v] inputted and error was expected", t.Name(), test.input) 270 | } else if rawKey == nil && !test.expectedNil { 271 | t.Fatalf("%s Failed: [%v] inputted and was nil but not expected", t.Name(), test.input) 272 | } else if rawKey != nil && test.expectedNil { 273 | t.Fatalf("%s Failed: [%v] inputted and was NOT nil but expected to be nil", t.Name(), test.input) 274 | } else if rawKey != nil && rawKey.AddressString != test.expectedAddress { 275 | t.Fatalf("%s Failed: [%v] inputted [%s] expected but failed comparison of addresses, got: %s", t.Name(), test.input, test.expectedAddress, rawKey.AddressString) 276 | } 277 | } 278 | } 279 | 280 | // ExampleGetAddressFromPubKeyString example using GetAddressFromPubKeyString() 281 | func ExampleGetAddressFromPubKeyString() { 282 | rawAddress, err := GetAddressFromPubKeyString("03ce8a73eb5e4d45966d719ac3ceb431cd0ee203e6395357a167b9abebc4baeacf", true) 283 | if err != nil { 284 | fmt.Printf("error occurred: %s", err.Error()) 285 | return 286 | } 287 | fmt.Printf("address found: %s", rawAddress.AddressString) 288 | // Output:address found: 17HeHWVDqDqexLJ31aG4qtVMoX8pKMGSuJ 289 | } 290 | 291 | // BenchmarkGetAddressFromPubKeyString benchmarks the method GetAddressFromPubKeyString() 292 | func BenchmarkGetAddressFromPubKeyString(b *testing.B) { 293 | for i := 0; i < b.N; i++ { 294 | _, _ = GetAddressFromPubKeyString("03ce8a73eb5e4d45966d719ac3ceb431cd0ee203e6395357a167b9abebc4baeacf", true) 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /bitcoin.go: -------------------------------------------------------------------------------- 1 | // Package bitcoin is a small collection of utility functions for working with Bitcoin (BSV) 2 | // 3 | // If you have any suggestions or comments, please feel free to open an issue on 4 | // this GitHub repository! 5 | // 6 | // By BitcoinSchema Organization (https://bitcoinschema.org) 7 | package bitcoin 8 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # Reference: https://docs.codecov.com/docs/codecovyml-reference 2 | # ---------------------- 3 | codecov: 4 | require_ci_to_pass: true 5 | 6 | # Coverage configuration 7 | # ---------------------- 8 | coverage: 9 | status: 10 | patch: false 11 | range: 70..90 # First number represents red, and second represents green 12 | # (default is 70..100) 13 | round: down # up, down, or nearest 14 | precision: 2 # Number of decimal places, between 0 and 5 15 | 16 | # Ignoring Paths 17 | # -------------- 18 | # which folders/files to ignore 19 | ignore: 20 | - "*/.make/.*" 21 | - "*/.github/.*" 22 | - "*/examples/.*" 23 | 24 | # Parsers 25 | # -------------- 26 | parsers: 27 | gcov: 28 | branch_detection: 29 | conditional: yes 30 | loop: yes 31 | method: no 32 | macro: no 33 | 34 | # Pull request comments: 35 | # ---------------------- 36 | # Diff is the Coverage Diff of the pull request. 37 | # Files are the files impacted by the pull request 38 | comment: 39 | layout: "reach,diff,flags,files,footer" 40 | behavior: default 41 | require_changes: false -------------------------------------------------------------------------------- /encryption.go: -------------------------------------------------------------------------------- 1 | package bitcoin 2 | 3 | import ( 4 | "encoding/hex" 5 | 6 | "github.com/libsv/go-bk/bec" 7 | ) 8 | 9 | // EncryptWithPrivateKey will encrypt the data using a given private key 10 | func EncryptWithPrivateKey(privateKey *bec.PrivateKey, data string) (string, error) { 11 | 12 | // Encrypt using bec 13 | encryptedData, err := bec.Encrypt(privateKey.PubKey(), []byte(data)) 14 | if err != nil { 15 | return "", err 16 | } 17 | 18 | // Return the hex encoded value 19 | return hex.EncodeToString(encryptedData), nil 20 | } 21 | 22 | // DecryptWithPrivateKey is a wrapper to decrypt the previously encrypted 23 | // information, given a corresponding private key 24 | func DecryptWithPrivateKey(privateKey *bec.PrivateKey, data string) (string, error) { 25 | 26 | // Decode the hex encoded string 27 | rawData, err := hex.DecodeString(data) 28 | if err != nil { 29 | return "", err 30 | } 31 | 32 | // Decrypt the data 33 | var decrypted []byte 34 | if decrypted, err = bec.Decrypt(privateKey, rawData); err != nil { 35 | return "", err 36 | } 37 | return string(decrypted), nil 38 | } 39 | 40 | // EncryptWithPrivateKeyString is a convenience wrapper for EncryptWithPrivateKey() 41 | func EncryptWithPrivateKeyString(privateKey, data string) (string, error) { 42 | 43 | // Get the private key from string 44 | rawPrivateKey, err := PrivateKeyFromString(privateKey) 45 | if err != nil { 46 | return "", err 47 | } 48 | 49 | // Encrypt using bec 50 | return EncryptWithPrivateKey(rawPrivateKey, data) 51 | } 52 | 53 | // DecryptWithPrivateKeyString is a convenience wrapper for DecryptWithPrivateKey() 54 | func DecryptWithPrivateKeyString(privateKey, data string) (string, error) { 55 | 56 | // Get private key 57 | rawPrivateKey, _, err := PrivateAndPublicKeys(privateKey) 58 | if err != nil { 59 | return "", err 60 | } 61 | 62 | // Decrypt 63 | return DecryptWithPrivateKey(rawPrivateKey, data) 64 | } 65 | 66 | // EncryptShared will encrypt data and provide shared keys for decryption 67 | func EncryptShared(user1PrivateKey *bec.PrivateKey, user2PubKey *bec.PublicKey, data []byte) ( 68 | *bec.PrivateKey, *bec.PublicKey, []byte, error) { 69 | 70 | // Generate shared keys that can be decrypted by either user 71 | sharedPrivKey, sharedPubKey := GenerateSharedKeyPair(user1PrivateKey, user2PubKey) 72 | 73 | // Encrypt data with shared key 74 | encryptedData, err := bec.Encrypt(sharedPubKey, data) 75 | return sharedPrivKey, sharedPubKey, encryptedData, err 76 | } 77 | 78 | // EncryptSharedString will encrypt a string to a hex encoded encrypted payload, and provide shared keys for decryption 79 | func EncryptSharedString(user1PrivateKey *bec.PrivateKey, user2PubKey *bec.PublicKey, data string) ( 80 | *bec.PrivateKey, *bec.PublicKey, string, error) { 81 | 82 | // Generate shared keys that can be decrypted by either user 83 | sharedPrivKey, sharedPubKey := GenerateSharedKeyPair(user1PrivateKey, user2PubKey) 84 | 85 | // Encrypt data with shared key 86 | encryptedData, err := bec.Encrypt(sharedPubKey, []byte(data)) 87 | 88 | return sharedPrivKey, sharedPubKey, hex.EncodeToString(encryptedData), err 89 | } 90 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package bitcoin 2 | 3 | import "errors" 4 | 5 | // ErrPrivateKeyMissing is returned when a private key is missing 6 | var ErrPrivateKeyMissing = errors.New("private key is missing") 7 | 8 | // ErrWifMissing is returned when a wif is missing 9 | var ErrWifMissing = errors.New("wif is missing") 10 | 11 | // ErrBadCharacter is returned when a bad character is found 12 | var ErrBadCharacter = errors.New("bad char") 13 | 14 | // ErrTooLong is returned when a string is too long 15 | var ErrTooLong = errors.New("too long") 16 | 17 | // ErrNotVersion0 is returned when a string is not version 0 18 | var ErrNotVersion0 = errors.New("not version 0") 19 | 20 | // ErrMissingScript is returned when a script is missing 21 | var ErrMissingScript = errors.New("missing script") 22 | 23 | // ErrMissingPubKey is returned when a pubkey is missing 24 | var ErrMissingPubKey = errors.New("missing pubkey") 25 | 26 | // ErrMissingAddress is returned when an address is missing 27 | var ErrMissingAddress = errors.New("missing address") 28 | 29 | // ErrUtxosRequired is returned when utxos are required to create a tx 30 | var ErrUtxosRequired = errors.New("utxo(s) are required to create a tx") 31 | 32 | // ErrChangeAddressRequired is returned when a change address is required to create a tx 33 | var ErrChangeAddressRequired = errors.New("change address is required") 34 | -------------------------------------------------------------------------------- /examples/address_from_private_key/address_from_private_key.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/bitcoinschema/go-bitcoin/v2" 7 | ) 8 | 9 | func main() { 10 | 11 | // Start with a private key (we will make one for this example) 12 | privateKey, err := bitcoin.CreatePrivateKey() 13 | if err != nil { 14 | log.Fatalf("error occurred: %s", err.Error()) 15 | } 16 | 17 | // Get an address 18 | var address string 19 | if address, err = bitcoin.GetAddressFromPrivateKey(privateKey, true); err != nil { 20 | log.Fatalf("error occurred: %s", err.Error()) 21 | } 22 | 23 | // Success! 24 | log.Printf("found address: %s from private key: %s", address, privateKey) 25 | } 26 | -------------------------------------------------------------------------------- /examples/address_from_wif/address_from_wif.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/bitcoinschema/go-bitcoin/v2" 7 | ) 8 | 9 | func main() { 10 | 11 | // Convert the wif into a private key 12 | privateKey, err := bitcoin.WifToPrivateKey("5KgHn2qiftW5LQgCYFtkbrLYB1FuvisDtacax8NCvumw3UTKdcP") 13 | if err != nil { 14 | log.Fatalf("error occurred: %s", err.Error()) 15 | } 16 | 17 | // Get an address 18 | var address string 19 | if address, err = bitcoin.GetAddressFromPrivateKey(privateKey, true); err != nil { 20 | log.Fatalf("error occurred: %s", err.Error()) 21 | } 22 | 23 | // Success! 24 | log.Printf("found address: %s from private key: %s", address, privateKey) 25 | } 26 | -------------------------------------------------------------------------------- /examples/calculate_fee_for_tx/calculate_fee_for_tx.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/bitcoinschema/go-bitcoin/v2" 7 | ) 8 | 9 | func main() { 10 | 11 | // Get the tx from hex string 12 | rawTx := "0100000001760595866e99c1ce920197844740f5598b34763878696371d41b3a7c0a65b0b7000000006b483045022100e07b7661af4e4b521c012a146b25da2c7b9d606e9ceaae28fa73eb347ef6da6f0220527f0638a89ff11cbe53d5f8c4c2962484a370dcd9463a6330f45d31247c2512412102ea87d1fd77d169bd56a71e700628113d0f8dfe57faa0ba0e55a36f9ce8e10be3ffffffff0364030000000000001976a9147a1980655efbfec416b2b0c663a7b3ac0b6a25d288ac00000000000000001a006a07707265666978310c6578616d706c65206461746102133700000000000000001c006a0770726566697832116d6f7265206578616d706c65206461746100000000" 13 | tx, err := bitcoin.TxFromHex(rawTx) 14 | if err != nil { 15 | log.Fatalf("error occurred: %s", err.Error()) 16 | } 17 | 18 | // Calculate the fee using default rates (you can replace with MinerAPI rates) 19 | estimatedFee := bitcoin.CalculateFeeForTx(tx, nil, nil) 20 | 21 | // Success! 22 | log.Printf("tx id: %s estimated fee: %d satoshis", tx.TxID(), estimatedFee) 23 | } 24 | -------------------------------------------------------------------------------- /examples/create_pubkey/create_pubkey.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/bitcoinschema/go-bitcoin/v2" 7 | ) 8 | 9 | func main() { 10 | 11 | // Start with a private key (we will make one for this example) 12 | privateKey, err := bitcoin.CreatePrivateKeyString() 13 | if err != nil { 14 | log.Fatalf("error occurred: %s", err.Error()) 15 | } 16 | 17 | // Create a pubkey 18 | var pubKey string 19 | if pubKey, err = bitcoin.PubKeyFromPrivateKeyString(privateKey, true); err != nil { 20 | log.Fatalf("error occurred: %s", err.Error()) 21 | } 22 | 23 | // Success! 24 | log.Printf("created pubkey: %s from private key: %s", pubKey, privateKey) 25 | } 26 | -------------------------------------------------------------------------------- /examples/create_tx/create_tx.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/bitcoinschema/go-bitcoin/v2" 7 | "github.com/libsv/go-bt/v2" 8 | ) 9 | 10 | func main() { 11 | 12 | // Use a new UTXO 13 | utxo := &bitcoin.Utxo{ 14 | TxID: "b7b0650a7c3a1bd4716369783876348b59f5404784970192cec1996e86950576", 15 | Vout: 0, 16 | ScriptPubKey: "76a9149cbe9f5e72fa286ac8a38052d1d5337aa363ea7f88ac", 17 | Satoshis: 1000, 18 | } 19 | 20 | // Add a pay-to address 21 | payTo := &bitcoin.PayToAddress{ 22 | Address: "1C8bzHM8XFBHZ2ZZVvFy2NSoAZbwCXAicL", 23 | Satoshis: 500, 24 | } 25 | 26 | // Add some op return data 27 | opReturn1 := bitcoin.OpReturnData{[]byte("prefix1"), []byte("example data"), []byte{0x13, 0x37}} 28 | opReturn2 := bitcoin.OpReturnData{[]byte("prefix2"), []byte("more example data")} 29 | 30 | // Use a private key 31 | privateKey, err := bitcoin.PrivateKeyFromString("54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd") 32 | if err != nil { 33 | log.Printf("error occurred: %s", err.Error()) 34 | return 35 | } 36 | 37 | // Generate the TX 38 | var rawTx *bt.Tx 39 | rawTx, err = bitcoin.CreateTx( 40 | []*bitcoin.Utxo{utxo}, 41 | []*bitcoin.PayToAddress{payTo}, 42 | []bitcoin.OpReturnData{opReturn1, opReturn2}, 43 | privateKey, 44 | ) 45 | if err != nil { 46 | log.Printf("error occurred: %s", err.Error()) 47 | return 48 | } 49 | 50 | // Success! 51 | log.Printf("rawTx: %s", rawTx.String()) 52 | log.Printf("tx_id: %s", rawTx.TxID()) 53 | } 54 | -------------------------------------------------------------------------------- /examples/create_tx_using_wif/create_tx_using_wif.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/bitcoinschema/go-bitcoin/v2" 7 | ) 8 | 9 | func main() { 10 | 11 | // Use a new UTXO 12 | utxo := &bitcoin.Utxo{ 13 | TxID: "b7b0650a7c3a1bd4716369783876348b59f5404784970192cec1996e86950576", 14 | Vout: 0, 15 | ScriptPubKey: "76a9149cbe9f5e72fa286ac8a38052d1d5337aa363ea7f88ac", 16 | Satoshis: 1000, 17 | } 18 | 19 | // Add a pay-to address 20 | payTo := &bitcoin.PayToAddress{ 21 | Address: "1C8bzHM8XFBHZ2ZZVvFy2NSoAZbwCXAicL", 22 | Satoshis: 500, 23 | } 24 | 25 | // Add some op return data 26 | opReturn1 := bitcoin.OpReturnData{[]byte("prefix1"), []byte("example data"), []byte{0x13, 0x37}} 27 | opReturn2 := bitcoin.OpReturnData{[]byte("prefix2"), []byte("more example data")} 28 | 29 | // Generate the TX 30 | rawTx, err := bitcoin.CreateTxUsingWif( 31 | []*bitcoin.Utxo{utxo}, 32 | []*bitcoin.PayToAddress{payTo}, 33 | []bitcoin.OpReturnData{opReturn1, opReturn2}, 34 | "L3VJH2hcRGYYG6YrbWGmsxQC1zyYixA82YjgEyrEUWDs4ALgk8Vu", 35 | ) 36 | if err != nil { 37 | log.Printf("error occurred: %s", err.Error()) 38 | return 39 | } 40 | 41 | // Success! 42 | log.Printf("rawTx: %s", rawTx.String()) 43 | } 44 | -------------------------------------------------------------------------------- /examples/create_tx_with_change/create_tx_with_change.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/bitcoinschema/go-bitcoin/v2" 7 | ) 8 | 9 | func main() { 10 | 11 | // Use a new UTXO 12 | utxo := &bitcoin.Utxo{ 13 | TxID: "b7b0650a7c3a1bd4716369783876348b59f5404784970192cec1996e86950576", 14 | Vout: 0, 15 | ScriptPubKey: "76a9149cbe9f5e72fa286ac8a38052d1d5337aa363ea7f88ac", 16 | Satoshis: 1000, 17 | } 18 | 19 | // Add a pay-to address 20 | payTo := &bitcoin.PayToAddress{ 21 | Address: "1C8bzHM8XFBHZ2ZZVvFy2NSoAZbwCXAicL", 22 | Satoshis: 500, 23 | } 24 | 25 | // Add some op return data 26 | opReturn1 := bitcoin.OpReturnData{[]byte("prefix1"), []byte("example data"), []byte{0x13, 0x37}} 27 | opReturn2 := bitcoin.OpReturnData{[]byte("prefix2"), []byte("more example data")} 28 | 29 | // Generate the TX (use a WIF) 30 | rawTx, err := bitcoin.CreateTxWithChangeUsingWif( 31 | []*bitcoin.Utxo{utxo}, 32 | []*bitcoin.PayToAddress{payTo}, 33 | []bitcoin.OpReturnData{opReturn1, opReturn2}, 34 | "1KQG5AY9GrPt3b5xrFqVh2C3YEhzSdu4kc", 35 | nil, 36 | nil, 37 | "L3VJH2hcRGYYG6YrbWGmsxQC1zyYixA82YjgEyrEUWDs4ALgk8Vu", 38 | ) 39 | if err != nil { 40 | log.Printf("error occurred: %s", err.Error()) 41 | return 42 | } 43 | 44 | // Success! 45 | log.Printf("rawTx: %s", rawTx.String()) 46 | } 47 | -------------------------------------------------------------------------------- /examples/create_wif/create_wif.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/bitcoinschema/go-bitcoin/v2" 7 | ) 8 | 9 | func main() { 10 | 11 | // Create a wif 12 | wifString, err := bitcoin.CreateWifString() 13 | if err != nil { 14 | log.Fatalf("error occurred: %s", err.Error()) 15 | } 16 | 17 | // Success! 18 | log.Printf("wif key: %s", wifString) 19 | } 20 | -------------------------------------------------------------------------------- /examples/decrypt_with_private_key/decrypt_with_private_key.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/bitcoinschema/go-bitcoin/v2" 7 | ) 8 | 9 | func main() { 10 | 11 | // Start with a private key (keep this a secret) 12 | privateKey, err := bitcoin.PrivateKeyFromString("b7a1f94ac7be8ed369421c3afe4eae548f10b96435e9c94e35590b85404a5ae4") 13 | if err != nil { 14 | log.Fatalf("error occurred: %s", err.Error()) 15 | } 16 | 17 | encryptedData := "00443645948b35d031859c200cd5c73e02ca0020985b837a7c1659660924ded90b0afb94c0de0b77a602fda965d5de3d94677e9200208593a7d1f7cc64023403b90c4562c0cb1cec5cb2849e7fbc5b1fe01b8570f1663bc4bca2e548981e355fb252168b48bc3c7a302a6da2c4d06e8f4900685b7bf9c9530b2b3b7f486d78d43eab21284545" 18 | 19 | // Encrypted data 20 | log.Println("encrypted: ", encryptedData) 21 | 22 | // Decrypt the data 23 | var decrypted string 24 | decrypted, err = bitcoin.DecryptWithPrivateKey(privateKey, encryptedData) 25 | if err != nil { 26 | log.Fatalf("error occurred: %s", err.Error()) 27 | } 28 | log.Println("decrypted: ", decrypted) 29 | 30 | } 31 | -------------------------------------------------------------------------------- /examples/encrypt_shared_keys/encrypt_shared_keys.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/hex" 5 | "log" 6 | 7 | "github.com/bitcoinschema/go-bitcoin/v2" 8 | "github.com/libsv/go-bk/bec" 9 | ) 10 | 11 | func main() { 12 | 13 | // This data will be encrypted / shared 14 | testString := "testing 1, 2, 3..." 15 | 16 | // User 1's private key 17 | privKey1, _ := bitcoin.CreatePrivateKey() 18 | 19 | // User 2's private key 20 | privKey2, _ := bitcoin.CreatePrivateKey() 21 | 22 | // User 1 encrypts using their private key and user 2's pubkey 23 | _, _, encryptedData, err := bitcoin.EncryptShared(privKey1, privKey2.PubKey(), []byte(testString)) 24 | if err != nil { 25 | log.Fatalf("failed to encrypt data for sharing %s", err) 26 | } 27 | 28 | // Generate the shared key 29 | user2SharedPrivKey, _ := bitcoin.GenerateSharedKeyPair(privKey2, privKey1.PubKey()) 30 | 31 | // User 2 can decrypt using the shared private key 32 | var decryptedTestData []byte 33 | decryptedTestData, err = bec.Decrypt(user2SharedPrivKey, encryptedData) 34 | if err != nil { 35 | log.Fatalf("failed to decrypt test data %s", err) 36 | } 37 | 38 | // Success 39 | log.Printf("test string: %s", testString) 40 | log.Printf("encrypted: %s", hex.EncodeToString(encryptedData)) 41 | log.Printf("decrypted: %s", decryptedTestData) 42 | } 43 | -------------------------------------------------------------------------------- /examples/encrypt_with_private_key/encrypt_with_private_key.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/hex" 5 | "log" 6 | 7 | "github.com/bitcoinschema/go-bitcoin/v2" 8 | ) 9 | 10 | func main() { 11 | 12 | // Start with a private key (keep this a secret) 13 | privateKey, err := bitcoin.CreatePrivateKey() 14 | if err != nil { 15 | log.Fatalf("error occurred: %s", err.Error()) 16 | } 17 | log.Println("private key (used for encryption): ", hex.EncodeToString(privateKey.Serialise())) 18 | 19 | // Encrypt 20 | var data string 21 | if data, err = bitcoin.EncryptWithPrivateKey(privateKey, `{"some":"data"}`); err != nil { 22 | log.Fatalf("error occurred: %s", err.Error()) 23 | } 24 | 25 | // Encrypted data 26 | log.Println("encrypted: ", data) 27 | 28 | // Decrypt the data 29 | var decrypted string 30 | decrypted, err = bitcoin.DecryptWithPrivateKey(privateKey, data) 31 | if err != nil { 32 | log.Fatalf("error occurred: %s", err.Error()) 33 | } 34 | log.Println("decrypted: ", decrypted) 35 | 36 | } 37 | -------------------------------------------------------------------------------- /examples/generate_hd_key/generate_hd_key.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/bitcoinschema/go-bitcoin/v2" 7 | ) 8 | 9 | func main() { 10 | xPrivateKey, xPublicKey, err := bitcoin.GenerateHDKeyPair(bitcoin.SecureSeedLength) 11 | if err != nil { 12 | log.Fatalf("error occurred: %s", err.Error()) 13 | } 14 | 15 | // Success! 16 | log.Printf("xPrivateKey: %s \n xPublicKey: %s", xPrivateKey, xPublicKey) 17 | } 18 | -------------------------------------------------------------------------------- /examples/get_address_from_hd_key/get_address_from_hd_key.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/libsv/go-bt/v2/bscript" 7 | 8 | "github.com/bitcoinschema/go-bitcoin/v2" 9 | ) 10 | 11 | func main() { 12 | 13 | // Start with an HD key (we will make one for this example) 14 | hdKey, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) 15 | if err != nil { 16 | log.Fatalf("error occurred: %s", err.Error()) 17 | } 18 | 19 | // Get an address 20 | var rawAddress *bscript.Address 21 | if rawAddress, err = bitcoin.GetAddressFromHDKey(hdKey); err != nil { 22 | log.Fatalf("error occurred: %s", err.Error()) 23 | } 24 | 25 | // Success! 26 | log.Printf("got address: %s", rawAddress.AddressString) 27 | } 28 | -------------------------------------------------------------------------------- /examples/get_addresses_for_path/get_addresses_for_path.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/bitcoinschema/go-bitcoin/v2" 7 | ) 8 | 9 | func main() { 10 | // Start with an HD key (we will make one for this example) 11 | hdKey, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) 12 | if err != nil { 13 | log.Fatalf("error occurred: %s", err.Error()) 14 | } 15 | 16 | // Get the addresses for the given path 17 | var addresses []string 18 | addresses, err = bitcoin.GetAddressesForPath(hdKey, 2) 19 | if err != nil { 20 | log.Fatalf("error occurred: %s", err.Error()) 21 | } 22 | 23 | // Success! 24 | log.Printf("address 1: %s address 2: %s", addresses[0], addresses[1]) 25 | } 26 | -------------------------------------------------------------------------------- /examples/get_extended_public_key/get_extended_public_key.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/bitcoinschema/go-bitcoin/v2" 7 | ) 8 | 9 | func main() { 10 | // Start with an HD key (we will make one for this example) 11 | hdKey, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) 12 | if err != nil { 13 | log.Fatalf("error occurred: %s", err.Error()) 14 | } 15 | 16 | // Get the extended public key (xPub) 17 | var xPub string 18 | xPub, err = bitcoin.GetExtendedPublicKey(hdKey) 19 | if err != nil { 20 | log.Fatalf("error occurred: %s", err.Error()) 21 | } 22 | 23 | log.Printf("xPub: %s", xPub) 24 | } 25 | -------------------------------------------------------------------------------- /examples/get_hd_key_from_xpub/get_hd_key_from_xpub.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/bitcoinschema/go-bitcoin/v2" 7 | ) 8 | 9 | func main() { 10 | 11 | // Start with an existing xPub 12 | xPub := "xpub661MyMwAqRbcH3WGvLjupmr43L1GVH3MP2WQWvdreDraBeFJy64Xxv4LLX9ZVWWz3ZjZkMuZtSsc9qH9JZR74bR4PWkmtEvP423r6DJR8kA" 13 | 14 | // Convert to a HD key 15 | key, err := bitcoin.GetHDKeyFromExtendedPublicKey(xPub) 16 | if err != nil { 17 | log.Fatalf("error occurred: %s", err.Error()) 18 | } 19 | 20 | log.Printf("converted key: %s private: %v", key.String(), key.IsPrivate()) 21 | } 22 | -------------------------------------------------------------------------------- /examples/get_private_key_for_path/get_private_key_for_path.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/hex" 5 | "log" 6 | 7 | "github.com/bitcoinschema/go-bitcoin/v2" 8 | "github.com/libsv/go-bk/bec" 9 | ) 10 | 11 | func main() { 12 | 13 | // Start with an HD key (we will make one for this example) 14 | hdKey, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) 15 | if err != nil { 16 | log.Fatalf("error occurred: %s", err.Error()) 17 | } 18 | 19 | // Get a private key from a specific path (chain/num) 20 | var privateKey *bec.PrivateKey 21 | privateKey, err = bitcoin.GetPrivateKeyByPath(hdKey, 10, 2) 22 | if err != nil { 23 | log.Fatalf("error occurred: %s", err.Error()) 24 | } 25 | 26 | // Success! 27 | log.Printf("private key: %s for chain/path: %d/%d", hex.EncodeToString(privateKey.Serialise()), 10, 2) 28 | } 29 | -------------------------------------------------------------------------------- /examples/get_public_keys_for_path/get_public_keys_for_path.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/hex" 5 | "log" 6 | 7 | "github.com/bitcoinschema/go-bitcoin/v2" 8 | "github.com/libsv/go-bk/bec" 9 | ) 10 | 11 | func main() { 12 | // Start with an HD key (we will make one for this example) 13 | hdKey, err := bitcoin.GenerateHDKey(bitcoin.SecureSeedLength) 14 | if err != nil { 15 | log.Fatalf("error occurred: %s", err.Error()) 16 | } 17 | 18 | // Get keys by path (example showing 5 sets of keys) 19 | var pubKeys []*bec.PublicKey 20 | for i := 1; i <= 5; i++ { 21 | if pubKeys, err = bitcoin.GetPublicKeysForPath(hdKey, uint32(i)); err != nil { 22 | log.Fatalf("error occurred: %s", err.Error()) 23 | } 24 | for index, key := range pubKeys { 25 | log.Printf("#%d found at m/%d/%d key: %s", i, index, i, hex.EncodeToString(key.SerialiseCompressed())) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/private_key_to_wif/private_key_to_wif.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/bitcoinschema/go-bitcoin/v2" 7 | ) 8 | 9 | func main() { 10 | 11 | // Start with a private key (we will make one for this example) 12 | privateKey, err := bitcoin.CreatePrivateKeyString() 13 | if err != nil { 14 | log.Fatalf("error occurred: %s", err.Error()) 15 | } 16 | 17 | // Create a wif 18 | var privateWif string 19 | if privateWif, err = bitcoin.PrivateKeyToWifString(privateKey); err != nil { 20 | log.Fatalf("error occurred: %s", err.Error()) 21 | } 22 | 23 | // Success! 24 | log.Printf("private key: %s converted to wif: %s", privateKey, privateWif) 25 | } 26 | -------------------------------------------------------------------------------- /examples/script_from_address/script_from_address.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/bitcoinschema/go-bitcoin/v2" 7 | ) 8 | 9 | func main() { 10 | // Start with a private key (we will make one for this example) 11 | privateKey, err := bitcoin.CreatePrivateKey() 12 | if err != nil { 13 | log.Fatalf("error occurred: %s", err.Error()) 14 | } 15 | 16 | // Get an address 17 | var address string 18 | if address, err = bitcoin.GetAddressFromPrivateKey(privateKey, true); err != nil { 19 | log.Fatalf("error occurred: %s", err.Error()) 20 | } 21 | 22 | // Get the script 23 | var script string 24 | if script, err = bitcoin.ScriptFromAddress(address); err != nil { 25 | log.Fatalf("error occurred: %s", err.Error()) 26 | } 27 | 28 | // Success! 29 | log.Printf("generated script: %s from address: %s", script, address) 30 | } 31 | -------------------------------------------------------------------------------- /examples/sign_message/sign_message.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/bitcoinschema/go-bitcoin/v2" 7 | ) 8 | 9 | func main() { 10 | // Create a private key (we will make one for this example) 11 | privateKey, err := bitcoin.CreatePrivateKeyString() 12 | if err != nil { 13 | log.Fatalf("error occurred: %s", err.Error()) 14 | } 15 | 16 | // Sign the message (returning a signature) 17 | 18 | // Note: If your signature references a compressed key, 19 | // the address you provide to verify must also come from a compressed key 20 | var signature string 21 | if signature, err = bitcoin.SignMessage(privateKey, "This is the example message", false); err != nil { 22 | log.Fatalf("error occurred: %s", err.Error()) 23 | } 24 | 25 | // Final signature for the given message 26 | log.Printf("private key: %s signature: %s", privateKey, signature) 27 | } 28 | -------------------------------------------------------------------------------- /examples/tx_from_hex/tx_from_hex.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/bitcoinschema/go-bitcoin/v2" 7 | ) 8 | 9 | func main() { 10 | 11 | // Example raw tx 12 | exampleTx := "0100000001760595866e99c1ce920197844740f5598b34763878696371d41b3a7c0a65b0b7000000006b483045022100eea3d606bd1627be6459a9de4860919225db74843d2fc7f4e7caa5e01f42c2d0022017978d9c6a0e934955a70e7dda71d68cb614f7dd89eb7b9d560aea761834ddd4412102ea87d1fd77d169bd56a71e700628113d0f8dfe57faa0ba0e55a36f9ce8e10be3ffffffff03f4010000000000001976a9147a1980655efbfec416b2b0c663a7b3ac0b6a25d288ac00000000000000001a006a07707265666978310c6578616d706c65206461746102133700000000000000001c006a0770726566697832116d6f7265206578616d706c65206461746100000000" 13 | 14 | rawTx, err := bitcoin.TxFromHex(exampleTx) 15 | if err != nil { 16 | log.Printf("error occurred: %s", err.Error()) 17 | return 18 | } 19 | 20 | log.Printf("tx id: %s", rawTx.TxID()) 21 | } 22 | -------------------------------------------------------------------------------- /examples/verify_signature/verify_signature.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/bitcoinschema/go-bitcoin/v2" 7 | ) 8 | 9 | func main() { 10 | 11 | // Example values (from sign_message.go) 12 | privateKey := "ac40784f09304a88b8db0cfcaff3a4e7d81b6b6ccdef68fad8ddf853ea1d1dce" 13 | signature := "H/sEz5QDQYkXCox9shPB4MMVAVUM/JzfbPHNpPRwNl+hMI2gxy3x7xs9Ed5ryuny5s2hY4Qxc5uirqjMyEEON6k=" 14 | message := "This is the example message" 15 | 16 | rawKey, err := bitcoin.PrivateKeyFromString(privateKey) 17 | if err != nil { 18 | log.Fatalf("error occurred: %s", err.Error()) 19 | } 20 | 21 | // Get an address from private key 22 | // the compressed flag must match the flag provided during signing 23 | var address string 24 | address, err = bitcoin.GetAddressFromPrivateKey(rawKey, true) 25 | if err != nil { 26 | log.Fatalf("error occurred: %s", err.Error()) 27 | } 28 | 29 | // Verify the signature 30 | if err = bitcoin.VerifyMessage(address, signature, message); err != nil { 31 | log.Fatalf("verify failed: %s", err.Error()) 32 | } else { 33 | log.Println("verification passed") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/verify_signature_der/verify_signature_der.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha256" 5 | "log" 6 | 7 | "github.com/bitcoinschema/go-bitcoin/v2" 8 | ) 9 | 10 | func main() { 11 | 12 | // Example values (from Merchant API request) 13 | message := []byte(`{"apiVersion":"0.1.0","timestamp":"2020-10-08T14:25:31.539Z","expiryTime":"2020-10-08T14:35:31.539Z","minerId":"03e92d3e5c3f7bd945dfbf48e7a99393b1bfb3f11f380ae30d286e7ff2aec5a270","currentHighestBlockHash":"0000000000000000021af4ee1f179a64e530bf818ef67acd09cae24a89124519","currentHighestBlockHeight":656007,"minerReputation":null,"fees":[{"id":1,"feeType":"standard","miningFee":{"satoshis":500,"bytes":1000},"relayFee":{"satoshis":250,"bytes":1000}},{"id":2,"feeType":"data","miningFee":{"satoshis":500,"bytes":1000},"relayFee":{"satoshis":250,"bytes":1000}}]}`) 14 | signature := "3045022100b976be863fffd361716b375a9a5c4e77073dfaa29d2b9af9addef94f029c2d0902205b1fffc58343f3d4bd8fc48a118e998072c655d318061e13e1ef0902fb42e15c" 15 | pubKey := "03e92d3e5c3f7bd945dfbf48e7a99393b1bfb3f11f380ae30d286e7ff2aec5a270" 16 | 17 | // Verify the signature 18 | if verified, err := bitcoin.VerifyMessageDER(sha256.Sum256(message), pubKey, signature); err != nil { 19 | log.Fatalf("verify failed: %s", err.Error()) 20 | } else if !verified { 21 | log.Fatalf("verification failed") 22 | } 23 | log.Println("verification passed") 24 | } 25 | -------------------------------------------------------------------------------- /examples/wif_from_string/wif_from_string.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/libsv/go-bk/wif" 7 | 8 | "github.com/bitcoinschema/go-bitcoin/v2" 9 | ) 10 | 11 | func main() { 12 | 13 | // Create a wif 14 | wifString, err := bitcoin.CreateWifString() 15 | if err != nil { 16 | log.Fatalf("error occurred: %s", err.Error()) 17 | } 18 | 19 | // Create a wif from a string 20 | var wifKey *wif.WIF 21 | wifKey, err = bitcoin.WifFromString(wifString) 22 | if err != nil { 23 | log.Fatalf("error occurred: %s", err.Error()) 24 | } 25 | 26 | // Success! 27 | log.Printf("wif key: %s is also: %s", wifString, wifKey.String()) 28 | } 29 | -------------------------------------------------------------------------------- /examples/wif_to_private_key/wif_to_private_key.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/bitcoinschema/go-bitcoin/v2" 7 | ) 8 | 9 | func main() { 10 | 11 | // Convert the wif into a private key 12 | privateKey, err := bitcoin.WifToPrivateKeyString("5KgHn2qiftW5LQgCYFtkbrLYB1FuvisDtacax8NCvumw3UTKdcP") 13 | if err != nil { 14 | log.Fatalf("error occurred: %s", err.Error()) 15 | } 16 | 17 | // Success! 18 | log.Printf("private key: %s", privateKey) 19 | } 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bitcoinschema/go-bitcoin/v2 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/bitcoinsv/bsvd v0.0.0-20190609155523-4c29707f7173 7 | github.com/libsv/go-bk v0.1.6 8 | github.com/libsv/go-bt/v2 v2.2.5 9 | github.com/stretchr/testify v1.10.0 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/kr/text v0.2.0 // indirect 15 | github.com/pkg/errors v0.9.1 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | github.com/rogpeppe/go-internal v1.11.0 // indirect 18 | golang.org/x/crypto v0.35.0 // indirect 19 | gopkg.in/yaml.v3 v3.0.1 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bitcoinsv/bsvd v0.0.0-20190609155523-4c29707f7173 h1:2yTIV9u7H0BhRDGXH5xrAwAz7XibWJtX2dNezMeNsUo= 2 | github.com/bitcoinsv/bsvd v0.0.0-20190609155523-4c29707f7173/go.mod h1:BZ1UcC9+tmcDEcdVXgpt13hMczwJxWzpAn68wNs7zRA= 3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 7 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 8 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 9 | github.com/libsv/go-bk v0.1.6 h1:c9CiT5+64HRDbzxPl1v/oiFmbvWZTuUYqywCf+MBs/c= 10 | github.com/libsv/go-bk v0.1.6/go.mod h1:khJboDoH18FPUaZlzRFKzlVN84d4YfdmlDtdX4LAjQA= 11 | github.com/libsv/go-bt/v2 v2.2.5 h1:VoggBLMRW9NYoFujqe5bSYKqnw5y+fYfufgERSoubog= 12 | github.com/libsv/go-bt/v2 v2.2.5/go.mod h1:cV45+jDlPOLfhJLfpLmpQoWzrIvVth9Ao2ZO1f6CcqU= 13 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 14 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 18 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 19 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 20 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 21 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 22 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 23 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 24 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 25 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 26 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 27 | -------------------------------------------------------------------------------- /hd_key.go: -------------------------------------------------------------------------------- 1 | package bitcoin 2 | 3 | import ( 4 | "encoding/hex" 5 | 6 | "github.com/libsv/go-bk/bec" 7 | "github.com/libsv/go-bk/bip32" 8 | "github.com/libsv/go-bk/chaincfg" 9 | "github.com/libsv/go-bt/v2/bscript" 10 | ) 11 | 12 | const ( 13 | // RecommendedSeedLength is the recommended length in bytes for a seed to a master node 14 | RecommendedSeedLength = 32 // 256 bits 15 | 16 | // SecureSeedLength is the max size of a seed length (most secure) 17 | SecureSeedLength = 64 // 512 bits 18 | 19 | // DefaultExternalChain is the default external chain (for public use to accept incoming txs) 20 | // Reference: https://en.bitcoin.it/wiki/BIP_0032#The_default_wallet_layout 21 | DefaultExternalChain = 0 22 | 23 | // DefaultInternalChain is the default internal chain (for change, generating, other purposes...) 24 | // Reference: https://en.bitcoin.it/wiki/BIP_0032#The_default_wallet_layout 25 | DefaultInternalChain = 1 26 | ) 27 | 28 | // GenerateHDKey will create a new master node for use in creating a hierarchical deterministic keychain 29 | func GenerateHDKey(seedLength uint8) (hdKey *bip32.ExtendedKey, err error) { 30 | 31 | // Missing or invalid seed length 32 | if seedLength == 0 { 33 | seedLength = RecommendedSeedLength 34 | } 35 | 36 | // Generate a new seed (added extra security from 256 to 512 bits for seed length) 37 | var seed []byte 38 | if seed, err = bip32.GenerateSeed(seedLength); err != nil { 39 | return 40 | } 41 | 42 | // Generate a new master key 43 | return bip32.NewMaster(seed, &chaincfg.MainNet) 44 | } 45 | 46 | // GenerateHDKeyFromString will create a new master node for use in creating a 47 | // hierarchical deterministic keychain from an xPrivKey string 48 | func GenerateHDKeyFromString(xPriv string) (hdKey *bip32.ExtendedKey, err error) { 49 | return bip32.NewKeyFromString(xPriv) 50 | } 51 | 52 | // GenerateHDKeyPair will generate a new xPub HD master node (xPrivateKey & xPublicKey) 53 | func GenerateHDKeyPair(seedLength uint8) (xPrivateKey, xPublicKey string, err error) { 54 | 55 | // Generate an HD master key 56 | var masterKey *bip32.ExtendedKey 57 | if masterKey, err = GenerateHDKey(seedLength); err != nil { 58 | return 59 | } 60 | 61 | // Set the xPriv (string) 62 | xPrivateKey = masterKey.String() 63 | 64 | // Set the xPub (string) 65 | xPublicKey, err = GetExtendedPublicKey(masterKey) 66 | 67 | return 68 | } 69 | 70 | // GetHDKeyByPath gets the corresponding HD key from a chain/num path 71 | // Reference: https://en.bitcoin.it/wiki/BIP_0032#The_default_wallet_layout 72 | func GetHDKeyByPath(hdKey *bip32.ExtendedKey, chain, num uint32) (*bip32.ExtendedKey, error) { 73 | 74 | // Derive the child key from the chain path 75 | childKeyChain, err := GetHDKeyChild(hdKey, chain) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | // Get the child key from the num path 81 | return GetHDKeyChild(childKeyChain, num) 82 | } 83 | 84 | // GetHDKeyChild gets the child hd key for a given num 85 | // Note: For a hardened child, start at 0x80000000. (For reference, 0x8000000 = 0') 86 | // 87 | // Expects hdKey to not be nil (otherwise will panic) 88 | func GetHDKeyChild(hdKey *bip32.ExtendedKey, num uint32) (*bip32.ExtendedKey, error) { 89 | return hdKey.Child(num) 90 | } 91 | 92 | // GetPrivateKeyByPath gets the key for a given derivation path (chain/num) 93 | // 94 | // Expects hdKey to not be nil (otherwise will panic) 95 | func GetPrivateKeyByPath(hdKey *bip32.ExtendedKey, chain, num uint32) (*bec.PrivateKey, error) { 96 | 97 | // Get the child key from the num & chain 98 | childKeyNum, err := GetHDKeyByPath(hdKey, chain, num) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | // Get the private key 104 | return childKeyNum.ECPrivKey() 105 | } 106 | 107 | // GetPrivateKeyFromHDKey is a helper function to get the Private Key associated 108 | // with a given hdKey 109 | // 110 | // Expects hdKey to not be nil (otherwise will panic) 111 | func GetPrivateKeyFromHDKey(hdKey *bip32.ExtendedKey) (*bec.PrivateKey, error) { 112 | return hdKey.ECPrivKey() 113 | } 114 | 115 | // GetPrivateKeyStringFromHDKey is a helper function to get the Private Key (string) 116 | // associated with a given hdKey 117 | // 118 | // Expects hdKey to not be nil (otherwise will panic) 119 | func GetPrivateKeyStringFromHDKey(hdKey *bip32.ExtendedKey) (string, error) { 120 | key, err := GetPrivateKeyFromHDKey(hdKey) 121 | if err != nil { 122 | return "", err 123 | } 124 | return hex.EncodeToString(key.Serialise()), nil 125 | } 126 | 127 | // GetPublicKeyFromHDKey is a helper function to get the Public Key associated with a given hdKey 128 | // 129 | // Expects hdKey to not be nil (otherwise will panic) 130 | func GetPublicKeyFromHDKey(hdKey *bip32.ExtendedKey) (*bec.PublicKey, error) { 131 | return hdKey.ECPubKey() 132 | } 133 | 134 | // GetAddressFromHDKey is a helper function to get the Address associated with a given hdKey 135 | // 136 | // Expects hdKey to not be nil (otherwise will panic) 137 | func GetAddressFromHDKey(hdKey *bip32.ExtendedKey) (*bscript.Address, error) { 138 | pubKey, err := GetPublicKeyFromHDKey(hdKey) 139 | if err != nil { 140 | return nil, err 141 | } 142 | return GetAddressFromPubKey(pubKey, true) 143 | } 144 | 145 | // GetAddressStringFromHDKey is a helper function to get the Address (string) associated with a given hdKey 146 | // 147 | // Expects hdKey to not be nil (otherwise will panic) 148 | func GetAddressStringFromHDKey(hdKey *bip32.ExtendedKey) (string, error) { 149 | address, err := GetAddressFromHDKey(hdKey) 150 | if err != nil { 151 | return "", err 152 | } 153 | return address.AddressString, nil 154 | } 155 | 156 | // GetPublicKeysForPath gets the PublicKeys for a given derivation path 157 | // Uses the standard m/0/0 (external) and m/0/1 (internal) paths 158 | // Reference: https://en.bitcoin.it/wiki/BIP_0032#The_default_wallet_layout 159 | func GetPublicKeysForPath(hdKey *bip32.ExtendedKey, num uint32) (pubKeys []*bec.PublicKey, err error) { 160 | 161 | // m/0/x 162 | var childM0x *bip32.ExtendedKey 163 | if childM0x, err = GetHDKeyByPath(hdKey, DefaultExternalChain, num); err != nil { 164 | return 165 | } 166 | 167 | // Get the external pubKey from m/0/x 168 | var pubKey *bec.PublicKey 169 | if pubKey, err = childM0x.ECPubKey(); err != nil { 170 | // Should never error since the previous method ensures a valid hdKey 171 | return 172 | } 173 | pubKeys = append(pubKeys, pubKey) 174 | 175 | // m/1/x 176 | var childM1x *bip32.ExtendedKey 177 | if childM1x, err = GetHDKeyByPath(hdKey, DefaultInternalChain, num); err != nil { 178 | // Should never error since the previous method ensures a valid hdKey 179 | return 180 | } 181 | 182 | // Get the internal pubKey from m/1/x 183 | if pubKey, err = childM1x.ECPubKey(); err != nil { 184 | // Should never error since the previous method ensures a valid hdKey 185 | return 186 | } 187 | pubKeys = append(pubKeys, pubKey) 188 | 189 | return 190 | } 191 | 192 | // GetAddressesForPath will get the corresponding addresses for the PublicKeys at the given path m/0/x 193 | // Returns 2 keys, first is internal and second is external 194 | func GetAddressesForPath(hdKey *bip32.ExtendedKey, num uint32) (addresses []string, err error) { 195 | 196 | // Get the public keys for the corresponding chain/num (using default chain) 197 | var pubKeys []*bec.PublicKey 198 | if pubKeys, err = GetPublicKeysForPath(hdKey, num); err != nil { 199 | return 200 | } 201 | 202 | // Loop, get address and append to results 203 | var address *bscript.Address 204 | for _, key := range pubKeys { 205 | if address, err = GetAddressFromPubKey(key, true); err != nil { 206 | // Should never error if the pubKeys are valid keys 207 | return 208 | } 209 | addresses = append(addresses, address.AddressString) 210 | } 211 | 212 | return 213 | } 214 | 215 | // GetExtendedPublicKey will get the extended public key (xPub) 216 | func GetExtendedPublicKey(hdKey *bip32.ExtendedKey) (string, error) { 217 | 218 | // Neuter the extended public key from hd key 219 | pub, err := hdKey.Neuter() 220 | if err != nil { 221 | // Error should never occur if using a valid hd key 222 | return "", err 223 | } 224 | 225 | // Return the string version 226 | return pub.String(), nil 227 | } 228 | 229 | // GetHDKeyFromExtendedPublicKey will get the hd key from an existing extended public key (xPub) 230 | func GetHDKeyFromExtendedPublicKey(xPublicKey string) (*bip32.ExtendedKey, error) { 231 | return bip32.NewKeyFromString(xPublicKey) 232 | } 233 | -------------------------------------------------------------------------------- /private_key.go: -------------------------------------------------------------------------------- 1 | package bitcoin 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "encoding/hex" 6 | "math/big" 7 | 8 | "github.com/libsv/go-bk/bec" 9 | "github.com/libsv/go-bk/chaincfg" 10 | "github.com/libsv/go-bk/wif" 11 | ) 12 | 13 | // GenerateSharedKeyPair creates shared keys that can be used to encrypt/decrypt data 14 | // that can be decrypted by yourself (privateKey) and also the owner of the given public key 15 | func GenerateSharedKeyPair(privateKey *bec.PrivateKey, 16 | pubKey *bec.PublicKey) (*bec.PrivateKey, *bec.PublicKey) { 17 | return bec.PrivKeyFromBytes( 18 | bec.S256(), 19 | bec.GenerateSharedSecret(privateKey, pubKey), 20 | ) 21 | } 22 | 23 | // PrivateKeyFromString turns a private key (hex encoded string) into an bec.PrivateKey 24 | func PrivateKeyFromString(privateKey string) (*bec.PrivateKey, error) { 25 | if len(privateKey) == 0 { 26 | return nil, ErrPrivateKeyMissing 27 | } 28 | privateKeyBytes, err := hex.DecodeString(privateKey) 29 | if err != nil { 30 | return nil, err 31 | } 32 | x, y := bec.S256().ScalarBaseMult(privateKeyBytes) 33 | ecdsaPubKey := ecdsa.PublicKey{ 34 | Curve: bec.S256(), 35 | X: x, 36 | Y: y, 37 | } 38 | return &bec.PrivateKey{PublicKey: ecdsaPubKey, D: new(big.Int).SetBytes(privateKeyBytes)}, nil 39 | } 40 | 41 | // CreatePrivateKey will create a new private key (*bec.PrivateKey) 42 | func CreatePrivateKey() (*bec.PrivateKey, error) { 43 | return bec.NewPrivateKey(bec.S256()) 44 | } 45 | 46 | // CreatePrivateKeyString will create a new private key (hex encoded) 47 | func CreatePrivateKeyString() (string, error) { 48 | privateKey, err := CreatePrivateKey() 49 | if err != nil { 50 | return "", err 51 | } 52 | 53 | return hex.EncodeToString(privateKey.Serialise()), nil 54 | } 55 | 56 | // CreateWif will create a new WIF (*wif.WIF) 57 | func CreateWif() (*wif.WIF, error) { 58 | privateKey, err := CreatePrivateKey() 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | return wif.NewWIF(privateKey, &chaincfg.MainNet, false) 64 | } 65 | 66 | // CreateWifString will create a new WIF (string) 67 | func CreateWifString() (string, error) { 68 | wifKey, err := CreateWif() 69 | if err != nil { 70 | return "", err 71 | } 72 | 73 | return wifKey.String(), nil 74 | } 75 | 76 | // PrivateAndPublicKeys will return both the private and public key in one method 77 | // Expects a hex encoded privateKey 78 | func PrivateAndPublicKeys(privateKey string) (*bec.PrivateKey, *bec.PublicKey, error) { 79 | 80 | // No key? 81 | if len(privateKey) == 0 { 82 | return nil, nil, ErrPrivateKeyMissing 83 | } 84 | 85 | // Decode the private key into bytes 86 | privateKeyBytes, err := hex.DecodeString(privateKey) 87 | if err != nil { 88 | return nil, nil, err 89 | } 90 | 91 | // Get the public and private key from the bytes 92 | rawKey, publicKey := bec.PrivKeyFromBytes(bec.S256(), privateKeyBytes) 93 | return rawKey, publicKey, nil 94 | } 95 | 96 | // PrivateKeyToWif will convert a private key to a WIF (*wif.WIF) 97 | func PrivateKeyToWif(privateKey string) (*wif.WIF, error) { 98 | 99 | // Missing private key 100 | if len(privateKey) == 0 { 101 | return nil, ErrPrivateKeyMissing 102 | } 103 | 104 | // Decode the private key 105 | decodedKey, err := hex.DecodeString(privateKey) 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | // Get the private key from bytes 111 | rawKey, _ := bec.PrivKeyFromBytes(bec.S256(), decodedKey) 112 | 113 | // Create a new WIF (error never gets hit since (net) is set correctly) 114 | return wif.NewWIF(rawKey, &chaincfg.MainNet, false) 115 | } 116 | 117 | // PrivateKeyToWifString will convert a private key to a WIF (string) 118 | func PrivateKeyToWifString(privateKey string) (string, error) { 119 | privateWif, err := PrivateKeyToWif(privateKey) 120 | if err != nil { 121 | return "", err 122 | } 123 | 124 | return privateWif.String(), nil 125 | } 126 | 127 | // WifToPrivateKey will convert a WIF to a private key (*bec.PrivateKey) 128 | func WifToPrivateKey(wifKey string) (*bec.PrivateKey, error) { 129 | 130 | // Missing wif? 131 | if len(wifKey) == 0 { 132 | return nil, ErrWifMissing 133 | } 134 | 135 | // Decode the wif 136 | decodedWif, err := wif.DecodeWIF(wifKey) 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | // Return the private key 142 | return decodedWif.PrivKey, nil 143 | } 144 | 145 | // WifToPrivateKeyString will convert a WIF to private key (string) 146 | func WifToPrivateKeyString(wif string) (string, error) { 147 | 148 | // Convert the wif to private key 149 | privateKey, err := WifToPrivateKey(wif) 150 | if err != nil { 151 | return "", err 152 | } 153 | 154 | // Return the hex (string) version of the private key 155 | return hex.EncodeToString(privateKey.Serialise()), nil 156 | } 157 | 158 | // WifFromString will convert a WIF (string) to a WIF (*wif.WIF) 159 | func WifFromString(wifKey string) (*wif.WIF, error) { 160 | 161 | // Missing wif? 162 | if len(wifKey) == 0 { 163 | return nil, ErrWifMissing 164 | } 165 | 166 | // Decode the WIF 167 | decodedWif, err := wif.DecodeWIF(wifKey) 168 | if err != nil { 169 | return nil, err 170 | } 171 | 172 | return decodedWif, nil 173 | } 174 | -------------------------------------------------------------------------------- /private_key_test.go: -------------------------------------------------------------------------------- 1 | package bitcoin 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/libsv/go-bk/bec" 9 | "github.com/libsv/go-bk/wif" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | // TestCreatePrivateKey will test the method CreatePrivateKey() 15 | func TestCreatePrivateKey(t *testing.T) { 16 | rawKey, err := CreatePrivateKey() 17 | assert.NoError(t, err) 18 | assert.NotNil(t, rawKey) 19 | assert.Equal(t, 32, len(rawKey.Serialise())) 20 | } 21 | 22 | // ExampleCreatePrivateKey example using CreatePrivateKey() 23 | func ExampleCreatePrivateKey() { 24 | rawKey, err := CreatePrivateKey() 25 | if err != nil { 26 | fmt.Printf("error occurred: %s", err.Error()) 27 | return 28 | } else if len(rawKey.Serialise()) > 0 { 29 | fmt.Printf("key created successfully!") 30 | } 31 | // Output:key created successfully! 32 | } 33 | 34 | // BenchmarkCreatePrivateKey benchmarks the method CreatePrivateKey() 35 | func BenchmarkCreatePrivateKey(b *testing.B) { 36 | for i := 0; i < b.N; i++ { 37 | _, _ = CreatePrivateKey() 38 | } 39 | } 40 | 41 | // TestCreatePrivateKeyString will test the method CreatePrivateKeyString() 42 | func TestCreatePrivateKeyString(t *testing.T) { 43 | key, err := CreatePrivateKeyString() 44 | assert.NoError(t, err) 45 | assert.Equal(t, 64, len(key)) 46 | } 47 | 48 | // ExampleCreatePrivateKeyString example using CreatePrivateKeyString() 49 | func ExampleCreatePrivateKeyString() { 50 | key, err := CreatePrivateKeyString() 51 | if err != nil { 52 | fmt.Printf("error occurred: %s", err.Error()) 53 | return 54 | } else if len(key) > 0 { 55 | fmt.Printf("key created successfully!") 56 | } 57 | // Output:key created successfully! 58 | } 59 | 60 | // BenchmarkCreatePrivateKeyString benchmarks the method CreatePrivateKeyString() 61 | func BenchmarkCreatePrivateKeyString(b *testing.B) { 62 | for i := 0; i < b.N; i++ { 63 | _, _ = CreatePrivateKeyString() 64 | } 65 | } 66 | 67 | // TestPrivateKeyFromString will test the method PrivateKeyFromString() 68 | func TestPrivateKeyFromString(t *testing.T) { 69 | t.Parallel() 70 | 71 | var tests = []struct { 72 | input string 73 | expectedKey string 74 | expectedNil bool 75 | expectedError bool 76 | }{ 77 | {"54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd", "54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd", false, false}, 78 | {"E83385AF76B2B1997326B567461FB73DD9C27EAB9E1E86D26779F4650C5F2B75", "e83385af76b2b1997326b567461fb73dd9c27eab9e1e86d26779f4650c5f2b75", false, false}, 79 | {"E83385AF76B2B1997326B567461FB73DD9C27EAB9E1E86D26779F4650C5F", "0000e83385af76b2b1997326b567461fb73dd9c27eab9e1e86d26779f4650c5f", false, false}, 80 | {"E83385AF76B2B1997326B567461FB73DD9C27EAB9E1E86D26779F", "", true, true}, 81 | {"1234567", "", true, true}, 82 | {"0", "", true, true}, 83 | {"", "", true, true}, 84 | } 85 | 86 | for _, test := range tests { 87 | if rawKey, err := PrivateKeyFromString(test.input); err != nil && !test.expectedError { 88 | t.Fatalf("%s Failed: [%s] inputted and error not expected but got: %s", t.Name(), test.input, err.Error()) 89 | } else if err == nil && test.expectedError { 90 | t.Fatalf("%s Failed: [%s] inputted and error was expected", t.Name(), test.input) 91 | } else if rawKey == nil && !test.expectedNil { 92 | t.Fatalf("%s Failed: [%s] inputted and was nil but not expected", t.Name(), test.input) 93 | } else if rawKey != nil && test.expectedNil { 94 | t.Fatalf("%s Failed: [%s] inputted and was NOT nil but expected to be nil", t.Name(), test.input) 95 | } else if rawKey != nil && hex.EncodeToString(rawKey.Serialise()) != test.expectedKey { 96 | t.Fatalf("%s Failed: [%s] inputted [%s] expected but failed comparison of keys, got: %s", t.Name(), test.input, test.expectedKey, hex.EncodeToString(rawKey.Serialise())) 97 | } 98 | } 99 | } 100 | 101 | // ExamplePrivateKeyFromString example using PrivateKeyFromString() 102 | func ExamplePrivateKeyFromString() { 103 | key, err := PrivateKeyFromString("54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd") 104 | if err != nil { 105 | fmt.Printf("error occurred: %s", err.Error()) 106 | return 107 | } 108 | fmt.Printf("key converted: %s", hex.EncodeToString(key.Serialise())) 109 | // Output:key converted: 54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd 110 | } 111 | 112 | // BenchmarkPrivateKeyFromString benchmarks the method PrivateKeyFromString() 113 | func BenchmarkPrivateKeyFromString(b *testing.B) { 114 | key, _ := CreatePrivateKeyString() 115 | for i := 0; i < b.N; i++ { 116 | _, _ = PrivateKeyFromString(key) 117 | } 118 | } 119 | 120 | // TestPrivateAndPublicKeys will test the method PrivateAndPublicKeys() 121 | func TestPrivateAndPublicKeys(t *testing.T) { 122 | 123 | t.Parallel() 124 | 125 | var tests = []struct { 126 | input string 127 | expectedPrivateKey string 128 | expectedNil bool 129 | expectedError bool 130 | }{ 131 | {"", "", true, true}, 132 | {"0", "", true, true}, 133 | {"00000", "", true, true}, 134 | {"0-0-0-0-0", "", true, true}, 135 | {"z4035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abz", "", true, true}, 136 | {"54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd", "54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd", false, false}, 137 | } 138 | 139 | for _, test := range tests { 140 | if privateKey, publicKey, err := PrivateAndPublicKeys(test.input); err != nil && !test.expectedError { 141 | t.Fatalf("%s Failed: [%s] inputted and error not expected but got: %s", t.Name(), test.input, err.Error()) 142 | } else if err == nil && test.expectedError { 143 | t.Fatalf("%s Failed: [%s] inputted and error was expected", t.Name(), test.input) 144 | } else if (privateKey == nil || publicKey == nil) && !test.expectedNil { 145 | t.Fatalf("%s Failed: [%s] inputted and was nil but not expected", t.Name(), test.input) 146 | } else if (privateKey != nil || publicKey != nil) && test.expectedNil { 147 | t.Fatalf("%s Failed: [%s] inputted and was NOT nil but expected to be nil", t.Name(), test.input) 148 | } else if privateKey != nil && hex.EncodeToString(privateKey.Serialise()) != test.expectedPrivateKey { 149 | t.Fatalf("%s Failed: [%s] inputted [%s] expected but failed comparison of keys, got: %s", t.Name(), test.input, test.expectedPrivateKey, hex.EncodeToString(privateKey.Serialise())) 150 | } 151 | } 152 | } 153 | 154 | // ExamplePrivateAndPublicKeys example using PrivateAndPublicKeys() 155 | func ExamplePrivateAndPublicKeys() { 156 | privateKey, publicKey, err := PrivateAndPublicKeys("54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd") 157 | if err != nil { 158 | fmt.Printf("error occurred: %s", err.Error()) 159 | return 160 | } 161 | fmt.Printf("private key: %s public key: %s", hex.EncodeToString(privateKey.Serialise()), hex.EncodeToString(publicKey.SerialiseCompressed())) 162 | 163 | // Output:private key: 54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd public key: 031b8c93100d35bd448f4646cc4678f278351b439b52b303ea31ec9edb5475e73f 164 | } 165 | 166 | // BenchmarkPrivateAndPublicKeys benchmarks the method PrivateAndPublicKeys() 167 | func BenchmarkPrivateAndPublicKeys(b *testing.B) { 168 | key, _ := CreatePrivateKeyString() 169 | for i := 0; i < b.N; i++ { 170 | _, _, _ = PrivateAndPublicKeys(key) 171 | } 172 | } 173 | 174 | // TestPrivateKeyToWif will test the method PrivateKeyToWif() 175 | func TestPrivateKeyToWif(t *testing.T) { 176 | 177 | t.Parallel() 178 | 179 | var tests = []struct { 180 | input string 181 | expectedWif string 182 | expectedNil bool 183 | expectedError bool 184 | }{ 185 | {"", "", true, true}, 186 | {"0", "", true, true}, 187 | {"000000", "5HpHagT65TZzG1PH3CSu63k8DbpvD8s5ip4nEB3kEsreAbuatmU", false, false}, 188 | {"6D792070726976617465206B6579", "5HpHagT65TZzG1PH3CSu63k8DbuTZnNJf6HgyQNymvXmALAsm9s", false, false}, 189 | {"54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8azz", "", true, true}, 190 | {"54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd", "5JTHas7yTFMBLqgFogxZFf8Vc5uKEbkE7yQAQ2g3xPHo2sNG1Ei", false, false}, 191 | } 192 | 193 | for _, test := range tests { 194 | if privateWif, err := PrivateKeyToWif(test.input); err != nil && !test.expectedError { 195 | t.Fatalf("%s Failed: [%s] inputted and error not expected but got: %s", t.Name(), test.input, err.Error()) 196 | } else if err == nil && test.expectedError { 197 | t.Fatalf("%s Failed: [%s] inputted and error was expected", t.Name(), test.input) 198 | } else if privateWif == nil && !test.expectedNil { 199 | t.Fatalf("%s Failed: [%s] inputted and was nil but not expected", t.Name(), test.input) 200 | } else if privateWif != nil && test.expectedNil { 201 | t.Fatalf("%s Failed: [%s] inputted and was NOT nil but expected to be nil", t.Name(), test.input) 202 | } else if privateWif != nil && privateWif.String() != test.expectedWif { 203 | t.Fatalf("%s Failed: [%s] inputted [%s] expected but failed comparison of keys, got: %s", t.Name(), test.input, test.expectedWif, privateWif.String()) 204 | } 205 | } 206 | 207 | } 208 | 209 | // ExamplePrivateKeyToWif example using PrivateKeyToWif() 210 | func ExamplePrivateKeyToWif() { 211 | privateWif, err := PrivateKeyToWif("54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd") 212 | if err != nil { 213 | fmt.Printf("error occurred: %s", err.Error()) 214 | return 215 | } 216 | fmt.Printf("converted wif: %s", privateWif.String()) 217 | 218 | // Output:converted wif: 5JTHas7yTFMBLqgFogxZFf8Vc5uKEbkE7yQAQ2g3xPHo2sNG1Ei 219 | } 220 | 221 | // BenchmarkPrivateKeyToWif benchmarks the method PrivateKeyToWif() 222 | func BenchmarkPrivateKeyToWif(b *testing.B) { 223 | key, _ := CreatePrivateKeyString() 224 | for i := 0; i < b.N; i++ { 225 | _, _ = PrivateKeyToWif(key) 226 | } 227 | } 228 | 229 | // TestPrivateKeyToWifString will test the method PrivateKeyToWifString() 230 | func TestPrivateKeyToWifString(t *testing.T) { 231 | 232 | t.Parallel() 233 | 234 | var tests = []struct { 235 | input string 236 | expectedWif string 237 | expectedError bool 238 | }{ 239 | {"", "", true}, 240 | {"0", "", true}, 241 | {"000000", "5HpHagT65TZzG1PH3CSu63k8DbpvD8s5ip4nEB3kEsreAbuatmU", false}, 242 | {"6D792070726976617465206B6579", "5HpHagT65TZzG1PH3CSu63k8DbuTZnNJf6HgyQNymvXmALAsm9s", false}, 243 | {"54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8azz", "", true}, 244 | {"54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd", "5JTHas7yTFMBLqgFogxZFf8Vc5uKEbkE7yQAQ2g3xPHo2sNG1Ei", false}, 245 | } 246 | 247 | for _, test := range tests { 248 | if privateWif, err := PrivateKeyToWifString(test.input); err != nil && !test.expectedError { 249 | t.Fatalf("%s Failed: [%s] inputted and error not expected but got: %s", t.Name(), test.input, err.Error()) 250 | } else if err == nil && test.expectedError { 251 | t.Fatalf("%s Failed: [%s] inputted and error was expected", t.Name(), test.input) 252 | } else if privateWif != test.expectedWif { 253 | t.Fatalf("%s Failed: [%s] inputted [%s] expected but failed comparison of keys, got: %s", t.Name(), test.input, test.expectedWif, privateWif) 254 | } 255 | } 256 | 257 | } 258 | 259 | // ExamplePrivateKeyToWifString example using PrivateKeyToWifString() 260 | func ExamplePrivateKeyToWifString() { 261 | privateWif, err := PrivateKeyToWifString("54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd") 262 | if err != nil { 263 | fmt.Printf("error occurred: %s", err.Error()) 264 | return 265 | } 266 | fmt.Printf("converted wif: %s", privateWif) 267 | 268 | // Output:converted wif: 5JTHas7yTFMBLqgFogxZFf8Vc5uKEbkE7yQAQ2g3xPHo2sNG1Ei 269 | } 270 | 271 | // BenchmarkPrivateKeyToWifString benchmarks the method PrivateKeyToWifString() 272 | func BenchmarkPrivateKeyToWifString(b *testing.B) { 273 | key, _ := CreatePrivateKeyString() 274 | for i := 0; i < b.N; i++ { 275 | _, _ = PrivateKeyToWifString(key) 276 | } 277 | } 278 | 279 | // TestWifToPrivateKey will test the method WifToPrivateKey() 280 | func TestWifToPrivateKey(t *testing.T) { 281 | t.Parallel() 282 | 283 | var tests = []struct { 284 | input string 285 | expectedKey string 286 | expectedNil bool 287 | expectedError bool 288 | }{ 289 | {"", "", true, true}, 290 | {"0", "", true, true}, 291 | {"5HpHagT65TZzG1PH3CSu63k8DbpvD8s5ip4nEB3kEsreAbuatmU", "0000000000000000000000000000000000000000000000000000000000000000", false, false}, 292 | {"5HpHagT65TZzG1PH3CSu63k8DbuTZnNJf6HgyQNymvXmALAsm9s", "0000000000000000000000000000000000006d792070726976617465206b6579", false, false}, 293 | {"54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8azz", "", true, true}, 294 | {"5JTHas7yTFMBLqgFogxZFf8Vc5uKEbkE7yQAQ2g3xPHo2sNG1Ei", "54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd", false, false}, 295 | } 296 | 297 | for _, test := range tests { 298 | if privateKey, err := WifToPrivateKey(test.input); err != nil && !test.expectedError { 299 | t.Fatalf("%s Failed: [%s] inputted and error not expected but got: %s", t.Name(), test.input, err.Error()) 300 | } else if err == nil && test.expectedError { 301 | t.Fatalf("%s Failed: [%s] inputted and error was expected", t.Name(), test.input) 302 | } else if privateKey == nil && !test.expectedNil { 303 | t.Fatalf("%s Failed: [%s] inputted and was nil but not expected", t.Name(), test.input) 304 | } else if privateKey != nil && test.expectedNil { 305 | t.Fatalf("%s Failed: [%s] inputted and was NOT nil but expected to be nil", t.Name(), test.input) 306 | } else if privateKey != nil && hex.EncodeToString(privateKey.Serialise()) != test.expectedKey { 307 | t.Fatalf("%s Failed: [%s] inputted [%s] expected but failed comparison of keys, got: %s", t.Name(), test.input, test.expectedKey, hex.EncodeToString(privateKey.Serialise())) 308 | } 309 | } 310 | } 311 | 312 | // ExampleWifToPrivateKey example using WifToPrivateKey() 313 | func ExampleWifToPrivateKey() { 314 | privateKey, err := WifToPrivateKey("5JTHas7yTFMBLqgFogxZFf8Vc5uKEbkE7yQAQ2g3xPHo2sNG1Ei") 315 | if err != nil { 316 | fmt.Printf("error occurred: %s", err.Error()) 317 | return 318 | } 319 | fmt.Printf("private key: %s", hex.EncodeToString(privateKey.Serialise())) 320 | 321 | // Output:private key: 54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd 322 | } 323 | 324 | // BenchmarkWifToPrivateKey benchmarks the method WifToPrivateKey() 325 | func BenchmarkWifToPrivateKey(b *testing.B) { 326 | for i := 0; i < b.N; i++ { 327 | _, _ = WifToPrivateKey("5JTHas7yTFMBLqgFogxZFf8Vc5uKEbkE7yQAQ2g3xPHo2sNG1Ei") 328 | } 329 | } 330 | 331 | // TestWifToPrivateKeyString will test the method WifToPrivateKeyString() 332 | func TestWifToPrivateKeyString(t *testing.T) { 333 | t.Parallel() 334 | 335 | var tests = []struct { 336 | input string 337 | expectedKey string 338 | expectedError bool 339 | }{ 340 | {"", "", true}, 341 | {"0", "", true}, 342 | {"5HpHagT65TZzG1PH3CSu63k8DbpvD8s5ip4nEB3kEsreAbuatmU", "0000000000000000000000000000000000000000000000000000000000000000", false}, 343 | {"5HpHagT65TZzG1PH3CSu63k8DbuTZnNJf6HgyQNymvXmALAsm9s", "0000000000000000000000000000000000006d792070726976617465206b6579", false}, 344 | {"54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8azz", "", true}, 345 | {"5JTHas7yTFMBLqgFogxZFf8Vc5uKEbkE7yQAQ2g3xPHo2sNG1Ei", "54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd", false}, 346 | } 347 | 348 | for _, test := range tests { 349 | if privateKey, err := WifToPrivateKeyString(test.input); err != nil && !test.expectedError { 350 | t.Fatalf("%s Failed: [%s] inputted and error not expected but got: %s", t.Name(), test.input, err.Error()) 351 | } else if err == nil && test.expectedError { 352 | t.Fatalf("%s Failed: [%s] inputted and error was expected", t.Name(), test.input) 353 | } else if privateKey != test.expectedKey { 354 | t.Fatalf("%s Failed: [%s] inputted [%s] expected but failed comparison of keys, got: %s", t.Name(), test.input, test.expectedKey, privateKey) 355 | } 356 | } 357 | } 358 | 359 | // ExampleWifToPrivateKeyString example using WifToPrivateKeyString() 360 | func ExampleWifToPrivateKeyString() { 361 | privateKey, err := WifToPrivateKeyString("5JTHas7yTFMBLqgFogxZFf8Vc5uKEbkE7yQAQ2g3xPHo2sNG1Ei") 362 | if err != nil { 363 | fmt.Printf("error occurred: %s", err.Error()) 364 | return 365 | } 366 | fmt.Printf("private key: %s", privateKey) 367 | 368 | // Output:private key: 54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd 369 | } 370 | 371 | // BenchmarkWifToPrivateKeyString benchmarks the method WifToPrivateKeyString() 372 | func BenchmarkWifToPrivateKeyString(b *testing.B) { 373 | for i := 0; i < b.N; i++ { 374 | _, _ = WifToPrivateKeyString("5JTHas7yTFMBLqgFogxZFf8Vc5uKEbkE7yQAQ2g3xPHo2sNG1Ei") 375 | } 376 | } 377 | 378 | // TestCreateWif will test the method CreateWif() 379 | func TestCreateWif(t *testing.T) { 380 | t.Run("TestCreateWif", func(t *testing.T) { 381 | t.Parallel() 382 | 383 | // Create a WIF 384 | wifKey, err := CreateWif() 385 | require.NoError(t, err) 386 | require.NotNil(t, wifKey) 387 | // t.Log("WIF:", wifKey.String()) 388 | require.Equalf(t, 51, len(wifKey.String()), "WIF should be 51 characters long, got: %d", len(wifKey.String())) 389 | }) 390 | 391 | t.Run("TestWifToPrivateKey", func(t *testing.T) { 392 | t.Parallel() 393 | 394 | // Create a WIF 395 | wifKey, err := CreateWif() 396 | require.NoError(t, err) 397 | require.NotNil(t, wifKey) 398 | // t.Log("WIF:", wifKey.String()) 399 | require.Equalf(t, 51, len(wifKey.String()), "WIF should be 51 characters long, got: %d", len(wifKey.String())) 400 | 401 | // Convert WIF to Private Key 402 | var privateKey *bec.PrivateKey 403 | privateKey, err = WifToPrivateKey(wifKey.String()) 404 | require.NoError(t, err) 405 | require.NotNil(t, privateKey) 406 | privateKeyString := hex.EncodeToString(privateKey.Serialise()) 407 | // t.Log("Private Key:", privateKeyString) 408 | require.Equalf(t, 64, len(privateKeyString), "Private Key should be 64 characters long, got: %d", len(privateKeyString)) 409 | }) 410 | } 411 | 412 | // ExampleCreateWif example using CreateWif() 413 | func ExampleCreateWif() { 414 | wifKey, err := CreateWif() 415 | if err != nil { 416 | fmt.Println(err) 417 | return 418 | } 419 | fmt.Println("WIF Key Generated Length:", len(wifKey.String())) 420 | // Output: WIF Key Generated Length: 51 421 | } 422 | 423 | // BenchmarkCreateWif benchmarks the method CreateWif() 424 | func BenchmarkCreateWif(b *testing.B) { 425 | for i := 0; i < b.N; i++ { 426 | _, _ = CreateWif() 427 | } 428 | } 429 | 430 | // TestCreateWifString will test the method CreateWifString() 431 | func TestCreateWifString(t *testing.T) { 432 | t.Run("TestCreateWifString", func(t *testing.T) { 433 | t.Parallel() 434 | 435 | // Create a WIF 436 | wifKey, err := CreateWifString() 437 | require.NoError(t, err) 438 | require.NotNil(t, wifKey) 439 | // t.Log("WIF:", wifKey) 440 | require.Equalf(t, 51, len(wifKey), "WIF should be 51 characters long, got: %d", len(wifKey)) 441 | }) 442 | 443 | t.Run("TestWifToPrivateKeyString", func(t *testing.T) { 444 | t.Parallel() 445 | 446 | // Create a WIF 447 | wifKey, err := CreateWifString() 448 | require.NoError(t, err) 449 | require.NotNil(t, wifKey) 450 | // t.Log("WIF:", wifKey) 451 | require.Equalf(t, 51, len(wifKey), "WIF should be 51 characters long, got: %d", len(wifKey)) 452 | 453 | // Convert WIF to Private Key 454 | var privateKeyString string 455 | privateKeyString, err = WifToPrivateKeyString(wifKey) 456 | require.NoError(t, err) 457 | require.NotNil(t, privateKeyString) 458 | // t.Log("Private Key:", privateKeyString) 459 | require.Equalf(t, 64, len(privateKeyString), "Private Key should be 64 characters long, got: %d", len(privateKeyString)) 460 | 461 | }) 462 | } 463 | 464 | // ExampleCreateWifString example using CreateWifString() 465 | func ExampleCreateWifString() { 466 | wifKey, err := CreateWifString() 467 | if err != nil { 468 | fmt.Println(err) 469 | return 470 | } 471 | fmt.Println("WIF Key Generated Length:", len(wifKey)) 472 | // Output: WIF Key Generated Length: 51 473 | } 474 | 475 | // BenchmarkCreateWifString benchmarks the method CreateWifString() 476 | func BenchmarkCreateWifString(b *testing.B) { 477 | for i := 0; i < b.N; i++ { 478 | _, _ = CreateWifString() 479 | } 480 | } 481 | 482 | // TestWifFromString will test the method WifFromString() 483 | func TestWifFromString(t *testing.T) { 484 | t.Run("TestCreateWifFromPrivateKey", func(t *testing.T) { 485 | t.Parallel() 486 | 487 | // Create a Private Key 488 | privateKey, err := CreatePrivateKeyString() 489 | require.NoError(t, err) 490 | require.NotNil(t, privateKey) 491 | 492 | // Create a WIF 493 | var wifKey *wif.WIF 494 | wifKey, err = PrivateKeyToWif(privateKey) 495 | require.NoError(t, err) 496 | require.NotNil(t, wifKey) 497 | wifKeyString := wifKey.String() 498 | t.Log("WIF:", wifKeyString) 499 | require.Equalf(t, 51, len(wifKeyString), "WIF should be 51 characters long, got: %d", len(wifKeyString)) 500 | 501 | // Convert WIF to Private Key 502 | var privateKeyString string 503 | privateKeyString, err = WifToPrivateKeyString(wifKeyString) 504 | require.NoError(t, err) 505 | require.NotNil(t, privateKeyString) 506 | t.Log("Private Key:", privateKeyString) 507 | require.Equalf(t, 64, len(privateKeyString), "Private Key should be 64 characters long, got: %d", len(privateKeyString)) 508 | 509 | // Compare Private Keys 510 | require.Equalf(t, privateKey, privateKeyString, "Private Key should be equal, got: %s", privateKeyString) 511 | 512 | // Decode WIF 513 | var decodedWif *wif.WIF 514 | decodedWif, err = WifFromString(wifKeyString) 515 | require.NoError(t, err) 516 | require.NotNil(t, decodedWif) 517 | require.Equalf(t, wifKeyString, decodedWif.String(), "WIF should be equal, got: %s", decodedWif.String()) 518 | }) 519 | 520 | t.Run("TestWifFromStringMissingWIF", func(t *testing.T) { 521 | t.Parallel() 522 | 523 | _, err := WifFromString("") 524 | require.Error(t, err) 525 | require.Equal(t, ErrWifMissing, err) 526 | }) 527 | 528 | t.Run("TestWifFromStringInvalidWIF", func(t *testing.T) { 529 | t.Parallel() 530 | 531 | _, err := WifFromString("invalid") 532 | require.Error(t, err) 533 | require.Equal(t, "malformed private key", err.Error()) 534 | }) 535 | } 536 | 537 | // ExampleWifFromString example using WifFromString() 538 | func ExampleWifFromString() { 539 | // Create a Private Key 540 | privateKey, err := CreatePrivateKeyString() 541 | if err != nil { 542 | fmt.Println(err) 543 | return 544 | } 545 | fmt.Println("Private Key Generated Length:", len(privateKey)) 546 | 547 | // Create a WIF 548 | var wifKey *wif.WIF 549 | wifKey, err = PrivateKeyToWif(privateKey) 550 | if err != nil { 551 | fmt.Println(err) 552 | return 553 | } 554 | fmt.Println("WIF Key Generated Length:", len(wifKey.String())) 555 | 556 | // Decode WIF 557 | var decodedWif *wif.WIF 558 | decodedWif, err = WifFromString(wifKey.String()) 559 | if err != nil { 560 | fmt.Println(err) 561 | return 562 | } 563 | fmt.Println("WIF Key Decoded Length:", len(decodedWif.String())) 564 | // Output: Private Key Generated Length: 64 565 | // WIF Key Generated Length: 51 566 | // WIF Key Decoded Length: 51 567 | } 568 | 569 | // BenchmarkWifFromString benchmarks the method WifFromString() 570 | func BenchmarkWifFromString(b *testing.B) { 571 | wifKey, _ := CreateWif() 572 | wifString := wifKey.String() 573 | for i := 0; i < b.N; i++ { 574 | _, _ = WifFromString(wifString) 575 | } 576 | } 577 | -------------------------------------------------------------------------------- /pubkey.go: -------------------------------------------------------------------------------- 1 | package bitcoin 2 | 3 | import ( 4 | "encoding/hex" 5 | 6 | "github.com/libsv/go-bk/bec" 7 | ) 8 | 9 | // PubKeyFromPrivateKeyString will derive a pubKey (hex encoded) from a given private key 10 | func PubKeyFromPrivateKeyString(privateKey string, compressed bool) (string, error) { 11 | rawKey, err := PrivateKeyFromString(privateKey) 12 | if err != nil { 13 | return "", err 14 | } 15 | 16 | return PubKeyFromPrivateKey(rawKey, compressed), nil 17 | } 18 | 19 | // PubKeyFromPrivateKey will derive a pubKey (hex encoded) from a given private key 20 | func PubKeyFromPrivateKey(privateKey *bec.PrivateKey, compressed bool) string { 21 | if compressed { 22 | return hex.EncodeToString(privateKey.PubKey().SerialiseCompressed()) 23 | } 24 | return hex.EncodeToString(privateKey.PubKey().SerialiseUncompressed()) 25 | 26 | } 27 | 28 | // PubKeyFromString will convert a pubKey (string) into a pubkey (*bec.PublicKey) 29 | func PubKeyFromString(pubKey string) (*bec.PublicKey, error) { 30 | 31 | // Invalid pubKey 32 | if len(pubKey) == 0 { 33 | return nil, ErrMissingPubKey 34 | } 35 | 36 | // Decode from hex string 37 | decoded, err := hex.DecodeString(pubKey) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | // Parse into a pubKey 43 | return bec.ParsePubKey(decoded, bec.S256()) 44 | } 45 | -------------------------------------------------------------------------------- /pubkey_test.go: -------------------------------------------------------------------------------- 1 | package bitcoin 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/libsv/go-bk/bec" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | // TestPubKeyFromPrivateKeyString will test the method PubKeyFromPrivateKeyString() 13 | func TestPubKeyFromPrivateKeyString(t *testing.T) { 14 | t.Parallel() 15 | 16 | var tests = []struct { 17 | inputKey string 18 | expectedPubKey string 19 | compressed bool 20 | expectedError bool 21 | }{ 22 | {"54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd", "031b8c93100d35bd448f4646cc4678f278351b439b52b303ea31ec9edb5475e73f", true, false}, 23 | {"54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd", "041b8c93100d35bd448f4646cc4678f278351b439b52b303ea31ec9edb5475e73f36e7ef720509250313fcf1b4c5af0dc7c5efa126efe2c3b7008e6f1487c61f31", false, false}, 24 | {"0", "", true, true}, 25 | {"", "", true, true}, 26 | } 27 | 28 | for _, test := range tests { 29 | if pubKey, err := PubKeyFromPrivateKeyString(test.inputKey, test.compressed); err != nil && !test.expectedError { 30 | t.Fatalf("%s Failed: [%s] inputted and error not expected but got: %s", t.Name(), test.inputKey, err.Error()) 31 | } else if err == nil && test.expectedError { 32 | t.Fatalf("%s Failed: [%s] inputted and error was expected", t.Name(), test.inputKey) 33 | } else if pubKey != test.expectedPubKey { 34 | t.Fatalf("%s Failed: [%s] inputted and [%s] expected, but got: %s", t.Name(), test.inputKey, test.expectedPubKey, pubKey) 35 | } 36 | } 37 | } 38 | 39 | // ExamplePubKeyFromPrivateKeyString example using PubKeyFromPrivateKeyString() 40 | func ExamplePubKeyFromPrivateKeyString() { 41 | pubKey, err := PubKeyFromPrivateKeyString("54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd", true) 42 | if err != nil { 43 | fmt.Printf("error occurred: %s", err.Error()) 44 | return 45 | } 46 | fmt.Printf("pubkey generated: %s", pubKey) 47 | // Output:pubkey generated: 031b8c93100d35bd448f4646cc4678f278351b439b52b303ea31ec9edb5475e73f 48 | } 49 | 50 | // BenchmarkPubKeyFromPrivateKeyString benchmarks the method PubKeyFromPrivateKeyString() 51 | func BenchmarkPubKeyFromPrivateKeyString(b *testing.B) { 52 | key, _ := CreatePrivateKeyString() 53 | for i := 0; i < b.N; i++ { 54 | _, _ = PubKeyFromPrivateKeyString(key, true) 55 | } 56 | } 57 | 58 | // TestPubKeyFromPrivateKey will test the method PubKeyFromPrivateKey() 59 | func TestPubKeyFromPrivateKey(t *testing.T) { 60 | t.Parallel() 61 | 62 | priv, err := PrivateKeyFromString("54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd") 63 | assert.NoError(t, err) 64 | assert.NotNil(t, priv) 65 | 66 | var tests = []struct { 67 | inputKey *bec.PrivateKey 68 | expectedPubKey string 69 | expectedError bool 70 | }{ 71 | {priv, "031b8c93100d35bd448f4646cc4678f278351b439b52b303ea31ec9edb5475e73f", false}, 72 | } 73 | 74 | for _, test := range tests { 75 | if pubKey := PubKeyFromPrivateKey(test.inputKey, true); pubKey != test.expectedPubKey { 76 | t.Fatalf("%s Failed: [%v] inputted and [%s] expected, but got: %s", t.Name(), test.inputKey, test.expectedPubKey, pubKey) 77 | } 78 | } 79 | } 80 | 81 | // TestPubKeyFromPrivateKeyPanic tests for nil case in PubKeyFromPrivateKey() 82 | func TestPubKeyFromPrivateKeyPanic(t *testing.T) { 83 | t.Parallel() 84 | 85 | assert.Panics(t, func() { 86 | pubKey := PubKeyFromPrivateKey(nil, true) 87 | assert.NotEqual(t, 0, len(pubKey)) 88 | }) 89 | } 90 | 91 | // ExamplePubKeyFromPrivateKey example using PubKeyFromPrivateKey() 92 | func ExamplePubKeyFromPrivateKey() { 93 | privateKey, err := PrivateKeyFromString("54035dd4c7dda99ac473905a3d82f7864322b49bab1ff441cc457183b9bd8abd") 94 | if err != nil { 95 | fmt.Printf("error occurred: %s", err.Error()) 96 | return 97 | } 98 | 99 | pubKey := PubKeyFromPrivateKey(privateKey, true) 100 | fmt.Printf("pubkey generated: %s", pubKey) 101 | // Output:pubkey generated: 031b8c93100d35bd448f4646cc4678f278351b439b52b303ea31ec9edb5475e73f 102 | } 103 | 104 | // BenchmarkPubKeyFromPrivateKey benchmarks the method PubKeyFromPrivateKey() 105 | func BenchmarkPubKeyFromPrivateKey(b *testing.B) { 106 | key, _ := CreatePrivateKey() 107 | for i := 0; i < b.N; i++ { 108 | _ = PubKeyFromPrivateKey(key, true) 109 | } 110 | } 111 | 112 | // TestPubKeyFromString will test the method PubKeyFromString() 113 | func TestPubKeyFromString(t *testing.T) { 114 | 115 | t.Parallel() 116 | 117 | var tests = []struct { 118 | inputKey string 119 | expectedPubKey string 120 | expectedNil bool 121 | expectedError bool 122 | }{ 123 | {"", "", true, true}, 124 | {"0", "", true, true}, 125 | {"00000", "", true, true}, 126 | {"031b8c93100d35bd448f4646cc4678f278351b439b52b303ea31ec9edb5475e73f", "031b8c93100d35bd448f4646cc4678f278351b439b52b303ea31ec9edb5475e73f", false, false}, 127 | } 128 | 129 | for _, test := range tests { 130 | if pubKey, err := PubKeyFromString(test.inputKey); err != nil && !test.expectedError { 131 | t.Fatalf("%s Failed: [%s] inputted and error not expected but got: %s", t.Name(), test.inputKey, err.Error()) 132 | } else if err == nil && test.expectedError { 133 | t.Fatalf("%s Failed: [%s] inputted and error was expected", t.Name(), test.inputKey) 134 | } else if pubKey != nil && test.expectedNil { 135 | t.Fatalf("%s Failed: [%s] inputted and nil was expected", t.Name(), test.inputKey) 136 | } else if pubKey == nil && !test.expectedNil { 137 | t.Fatalf("%s Failed: [%s] inputted and nil was NOT expected", t.Name(), test.inputKey) 138 | } else if pubKey != nil && hex.EncodeToString(pubKey.SerialiseCompressed()) != test.expectedPubKey { 139 | t.Fatalf("%s Failed: [%s] inputted and [%s] expected, but got: %s", t.Name(), test.inputKey, test.expectedPubKey, hex.EncodeToString(pubKey.SerialiseCompressed())) 140 | } 141 | } 142 | } 143 | 144 | // ExamplePubKeyFromString example using PubKeyFromString() 145 | func ExamplePubKeyFromString() { 146 | pubKey, err := PubKeyFromString("031b8c93100d35bd448f4646cc4678f278351b439b52b303ea31ec9edb5475e73f") 147 | if err != nil { 148 | fmt.Printf("error occurred: %s", err.Error()) 149 | return 150 | } 151 | fmt.Printf("pubkey from string: %s", hex.EncodeToString(pubKey.SerialiseCompressed())) 152 | // Output:pubkey from string: 031b8c93100d35bd448f4646cc4678f278351b439b52b303ea31ec9edb5475e73f 153 | } 154 | 155 | // BenchmarkPubKeyFromString benchmarks the method PubKeyFromString() 156 | func BenchmarkPubKeyFromString(b *testing.B) { 157 | for i := 0; i < b.N; i++ { 158 | _, _ = PubKeyFromString("031b8c93100d35bd448f4646cc4678f278351b439b52b303ea31ec9edb5475e73f") 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /script.go: -------------------------------------------------------------------------------- 1 | package bitcoin 2 | 3 | import ( 4 | "github.com/libsv/go-bt/v2/bscript" 5 | ) 6 | 7 | // ScriptFromAddress will create an output P2PKH script from an address string 8 | func ScriptFromAddress(address string) (string, error) { 9 | // Missing address? 10 | if len(address) == 0 { 11 | return "", ErrMissingAddress 12 | } 13 | 14 | // Generate a script from address 15 | rawScript, err := bscript.NewP2PKHFromAddress(address) 16 | if err != nil { 17 | return "", err 18 | } 19 | 20 | // Return the string version 21 | return rawScript.String(), nil 22 | } 23 | -------------------------------------------------------------------------------- /script_test.go: -------------------------------------------------------------------------------- 1 | package bitcoin 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | // TestScriptFromAddress will test the method ScriptFromAddress() 9 | func TestScriptFromAddress(t *testing.T) { 10 | t.Parallel() 11 | 12 | var tests = []struct { 13 | inputAddress string 14 | expectedScript string 15 | expectedError bool 16 | }{ 17 | {"", "", true}, 18 | {"0", "", true}, 19 | {"1234567", "", true}, 20 | {"1HRVqUGDzpZSMVuNSZxJVaB9xjneEShfA7", "76a914b424110292f4ea2ac92beb9e83cf5e6f0fa2996388ac", false}, 21 | {"13Rj7G3pn2GgG8KE6SFXLc7dCJdLNnNK7M", "76a9141a9d62736746f85ca872dc555ff51b1fed2471e288ac", false}, 22 | } 23 | 24 | for _, test := range tests { 25 | if script, err := ScriptFromAddress(test.inputAddress); err != nil && !test.expectedError { 26 | t.Fatalf("%s Failed: [%v] inputted and error not expected but got: %s", t.Name(), test.inputAddress, err.Error()) 27 | } else if err == nil && test.expectedError { 28 | t.Fatalf("%s Failed: [%v] inputted and error was expected", t.Name(), test.inputAddress) 29 | } else if script != test.expectedScript { 30 | t.Fatalf("%s Failed: [%v] inputted [%s] expected but failed comparison of scripts, got: %s", t.Name(), test.inputAddress, test.expectedScript, script) 31 | } 32 | } 33 | } 34 | 35 | // ExampleScriptFromAddress example using ScriptFromAddress() 36 | func ExampleScriptFromAddress() { 37 | script, err := ScriptFromAddress("1HRVqUGDzpZSMVuNSZxJVaB9xjneEShfA7") 38 | if err != nil { 39 | fmt.Printf("error occurred: %s", err.Error()) 40 | return 41 | } 42 | fmt.Printf("script generated: %s", script) 43 | // Output:script generated: 76a914b424110292f4ea2ac92beb9e83cf5e6f0fa2996388ac 44 | } 45 | 46 | // BenchmarkScriptFromAddress benchmarks the method ScriptFromAddress() 47 | func BenchmarkScriptFromAddress(b *testing.B) { 48 | for i := 0; i < b.N; i++ { 49 | _, _ = ScriptFromAddress("1HRVqUGDzpZSMVuNSZxJVaB9xjneEShfA7") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /sign.go: -------------------------------------------------------------------------------- 1 | package bitcoin 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | 7 | "github.com/bitcoinsv/bsvd/chaincfg/chainhash" 8 | "github.com/bitcoinsv/bsvd/wire" 9 | "github.com/libsv/go-bk/bec" 10 | ) 11 | 12 | // SignMessage signs a string with the provided private key using Bitcoin Signed Message encoding 13 | // sigRefCompressedKey bool determines whether the signature will reference a compressed or uncompresed key 14 | // Spec: https://docs.moneybutton.com/docs/bsv-message.html 15 | func SignMessage(privateKey string, message string, sigRefCompressedKey bool) (string, error) { 16 | if len(privateKey) == 0 { 17 | return "", ErrPrivateKeyMissing 18 | } 19 | 20 | var buf bytes.Buffer 21 | var err error 22 | if err = wire.WriteVarString(&buf, 0, hBSV); err != nil { 23 | return "", err 24 | } 25 | if err = wire.WriteVarString(&buf, 0, message); err != nil { 26 | return "", err 27 | } 28 | 29 | // Create the hash 30 | messageHash := chainhash.DoubleHashB(buf.Bytes()) 31 | 32 | // Get the private key 33 | var ecdsaPrivateKey *bec.PrivateKey 34 | if ecdsaPrivateKey, err = PrivateKeyFromString(privateKey); err != nil { 35 | return "", err 36 | } 37 | 38 | // Sign 39 | var sigBytes []byte 40 | if sigBytes, err = bec.SignCompact(bec.S256(), ecdsaPrivateKey, messageHash, sigRefCompressedKey); err != nil { 41 | return "", err 42 | } 43 | 44 | return base64.StdEncoding.EncodeToString(sigBytes), nil 45 | } 46 | -------------------------------------------------------------------------------- /sign_test.go: -------------------------------------------------------------------------------- 1 | package bitcoin 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestSigningCompression(t *testing.T) { 9 | testKey := "0499f8239bfe10eb0f5e53d543635a423c96529dd85fa4bad42049a0b435ebdd" 10 | testData := "test message" 11 | 12 | // Test sign uncompressed 13 | address, err := GetAddressFromPrivateKeyString(testKey, false) 14 | if err != nil { 15 | t.Errorf("Get address err %s", err) 16 | } 17 | sig, err := SignMessage(testKey, testData, false) 18 | if err != nil { 19 | t.Errorf("Failed to sign uncompressed %s", err) 20 | } 21 | 22 | err = VerifyMessage(address, sig, testData) 23 | 24 | if err != nil { 25 | t.Errorf("Failed to validate uncompressed %s", err) 26 | } 27 | 28 | // Test sign compressed 29 | address, err = GetAddressFromPrivateKeyString(testKey, true) 30 | if err != nil { 31 | t.Errorf("Get address err %s", err) 32 | } 33 | sig, err = SignMessage(testKey, testData, true) 34 | if err != nil { 35 | t.Errorf("Failed to sign compressed %s", err) 36 | } 37 | 38 | err = VerifyMessage(address, sig, testData) 39 | 40 | if err != nil { 41 | t.Errorf("Failed to validate compressed %s", err) 42 | } 43 | } 44 | 45 | // TestSignMessage will test the method SignMessage() 46 | func TestSignMessage(t *testing.T) { 47 | 48 | t.Parallel() 49 | 50 | var tests = []struct { 51 | inputKey string 52 | inputMessage string 53 | expectedSignature string 54 | expectedError bool 55 | }{ 56 | { 57 | "0499f8239bfe10eb0f5e53d543635a423c96529dd85fa4bad42049a0b435ebdd", 58 | "test message", 59 | "HFxPx8JHsCiivB+DW/RgNpCLT6yG3j436cUNWKekV3ORBrHNChIjeVReyAco7PVmmDtVD3POs9FhDlm/nk5I6O8=", 60 | false, 61 | }, 62 | { 63 | "ef0b8bad0be285099534277fde328f8f19b3be9cadcd4c08e6ac0b5f863745ac", 64 | "This is a test message", 65 | "G+zZagsyz7ioC/ZOa5EwsaKice0vs2BvZ0ljgkFHxD3vGsMlGeD4sXHEcfbI4h8lP29VitSBdf4A+nHXih7svf4=", 66 | false, 67 | }, 68 | { 69 | "0499f8239bfe10eb0f5e53d543635a423c96529dd85fa4bad42049a0b435ebdd", 70 | "This time I'm writing a new message that is obnixiously long af. This time I'm writing a new message that is obnixiously long af. This time I'm writing a new message that is obnixiously long af. This time I'm writing a new message that is obnixiously long af. This time I'm writing a new message that is obnixiously long af. This time I'm writing a new message that is obnixiously long af. This time I'm writing a new message that is obnixiously long af. This time I'm writing a new message that is obnixiously long af. This time I'm writing a new message that is obnixiously long af. This time I'm writing a new message that is obnixiously long af. This time I'm writing a new message that is obnixiously long af. This time I'm writing a new message that is obnixiously long af. This time I'm writing a new message that is obnixiously long af. This time I'm writing a new message that is obnixiously long af.", 71 | "GxRcFXQc7LHxFNpK5lzhR+LF5ixIvhB089bxYzTAV02yGHm/3ALxltz/W4lGp77Q5UTxdj+TU+96mdAcJ5b/fGs=", 72 | false, 73 | }, 74 | { 75 | "93596babb564cbbdc84f2370c710b9bcc94333495b60af719b5fcf9ba00ba82c", 76 | "This is a test message", 77 | "HIuDw09ffPgEDuxEw5yHVp1+mi4QpuhAwLyQdpMTfsHCOkMqTKXuP7dSNWMEJqZsiQ8eKMDRvf2wZ4e5bxcu4O0=", 78 | false, 79 | }, 80 | { 81 | "50381cf8f52936faae4a05a073a03d688a9fa206d005e87a39da436c75476d78", 82 | "This is a test message", 83 | "HLBmbjCY2Z7eSXGXZoBI3x2ZRaYUYOGtEaDjXetaY+zNDtMOvagsOGEHnVT3f5kXlEbuvmPydHqLnyvZP3cDOWk=", 84 | false, 85 | }, 86 | { 87 | "c7726663147afd1add392d129086e57c0b05aa66a6ded564433c04bd55741434", 88 | "This is a test message", 89 | "HOI207QUnTLr2Ll+s4kUxNgLgorkc/Z5Pc+XNvUBYLy2TxaU6oHEJ2TTJ1mZVrtUyHm6e315v1tIjeosW3Odfqw=", 90 | false, 91 | }, 92 | { 93 | "c7726663147afd1add392d129086e57c0b05aa66a6ded564433c04bd55741434", 94 | "1", 95 | "HMcRFG1VNN9TDGXpCU+9CqKLNOuhwQiXI5hZpkTOuYHKBDOWayNuAABofYLqUHYTMiMf9mYFQ0sPgFJZz3F7ELQ=", 96 | false, 97 | }, 98 | { 99 | "", 100 | "This is a test message", 101 | "", 102 | true, 103 | }, 104 | { 105 | "0", 106 | "This is a test message", 107 | "", 108 | true, 109 | }, 110 | { 111 | "0000000", 112 | "This is a test message", 113 | "", 114 | true, 115 | }, 116 | { 117 | "c7726663147afd1add392d129086e57c0b", 118 | "This is a test message", 119 | "G6N+iPf23i2YkLsNzF/yyeBm9eSYBoY/HFV1Md1F0ElWBXW5E5mkdRtgjoRuq0yNb1CCFNWWlkn2gZknFJNUFJ8=", 120 | false, 121 | }, 122 | } 123 | 124 | for idx, test := range tests { 125 | if signature, err := SignMessage(test.inputKey, test.inputMessage, false); err != nil && !test.expectedError { 126 | t.Fatalf("%d %s Failed: [%s] [%s] inputted and error not expected but got: %s", idx, t.Name(), test.inputKey, test.inputMessage, err.Error()) 127 | } else if err == nil && test.expectedError { 128 | t.Fatalf("%d %s Failed: [%s] [%s] inputted and error was expected", idx, t.Name(), test.inputKey, test.inputMessage) 129 | } else if signature != test.expectedSignature { 130 | t.Fatalf("%d %s Failed: [%s] [%s] inputted [%s] expected but got: %s", idx, t.Name(), test.inputKey, test.inputMessage, test.expectedSignature, signature) 131 | } 132 | } 133 | } 134 | 135 | // ExampleSignMessage example using SignMessage() 136 | func ExampleSignMessage() { 137 | signature, err := SignMessage("ef0b8bad0be285099534277fde328f8f19b3be9cadcd4c08e6ac0b5f863745ac", "This is a test message", false) 138 | if err != nil { 139 | fmt.Printf("error occurred: %s", err.Error()) 140 | return 141 | } 142 | fmt.Printf("signature created: %s", signature) 143 | // Output:signature created: G+zZagsyz7ioC/ZOa5EwsaKice0vs2BvZ0ljgkFHxD3vGsMlGeD4sXHEcfbI4h8lP29VitSBdf4A+nHXih7svf4= 144 | } 145 | 146 | // BenchmarkSignMessage benchmarks the method SignMessage() 147 | func BenchmarkSignMessage(b *testing.B) { 148 | key, _ := CreatePrivateKeyString() 149 | for i := 0; i < b.N; i++ { 150 | _, _ = SignMessage(key, "This is a test message", false) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /transaction.go: -------------------------------------------------------------------------------- 1 | package bitcoin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/libsv/go-bk/bec" 9 | "github.com/libsv/go-bt/v2" 10 | "github.com/libsv/go-bt/v2/bscript" 11 | "github.com/libsv/go-bt/v2/unlocker" 12 | ) 13 | 14 | const ( 15 | 16 | // DustLimit is the minimum value for a tx that can be spent 17 | // Note: this is being deprecated in the new node software (TBD) 18 | DustLimit uint64 = 546 19 | ) 20 | 21 | // Utxo is an unspent transaction output 22 | type Utxo struct { 23 | Satoshis uint64 `json:"satoshis"` 24 | ScriptPubKey string `json:"string"` 25 | TxID string `json:"tx_id"` 26 | Vout uint32 `json:"vout"` 27 | } 28 | 29 | // PayToAddress is the pay-to-address 30 | type PayToAddress struct { 31 | Address string `json:"address"` 32 | Satoshis uint64 `json:"satoshis"` 33 | } 34 | 35 | // account is a struct/interface for implementing unlocker 36 | type account struct { 37 | PrivateKey *bec.PrivateKey 38 | } 39 | 40 | // Unlocker get the correct un-locker for a given locking script 41 | func (a *account) Unlocker(context.Context, *bscript.Script) (bt.Unlocker, error) { 42 | return &unlocker.Simple{ 43 | PrivateKey: a.PrivateKey, 44 | }, nil 45 | } 46 | 47 | // OpReturnData is the op return data to include in the tx 48 | type OpReturnData [][]byte 49 | 50 | // TxFromHex will return a libsv.tx from a raw hex string 51 | func TxFromHex(rawHex string) (*bt.Tx, error) { 52 | return bt.NewTxFromString(rawHex) 53 | } 54 | 55 | // CreateTxWithChange will automatically create the change output and calculate fees 56 | // 57 | // Use this if you don't want to figure out fees/change for a tx 58 | // USE AT YOUR OWN RISK - this will modify a "pay-to" output to accomplish auto-fees 59 | func CreateTxWithChange(utxos []*Utxo, payToAddresses []*PayToAddress, opReturns []OpReturnData, 60 | changeAddress string, standardRate, dataRate *bt.Fee, 61 | privateKey *bec.PrivateKey) (*bt.Tx, error) { 62 | 63 | // Missing utxo(s) or change address 64 | if len(utxos) == 0 { 65 | return nil, ErrUtxosRequired 66 | } else if len(changeAddress) == 0 { 67 | return nil, ErrChangeAddressRequired 68 | } 69 | 70 | // Accumulate the total satoshis from all utxo(s) 71 | var totalSatoshis uint64 72 | var totalPayToSatoshis uint64 73 | var remainder uint64 74 | var hasChange bool 75 | 76 | // Loop utxos and get total usable satoshis 77 | for _, utxo := range utxos { 78 | totalSatoshis += utxo.Satoshis 79 | } 80 | 81 | // Loop all payout address amounts 82 | for _, address := range payToAddresses { 83 | totalPayToSatoshis += address.Satoshis 84 | } 85 | 86 | // Sanity check - already not enough satoshis? 87 | if totalPayToSatoshis > totalSatoshis { 88 | return nil, fmt.Errorf( 89 | "not enough in utxo(s) to cover: %d + (fee), total found: %d", 90 | totalPayToSatoshis, 91 | totalSatoshis, 92 | ) 93 | } 94 | 95 | // Add the change address as the difference (all change except 1 sat for Draft tx) 96 | // Only if the tx is NOT for the full amount 97 | if totalPayToSatoshis != totalSatoshis { 98 | hasChange = true 99 | payToAddresses = append(payToAddresses, &PayToAddress{ 100 | Address: changeAddress, 101 | Satoshis: totalSatoshis - (totalPayToSatoshis + 1), 102 | }) 103 | } 104 | 105 | // Create the "Draft tx" 106 | fee, err := draftTx(utxos, payToAddresses, opReturns, privateKey, standardRate, dataRate) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | // Check that we have enough to cover the fee 112 | if (totalPayToSatoshis + fee) > totalSatoshis { 113 | 114 | // Remove temporary change address first 115 | if hasChange { 116 | payToAddresses = payToAddresses[:len(payToAddresses)-1] 117 | } 118 | 119 | // Re-run draft tx with no change address 120 | if fee, err = draftTx( 121 | utxos, payToAddresses, opReturns, privateKey, standardRate, dataRate, 122 | ); err != nil { 123 | return nil, err 124 | } 125 | 126 | // Get the remainder missing (handle negative overflow safer) 127 | totalToPay := totalPayToSatoshis + fee 128 | if totalToPay >= totalSatoshis { 129 | remainder = totalToPay - totalSatoshis 130 | } else { 131 | remainder = totalSatoshis - totalToPay 132 | } 133 | 134 | // Remove remainder from last used payToAddress (or continue until found) 135 | feeAdjusted := false 136 | for i := len(payToAddresses) - 1; i >= 0; i-- { // Working backwards 137 | if payToAddresses[i].Satoshis > remainder { 138 | payToAddresses[i].Satoshis = payToAddresses[i].Satoshis - remainder 139 | feeAdjusted = true 140 | break 141 | } 142 | } 143 | 144 | // Fee was not adjusted (all inputs do not cover the fee) 145 | if !feeAdjusted { 146 | return nil, fmt.Errorf( 147 | "auto-fee could not be applied without removing an output (payTo %d) "+ 148 | "(amount %d) (remainder %d) (fee %d) (total %d)", 149 | len(payToAddresses), totalPayToSatoshis, remainder, fee, totalSatoshis, 150 | ) 151 | } 152 | 153 | } else { 154 | 155 | // Remove the change address (old version with original satoshis) 156 | // Add the change address as the difference (now with adjusted fee) 157 | if hasChange { 158 | payToAddresses = payToAddresses[:len(payToAddresses)-1] 159 | 160 | payToAddresses = append(payToAddresses, &PayToAddress{ 161 | Address: changeAddress, 162 | Satoshis: totalSatoshis - (totalPayToSatoshis + fee), 163 | }) 164 | } 165 | } 166 | 167 | // Create the "Final tx" (or error) 168 | return CreateTx(utxos, payToAddresses, opReturns, privateKey) 169 | } 170 | 171 | // draftTx is a helper method to create a draft tx and associated fees 172 | func draftTx(utxos []*Utxo, payToAddresses []*PayToAddress, opReturns []OpReturnData, 173 | privateKey *bec.PrivateKey, standardRate, dataRate *bt.Fee) (uint64, error) { 174 | 175 | // Create the "Draft tx" 176 | tx, err := CreateTx(utxos, payToAddresses, opReturns, privateKey) 177 | if err != nil { 178 | return 0, err 179 | } 180 | 181 | // Calculate the fees for the "Draft tx" 182 | // todo: hack to add 1 extra sat - ensuring that fee is over the minimum with rounding issues in WOC and other systems 183 | fee := CalculateFeeForTx(tx, standardRate, dataRate) + 1 184 | return fee, nil 185 | } 186 | 187 | // CreateTxWithChangeUsingWif will automatically create the change output and calculate fees 188 | // 189 | // Use this if you don't want to figure out fees/change for a tx 190 | // USE AT YOUR OWN RISK - this will modify a "pay-to" output to accomplish auto-fees 191 | func CreateTxWithChangeUsingWif(utxos []*Utxo, payToAddresses []*PayToAddress, opReturns []OpReturnData, 192 | changeAddress string, standardRate, dataRate *bt.Fee, wif string) (*bt.Tx, error) { 193 | 194 | // Decode the WIF 195 | privateKey, err := WifToPrivateKey(wif) 196 | if err != nil { 197 | return nil, err 198 | } 199 | 200 | // Create the "Final tx" (or error) 201 | return CreateTxWithChange(utxos, payToAddresses, opReturns, changeAddress, standardRate, dataRate, privateKey) 202 | } 203 | 204 | // CreateTx will create a basic transaction and return the raw transaction (*transaction.Transaction) 205 | // 206 | // Note: this will NOT create a change output (funds are sent to "addresses") 207 | // Note: this will NOT handle fee calculation (it's assumed you have already calculated the fee) 208 | // 209 | // Get the raw hex version: tx.ToString() 210 | // Get the tx id: tx.GetTxID() 211 | func CreateTx(utxos []*Utxo, addresses []*PayToAddress, 212 | opReturns []OpReturnData, privateKey *bec.PrivateKey) (*bt.Tx, error) { 213 | 214 | // Start creating a new transaction 215 | tx := bt.NewTx() 216 | 217 | // Accumulate the total satoshis from all utxo(s) 218 | var totalSatoshis uint64 219 | 220 | // Loop all utxos and add to the transaction 221 | var err error 222 | for _, utxo := range utxos { 223 | if err = tx.From(utxo.TxID, utxo.Vout, utxo.ScriptPubKey, utxo.Satoshis); err != nil { 224 | return nil, err 225 | } 226 | totalSatoshis += utxo.Satoshis 227 | } 228 | 229 | // Loop any pay addresses 230 | for _, address := range addresses { 231 | var a *bscript.Script 232 | a, err = bscript.NewP2PKHFromAddress(address.Address) 233 | if err != nil { 234 | return nil, err 235 | } 236 | 237 | if err = tx.PayTo(a, address.Satoshis); err != nil { 238 | return nil, err 239 | } 240 | } 241 | 242 | // Loop any op returns 243 | for _, op := range opReturns { 244 | if err = tx.AddOpReturnPartsOutput(op); err != nil { 245 | return nil, err 246 | } 247 | } 248 | 249 | // If inputs are supplied, make sure they are sufficient for this transaction 250 | if len(tx.Inputs) > 0 { 251 | // Sanity check - not enough satoshis in utxo(s) to cover all paid amount(s) 252 | // They should never be equal, since the fee is the spread between the two amounts 253 | totalOutputSatoshis := tx.TotalOutputSatoshis() // Does not work properly 254 | if totalOutputSatoshis > totalSatoshis { 255 | return nil, fmt.Errorf("not enough in utxo(s) to cover: %d + (fee) found: %d", totalOutputSatoshis, totalSatoshis) 256 | } 257 | } 258 | 259 | // Sign the transaction 260 | if privateKey != nil { 261 | myAccount := &account{PrivateKey: privateKey} 262 | // todo: support context (ctx) 263 | if err = tx.FillAllInputs(context.Background(), myAccount); err != nil { 264 | return nil, err 265 | } 266 | } 267 | 268 | // Return the transaction as a raw string 269 | return tx, nil 270 | } 271 | 272 | // CreateTxUsingWif will create a basic transaction and return the raw transaction (*transaction.Transaction) 273 | // 274 | // Note: this will NOT create a "change" address (it's assumed you have already specified an address) 275 | // Note: this will NOT handle "fee" calculation (it's assumed you have already calculated the fee) 276 | // 277 | // Get the raw hex version: tx.ToString() 278 | // Get the tx id: tx.GetTxID() 279 | func CreateTxUsingWif(utxos []*Utxo, addresses []*PayToAddress, 280 | opReturns []OpReturnData, wif string) (*bt.Tx, error) { 281 | 282 | // Decode the WIF 283 | privateKey, err := WifToPrivateKey(wif) 284 | if err != nil { 285 | return nil, err 286 | } 287 | 288 | // Create the Tx 289 | return CreateTx(utxos, addresses, opReturns, privateKey) 290 | } 291 | 292 | // DefaultStandardFee returns the default standard fees offered by most miners. 293 | // this function is not public anymore in go-bt 294 | func DefaultStandardFee() *bt.Fee { 295 | return &bt.Fee{ 296 | FeeType: bt.FeeTypeStandard, 297 | MiningFee: bt.FeeUnit{ 298 | Satoshis: 5, 299 | Bytes: 10, 300 | }, 301 | RelayFee: bt.FeeUnit{ 302 | Satoshis: 5, 303 | Bytes: 10, 304 | }, 305 | } 306 | } 307 | 308 | // CalculateFeeForTx will estimate a fee for the given transaction 309 | // 310 | // If tx is nil this will panic 311 | // Rate(s) can be derived from MinerAPI (default is DefaultDataRate and DefaultStandardRate) 312 | // If rate is nil it will use default rates (0.5 sat per byte) 313 | // Reference: https://tncpw.co/c215a75c 314 | func CalculateFeeForTx(tx *bt.Tx, standardRate, dataRate *bt.Fee) uint64 { 315 | 316 | // Set the totals 317 | var totalFee int 318 | var totalDataBytes int 319 | 320 | // Set defaults if not found 321 | if standardRate == nil { 322 | standardRate = DefaultStandardFee() 323 | } 324 | if dataRate == nil { 325 | dataRate = DefaultStandardFee() 326 | // todo: adjusted to 5/10 for now, since all miners accept that rate 327 | dataRate.FeeType = bt.FeeTypeData 328 | } 329 | 330 | // Set the total bytes of the tx 331 | totalBytes := len(tx.Bytes()) 332 | 333 | // Loop all outputs and accumulate size (find data related outputs) 334 | for _, out := range tx.Outputs { 335 | outHexString := out.LockingScriptHexString() 336 | if strings.HasPrefix(outHexString, "006a") || strings.HasPrefix(outHexString, "6a") { 337 | totalDataBytes += len(out.Bytes()) 338 | } 339 | } 340 | 341 | // Got some data bytes? 342 | if totalDataBytes > 0 { 343 | totalBytes = totalBytes - totalDataBytes 344 | totalFee += (dataRate.MiningFee.Satoshis * totalDataBytes) / dataRate.MiningFee.Bytes 345 | } 346 | 347 | // Still have regular standard bytes? 348 | if totalBytes > 0 { 349 | totalFee += (standardRate.MiningFee.Satoshis * totalBytes) / standardRate.MiningFee.Bytes 350 | } 351 | 352 | // Safety check (possible division by zero?) 353 | if totalFee == 0 { 354 | totalFee = 1 355 | } 356 | 357 | // Return the total fee as an uint (easier to use with satoshi values) 358 | return uint64(totalFee) 359 | } 360 | -------------------------------------------------------------------------------- /verify.go: -------------------------------------------------------------------------------- 1 | package bitcoin 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/hex" 7 | "fmt" 8 | 9 | "github.com/bitcoinsv/bsvd/chaincfg/chainhash" 10 | "github.com/bitcoinsv/bsvd/wire" 11 | "github.com/libsv/go-bk/bec" 12 | "github.com/libsv/go-bt/v2/bscript" 13 | ) 14 | 15 | const ( 16 | // hBSV is the magic header string required fore Bitcoin Signed Messages 17 | hBSV string = "Bitcoin Signed Message:\n" 18 | ) 19 | 20 | // PubKeyFromSignature gets a publickey for a signature and tells you whether is was compressed 21 | func PubKeyFromSignature(sig, data string) (pubKey *bec.PublicKey, wasCompressed bool, err error) { 22 | 23 | var decodedSig []byte 24 | if decodedSig, err = base64.StdEncoding.DecodeString(sig); err != nil { 25 | return nil, false, err 26 | } 27 | 28 | // Validate the signature - this just shows that it was valid at all 29 | // we will compare it with the key next 30 | var buf bytes.Buffer 31 | if err = wire.WriteVarString(&buf, 0, hBSV); err != nil { 32 | return nil, false, err 33 | } 34 | if err = wire.WriteVarString(&buf, 0, data); err != nil { 35 | return nil, false, err 36 | } 37 | 38 | // Create the hash 39 | expectedMessageHash := chainhash.DoubleHashB(buf.Bytes()) 40 | return bec.RecoverCompact(bec.S256(), decodedSig, expectedMessageHash) 41 | } 42 | 43 | // VerifyMessage verifies a string and address against the provided 44 | // signature and assumes Bitcoin Signed Message encoding. 45 | // The key referenced by the signature must relate to the address provided. 46 | // Do not provide an address from an uncompressed key along with 47 | // a signature from a compressed key 48 | // 49 | // Error will occur if verify fails or verification is not successful (no bool) 50 | // Spec: https://docs.moneybutton.com/docs/bsv-message.html 51 | func VerifyMessage(address, sig, data string) error { 52 | 53 | // Reconstruct the pubkey 54 | publicKey, wasCompressed, err := PubKeyFromSignature(sig, data) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | // Get the address 60 | var bscriptAddress *bscript.Address 61 | if bscriptAddress, err = GetAddressFromPubKey(publicKey, wasCompressed); err != nil { 62 | return err 63 | } 64 | 65 | // Return nil if addresses match. 66 | if bscriptAddress.AddressString == address { 67 | return nil 68 | } 69 | return fmt.Errorf( 70 | "address (%s) not found - compressed: %t\n%s was found instead", 71 | address, 72 | wasCompressed, 73 | bscriptAddress.AddressString, 74 | ) 75 | } 76 | 77 | // VerifyMessageDER will take a message string, a public key string and a signature string 78 | // (in strict DER format) and verify that the message was signed by the public key. 79 | // 80 | // Copyright (c) 2019 Bitcoin Association 81 | // License: https://github.com/bitcoin-sv/merchantapi-reference/blob/master/LICENSE 82 | // 83 | // Source: https://github.com/bitcoin-sv/merchantapi-reference/blob/master/handler/global.go 84 | func VerifyMessageDER(hash [32]byte, pubKey string, signature string) (verified bool, err error) { 85 | 86 | // Decode the signature string 87 | var sigBytes []byte 88 | if sigBytes, err = hex.DecodeString(signature); err != nil { 89 | return 90 | } 91 | 92 | // Parse the signature 93 | var sig *bec.Signature 94 | if sig, err = bec.ParseDERSignature(sigBytes, bec.S256()); err != nil { 95 | return 96 | } 97 | 98 | // Decode the pubKey 99 | var pubKeyBytes []byte 100 | if pubKeyBytes, err = hex.DecodeString(pubKey); err != nil { 101 | return 102 | } 103 | 104 | // Parse the pubKey 105 | var rawPubKey *bec.PublicKey 106 | if rawPubKey, err = bec.ParsePubKey(pubKeyBytes, bec.S256()); err != nil { 107 | return 108 | } 109 | 110 | // Verify the signature against the pubKey 111 | verified = sig.Verify(hash[:], rawPubKey) 112 | return 113 | } 114 | -------------------------------------------------------------------------------- /verify_test.go: -------------------------------------------------------------------------------- 1 | package bitcoin 2 | 3 | import ( 4 | "crypto/sha256" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | const ( 12 | testDERSignature = "3045022100b976be863fffd361716b375a9a5c4e77073dfaa29d2b9af9addef94f029c2d0902205b1fffc58343f3d4bd8fc48a118e998072c655d318061e13e1ef0902fb42e15c" 13 | testDERPubKey = "03e92d3e5c3f7bd945dfbf48e7a99393b1bfb3f11f380ae30d286e7ff2aec5a270" 14 | ) 15 | 16 | // TestVerifyMessage will test the method VerifyMessage() 17 | func TestVerifyMessage(t *testing.T) { 18 | 19 | t.Parallel() 20 | 21 | var tests = []struct { 22 | inputAddress string 23 | inputSignature string 24 | inputData string 25 | expectedError bool 26 | }{ 27 | { 28 | "12SsqqYk43kggMBpSvWHwJwR31NsgMePKS", 29 | "HFxPx8JHsCiivB+DW/RgNpCLT6yG3j436cUNWKekV3ORBrHNChIjeVReyAco7PVmmDtVD3POs9FhDlm/nk5I6O8=", 30 | "test message", 31 | false, 32 | }, 33 | { 34 | "1LN5p7Eg9Zju1b4g4eFPTBMPoMZGCxzrET", 35 | "IKmgOFrfWRffRNjrQcJQHSBD7WL2di+4doWdaz/a/p5RUiT7ErpUqbYeLi0yzmONFaV8uLWF2vydTjA8W8KnjZU=", 36 | "This time I'm writing a new message that is obnoxiously long af. This time I'm writing a " + 37 | "new message that is obnoxiously long af. This time I'm writing a new message that is obnoxiously " + 38 | "long af. This time I'm writing a new message that is obnoxiously long af. This time I'm writing a " + 39 | "new message that is obnoxiously long af. This time I'm writing a new message that is obnoxiously " + 40 | "long af. This time I'm writing a new message that is obnoxiously long af. This time I'm writing a " + 41 | "new message that is obnoxiously long af. This time I'm writing a new message that is obnoxiously " + 42 | "long af. This time I'm writing a new message that is obnoxiously long af. This time I'm writing a " + 43 | "new message that is obnoxiously long af. This time I'm writing a new message that is obnoxiously " + 44 | "long af. This time I'm writing a new message that is obnoxiously long af. This time I'm writing a " + 45 | "new message that is obnoxiously long af.", 46 | false, 47 | }, 48 | { 49 | "1LN5p7Eg9Zju1b4g4eFPTBMPoMZGCxzrET", 50 | "IBDscOd/Ov4yrd/YXantqajSAnW4fudpfr2KQy5GNo9pZybF12uNaal4KI822UpQLS/UJD+UK2SnNMn6Z3E4na8=", 51 | "Testing!", 52 | true, 53 | }, 54 | { 55 | "1FiyJnrgwBc3Ff83V1yRWAkmXBdGrDQnXQ", 56 | "", 57 | "Testing!", 58 | true, 59 | }, 60 | { 61 | "1FiyJnrgwBc3Ff83V1yRWAkmXBdGrDQnXQ", 62 | "IBDscOd/Ov4yrd/YXantqajSAnW4fudpfr2KQy5GNo9pZybF12uNaal4KI822UpQLS/UJD+UK2SnNMn6Z3E4na8=", 63 | "", 64 | true, 65 | }, 66 | { 67 | "0", 68 | "IBDscOd/Ov4yrd/YXantqajSAnW4fudpfr2KQy5GNo9pZybF12uNaal4KI822UpQLS/UJD+UK2SnNMn6Z3E4na8=", 69 | "Testing!", 70 | true, 71 | }, 72 | { 73 | "1FiyJnrgwBc3Ff83V1yRWAkmXBdGrDQnXQ", 74 | "GBDscOd/Ov4yrd/YXantqajSAnW4fudpfr2KQy5GNo9pZybF12uNaal4KI822UpQLS/UJD+UK2SnNMn6Z3E4naZ=", 75 | "Testing!", 76 | true, 77 | }, 78 | { 79 | "1FiyJnrgwBc3Ff83V1yRWAkmXBdGrDQnXQ", 80 | "GBD=", 81 | "Testing!", 82 | true, 83 | }, 84 | { 85 | "1FiyJnrgwBc3Ff83V1yRWAkmXBdGrDQnXQ", 86 | "GBse5w0f839t8wej8f2D=", 87 | "Testing!", 88 | true, 89 | }, 90 | } 91 | 92 | for _, test := range tests { 93 | if err := VerifyMessage(test.inputAddress, test.inputSignature, test.inputData); err != nil && !test.expectedError { 94 | t.Fatalf("%s Failed: [%s] [%s] [%s] inputted and error not expected but got: %s", t.Name(), test.inputAddress, test.inputSignature, test.inputData, err.Error()) 95 | } else if err == nil && test.expectedError { 96 | t.Fatalf("%s Failed: [%s] [%s] [%s] inputted and error was expected", t.Name(), test.inputAddress, test.inputSignature, test.inputData) 97 | } 98 | } 99 | 100 | } 101 | 102 | // ExampleVerifyMessage example using VerifyMessage() 103 | func ExampleVerifyMessage() { 104 | if err := VerifyMessage( 105 | "1FiyJnrgwBc3Ff83V1yRWAkmXBdGrDQnXQ", 106 | "IBDscOd/Ov4yrd/YXantqajSAnW4fudpfr2KQy5GNo9pZybF12uNaal4KI822UpQLS/UJD+UK2SnNMn6Z3E4na8=", 107 | "Testing!", 108 | ); err != nil { 109 | fmt.Printf("error occurred: %s", err.Error()) 110 | return 111 | } 112 | fmt.Printf("verification passed") 113 | // Output:verification passed 114 | } 115 | 116 | // BenchmarkVerifyMessage benchmarks the method VerifyMessage() 117 | func BenchmarkVerifyMessage(b *testing.B) { 118 | for i := 0; i < b.N; i++ { 119 | _ = VerifyMessage( 120 | "1FiyJnrgwBc3Ff83V1yRWAkmXBdGrDQnXQ", 121 | "IBDscOd/Ov4yrd/YXantqajSAnW4fudpfr2KQy5GNo9pZybF12uNaal4KI822UpQLS/UJD+UK2SnNMn6Z3E4na8=", 122 | "Testing!", 123 | ) 124 | } 125 | } 126 | 127 | // TestVerifyMessageDER will test the method VerifyMessageDER() 128 | func TestVerifyMessageDER(t *testing.T) { 129 | 130 | // Example message (payload from Merchant API) 131 | message := []byte(`{"apiVersion":"0.1.0","timestamp":"2020-10-08T14:25:31.539Z","expiryTime":"2020-10-08T14:35:31.539Z","minerId":"` + testDERPubKey + `","currentHighestBlockHash":"0000000000000000021af4ee1f179a64e530bf818ef67acd09cae24a89124519","currentHighestBlockHeight":656007,"minerReputation":null,"fees":[{"id":1,"feeType":"standard","miningFee":{"satoshis":500,"bytes":1000},"relayFee":{"satoshis":250,"bytes":1000}},{"id":2,"feeType":"data","miningFee":{"satoshis":500,"bytes":1000},"relayFee":{"satoshis":250,"bytes":1000}}]}`) 132 | invalidMessage := []byte("invalid-message") 133 | validHash := sha256.Sum256(message) 134 | 135 | t.Run("valid signature", func(t *testing.T) { 136 | verified, err := VerifyMessageDER(validHash, testDERPubKey, testDERSignature) 137 | assert.NoError(t, err) 138 | assert.Equal(t, true, verified) 139 | }) 140 | 141 | t.Run("invalid pubkey", func(t *testing.T) { 142 | verified, err := VerifyMessageDER(validHash, testDERPubKey+"00", testDERSignature) 143 | assert.Error(t, err) 144 | assert.Equal(t, false, verified) 145 | }) 146 | 147 | t.Run("invalid pubkey 2", func(t *testing.T) { 148 | verified, err := VerifyMessageDER(validHash, "0", testDERSignature) 149 | assert.Error(t, err) 150 | assert.Equal(t, false, verified) 151 | }) 152 | 153 | t.Run("invalid signature (prefix)", func(t *testing.T) { 154 | verified, err := VerifyMessageDER(validHash, testDERPubKey, "0"+testDERSignature) 155 | assert.Error(t, err) 156 | assert.Equal(t, false, verified) 157 | }) 158 | 159 | t.Run("invalid signature (suffix)", func(t *testing.T) { 160 | verified, err := VerifyMessageDER(validHash, testDERPubKey, testDERSignature+"-1") 161 | assert.Error(t, err) 162 | assert.Equal(t, false, verified) 163 | }) 164 | 165 | t.Run("invalid signature (length)", func(t *testing.T) { 166 | verified, err := VerifyMessageDER(validHash, testDERPubKey, "1234567") 167 | assert.Error(t, err) 168 | assert.Equal(t, false, verified) 169 | }) 170 | 171 | t.Run("invalid message", func(t *testing.T) { 172 | verified, err := VerifyMessageDER(sha256.Sum256(invalidMessage), testDERPubKey, testDERSignature) 173 | assert.NoError(t, err) 174 | assert.Equal(t, false, verified) 175 | }) 176 | } 177 | 178 | // ExampleVerifyMessageDER example using VerifyMessageDER() 179 | func ExampleVerifyMessageDER() { 180 | message := []byte(`{"apiVersion":"0.1.0","timestamp":"2020-10-08T14:25:31.539Z","expiryTime":"2020-10-08T14:35:31.539Z","minerId":"` + testDERPubKey + `","currentHighestBlockHash":"0000000000000000021af4ee1f179a64e530bf818ef67acd09cae24a89124519","currentHighestBlockHeight":656007,"minerReputation":null,"fees":[{"id":1,"feeType":"standard","miningFee":{"satoshis":500,"bytes":1000},"relayFee":{"satoshis":250,"bytes":1000}},{"id":2,"feeType":"data","miningFee":{"satoshis":500,"bytes":1000},"relayFee":{"satoshis":250,"bytes":1000}}]}`) 181 | 182 | verified, err := VerifyMessageDER(sha256.Sum256(message), testDERPubKey, testDERSignature) 183 | if err != nil { 184 | fmt.Printf("error occurred: %s", err.Error()) 185 | return 186 | } else if !verified { 187 | fmt.Printf("verification failed") 188 | return 189 | } 190 | fmt.Printf("verification passed") 191 | // Output:verification passed 192 | } 193 | 194 | // BenchmarkVerifyMessageDER benchmarks the method VerifyMessageDER() 195 | func BenchmarkVerifyMessageDER(b *testing.B) { 196 | message := []byte(`{"apiVersion":"0.1.0","timestamp":"2020-10-08T14:25:31.539Z","expiryTime":"2020-10-08T14:35:31.539Z","minerId":"` + testDERPubKey + `","currentHighestBlockHash":"0000000000000000021af4ee1f179a64e530bf818ef67acd09cae24a89124519","currentHighestBlockHeight":656007,"minerReputation":null,"fees":[{"id":1,"feeType":"standard","miningFee":{"satoshis":500,"bytes":1000},"relayFee":{"satoshis":250,"bytes":1000}},{"id":2,"feeType":"data","miningFee":{"satoshis":500,"bytes":1000},"relayFee":{"satoshis":250,"bytes":1000}}]}`) 197 | 198 | for i := 0; i < b.N; i++ { 199 | _, _ = VerifyMessageDER(sha256.Sum256(message), testDERPubKey, testDERSignature) 200 | } 201 | } 202 | --------------------------------------------------------------------------------