bitpay-sdk 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+