bitpay-sdk 2.1.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.
@@ -0,0 +1,181 @@
1
+ # license Copyright 2011-2014 BitPay, Inc., MIT License
2
+ # see http://opensource.org/licenses/MIT
3
+ # or https://github.com/bitpay/php-bitpay-client/blob/master/LICENSE
4
+
5
+ require 'uri'
6
+ require 'net/https'
7
+ require 'json'
8
+
9
+ require_relative 'key_utils'
10
+
11
+ module BitPay
12
+ # This class is used to instantiate a BitPay Client object. It is expected to be thread safe.
13
+ #
14
+ class Client
15
+
16
+
17
+ # @return [Client]
18
+ # @example
19
+ # # Create a client with a pem file created by the bitpay client:
20
+ # client = BitPay::Client.new
21
+ def initialize(opts={})
22
+ @pem = opts[:pem] || ENV['BITPAY_PEM'] || KeyUtils.generate_pem
23
+ @key = KeyUtils.create_key @pem
24
+ @priv_key = KeyUtils.get_private_key @key
25
+ @pub_key = KeyUtils.get_public_key @key
26
+ @client_id = KeyUtils.generate_sin_from_pem @pem
27
+ @uri = URI.parse opts[:api_uri] || API_URI
28
+ @user_agent = opts[:user_agent] || USER_AGENT
29
+ @https = Net::HTTP.new @uri.host, @uri.port
30
+ @https.use_ssl = true
31
+ @https.ca_file = CA_FILE
32
+ @tokens = opts[:tokens] || {}
33
+
34
+ # Option to disable certificate validation in extraordinary circumstance. NOT recommended for production use
35
+ @https.verify_mode = opts[:insecure] == true ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
36
+
37
+ # Option to enable http request debugging
38
+ @https.set_debug_output($stdout) if opts[:debug] == true
39
+
40
+ end
41
+
42
+ def pair_pos_client(claimCode)
43
+ raise BitPay::ArgumentError, "pairing code is not legal" unless verify_claim_code(claimCode)
44
+ response = set_pos_token(claimCode)
45
+ get_token 'pos'
46
+ response
47
+ end
48
+
49
+ def create_invoice(price:, currency:, facade: 'pos', params:{})
50
+ raise BitPay::ArgumentError, "Illegal Argument: Price must be formatted as a float" unless ( price.is_a?(Numeric) || /^[[:digit:]]+(\.[[:digit:]]{2})?$/.match(price) )
51
+ raise BitPay::ArgumentError, "Illegal Argument: Currency is invalid." unless /^[[:upper:]]{3}$/.match(currency)
52
+ params.merge!({price: price, currency: currency})
53
+ response = send_request("POST", "invoices", facade: facade, params: params)
54
+ response["data"]
55
+ end
56
+
57
+ def get_public_invoice(id:)
58
+ request = Net::HTTP::Get.new("/invoices/#{id}")
59
+ response = process_request(request)
60
+ response["data"]
61
+ end
62
+
63
+ def set_token
64
+ end
65
+
66
+ def verify_token
67
+ server_tokens = load_tokens
68
+ @tokens.each{|key, value| return false if server_tokens[key] != value}
69
+ return true
70
+ end
71
+ ## Generates REST request to api endpoint
72
+
73
+ def send_request(verb, path, facade: 'merchant', params: {}, token: nil)
74
+ token ||= get_token(facade)
75
+
76
+ # Verb-specific logic
77
+ case verb.upcase
78
+ when "GET"
79
+ urlpath = '/' + path + '?nonce=' + KeyUtils.nonce + '&token=' + token
80
+ request = Net::HTTP::Get.new urlpath
81
+ request['X-Signature'] = KeyUtils.sign(@uri.to_s + urlpath, @priv_key)
82
+
83
+ when "PUT"
84
+
85
+ when "POST" # Requires a GUID
86
+
87
+ urlpath = '/' + path
88
+ request = Net::HTTP::Post.new urlpath
89
+ params[:token] = token
90
+ params[:nonce] = KeyUtils.nonce
91
+ params[:guid] = SecureRandom.uuid
92
+ params[:id] = @client_id
93
+ request.body = params.to_json
94
+ request['X-Signature'] = KeyUtils.sign(@uri.to_s + urlpath + request.body, @priv_key)
95
+
96
+ when "DELETE"
97
+
98
+ raise(BitPayError, "Invalid HTTP verb: #{verb.upcase}")
99
+ end
100
+
101
+ # Build request headers and submit
102
+ request['X-Identity'] = @pub_key
103
+
104
+ response = process_request(request)
105
+ end
106
+
107
+ ##### PRIVATE METHODS #####
108
+ private
109
+
110
+ ## Processes HTTP Request and returns parsed response
111
+ # Otherwise throws error
112
+ #
113
+ def process_request(request)
114
+
115
+ request['User-Agent'] = @user_agent
116
+ request['Content-Type'] = 'application/json'
117
+ request['X-BitPay-Plugin-Info'] = 'Rubylib' + VERSION
118
+
119
+ begin
120
+ response = @https.request request
121
+ rescue => error
122
+ raise BitPay::ConnectionError, "#{error.message}"
123
+ end
124
+
125
+ if response.kind_of? Net::HTTPSuccess
126
+ return JSON.parse(response.body)
127
+ elsif JSON.parse(response.body)["error"]
128
+ raise(BitPayError, "#{response.code}: #{JSON.parse(response.body)['error']}")
129
+ else
130
+ raise BitPayError, "#{response.code}: #{JSON.parse(response.body)}"
131
+ end
132
+
133
+ end
134
+
135
+ ## Requests token by appending nonce and signing URL
136
+ # Returns a hash of available tokens
137
+ #
138
+ def load_tokens
139
+
140
+ urlpath = '/tokens?nonce=' + KeyUtils.nonce
141
+
142
+ request = Net::HTTP::Get.new(urlpath)
143
+ request['x-identity'] = @pub_key
144
+ request['x-signature'] = KeyUtils.sign(@uri.to_s + urlpath, @priv_key)
145
+
146
+ response = process_request(request)
147
+
148
+ token_array = response["data"] || {}
149
+
150
+ tokens = {}
151
+ token_array.each do |t|
152
+ tokens[t.keys.first] = t.values.first
153
+ end
154
+
155
+ @tokens = tokens
156
+ return tokens
157
+
158
+ end
159
+
160
+ ## Retrieves specified token from hash, otherwise tries to refresh @tokens and retry
161
+ def set_pos_token(claim_code)
162
+ params = {pairingCode: claim_code}
163
+ urlpath = '/tokens'
164
+ request = Net::HTTP::Post.new urlpath
165
+ params[:guid] = SecureRandom.uuid
166
+ params[:id] = @client_id
167
+ request.body = params.to_json
168
+ process_request(request)
169
+ end
170
+
171
+ def get_token(facade)
172
+ token = @tokens[facade] || load_tokens[facade] || raise(BitPayError, "Not authorized for facade: #{facade}")
173
+ end
174
+
175
+ def verify_claim_code(claim_code)
176
+ regex = /^[[:alnum:]]{7}$/
177
+ matches = regex.match(claim_code)
178
+ !(matches.nil?)
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,130 @@
1
+ # license Copyright 2011-2014 BitPay, Inc., MIT License
2
+ # see http://opensource.org/licenses/MIT
3
+ # or https://github.com/bitpay/php-bitpay-client/blob/master/LICENSE
4
+
5
+ require 'uri'
6
+ require 'net/https'
7
+ require 'json'
8
+ require 'openssl'
9
+ require 'ecdsa'
10
+ require 'securerandom'
11
+ require 'digest/sha2'
12
+ require 'cgi'
13
+
14
+ module BitPay
15
+ class KeyUtils
16
+ class << self
17
+ def nonce
18
+ Time.now.utc.strftime('%Y%m%d%H%M%S%L')
19
+ end
20
+
21
+ ## Generates a new private key
22
+ #
23
+
24
+ def generate_pem
25
+ key = OpenSSL::PKey::EC.new("secp256k1")
26
+ key.generate_key
27
+ key.to_pem
28
+ end
29
+
30
+ def create_key pem
31
+ OpenSSL::PKey::EC.new(pem)
32
+ end
33
+
34
+ def create_new_key
35
+ key = OpenSSL::PKey::EC.new("secp256k1")
36
+ key.generate_key
37
+ key
38
+ end
39
+
40
+ ## Gets private key from ENV variable or local FS
41
+ #
42
+ def get_local_pem_file
43
+ ENV['BITPAY_PEM'] || (raise BitPayError, MISSING_KEY)
44
+ end
45
+
46
+ def get_private_key key
47
+ key.private_key.to_int.to_s(16)
48
+ end
49
+
50
+ def get_public_key key
51
+ key.public_key.group.point_conversion_form = :compressed
52
+ key.public_key.to_bn.to_s(16).downcase
53
+ end
54
+
55
+ def get_private_key_from_pem pem
56
+ raise BitPayError, "Missing key" unless pem
57
+ key = OpenSSL::PKey::EC.new(pem)
58
+ get_private_key key
59
+ end
60
+
61
+ def get_public_key_from_pem pem
62
+ raise BitPayError, MISSING_KEY unless pem
63
+ key = OpenSSL::PKey::EC.new(pem)
64
+ get_public_key key
65
+ end
66
+
67
+ def generate_sin_from_pem(pem = nil)
68
+ #http://blog.bitpay.com/2014/07/01/bitauth-for-decentralized-authentication.html
69
+ #https://en.bitcoin.it/wiki/Identity_protocol_v1
70
+
71
+ # NOTE: All Digests are calculated against the binary representation,
72
+ # hence the requirement to use [].pack("H*") to convert to binary for each step
73
+
74
+ #Generate Private Key
75
+ key = OpenSSL::PKey::EC.new(pem ||= get_local_pem_file)
76
+ key.public_key.group.point_conversion_form = :compressed
77
+ public_key = key.public_key.to_bn.to_s(2)
78
+ step_one = Digest::SHA256.hexdigest(public_key)
79
+ step_two = Digest::RMD160.hexdigest([step_one].pack("H*"))
80
+ step_three = "0F02" + step_two
81
+ step_four_a = Digest::SHA256.hexdigest([step_three].pack("H*"))
82
+ step_four = Digest::SHA256.hexdigest([step_four_a].pack("H*"))
83
+ step_five = step_four[0..7]
84
+ step_six = step_three + step_five
85
+ encode_base58(step_six)
86
+ end
87
+
88
+
89
+ ## Generate ECDSA signature
90
+ # This is the last method that requires the ecdsa gem, which we would like to replace
91
+
92
+ def sign(message, privkey)
93
+ group = ECDSA::Group::Secp256k1
94
+ digest = Digest::SHA256.digest(message)
95
+ signature = nil
96
+ while signature.nil?
97
+ temp_key = 1 + SecureRandom.random_number(group.order - 1)
98
+ signature = ECDSA.sign(group, privkey.to_i(16), digest, temp_key)
99
+ return ECDSA::Format::SignatureDerString.encode(signature).unpack("H*").first
100
+ end
101
+ end
102
+
103
+ ########## Private Class Methods ################
104
+
105
+ ## Base58 Encoding Method
106
+ #
107
+ private
108
+ def encode_base58 (data)
109
+ code_string = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
110
+ base = 58
111
+ x = data.hex
112
+ output_string = ""
113
+
114
+ while x > 0 do
115
+ remainder = x % base
116
+ x = x / base
117
+ output_string << code_string[remainder]
118
+ end
119
+
120
+ pos = 0
121
+ while data[pos,2] == "00" do
122
+ output_string << code_string[0]
123
+ pos += 2
124
+ end
125
+
126
+ output_string.reverse()
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,7 @@
1
+ # license Copyright 2011-2014 BitPay, Inc., MIT License
2
+ # see http://opensource.org/licenses/MIT
3
+ # or https://github.com/bitpay/php-bitpay-client/blob/master/LICENSE
4
+
5
+ module BitPay
6
+ VERSION = '2.1.0'
7
+ end
data/lib/harness.rb ADDED
@@ -0,0 +1,40 @@
1
+ # license Copyright 2011-2014 BitPay, Inc., MIT License
2
+ # see http://opensource.org/licenses/MIT
3
+ # or https://github.com/bitpay/php-bitpay-client/blob/master/LICENSE
4
+
5
+ require_relative 'bitpay.rb'
6
+ require_relative 'bitpay/key_utils.rb'
7
+
8
+ # Test SIN Generation class methods
9
+
10
+ # Generate SIN
11
+ ENV["PRIV_KEY"] = "16d7c3508ec59773e71ae728d29f41fcf5d1f380c379b99d68fa9f552ce3ebc3"
12
+ puts "privkey: #{ENV['PRIV_KEY']}"
13
+ puts "target SIN: TfFVQhy2hQvchv4VVG4c7j4XPa2viJ9HrR8"
14
+ puts "Derived SIN: #{BitPay::KeyUtils.get_client_id}"
15
+
16
+ puts "\n\n------------------\n\n"
17
+
18
+ uri = "https://localhost:8088"
19
+ #name = "Ridonculous.label That shouldn't work really"
20
+ name = "somethinginnocuous"
21
+ facade = "pos"
22
+ client_id = BitPay::KeyUtils.get_client_id
23
+
24
+ BitPay::KeyUtils.generate_registration_url(uri,name,facade,client_id)
25
+
26
+ puts "\n\n------------------\n\n"
27
+
28
+ #### Test Invoice Creation using directly assigned keys
29
+ ## (Ultimately pubkey and SIN should be derived)
30
+
31
+ ENV["PRIV_KEY"] = "16d7c3508ec59773e71ae728d29f41fcf5d1f380c379b99d68fa9f552ce3ebc3"
32
+ #ENV["pub_key"] = "0353a036fb495c5846f26a3727a28198da8336ae4f5aaa09e24c14a4126b5d969d"
33
+ #ENV['SIN'] = "TfFVQhy2hQvchv4VVG4c7j4XPa2viJ9HrR8"
34
+
35
+ client = BitPay::Client.new({insecure: true, debug: false})
36
+
37
+ invoice = client.post 'invoices', {:price => 10.00, :currency => 'USD'}
38
+
39
+ puts "Here's the invoice: \n" + JSON.pretty_generate(invoice)
40
+
@@ -0,0 +1,124 @@
1
+ require 'spec_helper'
2
+
3
+ def tokens
4
+ {"data" =>
5
+ [{"merchant" => "MERCHANTTOKEN"},
6
+ {"pos" =>"POSTOKEN"},
7
+ {"merchant/invoice" => "9kv7gGqZLoQ2fxbKEgfgndLoxwjp5na6VtGSH3sN7buX"}
8
+ ]
9
+ }
10
+ end
11
+
12
+ describe BitPay::Client do
13
+ let(:bitpay_client) { BitPay::Client.new({api_uri: BitPay::TEST_API_URI}) }
14
+ let(:claim_code) { "a12bc3d" }
15
+
16
+ before do
17
+ allow(BitPay::KeyUtils).to receive(:nonce).and_return('1')
18
+ stub_request(:get, /#{BitPay::TEST_API_URI}\/tokens.*/).to_return(:status => 200, :body => tokens.to_json, :headers => {})
19
+ end
20
+
21
+ describe "#initialize" do
22
+
23
+ it 'should be able to get pem file from the env' do
24
+ stub_const('ENV', {'BITPAY_PEM' => PEM})
25
+ expect {bitpay_client}.to_not raise_error
26
+ end
27
+
28
+ end
29
+
30
+ describe "#send_request" do
31
+ before do
32
+ stub_const('ENV', {'BITPAY_PEM' => PEM})
33
+ end
34
+
35
+ context "GET" do
36
+ it 'should generate a get request' do
37
+ stub_request(:get, /#{BitPay::TEST_API_URI}\/whatever.*/).to_return(:body => '{"awesome": "json"}')
38
+ bitpay_client.send_request("GET", "whatever", facade: "merchant")
39
+ expect(WebMock).to have_requested(:get, "#{BitPay::TEST_API_URI}/whatever?nonce=1&token=MERCHANTTOKEN")
40
+ end
41
+ end
42
+
43
+ context "POST" do
44
+ it 'should generate a post request' do
45
+ stub_request(:post, /#{BitPay::TEST_API_URI}.*/).to_return(:body => '{"awesome": "json"}')
46
+ bitpay_client.send_request("POST", "whatever", facade: "merchant")
47
+ expect(WebMock).to have_requested(:post, "#{BitPay::TEST_API_URI}/whatever")
48
+ end
49
+ end
50
+
51
+ end
52
+
53
+ describe "#pair_pos_client" do
54
+ before do
55
+ stub_const('ENV', {'BITPAY_PEM' => PEM})
56
+ end
57
+
58
+ it 'throws a BitPayError with the error message if the token setting fails' do
59
+ stub_request(:any, /#{BitPay::TEST_API_URI}.*/).to_return(status: 500, body: "{\n \"error\": \"Unable to create token\"\n}")
60
+ expect { bitpay_client.pair_pos_client(claim_code) }.to raise_error(BitPay::BitPayError, '500: Unable to create token')
61
+ end
62
+
63
+ it 'gracefully handles 4xx errors' do
64
+ stub_request(:any, /#{BitPay::TEST_API_URI}.*/).to_return(status: 403, body: "{\n \"error\": \"this is a 403 error\"\n}")
65
+ expect { bitpay_client.pair_pos_client(claim_code) }.to raise_error(BitPay::BitPayError, '403: this is a 403 error')
66
+ end
67
+
68
+ it 'short circuits on invalid pairing codes' do
69
+ 100.times do
70
+ claim_code = an_illegal_claim_code
71
+ expect{bitpay_client.pair_pos_client(claim_code)}.to raise_error BitPay::ArgumentError, "pairing code is not legal"
72
+ end
73
+ end
74
+ end
75
+
76
+ describe "#create_invoice" do
77
+ subject { bitpay_client }
78
+ before {stub_const('ENV', {'BITPAY_PEM' => PEM})}
79
+ it { is_expected.to respond_to(:create_invoice) }
80
+
81
+ describe "should make the call to the server to create an invoice" do
82
+ it 'allows numeric input for the price' do
83
+ stub_request(:post, /#{BitPay::TEST_API_URI}\/invoices.*/).to_return(:body => '{"data": "awesome"}')
84
+ bitpay_client.create_invoice(price: 20.00, currency: "USD")
85
+ assert_requested :post, "#{BitPay::TEST_API_URI}/invoices"
86
+ end
87
+
88
+ it 'allows string input for the price' do
89
+ stub_request(:post, /#{BitPay::TEST_API_URI}\/invoices.*/).to_return(:body => '{"data": "awesome"}')
90
+ bitpay_client.create_invoice(price: "20.00", currency: "USD")
91
+ assert_requested :post, "#{BitPay::TEST_API_URI}/invoices"
92
+ end
93
+ end
94
+
95
+ it 'should pass through the API error message from load_tokens' do
96
+ stub_request(:get, /#{BitPay::TEST_API_URI}\/tokens.*/).to_return(status: 500, body: '{"error": "load_tokens_error"}')
97
+ expect { bitpay_client.create_invoice(price: 20, currency: "USD") }.to raise_error(BitPay::BitPayError, '500: load_tokens_error')
98
+ end
99
+
100
+ it 'verifies the validity of the price argument' do
101
+ expect { bitpay_client.create_invoice(price: "3,999", currency: "USD") }.to raise_error(BitPay::ArgumentError, 'Illegal Argument: Price must be formatted as a float')
102
+ end
103
+
104
+ it 'verifies the validity of the currency argument' do
105
+ expect { bitpay_client.create_invoice(price: "3999", currency: "UASD") }.to raise_error(BitPay::ArgumentError, 'Illegal Argument: Currency is invalid.')
106
+ end
107
+ end
108
+
109
+ describe '#set_token' do
110
+ subject { bitpay_client }
111
+ before {stub_const('ENV', {'BITPAY_PEM' => PEM})}
112
+ it { is_expected.to respond_to(:set_token) }
113
+ it 'sets a token in the client' do
114
+
115
+ end
116
+ end
117
+
118
+ describe "#verify_token" do
119
+ subject { bitpay_client }
120
+ before {stub_const('ENV', {'BITPAY_PEM' => PEM})}
121
+ it { is_expected.to respond_to(:verify_token) }
122
+ end
123
+ end
124
+