sibit 0.32.8 → 0.34.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 +4 -5
- data/Gemfile.lock +76 -56
- data/README.md +16 -2
- data/Rakefile +1 -1
- data/bin/sibit +6 -4
- data/cross/alpine.df +6 -0
- data/cross/centos.df +13 -0
- data/cross/debian.df +9 -0
- data/cross/fedora.df +7 -0
- data/cross/opensuse.df +9 -0
- data/cross/rocky.df +13 -0
- data/cross/ubuntu.df +9 -0
- data/features/cli.feature +1 -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 +9 -14
- data/lib/sibit/bestof.rb +7 -4
- data/lib/sibit/bin.rb +65 -51
- data/lib/sibit/bitcoinchain.rb +8 -8
- data/lib/sibit/blockchain.rb +5 -19
- data/lib/sibit/blockchair.rb +14 -24
- data/lib/sibit/btc.rb +39 -45
- data/lib/sibit/cex.rb +12 -11
- data/lib/sibit/cryptoapis.rb +30 -27
- data/lib/sibit/firstof.rb +8 -5
- data/lib/sibit/httpproxy.rb +1 -1
- data/lib/sibit/json.rb +7 -7
- data/lib/sibit/key.rb +39 -41
- data/lib/sibit/script.rb +3 -6
- data/lib/sibit/sochain.rb +177 -0
- data/lib/sibit/tx.rb +48 -32
- data/lib/sibit/txbuilder.rb +3 -3
- data/lib/sibit/version.rb +1 -2
- data/lib/sibit.rb +71 -56
- data/sibit.gemspec +11 -11
- metadata +9 -1
data/lib/sibit/cryptoapis.rb
CHANGED
|
@@ -27,7 +27,7 @@ class Sibit::Cryptoapis
|
|
|
27
27
|
|
|
28
28
|
# Current price of BTC in USD (float returned).
|
|
29
29
|
def price(_currency = 'USD')
|
|
30
|
-
raise
|
|
30
|
+
raise(Sibit::NotSupportedError, 'Cryptoapis doesn\'t provide BTC price')
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
# Get hash of the block after this one.
|
|
@@ -43,29 +43,31 @@ class Sibit::Cryptoapis
|
|
|
43
43
|
|
|
44
44
|
# The height of the block.
|
|
45
45
|
def height(hash)
|
|
46
|
-
|
|
46
|
+
h = Sibit::Json.new(http: @http, log: @log).get(
|
|
47
47
|
Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/blocks').append(hash),
|
|
48
48
|
headers: headers
|
|
49
|
-
)['payload']
|
|
50
|
-
h = json['height']
|
|
49
|
+
)['payload']['height']
|
|
51
50
|
@log.debug("The height of #{hash} is #{h}")
|
|
52
51
|
h
|
|
53
52
|
end
|
|
54
53
|
|
|
55
54
|
# Gets the balance of the address, in satoshi.
|
|
56
55
|
def balance(address)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
b = Integer(
|
|
57
|
+
Float(
|
|
58
|
+
Sibit::Json.new(http: @http, log: @log).get(
|
|
59
|
+
Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/address').append(address),
|
|
60
|
+
headers: headers
|
|
61
|
+
)['payload']['balance']
|
|
62
|
+
) * 100_000_000
|
|
63
|
+
)
|
|
62
64
|
@log.debug("The balance of #{address} is #{b} satoshi")
|
|
63
65
|
b
|
|
64
66
|
end
|
|
65
67
|
|
|
66
68
|
# Get recommended fees, in satoshi per byte.
|
|
67
69
|
def fees
|
|
68
|
-
raise
|
|
70
|
+
raise(Sibit::NotSupportedError, 'Cryptoapis doesn\'t provide recommended fees')
|
|
69
71
|
end
|
|
70
72
|
|
|
71
73
|
# Gets the hash of the latest block.
|
|
@@ -80,13 +82,13 @@ class Sibit::Cryptoapis
|
|
|
80
82
|
|
|
81
83
|
# Fetch all unspent outputs per address.
|
|
82
84
|
def utxos(_sources)
|
|
83
|
-
raise
|
|
85
|
+
raise(Sibit::NotSupportedError, 'Not implemented yet')
|
|
84
86
|
end
|
|
85
87
|
|
|
86
88
|
# Push this transaction (in hex format) to the network.
|
|
87
89
|
def push(hex)
|
|
88
90
|
Sibit::Json.new(http: @http, log: @log).post(
|
|
89
|
-
Iri.new('https://api.cryptoapis.io/v1/bc/btc/
|
|
91
|
+
Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/txs/send'),
|
|
90
92
|
JSON.pretty_generate(hex: hex),
|
|
91
93
|
headers: headers
|
|
92
94
|
)
|
|
@@ -122,21 +124,22 @@ class Sibit::Cryptoapis
|
|
|
122
124
|
limit = 200
|
|
123
125
|
all = []
|
|
124
126
|
loop do
|
|
125
|
-
txns =
|
|
126
|
-
|
|
127
|
-
.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
127
|
+
txns =
|
|
128
|
+
Sibit::Json.new(http: @http, log: @log).get(
|
|
129
|
+
Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/txs/block/')
|
|
130
|
+
.append(hash).add(index: index, limit: limit),
|
|
131
|
+
headers: headers
|
|
132
|
+
)['payload'].map do |t|
|
|
133
|
+
{
|
|
134
|
+
hash: t['hash'],
|
|
135
|
+
outputs: t['txouts'].map do |o|
|
|
136
|
+
{
|
|
137
|
+
address: o['addresses'][0],
|
|
138
|
+
value: Float(o['amount'] || 0) * 100_000_000
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
}
|
|
142
|
+
end
|
|
140
143
|
all += txns
|
|
141
144
|
index += txns.length
|
|
142
145
|
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
|
@@ -43,16 +43,16 @@ class Sibit::Json
|
|
|
43
43
|
'Accept-Encoding' => ''
|
|
44
44
|
}.merge(headers)
|
|
45
45
|
)
|
|
46
|
-
unless accept.include?(res.code
|
|
47
|
-
raise
|
|
46
|
+
unless accept.include?(Integer(res.code, 10))
|
|
47
|
+
raise(Sibit::Error, "Failed to retrieve #{uri} (#{res.code}): #{res.body}")
|
|
48
48
|
end
|
|
49
49
|
ret =
|
|
50
50
|
begin
|
|
51
51
|
JSON.parse(res.body)
|
|
52
52
|
rescue JSON::ParserError => e
|
|
53
|
-
raise
|
|
53
|
+
raise(Sibit::Error, "Can't parse JSON: #{e.message}")
|
|
54
54
|
end
|
|
55
|
-
throw
|
|
55
|
+
throw(:"GET #{uri}: #{res.code}/#{length(res.body.length)}")
|
|
56
56
|
end
|
|
57
57
|
ret
|
|
58
58
|
end
|
|
@@ -61,7 +61,7 @@ class Sibit::Json
|
|
|
61
61
|
uri = URI(address.to_s)
|
|
62
62
|
elapsed(@log) do
|
|
63
63
|
res = @http.client(uri).post(
|
|
64
|
-
"#{uri.path}?#{uri.query}",
|
|
64
|
+
"#{uri.path.empty? ? '/' : uri.path}#{"?#{uri.query}" if uri.query}",
|
|
65
65
|
"tx=#{CGI.escape(body)}",
|
|
66
66
|
{
|
|
67
67
|
'Accept' => 'text/plain',
|
|
@@ -72,9 +72,9 @@ class Sibit::Json
|
|
|
72
72
|
}.merge(headers)
|
|
73
73
|
)
|
|
74
74
|
unless res.code == '200'
|
|
75
|
-
raise
|
|
75
|
+
raise(Sibit::Error, "Failed to post tx to #{uri}: #{res.code}\n#{res.body}")
|
|
76
76
|
end
|
|
77
|
-
throw
|
|
77
|
+
throw(:"POST #{uri}: #{res.code}")
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
|
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
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
require 'iri'
|
|
7
|
+
require 'json'
|
|
8
|
+
require 'loog'
|
|
9
|
+
require 'uri'
|
|
10
|
+
require_relative 'error'
|
|
11
|
+
require_relative 'http'
|
|
12
|
+
require_relative 'json'
|
|
13
|
+
require_relative 'version'
|
|
14
|
+
|
|
15
|
+
# SoChain API (formerly known as Block.io).
|
|
16
|
+
#
|
|
17
|
+
# Documentation: https://sochain.com/api
|
|
18
|
+
#
|
|
19
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
|
20
|
+
# Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
|
|
21
|
+
# License:: MIT
|
|
22
|
+
class Sibit::Sochain
|
|
23
|
+
# Constructor.
|
|
24
|
+
def initialize(log: Loog::NULL, http: Sibit::Http.new, network: 'BTC')
|
|
25
|
+
@http = http
|
|
26
|
+
@log = log
|
|
27
|
+
@network = network
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Current price of BTC in USD (float returned).
|
|
31
|
+
def price(currency = 'USD')
|
|
32
|
+
data = Sibit::Json.new(http: @http, log: @log).get(
|
|
33
|
+
Iri.new('https://sochain.com/api/v2/get_price').append(@network).append(currency)
|
|
34
|
+
)['data']
|
|
35
|
+
raise(Sibit::Error, "No price data returned for #{@network}/#{currency}") if data.nil?
|
|
36
|
+
prices = data['prices']
|
|
37
|
+
if prices.nil? || prices.empty?
|
|
38
|
+
raise(Sibit::Error, "No price quotes for #{@network}/#{currency}")
|
|
39
|
+
end
|
|
40
|
+
price = Float(prices[0]['price'])
|
|
41
|
+
@log.debug("The price of #{@network} is #{price} #{currency}")
|
|
42
|
+
price
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The height of the block, identified by hash.
|
|
46
|
+
def height(hash)
|
|
47
|
+
data = Sibit::Json.new(http: @http, log: @log).get(
|
|
48
|
+
Iri.new('https://sochain.com/api/v2/block').append(@network).append(hash)
|
|
49
|
+
)['data']
|
|
50
|
+
raise(Sibit::Error, "The block #{hash} not found") if data.nil?
|
|
51
|
+
h = data['block_no']
|
|
52
|
+
raise(Sibit::Error, "The block #{hash} found but the height is absent") if h.nil?
|
|
53
|
+
@log.debug("The height of #{hash} is #{h}")
|
|
54
|
+
Integer(h)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Get hash of the block after this one.
|
|
58
|
+
def next_of(hash)
|
|
59
|
+
data = Sibit::Json.new(http: @http, log: @log).get(
|
|
60
|
+
Iri.new('https://sochain.com/api/v2/block').append(@network).append(hash)
|
|
61
|
+
)['data']
|
|
62
|
+
raise(Sibit::Error, "The block #{hash} not found") if data.nil?
|
|
63
|
+
nxt = data['next_blockhash']
|
|
64
|
+
nxt = nil if nxt == '0000000000000000000000000000000000000000000000000000000000000000'
|
|
65
|
+
@log.debug("In SoChain the block #{hash} is the latest, there is no next block") if nxt.nil?
|
|
66
|
+
@log.debug("The next block of #{hash} is #{nxt}") unless nxt.nil?
|
|
67
|
+
nxt
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Gets the balance of the address, in satoshi.
|
|
71
|
+
def balance(address)
|
|
72
|
+
data = Sibit::Json.new(http: @http, log: @log).get(
|
|
73
|
+
Iri.new('https://sochain.com/api/v2/get_address_balance').append(@network).append(address)
|
|
74
|
+
)['data']
|
|
75
|
+
if data.nil?
|
|
76
|
+
@log.debug("The balance of #{address} is probably zero (not found)")
|
|
77
|
+
return 0
|
|
78
|
+
end
|
|
79
|
+
confirmed = data['confirmed_balance']
|
|
80
|
+
if confirmed.nil?
|
|
81
|
+
@log.debug("The balance of #{address} is probably zero (no confirmed_balance)")
|
|
82
|
+
return 0
|
|
83
|
+
end
|
|
84
|
+
b = Integer((Float(confirmed) * 100_000_000).round)
|
|
85
|
+
@log.debug("The balance of #{address} is #{b} satoshi")
|
|
86
|
+
b
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Get recommended fees, in satoshi per byte.
|
|
90
|
+
def fees
|
|
91
|
+
raise(Sibit::NotSupportedError, 'SoChain doesn\'t provide recommended fees')
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Gets the hash of the latest block.
|
|
95
|
+
def latest
|
|
96
|
+
data = Sibit::Json.new(http: @http, log: @log).get(
|
|
97
|
+
Iri.new('https://sochain.com/api/v2/get_info').append(@network)
|
|
98
|
+
)['data']
|
|
99
|
+
raise(Sibit::Error, 'The latest block info not found') if data.nil?
|
|
100
|
+
hash = data['blockhash']
|
|
101
|
+
raise(Sibit::Error, 'The latest block hash is absent') if hash.nil?
|
|
102
|
+
@log.debug("The latest block hash is #{hash}")
|
|
103
|
+
hash
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Fetch all unspent outputs per address. The argument is an array
|
|
107
|
+
# of Bitcoin addresses.
|
|
108
|
+
def utxos(sources)
|
|
109
|
+
out = []
|
|
110
|
+
sources.each do |address|
|
|
111
|
+
data = Sibit::Json.new(http: @http, log: @log).get(
|
|
112
|
+
Iri.new('https://sochain.com/api/v2/get_tx_unspent').append(@network).append(address)
|
|
113
|
+
)['data']
|
|
114
|
+
next if data.nil?
|
|
115
|
+
txs = data['txs']
|
|
116
|
+
next if txs.nil?
|
|
117
|
+
txs.each do |u|
|
|
118
|
+
out << {
|
|
119
|
+
value: Integer((Float(u['value']) * 100_000_000).round),
|
|
120
|
+
hash: u['txid'],
|
|
121
|
+
index: u['output_no'],
|
|
122
|
+
confirmations: u['confirmations'],
|
|
123
|
+
script: [u['script_hex']].pack('H*')
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
out
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Push this transaction (in hex format) to the network. SoChain expects a
|
|
131
|
+
# JSON payload with the +tx_hex+ field on POST /send_tx/{network}.
|
|
132
|
+
def push(hex)
|
|
133
|
+
uri = URI(Iri.new('https://sochain.com/api/v2/send_tx').append(@network).to_s)
|
|
134
|
+
res = @http.client(uri).post(
|
|
135
|
+
uri.path,
|
|
136
|
+
JSON.generate(tx_hex: hex),
|
|
137
|
+
{
|
|
138
|
+
'Accept' => 'application/json',
|
|
139
|
+
'Content-Type' => 'application/json',
|
|
140
|
+
'Accept-Charset' => 'UTF-8',
|
|
141
|
+
'Accept-Encoding' => ''
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
unless res.code == '200'
|
|
145
|
+
raise(Sibit::Error, "Failed to push transaction to #{uri}: #{res.code}\n#{res.body}")
|
|
146
|
+
end
|
|
147
|
+
@log.debug("Transaction (#{hex.length} chars in hex) has been pushed to SoChain")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# This method should fetch a block and return it as a hash.
|
|
151
|
+
def block(hash)
|
|
152
|
+
data = Sibit::Json.new(http: @http, log: @log).get(
|
|
153
|
+
Iri.new('https://sochain.com/api/v2/block').append(@network).append(hash)
|
|
154
|
+
)['data']
|
|
155
|
+
raise(Sibit::Error, "The block #{hash} not found") if data.nil?
|
|
156
|
+
nxt = data['next_blockhash']
|
|
157
|
+
nxt = nil if nxt == '0000000000000000000000000000000000000000000000000000000000000000'
|
|
158
|
+
{
|
|
159
|
+
provider: self.class.name,
|
|
160
|
+
hash: data['blockhash'],
|
|
161
|
+
orphan: data['is_orphan'] == true,
|
|
162
|
+
next: nxt,
|
|
163
|
+
previous: data['previous_blockhash'],
|
|
164
|
+
txns: (data['txs'] || []).map do |t|
|
|
165
|
+
{
|
|
166
|
+
hash: t['txid'] || t,
|
|
167
|
+
outputs: ((t.is_a?(Hash) ? t['outputs'] : nil) || []).map do |o|
|
|
168
|
+
{
|
|
169
|
+
address: o['address'],
|
|
170
|
+
value: Integer((Float(o['value']) * 100_000_000).round)
|
|
171
|
+
}
|
|
172
|
+
end
|
|
173
|
+
}
|
|
174
|
+
end
|
|
175
|
+
}
|
|
176
|
+
end
|
|
177
|
+
end
|
data/lib/sibit/tx.rb
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
require 'digest'
|
|
7
7
|
require_relative 'base58'
|
|
8
8
|
require_relative 'bech32'
|
|
9
|
+
require_relative 'error'
|
|
9
10
|
require_relative 'key'
|
|
10
11
|
require_relative 'script'
|
|
11
12
|
|
|
@@ -105,7 +106,9 @@ class Sibit
|
|
|
105
106
|
|
|
106
107
|
def script
|
|
107
108
|
return segwit_script if segwit?
|
|
108
|
-
p2pkh_script
|
|
109
|
+
return p2pkh_script if %w[00 6f].include?(version)
|
|
110
|
+
return p2sh_script if %w[05 c4].include?(version)
|
|
111
|
+
raise(Sibit::Error, "Address '#{@address}' has an unsupported version byte 0x#{version}")
|
|
109
112
|
end
|
|
110
113
|
|
|
111
114
|
def segwit?
|
|
@@ -118,17 +121,25 @@ class Sibit
|
|
|
118
121
|
|
|
119
122
|
private
|
|
120
123
|
|
|
124
|
+
def version
|
|
125
|
+
Base58.new(@address).decode[0, 2]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def hash160
|
|
129
|
+
[Base58.new(@address).decode[2, 40]].pack('H*')
|
|
130
|
+
end
|
|
131
|
+
|
|
121
132
|
def p2pkh_script
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
133
|
+
[0x76, 0xa9, 0x14].pack('C*') + hash160 + [0x88, 0xac].pack('C*')
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def p2sh_script
|
|
137
|
+
[0xa9, 0x14].pack('C*') + hash160 + [0x87].pack('C*')
|
|
125
138
|
end
|
|
126
139
|
|
|
127
140
|
def segwit_script
|
|
128
|
-
|
|
129
|
-
witness
|
|
130
|
-
len = witness.length / 2
|
|
131
|
-
[0x00, len].pack('C*') + [witness].pack('H*')
|
|
141
|
+
witness = Bech32.new(@address).witness
|
|
142
|
+
[0x00, (witness.length / 2)].pack('C*') + [witness].pack('H*')
|
|
132
143
|
end
|
|
133
144
|
end
|
|
134
145
|
|
|
@@ -140,12 +151,10 @@ class Sibit
|
|
|
140
151
|
|
|
141
152
|
def sign_inputs
|
|
142
153
|
@inputs.each_with_index do |input, idx|
|
|
143
|
-
|
|
144
|
-
sig = sign(input.key, sighash)
|
|
154
|
+
sig = sign(input.key, (input.segwit? ? segwit_sighash(idx) : legacy_sighash(idx)))
|
|
145
155
|
pubkey = [input.key.pub].pack('H*')
|
|
146
156
|
if input.segwit?
|
|
147
|
-
|
|
148
|
-
input.witness = [witness_sig.bytes, pubkey.bytes]
|
|
157
|
+
input.witness = [(sig + [SIGHASH_ALL].pack('C')).bytes, pubkey.bytes]
|
|
149
158
|
else
|
|
150
159
|
input.script_sig = der_sig(sig) + pubkey_script(pubkey)
|
|
151
160
|
end
|
|
@@ -153,9 +162,9 @@ class Sibit
|
|
|
153
162
|
end
|
|
154
163
|
|
|
155
164
|
def legacy_sighash(idx)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
165
|
+
Digest::SHA256.digest(
|
|
166
|
+
Digest::SHA256.digest(serialize_for_signing(idx) + [SIGHASH_ALL].pack('V'))
|
|
167
|
+
)
|
|
159
168
|
end
|
|
160
169
|
|
|
161
170
|
def segwit_sighash(idx)
|
|
@@ -175,48 +184,55 @@ class Sibit
|
|
|
175
184
|
end
|
|
176
185
|
|
|
177
186
|
def hash_prevouts
|
|
178
|
-
|
|
179
|
-
|
|
187
|
+
Digest::SHA256.digest(
|
|
188
|
+
Digest::SHA256.digest(
|
|
189
|
+
@inputs.map do |i|
|
|
190
|
+
[i.hash].pack('H*').reverse + [i.index].pack('V')
|
|
191
|
+
end.join
|
|
192
|
+
)
|
|
193
|
+
)
|
|
180
194
|
end
|
|
181
195
|
|
|
182
196
|
def hash_sequence
|
|
183
|
-
|
|
184
|
-
Digest::SHA256.digest(Digest::SHA256.digest(data))
|
|
197
|
+
Digest::SHA256.digest(Digest::SHA256.digest(@inputs.map { [SEQUENCE].pack('V') }.join))
|
|
185
198
|
end
|
|
186
199
|
|
|
187
200
|
def hash_outputs
|
|
188
|
-
|
|
189
|
-
|
|
201
|
+
Digest::SHA256.digest(
|
|
202
|
+
Digest::SHA256.digest(
|
|
203
|
+
@outputs.map do |o|
|
|
204
|
+
[o.value].pack('Q<') + varint(o.script.length) + o.script
|
|
205
|
+
end.join
|
|
206
|
+
)
|
|
207
|
+
)
|
|
190
208
|
end
|
|
191
209
|
|
|
192
210
|
def script_code(input)
|
|
193
|
-
|
|
194
|
-
|
|
211
|
+
code = [
|
|
212
|
+
0x76, 0xa9,
|
|
213
|
+
0x14
|
|
214
|
+
].pack('C*') + [input.prev_script[4..]].pack('H*') + [0x88, 0xac].pack('C*')
|
|
195
215
|
varint(code.length) + code
|
|
196
216
|
end
|
|
197
217
|
|
|
198
218
|
def sign(key, hash)
|
|
199
|
-
|
|
200
|
-
repack(der)
|
|
219
|
+
repack(key.sign(hash))
|
|
201
220
|
end
|
|
202
221
|
|
|
203
222
|
def repack(der)
|
|
204
223
|
return der if low_s?(der)
|
|
205
224
|
seq = OpenSSL::ASN1.decode(der)
|
|
206
|
-
|
|
207
|
-
s = seq.value[1].value.to_i
|
|
225
|
+
s = Integer(seq.value[1].value)
|
|
208
226
|
order = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
|
209
227
|
s = order - s if s > order / 2
|
|
210
228
|
OpenSSL::ASN1::Sequence.new(
|
|
211
|
-
[OpenSSL::ASN1::Integer.new(
|
|
229
|
+
[OpenSSL::ASN1::Integer.new(Integer(seq.value[0].value)), OpenSSL::ASN1::Integer.new(s)]
|
|
212
230
|
).to_der
|
|
213
231
|
end
|
|
214
232
|
|
|
215
233
|
def low_s?(der)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
order = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
|
219
|
-
s <= order / 2
|
|
234
|
+
Integer(OpenSSL::ASN1.decode(der).value[1].value) <=
|
|
235
|
+
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 / 2
|
|
220
236
|
end
|
|
221
237
|
|
|
222
238
|
def der_sig(sig)
|