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
data/lib/sibit/bitcoin/tx.rb
CHANGED
|
@@ -8,205 +8,203 @@ require_relative 'base58'
|
|
|
8
8
|
require_relative 'key'
|
|
9
9
|
require_relative 'script'
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
end
|
|
11
|
+
module Sibit::Bitcoin
|
|
12
|
+
# Bitcoin Transaction structure.
|
|
13
|
+
#
|
|
14
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
|
15
|
+
# Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
|
|
16
|
+
# License:: MIT
|
|
17
|
+
class Tx
|
|
18
|
+
SIGHASH_ALL = 0x01
|
|
19
|
+
VERSION = 1
|
|
20
|
+
SEQUENCE = 0xffffffff
|
|
21
|
+
|
|
22
|
+
attr_reader :inputs, :outputs
|
|
23
|
+
|
|
24
|
+
def initialize
|
|
25
|
+
@inputs = []
|
|
26
|
+
@outputs = []
|
|
27
|
+
end
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
def add_input(hash:, index:, script:, key:)
|
|
30
|
+
@inputs << Input.new(hash, index, script, key)
|
|
31
|
+
end
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
def add_output(value, address)
|
|
34
|
+
@outputs << Output.new(value, address)
|
|
35
|
+
end
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
def hash
|
|
38
|
+
Digest::SHA256.hexdigest(Digest::SHA256.digest(payload)).reverse.scan(/../).join
|
|
39
|
+
end
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
def payload
|
|
42
|
+
sign_inputs
|
|
43
|
+
serialize
|
|
44
|
+
end
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
def hex
|
|
47
|
+
payload.unpack1('H*')
|
|
48
|
+
end
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
def in
|
|
51
|
+
@inputs
|
|
52
|
+
end
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
def out
|
|
55
|
+
@outputs
|
|
56
|
+
end
|
|
58
57
|
|
|
59
|
-
|
|
58
|
+
private
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
end
|
|
60
|
+
def sign_inputs
|
|
61
|
+
@inputs.each_with_index do |input, idx|
|
|
62
|
+
sighash = signature_hash(idx)
|
|
63
|
+
sig = sign(input.key, sighash)
|
|
64
|
+
pubkey = [input.key.pub].pack('H*')
|
|
65
|
+
input.script_sig = der_sig(sig) + pubkey_script(pubkey)
|
|
68
66
|
end
|
|
67
|
+
end
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
def signature_hash(idx)
|
|
70
|
+
tx_copy = serialize_for_signing(idx)
|
|
71
|
+
hash_type = [SIGHASH_ALL].pack('V')
|
|
72
|
+
Digest::SHA256.digest(Digest::SHA256.digest(tx_copy + hash_type))
|
|
73
|
+
end
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
def sign(key, hash)
|
|
76
|
+
der = key.sign(hash)
|
|
77
|
+
repack(der)
|
|
78
|
+
end
|
|
80
79
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
80
|
+
def repack(der)
|
|
81
|
+
return der if low_s?(der)
|
|
82
|
+
seq = OpenSSL::ASN1.decode(der)
|
|
83
|
+
r = seq.value[0].value.to_i
|
|
84
|
+
s = seq.value[1].value.to_i
|
|
85
|
+
order = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
|
86
|
+
s = order - s if s > order / 2
|
|
87
|
+
OpenSSL::ASN1::Sequence.new(
|
|
88
|
+
[OpenSSL::ASN1::Integer.new(r), OpenSSL::ASN1::Integer.new(s)]
|
|
89
|
+
).to_der
|
|
90
|
+
end
|
|
92
91
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
def low_s?(der)
|
|
93
|
+
seq = OpenSSL::ASN1.decode(der)
|
|
94
|
+
s = seq.value[1].value.to_i
|
|
95
|
+
order = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
|
96
|
+
s <= order / 2
|
|
97
|
+
end
|
|
99
98
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
def der_sig(sig)
|
|
100
|
+
data = sig + [SIGHASH_ALL].pack('C')
|
|
101
|
+
[data.length].pack('C') + data
|
|
102
|
+
end
|
|
104
103
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
def pubkey_script(pubkey)
|
|
105
|
+
[pubkey.length].pack('C') + pubkey
|
|
106
|
+
end
|
|
108
107
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
108
|
+
def serialize
|
|
109
|
+
result = [VERSION].pack('V')
|
|
110
|
+
result += varint(@inputs.length)
|
|
111
|
+
@inputs.each do |input|
|
|
112
|
+
result += [input.hash].pack('H*').reverse
|
|
113
|
+
result += [input.index].pack('V')
|
|
114
|
+
result += varint(input.script_sig.length)
|
|
115
|
+
result += input.script_sig
|
|
116
|
+
result += [SEQUENCE].pack('V')
|
|
117
|
+
end
|
|
118
|
+
result += varint(@outputs.length)
|
|
119
|
+
@outputs.each do |output|
|
|
120
|
+
result += [output.value].pack('Q<')
|
|
121
|
+
script = output.script
|
|
122
|
+
result += varint(script.length)
|
|
123
|
+
result += script
|
|
124
|
+
end
|
|
125
|
+
result += [0].pack('V')
|
|
126
|
+
result
|
|
127
|
+
end
|
|
129
128
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
result += varint(script.length)
|
|
139
|
-
result += script
|
|
140
|
-
else
|
|
141
|
-
result += varint(0)
|
|
142
|
-
end
|
|
143
|
-
result += [SEQUENCE].pack('V')
|
|
144
|
-
end
|
|
145
|
-
result += varint(@outputs.length)
|
|
146
|
-
@outputs.each do |output|
|
|
147
|
-
result += [output.value].pack('Q<')
|
|
148
|
-
script = output.script
|
|
129
|
+
def serialize_for_signing(idx)
|
|
130
|
+
result = [VERSION].pack('V')
|
|
131
|
+
result += varint(@inputs.length)
|
|
132
|
+
@inputs.each_with_index do |input, i|
|
|
133
|
+
result += [input.hash].pack('H*').reverse
|
|
134
|
+
result += [input.index].pack('V')
|
|
135
|
+
if i == idx
|
|
136
|
+
script = [input.prev_script].pack('H*')
|
|
149
137
|
result += varint(script.length)
|
|
150
138
|
result += script
|
|
139
|
+
else
|
|
140
|
+
result += varint(0)
|
|
151
141
|
end
|
|
152
|
-
result += [
|
|
153
|
-
result
|
|
142
|
+
result += [SEQUENCE].pack('V')
|
|
154
143
|
end
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
144
|
+
result += varint(@outputs.length)
|
|
145
|
+
@outputs.each do |output|
|
|
146
|
+
result += [output.value].pack('Q<')
|
|
147
|
+
script = output.script
|
|
148
|
+
result += varint(script.length)
|
|
149
|
+
result += script
|
|
161
150
|
end
|
|
151
|
+
result += [0].pack('V')
|
|
152
|
+
result
|
|
162
153
|
end
|
|
163
154
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
155
|
+
def varint(num)
|
|
156
|
+
return [num].pack('C') if num < 0xfd
|
|
157
|
+
return [0xfd, num].pack('Cv') if num <= 0xffff
|
|
158
|
+
return [0xfe, num].pack('CV') if num <= 0xffffffff
|
|
159
|
+
[0xff, num].pack('CQ<')
|
|
160
|
+
end
|
|
161
|
+
end
|
|
168
162
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
163
|
+
# Transaction input.
|
|
164
|
+
class Input
|
|
165
|
+
attr_reader :hash, :index, :prev_script, :key
|
|
166
|
+
attr_accessor :script_sig
|
|
167
|
+
|
|
168
|
+
def initialize(hash, index, script, key)
|
|
169
|
+
@hash = hash
|
|
170
|
+
@index = index
|
|
171
|
+
@prev_script = script
|
|
172
|
+
@key = key
|
|
173
|
+
@script_sig = ''
|
|
174
|
+
end
|
|
176
175
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
176
|
+
def prev_out
|
|
177
|
+
[@hash].pack('H*')
|
|
178
|
+
end
|
|
180
179
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
end
|
|
180
|
+
def prev_out_index
|
|
181
|
+
@index
|
|
184
182
|
end
|
|
183
|
+
end
|
|
185
184
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
185
|
+
# Transaction output.
|
|
186
|
+
class Output
|
|
187
|
+
attr_reader :value
|
|
189
188
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
189
|
+
def initialize(value, address)
|
|
190
|
+
@value = value
|
|
191
|
+
@address = address
|
|
192
|
+
end
|
|
194
193
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
194
|
+
def script
|
|
195
|
+
hash160 = address_to_hash160(@address)
|
|
196
|
+
[0x76, 0xa9, 0x14].pack('C*') + [hash160].pack('H*') + [0x88, 0xac].pack('C*')
|
|
197
|
+
end
|
|
199
198
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
199
|
+
def script_hex
|
|
200
|
+
script.unpack1('H*')
|
|
201
|
+
end
|
|
203
202
|
|
|
204
|
-
|
|
203
|
+
private
|
|
205
204
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
end
|
|
205
|
+
def address_to_hash160(addr)
|
|
206
|
+
decoded = Base58.decode(addr)
|
|
207
|
+
decoded[2..41]
|
|
210
208
|
end
|
|
211
209
|
end
|
|
212
210
|
end
|
data/lib/sibit/bitcoinchain.rb
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
require 'iri'
|
|
7
7
|
require 'json'
|
|
8
|
+
require 'loog'
|
|
8
9
|
require 'uri'
|
|
9
10
|
require_relative 'error'
|
|
10
11
|
require_relative 'http'
|
|
11
|
-
require 'loog'
|
|
12
12
|
require_relative 'json'
|
|
13
13
|
require_relative 'version'
|
|
14
14
|
|
|
@@ -17,110 +17,107 @@ require_relative 'version'
|
|
|
17
17
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
|
18
18
|
# Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
|
|
19
19
|
# License:: MIT
|
|
20
|
-
class Sibit
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@dry = dry
|
|
28
|
-
end
|
|
20
|
+
class Sibit::Bitcoinchain
|
|
21
|
+
# Constructor.
|
|
22
|
+
def initialize(log: Loog::NULL, http: Sibit::Http.new, dry: false)
|
|
23
|
+
@http = http
|
|
24
|
+
@log = log
|
|
25
|
+
@dry = dry
|
|
26
|
+
end
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
# Current price of BTC in USD (float returned).
|
|
29
|
+
def price(_currency = 'USD')
|
|
30
|
+
raise Sibit::NotSupportedError, 'Bitcoinchain API doesn\'t provide BTC price'
|
|
31
|
+
end
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
# The height of the block.
|
|
34
|
+
def height(_hash)
|
|
35
|
+
raise Sibit::NotSupportedError, 'Bitcoinchain API doesn\'t provide height()'
|
|
36
|
+
end
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
38
|
+
# Get hash of the block after this one.
|
|
39
|
+
def next_of(hash)
|
|
40
|
+
block = Sibit::Json.new(http: @http, log: @log).get(
|
|
41
|
+
Iri.new('https://api-r.bitcoinchain.com/v1/block').append(hash)
|
|
42
|
+
)[0]
|
|
43
|
+
raise Sibit::Error, "Block #{hash} not found" if block.nil?
|
|
44
|
+
nxt = block['next_block']
|
|
45
|
+
nxt = nil if nxt == '0000000000000000000000000000000000000000000000000000000000000000'
|
|
46
|
+
@log.info("The block #{hash} is the latest, there is no next block") if nxt.nil?
|
|
47
|
+
@log.info("The next block of #{hash} is #{nxt}") unless nxt.nil?
|
|
48
|
+
nxt
|
|
49
|
+
end
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
end
|
|
64
|
-
b *= 100_000_000
|
|
65
|
-
b = b.to_i
|
|
66
|
-
@log.info("The balance of #{address} is #{b} satoshi (#{json['transactions']} txns)")
|
|
67
|
-
b
|
|
51
|
+
# Gets the balance of the address, in satoshi.
|
|
52
|
+
def balance(address)
|
|
53
|
+
json = Sibit::Json.new(http: @http, log: @log).get(
|
|
54
|
+
Iri.new('https://api-r.bitcoinchain.com/v1/address').append(address),
|
|
55
|
+
accept: [200, 409]
|
|
56
|
+
)[0]
|
|
57
|
+
b = json['balance']
|
|
58
|
+
if b.nil?
|
|
59
|
+
@log.info("The balance of #{address} is not visible")
|
|
60
|
+
return 0
|
|
68
61
|
end
|
|
62
|
+
b *= 100_000_000
|
|
63
|
+
b = b.to_i
|
|
64
|
+
@log.info("The balance of #{address} is #{b} satoshi (#{json['transactions']} txns)")
|
|
65
|
+
b
|
|
66
|
+
end
|
|
69
67
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
# Get recommended fees, in satoshi per byte.
|
|
69
|
+
def fees
|
|
70
|
+
raise Sibit::NotSupportedError, 'Not implemented yet'
|
|
71
|
+
end
|
|
74
72
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
73
|
+
# Gets the hash of the latest block.
|
|
74
|
+
def latest
|
|
75
|
+
hash = Sibit::Json.new(http: @http, log: @log).get(
|
|
76
|
+
Iri.new('https://api-r.bitcoinchain.com/v1/status')
|
|
77
|
+
)['hash']
|
|
78
|
+
@log.info("The latest block hash is #{hash}")
|
|
79
|
+
hash
|
|
80
|
+
end
|
|
83
81
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
# Fetch all unspent outputs per address.
|
|
83
|
+
def utxos(_sources)
|
|
84
|
+
raise Sibit::NotSupportedError, 'Not implemented yet'
|
|
85
|
+
end
|
|
88
86
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
# Push this transaction (in hex format) to the network.
|
|
88
|
+
def push(_hex)
|
|
89
|
+
raise Sibit::NotSupportedError, 'Not implemented yet'
|
|
90
|
+
end
|
|
93
91
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
end
|
|
92
|
+
# This method should fetch a Blockchain block and return as a hash. Raises
|
|
93
|
+
# an exception if the block is not found.
|
|
94
|
+
def block(hash)
|
|
95
|
+
head = Sibit::Json.new(http: @http, log: @log).get(
|
|
96
|
+
Iri.new('https://api-r.bitcoinchain.com/v1/block').append(hash)
|
|
97
|
+
)[0]
|
|
98
|
+
raise Sibit::Error, "The block #{hash} is not found" if head.nil?
|
|
99
|
+
txs = Sibit::Json.new(http: @http, log: @log).get(
|
|
100
|
+
Iri.new('https://api-r.bitcoinchain.com/v1/block/txs').append(hash)
|
|
101
|
+
)
|
|
102
|
+
nxt = head['next_block']
|
|
103
|
+
nxt = nil if nxt == '0000000000000000000000000000000000000000000000000000000000000000'
|
|
104
|
+
{
|
|
105
|
+
provider: self.class.name,
|
|
106
|
+
hash: head['hash'],
|
|
107
|
+
orphan: !head['is_main'],
|
|
108
|
+
next: nxt,
|
|
109
|
+
previous: head['prev_block'],
|
|
110
|
+
txns: txs[0]['txs'].map do |t|
|
|
111
|
+
{
|
|
112
|
+
hash: t['self_hash'],
|
|
113
|
+
outputs: t['outputs'].map do |o|
|
|
114
|
+
{
|
|
115
|
+
address: o['receiver'],
|
|
116
|
+
value: o['value'] * 100_000_000
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
}
|
|
125
122
|
end
|
|
126
123
|
end
|