├── .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 | [](https://github.com/BitcoinSchema/go-bitcoin/releases)
5 | [](https://github.com/BitcoinSchema/go-bitcoin/actions)
6 | [](https://goreportcard.com/report/github.com/BitcoinSchema/go-bitcoin)
7 | [](https://codecov.io/gh/BitcoinSchema/go-bitcoin)
8 | [](https://golang.org/)
9 |
10 | [](https://gitpod.io/#https://github.com/BitcoinSchema/go-bitcoin)
11 | [](https://mergify.io)
12 | [](https://github.com/sponsors/BitcoinSchema)
13 | [](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 | [](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 | | [
](https://github.com/mrz1836) | [
](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 | [](https://github.com/BitcoinSchema/go-bitcoin/stargazers)
226 |
227 |
228 |
229 | ## License
230 |
231 | [](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 |
--------------------------------------------------------------------------------