block_io 1.2.1 → 3.0.2
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/.appveyor.yml-disabled +26 -0
- data/.gitignore +2 -1
- data/.rspec +1 -0
- data/.travis.yml +14 -0
- data/LICENSE +1 -1
- data/README.md +24 -15
- data/block_io.gemspec +9 -7
- data/examples/basic.rb +32 -10
- data/examples/dtrust.rb +59 -42
- data/examples/proxy.rb +36 -0
- data/examples/sweeper.rb +24 -14
- data/lib/block_io.rb +16 -411
- data/lib/block_io/api_exception.rb +11 -0
- data/lib/block_io/chainparams/BTC.yml +8 -0
- data/lib/block_io/chainparams/BTCTEST.yml +8 -0
- data/lib/block_io/chainparams/DOGE.yml +8 -0
- data/lib/block_io/chainparams/DOGETEST.yml +8 -0
- data/lib/block_io/chainparams/LTC.yml +8 -0
- data/lib/block_io/chainparams/LTCTEST.yml +8 -0
- data/lib/block_io/client.rb +244 -0
- data/lib/block_io/extended_bitcoinrb.rb +127 -0
- data/lib/block_io/helper.rb +322 -0
- data/lib/block_io/key.rb +38 -0
- data/lib/block_io/version.rb +1 -1
- data/spec/client_misc_spec.rb +76 -0
- data/spec/client_spec.rb +68 -0
- data/spec/dtrust_spec.rb +167 -0
- data/spec/helper_spec.rb +154 -0
- data/spec/key_spec.rb +92 -0
- data/spec/larger_transaction_spec.rb +351 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/sweep_spec.rb +115 -0
- data/spec/test-cases/.gitignore +2 -0
- data/spec/test-cases/LICENSE +21 -0
- data/spec/test-cases/README.md +2 -0
- data/spec/test-cases/json/create_and_sign_transaction_response.json +61 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_P2WSH-over-P2SH_1of2_251inputs.json +1261 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_P2WSH-over-P2SH_1of2_252inputs.json +1266 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_P2WSH-over-P2SH_1of2_253inputs.json +1271 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_P2WSH-over-P2SH_1of2_762inputs.json +3816 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2SH_3of5_195inputs.json +2931 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2SH_4of5_195inputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_3of5_251inputs.json +3771 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_3of5_252inputs.json +3786 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_3of5_253inputs.json +3801 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_4of5_251inputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_4of5_252inputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_P2WSH-over-P2SH_4of5_253inputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_3of5_251inputs.json +3771 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_3of5_252inputs.json +3786 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_3of5_253inputs.json +3801 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_4of5_251inputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_4of5_252inputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_WITNESS_V0_4of5_253inputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_p2sh_3_of_5_keys.json +21 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_p2sh_4_of_5_keys.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_p2wsh_over_p2sh_3_of_5_keys.json +21 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_p2wsh_over_p2sh_4_of_5_keys.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_3_of_5_keys.json +36 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_3of5_251outputs.json +591 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_3of5_252outputs.json +576 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_3of5_253outputs.json +531 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_4_of_5_keys.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_4of5_251outputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_4of5_252outputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_dtrust_witness_v0_4of5_253outputs.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_sweep_p2pkh.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_sweep_p2wpkh.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_sweep_p2wpkh_over_p2sh.json +5 -0
- data/spec/test-cases/json/create_and_sign_transaction_response_with_blockio_fee_and_expected_unsigned_txid.json +21 -0
- data/spec/test-cases/json/get_balance_response.json +8 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2SH_3of5_195inputs.json +1397 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2SH_4of5_195inputs.json +1397 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_3of5_251inputs.json +1795 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_3of5_252inputs.json +1802 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_3of5_253inputs.json +1809 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_4of5_251inputs.json +1789 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_4of5_252inputs.json +1802 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_P2WSH-over-P2SH_4of5_253inputs.json +1809 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_3of5_251inputs.json +1795 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_3of5_252inputs.json +1802 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_3of5_253inputs.json +1809 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_4of5_251inputs.json +1795 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_4of5_252inputs.json +1802 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_WITNESS_V0_4of5_253inputs.json +1809 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_p2sh.json +45 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_p2wsh_over_p2sh.json +45 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0.json +52 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_3of5_251outputs.json +1805 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_3of5_252outputs.json +1804 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_3of5_253outputs.json +1789 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_4of5_251outputs.json +1805 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_4of5_252outputs.json +1804 -0
- data/spec/test-cases/json/prepare_dtrust_transaction_response_witness_v0_4of5_253outputs.json +1789 -0
- data/spec/test-cases/json/prepare_sweep_transaction_response_p2pkh.json +35 -0
- data/spec/test-cases/json/prepare_sweep_transaction_response_p2wpkh.json +35 -0
- data/spec/test-cases/json/prepare_sweep_transaction_response_p2wpkh_over_p2sh.json +35 -0
- data/spec/test-cases/json/prepare_transaction_response.json +164 -0
- data/spec/test-cases/json/prepare_transaction_response_P2WSH-over-P2SH_1of2_251inputs.json +1796 -0
- data/spec/test-cases/json/prepare_transaction_response_P2WSH-over-P2SH_1of2_252inputs.json +1803 -0
- data/spec/test-cases/json/prepare_transaction_response_P2WSH-over-P2SH_1of2_253inputs.json +1810 -0
- data/spec/test-cases/json/prepare_transaction_response_P2WSH-over-P2SH_1of2_762inputs.json +5367 -0
- data/spec/test-cases/json/prepare_transaction_response_with_blockio_fee_and_expected_unsigned_txid.json +76 -0
- data/spec/test-cases/json/summarize_prepared_transaction_response_with_blockio_fee_and_expected_unsigned_txid.json +6 -0
- metadata +270 -44
- data/examples/change.rb +0 -117
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
module BlockIo
|
|
2
|
+
|
|
3
|
+
class Client
|
|
4
|
+
|
|
5
|
+
attr_reader :api_key, :version, :network
|
|
6
|
+
|
|
7
|
+
def initialize(args = {})
|
|
8
|
+
# api_key
|
|
9
|
+
# pin
|
|
10
|
+
# version
|
|
11
|
+
# hostname
|
|
12
|
+
# proxy
|
|
13
|
+
# pool_size
|
|
14
|
+
# keys
|
|
15
|
+
|
|
16
|
+
raise "Must provide an API Key." unless args.key?(:api_key) and args[:api_key].to_s.size > 0
|
|
17
|
+
|
|
18
|
+
@api_key = args[:api_key]
|
|
19
|
+
@pin = args[:pin]
|
|
20
|
+
@version = args[:version] || 2
|
|
21
|
+
@hostname = args[:hostname] || "block.io"
|
|
22
|
+
@proxy = args[:proxy] || {}
|
|
23
|
+
@keys = {}
|
|
24
|
+
|
|
25
|
+
raise Exception.new("Must specify hostname, port, username, password if using a proxy.") if @proxy.keys.size > 0 and [:hostname, :port, :username, :password].any?{|x| !@proxy.key?(x)}
|
|
26
|
+
|
|
27
|
+
@conn = ConnectionPool.new(:size => args[:pool_size] || 5) { http = HTTP.headers(:accept => "application/json", :user_agent => "gem:block_io:#{VERSION}");
|
|
28
|
+
http = http.via(args.dig(:proxy, :hostname), args.dig(:proxy, :port), args.dig(:proxy, :username), args.dig(:proxy, :password)) if @proxy.key?(:hostname);
|
|
29
|
+
http = http.persistent("https://#{@hostname}");
|
|
30
|
+
http }
|
|
31
|
+
|
|
32
|
+
# this will get populated after a successful API call
|
|
33
|
+
@network = nil
|
|
34
|
+
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def method_missing(m, *args)
|
|
38
|
+
|
|
39
|
+
method_name = m.to_s
|
|
40
|
+
|
|
41
|
+
raise Exception.new("Must provide arguments as a Hash.") unless args.size <= 1 and args.all?{|x| x.is_a?(Hash)}
|
|
42
|
+
raise Exception.new("Parameter keys must be symbols. For instance: :label => 'default' instead of 'label' => 'default'") unless args[0].nil? or args[0].keys.all?{|x| x.is_a?(Symbol)}
|
|
43
|
+
raise Exception.new("Cannot pass PINs to any calls. PINs can only be set when initiating this library.") if !args[0].nil? and args[0].key?(:pin)
|
|
44
|
+
raise Exception.new("Do not specify API Keys here. Initiate a new BlockIo object instead if you need to use another API Key.") if !args[0].nil? and args[0].key?(:api_key)
|
|
45
|
+
|
|
46
|
+
if method_name.eql?("prepare_sweep_transaction") then
|
|
47
|
+
# we need to ensure @network is set before we allow this
|
|
48
|
+
# we need to send only the public key, not the given private key
|
|
49
|
+
# we're sweeping from an address
|
|
50
|
+
internal_prepare_sweep_transaction(args[0], method_name)
|
|
51
|
+
else
|
|
52
|
+
api_call({:method_name => method_name, :params => args[0] || {}})
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def summarize_prepared_transaction(data)
|
|
58
|
+
# takes the response from prepare_transaction/prepare_dtrust_transaction/prepare_sweep_transaction
|
|
59
|
+
# returns the network fee being paid, the blockio fee being paid, amounts being sent
|
|
60
|
+
|
|
61
|
+
input_sum = data['data']['inputs'].map{|input| BigDecimal(input['input_value'])}.inject(:+)
|
|
62
|
+
|
|
63
|
+
output_values = [BigDecimal(0)]
|
|
64
|
+
blockio_fees = [BigDecimal(0)]
|
|
65
|
+
change_amounts = [BigDecimal(0)]
|
|
66
|
+
|
|
67
|
+
data['data']['outputs'].each do |output|
|
|
68
|
+
if output['output_category'] == 'blockio-fee' then
|
|
69
|
+
blockio_fees << BigDecimal(output['output_value'])
|
|
70
|
+
elsif output['output_category'] == 'change' then
|
|
71
|
+
change_amounts << BigDecimal(output['output_value'])
|
|
72
|
+
else
|
|
73
|
+
# user-specified
|
|
74
|
+
output_values << BigDecimal(output['output_value'])
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
output_sum = output_values.inject(:+)
|
|
79
|
+
blockio_fee = blockio_fees.inject(:+)
|
|
80
|
+
change_amount = change_amounts.inject(:+)
|
|
81
|
+
|
|
82
|
+
network_fee = input_sum - output_sum - blockio_fee - change_amount
|
|
83
|
+
|
|
84
|
+
{
|
|
85
|
+
'network' => data['data']['network'],
|
|
86
|
+
'network_fee' => '%0.8f' % network_fee,
|
|
87
|
+
"blockio_fee" => '%0.8f' % blockio_fee,
|
|
88
|
+
"total_amount_to_send" => '%0.8f' % output_sum
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def create_and_sign_transaction(data, keys = [])
|
|
94
|
+
# takes data from prepare_transaction, prepare_dtrust_transaction, prepare_sweep_transaction
|
|
95
|
+
# creates the transaction given the inputs and outputs from data
|
|
96
|
+
# signs the transaction using keys (if not provided, decrypts the key using the PIN)
|
|
97
|
+
|
|
98
|
+
set_network(data['data']['network']) if data['data'].key?('network')
|
|
99
|
+
|
|
100
|
+
raise "Data must be contain one or more inputs" unless data['data']['inputs'].size > 0
|
|
101
|
+
raise "Data must contain one or more outputs" unless data['data']['outputs'].size > 0
|
|
102
|
+
raise "Data must contain information about addresses" unless data['data']['input_address_data'].size > 0 # TODO make stricter
|
|
103
|
+
|
|
104
|
+
private_keys = keys.map{|x| Key.from_private_key_hex(x)}
|
|
105
|
+
|
|
106
|
+
# TODO debug all of this
|
|
107
|
+
|
|
108
|
+
inputs = data['data']['inputs']
|
|
109
|
+
outputs = data['data']['outputs']
|
|
110
|
+
|
|
111
|
+
tx = Bitcoin::Tx.new
|
|
112
|
+
|
|
113
|
+
# populate the inputs
|
|
114
|
+
inputs.each do |input|
|
|
115
|
+
tx.in << Bitcoin::TxIn.new(:out_point => Bitcoin::OutPoint.from_txid(input['previous_txid'], input['previous_output_index']))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# populate the outputs
|
|
119
|
+
outputs.each do |output|
|
|
120
|
+
tx.out << Bitcoin::TxOut.new(:value => (BigDecimal(output['output_value']) * BigDecimal(100000000)).to_i, :script_pubkey => Bitcoin::Script.parse_from_addr(output['receiving_address']))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# some protection against misbehaving machines and/or code
|
|
125
|
+
raise Exception.new("Expected unsigned transaction ID mismatch. Please report this error to support@block.io.") unless (data['data']['expected_unsigned_txid'].nil? or
|
|
126
|
+
data['data']['expected_unsigned_txid'] == tx.txid)
|
|
127
|
+
|
|
128
|
+
# extract key
|
|
129
|
+
encrypted_key = data['data']['user_key']
|
|
130
|
+
|
|
131
|
+
if !encrypted_key.nil? and !@keys.key?(encrypted_key['public_key']) then
|
|
132
|
+
# decrypt the key with PIN
|
|
133
|
+
|
|
134
|
+
raise Exception.new("PIN not set and no keys provided. Cannot sign transaction.") unless !@pin.nil? or @keys.size > 0
|
|
135
|
+
|
|
136
|
+
key = Helper.dynamicExtractKey(encrypted_key, @pin)
|
|
137
|
+
|
|
138
|
+
raise Exception.new("Public key mismatch for requested signer and ourselves. Invalid Secret PIN detected.") unless key.public_key_hex.eql?(encrypted_key["public_key"])
|
|
139
|
+
|
|
140
|
+
# store this key for later use
|
|
141
|
+
@keys[key.public_key_hex] = key
|
|
142
|
+
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# store the provided keys, if any, for later use
|
|
146
|
+
private_keys.each{|key| @keys[key.public_key_hex] = key}
|
|
147
|
+
|
|
148
|
+
signatures = []
|
|
149
|
+
|
|
150
|
+
if @keys.size > 0 then
|
|
151
|
+
# try to sign whatever we can here and give the user the data back
|
|
152
|
+
# Block.io will check to see if all signatures are present, or return an error otherwise saying insufficient signatures provided
|
|
153
|
+
|
|
154
|
+
i = 0
|
|
155
|
+
while i < inputs.size do
|
|
156
|
+
input = inputs[i]
|
|
157
|
+
|
|
158
|
+
input_address_data = data['data']['input_address_data'].detect{|d| d['address'] == input['spending_address']}
|
|
159
|
+
sighash_for_input = Helper.getSigHashForInput(tx, i, input, input_address_data) # in bytes
|
|
160
|
+
|
|
161
|
+
input_address_data['public_keys'].each do |signer_public_key|
|
|
162
|
+
# sign what we can and append signatures to the signatures object
|
|
163
|
+
|
|
164
|
+
next unless @keys.key?(signer_public_key)
|
|
165
|
+
|
|
166
|
+
signature = @keys[signer_public_key].sign(sighash_for_input).unpack("H*")[0] # in hex
|
|
167
|
+
signatures << {"input_index" => i, "public_key" => signer_public_key, "signature" => signature}
|
|
168
|
+
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
i += 1 # go to next input
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# if we have everything we need for this transaction, just finalize the transaction
|
|
177
|
+
if Helper.allSignaturesPresent?(tx, inputs, signatures, data['data']['input_address_data']) then
|
|
178
|
+
Helper.finalizeTransaction(tx, inputs, signatures, data['data']['input_address_data'])
|
|
179
|
+
signatures = [] # no signatures left to append
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# reset keys
|
|
183
|
+
@keys = {}
|
|
184
|
+
|
|
185
|
+
# the response for submitting the transaction
|
|
186
|
+
{"tx_type" => data['data']['tx_type'], "tx_hex" => tx.to_hex, "signatures" => (signatures.size == 0 ? nil : signatures)}
|
|
187
|
+
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
def internal_prepare_sweep_transaction(args = {}, method_name = "prepare_sweep_transaction")
|
|
193
|
+
|
|
194
|
+
# set the network first if not already known
|
|
195
|
+
api_call({:method_name => "get_balance", :params => {}}) if @network.nil?
|
|
196
|
+
|
|
197
|
+
raise Exception.new("No private_key provided.") unless args.key?(:private_key) and (args[:private_key] || "").size > 0
|
|
198
|
+
|
|
199
|
+
# ensure the private key never goes to Block.io
|
|
200
|
+
key = Key.from_wif(args[:private_key])
|
|
201
|
+
sanitized_args = args.merge({:public_key => key.public_key_hex})
|
|
202
|
+
sanitized_args.delete(:private_key)
|
|
203
|
+
|
|
204
|
+
@keys[key.public_key_hex] = key # store this in our set of keys for later use
|
|
205
|
+
|
|
206
|
+
api_call({:method_name => method_name, :params => sanitized_args})
|
|
207
|
+
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def set_network(network)
|
|
211
|
+
# load the chain_params for this network
|
|
212
|
+
@network ||= network
|
|
213
|
+
Bitcoin.chain_params = @network unless @network.to_s.size == 0
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def api_call(args)
|
|
217
|
+
|
|
218
|
+
raise Exception.new("No connections left to perform API call. Please re-initialize BlockIo::Client with :pool_size greater than #{@conn.size}.") unless @conn.available > 0
|
|
219
|
+
|
|
220
|
+
response = @conn.with {|http| http.post("/api/v#{@version}/#{args[:method_name]}", :json => args[:params].merge({:api_key => @api_key}))}
|
|
221
|
+
|
|
222
|
+
begin
|
|
223
|
+
body = Oj.safe_load(response.to_s)
|
|
224
|
+
rescue
|
|
225
|
+
body = {"status" => "fail", "data" => {"error_message" => "Unknown error occurred. Please report this to support@block.io. Status #{response.code}."}}
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
if !body["status"].eql?("success") then
|
|
229
|
+
# raise an exception on error for easy handling
|
|
230
|
+
# user can extract raw response using e.raw_data
|
|
231
|
+
e = APIException.new("#{body["data"]["error_message"]}")
|
|
232
|
+
e.set_raw_data(body)
|
|
233
|
+
raise e
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
set_network(body['data']['network']) if body['data'].key?('network')
|
|
237
|
+
|
|
238
|
+
body
|
|
239
|
+
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
|
|
3
|
+
module Bitcoin
|
|
4
|
+
|
|
5
|
+
# set network chain params
|
|
6
|
+
def self.chain_params=(name)
|
|
7
|
+
raise "chain params for #{name} is not defined." unless %i(BTC DOGE LTC BTCTEST DOGETEST LTCTEST).include?(name.to_sym)
|
|
8
|
+
@current_chain = nil
|
|
9
|
+
@chain_param = name.to_sym
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# current network chain params.
|
|
13
|
+
def self.chain_params
|
|
14
|
+
return @current_chain if @current_chain
|
|
15
|
+
return (@current_chain = Bitcoin::ChainParams.get(@chain_param.to_s))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class ChainParams
|
|
19
|
+
def self.get(network)
|
|
20
|
+
init(network)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.init(name)
|
|
24
|
+
i = YAML.load(File.open("#{__dir__}/chainparams/#{name}.yml"))
|
|
25
|
+
i.dust_relay_fee ||= Bitcoin::DUST_RELAY_TX_FEE
|
|
26
|
+
i
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
module Secp256k1
|
|
31
|
+
|
|
32
|
+
module Ruby
|
|
33
|
+
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
def sign_ecdsa(data, privkey, extra_entropy)
|
|
37
|
+
privkey = privkey.htb
|
|
38
|
+
private_key = ECDSA::Format::IntegerOctetString.decode(privkey)
|
|
39
|
+
extra_entropy ||= ''
|
|
40
|
+
nonce = RFC6979.generate_rfc6979_nonce(privkey + data, extra_entropy)
|
|
41
|
+
|
|
42
|
+
# port form ecdsa gem.
|
|
43
|
+
r_point = GROUP.new_point(nonce)
|
|
44
|
+
|
|
45
|
+
point_field = ECDSA::PrimeField.new(GROUP.order)
|
|
46
|
+
r = point_field.mod(r_point.x)
|
|
47
|
+
return nil if r.zero?
|
|
48
|
+
|
|
49
|
+
e = ECDSA.normalize_digest(data, GROUP.bit_length)
|
|
50
|
+
s = point_field.mod(point_field.inverse(nonce) * (e + r * private_key))
|
|
51
|
+
|
|
52
|
+
# covert to low-s
|
|
53
|
+
s = GROUP.order - s if s > (GROUP.order / 2)
|
|
54
|
+
|
|
55
|
+
return nil if s.zero?
|
|
56
|
+
|
|
57
|
+
signature = ECDSA::Signature.new(r, s).to_der
|
|
58
|
+
|
|
59
|
+
# comment lines below lead to performance issues
|
|
60
|
+
# public_key = Bitcoin::Key.new(priv_key: privkey.bth, :key_type => Bitcoin::Key::TYPES[:compressed]).pubkey # get rid of the key_type warning
|
|
61
|
+
# raise 'Creation of signature failed.' unless Bitcoin::Secp256k1::Ruby.verify_sig(data, signature, public_key)
|
|
62
|
+
|
|
63
|
+
signature
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class Key
|
|
70
|
+
|
|
71
|
+
def initialize(priv_key: nil, pubkey: nil, key_type: nil, compressed: true, allow_hybrid: false)
|
|
72
|
+
# override so enforce compressed keys
|
|
73
|
+
|
|
74
|
+
raise "key_type must always be Bitcoin::KEY::TYPES[:compressed]" unless key_type == TYPES[:compressed]
|
|
75
|
+
puts "[Warning] Use key_type parameter instead of compressed. compressed parameter removed in the future." if key_type.nil? && !compressed.nil? && pubkey.nil?
|
|
76
|
+
if key_type
|
|
77
|
+
@key_type = key_type
|
|
78
|
+
compressed = @key_type != TYPES[:uncompressed]
|
|
79
|
+
else
|
|
80
|
+
@key_type = compressed ? TYPES[:compressed] : TYPES[:uncompressed]
|
|
81
|
+
end
|
|
82
|
+
@secp256k1_module = Bitcoin.secp_impl
|
|
83
|
+
@priv_key = priv_key
|
|
84
|
+
if @priv_key
|
|
85
|
+
raise ArgumentError, Errors::Messages::INVALID_PRIV_KEY unless validate_private_key_range(@priv_key)
|
|
86
|
+
end
|
|
87
|
+
if pubkey
|
|
88
|
+
@pubkey = pubkey
|
|
89
|
+
else
|
|
90
|
+
@pubkey = generate_pubkey(priv_key, compressed: compressed) if priv_key
|
|
91
|
+
end
|
|
92
|
+
raise ArgumentError, Errors::Messages::INVALID_PUBLIC_KEY unless fully_valid_pubkey?(allow_hybrid)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def public_key_hex
|
|
96
|
+
@pubkey
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def private_key_hex
|
|
100
|
+
@priv_key
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
module Bech32
|
|
108
|
+
# override so we can parse non-Bitcoin Bech32 addresses
|
|
109
|
+
|
|
110
|
+
class SegwitAddr
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
def parse_addr(addr)
|
|
114
|
+
@hrp, data, spec = Bech32.decode(addr)
|
|
115
|
+
raise 'Invalid address.' if hrp.nil? || data[0].nil? || !['bc', 'ltc', 'tb', 'tltc', 'doge', 'tdge'].include?(hrp) # HRP_MAINNET, HRP_TESTNET, HRP_REGTEST].include?(hrp)
|
|
116
|
+
@ver = data[0]
|
|
117
|
+
raise 'Invalid witness version' if @ver > 16
|
|
118
|
+
@prog = convert_bits(data[1..-1], 5, 8, false)
|
|
119
|
+
raise 'Invalid witness program' if @prog.nil? || @prog.length < 2 || @prog.length > 40
|
|
120
|
+
raise 'Invalid witness program with version 0' if @ver == 0 && (@prog.length != 20 && @prog.length != 32)
|
|
121
|
+
raise 'Witness version and encoding spec do not match' if (@ver == 0 && spec != Bech32::Encoding::BECH32) || (@ver != 0 && spec != Bech32::Encoding::BECH32M)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
end
|
|
127
|
+
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
module BlockIo
|
|
2
|
+
|
|
3
|
+
class Helper
|
|
4
|
+
|
|
5
|
+
LEGACY_DECRYPTION_ALGORITHM = {
|
|
6
|
+
:pbkdf2_salt => "",
|
|
7
|
+
:pbkdf2_iterations => 2048,
|
|
8
|
+
:pbkdf2_hash_function => "SHA256",
|
|
9
|
+
:pbkdf2_phase1_key_length => 16,
|
|
10
|
+
:pbkdf2_phase2_key_length => 32,
|
|
11
|
+
:aes_iv => nil,
|
|
12
|
+
:aes_cipher => "AES-256-ECB",
|
|
13
|
+
:aes_auth_tag => nil,
|
|
14
|
+
:aes_auth_data => nil
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
def self.allSignaturesPresent?(tx, inputs, signatures, input_address_data)
|
|
18
|
+
# returns true if transaction has all signatures present
|
|
19
|
+
|
|
20
|
+
all_signatures_present = false
|
|
21
|
+
|
|
22
|
+
inputs.each do |input|
|
|
23
|
+
# check if each input has its required signatures
|
|
24
|
+
|
|
25
|
+
spending_address = input['spending_address']
|
|
26
|
+
current_input_address_data = input_address_data.detect{|x| x['address'] == spending_address}
|
|
27
|
+
required_signatures = current_input_address_data['required_signatures']
|
|
28
|
+
public_keys = current_input_address_data['public_keys']
|
|
29
|
+
|
|
30
|
+
signatures_present = signatures.map{|x| x if x['input_index'] == input['input_index']}.compact.inject({}){|h,v| h[v['public_key']] = v['signature']; h}
|
|
31
|
+
|
|
32
|
+
# break the loop if all signatures are not present for this input
|
|
33
|
+
all_signatures_present = (signatures_present.keys.size >= required_signatures)
|
|
34
|
+
break unless all_signatures_present
|
|
35
|
+
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
all_signatures_present
|
|
39
|
+
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.isSegwitAddressType?(address_type)
|
|
43
|
+
|
|
44
|
+
case address_type
|
|
45
|
+
when /^P2WPKH(-over-P2SH)?$/
|
|
46
|
+
true
|
|
47
|
+
when /^P2WSH(-over-P2SH)?$/
|
|
48
|
+
true
|
|
49
|
+
when /^WITNESS_V(\d)$/
|
|
50
|
+
true
|
|
51
|
+
else
|
|
52
|
+
false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.finalizeTransaction(tx, inputs, signatures, input_address_data)
|
|
58
|
+
# append signatures to the transaction and return its hexadecimal representation
|
|
59
|
+
|
|
60
|
+
inputs.each do |input|
|
|
61
|
+
# for each input
|
|
62
|
+
|
|
63
|
+
signatures_present = signatures.map{|x| x if x['input_index'] == input['input_index']}.compact.inject({}){|h,v| h[v['public_key']] = v['signature']; h}
|
|
64
|
+
address_data = input_address_data.detect{|x| x['address'] == input['spending_address']} # contains public keys (ordered) and the address type
|
|
65
|
+
input_index = input['input_index']
|
|
66
|
+
is_segwit = isSegwitAddressType?(address_data['address_type'])
|
|
67
|
+
script_stack = (is_segwit ? tx.in[input_index].script_witness.stack : tx.in[input_index].script_sig)
|
|
68
|
+
|
|
69
|
+
if ['P2PKH', 'P2WPKH', 'P2WPKH-over-P2SH'].include?(address_data['address_type']) then
|
|
70
|
+
# P2PKH will use script_sig as script_stack
|
|
71
|
+
# P2WPKH input, or P2WPKH-over-P2SH input will use script_witness.stack as script_stack
|
|
72
|
+
|
|
73
|
+
current_public_key = address_data['public_keys'][0]
|
|
74
|
+
current_signature = signatures_present[current_public_key]
|
|
75
|
+
|
|
76
|
+
# no blank push necessary for P2PKH, P2WPKH, P2WPKH-over-P2SH
|
|
77
|
+
script_stack << ([current_signature].pack("H*") + [Bitcoin::SIGHASH_TYPE[:all]].pack('C'))
|
|
78
|
+
script_stack << [current_public_key].pack("H*")
|
|
79
|
+
|
|
80
|
+
# P2WPKH-over-P2SH required script_sig still
|
|
81
|
+
tx.in[input_index].script_sig << (
|
|
82
|
+
Bitcoin::Script.to_p2wpkh(
|
|
83
|
+
Bitcoin::Key.new(:pubkey => current_public_key, :key_type => Bitcoin::Key::TYPES[:compressed]).hash160 # hash160 of the compressed pubkey
|
|
84
|
+
).to_payload
|
|
85
|
+
) if address_data['address_type'] == "P2WPKH-over-P2SH"
|
|
86
|
+
|
|
87
|
+
elsif ['P2SH', 'WITNESS_V0', 'P2WSH-over-P2SH'].include?(address_data['address_type']) then
|
|
88
|
+
# P2SH will use script_sig as script_stack
|
|
89
|
+
# P2WSH or P2WSH-over-P2SH input will use script_witness.stack as script_stack
|
|
90
|
+
|
|
91
|
+
script = Bitcoin::Script.to_p2sh_multisig_script(address_data['required_signatures'], address_data['public_keys'])
|
|
92
|
+
|
|
93
|
+
script_stack << '' # blank push for scripthash always
|
|
94
|
+
|
|
95
|
+
signatures_added = 0
|
|
96
|
+
|
|
97
|
+
address_data['public_keys'].each do |public_key|
|
|
98
|
+
next unless signatures_present.key?(public_key)
|
|
99
|
+
|
|
100
|
+
# append signatures, no sighash needed, in correct order of public keys
|
|
101
|
+
current_signature = signatures_present[public_key]
|
|
102
|
+
script_stack << ([current_signature].pack("H*") + [Bitcoin::SIGHASH_TYPE[:all]].pack('C'))
|
|
103
|
+
|
|
104
|
+
signatures_added += 1
|
|
105
|
+
|
|
106
|
+
# required signatures added? break loop and move on
|
|
107
|
+
break if signatures_added == address_data['required_signatures']
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
script_stack << script.last.to_payload
|
|
111
|
+
|
|
112
|
+
# P2WSH-over-P2SH needs script_sig populated still
|
|
113
|
+
tx.in[input_index].script_sig << Bitcoin::Script.to_p2wsh(script.last).to_payload if address_data['address_type'] == "P2WSH-over-P2SH"
|
|
114
|
+
|
|
115
|
+
else
|
|
116
|
+
raise "Unrecognized input address: #{address_data['address_type']}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
tx.to_hex
|
|
122
|
+
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def self.getSigHashForInput(tx, input_index, input_data, input_address_data)
|
|
126
|
+
# returns the sighash for the given input in bytes
|
|
127
|
+
|
|
128
|
+
address_type = input_address_data["address_type"]
|
|
129
|
+
input_value = (BigDecimal(input_data['input_value']) * BigDecimal(100000000)).to_i # in sats
|
|
130
|
+
sighash = nil
|
|
131
|
+
|
|
132
|
+
if address_type == "P2SH" then
|
|
133
|
+
# P2SH addresses
|
|
134
|
+
|
|
135
|
+
script = Bitcoin::Script.to_p2sh_multisig_script(input_address_data["required_signatures"], input_address_data["public_keys"])
|
|
136
|
+
sighash = tx.sighash_for_input(input_index, script.last)
|
|
137
|
+
|
|
138
|
+
elsif address_type == "P2WSH-over-P2SH" or address_type == "WITNESS_V0" then
|
|
139
|
+
# P2WSH-over-P2SH addresses
|
|
140
|
+
# WITNESS_V0 addresses
|
|
141
|
+
|
|
142
|
+
script = Bitcoin::Script.to_p2sh_multisig_script(input_address_data["required_signatures"], input_address_data["public_keys"])
|
|
143
|
+
sighash = tx.sighash_for_input(input_index, script.last, amount: input_value, sig_version: :witness_v0)
|
|
144
|
+
|
|
145
|
+
elsif address_type == "P2WPKH-over-P2SH" or address_type == "P2WPKH" then
|
|
146
|
+
# P2WPKH-over-P2SH addresses
|
|
147
|
+
# P2WPKH addresses
|
|
148
|
+
|
|
149
|
+
pub_key = Bitcoin::Key.new(:pubkey => input_address_data['public_keys'].first, :key_type => Bitcoin::Key::TYPES[:compressed]) # compressed
|
|
150
|
+
script = Bitcoin::Script.to_p2wpkh(pub_key.hash160)
|
|
151
|
+
sighash = tx.sighash_for_input(input_index, script, amount: input_value, sig_version: :witness_v0)
|
|
152
|
+
|
|
153
|
+
elsif address_type == "P2PKH" then
|
|
154
|
+
# P2PKH addresses
|
|
155
|
+
|
|
156
|
+
pub_key = Bitcoin::Key.new(:pubkey => input_address_data['public_keys'].first, :key_type => Bitcoin::Key::TYPES[:compressed]) # compressed
|
|
157
|
+
script = Bitcoin::Script.to_p2pkh(pub_key.hash160)
|
|
158
|
+
sighash = tx.sighash_for_input(input_index, script)
|
|
159
|
+
|
|
160
|
+
else
|
|
161
|
+
raise "Unrecognize address type: #{address_type}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
sighash
|
|
165
|
+
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def self.getDecryptionAlgorithm(user_key_algorithm = nil)
|
|
169
|
+
# mainly used so existing unit tests do not break
|
|
170
|
+
|
|
171
|
+
algorithm = ({}).merge(LEGACY_DECRYPTION_ALGORITHM)
|
|
172
|
+
|
|
173
|
+
if !user_key_algorithm.nil? then
|
|
174
|
+
algorithm[:pbkdf2_salt] = user_key_algorithm['pbkdf2_salt']
|
|
175
|
+
algorithm[:pbkdf2_iterations] = user_key_algorithm['pbkdf2_iterations']
|
|
176
|
+
algorithm[:pbkdf2_hash_function] = user_key_algorithm['pbkdf2_hash_function']
|
|
177
|
+
algorithm[:pbkdf2_phase1_key_length] = user_key_algorithm['pbkdf2_phase1_key_length']
|
|
178
|
+
algorithm[:pbkdf2_phase2_key_length] = user_key_algorithm['pbkdf2_phase2_key_length']
|
|
179
|
+
algorithm[:aes_iv] = user_key_algorithm['aes_iv']
|
|
180
|
+
algorithm[:aes_cipher] = user_key_algorithm['aes_cipher']
|
|
181
|
+
algorithm[:aes_auth_tag] = user_key_algorithm['aes_auth_tag']
|
|
182
|
+
algorithm[:aes_auth_data] = user_key_algorithm['aes_auth_data']
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
algorithm
|
|
186
|
+
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def self.dynamicExtractKey(user_key, pin)
|
|
190
|
+
# user_key object contains the encrypted user key and decryption algorithm
|
|
191
|
+
|
|
192
|
+
algorithm = self.getDecryptionAlgorithm(user_key['algorithm'])
|
|
193
|
+
|
|
194
|
+
aes_key = self.pinToAesKey(pin, algorithm[:pbkdf2_iterations],
|
|
195
|
+
algorithm[:pbkdf2_salt],
|
|
196
|
+
algorithm[:pbkdf2_hash_function],
|
|
197
|
+
algorithm[:pbkdf2_phase1_key_length],
|
|
198
|
+
algorithm[:pbkdf2_phase2_key_length])
|
|
199
|
+
|
|
200
|
+
decrypted = self.decrypt(user_key['encrypted_passphrase'], aes_key, algorithm[:aes_iv], algorithm[:aes_cipher], algorithm[:aes_auth_tag], algorithm[:aes_auth_data])
|
|
201
|
+
|
|
202
|
+
Key.from_passphrase(decrypted)
|
|
203
|
+
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def self.extractKey(encrypted_data, b64_enc_key)
|
|
207
|
+
# passphrase is in plain text
|
|
208
|
+
# encrypted_data is in base64, as it was stored on Block.io
|
|
209
|
+
# returns the private key extracted from the given encrypted data
|
|
210
|
+
|
|
211
|
+
decrypted = self.decrypt(encrypted_data, b64_enc_key)
|
|
212
|
+
|
|
213
|
+
Key.from_passphrase(decrypted)
|
|
214
|
+
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def self.sha256(value)
|
|
218
|
+
# returns the hex of the hash of the given value
|
|
219
|
+
OpenSSL::Digest::SHA256.digest(value).unpack("H*")[0]
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def self.pinToAesKey(secret_pin, iterations = 2048, salt = "", hash_function = "SHA256", pbkdf2_phase1_key_length = 16, pbkdf2_phase2_key_length = 32)
|
|
223
|
+
# converts the pincode string to PBKDF2
|
|
224
|
+
# returns a base64 version of PBKDF2 pincode
|
|
225
|
+
|
|
226
|
+
raise Exception.new("Unknown hash function specified. Are you using current version of this library?") unless hash_function == "SHA256"
|
|
227
|
+
|
|
228
|
+
part1 = OpenSSL::PKCS5.pbkdf2_hmac(
|
|
229
|
+
secret_pin,
|
|
230
|
+
salt,
|
|
231
|
+
iterations/2,
|
|
232
|
+
pbkdf2_phase1_key_length,
|
|
233
|
+
OpenSSL::Digest::SHA256.new
|
|
234
|
+
).unpack("H*")[0]
|
|
235
|
+
|
|
236
|
+
part2 = OpenSSL::PKCS5.pbkdf2_hmac(
|
|
237
|
+
part1,
|
|
238
|
+
salt,
|
|
239
|
+
iterations/2,
|
|
240
|
+
pbkdf2_phase2_key_length,
|
|
241
|
+
OpenSSL::Digest::SHA256.new
|
|
242
|
+
) # binary
|
|
243
|
+
|
|
244
|
+
[part2].pack("m0") # the base64 encryption key
|
|
245
|
+
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Decrypts a block of data (encrypted_data) given an encryption key
|
|
249
|
+
def self.decrypt(encrypted_data, b64_enc_key, iv = nil, cipher_type = "AES-256-ECB", auth_tag = nil, auth_data = nil)
|
|
250
|
+
|
|
251
|
+
raise Exception.new("Auth tag must be 16 bytes exactly.") unless auth_tag.nil? or auth_tag.size == 32
|
|
252
|
+
|
|
253
|
+
response = nil
|
|
254
|
+
|
|
255
|
+
begin
|
|
256
|
+
aes = OpenSSL::Cipher.new(cipher_type.downcase)
|
|
257
|
+
aes.decrypt
|
|
258
|
+
aes.key = b64_enc_key.unpack("m0")[0]
|
|
259
|
+
aes.iv = [iv].pack("H*") unless iv.nil?
|
|
260
|
+
aes.auth_tag = [auth_tag].pack("H*") unless auth_tag.nil?
|
|
261
|
+
aes.auth_data = [auth_data].pack("H*") unless auth_data.nil?
|
|
262
|
+
response = aes.update(encrypted_data.unpack("m0")[0]) << aes.final
|
|
263
|
+
rescue Exception => e
|
|
264
|
+
# decryption failed, must be an invalid Secret PIN
|
|
265
|
+
raise Exception.new("Invalid Secret PIN provided.")
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
response
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Encrypts a block of data given an encryption key
|
|
272
|
+
def self.encrypt(data, b64_enc_key, iv = nil, cipher_type = "AES-256-ECB", auth_data = nil)
|
|
273
|
+
aes = OpenSSL::Cipher.new(cipher_type.downcase)
|
|
274
|
+
aes.encrypt
|
|
275
|
+
aes.key = b64_enc_key.unpack("m0")[0]
|
|
276
|
+
aes.iv = [iv].pack("H*") unless iv.nil?
|
|
277
|
+
aes.auth_data = [auth_data].pack("H*") unless auth_data.nil?
|
|
278
|
+
result = [aes.update(data) << aes.final].pack("m0")
|
|
279
|
+
auth_tag = (cipher_type.end_with?("-GCM") ? aes.auth_tag.unpack("H*")[0] : nil)
|
|
280
|
+
|
|
281
|
+
{:aes_auth_tag => auth_tag, :aes_cipher_text => result, :aes_iv => iv, :aes_cipher => cipher_type, :aes_auth_data => auth_data}
|
|
282
|
+
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# courtesy bitcoin-ruby
|
|
286
|
+
|
|
287
|
+
def self.int_to_base58(int_val, leading_zero_bytes=0)
|
|
288
|
+
alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
|
289
|
+
base58_val, base = "", alpha.size
|
|
290
|
+
while int_val > 0
|
|
291
|
+
int_val, remainder = int_val.divmod(base)
|
|
292
|
+
base58_val = alpha[remainder] << base58_val
|
|
293
|
+
end
|
|
294
|
+
base58_val
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def self.base58_to_int(base58_val)
|
|
298
|
+
alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
|
299
|
+
int_val, base = 0, alpha.size
|
|
300
|
+
base58_val.reverse.each_char.with_index do |char,index|
|
|
301
|
+
raise ArgumentError, "Value not a valid Base58 String." unless char_index = alpha.index(char)
|
|
302
|
+
int_val += char_index*(base**index)
|
|
303
|
+
end
|
|
304
|
+
int_val
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def self.encode_base58(hex)
|
|
308
|
+
leading_zero_bytes = (hex.match(/^([0]+)/) ? $1 : "").size / 2
|
|
309
|
+
("1"*leading_zero_bytes) << Helper.int_to_base58( hex.to_i(16) )
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def self.decode_base58(base58_val)
|
|
313
|
+
s = Helper.base58_to_int(base58_val).to_s(16)
|
|
314
|
+
s = (s.bytesize.odd? ? ("0" << s) : s)
|
|
315
|
+
s = "" if s == "00"
|
|
316
|
+
leading_zero_bytes = (base58_val.match(/^([1]+)/) ? $1 : "").size
|
|
317
|
+
s = ("00"*leading_zero_bytes) << s if leading_zero_bytes > 0
|
|
318
|
+
s
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
end
|