block_io 1.0.5 → 2.0.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 +5 -5
- data/.appveyor.yml +26 -0
- data/.gitignore +4 -1
- data/.rspec +1 -0
- data/.travis.yml +14 -0
- data/README.md +17 -7
- data/block_io.gemspec +9 -8
- data/examples/basic.rb +17 -13
- data/examples/dtrust.rb +37 -38
- data/examples/max_withdrawal.rb +29 -0
- data/examples/proxy.rb +36 -0
- data/examples/sweeper.rb +29 -0
- data/lib/block_io.rb +15 -302
- data/lib/block_io/client.rb +179 -0
- data/lib/block_io/constants.rb +10 -0
- data/lib/block_io/helper.rb +164 -0
- data/lib/block_io/key.rb +151 -0
- data/lib/block_io/version.rb +1 -1
- data/spec/client_spec.rb +223 -0
- data/spec/data/sign_and_finalize_dtrust_withdrawal_request.json +1 -0
- data/spec/data/sign_and_finalize_sweep_request.json +1 -0
- data/spec/data/sign_and_finalize_withdrawal_request.json +4 -0
- data/spec/data/sweep_from_address_response.json +1 -0
- data/spec/data/withdraw_from_dtrust_address_response.json +1 -0
- data/spec/data/withdraw_response.json +1227 -0
- data/spec/helper_spec.rb +44 -0
- data/spec/key_spec.rb +61 -0
- data/spec/rfc6979_spec.rb +59 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/withdraw_spec.rb +90 -0
- metadata +103 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: eb3eee8a4c13ad41febe0e5f07271c0e9d8cab44cfc5540d898aa8802c89e334
|
4
|
+
data.tar.gz: 52c0524074d9ea5da705be0190b613c56e10b1f52f5bdd10bbac163a415ba2e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8b85df794d052257b15add0e7a298b6b3482f04eaa67db0d4fe8b944898fd8a9e00451a2f4f6b351f867a8e33db7694421b2a419102941e41b9543a0a5ba2048
|
7
|
+
data.tar.gz: b58bb5628274ac2609a6103e68ba0fe5b829705231ee4264126521703907b83e46da97b6ebc9a8698c5d3ff39c11685011fcd4aa6e3d287fdb5ead9e0553054d
|
data/.appveyor.yml
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
version: '{build}'
|
2
|
+
|
3
|
+
skip_tags: true
|
4
|
+
|
5
|
+
environment:
|
6
|
+
matrix:
|
7
|
+
- ruby_version: "23"
|
8
|
+
- ruby_version: "23-x64"
|
9
|
+
- ruby_version: "24"
|
10
|
+
- ruby_version: "24-x64"
|
11
|
+
- ruby_version: "25"
|
12
|
+
- ruby_version: "25-x64"
|
13
|
+
- ruby_version: "26"
|
14
|
+
- ruby_version: "26-x64"
|
15
|
+
- ruby_version: "27"
|
16
|
+
- ruby_version: "27-x64"
|
17
|
+
|
18
|
+
install:
|
19
|
+
- SET PATH=C:\Ruby%ruby_version%\bin;%PATH%
|
20
|
+
- gem install bundler --no-document -v 2.1.4
|
21
|
+
- bundle install --retry=3
|
22
|
+
|
23
|
+
test_script:
|
24
|
+
- bundle exec rspec
|
25
|
+
|
26
|
+
build: off
|
data/.gitignore
CHANGED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -10,14 +10,19 @@ Add this line to your application's Gemfile:
|
|
10
10
|
|
11
11
|
And then execute:
|
12
12
|
|
13
|
-
$ bundle
|
13
|
+
$ bundle install
|
14
14
|
|
15
15
|
Or install it yourself as:
|
16
16
|
|
17
|
-
$ gem install block_io -v=
|
17
|
+
$ gem install block_io -v=2.0.0
|
18
18
|
|
19
19
|
## Changelog
|
20
20
|
|
21
|
+
*07/02/20*: BREAKING CHANGES. Version 2.0.0. Remove support for Ruby < 2.3.0. Behavior and interfaces have changed. By upgrading you'll need to revise your code and tests.
|
22
|
+
*05/10/19*: Prevent inadvertent passing of PINs (user error).
|
23
|
+
*06/25/18*: Remove support for Ruby < 1.9.3 (OpenSSL::Cipher::Cipher). Remove connection_pool dependency.
|
24
|
+
*01/21/15*: Added ability to sweep coins from one address to another.
|
25
|
+
*11/04/14*: Fix issue with nil parameters in an API call.
|
21
26
|
*11/03/14*: Reduce dependence on OpenSSL. PBKDF2 function is now Ruby-based. Should work well with Heroku's libraries.
|
22
27
|
*10/18/14*: Now using deterministic signatures (RFC6979), and BIP62 to hinder transaction malleability.
|
23
28
|
|
@@ -27,14 +32,19 @@ Or install it yourself as:
|
|
27
32
|
It's super easy to get started. In your Ruby shell ($ irb), for example, do this:
|
28
33
|
|
29
34
|
require 'block_io'
|
30
|
-
BlockIo.
|
31
|
-
|
35
|
+
blockio = BlockIo::Client.new(:api_key => "API KEY", :pin => "SECRET PIN")
|
36
|
+
|
37
|
+
If you do not have your PIN, or just wish to use your private key backup directly, do this:
|
38
|
+
|
39
|
+
blockio = BlockIo::Client.new(:api_key => "API KEY", :keys => [BlockIo::Key.from_wif("PRIVATE KEY BACKUP")])
|
40
|
+
|
32
41
|
And you're good to go:
|
33
42
|
|
34
|
-
|
35
|
-
|
43
|
+
blockio.get_new_address
|
44
|
+
blockio.get_my_addresses
|
36
45
|
|
37
|
-
For
|
46
|
+
For other initialization options/parameters, see `lib/block_io/client.rb`.
|
47
|
+
For more information, see https://block.io/api/simple/ruby.
|
38
48
|
|
39
49
|
## Contributing
|
40
50
|
|
data/block_io.gemspec
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
# coding: utf-8
|
2
1
|
lib = File.expand_path('../lib', __FILE__)
|
3
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
3
|
require 'block_io/version'
|
@@ -16,13 +15,15 @@ Gem::Specification.new do |spec|
|
|
16
15
|
spec.files = `git ls-files -z`.split("\x0")
|
17
16
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.required_ruby_version = '>= 2.3.0'
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
spec.add_development_dependency "bundler", "
|
22
|
-
spec.add_development_dependency "rake", "~>
|
23
|
-
spec.
|
24
|
-
spec.
|
25
|
-
spec.add_runtime_dependency "
|
26
|
-
spec.add_runtime_dependency "
|
27
|
-
spec.add_runtime_dependency "
|
21
|
+
spec.add_development_dependency "bundler", ">= 1.16", "< 3.0"
|
22
|
+
spec.add_development_dependency "rake", "~> 12.3", ">= 12.3.3"
|
23
|
+
spec.add_development_dependency "rspec", "~> 3.6", ">= 3.6"
|
24
|
+
spec.add_development_dependency "webmock", "~> 3.8", "< 4.0"
|
25
|
+
spec.add_runtime_dependency "ecdsa", "~> 1.2.0", ">= 1.2.0"
|
26
|
+
spec.add_runtime_dependency "http", "~> 4.4.1", ">= 4.4.1"
|
27
|
+
spec.add_runtime_dependency "oj", "~> 3.10.6", ">= 3.10"
|
28
|
+
spec.add_runtime_dependency "connection_pool", "~> 2.2.3", ">= 2.2"
|
28
29
|
end
|
data/examples/basic.rb
CHANGED
@@ -1,22 +1,26 @@
|
|
1
|
-
# creates a new destination address, withdraws from the default label to it, gets sent transactions, and the current price
|
1
|
+
# creates a new destination address, withdraws from the default label to it, gets updated address balances, gets sent/received transactions, and the current price
|
2
|
+
#
|
3
|
+
# basic example: $ API_KEY=TESTNET_API_KEY PIN=YOUR_SECRET_PIN ruby basic.rb
|
4
|
+
# bundler example: $ API_KEY=TESTNET_API_KEY PIN=YOUR_SECRET_PIN bundle exec ruby basic.rb
|
5
|
+
#
|
6
|
+
# adjust amount below if not using the Dogecoin Testnet
|
2
7
|
|
3
8
|
require 'block_io'
|
4
9
|
|
5
|
-
|
6
|
-
puts
|
10
|
+
blockio = BlockIo::Client.new(:api_key => ENV['API_KEY'], :pin => ENV['PIN'], :version => 2)
|
11
|
+
puts blockio.get_balance
|
12
|
+
puts blockio.network
|
7
13
|
|
8
|
-
|
9
|
-
|
10
|
-
rescue Exception => e
|
11
|
-
# if this failed, we probably created testDest label before
|
12
|
-
puts e.to_s
|
13
|
-
end
|
14
|
+
# create the address if it doesn't exist
|
15
|
+
puts blockio.get_new_address(:label => 'testDest')
|
14
16
|
|
15
|
-
puts
|
17
|
+
puts blockio.withdraw_from_labels(:from_labels => 'default', :to_label => 'testDest', :amount => '2.5')
|
16
18
|
|
17
|
-
puts
|
19
|
+
puts blockio.get_address_balance(:labels => 'default,testDest')
|
18
20
|
|
19
|
-
puts
|
21
|
+
puts blockio.get_transactions(:type => 'sent')
|
20
22
|
|
21
|
-
puts
|
23
|
+
puts blockio.get_transactions(:type => 'received')
|
24
|
+
|
25
|
+
puts blockio.get_current_price(:base_price => 'BTC')
|
22
26
|
|
data/examples/dtrust.rb
CHANGED
@@ -1,31 +1,47 @@
|
|
1
1
|
# creates a new destination address, withdraws from the default label to it, gets sent transactions, and the current price
|
2
2
|
|
3
3
|
require 'block_io'
|
4
|
+
require 'json'
|
4
5
|
|
5
|
-
# please use the
|
6
|
+
# please use the Litecoin Testnet API key here
|
6
7
|
puts "*** Initialize BlockIo library: "
|
7
|
-
|
8
|
+
blockio = BlockIo::Client.new(:api_key => ENV['API_KEY'], :pin => ENV['PIN'], :version => 2)
|
9
|
+
puts blockio.get_dtrust_balance
|
10
|
+
puts blockio.network
|
8
11
|
|
12
|
+
raise "Please use the LTCTEST network API Key here or modify this script for another network." unless blockio.network == "LTCTEST"
|
9
13
|
|
10
14
|
# create 4 keys
|
11
|
-
keys =
|
15
|
+
# you will generate your own private keys, for instance: key = BlockIo::Key.new. Just note down key.public_key and key.private_key somewhere safe before you use your keys to generate dTrust addresses.
|
16
|
+
# if you already have hex private keys, load the keys with BlockIo::Key.new(private_key_hex). Ensure the key's .public_key matches what you expect.
|
17
|
+
# WARNING: The keys below are just for demonstration, DO NOT use them on mainnets, DO NOT use insecurely generated keys
|
18
|
+
keys = [
|
19
|
+
BlockIo::Key.new("b515fd806a662e061b488e78e5d0c2ff46df80083a79818e166300666385c0a2"), # alpha1alpha2alpha3alpha4
|
20
|
+
BlockIo::Key.new("1584b821c62ecdc554e185222591720d6fe651ed1b820d83f92cdc45c5e21f"), # alpha2alpha3alpha4alpha1
|
21
|
+
BlockIo::Key.new("2f9090b8aa4ddb32c3b0b8371db1b50e19084c720c30db1d6bb9fcd3a0f78e61"), # alpha3alpha4alpha1alpha2
|
22
|
+
BlockIo::Key.new("6c1cefdfd9187b36b36c3698c1362642083dcc1941dc76d751481d3aa29ca65") # alpha4alpha1alpha2alpha3
|
23
|
+
].freeze
|
12
24
|
|
13
25
|
dtrust_address = nil
|
26
|
+
dtrust_address_label = "dTrust1_witness_v0"
|
14
27
|
|
15
28
|
begin
|
16
29
|
# let's create a new address with all 4 keys as signers, but only 3 signers required (i.e., 4 of 5 multisig, with 1 signature being Block.io)
|
30
|
+
# you will need all 4 of your keys to use your address without interacting with Block.io
|
17
31
|
|
18
|
-
signers =
|
19
|
-
keys.each { |key| signers += ',' if signers.length > 0; signers += key.public_key; }
|
32
|
+
signers = keys.map{|k| k.public_key}.join(',')
|
20
33
|
|
21
|
-
response =
|
34
|
+
response = blockio.get_new_dtrust_address(:label => dtrust_address_label, :public_keys => signers, :required_signatures => 3, :address_type => "witness_v0")
|
22
35
|
|
23
36
|
dtrust_address = response['data']['address']
|
37
|
+
|
38
|
+
raise response["data"]["error_message"] unless response["status"].eql?("success")
|
39
|
+
|
24
40
|
rescue Exception => e
|
25
41
|
# if this failed, we probably created the same label before. let's fetch the address then.
|
26
42
|
puts e.to_s
|
27
43
|
|
28
|
-
response =
|
44
|
+
response = blockio.get_dtrust_address_by_label(:label => dtrust_address_label)
|
29
45
|
|
30
46
|
dtrust_address = response['data']['address']
|
31
47
|
end
|
@@ -33,57 +49,40 @@ end
|
|
33
49
|
puts "*** Our dTrust Address: #{dtrust_address}"
|
34
50
|
|
35
51
|
# let's deposit some coins into this new address
|
36
|
-
response =
|
52
|
+
response = blockio.withdraw_from_labels(:from_labels => 'default', :to_address => dtrust_address, :amount => '0.001')
|
37
53
|
|
38
54
|
puts "*** Withdrawal response:"
|
39
55
|
puts JSON.pretty_generate(response)
|
40
56
|
|
41
57
|
|
42
58
|
# fetch the dtrust address' balance
|
43
|
-
puts "***
|
44
|
-
puts JSON.pretty_generate(
|
59
|
+
puts "*** dtrust_address_label Balance:"
|
60
|
+
puts JSON.pretty_generate(blockio.get_dtrust_address_balance(:label => dtrust_address_label))
|
45
61
|
|
46
|
-
# withdraw a few coins from
|
47
|
-
normal_address =
|
62
|
+
# withdraw a few coins from dtrust_address_label to the default label
|
63
|
+
normal_address = blockio.get_address_by_label(:label => 'default')['data']['address']
|
48
64
|
|
49
|
-
puts "*** Withdrawing from
|
65
|
+
puts "*** Withdrawing from dtrust_address_label to the 'default' label in normal multisig"
|
50
66
|
|
51
|
-
response =
|
67
|
+
response = blockio.withdraw_from_dtrust_address(:from_labels => dtrust_address_label, :to_addresses => normal_address, :amounts => '0.0009')
|
52
68
|
|
53
69
|
puts JSON.pretty_generate(response)
|
54
70
|
|
55
71
|
# let's sign for the public keys specified
|
72
|
+
signatures_added = BlockIo::Helper.signData(response["data"]["inputs"], keys)
|
56
73
|
|
57
|
-
|
58
|
-
# for each input
|
59
|
-
|
60
|
-
data_to_sign = input['data_to_sign']
|
61
|
-
|
62
|
-
input['signers'].each do |signer|
|
63
|
-
|
64
|
-
# figure out if we have the public key that matches this signer
|
65
|
-
|
66
|
-
keys.each do |key|
|
67
|
-
# iterate over all keys till we've found the one that we need
|
68
|
-
|
69
|
-
signer['signed_data'] = key.sign(data_to_sign) if key.public_key == signer['signer_public_key']
|
70
|
-
|
71
|
-
end
|
72
|
-
|
73
|
-
end
|
74
|
-
|
75
|
-
end
|
74
|
+
puts "*** Signatures added? #{signatures_added}"
|
76
75
|
|
77
|
-
puts "*** Our signed
|
78
|
-
puts JSON.pretty_generate(response['data'])
|
76
|
+
puts "*** Our (signed) request:"
|
77
|
+
puts JSON.pretty_generate(response['data'])
|
79
78
|
|
80
79
|
# let's final the withdrawal
|
81
80
|
puts "*** Finalize withdrawal: "
|
82
|
-
puts JSON.pretty_generate(
|
81
|
+
puts JSON.pretty_generate(blockio.sign_and_finalize_withdrawal({:signature_data => response["data"]}))
|
83
82
|
|
84
83
|
# get the sent transactions for this dTrust address
|
85
84
|
|
86
|
-
puts "*** Get transactions sent by our
|
85
|
+
puts "*** Get transactions sent by our dtrust_address_label address: "
|
87
86
|
|
88
|
-
puts JSON.pretty_generate(
|
87
|
+
puts JSON.pretty_generate(blockio.get_dtrust_transactions(:type => 'sent', :labels => dtrust_address_label))
|
89
88
|
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# withdraws maximum balance to a given destination in a single transaction
|
2
|
+
# you will need to repeat this if a single transaction does not suffice for withdrawing the entire balance
|
3
|
+
# for error handling, please check response['data']['status'] != 'success'
|
4
|
+
|
5
|
+
require 'block_io'
|
6
|
+
|
7
|
+
blockio = BlockIo::Client.new(:api_key => ENV['API_KEY'], :pin => ENV['PIN'], :version => 2)
|
8
|
+
puts blockio.get_balance
|
9
|
+
puts blockio.network
|
10
|
+
|
11
|
+
TO_ADDRESS = ENV['TO_ADDRESS']
|
12
|
+
|
13
|
+
raise "must specify a TO_ADDRESS" unless TO_ADDRESS.to_s.size > 0
|
14
|
+
|
15
|
+
total_balance = blockio.get_balance['data']['available_balance']
|
16
|
+
|
17
|
+
puts " -- total balance: #{total_balance} #{blockio.network}"
|
18
|
+
|
19
|
+
while true do
|
20
|
+
response = blockio.withdraw(:to_address => TO_ADDRESS, :amount => total_balance)
|
21
|
+
maximum_withdrawable_balance = response['data']['max_withdrawal_available']
|
22
|
+
break if BigDecimal(maximum_withdrawable_balance).zero?
|
23
|
+
puts blockio.withdraw(:to_address => TO_ADDRESS, :amount => maximum_withdrawable_balance)
|
24
|
+
end
|
25
|
+
|
26
|
+
final_balance = blockio.get_balance['data']['available_balance']
|
27
|
+
|
28
|
+
puts " -- final balance: #{final_balance} #{blockio.network}"
|
29
|
+
|
data/examples/proxy.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# creates a new destination address, withdraws from the default label to it,
|
2
|
+
# gets updated address balances, gets sent/received transactions, and the
|
3
|
+
# current price
|
4
|
+
# ... through an https proxy (like Squid)
|
5
|
+
#
|
6
|
+
# Unauthenticated:
|
7
|
+
# $ PROXY_HOST=localhost PROXY_PORT=3128 API_KEY=TESTNET_API_KEY PIN=YOUR_SECRET_PIN ruby proxy.rb
|
8
|
+
#
|
9
|
+
# Authenticated:
|
10
|
+
# $ PROXY_HOST=localhost PROXY_PORT=3128 PROXY_USER=user PROXY_PASS=pass API_KEY=TESTNET_API_KEY PIN=YOUR_SECRET_PIN ruby proxy.rb
|
11
|
+
#
|
12
|
+
# adjust amount below if not using the Dogecoin Testnet
|
13
|
+
|
14
|
+
require '../lib/block_io'
|
15
|
+
|
16
|
+
blockio = BlockIo::Client.new(:api_key => ENV['API_KEY'], :pin => ENV['PIN'], :version => 2, :proxy => {
|
17
|
+
:hostname => ENV['PROXY_HOST'],
|
18
|
+
:port => ENV['PROXY_PORT'],
|
19
|
+
:username => ENV['PROXY_USER'],
|
20
|
+
:password => ENV['PROXY_PASS']
|
21
|
+
})
|
22
|
+
puts blockio.get_balance
|
23
|
+
puts blockio.network
|
24
|
+
|
25
|
+
# create the address if it doesn't exist
|
26
|
+
puts blockio.get_new_address(:label => 'testDest')
|
27
|
+
|
28
|
+
puts blockio.withdraw_from_labels(:from_labels => 'default', :to_label => 'testDest', :amount => '2.5')
|
29
|
+
|
30
|
+
puts blockio.get_address_balance(:labels => 'default,testDest')
|
31
|
+
|
32
|
+
puts blockio.get_transactions(:type => 'sent')
|
33
|
+
|
34
|
+
puts blockio.get_transactions(:type => 'received')
|
35
|
+
|
36
|
+
puts blockio.get_current_price(:base_price => 'BTC')
|
data/examples/sweeper.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Example script for sweeping all coins from a given address
|
2
|
+
# Must use the API Key for the Network the address belongs to
|
3
|
+
# Must also provide the Private Key to the sweep address in Wallet Import Format (WIF)
|
4
|
+
#
|
5
|
+
# Contact support@block.io if you have any issues
|
6
|
+
|
7
|
+
require "block_io"
|
8
|
+
|
9
|
+
blockio = BlockIo::Client.new(:api_key => ENV['API_KEY'], :version => 2)
|
10
|
+
puts blockio.get_balance
|
11
|
+
puts blockio.network
|
12
|
+
|
13
|
+
to_address = ENV['TO_ADDRESS'] # sweep coins into this address
|
14
|
+
|
15
|
+
from_address = ENV['FROM_ADDRESS'] # sweep coins from this address
|
16
|
+
private_key = ENV['PRIVATE_KEY'] # private key for from_address
|
17
|
+
|
18
|
+
begin
|
19
|
+
response = blockio.sweep_from_address(:to_address => to_address, :private_key => private_key, :from_address => from_address)
|
20
|
+
|
21
|
+
raise response["data"]["error_message"] unless response["status"].eql?("success")
|
22
|
+
|
23
|
+
puts "Sweep Complete: #{response['data']['amount_sent']} #{response['data']['network']} swept from #{from_address} to #{to_address}."
|
24
|
+
puts "Transaction ID: #{response['data']['txid']}"
|
25
|
+
puts "Network Fee Incurred: #{response['data']['network_fee']} #{response['data']['network']}"
|
26
|
+
|
27
|
+
rescue Exception => e
|
28
|
+
puts "Sweep failed: #{e}"
|
29
|
+
end
|
data/lib/block_io.rb
CHANGED
@@ -1,309 +1,22 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
1
|
+
require "http"
|
2
|
+
require "oj"
|
3
|
+
require "ecdsa"
|
4
|
+
require "openssl"
|
5
|
+
require "securerandom"
|
6
|
+
require "connection_pool"
|
7
|
+
|
8
|
+
require_relative "block_io/version"
|
9
|
+
require_relative "block_io/constants"
|
10
|
+
require_relative "block_io/helper"
|
11
|
+
require_relative "block_io/key"
|
12
|
+
require_relative "block_io/client"
|
11
13
|
|
12
14
|
module BlockIo
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
-
@pin = nil
|
17
|
-
@encryptionKey = nil
|
18
|
-
@conn_pool = nil
|
19
|
-
@version = nil
|
20
|
-
|
21
|
-
def self.set_options(args = {})
|
22
|
-
# initialize BlockIo
|
23
|
-
@api_key = args[:api_key]
|
24
|
-
@pin = args[:pin]
|
25
|
-
@encryptionKey = Helper.pinToAesKey(@pin) if !@pin.nil?
|
26
|
-
|
27
|
-
@conn_pool = ConnectionPool.new(size: 5, timeout: 300) { HTTPClient.new }
|
28
|
-
|
29
|
-
@version = args[:version] || 2 # default version is 2
|
30
|
-
|
31
|
-
self.api_call(['get_balance',""])
|
32
|
-
end
|
33
|
-
|
34
|
-
def self.method_missing(m, *args, &block)
|
35
|
-
|
36
|
-
method_name = m.to_s
|
37
|
-
|
38
|
-
if ['withdraw', 'withdraw_from_address', 'withdraw_from_addresses', 'withdraw_from_user', 'withdraw_from_users', 'withdraw_from_label', 'withdraw_from_labels'].include?(m.to_s) then
|
39
|
-
|
40
|
-
self.withdraw(args.first, m.to_s)
|
41
|
-
|
42
|
-
else
|
43
|
-
params = get_params(args.first)
|
44
|
-
self.api_call([method_name, params])
|
45
|
-
end
|
46
|
-
|
47
|
-
end
|
48
|
-
|
49
|
-
def self.withdraw(args = {}, method_name = 'withdraw')
|
50
|
-
# validate arguments for withdrawal of funds TODO
|
51
|
-
|
52
|
-
raise Exception.new("PIN not set. Use BlockIo.set_options(:api_key=>'API KEY',:pin=>'SECRET PIN',:version=>'API VERSION')") if @pin.nil?
|
53
|
-
|
54
|
-
params = get_params(args)
|
55
|
-
|
56
|
-
params += "&pin=#{@pin}" if @version == 1 # Block.io handles the Secret PIN in the legacy API (v1)
|
57
|
-
|
58
|
-
response = self.api_call([method_name, params])
|
59
|
-
|
60
|
-
if response['data'].has_key?('reference_id') then
|
61
|
-
# Block.io's asking us to provide some client-side signatures, let's get to it
|
62
|
-
|
63
|
-
# extract the passphrase
|
64
|
-
encrypted_passphrase = response['data']['encrypted_passphrase']['passphrase']
|
65
|
-
|
66
|
-
# let's get our private key
|
67
|
-
key = Helper.extractKey(encrypted_passphrase, @encryptionKey)
|
68
|
-
|
69
|
-
raise Exception.new('Public key mismatch for requested signer and ourselves. Invalid Secret PIN detected.') if key.public_key != response['data']['encrypted_passphrase']['signer_public_key']
|
70
|
-
|
71
|
-
# let's sign all the inputs we can
|
72
|
-
inputs = response['data']['inputs']
|
73
|
-
|
74
|
-
inputs.each do |input|
|
75
|
-
# iterate over all signers
|
76
|
-
|
77
|
-
input['signers'].each do |signer|
|
78
|
-
# if our public key matches this signer's public key, sign the data
|
79
|
-
|
80
|
-
signer['signed_data'] = key.sign(input['data_to_sign']) if signer['signer_public_key'] == key.public_key
|
81
|
-
|
82
|
-
end
|
83
|
-
|
84
|
-
end
|
85
|
-
|
86
|
-
# the response object is now signed, let's stringify it and finalize this withdrawal
|
87
|
-
|
88
|
-
response = self.api_call(['sign_and_finalize_withdrawal',{:signature_data => response['data'].to_json}])
|
89
|
-
|
90
|
-
# if we provided all the required signatures, this transaction went through
|
91
|
-
# otherwise Block.io responded with data asking for more signatures
|
92
|
-
# the latter will be the case for dTrust addresses
|
93
|
-
end
|
94
|
-
|
95
|
-
return response
|
96
|
-
|
97
|
-
end
|
98
|
-
|
99
|
-
|
100
|
-
private
|
101
|
-
|
102
|
-
def self.api_call(endpoint)
|
103
|
-
|
104
|
-
body = nil
|
105
|
-
|
106
|
-
@conn_pool.with do |hc|
|
107
|
-
# prevent initiation of HTTPClients every time we make this call, use a connection_pool
|
108
|
-
|
109
|
-
hc.ssl_config.ssl_version = :TLSv1
|
110
|
-
response = hc.post("#{@base_url.gsub('API_CALL',endpoint[0]).gsub('VERSION', 'v'+@version.to_s) + @api_key}", endpoint[1])
|
111
|
-
|
112
|
-
begin
|
113
|
-
body = JSON.parse(response.body)
|
114
|
-
raise Exception.new(body['data']['error_message']) if !body['status'].eql?('success')
|
115
|
-
rescue
|
116
|
-
raise Exception.new('Unknown error occurred. Please report this.')
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
body
|
121
|
-
end
|
122
|
-
|
123
|
-
private
|
124
|
-
|
125
|
-
def self.get_params(args = {})
|
126
|
-
# construct the parameter string
|
127
|
-
params = ""
|
128
|
-
args = {} if args.nil?
|
129
|
-
|
130
|
-
args.each do |k,v|
|
131
|
-
params += '&' if params.length > 0
|
132
|
-
params += "#{k.to_s}=#{v.to_s}"
|
133
|
-
end
|
134
|
-
|
135
|
-
return params
|
136
|
-
end
|
137
|
-
|
138
|
-
public
|
139
|
-
|
140
|
-
class Key
|
141
|
-
|
142
|
-
def initialize(privkey = nil)
|
143
|
-
# the privkey must be in hex if at all provided
|
144
|
-
|
145
|
-
@group = ECDSA::Group::Secp256k1
|
146
|
-
@private_key = privkey.to_i(16) || 1 + SecureRandom.random_number(group.order - 1)
|
147
|
-
@public_key = @group.generator.multiply_by_scalar(@private_key)
|
148
|
-
|
149
|
-
end
|
150
|
-
|
151
|
-
def private_key
|
152
|
-
# returns private key in hex form
|
153
|
-
return @private_key.to_s(16)
|
154
|
-
end
|
155
|
-
|
156
|
-
def public_key
|
157
|
-
# returns the compressed form of the public key to save network fees (shorter scripts)
|
158
|
-
|
159
|
-
return ECDSA::Format::PointOctetString.encode(@public_key, compression: true).unpack("H*")[0]
|
160
|
-
end
|
161
|
-
|
162
|
-
def sign(data)
|
163
|
-
# signed the given hexadecimal string
|
164
|
-
|
165
|
-
nonce = deterministicGenerateK([data].pack("H*"), @private_key) # RFC6979
|
166
|
-
|
167
|
-
signature = ECDSA.sign(@group, @private_key, data.to_i(16), nonce)
|
168
|
-
|
169
|
-
# BIP0062 -- use lower S values only
|
170
|
-
r, s = signature.components
|
171
|
-
|
172
|
-
over_two = @group.order >> 1 # half of what it was
|
173
|
-
s = @group.order - s if (s > over_two)
|
174
|
-
|
175
|
-
signature = ECDSA::Signature.new(r, s)
|
176
|
-
|
177
|
-
# DER encode this, and return it in hex form
|
178
|
-
return ECDSA::Format::SignatureDerString.encode(signature).unpack("H*")[0]
|
179
|
-
end
|
180
|
-
|
181
|
-
def self.from_passphrase(passphrase)
|
182
|
-
# create a private+public key pair from a given passphrase
|
183
|
-
# think of this as your brain wallet. be very sure to use a sufficiently long passphrase
|
184
|
-
# if you don't want a passphrase, just use Key.new and it will generate a random key for you
|
185
|
-
|
186
|
-
raise Exception.new('Must provide passphrase at least 8 characters long.') if passphrase.nil? or passphrase.length < 8
|
187
|
-
|
188
|
-
hashed_key = Helper.sha256([passphrase].pack("H*")) # must pass bytes to sha256
|
189
|
-
|
190
|
-
return Key.new(hashed_key)
|
191
|
-
end
|
192
|
-
|
193
|
-
def isPositive(i)
|
194
|
-
sig = "!+-"[i <=> 0]
|
195
|
-
|
196
|
-
return sig.eql?("+")
|
197
|
-
end
|
198
|
-
|
199
|
-
def deterministicGenerateK(data, privkey, group = ECDSA::Group::Secp256k1)
|
200
|
-
# returns a deterministic K -- RFC6979
|
201
|
-
|
202
|
-
hash = data.bytes.to_a
|
203
|
-
|
204
|
-
x = [privkey.to_s(16)].pack("H*").bytes.to_a
|
205
|
-
|
206
|
-
k = []
|
207
|
-
32.times { k.insert(0, 0) }
|
208
|
-
|
209
|
-
v = []
|
210
|
-
32.times { v.insert(0, 1) }
|
211
|
-
|
212
|
-
# step D
|
213
|
-
k = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), k.pack("C*"), [].concat(v).concat([0]).concat(x).concat(hash).pack("C*")).bytes.to_a
|
214
|
-
|
215
|
-
# step E
|
216
|
-
v = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), k.pack("C*"), v.pack("C*")).bytes.to_a
|
217
|
-
|
218
|
-
# puts "E: " + v.pack("C*").unpack("H*")[0]
|
219
|
-
|
220
|
-
# step F
|
221
|
-
k = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), k.pack("C*"), [].concat(v).concat([1]).concat(x).concat(hash).pack("C*")).bytes.to_a
|
222
|
-
|
223
|
-
# step G
|
224
|
-
v = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), k.pack("C*"), v.pack("C*")).bytes.to_a
|
225
|
-
|
226
|
-
# step H2b (Step H1/H2a ignored)
|
227
|
-
v = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), k.pack("C*"), v.pack("C*")).bytes.to_a
|
228
|
-
|
229
|
-
h2b = v.pack("C*").unpack("H*")[0]
|
230
|
-
tNum = h2b.to_i(16)
|
231
|
-
|
232
|
-
# step H3
|
233
|
-
while (!isPositive(tNum) or tNum >= group.order) do
|
234
|
-
# k = crypto.HmacSHA256(Buffer.concat([v, new Buffer([0])]), k)
|
235
|
-
k = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), k.pack("C*"), [].concat(v).concat([0]).pack("C*")).bytes.to_a
|
236
|
-
|
237
|
-
# v = crypto.HmacSHA256(v, k)
|
238
|
-
v = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), k.pack("C*"), v.pack("C*")).bytes.to_a
|
239
|
-
|
240
|
-
# T = BigInteger.fromBuffer(v)
|
241
|
-
tNum = v.pack("C*").unpack("H*")[0].to_i(16)
|
242
|
-
end
|
243
|
-
|
244
|
-
return tNum
|
245
|
-
end
|
246
|
-
|
16
|
+
def self.version
|
17
|
+
BlockIo::VERSION
|
247
18
|
end
|
248
19
|
|
249
|
-
|
250
|
-
|
251
|
-
def self.extractKey(encrypted_data, b64_enc_key)
|
252
|
-
# passphrase is in plain text
|
253
|
-
# encrypted_data is in base64, as it was stored on Block.io
|
254
|
-
# returns the private key extracted from the given encrypted data
|
255
|
-
|
256
|
-
decrypted = self.decrypt(encrypted_data, b64_enc_key)
|
257
|
-
|
258
|
-
return Key.from_passphrase(decrypted)
|
259
|
-
end
|
260
|
-
|
261
|
-
def self.sha256(value)
|
262
|
-
# returns the hex of the hash of the given value
|
263
|
-
hash = Digest::SHA2.new(256)
|
264
|
-
hash << value
|
265
|
-
hash.hexdigest # return hex
|
266
|
-
end
|
267
|
-
|
268
|
-
def self.pinToAesKey(secret_pin, iterations = 2048)
|
269
|
-
# converts the pincode string to PBKDF2
|
270
|
-
# returns a base64 version of PBKDF2 pincode
|
271
|
-
salt = ""
|
272
|
-
|
273
|
-
# pbkdf2-ruby gem uses SHA256 as the default hash function
|
274
|
-
aes_key_bin = PBKDF2.new(:password => secret_pin, :salt => salt, :iterations => iterations/2, :key_length => 128/8).value
|
275
|
-
aes_key_bin = PBKDF2.new(:password => aes_key_bin.unpack("H*")[0], :salt => salt, :iterations => iterations/2, :key_length => 256/8).value
|
276
|
-
|
277
|
-
return Base64.strict_encode64(aes_key_bin) # the base64 encryption key
|
278
|
-
end
|
279
|
-
|
280
|
-
# Decrypts a block of data (encrypted_data) given an encryption key
|
281
|
-
def self.decrypt(encrypted_data, b64_enc_key, iv = nil, cipher_type = 'AES-256-ECB')
|
282
|
-
|
283
|
-
response = nil
|
284
|
-
|
285
|
-
begin
|
286
|
-
aes = OpenSSL::Cipher::Cipher.new(cipher_type)
|
287
|
-
aes.decrypt
|
288
|
-
aes.key = Base64.strict_decode64(b64_enc_key)
|
289
|
-
aes.iv = iv if iv != nil
|
290
|
-
response = aes.update(Base64.strict_decode64(encrypted_data)) + aes.final
|
291
|
-
rescue Exception => e
|
292
|
-
# decryption failed, must be an invalid Secret PIN
|
293
|
-
raise Exception.new('Invalid Secret PIN provided.')
|
294
|
-
end
|
20
|
+
end
|
295
21
|
|
296
|
-
return response
|
297
|
-
end
|
298
|
-
|
299
|
-
# Encrypts a block of data given an encryption key
|
300
|
-
def self.encrypt(data, b64_enc_key, iv = nil, cipher_type = 'AES-256-ECB')
|
301
|
-
aes = OpenSSL::Cipher::Cipher.new(cipher_type)
|
302
|
-
aes.encrypt
|
303
|
-
aes.key = Base64.strict_decode64(b64_enc_key)
|
304
|
-
aes.iv = iv if iv != nil
|
305
|
-
Base64.strict_encode64(aes.update(data) + aes.final)
|
306
|
-
end
|
307
|
-
end
|
308
22
|
|
309
|
-
end
|