sibit 0.27.1 → 0.29.0
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.lock +3 -3
- data/README.md +46 -47
- data/bin/sibit +12 -14
- data/lib/sibit/bestof.rb +68 -71
- data/lib/sibit/bitcoin/base58.rb +33 -35
- data/lib/sibit/bitcoin/key.rb +64 -66
- data/lib/sibit/bitcoin/script.rb +45 -47
- data/lib/sibit/bitcoin/tx.rb +162 -164
- data/lib/sibit/bitcoin/txbuilder.rb +1 -1
- 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 +42 -47
- data/lib/sibit/firstof.rb +73 -76
- data/lib/sibit/http.rb +17 -20
- data/lib/sibit/json.rb +63 -66
- data/lib/sibit/version.rb +1 -1
- data/lib/sibit.rb +9 -11
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ddde20b613a744a0618b9b91d4adfea42d945a13d778ecad2492b1a15afe3c0e
|
|
4
|
+
data.tar.gz: 7d83be599123ff738035fbf610d1ae6f763c5c36cec2a83a626658707cf37cda
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9811599df9ebb242927bcf50a8ed2b1e24adad74832b72748b1f1c113c9f30e89c0b9079f3a0898308963c55c59d0f899534d529d673f0120a718176402a4e10
|
|
7
|
+
data.tar.gz: 2f3821bb817a4fadbe8137ccce649d66ba388ae17f0a3b99f6293947e0d3ef084d2d0f1501fabcbec8d933b7127d655a1fae74bdb907019ae39e088d4bd6bf52
|
data/Gemfile.lock
CHANGED
|
@@ -72,6 +72,7 @@ GEM
|
|
|
72
72
|
logger (~> 1.0)
|
|
73
73
|
memoist3 (1.0.0)
|
|
74
74
|
mini_mime (1.1.5)
|
|
75
|
+
mini_portile2 (2.8.9)
|
|
75
76
|
minitest (6.0.1)
|
|
76
77
|
prism (~> 1.5)
|
|
77
78
|
minitest-reporters (1.7.1)
|
|
@@ -80,9 +81,8 @@ GEM
|
|
|
80
81
|
minitest (>= 5.0)
|
|
81
82
|
ruby-progressbar
|
|
82
83
|
multi_test (1.1.0)
|
|
83
|
-
nokogiri (1.18.10
|
|
84
|
-
|
|
85
|
-
nokogiri (1.18.10-x86_64-linux-gnu)
|
|
84
|
+
nokogiri (1.18.10)
|
|
85
|
+
mini_portile2 (~> 2.8.2)
|
|
86
86
|
racc (~> 1.4)
|
|
87
87
|
openssl (4.0.0)
|
|
88
88
|
parallel (1.27.0)
|
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
|
|
@@ -61,38 +59,37 @@ $ sibit balance 1PfsYNygsuVL8fvBarJNQnHytkg4rGih1U
|
|
|
61
59
|
To send a payment from a few addresses to a new address:
|
|
62
60
|
|
|
63
61
|
```bash
|
|
64
|
-
$ sibit pay AMOUNT FEE
|
|
62
|
+
$ sibit pay AMOUNT FEE P1,P2,... TARGET CHANGE
|
|
65
63
|
e87f138c9ebf5986151667719825c28458a28cc66f69fed4f1032a93b399fdf8
|
|
66
64
|
```
|
|
67
65
|
|
|
68
66
|
Here,
|
|
69
|
-
`AMOUNT` is the amount of [satoshi] you are sending,
|
|
70
|
-
`FEE` is the [miner fee] you are ready to spend to get
|
|
71
|
-
this transaction delivered
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
`
|
|
75
|
-
|
|
76
|
-
`
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
or one of
|
|
86
|
-
opposite, which is the default.
|
|
87
|
-
|
|
88
|
-
It is recommended to run it with `--dry --verbose` options first,
|
|
89
|
-
what's going to be sent to the network.
|
|
90
|
-
the `--dry` and run again,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
[Blockchain API].
|
|
94
|
-
Transactions are pushed to the Bitcoin network via
|
|
95
|
-
[this relay].
|
|
67
|
+
`AMOUNT` is the amount of [satoshi] you are sending,
|
|
68
|
+
`FEE` is the [miner fee] you are ready to spend to get
|
|
69
|
+
this transaction delivered (you can say `S`, `M`, `L`, or `XL` if you want it
|
|
70
|
+
to be calculated automatically),
|
|
71
|
+
`P1,P2,...` is a comma-separated list
|
|
72
|
+
of private keys `P` you are sending your coins from,
|
|
73
|
+
`TARGET` is the address you are sending to,
|
|
74
|
+
`CHANGE` is the address where the change goes.
|
|
75
|
+
The transaction hash is returned.
|
|
76
|
+
Not all [UTXOs] may be used, but only the necessary amount of them.
|
|
77
|
+
|
|
78
|
+
By default, the fee is paid on top of the payment amount you are sending.
|
|
79
|
+
Say, you are sending 0.5 BTC and the fee is 0.0001 BTC.
|
|
80
|
+
Totally, you spend 0.5001.
|
|
81
|
+
However, you can make Sibit deduct the fee from the payment amount.
|
|
82
|
+
In this case you should provide a negative amount
|
|
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.
|
|
85
|
+
|
|
86
|
+
It is recommended to run it with `--dry --verbose` options first,
|
|
87
|
+
to see what's going to be sent to the network.
|
|
88
|
+
If everything looks correct, remove the `--dry` and run again,
|
|
89
|
+
the transaction is pushed to the network.
|
|
90
|
+
|
|
91
|
+
All operations are performed through the [Blockchain API].
|
|
92
|
+
Transactions are pushed to the Bitcoin network via [this relay].
|
|
96
93
|
|
|
97
94
|
## Ruby SDK
|
|
98
95
|
|
|
@@ -105,7 +102,7 @@ pkey = sibit.generate
|
|
|
105
102
|
address = sibit.create(pkey)
|
|
106
103
|
balance = sibit.balance(address)
|
|
107
104
|
target = sibit.create(pkey) # where to send coins to
|
|
108
|
-
change = sibit.create(pkey) # where the change
|
|
105
|
+
change = sibit.create(pkey) # where the change goes
|
|
109
106
|
tx = sibit.pay(10_000_000, 'XL', { address => pkey }, target, change)
|
|
110
107
|
```
|
|
111
108
|
|
|
@@ -114,8 +111,8 @@ It should work.
|
|
|
114
111
|
## APIs
|
|
115
112
|
|
|
116
113
|
The library works through one (or a few) public APIs for fetching
|
|
117
|
-
Bitcoin data and pushing transactions to the network.
|
|
118
|
-
work with the following APIs:
|
|
114
|
+
Bitcoin data and pushing transactions to the network.
|
|
115
|
+
At the moment we work with the following APIs:
|
|
119
116
|
|
|
120
117
|
* [Blockchain.com] - `Sibit::Blockchain`
|
|
121
118
|
* [BTC.com] - `Sibit::Btc`
|
|
@@ -124,8 +121,9 @@ work with the following APIs:
|
|
|
124
121
|
* [Blockchair.com] - `Sibit::Blockchair`
|
|
125
122
|
* [Cex.io] - `Sibit::Cex`
|
|
126
123
|
|
|
127
|
-
The first one in this list is used by default.
|
|
128
|
-
|
|
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:
|
|
129
127
|
|
|
130
128
|
```ruby
|
|
131
129
|
require 'sibit'
|
|
@@ -133,10 +131,11 @@ require 'sibit/btc'
|
|
|
133
131
|
sibit = Sibit.new(api: Sibit::Btc.new)
|
|
134
132
|
```
|
|
135
133
|
|
|
136
|
-
You may also use a combination of APIs.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
134
|
+
You may also use a combination of APIs.
|
|
135
|
+
This may be very useful since some APIs are not reliable
|
|
136
|
+
and others don't have all the features required.
|
|
137
|
+
You can provide an array of objects and they are used one by one,
|
|
138
|
+
until a successful response is obtained:
|
|
140
139
|
|
|
141
140
|
```ruby
|
|
142
141
|
require 'sibit'
|
|
@@ -168,13 +167,13 @@ gem install sibit
|
|
|
168
167
|
```
|
|
169
168
|
|
|
170
169
|
It should work.
|
|
171
|
-
If it doesn't, submit an issue and I
|
|
170
|
+
If it doesn't, submit an issue and I can try to help.
|
|
172
171
|
|
|
173
172
|
## How to contribute
|
|
174
173
|
|
|
175
174
|
Read [these guidelines].
|
|
176
|
-
Make sure your build is green before you contribute
|
|
177
|
-
|
|
175
|
+
Make sure your build is green before you contribute your pull request.
|
|
176
|
+
You need to have [Ruby] 2.3+ and [Bundler] installed.
|
|
178
177
|
Then:
|
|
179
178
|
|
|
180
179
|
```bash
|
data/bin/sibit
CHANGED
|
@@ -6,10 +6,6 @@
|
|
|
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'
|
|
@@ -24,18 +20,18 @@ require_relative '../lib/sibit/fake'
|
|
|
24
20
|
require_relative '../lib/sibit/firstof'
|
|
25
21
|
require_relative '../lib/sibit/version'
|
|
26
22
|
|
|
27
|
-
|
|
23
|
+
opts =
|
|
28
24
|
begin
|
|
29
|
-
|
|
25
|
+
Slop.parse(ARGV, strict: true, help: true) do |o|
|
|
30
26
|
o.banner = "Usage (#{Sibit::VERSION}): sibit [options] command [args]
|
|
31
27
|
Commands are:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
price: Get current price of BTC in USD
|
|
29
|
+
fees: Get currently recommended transaction fees
|
|
30
|
+
latest: Get hash of the latest block
|
|
31
|
+
generate: Generate a new private key
|
|
32
|
+
create: Create a public Bitcoin address from the key
|
|
33
|
+
balance: Check the balance of the Bitcoin address
|
|
34
|
+
pay: Send a new Bitcoin transaction
|
|
39
35
|
Options are:"
|
|
40
36
|
o.string '--proxy', 'HTTPS proxy for all requests, e.g. "localhost:3128"'
|
|
41
37
|
o.integer(
|
|
@@ -63,6 +59,8 @@ Options are:"
|
|
|
63
59
|
rescue Slop::Error => e
|
|
64
60
|
raise e.message
|
|
65
61
|
end
|
|
62
|
+
|
|
63
|
+
begin
|
|
66
64
|
raise 'Try --help' if opts.arguments.empty?
|
|
67
65
|
log = opts[:verbose] ? Loog::VERBOSE : Loog::NULL
|
|
68
66
|
http = opts[:proxy] ? Sibit::HttpProxy.new(opts[:proxy]) : Sibit::Http.new
|
|
@@ -126,7 +124,7 @@ Options are:"
|
|
|
126
124
|
raise 'Change argument is required' if change.nil?
|
|
127
125
|
puts sibit.pay(
|
|
128
126
|
amount, fee,
|
|
129
|
-
sources.split(',')
|
|
127
|
+
sources.split(','),
|
|
130
128
|
target, change,
|
|
131
129
|
skip_utxo: opts['skip-utxo']
|
|
132
130
|
)
|
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,44 @@
|
|
|
5
5
|
|
|
6
6
|
require 'digest'
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
MIN_TX_FEE = 10_000
|
|
8
|
+
# Bitcoin primitives module.
|
|
9
|
+
#
|
|
10
|
+
# Pure Ruby implementation of Bitcoin functionality using OpenSSL 3.0+.
|
|
11
|
+
# Replaces the bitcoin-ruby dependency which is incompatible with OpenSSL 3.0.
|
|
12
|
+
module Sibit::Bitcoin
|
|
13
|
+
MIN_TX_FEE = 10_000
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
# Base58 encoding for Bitcoin addresses.
|
|
16
|
+
#
|
|
17
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
|
18
|
+
# Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
|
|
19
|
+
# License:: MIT
|
|
20
|
+
module Base58
|
|
21
|
+
ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
end
|
|
33
|
-
('1' * leading) + result
|
|
23
|
+
def self.encode(hex)
|
|
24
|
+
bytes = [hex].pack('H*')
|
|
25
|
+
leading = bytes.match(/^\x00*/)[0].length
|
|
26
|
+
num = hex.to_i(16)
|
|
27
|
+
result = ''
|
|
28
|
+
while num.positive?
|
|
29
|
+
num, remainder = num.divmod(58)
|
|
30
|
+
result = ALPHABET[remainder] + result
|
|
34
31
|
end
|
|
32
|
+
('1' * leading) + result
|
|
33
|
+
end
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
def self.decode(str)
|
|
36
|
+
leading = str.match(/^1*/)[0].length
|
|
37
|
+
num = 0
|
|
38
|
+
str.each_char { |c| num = (num * 58) + ALPHABET.index(c) }
|
|
39
|
+
hex = num.zero? ? '' : num.to_s(16)
|
|
40
|
+
hex = "0#{hex}" if hex.length.odd?
|
|
41
|
+
('00' * leading) + hex
|
|
42
|
+
end
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
end
|
|
44
|
+
def self.check(hex)
|
|
45
|
+
Digest::SHA256.hexdigest(Digest::SHA256.digest([hex].pack('H*')))[0...8]
|
|
48
46
|
end
|
|
49
47
|
end
|
|
50
48
|
end
|
data/lib/sibit/bitcoin/key.rb
CHANGED
|
@@ -3,85 +3,83 @@
|
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
MAX_PRIV = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140
|
|
10
|
+
module Sibit::Bitcoin
|
|
11
|
+
# Bitcoin ECDSA key using secp256k1 curve.
|
|
12
|
+
#
|
|
13
|
+
# Supports OpenSSL 3.0+ by constructing keys via DER encoding instead
|
|
14
|
+
# of using deprecated mutable key APIs.
|
|
15
|
+
#
|
|
16
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
|
17
|
+
# Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
|
|
18
|
+
# License:: MIT
|
|
19
|
+
class Key
|
|
20
|
+
MIN_PRIV = 0x01
|
|
21
|
+
MAX_PRIV = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
def self.generate
|
|
24
|
+
key = OpenSSL::PKey::EC.generate('secp256k1')
|
|
25
|
+
pvt = key.private_key.to_s(16).rjust(64, '0').downcase
|
|
26
|
+
new(pvt)
|
|
27
|
+
end
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
def initialize(privkey)
|
|
30
|
+
@privkey = privkey
|
|
31
|
+
@compressed = true
|
|
32
|
+
@key = build(privkey)
|
|
33
|
+
end
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
def priv
|
|
36
|
+
@privkey
|
|
37
|
+
end
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
def pub
|
|
40
|
+
point = @key.public_key
|
|
41
|
+
point.to_octet_string(@compressed ? :compressed : :uncompressed).unpack1('H*')
|
|
42
|
+
end
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
def addr
|
|
45
|
+
hash = hash160(pub)
|
|
46
|
+
versioned = "00#{hash}"
|
|
47
|
+
checksum = Base58.check(versioned)
|
|
48
|
+
Base58.encode(versioned + checksum)
|
|
49
|
+
end
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
def sign(data)
|
|
52
|
+
@key.sign('SHA256', data)
|
|
53
|
+
end
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
def verify(data, sig)
|
|
56
|
+
@key.verify('SHA256', sig, data)
|
|
57
|
+
rescue OpenSSL::PKey::PKeyError
|
|
58
|
+
false
|
|
59
|
+
end
|
|
61
60
|
|
|
62
|
-
|
|
61
|
+
private
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
63
|
+
def build(privkey)
|
|
64
|
+
value = privkey.to_i(16)
|
|
65
|
+
raise 'private key is not on curve' unless value.between?(MIN_PRIV, MAX_PRIV)
|
|
66
|
+
group = OpenSSL::PKey::EC::Group.new('secp256k1')
|
|
67
|
+
bn = OpenSSL::BN.new(privkey, 16)
|
|
68
|
+
pubkey = group.generator.mul(bn)
|
|
69
|
+
asn1 = OpenSSL::ASN1::Sequence(
|
|
70
|
+
[
|
|
71
|
+
OpenSSL::ASN1::Integer.new(1),
|
|
72
|
+
OpenSSL::ASN1::OctetString(bn.to_s(2)),
|
|
73
|
+
OpenSSL::ASN1::ObjectId('secp256k1', 0, :EXPLICIT),
|
|
74
|
+
OpenSSL::ASN1::BitString(pubkey.to_octet_string(:uncompressed), 1, :EXPLICIT)
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
OpenSSL::PKey::EC.new(asn1.to_der)
|
|
78
|
+
end
|
|
80
79
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
end
|
|
80
|
+
def hash160(hex)
|
|
81
|
+
bytes = [hex].pack('H*')
|
|
82
|
+
Digest::RMD160.hexdigest(Digest::SHA256.digest(bytes))
|
|
85
83
|
end
|
|
86
84
|
end
|
|
87
85
|
end
|