bitpay-sdk 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,17 +1,12 @@
1
1
  require 'capybara/poltergeist'
2
2
  require 'pry'
3
+ require 'fileutils'
3
4
 
4
5
  require File.join File.dirname(__FILE__), '..', '..', 'lib', 'bitpay_sdk.rb'
5
6
  require_relative '../../config/constants.rb'
6
7
  require_relative '../../config/capybara.rb'
7
8
 
8
- #
9
- ## Test Variables
10
- #
11
- #PEM = "-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEICg7E4NN53YkaWuAwpoqjfAofjzKI7Jq1f532dX+0O6QoAcGBSuBBAAK\noUQDQgAEjZcNa6Kdz6GQwXcUD9iJ+t1tJZCx7hpqBuJV2/IrQBfue8jh8H7Q/4vX\nfAArmNMaGotTpjdnymWlMfszzXJhlw==\n-----END EC PRIVATE KEY-----\n"
12
- #
13
- #PUB_KEY = '038d970d6ba29dcfa190c177140fd889fadd6d2590b1ee1a6a06e255dbf22b4017'
14
- #CLIENT_ID = "TeyN4LPrXiG5t2yuSamKqP3ynVk3F52iHrX"
9
+
15
10
  module BitPay
16
11
  # Location for API Credentials
17
12
  BITPAY_CREDENTIALS_DIR = File.join(Dir.home, ".bitpay")
@@ -21,26 +16,25 @@ module BitPay
21
16
  TOKEN_FILE_PATH = File.join(BITPAY_CREDENTIALS_DIR, TOKEN_FILE)
22
17
  end
23
18
 
19
+ # Lots of sleeps in here to deal with finicky transitions and PhantomJS
24
20
  def get_claim_code_from_server
25
21
  Capybara::visit ROOT_ADDRESS
26
- if logged_in
27
- Capybara::visit "#{ROOT_ADDRESS}/home"
28
- else
29
- log_in
30
- end
31
- Capybara::click_link "My Account"
32
- Capybara::click_link "API Tokens", match: :first
33
- Capybara::find(".token-access-new-button").find(".btn").find(".icon-plus").click
34
- sleep 0.25
35
- Capybara::find_button("Add Token", match: :first).click
22
+ log_in unless logged_in
23
+ Capybara::visit DASHBOARD_URL
24
+ raise "Bad Login" unless Capybara.current_session.current_url == DASHBOARD_URL
25
+ Capybara::visit "#{ROOT_ADDRESS}/api-tokens"
26
+ Capybara::find(".token-access-new-button").find(".btn").find(".icon-plus", match: :first).trigger("click")
27
+ sleep 0.50
28
+ Capybara::find(".token-access-new-button-wrapper").find_by_id("token-new-form", visible: true).find(".btn").trigger("click")
36
29
  Capybara::find(".token-claimcode", match: :first).text
37
30
  end
38
31
 
39
32
  def log_in
40
- Capybara::click_link('Login')
33
+ Capybara::visit "#{ROOT_ADDRESS}/dashboard/login/"
41
34
  Capybara::fill_in 'email', :with => TEST_USER
42
35
  Capybara::fill_in 'password', :with => TEST_PASS
43
- Capybara::click_button('loginButton')
36
+ Capybara::click_on('Login')
37
+ Capybara::find(".ion-gear-a", match: :first)
44
38
  end
45
39
 
46
40
  def new_paired_client
@@ -55,16 +49,20 @@ def new_client_from_stored_values
55
49
  if File.file?(BitPay::PRIVATE_KEY_PATH) && File.file?(BitPay::TOKEN_FILE_PATH)
56
50
  token = get_token_from_file
57
51
  pem = File.read(BitPay::PRIVATE_KEY_PATH)
58
- BitPay::SDK::Client.new(pem: pem, tokens: token, insecure: true, api_uri: ROOT_ADDRESS )
52
+ client = BitPay::SDK::Client.new(pem: pem, tokens: token, insecure: true, api_uri: ROOT_ADDRESS )
53
+ unless client.verify_tokens then
54
+ raise "Locally stored tokens are invalid, please remove #{BitPay::TOKEN_FILE_PATH}" end
59
55
  else
60
56
  claim_code = get_claim_code_from_server
61
57
  pem = BitPay::KeyUtils.generate_pem
62
58
  client = BitPay::SDK::Client.new(api_uri: ROOT_ADDRESS, pem: pem, insecure: true)
59
+ sleep 1 # rate limit compliance
63
60
  token = client.pair_pos_client(claim_code)
