bitpay-sdk 2.2.0 → 2.3.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.
@@ -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
+ }