sibit 0.28.0 → 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 +14 -17
- data/bin/sibit +0 -4
- 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 +7 -9
- 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
|
|
@@ -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/bin/sibit
CHANGED
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
|
data/lib/sibit/bitcoin/script.rb
CHANGED
|
@@ -6,53 +6,51 @@
|
|
|
6
6
|
require 'digest'
|
|
7
7
|
require_relative 'base58'
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@bytes
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Base58.encode(versioned + checksum)
|
|
55
|
-
end
|
|
9
|
+
module Sibit::Bitcoin
|
|
10
|
+
# Bitcoin Script parser.
|
|
11
|
+
#
|
|
12
|
+
# Parses standard P2PKH scripts to extract addresses.
|
|
13
|
+
#
|
|
14
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
|
15
|
+
# Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
|
|
16
|
+
# License:: MIT
|
|
17
|
+
class Script
|
|
18
|
+
OP_DUP = 0x76
|
|
19
|
+
OP_HASH160 = 0xa9
|
|
20
|
+
OP_EQUALVERIFY = 0x88
|
|
21
|
+
OP_CHECKSIG = 0xac
|
|
22
|
+
|
|
23
|
+
def initialize(hex)
|
|
24
|
+
@bytes = [hex].pack('H*').bytes
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def address
|
|
28
|
+
return p2pkh_address if p2pkh?
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def p2pkh?
|
|
33
|
+
@bytes.length == 25 &&
|
|
34
|
+
@bytes[0] == OP_DUP &&
|
|
35
|
+
@bytes[1] == OP_HASH160 &&
|
|
36
|
+
@bytes[2] == 20 &&
|
|
37
|
+
@bytes[23] == OP_EQUALVERIFY &&
|
|
38
|
+
@bytes[24] == OP_CHECKSIG
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def hash160
|
|
42
|
+
return nil unless p2pkh?
|
|
43
|
+
@bytes[3, 20].pack('C*').unpack1('H*')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def p2pkh_address
|
|
49
|
+
h = hash160
|
|
50
|
+
return nil unless h
|
|
51
|
+
versioned = "00#{h}"
|
|
52
|
+
checksum = Base58.check(versioned)
|
|
53
|
+
Base58.encode(versioned + checksum)
|
|
56
54
|
end
|
|
57
55
|
end
|
|
58
56
|
end
|