61
+ FileUtils.mkdir_p(BitPay::BITPAY_CREDENTIALS_DIR)
64
62
  File.write(BitPay::PRIVATE_KEY_PATH, pem)
65
63
  File.write(BitPay::TOKEN_FILE_PATH, JSON.generate(token))
66
- client
67
64
  end
65
+ client
68
66
  end
69
67
 
70
68
  def get_token_from_file
@@ -14,39 +14,52 @@ module BitPay
14
14
  module SDK
15
15
  class Client
16
16
 
17
-
18
17
  # @return [Client]
19
18
  # @example
20
19
  # # Create a client with a pem file created by the bitpay client:
21
- # client = BitPay::Client.new
20
+ # client = BitPay::SDK::Client.new
22
21
  def initialize(opts={})
23
- @pem = opts[:pem] || ENV['BITPAY_PEM'] || KeyUtils.generate_pem
24
- @key = KeyUtils.create_key @pem
25
- @priv_key = KeyUtils.get_private_key @key
26
- @pub_key = KeyUtils.get_public_key @key
27
- @client_id = KeyUtils.generate_sin_from_pem @pem
28
- @uri = URI.parse opts[:api_uri] || API_URI
29
- @user_agent = opts[:user_agent] || USER_AGENT
30
- @https = Net::HTTP.new @uri.host, @uri.port
31
- @https.use_ssl = true
32
- @https.ca_file = CA_FILE
33
- @tokens = opts[:tokens] || {}
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.open_timeout = 10
32
+ @https.read_timeout = 10
33
+
34
+ @https.ca_file = CA_FILE
35
+ @tokens = opts[:tokens] || {}
34
36
 
35
37
  # Option to disable certificate validation in extraordinary circumstance. NOT recommended for production use
36
38
  @https.verify_mode = opts[:insecure] == true ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
37
39
 
38
40
  # Option to enable http request debugging
39
41
  @https.set_debug_output($stdout) if opts[:debug] == true
42
+ end
40
43
 
44
+ ## Pair client with BitPay service
45
+ # => Pass empty hash {} to retreive client-initiated pairing code
46
+ # => Pass {pairingCode: 'WfD01d2'} to claim a server-initiated pairing code
47
+ #
48
+ def pair_client(params={})
49
+ pairing_request(params)
41
50
  end
42
51
 
52
+ ## Compatibility method for pos pairing
53
+ #
43
54
  def pair_pos_client(claimCode)
44
55
  raise BitPay::ArgumentError, "pairing code is not legal" unless verify_claim_code(claimCode)
45
- response = set_pos_token(claimCode)
46
- get_token 'pos'
47
- response
56
+ pair_client({pairingCode: claimCode})
48
57
  end
49
58
 
59
+ ## Create bitcoin invoice
60
+ #
61
+ # Defaults to pos facade, also works with merchant facade
62
+ #
50
63
  def create_invoice(price:, currency:, facade: 'pos', params:{})
51
64
  raise BitPay::ArgumentError, "Illegal Argument: Price must be formatted as a float" unless ( price.is_a?(Numeric) || /^[[:digit:]]+(\.[[:digit:]]{2})?$/.match(price) )
52
65
  raise BitPay::ArgumentError, "Illegal Argument: Currency is invalid." unless /^[[:upper:]]{3}$/.match(currency)
@@ -55,29 +68,92 @@ module BitPay
55
68
  response["data"]
56
69
  end
57
70
 
71
+ ## Gets the privileged merchant-version of the invoice
72
+ # Requires merchant facade token
73
+ #
74
+ def get_invoice(id:)
75
+ response = send_request("GET", "invoices/#{id}", facade: 'merchant')
76
+ response["data"]
77
+ end
78
+
79
+ ## Gets the public version of the invoice
80
+ #
58
81
  def get_public_invoice(id:)
59
82
  request = Net::HTTP::Get.new("/invoices/#{id}")
60
83
  response = process_request(request)
61
84
  response["data"]
62
85
  end
63
86
 
