sibit 0.28.0 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +2 -0
- data/Gemfile.lock +15 -3
- data/README.md +14 -17
- data/Rakefile +16 -3
- data/bin/sibit +1 -4
- data/features/cli.feature +1 -1
- data/features/dry.feature +25 -0
- data/lib/sibit/bestof.rb +68 -71
- data/lib/sibit/bitcoin/base58.rb +32 -33
- data/lib/sibit/bitcoin/key.rb +64 -65
- data/lib/sibit/bitcoin/script.rb +45 -46
- data/lib/sibit/bitcoin/tx.rb +153 -144
- data/lib/sibit/bitcoin/txbuilder.rb +65 -54
- data/lib/sibit/bitcoinchain.rb +93 -96
- data/lib/sibit/blockchain.rb +115 -118
- data/lib/sibit/blockchair.rb +62 -65
- data/lib/sibit/btc.rb +147 -150
- data/lib/sibit/cex.rb +49 -50
- data/lib/sibit/cryptoapis.rb +113 -116
- data/lib/sibit/fake.rb +50 -47
- data/lib/sibit/firstof.rb +73 -76
- data/lib/sibit/http.rb +10 -6
- data/lib/sibit/json.rb +63 -66
- data/lib/sibit/version.rb +1 -1
- data/lib/sibit.rb +18 -18
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 16d749128c92e0a715c1545d8cb6aa6a41a4afcbd7fc39db0e708fa87ea7383d
|
|
4
|
+
data.tar.gz: c522bc105501aedd9f3ef6140ac99bb1b18cec23fc060ff8dd6ceed4d4e69dff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 854ede7b60a6cd9e1334341b14f1b712e002b23499a5a192d61b53380b68d3203f4d7c3d0aaa87499d59a885190345e776410e1eb133f26f85b8aaac260772ed
|
|
7
|
+
data.tar.gz: f99cee0f4d8615d0a39ee7609c697b4238e0e867193934564d5f2d29cfffe454b630b8da0f4e7f1bda9620d57d395bdf4b365614cf318c6079b808dab330cc2f
|
data/Gemfile
CHANGED
|
@@ -14,6 +14,8 @@ gem 'logger', '~>1.7', require: false
|
|
|
14
14
|
gem 'minitest', '~>6.0', require: false
|
|
15
15
|
gem 'minitest-reporters', '~>1.7', require: false
|
|
16
16
|
gem 'nokogiri', '~>1.18', require: false
|
|
17
|
+
gem 'os', '~>1.1', require: false
|
|
18
|
+
gem 'qbash', '~>0.0', require: false
|
|
17
19
|
gem 'rake', '~>13.2', require: false
|
|
18
20
|
gem 'rdoc', '~>7.0', require: false
|
|
19
21
|
gem 'rubocop', '~>1.62', require: false
|
data/Gemfile.lock
CHANGED
|
@@ -59,6 +59,9 @@ GEM
|
|
|
59
59
|
date (3.5.1)
|
|
60
60
|
diff-lcs (1.6.2)
|
|
61
61
|
docile (1.4.1)
|
|
62
|
+
elapsed (0.2.1)
|
|
63
|
+
loog (~> 0.6)
|
|
64
|
+
tago (~> 0.1)
|
|
62
65
|
erb (6.0.1)
|
|
63
66
|
ffi (1.17.2-arm64-darwin)
|
|
64
67
|
ffi (1.17.2-x86_64-linux-gnu)
|
|
@@ -72,6 +75,7 @@ GEM
|
|
|
72
75
|
logger (~> 1.0)
|
|
73
76
|
memoist3 (1.0.0)
|
|
74
77
|
mini_mime (1.1.5)
|
|
78
|
+
mini_portile2 (2.8.9)
|
|
75
79
|
minitest (6.0.1)
|
|
76
80
|
prism (~> 1.5)
|
|
77
81
|
minitest-reporters (1.7.1)
|
|
@@ -80,11 +84,11 @@ GEM
|
|
|
80
84
|
minitest (>= 5.0)
|
|
81
85
|
ruby-progressbar
|
|
82
86
|
multi_test (1.1.0)
|
|
83
|
-
nokogiri (1.18.10
|
|
84
|
-
|
|
85
|
-
nokogiri (1.18.10-x86_64-linux-gnu)
|
|
87
|
+
nokogiri (1.18.10)
|
|
88
|
+
mini_portile2 (~> 2.8.2)
|
|
86
89
|
racc (~> 1.4)
|
|
87
90
|
openssl (4.0.0)
|
|
91
|
+
os (1.1.4)
|
|
88
92
|
parallel (1.27.0)
|
|
89
93
|
parser (3.3.10.0)
|
|
90
94
|
ast (~> 2.4.1)
|
|
@@ -94,6 +98,11 @@ GEM
|
|
|
94
98
|
date
|
|
95
99
|
stringio
|
|
96
100
|
public_suffix (7.0.0)
|
|
101
|
+
qbash (0.4.8)
|
|
102
|
+
backtrace (> 0)
|
|
103
|
+
elapsed (> 0)
|
|
104
|
+
loog (> 0)
|
|
105
|
+
tago (> 0)
|
|
97
106
|
racc (1.8.1)
|
|
98
107
|
rainbow (3.1.1)
|
|
99
108
|
rake (13.3.1)
|
|
@@ -150,6 +159,7 @@ GEM
|
|
|
150
159
|
sys-uname (1.4.1)
|
|
151
160
|
ffi (~> 1.1)
|
|
152
161
|
memoist3 (~> 1.0.0)
|
|
162
|
+
tago (0.6.0)
|
|
153
163
|
thor (1.4.0)
|
|
154
164
|
tsort (0.2.0)
|
|
155
165
|
unicode-display_width (3.2.0)
|
|
@@ -173,6 +183,8 @@ DEPENDENCIES
|
|
|
173
183
|
minitest (~> 6.0)
|
|
174
184
|
minitest-reporters (~> 1.7)
|
|
175
185
|
nokogiri (~> 1.18)
|
|
186
|
+
os (~> 1.1)
|
|
187
|
+
qbash (~> 0.0)
|
|
176
188
|
rake (~> 13.2)
|
|
177
189
|
rdoc (~> 7.0)
|
|
178
190
|
rubocop (~> 1.62)
|
data/README.md
CHANGED
|
@@ -7,15 +7,13 @@
|
|
|
7
7
|
[](https://github.com/yegor256/sibit/actions/workflows/rake.yml)
|
|
8
8
|
[](https://www.0pdd.com/p?name=yegor256/sibit)
|
|
9
9
|
[](https://badge.fury.io/rb/sibit)
|
|
10
|
-
[](https://codeclimate.com/github/yegor256/sibit/maintainability)
|
|
11
10
|
[](https://github.com/yegor256/takes/sibit/master/LICENSE.txt)
|
|
12
11
|
[](https://codecov.io/github/yegor256/sibit?branch=master)
|
|
13
12
|
[](https://hitsofcode.com/view/github/yegor256/sibit)
|
|
14
13
|
|
|
15
|
-
To understand how the Bitcoin protocol works,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
[_Sibit Demonstrates How Bitcoin Works_][blog].
|
|
14
|
+
To understand how the Bitcoin protocol works, I recommend you watching
|
|
15
|
+
this [short video] and then reading this blog post of mine:
|
|
16
|
+
[_Sibit Demonstrates How Bitcoin Works_][blog].
|
|
19
17
|
|
|
20
18
|
This is a simple Bitcoin client for use from the command line
|
|
21
19
|
or from your Ruby app.
|
|
@@ -26,10 +24,10 @@ If you need something more complex, I would recommend using
|
|
|
26
24
|
[bitcoin-ruby] for Ruby and [Electrum] as a GUI client.
|
|
27
25
|
|
|
28
26
|
You may want to discuss this tool at [Bitcointalk]
|
|
29
|
-
and give the thread a few merits.
|
|
27
|
+
and give the thread a few merits.
|
|
30
28
|
|
|
31
29
|
This is a Ruby gem, install it first (if it doesn't work, there are
|
|
32
|
-
some hints at the bottom of this page):
|
|
30
|
+
some hints at the bottom of this page):
|
|
33
31
|
|
|
34
32
|
```bash
|
|
35
33
|
gem install sibit
|
|
@@ -82,18 +80,16 @@ Say, you are sending 0.5 BTC and the fee is 0.0001 BTC.
|
|
|
82
80
|
Totally, you spend 0.5001.
|
|
83
81
|
However, you can make Sibit deduct the fee from the payment amount.
|
|
84
82
|
In this case you should provide a negative amount
|
|
85
|
-
of the fee or one of
|
|
86
|
-
You can also say
|
|
83
|
+
of the fee or one of `S-`, `M-`, `L-`, `XL-`.
|
|
84
|
+
You can also say `S+`, if you want the opposite, which is the default.
|
|
87
85
|
|
|
88
86
|
It is recommended to run it with `--dry --verbose` options first,
|
|
89
87
|
to see what's going to be sent to the network.
|
|
90
88
|
If everything looks correct, remove the `--dry` and run again,
|
|
91
89
|
the transaction is pushed to the network.
|
|
92
90
|
|
|
93
|
-
All operations are performed through the
|
|
94
|
-
[
|
|
95
|
-
Transactions are pushed to the Bitcoin network via
|
|
96
|
-
[this relay].
|
|
91
|
+
All operations are performed through the [Blockchain API].
|
|
92
|
+
Transactions are pushed to the Bitcoin network via [this relay].
|
|
97
93
|
|
|
98
94
|
## Ruby SDK
|
|
99
95
|
|
|
@@ -115,8 +111,8 @@ It should work.
|
|
|
115
111
|
## APIs
|
|
116
112
|
|
|
117
113
|
The library works through one (or a few) public APIs for fetching
|
|
118
|
-
Bitcoin data and pushing transactions to the network.
|
|
119
|
-
work with the following APIs:
|
|
114
|
+
Bitcoin data and pushing transactions to the network.
|
|
115
|
+
At the moment we work with the following APIs:
|
|
120
116
|
|
|
121
117
|
* [Blockchain.com] - `Sibit::Blockchain`
|
|
122
118
|
* [BTC.com] - `Sibit::Btc`
|
|
@@ -125,8 +121,9 @@ work with the following APIs:
|
|
|
125
121
|
* [Blockchair.com] - `Sibit::Blockchair`
|
|
126
122
|
* [Cex.io] - `Sibit::Cex`
|
|
127
123
|
|
|
128
|
-
The first one in this list is used by default.
|
|
129
|
-
|
|
124
|
+
The first one in this list is used by default.
|
|
125
|
+
If you want to use a different one,
|
|
126
|
+
you just specify it in the constructor of `Sibit` object:
|
|
130
127
|
|
|
131
128
|
```ruby
|
|
132
129
|
require 'sibit'
|
data/Rakefile
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
# SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
|
|
4
4
|
# SPDX-License-Identifier: MIT
|
|
5
5
|
|
|
6
|
-
require '
|
|
6
|
+
require 'os'
|
|
7
|
+
require 'qbash'
|
|
7
8
|
require 'rake'
|
|
8
|
-
require 'rdoc'
|
|
9
9
|
require 'rake/clean'
|
|
10
|
+
require 'rdoc'
|
|
11
|
+
require 'rubygems'
|
|
12
|
+
require 'shellwords'
|
|
10
13
|
|
|
11
14
|
def name
|
|
12
15
|
@name ||= File.basename(Dir['*.gemspec'].first, '.*')
|
|
@@ -16,7 +19,7 @@ def version
|
|
|
16
19
|
Gem::Specification.load(Dir['*.gemspec'].first).version
|
|
17
20
|
end
|
|
18
21
|
|
|
19
|
-
task default: %i[clean test features rubocop]
|
|
22
|
+
task default: %i[clean test picks features rubocop]
|
|
20
23
|
|
|
21
24
|
require 'rake/testtask'
|
|
22
25
|
Rake::TestTask.new(:test) do |test|
|
|
@@ -27,6 +30,16 @@ Rake::TestTask.new(:test) do |test|
|
|
|
27
30
|
test.verbose = false
|
|
28
31
|
end
|
|
29
32
|
|
|
33
|
+
desc 'Run them via Ruby, one by one'
|
|
34
|
+
task :picks do
|
|
35
|
+
next if OS.windows?
|
|
36
|
+
%w[test lib].each do |d|
|
|
37
|
+
Dir["#{d}/**/*.rb"].each do |f|
|
|
38
|
+
qbash("bundle exec ruby #{Shellwords.escape(f)}", log: $stdout, env: { 'PICKS' => 'yes' })
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
30
43
|
require 'rdoc/task'
|
|
31
44
|
Rake::RDocTask.new do |rdoc|
|
|
32
45
|
rdoc.rdoc_dir = 'rdoc'
|
data/bin/sibit
CHANGED
|
@@ -6,15 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
$stdout.sync = true
|
|
8
8
|
|
|
9
|
-
# see https://stackoverflow.com/a/6048451/187141
|
|
10
|
-
require 'openssl'
|
|
11
|
-
OpenSSL::SSL::VERIFY_PEER ||= OpenSSL::SSL::VERIFY_NONE
|
|
12
|
-
|
|
13
9
|
require 'backtrace'
|
|
14
10
|
require 'loog'
|
|
15
11
|
require 'retriable_proxy'
|
|
16
12
|
require 'slop'
|
|
17
13
|
require_relative '../lib/sibit'
|
|
14
|
+
require_relative '../lib/sibit/http'
|
|
18
15
|
require_relative '../lib/sibit/bitcoinchain'
|
|
19
16
|
require_relative '../lib/sibit/blockchain'
|
|
20
17
|
require_relative '../lib/sibit/blockchair'
|
data/features/cli.feature
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
|
|
2
2
|
# SPDX-License-Identifier: MIT
|
|
3
3
|
Feature: Command Line Processing
|
|
4
|
-
As a
|
|
4
|
+
As a holder of BTC I want to be able to use sibit
|
|
5
5
|
|
|
6
6
|
Scenario: Help can be printed
|
|
7
7
|
When I run bin/sibit with "--help"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
Feature: Command Line Processing
|
|
4
|
+
As a holder of BTC I want to use sibit in dry mode
|
|
5
|
+
|
|
6
|
+
Scenario: Bitcoin price can be retrieved
|
|
7
|
+
When I run bin/sibit with "price --dry --attempts=4"
|
|
8
|
+
Then Exit code is zero
|
|
9
|
+
|
|
10
|
+
Scenario: Bitcoin latest block hash can be retrieved
|
|
11
|
+
When I run bin/sibit with "latest --dry --api=blockchain"
|
|
12
|
+
Then Exit code is zero
|
|
13
|
+
|
|
14
|
+
Scenario: Bitcoin balance can be checked
|
|
15
|
+
When I run bin/sibit with "balance --dry 1MZT1fa6y8H9UmbZV6HqKF4UY41o9MGT5f --verbose --api=blockchain,btc"
|
|
16
|
+
Then Exit code is zero
|
|
17
|
+
|
|
18
|
+
Scenario: Bitcoin fees can be printed
|
|
19
|
+
When I run bin/sibit with "fees --dry --verbose --api=fake"
|
|
20
|
+
Then Exit code is zero
|
|
21
|
+
|
|
22
|
+
Scenario: Bitcoin payment can be sent
|
|
23
|
+
When I run bin/sibit with "pay --dry --verbose --api=fake --proxy=localhost:3128 999999 XL- 46feba063e9b59a8ae0dba68abd39a3cb8f52089e776576d6eb1bb5bfec123d1 1MZT1fa6y8H9UmbZV6HqKF4UY41o9MGT5f 1Fsyq5YGe8zbSjLS8YsDnZWM8U6AYMR6ZD"
|
|
24
|
+
Then Exit code is not zero
|
|
25
|
+
Then Stdout contains "UTXO arrived to 1JvCsJtLmCxEk7ddZFnVkGXpr9uhxZPmJi is incorrect"
|
data/lib/sibit/bestof.rb
CHANGED
|
@@ -12,96 +12,93 @@ require_relative 'error'
|
|
|
12
12
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
|
13
13
|
# Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
|
|
14
14
|
# License:: MIT
|
|
15
|
-
class Sibit
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@verbose = verbose
|
|
23
|
-
end
|
|
15
|
+
class Sibit::BestOf
|
|
16
|
+
# Constructor.
|
|
17
|
+
def initialize(list, log: Loog::NULL, verbose: false)
|
|
18
|
+
@list = list
|
|
19
|
+
@log = log
|
|
20
|
+
@verbose = verbose
|
|
21
|
+
end
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
end
|
|
23
|
+
# Current price of BTC in USD (float returned).
|
|
24
|
+
def price(currency = 'USD')
|
|
25
|
+
best_of('price') do |api|
|
|
26
|
+
api.price(currency)
|
|
30
27
|
end
|
|
28
|
+
end
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
end
|
|
30
|
+
# Gets the balance of the address, in satoshi.
|
|
31
|
+
def balance(address)
|
|
32
|
+
best_of('balance') do |api|
|
|
33
|
+
api.balance(address)
|
|
37
34
|
end
|
|
35
|
+
end
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
end
|
|
37
|
+
# Get the height of the block.
|
|
38
|
+
def height(hash)
|
|
39
|
+
best_of('height') do |api|
|
|
40
|
+
api.height(hash)
|
|
44
41
|
end
|
|
42
|
+
end
|
|
45
43
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
end
|
|
44
|
+
# Get the hash of the next block.
|
|
45
|
+
def next_of(hash)
|
|
46
|
+
best_of('next_of') do |api|
|
|
47
|
+
api.next_of(hash)
|
|
51
48
|
end
|
|
49
|
+
end
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
# Get recommended fees, in satoshi per byte. The method returns
|
|
52
|
+
# a hash: { S: 12, M: 45, L: 100, XL: 200 }
|
|
53
|
+
def fees
|
|
54
|
+
best_of('fees', &:fees)
|
|
55
|
+
end
|
|
58
56
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
end
|
|
57
|
+
# Fetch all unspent outputs per address.
|
|
58
|
+
def utxos(keys)
|
|
59
|
+
best_of('utxos') do |api|
|
|
60
|
+
api.utxos(keys)
|
|
64
61
|
end
|
|
62
|
+
end
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
# Latest block hash.
|
|
65
|
+
def latest
|
|
66
|
+
best_of('latest', &:latest)
|
|
67
|
+
end
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
end
|
|
69
|
+
# Push this transaction (in hex format) to the network.
|
|
70
|
+
def push(hex)
|
|
71
|
+
best_of('push') do |api|
|
|
72
|
+
api.push(hex)
|
|
76
73
|
end
|
|
74
|
+
end
|
|
77
75
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
end
|
|
76
|
+
# This method should fetch a block and return as a hash.
|
|
77
|
+
def block(hash)
|
|
78
|
+
best_of('block') do |api|
|
|
79
|
+
api.block(hash)
|
|
83
80
|
end
|
|
81
|
+
end
|
|
84
82
|
|
|
85
|
-
|
|
83
|
+
private
|
|
86
84
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
85
|
+
def best_of(method)
|
|
86
|
+
return yield @list unless @list.is_a?(Array)
|
|
87
|
+
results = []
|
|
88
|
+
errors = []
|
|
89
|
+
@list.each do |api|
|
|
90
|
+
results << yield(api)
|
|
91
|
+
rescue Sibit::NotSupportedError
|
|
92
|
+
# Just ignore it
|
|
93
|
+
rescue Sibit::Error => e
|
|
94
|
+
errors << e
|
|
95
|
+
@log.info("The API #{api.class.name} failed at #{method}(): #{e.message}") if @verbose
|
|
96
|
+
end
|
|
97
|
+
if results.empty?
|
|
98
|
+
errors.each { |e| @log.info(Backtrace.new(e).to_s) }
|
|
99
|
+
raise Sibit::Error, "No APIs out of #{@list.length} managed to succeed at #{method}(): \
|
|
102
100
|
#{@list.map { |a| a.class.name }.join(', ')}"
|
|
103
|
-
end
|
|
104
|
-
results.group_by(&:to_s).values.max_by(&:size)[0]
|
|
105
101
|
end
|
|
102
|
+
results.group_by(&:to_s).values.max_by(&:size)[0]
|
|
106
103
|
end
|
|
107
104
|
end
|
data/lib/sibit/bitcoin/base58.rb
CHANGED
|
@@ -5,46 +5,45 @@
|
|
|
5
5
|
|
|
6
6
|
require 'digest'
|
|
7
7
|
|
|
8
|
+
# Sibit main class.
|
|
8
9
|
class Sibit
|
|
9
|
-
# Bitcoin
|
|
10
|
+
# Base58 encoding for Bitcoin addresses.
|
|
10
11
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
# Encapsulates hex data and provides encoding/decoding functionality.
|
|
13
|
+
#
|
|
14
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
|
15
|
+
# Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
|
|
16
|
+
# License:: MIT
|
|
17
|
+
class Base58
|
|
18
|
+
ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
|
|
20
|
-
# License:: MIT
|
|
21
|
-
module Base58
|
|
22
|
-
ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
|
20
|
+
def initialize(data)
|
|
21
|
+
@data = data
|
|
22
|
+
end
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
end
|
|
33
|
-
('1' * leading) + result
|
|
24
|
+
def encode
|
|
25
|
+
bytes = [@data].pack('H*')
|
|
26
|
+
leading = bytes.match(/^\x00*/)[0].length
|
|
27
|
+
num = @data.to_i(16)
|
|
28
|
+
result = ''
|
|
29
|
+
while num.positive?
|
|
30
|
+
num, remainder = num.divmod(58)
|
|
31
|
+
result = ALPHABET[remainder] + result
|
|
34
32
|
end
|
|
33
|
+
('1' * leading) + result
|
|
34
|
+
end
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
def decode
|
|
37
|
+
leading = @data.match(/^1*/)[0].length
|
|
38
|
+
num = 0
|
|
39
|
+
@data.each_char { |c| num = (num * 58) + ALPHABET.index(c) }
|
|
40
|
+
hex = num.zero? ? '' : num.to_s(16)
|
|
41
|
+
hex = "0#{hex}" if hex.length.odd?
|
|
42
|
+
('00' * leading) + hex
|
|
43
|
+
end
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
end
|
|
45
|
+
def check
|
|
46
|
+
Digest::SHA256.hexdigest(Digest::SHA256.digest([@data].pack('H*')))[0...8]
|
|
48
47
|
end
|
|
49
48
|
end
|
|
50
49
|
end
|
data/lib/sibit/bitcoin/key.rb
CHANGED
|
@@ -3,85 +3,84 @@
|
|
|
3
3
|
# SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
|
|
4
4
|
# SPDX-License-Identifier: MIT
|
|
5
5
|
|
|
6
|
-
require 'openssl'
|
|
7
6
|
require 'digest'
|
|
7
|
+
require 'openssl'
|
|
8
8
|
require_relative 'base58'
|
|
9
9
|
|
|
10
|
+
# Sibit main class.
|
|
10
11
|
class Sibit
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
MAX_PRIV = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140
|
|
12
|
+
# Bitcoin ECDSA key using secp256k1 curve.
|
|
13
|
+
#
|
|
14
|
+
# Supports OpenSSL 3.0+ by constructing keys via DER encoding instead
|
|
15
|
+
# of using deprecated mutable key APIs.
|
|
16
|
+
#
|
|
17
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
|
18
|
+
# Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
|
|
19
|
+
# License:: MIT
|
|
20
|
+
class Key
|
|
21
|
+
MIN_PRIV = 0x01
|
|
22
|
+
MAX_PRIV = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
def self.generate
|
|
25
|
+
key = OpenSSL::PKey::EC.generate('secp256k1')
|
|
26
|
+
pvt = key.private_key.to_s(16).rjust(64, '0').downcase
|
|
27
|
+
new(pvt)
|
|
28
|
+
end
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
def initialize(privkey)
|
|
31
|
+
@privkey = privkey
|
|
32
|
+
@compressed = true
|
|
33
|
+
@key = build(privkey)
|
|
34
|
+
end
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
def priv
|
|
37
|
+
@privkey
|
|
38
|
+
end
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
def pub
|
|
41
|
+
point = @key.public_key
|
|
42
|
+
point.to_octet_string(@compressed ? :compressed : :uncompressed).unpack1('H*')
|
|
43
|
+
end
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
def addr
|
|
46
|
+
hash = hash160(pub)
|
|
47
|
+
versioned = "00#{hash}"
|
|
48
|
+
checksum = Base58.new(versioned).check
|
|
49
|
+
Base58.new(versioned + checksum).encode
|
|
50
|
+
end
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
def sign(data)
|
|
53
|
+
@key.sign('SHA256', data)
|
|
54
|
+
end
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
def verify(data, sig)
|
|
57
|
+
@key.verify('SHA256', sig, data)
|
|
58
|
+
rescue OpenSSL::PKey::PKeyError
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
private
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
64
|
+
def build(privkey)
|
|
65
|
+
value = privkey.to_i(16)
|
|
66
|
+
raise 'private key is not on curve' unless value.between?(MIN_PRIV, MAX_PRIV)
|
|
67
|
+
group = OpenSSL::PKey::EC::Group.new('secp256k1')
|
|
68
|
+
bn = OpenSSL::BN.new(privkey, 16)
|
|
69
|
+
pubkey = group.generator.mul(bn)
|
|
70
|
+
asn1 = OpenSSL::ASN1::Sequence(
|
|
71
|
+
[
|
|
72
|
+
OpenSSL::ASN1::Integer.new(1),
|
|
73
|
+
OpenSSL::ASN1::OctetString(bn.to_s(2)),
|
|
74
|
+
OpenSSL::ASN1::ObjectId('secp256k1', 0, :EXPLICIT),
|
|
75
|
+
OpenSSL::ASN1::BitString(pubkey.to_octet_string(:uncompressed), 1, :EXPLICIT)
|
|
76
|
+
]
|
|
77
|
+
)
|
|
78
|
+
OpenSSL::PKey::EC.new(asn1.to_der)
|
|
79
|
+
end
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
end
|
|
81
|
+
def hash160(hex)
|
|
82
|
+
bytes = [hex].pack('H*')
|
|
83
|
+
Digest::RMD160.hexdigest(Digest::SHA256.digest(bytes))
|
|
85
84
|
end
|
|
86
85
|
end
|
|
87
86
|
end
|