block_io 1.2.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.appveyor.yml +26 -0
- data/.gitignore +1 -1
- data/.rspec +1 -0
- data/.travis.yml +14 -0
- data/README.md +13 -7
- data/block_io.gemspec +9 -7
- data/examples/basic.rb +10 -12
- data/examples/dtrust.rb +36 -38
- data/examples/max_withdrawal.rb +29 -0
- data/examples/proxy.rb +36 -0
- data/examples/sweeper.rb +11 -6
- data/lib/block_io.rb +15 -411
- 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 +119 -35
- data/examples/change.rb +0 -117
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
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,15 @@ 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.
|
21
22
|
*05/10/19*: Prevent inadvertent passing of PINs (user error).
|
22
23
|
*06/25/18*: Remove support for Ruby < 1.9.3 (OpenSSL::Cipher::Cipher). Remove connection_pool dependency.
|
23
24
|
*01/21/15*: Added ability to sweep coins from one address to another.
|
@@ -31,14 +32,19 @@ Or install it yourself as:
|
|
31
32
|
It's super easy to get started. In your Ruby shell ($ irb), for example, do this:
|
32
33
|
|
33
34
|
require 'block_io'
|
34
|
-
BlockIo.
|
35
|
-
|
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
|
+
|
36
41
|
And you're good to go:
|
37
42
|
|
38
|
-
|
39
|
-
|
43
|
+
blockio.get_new_address
|
44
|
+
blockio.get_my_addresses
|
40
45
|
|
41
|
-
For
|
46
|
+
For other initialization options/parameters, see `lib/block_io/client.rb`.
|
47
|
+
For more information, see https://block.io/api/simple/ruby.
|
42
48
|
|
43
49
|
## Contributing
|
44
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,12 +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 "
|
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"
|
27
29
|
end
|
data/examples/basic.rb
CHANGED
@@ -7,22 +7,20 @@
|
|
7
7
|
|
8
8
|
require 'block_io'
|
9
9
|
|
10
|
-
|
10
|
+
blockio = BlockIo::Client.new(:api_key => ENV['API_KEY'], :pin => ENV['PIN'], :version => 2)
|
11
|
+
puts blockio.get_balance
|
12
|
+
puts blockio.network
|
11
13
|
|
12
|
-
|
13
|
-
|
14
|
-
rescue Exception => e
|
15
|
-
# if this failed, we probably created testDest label before
|
16
|
-
puts e.to_s
|
17
|
-
end
|
14
|
+
# create the address if it doesn't exist
|
15
|
+
puts blockio.get_new_address(:label => 'testDest')
|
18
16
|
|
19
|
-
puts
|
17
|
+
puts blockio.withdraw_from_labels(:from_labels => 'default', :to_label => 'testDest', :amount => '2.5')
|
20
18
|
|
21
|
-
puts
|
19
|
+
puts blockio.get_address_balance(:labels => 'default,testDest')
|
22
20
|
|
23
|
-
puts
|
21
|
+
puts blockio.get_transactions(:type => 'sent')
|
24
22
|
|
25
|
-
puts
|
23
|
+
puts blockio.get_transactions(:type => 'received')
|
26
24
|
|
27
|
-
puts
|
25
|
+
puts blockio.get_current_price(:base_price => 'BTC')
|
28
26
|
|
data/examples/dtrust.rb
CHANGED
@@ -3,30 +3,45 @@
|
|
3
3
|
require 'block_io'
|
4
4
|
require 'json'
|
5
5
|
|
6
|
-
# please use the
|
6
|
+
# please use the Litecoin Testnet API key here
|
7
7
|
puts "*** Initialize BlockIo library: "
|
8
|
-
|
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
|
9
11
|
|
12
|
+
raise "Please use the LTCTEST network API Key here or modify this script for another network." unless blockio.network == "LTCTEST"
|
10
13
|
|
11
14
|
# create 4 keys
|
12
|
-
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
|
13
24
|
|
14
25
|
dtrust_address = nil
|
26
|
+
dtrust_address_label = "dTrust1_witness_v0"
|
15
27
|
|
16
28
|
begin
|
17
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
|
18
31
|
|
19
|
-
signers =
|
20
|
-
keys.each { |key| signers += ',' if signers.length > 0; signers += key.public_key; }
|
32
|
+
signers = keys.map{|k| k.public_key}.join(',')
|
21
33
|
|
22
|
-
response =
|
34
|
+
response = blockio.get_new_dtrust_address(:label => dtrust_address_label, :public_keys => signers, :required_signatures => 3, :address_type => "witness_v0")
|
23
35
|
|
24
36
|
dtrust_address = response['data']['address']
|
37
|
+
|
38
|
+
raise response["data"]["error_message"] unless response["status"].eql?("success")
|
39
|
+
|
25
40
|
rescue Exception => e
|
26
41
|
# if this failed, we probably created the same label before. let's fetch the address then.
|
27
42
|
puts e.to_s
|
28
43
|
|
29
|
-
response =
|
44
|
+
response = blockio.get_dtrust_address_by_label(:label => dtrust_address_label)
|
30
45
|
|
31
46
|
dtrust_address = response['data']['address']
|
32
47
|
end
|
@@ -34,57 +49,40 @@ end
|
|
34
49
|
puts "*** Our dTrust Address: #{dtrust_address}"
|
35
50
|
|
36
51
|
# let's deposit some coins into this new address
|
37
|
-
response =
|
52
|
+
response = blockio.withdraw_from_labels(:from_labels => 'default', :to_address => dtrust_address, :amount => '0.001')
|
38
53
|
|
39
54
|
puts "*** Withdrawal response:"
|
40
55
|
puts JSON.pretty_generate(response)
|
41
56
|
|
42
57
|
|
43
58
|
# fetch the dtrust address' balance
|
44
|
-
puts "***
|
45
|
-
puts JSON.pretty_generate(
|
59
|
+
puts "*** dtrust_address_label Balance:"
|
60
|
+
puts JSON.pretty_generate(blockio.get_dtrust_address_balance(:label => dtrust_address_label))
|
46
61
|
|
47
|
-
# withdraw a few coins from
|
48
|
-
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']
|
49
64
|
|
50
|
-
puts "*** Withdrawing from
|
65
|
+
puts "*** Withdrawing from dtrust_address_label to the 'default' label in normal multisig"
|
51
66
|
|
52
|
-
response =
|
67
|
+
response = blockio.withdraw_from_dtrust_address(:from_labels => dtrust_address_label, :to_addresses => normal_address, :amounts => '0.0009')
|
53
68
|
|
54
69
|
puts JSON.pretty_generate(response)
|
55
70
|
|
56
71
|
# let's sign for the public keys specified
|
72
|
+
signatures_added = BlockIo::Helper.signData(response["data"]["inputs"], keys)
|
57
73
|
|
58
|
-
|
59
|
-
# for each input
|
60
|
-
|
61
|
-
data_to_sign = input['data_to_sign']
|
62
|
-
|
63
|
-
input['signers'].each do |signer|
|
64
|
-
|
65
|
-
# figure out if we have the public key that matches this signer
|
66
|
-
|
67
|
-
keys.each do |key|
|
68
|
-
# iterate over all keys till we've found the one that we need
|
69
|
-
|
70
|
-
signer['signed_data'] = key.sign(data_to_sign) if key.public_key == signer['signer_public_key']
|
71
|
-
|
72
|
-
end
|
73
|
-
|
74
|
-
end
|
75
|
-
|
76
|
-
end
|
74
|
+
puts "*** Signatures added? #{signatures_added}"
|
77
75
|
|
78
|
-
puts "*** Our signed
|
79
|
-
puts JSON.pretty_generate(response['data'])
|
76
|
+
puts "*** Our (signed) request:"
|
77
|
+
puts JSON.pretty_generate(response['data'])
|
80
78
|
|
81
79
|
# let's final the withdrawal
|
82
80
|
puts "*** Finalize withdrawal: "
|
83
|
-
puts JSON.pretty_generate(
|
81
|
+
puts JSON.pretty_generate(blockio.sign_and_finalize_withdrawal({:signature_data => response["data"]}))
|
84
82
|
|
85
83
|
# get the sent transactions for this dTrust address
|
86
84
|
|
87
|
-
puts "*** Get transactions sent by our
|
85
|
+
puts "*** Get transactions sent by our dtrust_address_label address: "
|
88
86
|
|
89
|
-
puts JSON.pretty_generate(
|
87
|
+
puts JSON.pretty_generate(blockio.get_dtrust_transactions(:type => 'sent', :labels => dtrust_address_label))
|
90
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
CHANGED
@@ -6,19 +6,24 @@
|
|
6
6
|
|
7
7
|
require "block_io"
|
8
8
|
|
9
|
-
BlockIo.
|
9
|
+
blockio = BlockIo::Client.new(:api_key => ENV['API_KEY'], :version => 2)
|
10
|
+
puts blockio.get_balance
|
11
|
+
puts blockio.network
|
10
12
|
|
11
|
-
to_address = '
|
13
|
+
to_address = ENV['TO_ADDRESS'] # sweep coins into this address
|
12
14
|
|
13
|
-
from_address = '
|
14
|
-
private_key = '
|
15
|
+
from_address = ENV['FROM_ADDRESS'] # sweep coins from this address
|
16
|
+
private_key = ENV['PRIVATE_KEY'] # private key for from_address
|
15
17
|
|
16
18
|
begin
|
17
|
-
response =
|
18
|
-
|
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
|
+
|
19
23
|
puts "Sweep Complete: #{response['data']['amount_sent']} #{response['data']['network']} swept from #{from_address} to #{to_address}."
|
20
24
|
puts "Transaction ID: #{response['data']['txid']}"
|
21
25
|
puts "Network Fee Incurred: #{response['data']['network_fee']} #{response['data']['network']}"
|
26
|
+
|
22
27
|
rescue Exception => e
|
23
28
|
puts "Sweep failed: #{e}"
|
24
29
|
end
|
data/lib/block_io.rb
CHANGED
@@ -1,418 +1,22 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
|
8
|
-
|
9
|
-
|
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"
|
10
13
|
|
11
14
|
module BlockIo
|
12
15
|
|
13
|
-
|
14
|
-
|
15
|
-
@pin = nil
|
16
|
-
@encryptionKey = nil
|
17
|
-
@client = 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
|
-
|
26
|
-
@encryptionKey = Helper.pinToAesKey(@pin) if !@pin.nil?
|
27
|
-
|
28
|
-
hostname = args[:hostname] || "block.io"
|
29
|
-
@base_url = "https://" << hostname << "/api/VERSION/API_CALL/?api_key="
|
30
|
-
|
31
|
-
@client = HTTPClient.new
|
32
|
-
@client.tcp_keepalive = true
|
33
|
-
@client.ssl_config.ssl_version = :auto
|
34
|
-
|
35
|
-
@version = args[:version] || 2 # default version is 2
|
36
|
-
|
37
|
-
self.api_call(['get_balance',""])
|
38
|
-
end
|
39
|
-
|
40
|
-
def self.method_missing(m, *args, &block)
|
41
|
-
|
42
|
-
method_name = m.to_s
|
43
|
-
|
44
|
-
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
|
45
|
-
# need to withdraw from an address
|
46
|
-
self.withdraw(args.first, m.to_s)
|
47
|
-
elsif ['sweep_from_address'].include?(m.to_s) then
|
48
|
-
# need to sweep from an address
|
49
|
-
self.sweep(args.first, m.to_s)
|
50
|
-
else
|
51
|
-
params = get_params(args.first)
|
52
|
-
self.api_call([method_name, params])
|
53
|
-
end
|
54
|
-
|
55
|
-
end
|
56
|
-
|
57
|
-
def self.withdraw(args = {}, method_name = 'withdraw')
|
58
|
-
# validate arguments for withdrawal of funds TODO
|
59
|
-
|
60
|
-
raise Exception.new("PIN not set. Use BlockIo.set_options(:api_key=>'API KEY',:pin=>'SECRET PIN',:version=>'API VERSION')") if @pin.nil?
|
61
|
-
|
62
|
-
# make sure pins don't get passed inadvertently
|
63
|
-
args.delete(:pin)
|
64
|
-
args.delete('pin')
|
65
|
-
|
66
|
-
params = get_params(args)
|
67
|
-
|
68
|
-
response = self.api_call([method_name, params])
|
69
|
-
|
70
|
-
if response['data'].has_key?('reference_id') then
|
71
|
-
# Block.io's asking us to provide some client-side signatures, let's get to it
|
72
|
-
|
73
|
-
# extract the passphrase
|
74
|
-
encrypted_passphrase = response['data']['encrypted_passphrase']['passphrase']
|
75
|
-
|
76
|
-
# let's get our private key
|
77
|
-
key = Helper.extractKey(encrypted_passphrase, @encryptionKey)
|
78
|
-
|
79
|
-
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']
|
80
|
-
|
81
|
-
# let's sign all the inputs we can
|
82
|
-
inputs = response['data']['inputs']
|
83
|
-
|
84
|
-
Helper.signData(inputs, [key])
|
85
|
-
|
86
|
-
# the response object is now signed, let's stringify it and finalize this withdrawal
|
87
|
-
response = self.api_call(['sign_and_finalize_withdrawal',{:signature_data => Oj.dump(response['data'])}])
|
88
|
-
|
89
|
-
# if we provided all the required signatures, this transaction went through
|
90
|
-
# otherwise Block.io responded with data asking for more signatures
|
91
|
-
# the latter will be the case for dTrust addresses
|
92
|
-
end
|
93
|
-
|
94
|
-
return response
|
95
|
-
|
16
|
+
def self.version
|
17
|
+
BlockIo::VERSION
|
96
18
|
end
|
97
|
-
|
98
|
-
def self.sweep(args = {}, method_name = 'sweep_from_address')
|
99
|
-
# sweep coins from a given address + key
|
100
|
-
|
101
|
-
raise Exception.new("No private_key provided.") unless args.has_key?(:private_key)
|
102
|
-
|
103
|
-
key = Key.from_wif(args[:private_key])
|
104
|
-
|
105
|
-
args[:public_key] = key.public_key # so Block.io can match things up
|
106
|
-
args.delete(:private_key) # the key must never leave this machine
|
107
|
-
|
108
|
-
params = get_params(args)
|
109
|
-
|
110
|
-
response = self.api_call([method_name, params])
|
111
|
-
|
112
|
-
if response['data'].has_key?('reference_id') then
|
113
|
-
# Block.io's asking us to provide some client-side signatures, let's get to it
|
114
|
-
|
115
|
-
# let's sign all the inputs we can
|
116
|
-
inputs = response['data']['inputs']
|
117
|
-
Helper.signData(inputs, [key])
|
118
|
-
|
119
|
-
# the response object is now signed, let's stringify it and finalize this withdrawal
|
120
|
-
response = self.api_call(['sign_and_finalize_sweep',{:signature_data => Oj.dump(response['data'])}])
|
121
|
-
|
122
|
-
# if we provided all the required signatures, this transaction went through
|
123
|
-
# otherwise Block.io responded with data asking for more signatures
|
124
|
-
# the latter will be the case for dTrust addresses
|
125
|
-
end
|
126
|
-
|
127
|
-
return response
|
128
|
-
|
129
|
-
end
|
130
|
-
|
131
|
-
|
132
|
-
private
|
133
19
|
|
134
|
-
|
135
|
-
|
136
|
-
body = nil
|
137
|
-
|
138
|
-
response = @client.post("#{@base_url.gsub('API_CALL',endpoint[0]).gsub('VERSION', 'v'+@version.to_s) + @api_key}", endpoint[1])
|
139
|
-
|
140
|
-
begin
|
141
|
-
body = Oj.load(response.body)
|
142
|
-
raise Exception.new(body['data']['error_message']) if !body['status'].eql?('success')
|
143
|
-
rescue
|
144
|
-
raise Exception.new('Unknown error occurred. Please report this.')
|
145
|
-
end
|
146
|
-
|
147
|
-
body
|
148
|
-
end
|
149
|
-
|
150
|
-
private
|
151
|
-
|
152
|
-
def self.get_params(args = {})
|
153
|
-
# construct the parameter string
|
154
|
-
params = ""
|
155
|
-
args = {} if args.nil?
|
156
|
-
|
157
|
-
args.each do |k,v|
|
158
|
-
params += '&' if params.length > 0
|
159
|
-
params += "#{k.to_s}=#{v.to_s}"
|
160
|
-
end
|
161
|
-
|
162
|
-
return params
|
163
|
-
end
|
164
|
-
|
165
|
-
public
|
166
|
-
|
167
|
-
class Key
|
168
|
-
|
169
|
-
def initialize(privkey = nil, compressed = true)
|
170
|
-
# the privkey must be in hex if at all provided
|
171
|
-
|
172
|
-
@group = ECDSA::Group::Secp256k1
|
173
|
-
@private_key = privkey.to_i(16) || 1 + SecureRandom.random_number(group.order - 1)
|
174
|
-
@public_key = @group.generator.multiply_by_scalar(@private_key)
|
175
|
-
@compressed = compressed
|
176
|
-
|
177
|
-
end
|
178
|
-
|
179
|
-
def private_key
|
180
|
-
# returns private key in hex form
|
181
|
-
return @private_key.to_s(16)
|
182
|
-
end
|
183
|
-
|
184
|
-
def public_key
|
185
|
-
# returns the compressed form of the public key to save network fees (shorter scripts)
|
186
|
-
|
187
|
-
return ECDSA::Format::PointOctetString.encode(@public_key, compression: @compressed).unpack("H*")[0]
|
188
|
-
end
|
189
|
-
|
190
|
-
def sign(data)
|
191
|
-
# signed the given hexadecimal string
|
192
|
-
|
193
|
-
nonce = deterministicGenerateK([data].pack("H*"), @private_key) # RFC6979
|
194
|
-
|
195
|
-
signature = ECDSA.sign(@group, @private_key, data.to_i(16), nonce)
|
196
|
-
|
197
|
-
# BIP0062 -- use lower S values only
|
198
|
-
r, s = signature.components
|
199
|
-
|
200
|
-
over_two = @group.order >> 1 # half of what it was
|
201
|
-
s = @group.order - s if (s > over_two)
|
202
|
-
|
203
|
-
signature = ECDSA::Signature.new(r, s)
|
204
|
-
|
205
|
-
# DER encode this, and return it in hex form
|
206
|
-
return ECDSA::Format::SignatureDerString.encode(signature).unpack("H*")[0]
|
207
|
-
end
|
208
|
-
|
209
|
-
def self.from_passphrase(passphrase)
|
210
|
-
# create a private+public key pair from a given passphrase
|
211
|
-
# think of this as your brain wallet. be very sure to use a sufficiently long passphrase
|
212
|
-
# if you don't want a passphrase, just use Key.new and it will generate a random key for you
|
213
|
-
|
214
|
-
raise Exception.new('Must provide passphrase at least 8 characters long.') if passphrase.nil? or passphrase.length < 8
|
215
|
-
|
216
|
-
hashed_key = Helper.sha256([passphrase].pack("H*")) # must pass bytes to sha256
|
217
|
-
|
218
|
-
return Key.new(hashed_key)
|
219
|
-
end
|
220
|
-
|
221
|
-
def self.from_wif(wif)
|
222
|
-
# returns a new key extracted from the Wallet Import Format provided
|
223
|
-
# TODO check against checksum
|
224
|
-
|
225
|
-
hexkey = Helper.decode_base58(wif)
|
226
|
-
actual_key = hexkey[2...66]
|
227
|
-
|
228
|
-
compressed = hexkey[2..hexkey.length].length-8 > 64 and hexkey[2..hexkey.length][64...66] == '01'
|
229
|
-
|
230
|
-
return Key.new(actual_key, compressed)
|
231
|
-
|
232
|
-
end
|
233
|
-
|
234
|
-
def isPositive(i)
|
235
|
-
sig = "!+-"[i <=> 0]
|
236
|
-
|
237
|
-
return sig.eql?("+")
|
238
|
-
end
|
239
|
-
|
240
|
-
def deterministicGenerateK(data, privkey, group = ECDSA::Group::Secp256k1)
|
241
|
-
# returns a deterministic K -- RFC6979
|
242
|
-
|
243
|
-
hash = data.bytes.to_a
|
244
|
-
|
245
|
-
x = [privkey.to_s(16)].pack("H*").bytes.to_a
|
246
|
-
|
247
|
-
k = []
|
248
|
-
32.times { k.insert(0, 0) }
|
249
|
-
|
250
|
-
v = []
|
251
|
-
32.times { v.insert(0, 1) }
|
252
|
-
|
253
|
-
# step D
|
254
|
-
k = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), k.pack("C*"), [].concat(v).concat([0]).concat(x).concat(hash).pack("C*")).bytes.to_a
|
255
|
-
|
256
|
-
# step E
|
257
|
-
v = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), k.pack("C*"), v.pack("C*")).bytes.to_a
|
258
|
-
|
259
|
-
# puts "E: " + v.pack("C*").unpack("H*")[0]
|
260
|
-
|
261
|
-
# step F
|
262
|
-
k = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), k.pack("C*"), [].concat(v).concat([1]).concat(x).concat(hash).pack("C*")).bytes.to_a
|
263
|
-
|
264
|
-
# step G
|
265
|
-
v = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), k.pack("C*"), v.pack("C*")).bytes.to_a
|
266
|
-
|
267
|
-
# step H2b (Step H1/H2a ignored)
|
268
|
-
v = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), k.pack("C*"), v.pack("C*")).bytes.to_a
|
269
|
-
|
270
|
-
h2b = v.pack("C*").unpack("H*")[0]
|
271
|
-
tNum = h2b.to_i(16)
|
272
|
-
|
273
|
-
# step H3
|
274
|
-
while (!isPositive(tNum) or tNum >= group.order) do
|
275
|
-
# k = crypto.HmacSHA256(Buffer.concat([v, new Buffer([0])]), k)
|
276
|
-
k = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), k.pack("C*"), [].concat(v).concat([0]).pack("C*")).bytes.to_a
|
277
|
-
|
278
|
-
# v = crypto.HmacSHA256(v, k)
|
279
|
-
v = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), k.pack("C*"), v.pack("C*")).bytes.to_a
|
280
|
-
|
281
|
-
# T = BigInteger.fromBuffer(v)
|
282
|
-
tNum = v.pack("C*").unpack("H*")[0].to_i(16)
|
283
|
-
end
|
284
|
-
|
285
|
-
return tNum
|
286
|
-
end
|
287
|
-
|
288
|
-
end
|
289
|
-
|
290
|
-
module Helper
|
291
|
-
|
292
|
-
def self.signData(inputs, keys)
|
293
|
-
# sign the given data with the given keys
|
294
|
-
# TODO loop is O(n^3), make it better
|
295
|
-
|
296
|
-
raise Exception.new('Keys object must be an array of keys, without at least one key inside it.') unless keys.is_a?(Array) and keys.size >= 1
|
297
|
-
|
298
|
-
i = 0
|
299
|
-
while i < inputs.size do
|
300
|
-
# iterate over all signers
|
301
|
-
input = inputs[i]
|
302
|
-
|
303
|
-
j = 0
|
304
|
-
while j < input['signers'].size do
|
305
|
-
# if our public key matches this signer's public key, sign the data
|
306
|
-
signer = inputs[i]['signers'][j]
|
307
|
-
|
308
|
-
k = 0
|
309
|
-
while k < keys.size do
|
310
|
-
# sign for each key provided, if we can
|
311
|
-
key = keys[k]
|
312
|
-
signer['signed_data'] = key.sign(input['data_to_sign']) if signer['signer_public_key'] == key.public_key
|
313
|
-
k = k + 1
|
314
|
-
end
|
315
|
-
|
316
|
-
j = j + 1
|
317
|
-
end
|
318
|
-
|
319
|
-
i = i + 1
|
320
|
-
end
|
321
|
-
|
322
|
-
inputs
|
323
|
-
end
|
324
|
-
|
325
|
-
def self.extractKey(encrypted_data, b64_enc_key)
|
326
|
-
# passphrase is in plain text
|
327
|
-
# encrypted_data is in base64, as it was stored on Block.io
|
328
|
-
# returns the private key extracted from the given encrypted data
|
329
|
-
|
330
|
-
decrypted = self.decrypt(encrypted_data, b64_enc_key)
|
331
|
-
|
332
|
-
return Key.from_passphrase(decrypted)
|
333
|
-
end
|
334
|
-
|
335
|
-
def self.sha256(value)
|
336
|
-
# returns the hex of the hash of the given value
|
337
|
-
hash = Digest::SHA2.new(256)
|
338
|
-
hash << value
|
339
|
-
hash.hexdigest # return hex
|
340
|
-
end
|
341
|
-
|
342
|
-
def self.pinToAesKey(secret_pin, iterations = 2048)
|
343
|
-
# converts the pincode string to PBKDF2
|
344
|
-
# returns a base64 version of PBKDF2 pincode
|
345
|
-
salt = ""
|
346
|
-
|
347
|
-
# pbkdf2-ruby gem uses SHA256 as the default hash function
|
348
|
-
aes_key_bin = PBKDF2.new(:password => secret_pin, :salt => salt, :iterations => iterations/2, :key_length => 128/8).value
|
349
|
-
aes_key_bin = PBKDF2.new(:password => aes_key_bin.unpack("H*")[0], :salt => salt, :iterations => iterations/2, :key_length => 256/8).value
|
350
|
-
|
351
|
-
return Base64.strict_encode64(aes_key_bin) # the base64 encryption key
|
352
|
-
end
|
353
|
-
|
354
|
-
# Decrypts a block of data (encrypted_data) given an encryption key
|
355
|
-
def self.decrypt(encrypted_data, b64_enc_key, iv = nil, cipher_type = 'AES-256-ECB')
|
356
|
-
|
357
|
-
response = nil
|
358
|
-
|
359
|
-
begin
|
360
|
-
aes = OpenSSL::Cipher.new(cipher_type)
|
361
|
-
aes.decrypt
|
362
|
-
aes.key = Base64.strict_decode64(b64_enc_key)
|
363
|
-
aes.iv = iv if iv != nil
|
364
|
-
response = aes.update(Base64.strict_decode64(encrypted_data)) + aes.final
|
365
|
-
rescue Exception => e
|
366
|
-
# decryption failed, must be an invalid Secret PIN
|
367
|
-
raise Exception.new('Invalid Secret PIN provided.')
|
368
|
-
end
|
369
|
-
|
370
|
-
return response
|
371
|
-
end
|
372
|
-
|
373
|
-
# Encrypts a block of data given an encryption key
|
374
|
-
def self.encrypt(data, b64_enc_key, iv = nil, cipher_type = 'AES-256-ECB')
|
375
|
-
aes = OpenSSL::Cipher.new(cipher_type)
|
376
|
-
aes.encrypt
|
377
|
-
aes.key = Base64.strict_decode64(b64_enc_key)
|
378
|
-
aes.iv = iv if iv != nil
|
379
|
-
Base64.strict_encode64(aes.update(data) + aes.final)
|
380
|
-
end
|
20
|
+
end
|
381
21
|
|
382
|
-
# courtesy bitcoin-ruby
|
383
|
-
|
384
|
-
def self.int_to_base58(int_val, leading_zero_bytes=0)
|
385
|
-
alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
386
|
-
base58_val, base = '', alpha.size
|
387
|
-
while int_val > 0
|
388
|
-
int_val, remainder = int_val.divmod(base)
|
389
|
-
base58_val = alpha[remainder] + base58_val
|
390
|
-
end
|
391
|
-
base58_val
|
392
|
-
end
|
393
|
-
|
394
|
-
def self.base58_to_int(base58_val)
|
395
|
-
alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
396
|
-
int_val, base = 0, alpha.size
|
397
|
-
base58_val.reverse.each_char.with_index do |char,index|
|
398
|
-
raise ArgumentError, 'Value not a valid Base58 String.' unless char_index = alpha.index(char)
|
399
|
-
int_val += char_index*(base**index)
|
400
|
-
end
|
401
|
-
int_val
|
402
|
-
end
|
403
|
-
|
404
|
-
def self.encode_base58(hex)
|
405
|
-
leading_zero_bytes = (hex.match(/^([0]+)/) ? $1 : '').size / 2
|
406
|
-
("1"*leading_zero_bytes) + Helper.int_to_base58( hex.to_i(16) )
|
407
|
-
end
|
408
|
-
|
409
|
-
def self.decode_base58(base58_val)
|
410
|
-
s = Helper.base58_to_int(base58_val).to_s(16); s = (s.bytesize.odd? ? '0'+s : s)
|
411
|
-
s = '' if s == '00'
|
412
|
-
leading_zero_bytes = (base58_val.match(/^([1]+)/) ? $1 : '').size
|
413
|
-
s = ("00"*leading_zero_bytes) + s if leading_zero_bytes > 0
|
414
|
-
s
|
415
|
-
end
|
416
|
-
end
|
417
22
|
|
418
|
-
end
|