64
- def set_token
87
+
88
+ ## Refund paid BitPay invoice
89
+ #
90
+ # If invoice["data"]["flags"]["refundable"] == true the a refund address was
91
+ # provided with the payment and the refund_address parameter is an optional override
92
+ #
93
+ # Amount and Currency are required fields for fully paid invoices but optional
94
+ # for under or overpaid invoices which will otherwise be completely refunded
95
+ #
96
+ # Requires merchant facade token
97
+ #
98
+ # @example
99
+ # client.refund_invoice(id: 'JB49z2MsDH7FunczeyDS8j', params: {amount: 10, currency: 'USD', bitcoinAddress: '1Jtcygf8W3cEmtGgepggtjCxtmFFjrZwRV'})
100
+ #
101
+ def refund_invoice(id:, params:{})
102
+ invoice = get_invoice(id: id)
103
+ response = send_request("POST", "invoices/#{id}/refunds", facade: nil, token: invoice["token"], params: params)
104
+ response["data"]
105
+ end
106
+
107
+ ## Get All Refunds for Invoice
108
+ # Returns an array of all refund requests for a specific invoice,
109
+ #
110
+ # Requires merchant facade token
111
+ #
112
+ # @example:
113
+ # client.get_all_refunds_for_invoice(id: 'JB49z2MsDH7FunczeyDS8j')
114
+ #
115
+ def get_all_refunds_for_invoice(id:)
116
+ urlpath = "invoices/#{id}/refunds"
117
+ invoice = get_invoice(id: id)
118
+ response = send_request("GET", urlpath, facade: nil, token: invoice["token"])
119
+ response["data"]
65
120
  end
66
121
 
67
- def verify_token
68
- server_tokens = load_tokens
69
- @tokens.each{|key, value| return false if server_tokens[key] != value}
122
+ ## Get Refund
123
+ # Requires merchant facade token
124
+ #
125
+ # @example:
126
+ # client.get_refund(id: 'JB49z2MsDH7FunczeyDS8j', request_id: '4evCrXq4EDXk4oqDXdWQhX')
127
+ #
128
+ def get_refund(id:, request_id:)
129
+ urlpath = "invoices/#{id}/refunds/#{request_id}"
130
+ invoice = get_invoice(id: id)
131
+ response = send_request("GET", urlpath, facade: nil, token: invoice["token"])
132
+ response["data"]
133
+ end
134
+
135
+ ## Checks that the passed tokens are valid by
136
+ # comparing them to those that are authorized by the server
137
+ #
138
+ # Uses local @tokens variable if no tokens are passed
139
+ # in order to validate the connector is properly paired
140
+ #
141
+ def verify_tokens(tokens: @tokens)
142
+ server_tokens = refresh_tokens
143
+ tokens.each{|key, value| return false if server_tokens[key] != value}
70
144
  return true
71
145
  end
146
+
72
147
  ## Generates REST request to api endpoint
73
-
148
+ # => Defaults to merchant facade unless token or facade is explicitly provided
149
+ #
74
150
  def send_request(verb, path, facade: 'merchant', params: {}, token: nil)
75
151
  token ||= get_token(facade)
76
152
 
77
153
  # Verb-specific logic
78
154
  case verb.upcase
79
155
  when "GET"
80
- urlpath = '/' + path + '?nonce=' + KeyUtils.nonce + '&token=' + token
156
+ urlpath = '/' + path + '?token=' + token
81
157
  request = Net::HTTP::Get.new urlpath
82
158
  request['X-Signature'] = KeyUtils.sign(@uri.to_s + urlpath, @priv_key)
83
159
 
@@ -88,7 +164,6 @@ module BitPay
88
164
  urlpath = '/' + path
89
165
  request = Net::HTTP::Post.new urlpath
90
166
  params[:token] = token
91
- params[:nonce] = KeyUtils.nonce
92
167
  params[:guid] = SecureRandom.uuid
93
168
  params[:id] = @client_id
94
169
  request.body = params.to_json
@@ -133,19 +208,17 @@ module BitPay
133
208
 
134
209
  end
135
210
 
136
- ## Requests token by appending nonce and signing URL
137
- # Returns a hash of available tokens
211
+ ## Fetches the tokens hash from the server and
212
+ # updates @tokens
138
213
  #
139
- def load_tokens
140
-
141
- urlpath = '/tokens?nonce=' + KeyUtils.nonce
214
+ def refresh_tokens
215
+ urlpath = '/tokens'
142
216
 
143
217
  request = Net::HTTP::Get.new(urlpath)
144
- request['x-identity'] = @pub_key
145
- request['x-signature'] = KeyUtils.sign(@uri.to_s + urlpath, @priv_key)
218
+ request['X-Identity'] = @pub_key
219
+ request['X-Signature'] = KeyUtils.sign(@uri.to_s + urlpath, @priv_key)
146
220
 
147
221
  response = process_request(request)
148
-
149
222
  token_array = response["data"] || {}
150
223
 
151
224
  tokens = {}
@@ -158,9 +231,13 @@ module BitPay
158
231
 
159
232
  end
160
233
 
