sibit 0.33.0 → 0.34.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 +4 -5
- data/Gemfile.lock +78 -58
- data/README.md +21 -2
- data/Rakefile +1 -1
- data/bin/sibit +6 -4
- data/cross/alpine.df +6 -0
- data/cross/centos.df +8 -0
- data/cross/debian.df +9 -0
- data/cross/fedora.df +7 -0
- data/cross/opensuse.df +17 -0
- data/cross/rocky.df +8 -0
- data/cross/ubuntu.df +9 -0
- data/features/step_definitions/steps.rb +5 -5
- data/features/support/env.rb +1 -1
- data/lib/sibit/base58.rb +3 -6
- data/lib/sibit/bech32.rb +23 -15
- data/lib/sibit/bestof.rb +41 -11
- data/lib/sibit/bin.rb +68 -54
- data/lib/sibit/bitcoinchain.rb +10 -10
- data/lib/sibit/blockchain.rb +10 -20
- data/lib/sibit/blockchair.rb +14 -24
- data/lib/sibit/btc.rb +51 -60
- data/lib/sibit/cex.rb +12 -11
- data/lib/sibit/coins.rb +35 -0
- data/lib/sibit/cryptoapis.rb +35 -30
- data/lib/sibit/firstof.rb +8 -5
- data/lib/sibit/httpproxy.rb +1 -1
- data/lib/sibit/json.rb +8 -9
- data/lib/sibit/key.rb +39 -41
- data/lib/sibit/script.rb +3 -6
- data/lib/sibit/sochain.rb +180 -0
- data/lib/sibit/tx.rb +75 -36
- data/lib/sibit/txbuilder.rb +15 -3
- data/lib/sibit/version.rb +1 -2
- data/lib/sibit.rb +80 -62
- data/sibit.gemspec +11 -11
- metadata +10 -1
data/lib/sibit/btc.rb
CHANGED
|
@@ -28,22 +28,18 @@ class Sibit::Btc
|
|
|
28
28
|
|
|
29
29
|
# Current price of BTC in USD (float returned).
|
|
30
30
|
def price(_currency = 'USD')
|
|
31
|
-
raise
|
|
31
|
+
raise(Sibit::NotSupportedError, 'Btc.com API doesn\'t provide prices')
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
# Gets the balance of the address, in satoshi.
|
|
35
35
|
def balance(address)
|
|
36
|
-
|
|
37
|
-
json = Sibit::Json.new(http: @http, log: @log).get(uri)
|
|
36
|
+
json = Sibit::Json.new(http: @http, log: @log).get(Iri.new('https://chain.api.btc.com/v3/address').append(address).append('unspent'))
|
|
38
37
|
if json['err_no'] == 1
|
|
39
38
|
@log.debug("The balance of #{address} is zero (not found)")
|
|
40
39
|
return 0
|
|
41
40
|
end
|
|
42
41
|
data = json['data']
|
|
43
|
-
if data.nil?
|
|
44
|
-
@log.debug("The balance of #{address} is probably zero (not found)")
|
|
45
|
-
return 0
|
|
46
|
-
end
|
|
42
|
+
raise(Sibit::Error, "The address #{address} not found") if data.nil?
|
|
47
43
|
txns = data['list']
|
|
48
44
|
if txns.nil?
|
|
49
45
|
@log.debug("The balance of #{address} is probably zero (not found)")
|
|
@@ -56,11 +52,10 @@ class Sibit::Btc
|
|
|
56
52
|
|
|
57
53
|
# Get hash of the block after this one, or NIL if it's the last one in Blockchain.
|
|
58
54
|
def next_of(hash)
|
|
59
|
-
|
|
55
|
+
data = Sibit::Json.new(http: @http, log: @log).get(
|
|
60
56
|
Iri.new('https://chain.api.btc.com/v3/block').append(hash)
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
raise Sibit::Error, "The block #{hash} not found" if data.nil?
|
|
57
|
+
)['data']
|
|
58
|
+
raise(Sibit::Error, "The block #{hash} not found") if data.nil?
|
|
64
59
|
nxt = data['next_block_hash']
|
|
65
60
|
nxt = nil if nxt == '0000000000000000000000000000000000000000000000000000000000000000'
|
|
66
61
|
@log.debug("In BTC.com the block #{hash} is the latest, there is no next block") if nxt.nil?
|
|
@@ -70,28 +65,25 @@ class Sibit::Btc
|
|
|
70
65
|
|
|
71
66
|
# The height of the block.
|
|
72
67
|
def height(hash)
|
|
73
|
-
|
|
68
|
+
data = Sibit::Json.new(http: @http, log: @log).get(
|
|
74
69
|
Iri.new('https://chain.api.btc.com/v3/block').append(hash)
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
raise Sibit::Error, "The block #{hash} not found" if data.nil?
|
|
70
|
+
)['data']
|
|
71
|
+
raise(Sibit::Error, "The block #{hash} not found") if data.nil?
|
|
78
72
|
h = data['height']
|
|
79
|
-
raise
|
|
73
|
+
raise(Sibit::Error, "The block #{hash} found but the height is absent") if h.nil?
|
|
80
74
|
@log.debug("The height of #{hash} is #{h}")
|
|
81
75
|
h
|
|
82
76
|
end
|
|
83
77
|
|
|
84
78
|
# Get recommended fees, in satoshi per byte.
|
|
85
79
|
def fees
|
|
86
|
-
raise
|
|
80
|
+
raise(Sibit::NotSupportedError, 'Btc.com doesn\'t provide recommended fees')
|
|
87
81
|
end
|
|
88
82
|
|
|
89
83
|
# Gets the hash of the latest block.
|
|
90
84
|
def latest
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
data = json['data']
|
|
94
|
-
raise Sibit::Error, 'The latest block not found' if data.nil?
|
|
85
|
+
data = Sibit::Json.new(http: @http, log: @log).get(Iri.new('https://chain.api.btc.com/v3/block/latest'))['data']
|
|
86
|
+
raise(Sibit::Error, 'The latest block not found') if data.nil?
|
|
95
87
|
hash = data['hash']
|
|
96
88
|
@log.debug("The hash of the latest block is #{hash}")
|
|
97
89
|
hash
|
|
@@ -99,46 +91,44 @@ class Sibit::Btc
|
|
|
99
91
|
|
|
100
92
|
# Fetch all unspent outputs per address.
|
|
101
93
|
def utxos(sources)
|
|
102
|
-
|
|
94
|
+
results = []
|
|
103
95
|
sources.each do |hash|
|
|
104
|
-
|
|
96
|
+
data = Sibit::Json.new(http: @http, log: @log).get(
|
|
105
97
|
Iri.new('https://chain.api.btc.com/v3/address').append(hash).append('unspent')
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
98
|
+
)['data']
|
|
99
|
+
raise(Sibit::Error, "The address #{hash} not found") if data.nil?
|
|
100
|
+
list = data['list']
|
|
101
|
+
next if list.nil?
|
|
102
|
+
list.each do |u|
|
|
103
|
+
index = u['tx_output_n']
|
|
104
|
+
raise(Sibit::Error, "The unspent output of #{hash} has no tx_output_n") if index.nil?
|
|
105
|
+
out = Sibit::Json.new(http: @http, log: @log).get(
|
|
113
106
|
Iri.new('https://chain.api.btc.com/v3/tx').append(u['tx_hash']).add(verbose: 3)
|
|
114
|
-
)['data']['outputs']
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
end
|
|
107
|
+
)['data']['outputs'][index]
|
|
108
|
+
raise(Sibit::Error, "The output #{index} of #{u['tx_hash']} is absent") if out.nil?
|
|
109
|
+
results << {
|
|
110
|
+
value: u['value'],
|
|
111
|
+
hash: u['tx_hash'],
|
|
112
|
+
index: index,
|
|
113
|
+
confirmations: u['confirmations'],
|
|
114
|
+
script: [out['script_hex']].pack('H*')
|
|
115
|
+
}
|
|
125
116
|
end
|
|
126
117
|
end
|
|
127
|
-
|
|
118
|
+
results
|
|
128
119
|
end
|
|
129
120
|
|
|
130
121
|
# Push this transaction (in hex format) to the network.
|
|
131
122
|
def push(_hex)
|
|
132
|
-
raise
|
|
123
|
+
raise(Sibit::NotSupportedError, 'Btc.com doesn\'t provide payment gateway')
|
|
133
124
|
end
|
|
134
125
|
|
|
135
126
|
# This method should fetch a Blockchain block and return as a hash.
|
|
136
127
|
def block(hash)
|
|
137
|
-
|
|
128
|
+
data = Sibit::Json.new(http: @http, log: @log).get(
|
|
138
129
|
Iri.new('https://chain.api.btc.com/v3/block').append(hash)
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
raise Sibit::Error, "The block #{hash} not found" if data.nil?
|
|
130
|
+
)['data']
|
|
131
|
+
raise(Sibit::Error, "The block #{hash} not found") if data.nil?
|
|
142
132
|
nxt = data['next_block_hash']
|
|
143
133
|
nxt = nil if nxt == '0000000000000000000000000000000000000000000000000000000000000000'
|
|
144
134
|
{
|
|
@@ -162,20 +152,21 @@ class Sibit::Btc
|
|
|
162
152
|
Iri.new('https://chain.api.btc.com/v3/block')
|
|
163
153
|
.append(hash).append('tx').add(page: page, pagesize: psize)
|
|
164
154
|
)['data']
|
|
165
|
-
raise
|
|
155
|
+
raise(Sibit::Error, "The block #{hash} has no data at page #{page}") if data.nil?
|
|
166
156
|
list = data['list']
|
|
167
|
-
raise
|
|
168
|
-
txns =
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
{
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
157
|
+
raise(Sibit::Error, "The list is empty for block #{hash} at page #{page}") if list.nil?
|
|
158
|
+
txns =
|
|
159
|
+
list.map do |t|
|
|
160
|
+
{
|
|
161
|
+
hash: t['hash'],
|
|
162
|
+
outputs: t['outputs'].reject { |o| o['spent_by_tx'] }.map do |o|
|
|
163
|
+
{
|
|
164
|
+
address: o['addresses'][0],
|
|
165
|
+
value: o['value']
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
}
|
|
169
|
+
end
|
|
179
170
|
all += txns
|
|
180
171
|
page += 1
|
|
181
172
|
break if txns.length < psize
|
data/lib/sibit/cex.rb
CHANGED
|
@@ -25,51 +25,52 @@ class Sibit::Cex
|
|
|
25
25
|
|
|
26
26
|
# Current price of BTC in USD (float returned).
|
|
27
27
|
def price(currency = 'USD')
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
p = Float(
|
|
29
|
+
Sibit::Json.new(http: @http, log: @log).get(
|
|
30
|
+
Iri.new('https://cex.io/api/last_price/BTC').append(currency)
|
|
31
|
+
)['lprice']
|
|
30
32
|
)
|
|
31
|
-
p = json['lprice'].to_f
|
|
32
33
|
@log.debug("The price of BTC is #{p} #{currency}")
|
|
33
34
|
p
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
# Get hash of the block after this one.
|
|
37
38
|
def next_of(_hash)
|
|
38
|
-
raise
|
|
39
|
+
raise(Sibit::NotSupportedError, 'Cex.io API doesn\'t provide next_of()')
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
# Gets the balance of the address, in satoshi.
|
|
42
43
|
def balance(_address)
|
|
43
|
-
raise
|
|
44
|
+
raise(Sibit::NotSupportedError, 'Cex.io doesn\'t implement balance()')
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
# The height of the block.
|
|
47
48
|
def height(_hash)
|
|
48
|
-
raise
|
|
49
|
+
raise(Sibit::NotSupportedError, 'Cex.io doesn\'t implement height()')
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
# Get recommended fees, in satoshi per byte.
|
|
52
53
|
def fees
|
|
53
|
-
raise
|
|
54
|
+
raise(Sibit::NotSupportedError, 'Cex.io doesn\'t implement fees()')
|
|
54
55
|
end
|
|
55
56
|
|
|
56
57
|
# Gets the hash of the latest block.
|
|
57
58
|
def latest
|
|
58
|
-
raise
|
|
59
|
+
raise(Sibit::NotSupportedError, 'Cex.io doesn\'t implement latest()')
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
# Fetch all unspent outputs per address.
|
|
62
63
|
def utxos(_sources)
|
|
63
|
-
raise
|
|
64
|
+
raise(Sibit::NotSupportedError, 'Cex.io doesn\'t implement utxos()')
|
|
64
65
|
end
|
|
65
66
|
|
|
66
67
|
# Push this transaction (in hex format) to the network.
|
|
67
68
|
def push(_hex)
|
|
68
|
-
raise
|
|
69
|
+
raise(Sibit::NotSupportedError, 'Cex.io doesn\'t implement push()')
|
|
69
70
|
end
|
|
70
71
|
|
|
71
72
|
# This method should fetch a Blockchain block and return as a hash.
|
|
72
73
|
def block(_hash)
|
|
73
|
-
raise
|
|
74
|
+
raise(Sibit::NotSupportedError, 'Cex.io doesn\'t implement block()')
|
|
74
75
|
end
|
|
75
76
|
end
|
data/lib/sibit/coins.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
require 'bigdecimal'
|
|
7
|
+
require_relative 'error'
|
|
8
|
+
|
|
9
|
+
# Sibit main class.
|
|
10
|
+
class Sibit
|
|
11
|
+
# An amount of bitcoins, convertible to an exact number of satoshi.
|
|
12
|
+
#
|
|
13
|
+
# It parses the amount through +BigDecimal+ instead of a binary +Float+,
|
|
14
|
+
# so decimal values like 0.29 map to exactly 29,000,000 satoshi. An amount
|
|
15
|
+
# with sub-satoshi precision is rejected instead of being truncated.
|
|
16
|
+
#
|
|
17
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
|
18
|
+
# Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
|
|
19
|
+
# License:: MIT
|
|
20
|
+
class Coins
|
|
21
|
+
def initialize(btc)
|
|
22
|
+
@btc = btc
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def satoshi
|
|
26
|
+
sat = BigDecimal(@btc.to_s) * 100_000_000
|
|
27
|
+
unless sat.frac.zero?
|
|
28
|
+
raise(Sibit::Error, "The amount #{@btc.inspect} is finer than one satoshi")
|
|
29
|
+
end
|
|
30
|
+
Integer(sat)
|
|
31
|
+
rescue ArgumentError, TypeError
|
|
32
|
+
raise(Sibit::Error, "The amount #{@btc.inspect} is not a valid number of bitcoins")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/sibit/cryptoapis.rb
CHANGED
|
@@ -7,6 +7,7 @@ require 'iri'
|
|
|
7
7
|
require 'json'
|
|
8
8
|
require 'loog'
|
|
9
9
|
require 'uri'
|
|
10
|
+
require_relative 'coins'
|
|
10
11
|
require_relative 'error'
|
|
11
12
|
require_relative 'http'
|
|
12
13
|
require_relative 'json'
|
|
@@ -27,15 +28,18 @@ class Sibit::Cryptoapis
|
|
|
27
28
|
|
|
28
29
|
# Current price of BTC in USD (float returned).
|
|
29
30
|
def price(_currency = 'USD')
|
|
30
|
-
raise
|
|
31
|
+
raise(Sibit::NotSupportedError, 'Cryptoapis doesn\'t provide BTC price')
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
# Get hash of the block after this one.
|
|
34
35
|
def next_of(hash)
|
|
35
|
-
|
|
36
|
+
head = Sibit::Json.new(http: @http, log: @log).get(
|
|
36
37
|
Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/blocks').append(hash),
|
|
37
38
|
headers: headers
|
|
38
|
-
)['payload']
|
|
39
|
+
)['payload']
|
|
40
|
+
raise(Sibit::Error, "The block #{hash} not found") if head.nil?
|
|
41
|
+
nxt = head['nextblockhash']
|
|
42
|
+
nxt = nil if nxt == '0000000000000000000000000000000000000000000000000000000000000000'
|
|
39
43
|
@log.debug("The block #{hash} is the latest, there is no next block") if nxt.nil?
|
|
40
44
|
@log.debug("The next block of #{hash} is #{nxt}") unless nxt.nil?
|
|
41
45
|
nxt
|
|
@@ -43,29 +47,29 @@ class Sibit::Cryptoapis
|
|
|
43
47
|
|
|
44
48
|
# The height of the block.
|
|
45
49
|
def height(hash)
|
|
46
|
-
|
|
50
|
+
h = Sibit::Json.new(http: @http, log: @log).get(
|
|
47
51
|
Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/blocks').append(hash),
|
|
48
52
|
headers: headers
|
|
49
|
-
)['payload']
|
|
50
|
-
h = json['height']
|
|
53
|
+
)['payload']['height']
|
|
51
54
|
@log.debug("The height of #{hash} is #{h}")
|
|
52
55
|
h
|
|
53
56
|
end
|
|
54
57
|
|
|
55
58
|
# Gets the balance of the address, in satoshi.
|
|
56
59
|
def balance(address)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
b = Sibit::Coins.new(
|
|
61
|
+
Sibit::Json.new(http: @http, log: @log).get(
|
|
62
|
+
Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/address').append(address),
|
|
63
|
+
headers: headers
|
|
64
|
+
)['payload']['balance']
|
|
65
|
+
).satoshi
|
|
62
66
|
@log.debug("The balance of #{address} is #{b} satoshi")
|
|
63
67
|
b
|
|
64
68
|
end
|
|
65
69
|
|
|
66
70
|
# Get recommended fees, in satoshi per byte.
|
|
67
71
|
def fees
|
|
68
|
-
raise
|
|
72
|
+
raise(Sibit::NotSupportedError, 'Cryptoapis doesn\'t provide recommended fees')
|
|
69
73
|
end
|
|
70
74
|
|
|
71
75
|
# Gets the hash of the latest block.
|
|
@@ -80,15 +84,15 @@ class Sibit::Cryptoapis
|
|
|
80
84
|
|
|
81
85
|
# Fetch all unspent outputs per address.
|
|
82
86
|
def utxos(_sources)
|
|
83
|
-
raise
|
|
87
|
+
raise(Sibit::NotSupportedError, 'Not implemented yet')
|
|
84
88
|
end
|
|
85
89
|
|
|
86
90
|
# Push this transaction (in hex format) to the network.
|
|
87
91
|
def push(hex)
|
|
88
92
|
Sibit::Json.new(http: @http, log: @log).post(
|
|
89
|
-
Iri.new('https://api.cryptoapis.io/v1/bc/btc/
|
|
93
|
+
Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/txs/send'),
|
|
90
94
|
JSON.pretty_generate(hex: hex),
|
|
91
|
-
headers: headers
|
|
95
|
+
headers: headers.merge('Content-Type' => 'application/json')
|
|
92
96
|
)
|
|
93
97
|
end
|
|
94
98
|
|
|
@@ -122,21 +126,22 @@ class Sibit::Cryptoapis
|
|
|
122
126
|
limit = 200
|
|
123
127
|
all = []
|
|
124
128
|
loop do
|
|
125
|
-
txns =
|
|
126
|
-
|
|
127
|
-
.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
129
|
+
txns =
|
|
130
|
+
Sibit::Json.new(http: @http, log: @log).get(
|
|
131
|
+
Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/txs/block/')
|
|
132
|
+
.append(hash).add(index: index, limit: limit),
|
|
133
|
+
headers: headers
|
|
134
|
+
)['payload'].map do |t|
|
|
135
|
+
{
|
|
136
|
+
hash: t['hash'],
|
|
137
|
+
outputs: t['txouts'].map do |o|
|
|
138
|
+
{
|
|
139
|
+
address: o['addresses'][0],
|
|
140
|
+
value: Sibit::Coins.new(o['amount'] || 0).satoshi
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
}
|
|
144
|
+
end
|
|
140
145
|
all += txns
|
|
141
146
|
index += txns.length
|
|
142
147
|
break if txns.length < limit
|
data/lib/sibit/firstof.rb
CHANGED
|
@@ -83,18 +83,18 @@ class Sibit::FirstOf
|
|
|
83
83
|
private
|
|
84
84
|
|
|
85
85
|
def first_of(method)
|
|
86
|
-
return yield
|
|
86
|
+
return yield(@list) unless @list.is_a?(Array)
|
|
87
87
|
errors = []
|
|
88
88
|
done = false
|
|
89
89
|
result = nil
|
|
90
90
|
@list.each do |api|
|
|
91
91
|
@log.debug("Calling #{api.class.name}##{method}()...")
|
|
92
92
|
begin
|
|
93
|
-
result = yield
|
|
93
|
+
result = yield(api)
|
|
94
94
|
done = true
|
|
95
95
|
break
|
|
96
96
|
rescue Sibit::NotSupportedError
|
|
97
|
-
|
|
97
|
+
nil
|
|
98
98
|
rescue Sibit::Error => e
|
|
99
99
|
errors << e
|
|
100
100
|
@log.debug("The API #{api.class.name} failed at #{method}(): #{e.message}") if @verbose
|
|
@@ -102,8 +102,11 @@ class Sibit::FirstOf
|
|
|
102
102
|
end
|
|
103
103
|
unless done
|
|
104
104
|
errors.each { |e| @log.debug(Backtrace.new(e).to_s) }
|
|
105
|
-
raise
|
|
106
|
-
|
|
105
|
+
raise(
|
|
106
|
+
Sibit::Error,
|
|
107
|
+
"No APIs out of #{@list.length} managed to succeed at #{method}(): " \
|
|
108
|
+
"#{@list.map { |a| a.class.name }.join(', ')}"
|
|
109
|
+
)
|
|
107
110
|
end
|
|
108
111
|
result
|
|
109
112
|
end
|
data/lib/sibit/httpproxy.rb
CHANGED
|
@@ -22,7 +22,7 @@ class Sibit
|
|
|
22
22
|
attr_reader :host
|
|
23
23
|
|
|
24
24
|
def client(uri)
|
|
25
|
-
http = Net::HTTP.new(uri.host, uri.port, @host, @port
|
|
25
|
+
http = Net::HTTP.new(uri.host, uri.port, @host, Integer(@port, 10), @user, @password)
|
|
26
26
|
http.use_ssl = true
|
|
27
27
|
http.read_timeout = 240
|
|
28
28
|
http
|
data/lib/sibit/json.rb
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
# SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
|
|
4
4
|
# SPDX-License-Identifier: MIT
|
|
5
5
|
|
|
6
|
-
require 'cgi'
|
|
7
6
|
require 'elapsed'
|
|
8
7
|
require 'json'
|
|
9
8
|
require 'loog'
|
|
@@ -43,16 +42,16 @@ class Sibit::Json
|
|
|
43
42
|
'Accept-Encoding' => ''
|
|
44
43
|
}.merge(headers)
|
|
45
44
|
)
|
|
46
|
-
unless accept.include?(res.code
|
|
47
|
-
raise
|
|
45
|
+
unless accept.include?(Integer(res.code, 10))
|
|
46
|
+
raise(Sibit::Error, "Failed to retrieve #{uri} (#{res.code}): #{res.body}")
|
|
48
47
|
end
|
|
49
48
|
ret =
|
|
50
49
|
begin
|
|
51
50
|
JSON.parse(res.body)
|
|
52
51
|
rescue JSON::ParserError => e
|
|
53
|
-
raise
|
|
52
|
+
raise(Sibit::Error, "Can't parse JSON: #{e.message}")
|
|
54
53
|
end
|
|
55
|
-
throw
|
|
54
|
+
throw(:"GET #{uri}: #{res.code}/#{length(res.body.length)}")
|
|
56
55
|
end
|
|
57
56
|
ret
|
|
58
57
|
end
|
|
@@ -61,8 +60,8 @@ class Sibit::Json
|
|
|
61
60
|
uri = URI(address.to_s)
|
|
62
61
|
elapsed(@log) do
|
|
63
62
|
res = @http.client(uri).post(
|
|
64
|
-
"#{uri.path}?#{uri.query}",
|
|
65
|
-
|
|
63
|
+
"#{uri.path.empty? ? '/' : uri.path}#{"?#{uri.query}" if uri.query}",
|
|
64
|
+
body,
|
|
66
65
|
{
|
|
67
66
|
'Accept' => 'text/plain',
|
|
68
67
|
'User-Agent' => user_agent,
|
|
@@ -72,9 +71,9 @@ class Sibit::Json
|
|
|
72
71
|
}.merge(headers)
|
|
73
72
|
)
|
|
74
73
|
unless res.code == '200'
|
|
75
|
-
raise
|
|
74
|
+
raise(Sibit::Error, "Failed to post tx to #{uri}: #{res.code}\n#{res.body}")
|
|
76
75
|
end
|
|
77
|
-
throw
|
|
76
|
+
throw(:"POST #{uri}: #{res.code}")
|
|
78
77
|
end
|
|
79
78
|
end
|
|
80
79
|
|
data/lib/sibit/key.rb
CHANGED
|
@@ -32,13 +32,14 @@ class Sibit
|
|
|
32
32
|
def self.generate(network: :mainnet)
|
|
33
33
|
key = OpenSSL::PKey::EC.generate('secp256k1')
|
|
34
34
|
pvt = key.private_key
|
|
35
|
-
raise 'Invalid private key: zero' if pvt.zero?
|
|
36
|
-
raise 'Invalid private key: out of range' if pvt >= SECP256K1_N
|
|
37
|
-
raise 'Invalid public key: not on curve' unless key.public_key.on_curve?
|
|
35
|
+
raise(Sibit::Error, 'Invalid private key: zero') if pvt.zero?
|
|
36
|
+
raise(Sibit::Error, 'Invalid private key: out of range') if pvt >= SECP256K1_N
|
|
37
|
+
raise(Sibit::Error, 'Invalid public key: not on curve') unless key.public_key.on_curve?
|
|
38
38
|
hex = key.private_key.to_s(16).rjust(64, '0').downcase
|
|
39
|
-
raise 'Invalid private key encoding' unless hex.match?(/\A[0-9a-f]{64}\z/)
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
raise(Sibit::Error, 'Invalid private key encoding') unless hex.match?(/\A[0-9a-f]{64}\z/)
|
|
40
|
+
unless OpenSSL::BN.new(hex, 16) == pvt
|
|
41
|
+
raise(Sibit::Error, 'Private key serialization is lossy')
|
|
42
|
+
end
|
|
42
43
|
new(hex, network: network)
|
|
43
44
|
end
|
|
44
45
|
|
|
@@ -55,42 +56,38 @@ class Sibit
|
|
|
55
56
|
end
|
|
56
57
|
|
|
57
58
|
def pub
|
|
58
|
-
|
|
59
|
-
point.to_octet_string(@compressed ? :compressed : :uncompressed).unpack1('H*')
|
|
59
|
+
@key.public_key.to_octet_string(@compressed ? :compressed : :uncompressed).unpack1('H*')
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def bech32
|
|
63
63
|
hrp = { mainnet: 'bc', testnet: 'tb', regtest: 'bcrt' }[@network]
|
|
64
64
|
hex = pub
|
|
65
|
-
raise
|
|
66
|
-
raise
|
|
65
|
+
raise(Error, 'Invalid public key: not on curve') unless @key.public_key.on_curve?
|
|
66
|
+
raise(Error, 'Invalid public key format') unless hex.match?(/\A0[23][0-9a-f]{64}\z/)
|
|
67
67
|
addr = Bech32.encode(hrp, 0, hash160(hex))
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
unless addr.match?(/\A#{hrp}1q[a-z0-9]{38,58}\z/)
|
|
69
|
+
raise(Error, "Invalid bech32 address: #{addr}")
|
|
70
|
+
end
|
|
70
71
|
addr
|
|
71
72
|
end
|
|
72
73
|
|
|
73
74
|
def base58
|
|
74
75
|
hex = pub
|
|
75
|
-
raise
|
|
76
|
-
raise
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
versioned = "#{prefix}#{hash}"
|
|
80
|
-
checksum = Base58.new(versioned).check
|
|
81
|
-
addr = Base58.new(versioned + checksum).encode
|
|
76
|
+
raise(Error, 'Invalid public key: not on curve') unless @key.public_key.on_curve?
|
|
77
|
+
raise(Error, 'Invalid public key format') unless hex.match?(/\A0[23][0-9a-f]{64}\z/)
|
|
78
|
+
versioned = "#{@network == :mainnet ? '00' : '6f'}#{hash160(hex)}"
|
|
79
|
+
addr = Base58.new(versioned + Base58.new(versioned).check).encode
|
|
82
80
|
mainnet = /\A1[1-9A-HJ-NP-Za-km-z]{25,34}\z/
|
|
83
81
|
testnet = /\A[mn][1-9A-HJ-NP-Za-km-z]{25,34}\z/
|
|
84
82
|
unless addr.match?(@network == :mainnet ? mainnet : testnet)
|
|
85
|
-
raise
|
|
86
|
-
"Invalid base58 address: #{addr}"
|
|
83
|
+
raise(Error, "Invalid base58 address: #{addr}")
|
|
87
84
|
end
|
|
88
85
|
addr
|
|
89
86
|
end
|
|
90
87
|
|
|
91
88
|
def sign(data)
|
|
92
89
|
sig = @key.dsa_sign_asn1(data)
|
|
93
|
-
raise
|
|
90
|
+
raise(Error, 'Signature verification failed') unless verify(data, sig)
|
|
94
91
|
sig
|
|
95
92
|
end
|
|
96
93
|
|
|
@@ -103,25 +100,29 @@ class Sibit
|
|
|
103
100
|
private
|
|
104
101
|
|
|
105
102
|
def build(privkey)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
raise(Error, 'Private key is not on curve') unless privkey.to_i(16).between?(
|
|
104
|
+
MIN_PRIV,
|
|
105
|
+
MAX_PRIV
|
|
106
|
+
)
|
|
109
107
|
bn = OpenSSL::BN.new(privkey, 16)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
108
|
+
OpenSSL::PKey::EC.new(
|
|
109
|
+
OpenSSL::ASN1::Sequence(
|
|
110
|
+
[
|
|
111
|
+
OpenSSL::ASN1::Integer.new(1),
|
|
112
|
+
OpenSSL::ASN1::OctetString(bn.to_s(2)),
|
|
113
|
+
OpenSSL::ASN1::ObjectId('secp256k1', 0, :EXPLICIT),
|
|
114
|
+
OpenSSL::ASN1::BitString(
|
|
115
|
+
OpenSSL::PKey::EC::Group.new('secp256k1').generator.mul(bn)
|
|
116
|
+
.to_octet_string(:uncompressed),
|
|
117
|
+
1, :EXPLICIT
|
|
118
|
+
)
|
|
119
|
+
]
|
|
120
|
+
).to_der
|
|
118
121
|
)
|
|
119
|
-
OpenSSL::PKey::EC.new(asn1.to_der)
|
|
120
122
|
end
|
|
121
123
|
|
|
122
124
|
def hash160(hex)
|
|
123
|
-
|
|
124
|
-
Digest::RMD160.hexdigest(Digest::SHA256.digest(bytes))
|
|
125
|
+
Digest::RMD160.hexdigest(Digest::SHA256.digest([hex].pack('H*')))
|
|
125
126
|
end
|
|
126
127
|
|
|
127
128
|
def decode(key)
|
|
@@ -130,12 +131,9 @@ class Sibit
|
|
|
130
131
|
return key.downcase
|
|
131
132
|
end
|
|
132
133
|
raw = Base58.new(key).decode
|
|
133
|
-
|
|
134
|
-
checksum = raw[-8..]
|
|
135
|
-
expected = Base58.new(payload).check
|
|
136
|
-
raise Error, 'Invalid WIF checksum' unless checksum == expected
|
|
134
|
+
raise(Error, 'Invalid WIF checksum') unless raw[-8..] == Base58.new(raw[0..-9]).check
|
|
137
135
|
version = raw[0, 2]
|
|
138
|
-
raise
|
|
136
|
+
raise(Error, "Invalid WIF version: #{version}") unless %w[80 ef].include?(version)
|
|
139
137
|
detected = version == '80' ? :mainnet : :testnet
|
|
140
138
|
@network = @override || detected
|
|
141
139
|
body = raw[2..-9]
|
data/lib/sibit/script.rb
CHANGED
|
@@ -59,17 +59,14 @@ class Sibit
|
|
|
59
59
|
def p2pkh_address(network)
|
|
60
60
|
h = hash160
|
|
61
61
|
return nil unless h
|
|
62
|
-
|
|
63
|
-
versioned
|
|
64
|
-
checksum = Base58.new(versioned).check
|
|
65
|
-
Base58.new(versioned + checksum).encode
|
|
62
|
+
versioned = "#{network == :mainnet ? '00' : '6f'}#{h}"
|
|
63
|
+
Base58.new(versioned + Base58.new(versioned).check).encode
|
|
66
64
|
end
|
|
67
65
|
|
|
68
66
|
def p2wpkh_address(network)
|
|
69
67
|
h = hash160
|
|
70
68
|
return nil unless h
|
|
71
|
-
|
|
72
|
-
Bech32.encode(hrp, 0, h)
|
|
69
|
+
Bech32.encode({ mainnet: 'bc', testnet: 'tb', regtest: 'bcrt' }[network], 0, h)
|
|
73
70
|
end
|
|
74
71
|
end
|
|
75
72
|
end
|