sibit 0.34.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.lock +10 -10
- data/README.md +5 -0
- data/cross/centos.df +2 -7
- data/cross/opensuse.df +9 -1
- data/cross/rocky.df +2 -7
- data/lib/sibit/bech32.rb +17 -4
- data/lib/sibit/bestof.rb +34 -7
- data/lib/sibit/bin.rb +4 -2
- data/lib/sibit/bitcoinchain.rb +3 -3
- data/lib/sibit/blockchain.rb +9 -5
- data/lib/sibit/btc.rb +13 -16
- data/lib/sibit/coins.rb +35 -0
- data/lib/sibit/cryptoapis.rb +14 -12
- data/lib/sibit/json.rb +1 -2
- data/lib/sibit/sochain.rb +7 -4
- data/lib/sibit/tx.rb +32 -9
- data/lib/sibit/txbuilder.rb +12 -0
- data/lib/sibit/version.rb +1 -1
- data/lib/sibit.rb +11 -6
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 469905d53f542975d0fe59c8b429289423762f4b36aca8b703928d54f5ba394f
|
|
4
|
+
data.tar.gz: c68a4237d70f25a11ea2e98c36d76c7b0917a6355353e6d23c22336eedd94f31
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f5aa717192c0417c572226c3a0f79924f61e5d70b0bc84efe53d0cc760c16515f3cd6ef63b00de57e4228dbcd82fdb60a269f35df858b0c1ff8bec81006db6bc
|
|
7
|
+
data.tar.gz: fe9f402e207051f02e31b54d0a4e41cca76efd2a0260fbc6dfb3fb69f1aa543af2a9af6045590bd653c6c26472c97fdb6d5ba3a8b5a98f4ca282e351d88f53ee
|
data/Gemfile.lock
CHANGED
|
@@ -72,7 +72,7 @@ GEM
|
|
|
72
72
|
loog (~> 0.6)
|
|
73
73
|
tago (~> 0.1)
|
|
74
74
|
ellipsized (0.3.0)
|
|
75
|
-
erb (6.0.
|
|
75
|
+
erb (6.0.6)
|
|
76
76
|
ffi (1.17.4-arm64-darwin)
|
|
77
77
|
ffi (1.17.4-x86_64-linux-gnu)
|
|
78
78
|
hashdiff (1.2.1)
|
|
@@ -83,8 +83,8 @@ GEM
|
|
|
83
83
|
rdoc (>= 4.0.0)
|
|
84
84
|
reline (>= 0.4.2)
|
|
85
85
|
iri (0.11.8)
|
|
86
|
-
json (2.
|
|
87
|
-
language_server-protocol (3.17.0.
|
|
86
|
+
json (2.21.1)
|
|
87
|
+
language_server-protocol (3.17.0.6)
|
|
88
88
|
lint_roller (1.1.0)
|
|
89
89
|
logger (1.7.0)
|
|
90
90
|
loog (0.8.0)
|
|
@@ -108,10 +108,10 @@ GEM
|
|
|
108
108
|
openssl (3.3.3)
|
|
109
109
|
os (1.1.4)
|
|
110
110
|
parallel (2.1.0)
|
|
111
|
-
parser (3.3.
|
|
111
|
+
parser (3.3.12.0)
|
|
112
112
|
ast (~> 2.4.1)
|
|
113
113
|
racc
|
|
114
|
-
pp (0.6.
|
|
114
|
+
pp (0.6.4)
|
|
115
115
|
prettyprint
|
|
116
116
|
prettyprint (0.2.0)
|
|
117
117
|
prism (1.9.0)
|
|
@@ -144,7 +144,7 @@ GEM
|
|
|
144
144
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
145
145
|
rspec-support (~> 3.13.0)
|
|
146
146
|
rspec-support (3.13.7)
|
|
147
|
-
rubocop (1.88.
|
|
147
|
+
rubocop (1.88.2)
|
|
148
148
|
json (~> 2.3)
|
|
149
149
|
language_server-protocol (~> 3.17.0.2)
|
|
150
150
|
lint_roller (~> 1.1.0)
|
|
@@ -155,13 +155,13 @@ GEM
|
|
|
155
155
|
rubocop-ast (>= 1.49.0, < 2.0)
|
|
156
156
|
ruby-progressbar (~> 1.7)
|
|
157
157
|
unicode-display_width (>= 2.4.0, < 4.0)
|
|
158
|
-
rubocop-ast (1.
|
|
158
|
+
rubocop-ast (1.50.0)
|
|
159
159
|
parser (>= 3.3.7.2)
|
|
160
160
|
prism (~> 1.7)
|
|
161
|
-
rubocop-elegant (0.
|
|
161
|
+
rubocop-elegant (0.7.1)
|
|
162
162
|
lint_roller (~> 1.1)
|
|
163
163
|
rubocop (~> 1.75)
|
|
164
|
-
rubocop-minitest (0.
|
|
164
|
+
rubocop-minitest (0.40.0)
|
|
165
165
|
lint_roller (~> 1.1)
|
|
166
166
|
rubocop (>= 1.75.0, < 2.0)
|
|
167
167
|
rubocop-ast (>= 1.38.0, < 2.0)
|
|
@@ -177,7 +177,7 @@ GEM
|
|
|
177
177
|
docile (~> 1.1)
|
|
178
178
|
simplecov-html (~> 0.11)
|
|
179
179
|
simplecov_json_formatter (~> 0.1)
|
|
180
|
-
simplecov-cobertura (3.
|
|
180
|
+
simplecov-cobertura (3.2.0)
|
|
181
181
|
rexml
|
|
182
182
|
simplecov (~> 0.19)
|
|
183
183
|
simplecov-html (0.13.2)
|
data/README.md
CHANGED
|
@@ -115,6 +115,11 @@ The proxy address may include authentication credentials:
|
|
|
115
115
|
sibit --proxy=user:password@host:port balance 1PfsYNygsuVL8fvBarJNQnHytkg4rGih1U
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
+
Some providers accept an API key, read from an environment variable:
|
|
119
|
+
`SIBIT_BLOCKCHAIR_KEY` for [Blockchair.com] and
|
|
120
|
+
`SIBIT_CRYPTOAPIS_KEY` for [Cryptoapis.io].
|
|
121
|
+
When the variable is set, requests to that provider use the authenticated tier.
|
|
122
|
+
|
|
118
123
|
All operations are performed through the [Blockchain API].
|
|
119
124
|
Transactions are pushed to the Bitcoin network via [this relay].
|
|
120
125
|
|
data/cross/centos.df
CHANGED
|
@@ -3,11 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
FROM quay.io/centos/centos:stream9
|
|
5
5
|
|
|
6
|
-
RUN dnf
|
|
7
|
-
dnf
|
|
8
|
-
dnf install -y gcc make openssl-devel libyaml-devel libffi-devel readline-devel zlib-devel tar && \
|
|
9
|
-
curl -fsSL https://github.com/rbenv/ruby-build/archive/refs/heads/master.tar.gz | tar -xz && \
|
|
10
|
-
PREFIX=/usr/local ./ruby-build-master/install.sh && \
|
|
11
|
-
ruby-build 3.2.6 /usr/local && \
|
|
12
|
-
rm -rf ruby-build-master && \
|
|
6
|
+
RUN dnf module enable -y ruby:3.3 && \
|
|
7
|
+
dnf install -y ruby ruby-devel gcc make openssl-devel redhat-rpm-config && \
|
|
13
8
|
dnf clean all
|
data/cross/opensuse.df
CHANGED
|
@@ -3,7 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
FROM opensuse/tumbleweed
|
|
5
5
|
|
|
6
|
-
RUN
|
|
6
|
+
RUN ok=; \
|
|
7
|
+
for i in 1 2 3 4 5; do \
|
|
8
|
+
if zypper --non-interactive --gpg-auto-import-keys refresh && \
|
|
9
|
+
zypper --non-interactive install ruby ruby-devel gcc make libopenssl-devel bash; then \
|
|
10
|
+
ok=1; break; \
|
|
11
|
+
fi; \
|
|
12
|
+
echo "zypper attempt $i failed, retrying in 15s"; sleep 15; \
|
|
13
|
+
done; \
|
|
14
|
+
test -n "$ok" && \
|
|
7
15
|
zypper clean --all && \
|
|
8
16
|
gem update --system --no-document && \
|
|
9
17
|
sed -i '/format.executable/d' /etc/gemrc
|
data/cross/rocky.df
CHANGED
|
@@ -3,11 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
FROM rockylinux:9
|
|
5
5
|
|
|
6
|
-
RUN dnf
|
|
7
|
-
dnf
|
|
8
|
-
dnf install -y gcc make openssl-devel libyaml-devel libffi-devel readline-devel zlib-devel tar && \
|
|
9
|
-
curl -fsSL https://github.com/rbenv/ruby-build/archive/refs/heads/master.tar.gz | tar -xz && \
|
|
10
|
-
PREFIX=/usr/local ./ruby-build-master/install.sh && \
|
|
11
|
-
ruby-build 3.2.6 /usr/local && \
|
|
12
|
-
rm -rf ruby-build-master && \
|
|
6
|
+
RUN dnf module enable -y ruby:3.3 && \
|
|
7
|
+
dnf install -y ruby ruby-devel gcc make openssl-devel redhat-rpm-config && \
|
|
13
8
|
dnf clean all
|
data/lib/sibit/bech32.rb
CHANGED
|
@@ -66,8 +66,19 @@ class Sibit
|
|
|
66
66
|
|
|
67
67
|
def witness
|
|
68
68
|
hrp, data = parse
|
|
69
|
+
ver = data[0]
|
|
70
|
+
unless ver.between?(0, 16)
|
|
71
|
+
raise(Sibit::Error, "Unsupported witness version #{ver} in '#{@addr}'")
|
|
72
|
+
end
|
|
69
73
|
raise(Sibit::Error, "Invalid Bech32 checksum in '#{@addr}'") unless verified?(hrp, data)
|
|
70
|
-
convert(data[1..-7], 5, 8
|
|
74
|
+
prog = convert(data[1..-7], 5, 8)
|
|
75
|
+
if ver.zero? && [20, 32].none?(prog.length)
|
|
76
|
+
raise(Sibit::Error, "Witness v0 program in '#{@addr}' must be 20 or 32 bytes")
|
|
77
|
+
end
|
|
78
|
+
unless prog.length.between?(2, 40)
|
|
79
|
+
raise(Sibit::Error, "Witness program in '#{@addr}' must be 2 to 40 bytes")
|
|
80
|
+
end
|
|
81
|
+
prog.pack('C*').unpack1('H*')
|
|
71
82
|
end
|
|
72
83
|
|
|
73
84
|
def version
|
|
@@ -88,7 +99,7 @@ class Sibit
|
|
|
88
99
|
end
|
|
89
100
|
|
|
90
101
|
def verified?(hrp, data)
|
|
91
|
-
|
|
102
|
+
polymod(expand(hrp) + data) == (data[0].zero? ? 1 : 0x2bc830a3)
|
|
92
103
|
end
|
|
93
104
|
|
|
94
105
|
def expand(hrp)
|
|
@@ -105,7 +116,7 @@ class Sibit
|
|
|
105
116
|
chk
|
|
106
117
|
end
|
|
107
118
|
|
|
108
|
-
def convert(data, frombits, tobits
|
|
119
|
+
def convert(data, frombits, tobits)
|
|
109
120
|
acc = 0
|
|
110
121
|
bits = 0
|
|
111
122
|
result = []
|
|
@@ -118,7 +129,9 @@ class Sibit
|
|
|
118
129
|
result << ((acc >> bits) & maxv)
|
|
119
130
|
end
|
|
120
131
|
end
|
|
121
|
-
|
|
132
|
+
if bits >= frombits || (acc << (tobits - bits)).anybits?(maxv)
|
|
133
|
+
raise(Sibit::Error, "Non-zero padding in Bech32 address '#{@addr}'")
|
|
134
|
+
end
|
|
122
135
|
result
|
|
123
136
|
end
|
|
124
137
|
end
|
data/lib/sibit/bestof.rb
CHANGED
|
@@ -66,11 +66,26 @@ class Sibit::BestOf
|
|
|
66
66
|
best_of('latest', &:latest)
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
-
# Push this transaction (in hex format) to the network.
|
|
69
|
+
# Push this transaction (in hex format) to the network. This one does
|
|
70
|
+
# not vote: pushing is a side effect, so it hands the transaction to
|
|
71
|
+
# each API in turn and stops at the first that accepts it.
|
|
70
72
|
def push(hex)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
return @list.push(hex) unless @list.is_a?(Array)
|
|
74
|
+
errors = []
|
|
75
|
+
@list.each do |api|
|
|
76
|
+
return api.push(hex)
|
|
77
|
+
rescue Sibit::NotSupportedError
|
|
78
|
+
nil
|
|
79
|
+
rescue Sibit::Error => e
|
|
80
|
+
errors << e
|
|
81
|
+
@log.debug("The API #{api.class.name} failed at push(): #{e.message}") if @verbose
|
|
73
82
|
end
|
|
83
|
+
errors.each { |e| @log.debug(Backtrace.new(e).to_s) }
|
|
84
|
+
raise(
|
|
85
|
+
Sibit::Error,
|
|
86
|
+
"No APIs out of #{@list.length} managed to succeed at push(): " \
|
|
87
|
+
"#{@list.map { |a| a.class.name }.join(', ')}"
|
|
88
|
+
)
|
|
74
89
|
end
|
|
75
90
|
|
|
76
91
|
# This method should fetch a block and return as a hash.
|
|
@@ -84,17 +99,17 @@ class Sibit::BestOf
|
|
|
84
99
|
|
|
85
100
|
def best_of(method)
|
|
86
101
|
return yield(@list) unless @list.is_a?(Array)
|
|
87
|
-
|
|
102
|
+
votes = []
|
|
88
103
|
errors = []
|
|
89
104
|
@list.each do |api|
|
|
90
|
-
|
|
105
|
+
votes << [api.class.name, yield(api)]
|
|
91
106
|
rescue Sibit::NotSupportedError
|
|
92
107
|
nil
|
|
93
108
|
rescue Sibit::Error => e
|
|
94
109
|
errors << e
|
|
95
110
|
@log.debug("The API #{api.class.name} failed at #{method}(): #{e.message}") if @verbose
|
|
96
111
|
end
|
|
97
|
-
if
|
|
112
|
+
if votes.empty?
|
|
98
113
|
errors.each { |e| @log.debug(Backtrace.new(e).to_s) }
|
|
99
114
|
raise(
|
|
100
115
|
Sibit::Error,
|
|
@@ -102,6 +117,18 @@ class Sibit::BestOf
|
|
|
102
117
|
"#{@list.map { |a| a.class.name }.join(', ')}"
|
|
103
118
|
)
|
|
104
119
|
end
|
|
105
|
-
|
|
120
|
+
majority(method, votes)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def majority(method, votes)
|
|
124
|
+
winner = votes.group_by { |v| v[1].to_s }.values.max_by(&:size)
|
|
125
|
+
if winner.size <= votes.size / 2
|
|
126
|
+
raise(
|
|
127
|
+
Sibit::Error,
|
|
128
|
+
"No strict majority among #{votes.size} answers at #{method}(): " \
|
|
129
|
+
"#{votes.map { |name, answer| "#{name}=#{answer}" }.join(', ')}"
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
winner[0][1]
|
|
106
133
|
end
|
|
107
134
|
end
|
data/lib/sibit/bin.rb
CHANGED
|
@@ -101,7 +101,8 @@ class Sibit
|
|
|
101
101
|
desc: 'Minimum number of confirmations required on a UTXO before it can be spent'
|
|
102
102
|
def pay(amount, fee, sources, target, change)
|
|
103
103
|
keys = sources.split(',')
|
|
104
|
-
|
|
104
|
+
sweep = amount.upcase == 'MAX'
|
|
105
|
+
if sweep
|
|
105
106
|
addrs =
|
|
106
107
|
keys.map do |k|
|
|
107
108
|
kk = Sibit::Key.new(k)
|
|
@@ -111,6 +112,7 @@ class Sibit
|
|
|
111
112
|
end
|
|
112
113
|
amount = Integer(amount, 10) if amount.is_a?(String) && /^[0-9]+$/.match?(amount)
|
|
113
114
|
fee = Integer(fee, 10) if /^[0-9]+$/.match?(fee)
|
|
115
|
+
amount -= fee if sweep && fee.is_a?(Integer)
|
|
114
116
|
args = [amount, fee, keys, target, change]
|
|
115
117
|
kwargs = {
|
|
116
118
|
skip_utxo: options[:skip_utxo], base58: options[:base58],
|
|
@@ -157,7 +159,7 @@ class Sibit
|
|
|
157
159
|
when 'bitcoinchain'
|
|
158
160
|
Sibit::Bitcoinchain.new(http: http, log: log)
|
|
159
161
|
when 'blockchair'
|
|
160
|
-
Sibit::Blockchair.new(http: http, log: log)
|
|
162
|
+
Sibit::Blockchair.new(key: ENV.fetch('SIBIT_BLOCKCHAIR_KEY', nil), http: http, log: log)
|
|
161
163
|
when 'cex'
|
|
162
164
|
Sibit::Cex.new(http: http, log: log)
|
|
163
165
|
when 'cryptoapis'
|
data/lib/sibit/bitcoinchain.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'
|
|
@@ -58,8 +59,7 @@ class Sibit::Bitcoinchain
|
|
|
58
59
|
@log.debug("The balance of #{address} is not visible")
|
|
59
60
|
return 0
|
|
60
61
|
end
|
|
61
|
-
b
|
|
62
|
-
b = Integer(b)
|
|
62
|
+
b = Sibit::Coins.new(b).satoshi
|
|
63
63
|
@log.debug("The balance of #{address} is #{b} satoshi (#{json['transactions']} txns)")
|
|
64
64
|
b
|
|
65
65
|
end
|
|
@@ -112,7 +112,7 @@ class Sibit::Bitcoinchain
|
|
|
112
112
|
outputs: t['outputs'].map do |o|
|
|
113
113
|
{
|
|
114
114
|
address: o['receiver'],
|
|
115
|
-
value: o['value']
|
|
115
|
+
value: Sibit::Coins.new(o['value']).satoshi
|
|
116
116
|
}
|
|
117
117
|
end
|
|
118
118
|
}
|
data/lib/sibit/blockchain.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
|
|
4
4
|
# SPDX-License-Identifier: MIT
|
|
5
5
|
|
|
6
|
+
require 'cgi'
|
|
6
7
|
require 'iri'
|
|
7
8
|
require 'json'
|
|
8
9
|
require 'loog'
|
|
@@ -32,7 +33,7 @@ class Sibit::Blockchain
|
|
|
32
33
|
h = Sibit::Json.new(http: @http, log: @log).get(
|
|
33
34
|
Iri.new('https://blockchain.info/ticker')
|
|
34
35
|
)[currency]
|
|
35
|
-
raise(Error, "Unrecognized currency #{currency}") if h.nil?
|
|
36
|
+
raise(Sibit::Error, "Unrecognized currency #{currency}") if h.nil?
|
|
36
37
|
price = h['15m']
|
|
37
38
|
@log.debug("The price of BTC is #{price} USD")
|
|
38
39
|
price
|
|
@@ -48,6 +49,7 @@ class Sibit::Blockchain
|
|
|
48
49
|
h = Sibit::Json.new(http: @http, log: @log).get(
|
|
49
50
|
Iri.new('https://blockchain.info/rawblock').append(hash)
|
|
50
51
|
)['height']
|
|
52
|
+
raise(Sibit::Error, "The block #{hash} found but the height is absent") if h.nil?
|
|
51
53
|
@log.debug("The height of #{hash} is #{h}")
|
|
52
54
|
h
|
|
53
55
|
end
|
|
@@ -55,10 +57,10 @@ class Sibit::Blockchain
|
|
|
55
57
|
# Gets the balance of the address, in satoshi.
|
|
56
58
|
def balance(address)
|
|
57
59
|
json = Sibit::Json.new(http: @http, log: @log).get(
|
|
58
|
-
Iri.new('https://blockchain.info/rawaddr').append(address).add(limit: 0)
|
|
59
|
-
accept: [200, 500]
|
|
60
|
+
Iri.new('https://blockchain.info/rawaddr').append(address).add(limit: 0)
|
|
60
61
|
)
|
|
61
62
|
b = json['final_balance']
|
|
63
|
+
raise(Sibit::Error, "The balance of #{address} is absent") unless b.is_a?(Integer)
|
|
62
64
|
@log.debug("The balance of #{address} is #{b} satoshi (#{json['n_tx']} txns)")
|
|
63
65
|
b
|
|
64
66
|
end
|
|
@@ -75,7 +77,7 @@ class Sibit::Blockchain
|
|
|
75
77
|
].join
|
|
76
78
|
)
|
|
77
79
|
{
|
|
78
|
-
S: json['regular'] / 3,
|
|
80
|
+
S: (json['regular'] / 3.0).ceil,
|
|
79
81
|
M: json['regular'],
|
|
80
82
|
L: json['priority'],
|
|
81
83
|
XL: json['limits']['max']
|
|
@@ -100,7 +102,9 @@ class Sibit::Blockchain
|
|
|
100
102
|
|
|
101
103
|
# Push this transaction (in hex format) to the network.
|
|
102
104
|
def push(hex)
|
|
103
|
-
Sibit::Json.new(http: @http, log: @log).post(
|
|
105
|
+
Sibit::Json.new(http: @http, log: @log).post(
|
|
106
|
+
Iri.new('https://blockchain.info/pushtx'), "tx=#{CGI.escape(hex)}"
|
|
107
|
+
)
|
|
104
108
|
end
|
|
105
109
|
|
|
106
110
|
# Gets the hash of the latest block.
|
data/lib/sibit/btc.rb
CHANGED
|
@@ -39,10 +39,7 @@ class Sibit::Btc
|
|
|
39
39
|
return 0
|
|
40
40
|
end
|
|
41
41
|
data = json['data']
|
|
42
|
-
if data.nil?
|
|
43
|
-
@log.debug("The balance of #{address} is probably zero (not found)")
|
|
44
|
-
return 0
|
|
45
|
-
end
|
|
42
|
+
raise(Sibit::Error, "The address #{address} not found") if data.nil?
|
|
46
43
|
txns = data['list']
|
|
47
44
|
if txns.nil?
|
|
48
45
|
@log.debug("The balance of #{address} is probably zero (not found)")
|
|
@@ -103,19 +100,19 @@ class Sibit::Btc
|
|
|
103
100
|
list = data['list']
|
|
104
101
|
next if list.nil?
|
|
105
102
|
list.each do |u|
|
|
106
|
-
|
|
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(
|
|
107
106
|
Iri.new('https://chain.api.btc.com/v3/tx').append(u['tx_hash']).add(verbose: 3)
|
|
108
|
-
)['data']['outputs']
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
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
|
+
}
|
|
119
116
|
end
|
|
120
117
|
end
|
|
121
118
|
results
|
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'
|
|
@@ -32,10 +33,13 @@ class Sibit::Cryptoapis
|
|
|
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
|
|
@@ -53,14 +57,12 @@ class Sibit::Cryptoapis
|
|
|
53
57
|
|
|
54
58
|
# Gets the balance of the address, in satoshi.
|
|
55
59
|
def balance(address)
|
|
56
|
-
b =
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
) * 100_000_000
|
|
63
|
-
)
|
|
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
|
|
64
66
|
@log.debug("The balance of #{address} is #{b} satoshi")
|
|
65
67
|
b
|
|
66
68
|
end
|
|
@@ -90,7 +92,7 @@ class Sibit::Cryptoapis
|
|
|
90
92
|
Sibit::Json.new(http: @http, log: @log).post(
|
|
91
93
|
Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/txs/send'),
|
|
92
94
|
JSON.pretty_generate(hex: hex),
|
|
93
|
-
headers: headers
|
|
95
|
+
headers: headers.merge('Content-Type' => 'application/json')
|
|
94
96
|
)
|
|
95
97
|
end
|
|
96
98
|
|
|
@@ -135,7 +137,7 @@ class Sibit::Cryptoapis
|
|
|
135
137
|
outputs: t['txouts'].map do |o|
|
|
136
138
|
{
|
|
137
139
|
address: o['addresses'][0],
|
|
138
|
-
value:
|
|
140
|
+
value: Sibit::Coins.new(o['amount'] || 0).satoshi
|
|
139
141
|
}
|
|
140
142
|
end
|
|
141
143
|
}
|
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'
|
|
@@ -62,7 +61,7 @@ class Sibit::Json
|
|
|
62
61
|
elapsed(@log) do
|
|
63
62
|
res = @http.client(uri).post(
|
|
64
63
|
"#{uri.path.empty? ? '/' : uri.path}#{"?#{uri.query}" if uri.query}",
|
|
65
|
-
|
|
64
|
+
body,
|
|
66
65
|
{
|
|
67
66
|
'Accept' => 'text/plain',
|
|
68
67
|
'User-Agent' => user_agent,
|
data/lib/sibit/sochain.rb
CHANGED
|
@@ -111,9 +111,9 @@ class Sibit::Sochain
|
|
|
111
111
|
data = Sibit::Json.new(http: @http, log: @log).get(
|
|
112
112
|
Iri.new('https://sochain.com/api/v2/get_tx_unspent').append(@network).append(address)
|
|
113
113
|
)['data']
|
|
114
|
-
|
|
114
|
+
raise(Sibit::Error, "The address #{address} unspent lookup returned no data") if data.nil?
|
|
115
115
|
txs = data['txs']
|
|
116
|
-
|
|
116
|
+
raise(Sibit::Error, "The address #{address} unspent lookup returned no txs") if txs.nil?
|
|
117
117
|
txs.each do |u|
|
|
118
118
|
out << {
|
|
119
119
|
value: Integer((Float(u['value']) * 100_000_000).round),
|
|
@@ -162,9 +162,12 @@ class Sibit::Sochain
|
|
|
162
162
|
next: nxt,
|
|
163
163
|
previous: data['previous_blockhash'],
|
|
164
164
|
txns: (data['txs'] || []).map do |t|
|
|
165
|
+
unless t.is_a?(Hash)
|
|
166
|
+
raise(Sibit::NotSupportedError, "SoChain returned bare txid #{t} without outputs")
|
|
167
|
+
end
|
|
165
168
|
{
|
|
166
|
-
hash: t['txid']
|
|
167
|
-
outputs: (
|
|
169
|
+
hash: t['txid'],
|
|
170
|
+
outputs: (t['outputs'] || []).map do |o|
|
|
168
171
|
{
|
|
169
172
|
address: o['address'],
|
|
170
173
|
value: Integer((Float(o['value']) * 100_000_000).round)
|
data/lib/sibit/tx.rb
CHANGED
|
@@ -38,12 +38,16 @@ class Sibit
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def hash
|
|
41
|
-
|
|
41
|
+
sign_inputs
|
|
42
|
+
Digest::SHA256.hexdigest(
|
|
43
|
+
Digest::SHA256.digest(serialize(witness: false))
|
|
44
|
+
).scan(/../).reverse.join
|
|
42
45
|
end
|
|
43
46
|
|
|
44
47
|
def payload
|
|
48
|
+
return @payload if @payload
|
|
45
49
|
sign_inputs
|
|
46
|
-
serialize
|
|
50
|
+
@payload = serialize
|
|
47
51
|
end
|
|
48
52
|
|
|
49
53
|
def hex
|
|
@@ -121,12 +125,24 @@ class Sibit
|
|
|
121
125
|
|
|
122
126
|
private
|
|
123
127
|
|
|
128
|
+
def decoded
|
|
129
|
+
return @decoded if @decoded
|
|
130
|
+
hex = Base58.new(@address).decode
|
|
131
|
+
unless hex.length == 50
|
|
132
|
+
raise(Sibit::Error, "Address '#{@address}' does not decode to 25 bytes")
|
|
133
|
+
end
|
|
134
|
+
unless hex[-8..] == Base58.new(hex[0..-9]).check
|
|
135
|
+
raise(Sibit::Error, "Address '#{@address}' fails its Base58 checksum")
|
|
136
|
+
end
|
|
137
|
+
@decoded = hex
|
|
138
|
+
end
|
|
139
|
+
|
|
124
140
|
def version
|
|
125
|
-
|
|
141
|
+
decoded[0, 2]
|
|
126
142
|
end
|
|
127
143
|
|
|
128
144
|
def hash160
|
|
129
|
-
[
|
|
145
|
+
[decoded[2, 40]].pack('H*')
|
|
130
146
|
end
|
|
131
147
|
|
|
132
148
|
def p2pkh_script
|
|
@@ -138,8 +154,13 @@ class Sibit
|
|
|
138
154
|
end
|
|
139
155
|
|
|
140
156
|
def segwit_script
|
|
141
|
-
|
|
142
|
-
|
|
157
|
+
bech = Bech32.new(@address)
|
|
158
|
+
program = bech.witness
|
|
159
|
+
[opcode(bech.version), program.length / 2].pack('C*') + [program].pack('H*')
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def opcode(version)
|
|
163
|
+
version.zero? ? 0x00 : 0x50 + version
|
|
143
164
|
end
|
|
144
165
|
end
|
|
145
166
|
|
|
@@ -150,6 +171,7 @@ class Sibit
|
|
|
150
171
|
end
|
|
151
172
|
|
|
152
173
|
def sign_inputs
|
|
174
|
+
return if @signed
|
|
153
175
|
@inputs.each_with_index do |input, idx|
|
|
154
176
|
sig = sign(input.key, (input.segwit? ? segwit_sighash(idx) : legacy_sighash(idx)))
|
|
155
177
|
pubkey = [input.key.pub].pack('H*')
|
|
@@ -159,6 +181,7 @@ class Sibit
|
|
|
159
181
|
input.script_sig = der_sig(sig) + pubkey_script(pubkey)
|
|
160
182
|
end
|
|
161
183
|
end
|
|
184
|
+
@signed = true
|
|
162
185
|
end
|
|
163
186
|
|
|
164
187
|
def legacy_sighash(idx)
|
|
@@ -244,9 +267,9 @@ class Sibit
|
|
|
244
267
|
[pubkey.length].pack('C') + pubkey
|
|
245
268
|
end
|
|
246
269
|
|
|
247
|
-
def serialize
|
|
270
|
+
def serialize(witness: witness?)
|
|
248
271
|
result = [VERSION].pack('V')
|
|
249
|
-
result += [0x00, 0x01].pack('CC') if witness
|
|
272
|
+
result += [0x00, 0x01].pack('CC') if witness
|
|
250
273
|
result += varint(@inputs.length)
|
|
251
274
|
@inputs.each do |input|
|
|
252
275
|
result += [input.hash].pack('H*').reverse
|
|
@@ -262,7 +285,7 @@ class Sibit
|
|
|
262
285
|
result += varint(script.length)
|
|
263
286
|
result += script
|
|
264
287
|
end
|
|
265
|
-
result += serialize_witness if witness
|
|
288
|
+
result += serialize_witness if witness
|
|
266
289
|
result += [0].pack('V')
|
|
267
290
|
result
|
|
268
291
|
end
|
data/lib/sibit/txbuilder.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
|
|
4
4
|
# SPDX-License-Identifier: MIT
|
|
5
5
|
|
|
6
|
+
require_relative 'error'
|
|
6
7
|
require_relative 'key'
|
|
7
8
|
require_relative 'tx'
|
|
8
9
|
|
|
@@ -17,6 +18,8 @@ class Sibit
|
|
|
17
18
|
# Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
|
|
18
19
|
# License:: MIT
|
|
19
20
|
class TxBuilder
|
|
21
|
+
DUST = 546
|
|
22
|
+
|
|
20
23
|
def initialize
|
|
21
24
|
@inputs = []
|
|
22
25
|
@outputs = []
|
|
@@ -47,6 +50,15 @@ class Sibit
|
|
|
47
50
|
@outputs.each { |o| txn.add_output(o[:value], o[:address]) }
|
|
48
51
|
if leave_fee
|
|
49
52
|
change = input_value - total - extra_fee
|
|
53
|
+
if change.negative?
|
|
54
|
+
raise(
|
|
55
|
+
Sibit::Error,
|
|
56
|
+
"The inputs of #{input_value} cannot cover outputs of #{total} plus fee of #{extra_fee}"
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
if change.positive? && change < DUST
|
|
60
|
+
raise(Sibit::Error, "The change of #{change} is below the dust limit of #{DUST}")
|
|
61
|
+
end
|
|
50
62
|
txn.add_output(change, change_address) if change.positive?
|
|
51
63
|
end
|
|
52
64
|
Built.new(txn, @inputs, @outputs)
|
data/lib/sibit/version.rb
CHANGED
data/lib/sibit.rb
CHANGED
|
@@ -7,6 +7,7 @@ require 'ellipsized'
|
|
|
7
7
|
require 'loog'
|
|
8
8
|
require_relative 'sibit/base58'
|
|
9
9
|
require_relative 'sibit/blockchain'
|
|
10
|
+
require_relative 'sibit/coins'
|
|
10
11
|
require_relative 'sibit/key'
|
|
11
12
|
require_relative 'sibit/script'
|
|
12
13
|
require_relative 'sibit/tx'
|
|
@@ -19,7 +20,7 @@ require_relative 'sibit/version'
|
|
|
19
20
|
# Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
|
|
20
21
|
# License:: MIT
|
|
21
22
|
class Sibit
|
|
22
|
-
MIN_SATOSHI_PER_BYTE =
|
|
23
|
+
MIN_SATOSHI_PER_BYTE = 1
|
|
23
24
|
|
|
24
25
|
# Constructor.
|
|
25
26
|
#
|
|
@@ -96,9 +97,13 @@ class Sibit
|
|
|
96
97
|
#
|
|
97
98
|
# If the payment can't be signed (the key is wrong, for example) or the
|
|
98
99
|
# previous transaction is not found, or there is a network error, or
|
|
99
|
-
# any other reason, you will get an exception.
|
|
100
|
-
#
|
|
101
|
-
#
|
|
100
|
+
# any other reason, you will get an exception. You may retry, but only
|
|
101
|
+
# while no earlier attempt could have reached the network: inputs are
|
|
102
|
+
# selected deterministically, so an immediate retry rebuilds a conflicting
|
|
103
|
+
# transaction that the network rejects as a double-spend. Once an earlier
|
|
104
|
+
# attempt might have confirmed, the wallet holds a fresh set of UTXOs and
|
|
105
|
+
# a blind retry may pay the target twice; check the target address (or the
|
|
106
|
+
# previous transaction hash) before re-sending.
|
|
102
107
|
#
|
|
103
108
|
# If there are more than 1000 UTXOs in the address where you are trying
|
|
104
109
|
# to send bitcoins from, this method won't be helpful.
|
|
@@ -150,7 +155,7 @@ class Sibit
|
|
|
150
155
|
builder = TxBuilder.new
|
|
151
156
|
unspent = 0
|
|
152
157
|
size = 100
|
|
153
|
-
utxos = @api.utxos(sources.keys)
|
|
158
|
+
utxos = @api.utxos(sources.keys).sort_by { |u| [u[:hash], u[:index]] }
|
|
154
159
|
@log.debug("#{utxos.count} UTXOs found, these will be used (value/confirmations at tx_hash):")
|
|
155
160
|
utxos.each do |utxo|
|
|
156
161
|
if skip_utxo.include?(utxo[:hash])
|
|
@@ -313,7 +318,7 @@ class Sibit
|
|
|
313
318
|
unless amount.is_a?(String)
|
|
314
319
|
raise(Error, "Amount should either be a String or Integer, #{amount.class.name} provided")
|
|
315
320
|
end
|
|
316
|
-
return
|
|
321
|
+
return Coins.new(amount.gsub(/BTC$/, '')).satoshi if amount.end_with?('BTC')
|
|
317
322
|
raise(Error, "Can't understand the amount #{amount.inspect}")
|
|
318
323
|
end
|
|
319
324
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sibit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.34.
|
|
4
|
+
version: 0.34.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yegor Bugayenko
|
|
@@ -192,6 +192,7 @@ files:
|
|
|
192
192
|
- lib/sibit/blockchair.rb
|
|
193
193
|
- lib/sibit/btc.rb
|
|
194
194
|
- lib/sibit/cex.rb
|
|
195
|
+
- lib/sibit/coins.rb
|
|
195
196
|
- lib/sibit/cryptoapis.rb
|
|
196
197
|
- lib/sibit/dry.rb
|
|
197
198
|
- lib/sibit/error.rb
|