161
- ## Retrieves specified token from hash, otherwise tries to refresh @tokens and retry
162
- def set_pos_token(claim_code)
163
- params = {pairingCode: claim_code}
234
+ ## Makes a request to /tokens for pairing
235
+ # Adds passed params as post parameters
236
+ # If empty params, retrieves server-generated pairing code
237
+ # If pairingCode key/value is passed, will pair client ID to this account
238
+ # Returns response hash
239
+ #
240
+ def pairing_request(params)
164
241
  urlpath = '/tokens'
165
242
  request = Net::HTTP::Post.new urlpath
166
243
  params[:guid] = SecureRandom.uuid
@@ -170,7 +247,7 @@ module BitPay
170
247
  end
171
248
 
172
249
  def get_token(facade)
173
- token = @tokens[facade] || load_tokens[facade] || raise(BitPayError, "Not authorized for facade: #{facade}")
250
+ token = @tokens[facade] || refresh_tokens[facade] || raise(BitPayError, "Not authorized for facade: #{facade}")
174
251
  end
175
252
 
176
253
  def verify_claim_code(claim_code)
@@ -3,5 +3,5 @@
3
3
  # or https://github.com/bitpay/php-bitpay-client/blob/master/LICENSE
4
4
 
5
5
  module BitPay
6
- VERSION = '2.2.0'
6
+ VERSION = '2.3.0'
7
7
  end
@@ -2,8 +2,8 @@ require 'spec_helper'
2
2
 
3
3
  def tokens
4
4
  {"data" =>
5
- [{"merchant" => "MERCHANTTOKEN"},
6
- {"pos" =>"POSTOKEN"},
5
+ [{"merchant" => "MERCHANT_TOKEN"},
6
+ {"pos" =>"POS_TOKEN"},
7
7
  {"merchant/invoice" => "9kv7gGqZLoQ2fxbKEgfgndLoxwjp5na6VtGSH3sN7buX"}
8
8
  ]
9
9
  }
@@ -14,8 +14,17 @@ describe BitPay::SDK::Client do
14
14
  let(:claim_code) { "a12bc3d" }
15
15
 
16
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 => {})
17
+ # Stub JSON responses from fixtures
18
+ stub_request(:get, /#{BitPay::TEST_API_URI}\/tokens.*/)
19
+ .to_return(:status => 200, :body => tokens.to_json, :headers => {})
20
+ stub_request(:get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID?token=MERCHANT_TOKEN").
21
+ to_return(:body => get_fixture('invoices_{id}-GET.json'))
22
+ stub_request(:get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID/refunds?token=MERCHANT_INVOICE_TOKEN").
23
+ to_return(:body => get_fixture('invoices_{id}_refunds-GET.json'))
24
+ stub_request(:get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID/refunds/TEST_REQUEST_ID?token=MERCHANT_INVOICE_TOKEN").
25
+ to_return(:body => get_fixture('invoices_{id}_refunds-GET.json'))
26
+ stub_request(:post, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID/refunds").
27
+ to_return(:body => get_fixture('invoices_{id}_refunds-POST.json'))
19
28
  end
20
29
 
21
30
  describe "#initialize" do
@@ -36,7 +45,7 @@ describe BitPay::SDK::Client do
36
45
  it 'should generate a get request' do
37
46
  stub_request(:get, /#{BitPay::TEST_API_URI}\/whatever.*/).to_return(:body => '{"awesome": "json"}')
38
47
  bitpay_client.send_request("GET", "whatever", facade: "merchant")
39
- expect(WebMock).to have_requested(:get, "#{BitPay::TEST_API_URI}/whatever?nonce=1&token=MERCHANTTOKEN")
48
+ expect(WebMock).to have_requested(:get, "#{BitPay::TEST_API_URI}/whatever?token=MERCHANT_TOKEN")
40
49
  end
41
50
  end
42
51
 
@@ -68,7 +77,7 @@ describe BitPay::SDK::Client do
68
77
  it 'short circuits on invalid pairing codes' do
69
78
  100.times do
70
79
  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"
80
+ expect { bitpay_client.pair_pos_client(claim_code) }.to raise_error BitPay::ArgumentError, "pairing code is not legal"
72
81
  end
73
82
  end
74
83
  end
@@ -106,19 +115,55 @@ describe BitPay::SDK::Client do
106
115
  end
107
116
  end
108
117
 
109
- describe '#set_token' do
118
+ describe '#refund_invoice' do
119
+ subject { bitpay_client }
120
+ before { stub_const('ENV', {'BITPAY_PEM' => PEM}) }
121
+ it { is_expected.to respond_to(:refund_invoice) }
122
+
123
+ it 'should get the token for the invoice' do
124
+ bitpay_client.refund_invoice(id: 'TEST_INVOICE_ID')
125
+ expect(WebMock).to have_requested :get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID?token=MERCHANT_TOKEN"
126
+ end
127
+
128
+ it 'should generate a POST to the invoices/refund endpoint' do
129
+ bitpay_client.refund_invoice(id: 'TEST_INVOICE_ID')
130
+ expect(WebMock).to have_requested :post, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID/refunds"
131
+ end
132
+ end
133
+
134
+ describe '#get_all_refunds_for_invoice' do
110
135
  subject { bitpay_client }
111
136
  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
-
137
+ it { is_expected.to respond_to(:get_all_refunds_for_invoice) }
138
+
139
+ it 'should get the token for the invoice' do
140
+ bitpay_client.get_all_refunds_for_invoice(id: 'TEST_INVOICE_ID')
141
+ expect(WebMock).to have_requested :get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID?token=MERCHANT_TOKEN"
142
+ end
143
+ it 'should GET all refunds' do
144
+ bitpay_client.get_all_refunds_for_invoice(id: 'TEST_INVOICE_ID')
145
+ expect(WebMock).to have_requested :get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID/refunds?token=MERCHANT_INVOICE_TOKEN"
146
+ end
147
+ end
148
+
149
+ describe '#get_refund' do
150
+ subject { bitpay_client }
151
+ before {stub_const('ENV', {'BITPAY_PEM' => PEM})}
152
+ it { is_expected.to respond_to(:get_refund) }
153
+ it 'should get the token for the invoice' do
154
+ bitpay_client.get_refund(id: 'TEST_INVOICE_ID', request_id: 'TEST_REQUEST_ID')
155
+ expect(WebMock).to have_requested :get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID?token=MERCHANT_TOKEN"
156
+ end
157
+ it 'should GET a single refund' do
158
+ bitpay_client.get_refund(id: 'TEST_INVOICE_ID', request_id: 'TEST_REQUEST_ID')
159
+ expect(WebMock).to have_requested :get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID/refunds/TEST_REQUEST_ID?token=MERCHANT_INVOICE_TOKEN"
115
160
  end
116
161
  end
117
162
 
118
- describe "#verify_token" do
163
+ describe "#verify_tokens" do
119
164
  subject { bitpay_client }
120
165
  before {stub_const('ENV', {'BITPAY_PEM' => PEM})}
121
- it { is_expected.to respond_to(:verify_token) }
166
+ it { is_expected.to respond_to(:verify_tokens) }
122
167
  end
123
168
  end
124
169
 
@@ -0,0 +1,29 @@
1
+ {
2
+ "facade": "pos/invoice",
3
+ "data": {
4
+ "url": "https://test.bitpay.com/invoice?id=2RSyNDvsiTrA31rPwnnEcd",
5
+ "status": "new",
6
+ "btcPrice": "0.037523",
7
+ "btcDue": "0.037523",
8
+ "price": 10,
9
+ "currency": "USD",
10
+ "exRates": {
11
+ "USD": 266.5
12
+ },
13
+ "invoiceTime": 1422319964413,
14
+ "expirationTime": 1422320864413,
15
+ "currentTime": 1422319964431,
16
+ "guid": "34d7be05-eb65-4f72-a2ce-79bf23e93f17",
17
+ "id": "2RSyNDvsiTrA31rPwnnEcd",
18
+ "btcPaid": "0.000000",
19
+ "rate": 266.5,
20
+ "exceptionStatus": false,
21
+ "paymentUrls": {
22
+ "BIP21": "bitcoin:mhPM48eieakd6AgCuHMwAtpFXE5yQ3N7om?amount=0.037523",
23
+ "BIP72": "bitcoin:mhPM48eieakd6AgCuHMwAtpFXE5yQ3N7om?amount=0.037523&r=https://test.bitpay.com/i/2RSyNDvsiTrA31rPwnnEcd",
24
+ "BIP72b": "bitcoin:?r=https://test.bitpay.com/i/2RSyNDvsiTrA31rPwnnEcd",
25
+ "BIP73": "https://test.bitpay.com/i/2RSyNDvsiTrA31rPwnnEcd"
26
+ },
27
+ "token": "2RPipMRUXAvt5wAfthCzF7Tj4SppBWPHGQ7hCeWYeWDm7RtwUtDds1XUNt11VTf5C6UfCAACBhsKwjW6SAocLsd7"
28
+ }
29
+ }