Compare commits

..

116 Commits

Author SHA1 Message Date
Anditto Heristyo
6e656cce74
Merge pull request #1 from anditto/singlesatoshipayment-fix
Allow single satoshi units for invoice creations
2018-12-13 17:26:50 +09:00
Anditto Heristyo
ba65ccb2f4 Bumped version number 2018-11-30 14:01:06 +09:00
Anditto Heristyo
fc8d920c27 Changed verification parameter to allow single satoshi units 2018-11-30 13:56:27 +09:00
Kevin Quiring
e9e2c1cc5e add support for /invoices route with params 2018-02-26 00:52:52 -06:00
Pieter Poorthuis
019140f049
Merge pull request #56 from sylv3rblade/master
update gemspec for rails 5
2018-02-01 16:01:54 +01:00
Pieter Poorthuis
7c74b936e0
Merge pull request #54 from sanemat/chore/travis-rubies
Update travis rubies
2018-02-01 15:52:46 +01:00
Pieter Poorthuis
357aac7a10
Merge pull request #55 from b1nary/patch-1
Just a small typo
2018-02-01 15:51:57 +01:00
Pieter Poorthuis
4751c66a37
Merge pull request #59 from marzapower/patch-1
[fix] Typos in the wiki
2018-02-01 15:51:31 +01:00
Pieter Poorthuis
ff3bce79de
Merge pull request #62 from ayazahmadtarar-pikessoft/patch-1
Update GUIDE.md
2018-02-01 15:50:44 +01:00
ayazahmadtarar-pikessoft
0acbbb5312
Update GUIDE.md
Function changed
2017-12-16 02:01:10 +05:00
Daniele Di Bernardo
774d3d59a1 [fix] Typos in the wiki 2017-09-04 10:09:50 +02:00
sylv3rblade
e90e3a188c update gemspec for rails 5 2016-12-12 20:14:55 +08:00
Roman Pramberger
98b88617d0 Just a small typo 2016-10-11 16:00:14 +02:00
Sanemat
4c5d8adfad Update travis rubies 2016-09-04 21:21:44 +09:00
Chris Kleeschulte
986ab1e3d6 Merge pull request #53 from javierjulio/patch-3
Remove unnecessary rack/json dependencies
2016-08-22 13:27:49 -04:00
Javier Julio
b789faf87d Add rack as a development dependency
Seems this failing due to addressable but that requires rack-mount but only used in development. Going off a suggestion from a coworker as this seems as an odd dependency to have but most likely should just be set for dev.
2016-08-18 11:33:00 -04:00
Javier Julio
53411dca98 Remove unnecessary rack/json dependencies
Rack isn't used at all so no need to specify. Based on [this comment](https://github.com/bitpay/ruby-client/pull/51#issuecomment-236736392) this seems to be carried over from a Rails app so is safe to remove. This will also allow this gem to be used in Rails 5 apps which now require Rack v2.

No need to specify json dependency since this gem requires Ruby 2.0+ and I've [seen this change made elsewhere](https://github.com/mperham/sidekiq/issues/2743) to clean up dependencies and also support other platforms (Windows). The json library is being required in `lib/bitpay/client.rb`.
2016-08-04 14:02:11 -04:00
J. Paul Daigle
0ec4bb1105 Update GUIDE.md 2015-09-16 10:31:41 -04:00
J. Paul Daigle
c534b83c31 Merge pull request #48 from philosodad/master
Eliminate web driver steps from cucumber
2015-09-13 16:37:18 -05:00
Paul Daigle
671fe5657b Eliminate web driver steps from cucumber
remove capybara completely

undo version bump, there's nothing here but test changes
2015-09-13 17:32:28 -04:00
J. Paul Daigle
d8890fa31a Remove reference to cli in the guide.md 2015-09-11 14:54:56 -04:00
J. Paul Daigle
5f3881f602 Update README.md
Remove mention of obsolete CLI gem.
2015-09-11 13:30:03 -04:00
J. Paul Daigle
902b157a08 Update README.md
update link to guide.md
2015-08-05 18:01:57 -04:00
Paul Daigle
931cc446b2 Test Changes to accomdate new dashboard 2015-07-08 12:33:27 -04:00
J. Paul Daigle
ad9ea4e2cd Update GUIDE.md
There is always a formatting problem.
2015-07-07 13:43:45 -04:00
J. Paul Daigle
aec7841782 Merge pull request #46 from bitpay/philosodad-patch-1
Update GUIDE.md
2015-07-07 13:41:38 -04:00
J. Paul Daigle
3f10dfb13d Update GUIDE.md 2015-07-01 15:39:23 -04:00
Paul Daigle
0e2c78cada Capybara changes 2015-05-18 11:04:00 -04:00
Paul Daigle
265556f23f Revert "add a call to remove bad token"
This reverts commit 763529b10a.

Revert "return to coveralls"

This reverts commit d2d22e33f6.

Revert "move codeclimate below webmock"

This reverts commit d90d2dae49.

Revert "Use code climate test coverage"

This reverts commit 2b248a4978.
2015-05-16 17:34:12 -04:00
Paul Daigle
763529b10a add a call to remove bad token 2015-05-16 16:53:48 -04:00
J. Paul Daigle
6447b18465 Update README.md
Add badges for code climate, coveralls, and the MIT license
2015-05-16 15:20:15 -04:00
Paul Daigle
d2d22e33f6 return to coveralls 2015-05-16 14:54:25 -04:00
Paul Daigle
d90d2dae49 move codeclimate below webmock 2015-05-16 14:45:45 -04:00
Paul Daigle
2b248a4978 Use code climate test coverage 2015-05-16 14:33:42 -04:00
Paul Daigle
e4addd06e2 adding coveralls 2015-05-15 18:46:24 -04:00
Paul Daigle
a9c4ec571a remove key utils
update changelog, gemfile, and gemspec
2015-04-14 13:03:41 -04:00
Paul Daigle
156113d24b bump gem version 2015-04-13 13:39:16 -04:00
J. Paul Daigle
d822eaf1f9 Merge pull request #44 from saizai/patch-1
Allow newer versions of gem dependencies
2015-04-13 13:00:33 -04:00
Sai
6ea17a7351 Allow newer versions of gem dependencies
Rack is now 1.6.0 & json is at 1.8.2; hard dependencies force downgrade of these gems.
2015-04-12 19:48:20 +01:00
Paul Daigle
1b49252fa6 Address github issue #39
Issue is a bit obscure, if a token is sent to the 'post' method but the
path sent already includes a '?', an error was thrown. This fix places
either a '?' or a '&' before the token as appropriate.
2015-03-24 17:44:18 -04:00
J. Paul Daigle
44ac2ec100 Merge pull request #41 from heisler3030/issue_39
fix and test for issue 39
2015-03-16 13:24:57 -04:00
Hamish Eisler
9809370c17 fix and test for issue 39 2015-03-13 06:59:56 +00:00
Paul Daigle
48cbc9d2a1 Fix github issue #40
Both post and get requests assumed that the response from the server
would include a "data" field, which is not the case. Moved handling of
the data element back into the invoice and pairing methods.

bumped gem version to 2.4.1
2015-03-13 01:52:45 -04:00
Paul Daigle
b45fbb2161 Add changelog file 2015-03-05 16:24:32 -05:00
Paul Daigle
4563a8beb2 Minor modifications to v2.4 functionality. 2015-03-05 16:01:51 -05:00
J. Paul Daigle
5ca3bd7e72 Merge pull request #38 from heisler3030/add_refunds
add refunds and cucumber tests with documentation
2015-03-05 11:01:39 -05:00
Hamish Eisler
f113cef580 correct BTC validation 2015-03-04 22:11:16 +00:00
Hamish Eisler
bfa5bc8c86 incorporate Paul feedback 2015-03-04 06:13:19 +00:00
Hamish Eisler
90b8f159db add refunds and cucumber tests with documentation 2015-03-03 01:01:19 +00:00
J. Paul Daigle
040f7c6f4f Merge pull request #37 from philosodad/master
Refactor: create rest_connector module
2015-02-25 22:28:20 -05:00
Paul Daigle
348f7a410e Refactor: create rest_connector module
The rest connector module moves most of the functionality of http
connection out of the client and into a module. The goal is to simplify
method calls and DRY up the code.

Changes include replacing all calls to send_request with calls to either
'get' or 'post'. process request now returns the "data" portion of the
JSON response from the server, as every method was retrieving that
separately.

removed some untested code.
2015-02-25 19:20:20 -05:00
Paul Daigle
0de7d26cd1 bumped gem version 2015-02-23 15:12:35 -05:00
J. Paul Daigle
90c50762dd Merge pull request #36 from philosodad/master
Removing refund functionality
2015-02-23 14:47:43 -05:00
Paul Daigle
ce31b96e3e Removing refund functionality
Features need to be accompanied by functional tests.
2015-02-23 14:24:48 -05:00
J. Paul Daigle
8e6083431d Merge pull request #33 from martindale/guide
Guide
2015-02-20 16:22:16 -05:00
Eric Martindale
9cee21268f Split guide into appropriate location. 2015-02-20 15:50:46 -05:00
Eric Martindale
29e90a62ab Merge branch 'master' of github.com:bitpay/ruby-client into guide
Conflicts:
	README.md
2015-02-20 15:44:18 -05:00
Paul Daigle
ee3ec36d3d bump the gem version 2015-02-19 11:04:46 -05:00
Paul Daigle
1296c3f3d0 Merge branch 'matugm-patch-1' 2015-02-17 09:45:58 -05:00
Paul Daigle
97df36429e Merge branch 'patch-1' of https://github.com/matugm/ruby-client into matugm-patch-1
Conflicts:
	spec/spec_helper.rb
2015-02-17 09:45:15 -05:00
J. Paul Daigle
aeaa979a21 Merge pull request #35 from philosodad/master
Hamish's changes for client initiated pairing
2015-02-16 19:51:45 -05:00
Paul Daigle
61f9312a5b use trigger('click') for buttons' 2015-02-16 19:32:06 -05:00
Paul Daigle
55e4300f62 tweak timing and selection 2015-02-16 19:15:34 -05:00
Paul Daigle
762d31cf52 Change cucumbers to work with new dashboard 2015-02-16 18:26:45 -05:00
Paul Daigle
4c4e5e7210 Locking gemfile versions to test Travis Build 2015-02-16 18:20:42 -05:00
Hamish Eisler
c7dfbf249c changes per Paul comments 2015-02-16 09:33:20 -08:00
matugm
2f40752a8d Refactor claim code generation
This refactor makes the code much easier to understand and takes advantage of **rand(Range)** and **Array.new** with a block.
2015-02-15 15:20:54 +01:00
Eric Martindale
0b30b7b1ab add GUIDE.md 2015-02-12 16:36:26 -05:00
Eric Martindale
b7c73eece7 Reformat Code Samples
This add syntax highlighting to the README.
2015-02-11 19:41:34 -05:00
Hamish Eisler
953848bfde increase sleep for travis reliability 2015-02-06 13:05:05 -08:00
Hamish Eisler
b3b4eddbc6 reduce invoice amounts to work with tier 0 accounts 2015-02-05 16:23:21 -08:00
Hamish Eisler
0e373b589b add env setup to travis 2015-02-05 15:11:18 -08:00
Hamish Eisler
e0dd445b44 migrate from rspec features to cucumbers 2015-02-04 23:34:27 -08:00
Hamish Eisler
f43ab470b8 update tests for new dashboard 2015-02-04 23:30:35 -08:00
Hamish Eisler
bf428ba0ba update tests 2015-01-30 00:27:50 +00:00
Hamish Eisler
3feb5e5805 documentation updates 2015-01-29 22:28:04 +00:00
Hamish Eisler
36e928209d refactor refund method signatures 2015-01-29 22:22:44 +00:00
Hamish Eisler
58cffd70b8 add refund methods and tests 2015-01-27 05:55:29 +00:00
Hamish Eisler
7abd514397 client-initiated pairing changes 2015-01-21 23:57:36 -08:00
J. Paul Daigle
e1cffb375a Update README.md 2015-01-07 15:31:40 -05:00
Alexander Leitner
e97137df4e Merge pull request #31 from philosodad/master
Change namespace of Client to SDK::Client
2015-01-07 11:10:04 -05:00
Paul Daigle
ad3a68e009 Change namespace of Client to SDK::Client
And bumps gem version, deletes redundant files, removes redundant
requires.
2015-01-07 10:46:46 -05:00
Paul Daigle
42a6a604f5 bump gem version 2015-01-06 06:45:55 -05:00
J. Paul Daigle
95dcd1a7c6 Merge pull request #30 from philosodad/master
Housekeeping: renaming the gem
2015-01-06 06:37:44 -05:00
Paul Daigle
7f26b781ff Removing all persistance/environment attributes
The core code should not have environmental expectations.

gitignore update
2015-01-05 19:47:32 -05:00
Paul Daigle
df063e35a2 Modified name of gemfile to bitpay-sdk
bitpay-client will be used for the command line tool that requires this
gem, and the name 'bitpay' was already taken by a dead project.
2015-01-05 16:04:35 -05:00
Paul Daigle
b7117c65dd Merge pull request#29 'philosodad-separated_client' 2014-12-29 17:35:39 -05:00
Paul Daigle
ec0d5c4780 Merge branch 'separated_client' of https://github.com/philosodad/ruby-client into philosodad-separated_client
Conflicts:
	README.md
	lib/bitpay/client.rb
2014-12-29 17:23:11 -05:00
Paul Daigle
c95945e710 Remove bitpay client.
Bitpay client removed, bitpay bin for client removed.
Features modified to run with local token/key saved or not.
Rake task to clean up local files if created.
Rake task to run all tests, cucumbers, and clean up.

Move local file constants to the cucumber helper
2014-12-29 17:06:28 -05:00
Rich Morgan
3a0ed03bdf Merge pull request #28 from philosodad/master
Added some error handling
2014-12-21 23:24:56 -05:00
Paul Daigle
4c06edc030 Added some error handling
Connection Refused error when pairing
Invalid pairing code if pairing code malformed
2014-12-19 16:52:54 -05:00
J. Paul Daigle
9cc0fc78d5 Update README.md 2014-12-15 16:11:46 -05:00
J. Paul Daigle
1863ece600 Update README.md 2014-12-15 16:04:23 -05:00
Paul Daigle
3b692398e0 Modified rake task to clear everything
Both before and after cucumbers.

Improved token clearance to use mongo

Rather than relying on capybara, deletes rather than revokes.
2014-12-13 18:18:24 -05:00
Paul Daigle
558a8530b6 Add cucumber testing to bitpay library
Work done simultaneously with removing the client from the library

removed 'id' as an argument, no longer valid
2014-12-13 17:30:44 -05:00
Paul Daigle
6e80e47ee0 Added some error handling
Connection Refused error when pairing
Invalid pairing code if pairing code malformed
2014-12-09 16:26:58 -05:00
J. Paul Daigle
9be4cfbf7d Update README.md
Better test running explanation.
2014-12-02 13:45:20 -05:00
Paul Daigle
95a6f5c6a2 Bumped the gem version 2014-12-02 13:38:01 -05:00
J. Paul Daigle
c50b9012b5 Merge pull request #23 from heisler3030/better_errors
Better errors
2014-12-02 13:26:39 -05:00
Hamish Eisler
387006b616 more request DRYing 2014-11-26 15:22:05 -08:00
Hamish Eisler
316ce90d34 remove dead code 2014-11-26 11:54:28 -08:00
Hamish Eisler
746fd8737b DRY up HTTP request/error handling 2014-11-26 11:54:28 -08:00
Hamish Eisler
5281aff957 better passthrough of error messages, plus test 2014-11-26 11:54:28 -08:00
J. Paul Daigle
2e0e881fc3 Merge pull request #22 from heisler3030/fix_nil_pem
Fix hardcoding and PEM utility
2014-11-26 10:49:42 -05:00
Hamish Eisler
7cca6fa77d fix nil pem, add test, cleanup hardcoding 2014-11-18 15:04:57 -08:00
Hamish Eisler
b199f32c59 bug fix 2014-11-18 14:47:09 -08:00
Alexander Leitner
2a36db7d44 Merge pull request #19 from philosodad/master
Modified tests to fix breaks caused by changes in prod
2014-10-23 10:18:24 -04:00
Paul Daigle
43164f9539 Modified tests to fix breaks caused by changes in prod
We are having to pause between two clicks that are running javascript,
which is a little odd.

Had to add an ssl-protocol option to the phantomjs options.
2014-10-22 15:10:08 -04:00
J. Paul Daigle
648ac1a72e Merge pull request #18 from philosodad/master
specs no longer create extra files
2014-10-15 17:06:17 -04:00
Paul Daigle
8545afd982 specs no longer create extra files 2014-10-13 09:51:05 -04:00
J. Paul Daigle
2c0205d777 Merge pull request #16 from thoerner/patch-1
Update README.md (formatting)
2014-10-12 16:46:01 -04:00
thoerner
67c54f4b00 Update README.md (formatting)
Formatting fix for "Pairing with Bitpay.com" ordered list.
2014-10-11 11:19:02 -04:00
Paul Daigle
33025f2b8e Changes for the 2.0.0 version. Detailed changes are on the 2.0.0 branch.
The library now uses the crpytographically secure API.

more tests plus CLI enhancements

Library no longer supporting ruby 1.9.x

1.9.x will be deprecated in February of 2015, so there is little
incentive to write our new library to support it. 1.9.x users should
still be able to use V1 of the API.

This commit adds some changes, such as named arguments, that are
incompatible with 1.9.x.

Add Rake tasks to clear limiters

When many integration tests are run in a short period, the local bitpay server
will refuse to create new keys due to the rate limiting function. This
rake task clears the ratelimiters database.
2014-10-08 13:21:00 -04:00
Joshua Estes
2d4f17dd78 Update version.rb
https://github.com/bitpay/ruby-client/pull/14
2014-09-19 09:35:36 -04:00
Joshua Estes
9b3add2c94 Merge pull request #14 from arrtchiu/exception_handling
refactor request handling in bitpay client
2014-09-19 06:58:47 -04:00
arrtchiu
f5b01d4530 refactor request handling in bitpay client 2014-09-19 11:33:20 +08:00
40 changed files with 1128 additions and 732 deletions

6
.gitignore vendored
View File

@ -5,3 +5,9 @@ pkg
.c9
.ruby-version
.ruby-gemset
*.swo
*.swp
bitpaykey.pem
constants.txt
coverage/
.pem.data

View File

@ -1,2 +1,5 @@
sudo: false
rvm:
- 2.1.0
- 2.1.10
- 2.2.5
- 2.3.1

24
CHANGELOG.md Normal file
View File

@ -0,0 +1,24 @@
# Change Log
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [2.4.4] - 2015-04-14
### Changed
- Separated key utilities into its own Gem
## [2.4.3] - 2015-04-13
### Changed
- Loosened production gem requirements from patch level to major level
## [2.4.2] - 2015-03-11
### Fixed
- GitHub issue 39: handling post paths that include a ? and require a token. A workaround exists for this issue.
## [2.4.1] - 2015-03-11
### Fixed
- GitHub issue 40: error for endpoints that did not return a 'data' field
## [2.4.0] - 2015-03-05
### Changed
- Add feature: Accept refunds
- Fix Bug: Accept bitcoin payments like 0.003

233
GUIDE.md Normal file
View File

@ -0,0 +1,233 @@
# Using the BitPay Ruby Client Library
## Prerequisites
You must have a BitPay merchant account to use this library. It's free to [sign-up for a BitPay merchant account](https://bitpay.com/start).
Once you have a BitPay merchant account, you will need [a working BitPay Access Token](/api/getting-access.html) this can be done either [via the library](#pairing) or manually in [the BitPay Dashboard](https://bitpay.com/tokens).
## Quick Start
### Installation
```bash
gem install bitpay-sdk
```
In your Gemfile:
```ruby
gem 'bitpay-sdk', :require => 'bitpay_sdk'
```
Or directly:
```ruby
require 'bitpay_sdk'
```
### Configuration
The bitpay client creates a cryptographically secure connection to your server by pairing an API code with keys generated by the library. The client can be initialized with pre-existing keys passed in as a pem file, or paired if initialized with a pem file and a tokens hash. Examples can be found in the cucumber step helpers.
## Pairing
Most calls to the BitPay REST API require that your client is paired with the bitpay.com server. To pair with bitpay.com you need to have an approved merchant account.
Your client can be paired via the `pos` (point-of-sale) or `merchant` facade (or both). The `pos` facade allows for invoices to be created. The `merchant` facade has broader privileges to view all invoices, bills, and ledger entries, as well as to issue refunds. Consider the level of access required when you pair your client.
### A quick note on keys
The BitPay client gem includes the BitPay KeyUtilities gem, which can be used to generate new public private key pairs which it returns in PEM format. However, there are no methods which save the keys anywhere, so it is your responsibility to store the PEM file somewhere secure.
### BitPay authentication
BitPay authentication depends on four parts:
1. An account on our servers.
1. A token shared between the client and the server.
1. A public key, shared between the client and the server.
1. A private key, held exclusively by the client.
In order to complete authentication, you have to associate your private key with a token, and associate that token with an account. Once this authentication is complete, as long as you have the private key, you never have to authenticate again. The token you created will always be associated with that private key, so any time you create a new bitpay client object with that key, it is authenticated with BitPay. This is true whether you use the ruby-client, python client, or no client at all, the key is the important thing.
There are two ways to authenticate, from the client side or the server side. The Ruby Client supports both.
To pair from the server side, you log in to the BitPay server, navigate to dashboard/merchant/api-tokens, and create a new token. This creates a new token, which is associated with your account. It is not associated with a key, so we provide a pairing code that you can use as a one time secret to associate the token with a key. From the client side, you can use the client.pair_pos_client(<pairing_code>) method to associate that method with a key held by the client.
To pair from the client side, you use the client to call the /tokens endpoint on the server with no parameters. This creates a token on the server and associates that token with a public key. What it doesn't do is associate that token to an account (because we don't know what account to associate with). This call returns a pairing code, which is a one time secret that allows you to find the token you just created. In order to associate the token with an account, you log in to the BitPay server, and use the dashboard/merchant/api-tokens interface to associate the token with a specific account. And example of client side pairing is shown below.
### Pairing Programatically
If you are developing a client with built-in pairing capability, you can pair programattically using the `pair_client` method. This method can be called in two ways:
* `pair_client()` will perform a client-initiated pairing, and will provide a pairing code that can be entered at https://bitpay.com/dashboard/merchant/api-tokens to assign either `merchant` or `pos` facade.
* `pair_client('pairing_code')` will complete a server-initiated pairing, when provided a pre-generated pairing code from https://bitpay.com/dashboard/merchant/api-tokens. In this case, the `pos` facade will be automatically assigned.
This is an example of creating a paired client with the BitPay toolset.
```bash
$ gem install bitpay-sdk
Successfully installed bitpay-sdk-2.2.0
1 gem installed
$ irb
2.1.1 :001 > require 'bitpay_sdk'
=> true
2.1.2 :002 > pem = BitPay::KeyUtils.generate_pem
=> "-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIH8oSTRm8lVMTVOsDZleIB8AmkiuHnp+ctEknqeUmZahoAcGBSuBBAAK\noUQDQgAEbjhdKA+X8NEKgcbHhyJaBMvePV7Sj6AQuOMQzuZYdskdkPY1/jlfQwNG\n4GVd/zSw4uhfukw/SDBOEKlQGVAmxQ==\n-----END EC PRIVATE KEY-----\n"
2.1.1 :002 > client = BitPay::SDK::Client.new(api_uri: 'https://test.bitpay.com', pem: pem)
=> #<BitPay::SDK::Client:0x000000019c6d40 @pem="---... @tokens={}>
2.1.1 :003 > client.pair_client()
=> {"data"=>[{"policies"=>[{"policy"=>"id", "method"=>"inactive", "params"=>["Tf49SFeiUAtytFEW2EUqZgWj32nP51PK73M"]}], "token"=>"BKQyVdaGQZAArdkkSuvtZN5gcN2355c8vXLj5eFPkfuK", "dateCreated"=>1422474475162, "pairingExpiration"=>1422560875162, "pairingCode"=>"Vy76yTh"}]}
```
As described above, using the value from the `pairingCode` element, visit https://test.bitpay.com/api-tokens and search to register for the appropriate facade. That client is now paired. As previously mentioned, you must save the pem string you generated in order to use the client again.
## General Usage
### Initialize the client
```ruby
client = BitPay::SDK::Client.new(pem: File.read('bitpaykey.pem'))
```
Optional parameters:
* `api_uri` - specify a different api endpoint (e.g. 'https://test.bitpay.com'). Ensure no trailing slash.
* `tokens` - pass a stored hash of bitpay API tokens
* `user-agent` - specify a custom user-agent value
* `debug: true` - enable HTTP request logging to $stdout
* `insecure: true` - disable HTTPs certificate validation (for local test environments)
### Create a new bitcoin invoice
```ruby
invoice = client.create_invoice(price: <price>, currency: <currency>)
```
With invoice creation, `price` and `currency` are the only required fields. If you are sending a customer from your website to make a purchase, setting `redirectURL` will redirect the customer to your website when the invoice is paid.
Response will be a hash with information on your newly created invoice. Send your customer to the `url` to complete payment:
```javascript
{
"url": "https://bitpay.com/invoice?id=NKaqMuZWy3BAcP77RdkEEv",
"paymentUrls": {
"BIP21": "bitcoin:mvYRECDxKPaPHnjNz9ZxiTpbx29xYNoRy4?amount=0.3745",
"BIP72": "bitcoin:mvYRECDxKPaPHnjNz9ZxiTpbx29xYNoRy4?amount=0.3745&r=https://bitpay.com/i/NKaqMuZWy3BAcP77RdkEEv",
"BIP72b": "bitcoin:?r=https://bitpay.com/i/NKaqMuZWy3BAcP77RdkEEv",
"BIP73": "https://bitpay.com/i/NKaqMuZWy3BAcP77RdkEEv"
},
"status": "new",
"btcPrice": "0.3745",
"btcDue": "0.3745",
"price": 148,
"currency": "USD",
"exRates": {
"USD": 395.20000000000005
},
"invoiceTime": 1415987168612,
"expirationTime": 1415988068612,
"currentTime": 1415987168629,
"guid": "438e8237-fff1-483c-81b4-dc7dba28922a",
"id": "NKaqMuZWy3BAcP77RdkEEv",
"transactions": [
],
"btcPaid": "0.0000",
"rate": 395.2,
"exceptionStatus": false,
"token": "9kZgUXFb5AC6qMuLaMpP9WopbM8X2UjMhkphKKdaprRbSKgUJNE6JNTX8bGsmgxKKv",
"buyer": {
}
}
```
There are many options available when creating invoices, which are listed in the [BitPay API documentation](https://bitpay.com/bitcoin-payment-gateway-api).
### Get invoice status
The ruby library provides two methods for fetching an existing invoice:
```ruby
# For authorized clients with a 'merchant' token
client.get_invoice(id: 'PvVhgBfA7wKPWhuVC24rJo')
# For non-authenticated clients (public facade)
# Returns the public subset of invoice fields
client.get_public_invoice(id: 'PvVhgBfA7wKPWhuVC24rJo')
```
### Create a refund request
Clients with a `merchant` token can initiate a refund request for a paid invoice:
```ruby
client.refund_invoice(id: '6pbV13VBZfGFJ8BBmXmLZ8', params: {amount: 10, currency: 'USD'})
```
Refund rules:
* Invoices cannot be refunded prior to 6 blockchain confirmations
* Invoices without `["flags"]["refundable"] == true` must specify a `bitcoinAddress` param (one was not provided as part of the transaction)
* Invoices that are paid in full must specify an `amount` and `currency` param to indicate the amount to be refunded
### View Refund Requests
The ruby library provides two methods for viewing refund requests. Both require a `merchant` token.
```ruby
# To get an array of all refunds against a specific invoice
client.get_all_refunds_for_invoice(id: 'PvVhgBfA7wKPWhuVC24rJo')
# To get a specific refund for a specific invoice
client.get_refund(id: 'JB49z2MsDH7FunczeyDS8j', request_id: '4evCrXq4EDXk4oqDXdWQhX')
```
### Cancel Refund Requests
Requires a `merchant` token.
```ruby
client.cancel_refund(id: 'JB49z2MsDH7FunczeyDS8j', request_id: '4evCrXq4EDXk4oqDXdWQhX')
```
### Make a HTTP request directly against the REST API
For API tasks which lack a dedicated library method, BitPay provides methods that will automatically apply the proper cryptographic parameters to a request.
```ruby
client.send_request("GET", "invoices/JB49z2MsDH7FunczeyDS8j", facade: 'merchant')
## This request is identical to:
token = client.get_token("merchant")
client.get(path: "invoices/JB49z2MsDH7FunczeyDS8j", token: token)
## post requests are also possible
token = client.get_token("merchant")
client.post(path: "tokens", token: token, params: {facade: "pos"}) #returns a new token with pairing code
## equivalent to
client.send_request("POST", "tokens", facade: 'merchant', params: {facade: 'pos'})
```
Usage:
* Specify HTTP verb and REST endpoint
* Specifying a `facade` will fetch and apply the corresponding `token`
* Alternatively provide a `token` explicitly
* For `POST` requests, the `params` hash will be included as the message body
## Testnet Usage
During development and testing, take advantage of the [Bitcoin TestNet](https://en.bitcoin.it/wiki/Testnet) by passing a custom `api_uri` option on initialization:
```ruby
BitPay::SDK::Client.new({api_uri: "https://test.bitpay.com/api"})
```
Note that in order to pair with testnet, you will need a pairing code from test.bitpay.com and will need to use the bitpay client with the --test option.
## API Documentation
API Documentation is available on the [BitPay site](https://bitpay.com/api).
## Running the Tests
In order to run the tests, you must have phantomjs installed and on your PATH.
The tests require that environment variables be set for the bitpay server, user name, password, an invoice id for refunds and a valid testnet bitcoin address for refunds. First run:
```bash
$ source ./spec/set_constants.sh https://test.bitpay.com <yourusername> <yourpassword> <a-confirmed-invoice-id> <a-valid-testnet-address>
$ bundle install
$ bundle exec rake
```
Tests are likely to run up against rate limiters on test.bitpay.com if used too frequently. Rake tasks which interact directly with BitPay will not run for the general public.

View File

@ -1,6 +1,2 @@
source 'https://rubygems.org'
gemspec
platform :jruby do
gem 'jruby-openssl'
end

View File

@ -1,82 +1,16 @@
# BitPay Library for Ruby [![](https://secure.travis-ci.org/bitpay/ruby-client.png)](http://travis-ci.org/bitpay/ruby-client)
# BitPay Library for Ruby
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://raw.githubusercontent.com/bitpay/ruby-client/master/LICENSE.md)
[![Travis](https://img.shields.io/travis/bitpay/ruby-client.svg?style=flat-square)](https://travis-ci.org/bitpay/ruby-client)
[![Gem](https://img.shields.io/gem/v/bitpay-sdk.svg?style=flat-square)](https://rubygems.org/gems/bitpay-sdk)
[![Code Coverage](https://img.shields.io/coveralls/bitpay/ruby-client.svg?style=flat-square)](https://coveralls.io/r/bitpay/ruby-client?branch=master)
[![Code Climate](https://img.shields.io/codeclimate/github/bitpay/ruby-client.svg?style=flat-square)](https://codeclimate.com/github/bitpay/ruby-client)
Powerful, flexible, lightweight interface to the BitPay Bitcoin Payment Gateway API.
## Installation
The `bitpay-sdk` gem provides all the programattic tools required to implement a ruby client application for the BitPay REST API.
gem install bitpay-client
In your Gemfile:
gem 'bitpay-client', :require => 'bitpay'
Or directly:
require 'bitpay'
## Configuration
The bitpay client creates a cryptographically secure connection to your server by pairing an API code with keys stored on your server. The library generates the keys as a .pem file, which is stored in `$HOME/.bitpay/bitpay.pem` or preferentially in an environment variable.
The client will generate a key when initialized if one does not already exist.
## Basic Usage
### Pairing with Bitpay.com
To pair with bitpay.com you need to have an approved merchant account.
1. Login to your account
1. Navigate to bitpay.com/api-tokens (Dashboard > My Account > API Tokens)
1. Copy an existing pairing code or create a new token and copy the pairing code.
1. Use the bitpay command line tool to pair with bitpay.com `bitpay pair <pairing_code>`
### To create an invoice with a paired client:
client = BitPay::Client.new
invoice = client.create_invoice (id: <id>, price: <price>, currency: <currency>, facade: <facade>)
With invoice creation, `price` and `currency` are the only required fields. If you are sending a customer from your website to make a purchase, setting `redirectURL` will redirect the customer to your website when the invoice is paid.
Response will be a hash with information on your newly created invoice. Send your customer to the `url` to complete payment:
{
"id" => "DGrAEmbsXe9bavBPMJ8kuk",
"url" => "https://bitpay.com/invoice?id=DGrAEmbsXe9bavBPMJ8kuk",
"status" => "new",
"btcPrice" => "0.0495",
"price" => 10,
"currency" => "USD",
"invoiceTime" => 1383265343674,
"expirationTime" => 1383266243674,
"currentTime" => 1383265957613
}
There are many options available when creating invoices, which are listed in the [BitPay API documentation](https://bitpay.com/bitcoin-payment-gateway-api).
To get updated information on this invoice, make a get call with the id returned:
invoice = client.get_public_invoice(DGrAEmbsXe9bavBPMJ8kuk)'
## Testnet Usage
During development and testing, take advantage of the [Bitcoin TestNet](https://en.bitcoin.it/wiki/Testnet) by passing a custom `api_uri` option on initialization:
BitPay::Client.new("myAPIKey", {api_uri: "https://test.bitpay.com/api"})
Note that in order to pair with testnet, you will need a pairing code from test.bitpay.com and will need to use the bitpay client with the --test option.
## API Documentation
API Documentation is available on the [BitPay site](https://bitpay.com/api).
## RDoc/YARD Documentation
The code has been fully code documented, and the latest version is always available at the [Rubydoc Site](http://rubydoc.info/gems/bitpay-client).
## Running the Tests
$ bundle install
$ bundle exec rake
In addition to a full test suite, there is Travis integration for MRI 1.9, JRuby and Rubinius.
## [Getting Started &raquo;](https://github.com/bitpay/ruby-client/blob/master/GUIDE.md)
## Found a bug?
Let us know! Send a pull request or a patch. Questions? Ask! We're here to help. We will respond to all filed issues.

View File

@ -1,15 +1,24 @@
require "bundler/gem_tasks"
require 'rspec/core/rake_task'
require 'capybara'
require 'capybara/poltergeist'
require 'mongo'
require 'cucumber'
require 'cucumber/rake/task'
require_relative 'config/constants.rb'
require_relative 'config/capybara.rb'
RSpec::Core::RakeTask.new(:spec)
task :default => :spec
#task :default => :spec
task :default => :default_tasks
Cucumber::Rake::Task.new(:features) do |t|
t.cucumber_opts = "features --format pretty"
end
desc "Run BitPay tests"
task :default_tasks do
Rake::Task["spec"].invoke
Rake::Task["features"].invoke
end
desc "Bitpay Tasks"
namespace :bitpay do
@ -17,26 +26,14 @@ namespace :bitpay do
desc "Clear all claim codes from the test server."
task :clear_claim_codes do
puts "clearing claim codes"
Capybara.visit ROOT_ADDRESS
Capybara.click_link('Login')
Capybara.fill_in 'email', :with => TEST_USER
Capybara.fill_in 'password', :with => TEST_PASS
Capybara.click_button('loginButton')
Capybara.click_link "My Account"
Capybara.click_link "API Tokens", match: :first
while Capybara.page.has_selector?(".token-claimcode") || Capybara.page.has_selector?(".token-requiredsins-key") do
Capybara.page.find(".api-manager-actions-edit", match: :first).click
Capybara.page.find(".api-manager-actions-revoke", match: :first).click
Capybara.click_button("Confirm Revoke")
# this back and forth bit is here because no other reload mechanism worked, and without it the task errors out: either because it can't find the revoke button or it finds multiple elements at each click point
Capybara.page.go_back
Capybara.click_link "API Tokens", match: :first
end
client = Mongo::MongoClient.new
db = client['bitpay-dev']
coll = db['tokenaccesses']
coll.remove()
puts "claim codes cleared"
end
desc "Clear rate limiters from local mongo host"
task :clear_rate_limiters do
puts "clearing rate limiters"
client = Mongo::MongoClient.new
@ -46,7 +43,35 @@ namespace :bitpay do
puts "rate limiters cleared"
end
desc "Clear local pem and token file"
task :clear_local_files do
puts "clearing local files"
HOME_DIR = File.join(Dir.home, '.bitpay')
KEY_FILE = File.join(HOME_DIR, 'bitpay.pem')
TOKEN_FILE = File.join(HOME_DIR, 'tokens.json')
File.delete(KEY_FILE) if File.file?(KEY_FILE)
File.delete(TOKEN_FILE) if File.file?(TOKEN_FILE)
puts "local files cleared"
end
desc "Clear tokens, rate limiters, and local files."
task :clear do
["bitpay:clear_local_files", "bitpay:clear_rate_limiters", "bitpay:clear_claim_codes"].each{|task| Rake::Task[task].reenable}
["bitpay:clear_local_files", "bitpay:clear_rate_limiters", "bitpay:clear_claim_codes"].each{|task| Rake::Task[task].invoke}
end
desc "Run specs and clear claim codes and rate_limiters."
task :spec_clear => ['spec', 'clear_claim_codes', 'clear_rate_limiters']
desc "Run specs, clear data, run cukes, clear data"
task :tests_clear do
Rake::Task["bitpay:clear"].invoke
Rake::Task["spec"].invoke
Rake::Task["bitpay:clear"].reenable
Rake::Task["bitpay:clear"].invoke
Rake::Task["features"].invoke
Rake::Task["bitpay:clear"].reenable
Rake::Task["bitpay:clear"].invoke
end
end

View File

@ -1,3 +0,0 @@
#!/usr/bin/env ruby
require_relative '../lib/bitpay'
require_relative '../lib/bitpay/cli'

View File

@ -1,35 +0,0 @@
require './lib/bitpay/version.rb'
Gem::Specification.new do |s|
s.name = 'bitpay-client'
s.version = BitPay::VERSION
s.licenses = ['MIT']
s.authors = 'Bitpay, Inc.'
s.email = 'info@bitpay.com'
s.homepage = 'https://github.com/bitpay/ruby-client'
s.summary = 'Official ruby client library for the BitPay API'
s.description = 'Powerful, flexible, lightweight, thread-safe interface to the BitPay developers API'
s.files = `git ls-files`.split("\n")
s.require_paths = ["lib"]
s.rubyforge_project = s.name
s.required_rubygems_version = '>= 1.3.4'
s.required_ruby_version = '~> 2.1'
s.bindir = 'bin'
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.add_dependency 'json'
s.add_dependency 'rack', '>= 0'
s.add_dependency 'ecdsa'
s.add_dependency 'commander'
s.add_development_dependency 'rake'
s.add_development_dependency 'webmock'
s.add_development_dependency 'pry'
s.add_development_dependency 'pry-byebug'
s.add_development_dependency 'pry-rescue'
s.add_development_dependency 'capybara'
s.add_development_dependency 'poltergeist'
s.add_development_dependency 'airborne'
s.add_development_dependency 'rspec'
s.add_development_dependency 'mongo'
end

33
bitpay-sdk.gemspec Normal file
View File

@ -0,0 +1,33 @@
require './lib/bitpay/version.rb'
Gem::Specification.new do |s|
s.name = 'bitpay-sdk'
s.version = BitPay::VERSION
s.licenses = ['MIT']
s.authors = 'Bitpay, Inc.'
s.email = 'info@bitpay.com'
s.homepage = 'https://github.com/bitpay/ruby-client'
s.summary = 'Official Ruby library for the BitPay API'
s.description = 'Powerful, flexible, lightweight, thread-safe interface to the BitPay developers API'
s.files = `git ls-files`.split("\n")
s.require_paths = ["lib"]
s.rubyforge_project = s.name
s.required_rubygems_version = '>= 1.3.4'
s.required_ruby_version = '~> 2.1'
s.bindir = 'bin'
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.add_dependency 'bitpay-key-utils', '~>2.0.0'
s.add_development_dependency 'rack', '~> 2.0'
s.add_development_dependency 'rake', '12.0'
s.add_development_dependency 'webmock', '1.18.0'
s.add_development_dependency 'pry', '0.10.1'
s.add_development_dependency 'pry-byebug', '2.0.0'
s.add_development_dependency 'pry-rescue', '1.4.1'
s.add_development_dependency 'cucumber', '~> 1.3.17'
s.add_development_dependency 'airborne', '0.0.20'
s.add_development_dependency 'rspec', '3.1.0'
s.add_development_dependency 'mongo', '1.11.1'
s.add_development_dependency 'coveralls'
end

View File

@ -1,5 +0,0 @@
Capybara.javascript_driver = :poltergeist
Capybara.default_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
Capybara::Poltergeist::Driver.new(app, js_errors: false, phantomjs_options: ['--ignore-ssl-errors=yes'])
end

View File

@ -1,3 +1,12 @@
ROOT_ADDRESS = ENV['RCROOTADDRESS']
TEST_USER = ENV['RCTESTUSER']
TEST_PASS = ENV['RCTESTPASSWORD']
## Verifies test variables have been set correctly
#
# Use 'set_constants.sh' to pre-configure test variables
# e.g.
# source ./spec/set_constants.sh https://test.bitpay.com testuser@gmail.com mypassword
#
APIURI = ENV['BITPAYAPI']
# Specify a bitpay txid which has 6+ confirmations. Default belongs to 'bitpayrubyclient@gmail.com' test account
REFUND_TRANSACTION = ENV['REFUND_TRANSACTION']
REFUND_ADDRESS = ENV['REFUND_ADDRESS']

View File

@ -0,0 +1,28 @@
@invoices
Feature: creating an invoice
The user won't get any money
If they can't
Create Invoices
Background:
Given the user is authenticated with BitPay
Scenario Outline: The request is correct
When the user creates an invoice for <price> <currency>
Then they should recieve an invoice in response for <price> <currency>
Examples:
| price | currency |
| "5.23" | "USD" |
| "10.21" | "EUR" |
| "0.225" | "BTC" |
Scenario Outline: The invoice contains illegal characters
When the user creates an invoice for <price> <currency>
Then they will receive a BitPay::ArgumentError matching <message>
Examples:
| price | currency | message |
| "5,023" | "USD" | "Price must be formatted as a float" |
| "3.21" | "EaUR" | "Currency is invalid." |
| "" | "USD" | "Price must be formatted as a float" |
| "Ten" | "USD" | "Price must be formatted as a float" |
| "10" | "" | "Currency is invalid." |

19
features/pairing.feature Normal file
View File

@ -0,0 +1,19 @@
Feature: pairing with bitpay
In order to access bitpay
It is required that the library
Is able to pair successfully
Scenario: the client has a correct pairing code
Given the user pairs with BitPay with a valid pairing code
Then the user receives a require token from bitpay
Scenario: the client initiates pairing
Given the user performs a client-side pairing
Then the user receives an inactive token from bitpay
Scenario Outline: the client has a bad pairing code
Given the user fails to pair with a semantically <valid> code <code>
Then they will receive a <error> matching <message>
Examples:
| valid | code | error | message |
| invalid | "a1b2c3d4" | BitPay::ArgumentError | "pairing code is not legal" |

23
features/refunds.feature Normal file
View File

@ -0,0 +1,23 @@
@refunds
Feature: issuing a refund
The merchant wants to issue a refund
So that they can serve their customers
Background:
Given the user is authenticated with BitPay
Scenario: creating a refund
Given the user creates a refund
Then they will receive a refund id
Scenario: retrieving a refund
Given the user requests a specific refund
Then they will receive the refund
Scenario: retrieving all refunds
Given the user requests all refunds for an invoice
Then they will receive an array of refunds
Scenario: canceling a refund
Given a properly formatted cancellation request
Then the refund will be cancelled

View File

@ -0,0 +1,11 @@
Feature: retrieving an invoice
The user may want to retrieve invoices
So that they can view them
Scenario: Correct public request
Given that a user knows an invoice id
Then they can retrieve the public version of that invoice
Scenario: Correct merchant request
Given that a user knows an invoice id
Then they can retrieve the merchant-scoped version of that invoice

View File

@ -0,0 +1,31 @@
When(/^the user (?:tries to |)creates? an invoice (?:for|without) "(.*?)" (?:or |and |)"(.*?)"$/) do |price, currency|
begin
@response = @client.create_invoice(price: price, currency: currency, facade: 'merchant')
rescue => error
@error = error
end
end
Then(/^they should recieve an invoice in response for "(.*?)" "(.*?)"$/) do |price, currency|
raise "#{@response['price']} != #{price} or #{@response['currency']} != #{currency}" unless (price == @response['price'].to_s && currency == @response['currency'])
end
Given(/^there is an invalid token$/) do
pending # express the regexp above with the code you wish you had
end
Given(/^that a user knows an invoice id$/) do
@client = new_client_from_stored_values
@id = (@client.create_invoice(price: 3, currency: "USD", facade: 'merchant' ))['id']
end
Then(/^they can retrieve the public version of that invoice$/) do
invoice = @client.get_public_invoice(id: @id)
raise "That's the wrong invoice" unless invoice['id'] == @id
end
Then(/^they can retrieve the merchant\-scoped version of that invoice$/) do
invoice = @client.get_invoice(id: @id)
raise "That's the wrong invoice" unless invoice['id'] == @id
end

View File

@ -0,0 +1,56 @@
@token = nil
@error = nil
When(/^the user pairs with BitPay(?: with a valid pairing code|)$/) do
@client = new_client_from_stored_values
claim_code = get_claim_code_from_server @client
sleep 1 # rate limit compliance
@token = @client.pair_pos_client(claim_code)
end
Given(/^the user is authenticated with BitPay$/) do
@client = new_client_from_stored_values
raise "client not authenticated" unless client_has_tokens(@client)
end
Given(/^the user is paired with BitPay$/) do
raise "Client is not paired" unless @client.verify_tokens
end
Then(/^the user receives an? ([A-z]+) token from bitpay$/) do |expected|
actual = @token[0]["policies"][0]["method"]
raise "Token not correct, #{actual} != #{expected}" unless actual == expected
end
Given(/^the user has a bad pairing_code "(.*?)"$/) do |arg1|
# This is a no-op, pairing codes are transient and never actually saved
end
Then(/^the user fails to pair with a semantically (?:in|)valid code "(.*?)"$/) do |code|
pem = BitPay::KeyUtils.generate_pem
client = BitPay::SDK::Client.new(api_uri: APIURI, pem: pem, insecure: true)
begin
sleep 1 # rate limit compliance
client.pair_pos_client(code)
raise "pairing unexpectedly worked"
rescue => error
@error = error
true
end
end
Then(/^they will receive an? (.*?) matching "(.*?)"$/) do |error_class, error_message|
raise "Error: #{@error.class}, message: #{@error.message}" unless Object.const_get(error_class) == @error.class && @error.message.include?(error_message)
end
Given(/^the user performs a client\-side pairing$/) do
sleep 1
pem = BitPay::KeyUtils.generate_pem
@client = BitPay::SDK::Client.new(api_uri: APIURI, pem: pem, insecure: true)
@token = @client.pair_client({facade: 'merchant'})
end
Then(/^the user has a merchant token$/) do
tokens = {'merchant' => @token}
raise "Merchant token not authorized" unless @client.verify_tokens(tokens: tokens)
end

View File

@ -0,0 +1,37 @@
Given(/^the user creates a refund$/) do
sleep(1)
@response = @client.refund_invoice(id: REFUND_TRANSACTION, params: {amount: 1, currency: 'USD', bitcoinAddress: REFUND_ADDRESS})
end
Then(/^they will receive a refund id$/) do
@refund_id = @response["id"]
expect(@refund_id).not_to be_empty
end
Given(/^the user requests a specific refund$/) do
@response = @client.get_refund(invoice_id: REFUND_TRANSACTION, request_id: @refund_id)
end
Then(/^they will receive the refund$/) do
expect(@response.first["status"]).not_to be_empty
end
Given(/^the user requests all refunds for an invoice$/) do
client = new_client_from_stored_values
@response = client.get_all_refunds_for_invoice(id: REFUND_TRANSACTION)
end
Then(/^they will receive an array of refunds$/) do
expect(@response).to be_instance_of Array
end
Given(/^a properly formatted cancellation request$/) do
sleep(1)
client = new_client_from_stored_values
@refund_id = client.get_all_refunds_for_invoice(id: REFUND_TRANSACTION).first["id"]
@response = client.cancel_refund(invoice_id: REFUND_TRANSACTION, request_id: @refund_id)
end
Then(/^the refund will be cancelled$/) do
expect(@response).to eq("Success")
end

View File

@ -0,0 +1,31 @@
require 'pry'
require 'fileutils'
require File.join File.dirname(__FILE__), '..', '..', 'lib', 'bitpay_sdk.rb'
require_relative '../../config/constants.rb'
module BitPay
# Location for API Credentials
BITPAY_CREDENTIALS_DIR = File.join(Dir.home, ".bitpay")
PRIVATE_KEY_FILE = 'bitpay.pem'
PRIVATE_KEY_PATH = File.join(BITPAY_CREDENTIALS_DIR, PRIVATE_KEY_FILE)
TOKEN_FILE = 'tokens.json'
TOKEN_FILE_PATH = File.join(BITPAY_CREDENTIALS_DIR, TOKEN_FILE)
end
def new_client_from_stored_values
pem = ENV['BITPAYPEM'].gsub("\\n", "\n")
BitPay::SDK::Client.new(api_uri: APIURI, pem: pem, insecure: true)
end
def get_claim_code_from_server client
token = client.get(path: "tokens")["data"].select{|tuple| tuple["merchant"]}.first.values.first
client.post(path: "tokens", token: token, params: {facade: "pos"})["data"][0]["pairingCode"]
end
def client_has_tokens client
data = client.get(path: "tokens")["data"]
data.select{|tuple| tuple["pos"]}.any? && data.select{|tuple| tuple["merchant"]}.any?
end

View File

@ -1,63 +0,0 @@
# license Copyright 2011-2014 BitPay, Inc., MIT License
# see http://opensource.org/licenses/MIT
# or https://github.com/bitpay/php-bitpay-client/blob/master/LICENSE
require 'rubygems'
require 'commander/import'
program :name, 'BitPay Ruby Library CLI'
program :version, BitPay::VERSION
program :description, 'Official BitPay Ruby API Client. Use to securely register your client with the BitPay API endpoint. '
program :help_formatter, :compact
command :pair do |c|
c.syntax = 'bitpay pair <code>'
c.summary = "Pair the local keys to a bitpay account."
c.option '--test', "Use the bitpay test server"
c.option '--custom <custom>', "Use a custom bitpay URI"
c.option '--insecure <insecure>', "Use an insecure custom bitpay URI"
c.action do |args, options|
raise ArgumentError, "Pairing failed, please call argument as 'bitpay pair <code> [options]'" unless args.first
case
when options.test
client = BitPay::Client.new(api_uri: "https://test.bitpay.com")
message = "Paired with test.bitpay.com"
when options.custom
client = BitPay::Client.new(api_uri: options.custom)
message = "Paired with #{options.custom}"
when options.insecure
client = BitPay::Client.new(insecure: true, api_uri: options.insecure)
message = "Paired with #{options.insecure}"
else
client = BitPay::Client.new
message = "Paired with bitpay.com"
end
begin
client.pair_pos_client args.first
puts message
rescue Exception => e
puts e.message
end
end
end
command :show_keys do |c|
c.syntax = 'bitpay show_keys'
c.summary = "Read current environment's key information to STDOUT"
c.description = ''
c.example 'description', 'command example'
c.action do |args, options|
pem = BitPay::KeyUtils.get_local_pem_file
private_key = BitPay::KeyUtils.get_private_key_from_pem pem
public_key = BitPay::KeyUtils.get_public_key_from_pem pem
client_id = BitPay::KeyUtils.generate_sin_from_pem pem
puts "Current BitPay Client Keys:\n"
puts "Private Key: #{private_key}"
puts "Public Key: #{public_key}"
puts "Client ID: #{client_id}"
end
end

View File

@ -1,4 +1,4 @@
# license Copyright 2011-2014 BitPay, Inc., MIT License
# license Copyright 2011-2015 BitPay, Inc., MIT License
# see http://opensource.org/licenses/MIT
# or https://github.com/bitpay/php-bitpay-client/blob/master/LICENSE
@ -6,152 +6,175 @@ require 'uri'
require 'net/https'
require 'json'
require_relative 'key_utils'
require 'bitpay_key_utils'
require_relative 'rest_connector'
module BitPay
# This class is used to instantiate a BitPay Client object. It is expected to be thread safe.
#
class Client
module SDK
class Client
include BitPay::RestConnector
# @return [Client]
# @example
# # Create a client with a pem file created by the bitpay client:
# client = BitPay::SDK::Client.new
def initialize(opts={})
@pem = opts[:pem] || ENV['BITPAY_PEM'] || KeyUtils.generate_pem
@key = KeyUtils.create_key @pem
@priv_key = KeyUtils.get_private_key @key
@pub_key = KeyUtils.get_public_key @key
@client_id = KeyUtils.generate_sin_from_pem @pem
@uri = URI.parse opts[:api_uri] || API_URI
@user_agent = opts[:user_agent] || USER_AGENT
@https = Net::HTTP.new @uri.host, @uri.port
@https.use_ssl = true
@https.open_timeout = 10
@https.read_timeout = 10
# @return [Client]
# @example
# # Create a client with a pem file created by the bitpay client:
# client = BitPay::Client.new
def initialize(opts={})
@pem = opts[:pem] || ENV['BITPAY_PEM'] || KeyUtils.retrieve_or_generate_pem
@key = KeyUtils.create_key @pem
@priv_key = KeyUtils.get_private_key @key
@pub_key = KeyUtils.get_public_key @key
@client_id = KeyUtils.generate_sin_from_pem @pem
@uri = URI.parse opts[:api_uri] || API_URI
@user_agent = opts[:user_agent] || USER_AGENT
@https = Net::HTTP.new @uri.host, @uri.port
@https.use_ssl = true
@https.ca_file = CA_FILE
@https.ca_file = CA_FILE
@tokens = opts[:tokens] || {}
# Option to disable certificate validation in extraordinary circumstance. NOT recommended for production use
@https.verify_mode = opts[:insecure] == true ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
# Option to disable certificate validation in extraordinary circumstance. NOT recommended for production use
@https.verify_mode = opts[:insecure] == true ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
# Option to enable http request debugging
@https.set_debug_output($stdout) if opts[:debug] == true
end
## Pair client with BitPay service
# => Pass empty hash {} to retreive client-initiated pairing code
# => Pass {pairingCode: 'WfD01d2'} to claim a server-initiated pairing code
#
def pair_client(params={})
tokens = post(path: 'tokens', params: params)
return tokens["data"]
end
## Compatibility method for pos pairing
#
def pair_pos_client(claimCode)
raise BitPay::ArgumentError, "pairing code is not legal" unless verify_claim_code(claimCode)
pair_client({pairingCode: claimCode})
end
## Create bitcoin invoice
#
# Defaults to pos facade, also works with merchant facade
#
def create_invoice(price:, currency:, facade: 'pos', params:{})
raise BitPay::ArgumentError, "Illegal Argument: Price must be formatted as a float" unless
price.is_a?(Numeric) ||
/^[[:digit:]]+(\.[[:digit:]]{2})?$/.match(price) ||
currency == 'BTC' && /^[[:digit:]]+(\.[[:digit:]]{1,8})?$/.match(price)
raise BitPay::ArgumentError, "Illegal Argument: Currency is invalid." unless /^[[:upper:]]{3}$/.match(currency)
params.merge!({price: price, currency: currency})
token = get_token(facade)
invoice = post(path: "invoices", token: token, params: params)
invoice["data"]
end
## Gets the privileged merchant-version of the invoice
# Requires merchant facade token
#
def get_invoice(id:)
token = get_token('merchant')
invoice = get(path: "invoices/#{id}", token: token)
invoice["data"]
end
def get_invoices(params = {})
token = get_token('merchant')
invoice = get(path: "invoices", token: token, params: params)
invoice["data"]
end
## Gets the public version of the invoice
def get_public_invoice(id:)
invoice = get(path: "invoices/#{id}", public_request: true)
invoice["data"]
end
# Option to enable http request debugging
@https.set_debug_output($stdout) if opts[:debug] == true
# Load all the available tokens into @tokens
load_tokens
end
def pair_pos_client(claimCode)
response = set_pos_token(claimCode)
case response.code
when "200"
get_token 'pos'
when "500"
raise BitPayError, JSON.parse(response.body)["error"]
else
raise BitPayError, "#{response.code}: #{JSON.parse(response.body)}"
## Refund paid BitPay invoice
#
# If invoice["data"]["flags"]["refundable"] == true the a refund address was
# provided with the payment and the refund_address parameter is an optional override
#
# Amount and Currency are required fields for fully paid invoices but optional
# for under or overpaid invoices which will otherwise be completely refunded
#
# Requires merchant facade token
#
# @example
# client.refund_invoice(id: 'JB49z2MsDH7FunczeyDS8j', params: {amount: 10, currency: 'USD', bitcoinAddress: '1Jtcygf8W3cEmtGgepggtjCxtmFFjrZwRV'})
#
def refund_invoice(id:, params:{})
invoice = get_invoice(id: id)
refund = post(path: "invoices/#{id}/refunds", token: invoice["token"], params: params)
refund["data"]
end
response
end
def create_invoice(id:, price:, currency:, facade: 'pos', params:{})
params.merge!({price: price, currency: currency})
response = send_request("POST", "invoices", facade: facade, params: params)
response["data"]
end
def get_public_invoice(id:)
request = Net::HTTP::Get.new("/invoices/#{id}")
response = @https.request request
(JSON.parse response.body)["data"]
end
## Generates REST request to api endpoint
def send_request(verb, path, facade: 'merchant', params: {}, token: nil)
token ||= @tokens[facade] || raise(BitPayError, "No token for specified facade: #{facade}")
# Verb-specific logic
case verb.upcase
when "GET"
urlpath = '/' + path + '?nonce=' + KeyUtils.nonce + '&token=' + token
request = Net::HTTP::Get.new urlpath
request['X-Signature'] = KeyUtils.sign(@uri.to_s + urlpath, @priv_key)
when "PUT"
when "POST" # Requires a GUID
urlpath = '/' + path
request = Net::HTTP::Post.new urlpath
params[:token] = token
params[:nonce] = KeyUtils.nonce
params[:guid] = SecureRandom.uuid
params[:id] = @client_id
request.body = params.to_json
request['X-Signature'] = KeyUtils.sign(@uri.to_s + urlpath + request.body, @priv_key)
when "DELETE"
raise(BitPayError, "Invalid HTTP verb: #{verb.upcase}")
## Get All Refunds for Invoice
# Returns an array of all refund requests for a specific invoice,
#
# Requires merchant facade token
#
# @example:
# client.get_all_refunds_for_invoice(id: 'JB49z2MsDH7FunczeyDS8j')
#
def get_all_refunds_for_invoice(id:)
urlpath = "invoices/#{id}/refunds"
invoice = get_invoice(id: id)
refunds = get(path: urlpath, token: invoice["token"])
refunds["data"]
end
# Build request headers and submit
request['User-Agent'] = @user_agent
request['Content-Type'] = 'application/json'
request['X-BitPay-Plugin-Info'] = 'Rubylib' + VERSION
request['X-Identity'] = @pub_key
response = @https.request request
JSON.parse response.body
end
## Get Refund
# Requires merchant facade token
#
# @example:
# client.get_refund(id: 'JB49z2MsDH7FunczeyDS8j', request_id: '4evCrXq4EDXk4oqDXdWQhX')
#
def get_refund(invoice_id:, request_id:)
urlpath = "invoices/#{invoice_id}/refunds/#{request_id}"
invoice = get_invoice(id: invoice_id)
refund = get(path: urlpath, token: invoice["token"])
refund["data"]
end
## Cancel Refund
# Requires merchant facade token
#
# @example:
# client.cancel_refund(id: 'JB49z2MsDH7FunczeyDS8j', request_id: '4evCrXq4EDXk4oqDXdWQhX')
#
def cancel_refund(invoice_id:, request_id:)
urlpath = "invoices/#{invoice_id}/refunds/#{request_id}"
refund = get_refund(invoice_id: invoice_id, request_id: request_id)
deletion = delete(path: urlpath, token: refund["token"])
deletion["data"]
end
##### PRIVATE METHODS #####
private
## Requests token by appending nonce and signing URL
# Returns a hash of available tokens
#
def load_tokens
urlpath = '/tokens?nonce=' + KeyUtils.nonce
request = Net::HTTP::Get.new(urlpath)
request['content-type'] = "application/json"
request['user-agent'] = @user_agent
request['x-identity'] = @pub_key
request['x-signature'] = KeyUtils.sign(@uri.to_s + urlpath, @priv_key)
response = @https.request request
# /tokens returns an array of hashes. Let's turn it into a more useful single hash
token_array = JSON.parse(response.body)["data"] || {}
tokens = {}
token_array.each do |t|
tokens[t.keys.first] = t.values.first
## Checks that the passed tokens are valid by
# comparing them to those that are authorized by the server
#
# Uses local @tokens variable if no tokens are passed
# in order to validate the connector is properly paired
#
def verify_tokens(tokens: @tokens)
server_tokens = refresh_tokens
tokens.each{|key, value| return false if server_tokens[key] != value}
return true
end
@tokens = tokens
return tokens
private
def verify_claim_code(claim_code)
regex = /^[[:alnum:]]{7}$/
matches = regex.match(claim_code)
!(matches.nil?)
end
end
## Retrieves specified token from hash, otherwise tries to refresh @tokens and retry
def set_pos_token(claim_code)
params = {pairingCode: claim_code}
urlpath = '/tokens'
request = Net::HTTP::Post.new urlpath
params[:guid] = SecureRandom.uuid
params[:id] = @client_id
request.body = params.to_json
request['User-Agent'] = @user_agent
request['Content-Type'] = 'application/json'
request['X-BitPay-Plugin-Info'] = 'Rubylib' + VERSION
@https.request request
end
def get_token(facade)
token = @tokens[facade] || load_tokens[facade] || raise(BitPayError, "Not authorized for facade: #{facade}")
end
end
end

View File

@ -1,143 +0,0 @@
# license Copyright 2011-2014 BitPay, Inc., MIT License
# see http://opensource.org/licenses/MIT
# or https://github.com/bitpay/php-bitpay-client/blob/master/LICENSE
require 'uri'
require 'net/https'
require 'json'
require 'openssl'
require 'ecdsa'
require 'securerandom'
require 'digest/sha2'
require 'cgi'
module BitPay
class KeyUtils
class << self
def nonce
Time.now.utc.strftime('%Y%m%d%H%M%S%L')
end
## Generates a new private key and writes to local FS
#
def retrieve_or_generate_pem
begin
pem = get_local_pem_file
rescue
pem = generate_pem
end
pem
end
def generate_pem
key = OpenSSL::PKey::EC.new("secp256k1")
key.generate_key
write_pem_file(key)
key.to_pem
end
def create_key pem
OpenSSL::PKey::EC.new(pem)
end
def create_new_key
key = OpenSSL::PKey::EC.new("secp256k1")
key.generate_key
key
end
def write_pem_file key
FileUtils.mkdir_p(BITPAY_CREDENTIALS_DIR)
File.open(PRIVATE_KEY_PATH, 'w') { |file| file.write(key.to_pem) }
end
## Gets private key from ENV variable or local FS
#
def get_local_pem_file
ENV['BITPAY_PEM'] || File.read(PRIVATE_KEY_PATH) || (raise BitPayError, MISSING_KEY)
end
def get_private_key key
key.private_key.to_int.to_s(16)
end
def get_public_key key
key.public_key.group.point_conversion_form = :compressed
key.public_key.to_bn.to_s(16).downcase
end
def get_private_key_from_pem pem
raise BitPayError, MISSING_KEY unless pem
key = OpenSSL::PKey::EC.new(pem)
get_private_key key
end
def get_public_key_from_pem pem
raise BitPayError, MISSING_KEY unless pem
key = OpenSSL::PKey::EC.new(pem)
get_public_key key
end
def generate_sin_from_pem(pem = nil)
#http://blog.bitpay.com/2014/07/01/bitauth-for-decentralized-authentication.html
#https://en.bitcoin.it/wiki/Identity_protocol_v1
# NOTE: All Digests are calculated against the binary representation,
# hence the requirement to use [].pack("H*") to convert to binary for each step
#Generate Private Key
key = pem.nil? ? get_local_pem_file : OpenSSL::PKey::EC.new(pem)
key.public_key.group.point_conversion_form = :compressed
public_key = key.public_key.to_bn.to_s(2)
step_one = Digest::SHA256.hexdigest(public_key)
step_two = Digest::RMD160.hexdigest([step_one].pack("H*"))
step_three = "0F02" + step_two
step_four_a = Digest::SHA256.hexdigest([step_three].pack("H*"))
step_four = Digest::SHA256.hexdigest([step_four_a].pack("H*"))
step_five = step_four[0..7]
step_six = step_three + step_five
encode_base58(step_six)
end
## Generate ECDSA signature
# This is the last method that requires the ecdsa gem, which we would like to replace
def sign(message, privkey)
group = ECDSA::Group::Secp256k1
digest = Digest::SHA256.digest(message)
signature = nil
while signature.nil?
temp_key = 1 + SecureRandom.random_number(group.order - 1)
signature = ECDSA.sign(group, privkey.to_i(16), digest, temp_key)
return ECDSA::Format::SignatureDerString.encode(signature).unpack("H*").first
end
end
########## Private Class Methods ################
## Base58 Encoding Method
#
private
def encode_base58 (data)
code_string = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
base = 58
x = data.hex
output_string = ""
while x > 0 do
remainder = x % base
x = x / base
output_string << code_string[remainder]
end
pos = 0
while data[pos,2] == "00" do
output_string << code_string[0]
pos += 2
end
output_string.reverse()
end
end
end
end

View File

@ -0,0 +1,106 @@
# license Copyright 2011-2015 BitPay, Inc., MIT License
# see http://opensource.org/licenses/MIT
# or https://github.com/bitpay/php-bitpay-client/blob/master/LICENSE
module BitPay
module RestConnector
def send_request(verb, path, facade: 'merchant', params: {}, token: nil)
token ||= get_token(facade)
case verb.upcase
when "GET"
return get(path: path, token: token, params: params)
when "POST"
return post(path: path, token: token, params: params)
else
raise(BitPayError, "Invalid HTTP verb: #{verb.upcase}")
end
end
def get(path:, token: nil, public_request: false, params: {})
urlpath = '/' + path + '?'
urlpath = urlpath + 'token=' + token if token
urlpath = urlpath + '&' + params.to_param if params.present?
request = Net::HTTP::Get.new urlpath
unless public_request
request['X-Signature'] = KeyUtils.sign(@uri.to_s + urlpath, @priv_key)
request['X-Identity'] = @pub_key
end
process_request(request)
end
def post(path:, token: nil, params:)
urlpath = '/' + path
request = Net::HTTP::Post.new urlpath
params[:token] = token if token
params[:guid] = SecureRandom.uuid
params[:id] = @client_id
request.body = params.to_json
if token
request['X-Signature'] = KeyUtils.sign(@uri.to_s + urlpath + request.body, @priv_key)
request['X-Identity'] = @pub_key
end
process_request(request)
end
def delete(path:, token: nil)
urlpath = '/' + path
urlpath = urlpath + '?token=' + token if token
request = Net::HTTP::Delete.new urlpath
request['X-Signature'] = KeyUtils.sign(@uri.to_s + urlpath, @priv_key)
request['X-Identity'] = @pub_key
process_request(request)
end
private
## Processes HTTP Request and returns parsed response
# Otherwise throws error
#
def process_request(request)
request['User-Agent'] = @user_agent
request['Content-Type'] = 'application/json'
request['X-BitPay-Plugin-Info'] = 'Rubylib' + VERSION
begin
response = @https.request request
rescue => error
raise BitPay::ConnectionError, "#{error.message}"
end
if response.kind_of? Net::HTTPSuccess
return JSON.parse(response.body)
elsif JSON.parse(response.body)["error"]
raise(BitPayError, "#{response.code}: #{JSON.parse(response.body)['error']}")
else
raise BitPayError, "#{response.code}: #{JSON.parse(response.body)}"
end
end
## Fetches the tokens hash from the server and
# updates @tokens
#
def refresh_tokens
response = get(path: 'tokens')["data"]
token_array = response || {}
tokens = {}
token_array.each do |t|
tokens[t.keys.first] = t.values.first
end
@tokens = tokens
return tokens
end
## Makes a request to /tokens for pairing
# Adds passed params as post parameters
# If empty params, retrieves server-generated pairing code
# If pairingCode key/value is passed, will pair client ID to this account
# Returns response hash
#
def get_token(facade)
token = @tokens[facade] || refresh_tokens[facade] || raise(BitPayError, "Not authorized for facade: #{facade}")
end
end
end

View File

@ -3,5 +3,5 @@
# or https://github.com/bitpay/php-bitpay-client/blob/master/LICENSE
module BitPay
VERSION = '2.0.0'
VERSION = '2.4.6'
end

View File

@ -18,17 +18,11 @@ module BitPay
TEST_API_URI = 'https://test.bitpay.com'
CLIENT_REGISTRATION_PATH = '/api-access-request'
# Location for API Credentials
BITPAY_CREDENTIALS_DIR = File.join(Dir.home, ".bitpay")
PRIVATE_KEY_FILE = 'bitpay.pem'
PRIVATE_KEY_PATH = File.join(BITPAY_CREDENTIALS_DIR, PRIVATE_KEY_FILE)
# User agent reported to API
USER_AGENT = 'ruby-bitpay-client '+VERSION
MISSING_KEY = 'No Private Key specified. Pass priv_key or set ENV variable PRIV_KEY'
MISSING_PEM = 'No pem file specified. Pass pem or set ENV variable BITPAY_PEM'
USER_AGENT = 'ruby-bitpay-sdk '+VERSION
class BitPayError < StandardError; end
class ConnectionError < Errno::ECONNREFUSED; end
end

View File

@ -1,40 +0,0 @@
# license Copyright 2011-2014 BitPay, Inc., MIT License
# see http://opensource.org/licenses/MIT
# or https://github.com/bitpay/php-bitpay-client/blob/master/LICENSE
require_relative 'bitpay.rb'
require_relative 'bitpay/key_utils.rb'
# Test SIN Generation class methods
# Generate SIN
ENV["PRIV_KEY"] = "16d7c3508ec59773e71ae728d29f41fcf5d1f380c379b99d68fa9f552ce3ebc3"
puts "privkey: #{ENV['PRIV_KEY']}"
puts "target SIN: TfFVQhy2hQvchv4VVG4c7j4XPa2viJ9HrR8"
puts "Derived SIN: #{BitPay::KeyUtils.get_client_id}"
puts "\n\n------------------\n\n"
uri = "https://localhost:8088"
#name = "Ridonculous.label That shouldn't work really"
name = "somethinginnocuous"
facade = "pos"
client_id = BitPay::KeyUtils.get_client_id
BitPay::KeyUtils.generate_registration_url(uri,name,facade,client_id)
puts "\n\n------------------\n\n"
#### Test Invoice Creation using directly assigned keys
## (Ultimately pubkey and SIN should be derived)
ENV["PRIV_KEY"] = "16d7c3508ec59773e71ae728d29f41fcf5d1f380c379b99d68fa9f552ce3ebc3"
#ENV["pub_key"] = "0353a036fb495c5846f26a3727a28198da8336ae4f5aaa09e24c14a4126b5d969d"
#ENV['SIN'] = "TfFVQhy2hQvchv4VVG4c7j4XPa2viJ9HrR8"
client = BitPay::Client.new({insecure: true, debug: false})
invoice = client.post 'invoices', {:price => 10.00, :currency => 'USD'}
puts "Here's the invoice: \n" + JSON.pretty_generate(invoice)

View File

@ -2,19 +2,35 @@ require 'spec_helper'
def tokens
{"data" =>
[{"merchant" => "MERCHANTTOKEN"},
{"pos" =>"POSTOKEN"},
[{"merchant" => "MERCHANT_TOKEN"},
{"pos" =>"POS_TOKEN"},
{"merchant/invoice" => "9kv7gGqZLoQ2fxbKEgfgndLoxwjp5na6VtGSH3sN7buX"}
]
}
end
describe BitPay::Client do
let(:bitpay_client) { BitPay::Client.new({api_uri: BitPay::TEST_API_URI}) }
describe BitPay::SDK::Client do
let(:bitpay_client) { BitPay::SDK::Client.new({api_uri: BitPay::TEST_API_URI}) }
let(:claim_code) { "a12bc3d" }
before do
allow(BitPay::KeyUtils).to receive(:nonce).and_return('1')
stub_request(:get, /#{BitPay::TEST_API_URI}\/tokens.*/).to_return(:status => 200, :body => tokens.to_json, :headers => {})
# Stub JSON responses from fixtures
stub_request(:get, /#{BitPay::TEST_API_URI}\/tokens.*/)
.to_return(:status => 200, :body => tokens.to_json, :headers => {})
stub_request(:get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID?token=MERCHANT_TOKEN").
to_return(:body => get_fixture('invoices_{id}-GET.json'))
stub_request(:get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID/refunds?token=MERCHANT_INVOICE_TOKEN").
to_return(:body => get_fixture('invoices_{id}_refunds-GET.json'))
stub_request(:get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID/refunds/TEST_REQUEST_ID?token=MERCHANT_INVOICE_TOKEN").
to_return(:body => get_fixture('invoices_{id}_refunds-GET.json'))
stub_request(:post, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID/refunds").
to_return(:body => get_fixture('invoices_{id}_refunds-POST.json'))
stub_request(:post, "#{BitPay::TEST_API_URI}/nuttin").
to_return(:body => get_fixture('response-nodata.json'))
stub_request(:get, "#{BitPay::TEST_API_URI}/nuttin").
to_return(:body => get_fixture('response-nodata.json'))
stub_request(:delete, "#{BitPay::TEST_API_URI}/nuttin").
to_return(:body => get_fixture('response-nodata.json'))
end
describe "#initialize" do
@ -26,6 +42,14 @@ describe BitPay::Client do
end
describe "requests to endpoint without data field" do
it "should return the json body" do
expect(bitpay_client.post(path: "nuttin", params: {})["facile"]).to eq("is easy")
expect(bitpay_client.get(path: "nuttin")["facile"]).to eq("is easy")
expect(bitpay_client.delete(path: "nuttin")["facile"]).to eq( "is easy")
end
end
describe "#send_request" do
before do
stub_const('ENV', {'BITPAY_PEM' => PEM})
@ -35,8 +59,15 @@ describe BitPay::Client do
it 'should generate a get request' do
stub_request(:get, /#{BitPay::TEST_API_URI}\/whatever.*/).to_return(:body => '{"awesome": "json"}')
bitpay_client.send_request("GET", "whatever", facade: "merchant")
expect(WebMock).to have_requested(:get, "#{BitPay::TEST_API_URI}/whatever?nonce=1&token=MERCHANTTOKEN")
expect(WebMock).to have_requested(:get, "#{BitPay::TEST_API_URI}/whatever?token=MERCHANT_TOKEN")
end
it 'should handle query parameters gracefully' do
stub_request(:get, /#{BitPay::TEST_API_URI}\/ledgers.*/).to_return(:body => '{"awesome": "json"}')
bitpay_client.send_request("GET", "ledgers/BTC?startDate=2015-01-01&endDate=2015-02-01", facade: "merchant")
expect(WebMock).to have_requested(:get, "#{BitPay::TEST_API_URI}/ledgers/BTC?startDate=2015-01-01&endDate=2015-02-01&token=MERCHANT_TOKEN")
end
end
context "POST" do
@ -50,16 +81,25 @@ describe BitPay::Client do
end
describe "#pair_pos_client" do
it 'throws a BitPayError with the error message if the token setting fails' do
before do
stub_const('ENV', {'BITPAY_PEM' => PEM})
end
it 'throws a BitPayError with the error message if the token setting fails' do
stub_request(:any, /#{BitPay::TEST_API_URI}.*/).to_return(status: 500, body: "{\n \"error\": \"Unable to create token\"\n}")
expect { bitpay_client.pair_pos_client(:claim_code) }.to raise_error(BitPay::BitPayError, 'Unable to create token')
expect { bitpay_client.pair_pos_client(claim_code) }.to raise_error(BitPay::BitPayError, '500: Unable to create token')
end
it 'gracefully handles 4xx errors' do
stub_const('ENV', {'BITPAY_PEM' => PEM})
stub_request(:any, /#{BitPay::TEST_API_URI}.*/).to_return(status: 403, body: "{\n \"error\": \"this is a 403 error\"\n}")
expect { bitpay_client.pair_pos_client(:claim_code) }.to raise_error(BitPay::BitPayError, '403: {"error"=>"this is a 403 error"}')
expect { bitpay_client.pair_pos_client(claim_code) }.to raise_error(BitPay::BitPayError, '403: this is a 403 error')
end
it 'short circuits on invalid pairing codes' do
100.times do
claim_code = an_illegal_claim_code
expect { bitpay_client.pair_pos_client(claim_code) }.to raise_error BitPay::ArgumentError, "pairing code is not legal"
end
end
end
@ -68,11 +108,83 @@ describe BitPay::Client do
before {stub_const('ENV', {'BITPAY_PEM' => PEM})}
it { is_expected.to respond_to(:create_invoice) }
it 'should make call to the server to create an invoice' do
stub_request(:post, /#{BitPay::TEST_API_URI}\/invoices.*/).to_return(:body => '{"data": "awesome"}')
bitpay_client.create_invoice(id: "addd", price: 20, currency: "USD")
assert_requested :post, "#{BitPay::TEST_API_URI}/invoices"
describe "should make the call to the server to create an invoice" do
it 'allows numeric input for the price' do
stub_request(:post, /#{BitPay::TEST_API_URI}\/invoices.*/).to_return(:body => '{"data": "awesome"}')
bitpay_client.create_invoice(price: 20.00, currency: "USD")
assert_requested :post, "#{BitPay::TEST_API_URI}/invoices"
end
it 'allows string input for the price' do
stub_request(:post, /#{BitPay::TEST_API_URI}\/invoices.*/).to_return(:body => '{"data": "awesome"}')
bitpay_client.create_invoice(price: "20.00", currency: "USD")
assert_requested :post, "#{BitPay::TEST_API_URI}/invoices"
end
end
it 'should pass through the API error message from load_tokens' do
stub_request(:get, /#{BitPay::TEST_API_URI}\/tokens.*/).to_return(status: 500, body: '{"error": "load_tokens_error"}')
expect { bitpay_client.create_invoice(price: 20, currency: "USD") }.to raise_error(BitPay::BitPayError, '500: load_tokens_error')
end
it 'verifies the validity of the price argument' do
expect { bitpay_client.create_invoice(price: "3,999", currency: "USD") }.to raise_error(BitPay::ArgumentError, 'Illegal Argument: Price must be formatted as a float')
end
it 'verifies the validity of the currency argument' do
expect { bitpay_client.create_invoice(price: "3999", currency: "UASD") }.to raise_error(BitPay::ArgumentError, 'Illegal Argument: Currency is invalid.')
end
end
describe '#refund_invoice' do
subject { bitpay_client }
before { stub_const('ENV', {'BITPAY_PEM' => PEM}) }
it { is_expected.to respond_to(:refund_invoice) }
it 'should get the token for the invoice' do
bitpay_client.refund_invoice(id: 'TEST_INVOICE_ID')
expect(WebMock).to have_requested :get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID?token=MERCHANT_TOKEN"
end
it 'should generate a POST to the invoices/refund endpoint' do
bitpay_client.refund_invoice(id: 'TEST_INVOICE_ID')
expect(WebMock).to have_requested :post, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID/refunds"
end
end
describe '#get_all_refunds_for_invoice' do
subject { bitpay_client }
before {stub_const('ENV', {'BITPAY_PEM' => PEM})}
it { is_expected.to respond_to(:get_all_refunds_for_invoice) }
it 'should get the token for the invoice' do
bitpay_client.get_all_refunds_for_invoice(id: 'TEST_INVOICE_ID')
expect(WebMock).to have_requested :get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID?token=MERCHANT_TOKEN"
end
it 'should GET all refunds' do
bitpay_client.get_all_refunds_for_invoice(id: 'TEST_INVOICE_ID')
expect(WebMock).to have_requested :get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID/refunds?token=MERCHANT_INVOICE_TOKEN"
end
end
describe '#get_refund' do
subject { bitpay_client }
before {stub_const('ENV', {'BITPAY_PEM' => PEM})}
it { is_expected.to respond_to(:get_refund) }
it 'should get the token for the invoice' do
bitpay_client.get_refund(invoice_id: 'TEST_INVOICE_ID', request_id: 'TEST_REQUEST_ID')
expect(WebMock).to have_requested :get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID?token=MERCHANT_TOKEN"
end
it 'should GET a single refund' do
bitpay_client.get_refund(invoice_id: 'TEST_INVOICE_ID', request_id: 'TEST_REQUEST_ID')
expect(WebMock).to have_requested :get, "#{BitPay::TEST_API_URI}/invoices/TEST_INVOICE_ID/refunds/TEST_REQUEST_ID?token=MERCHANT_INVOICE_TOKEN"
end
end
describe "#verify_tokens" do
subject { bitpay_client }
before {stub_const('ENV', {'BITPAY_PEM' => PEM})}
it { is_expected.to respond_to(:verify_tokens) }
end
end

View File

@ -1,28 +0,0 @@
require_relative '../spec_helper.rb'
describe "pairing a token", javascript: true, type: :feature do
let(:claimCode) do
visit ROOT_ADDRESS
click_link('Login')
fill_in 'email', :with => TEST_USER
fill_in 'password', :with => TEST_PASS
click_button('loginButton')
click_link "My Account"
click_link "API Tokens", match: :first
find(".token-access-new-button").find(".btn").click
find(".token-claimcode", match: :first).text
end
let(:pem) { BitPay::KeyUtils.generate_pem }
let(:client) { BitPay::Client.new(api_uri: ROOT_ADDRESS, pem: pem, insecure: true) }
context "pairing an unpaired client" do
it "should have no tokens before pairing" do
expect(client.instance_variable_get(:@tokens)).to be_empty
end
it "should have a pos token after pairing" do
client.pair_pos_client(claimCode)
expect(client.instance_variable_get(:@tokens)['pos']).not_to be_empty
end
end
end

View File

@ -1,37 +0,0 @@
require_relative '../spec_helper.rb'
describe "create an invoice", javascript: true, type: :feature do
before :all do
WebMock.allow_net_connect!
get_claim_code = -> {
visit ROOT_ADDRESS
click_link('Login')
fill_in 'email', :with => TEST_USER
fill_in 'password', :with => TEST_PASS
click_button('loginButton')
click_link "My Account"
click_link "API Tokens", match: :first
find(".token-access-new-button").find(".btn").click
find(".token-claimcode", match: :first).text
}
set_client = -> {
private_key = BitPay::KeyUtils.get_private_key_from_pem PEM
client = BitPay::Client.new(api_uri: ROOT_ADDRESS, pem: PEM, insecure: true)
client.pair_pos_client(get_claim_code.call)
client
}
@client ||= set_client.call
@invoice_id ||= SecureRandom.uuid
@price ||= (100..150).to_a.sample
@invoice = @client.create_invoice(id: @invoice_id, currency: "USD", price: @price)
end
it "should create an invoice" do
expect(@invoice["status"]).to eq "new"
end
it "should be able to retrieve an invoice" do
expect(@client.get_public_invoice(id: @invoice['id'])["price"]).to eq @price
end
end

View File

@ -1,13 +0,0 @@
require 'spec_helper'
context 'local variables' do
it "should find the root address" do
expect(ROOT_ADDRESS).not_to be_nil
end
it "should find the user" do
expect(TEST_USER).not_to be_nil
end
it "should find the user" do
expect(TEST_PASS).not_to be_nil
end
end

29
spec/fixtures/invoices-POST.json vendored Normal file
View File

@ -0,0 +1,29 @@
{
"facade": "pos/invoice",
"data": {
"url": "https://test.bitpay.com/invoice?id=2RSyNDvsiTrA31rPwnnEcd",
"status": "new",
"btcPrice": "0.037523",
"btcDue": "0.037523",
"price": 10,
"currency": "USD",
"exRates": {
"USD": 266.5
},
"invoiceTime": 1422319964413,
"expirationTime": 1422320864413,
"currentTime": 1422319964431,
"guid": "34d7be05-eb65-4f72-a2ce-79bf23e93f17",
"id": "2RSyNDvsiTrA31rPwnnEcd",
"btcPaid": "0.000000",
"rate": 266.5,
"exceptionStatus": false,
"paymentUrls": {
"BIP21": "bitcoin:mhPM48eieakd6AgCuHMwAtpFXE5yQ3N7om?amount=0.037523",
"BIP72": "bitcoin:mhPM48eieakd6AgCuHMwAtpFXE5yQ3N7om?amount=0.037523&r=https://test.bitpay.com/i/2RSyNDvsiTrA31rPwnnEcd",
"BIP72b": "bitcoin:?r=https://test.bitpay.com/i/2RSyNDvsiTrA31rPwnnEcd",
"BIP73": "https://test.bitpay.com/i/2RSyNDvsiTrA31rPwnnEcd"
},
"token": "2RPipMRUXAvt5wAfthCzF7Tj4SppBWPHGQ7hCeWYeWDm7RtwUtDds1XUNt11VTf5C6UfCAACBhsKwjW6SAocLsd7"
}
}

35
spec/fixtures/invoices_{id}-GET.json vendored Normal file
View File

@ -0,0 +1,35 @@
{
"facade": "merchant/invoice",
"data": {
"url": "https://test.bitpay.com/invoice?id=CcRgegwTMs866Sr7vdLnru",
"status": "complete",
"btcPrice": "0.074661",
"btcDue": "0.000000",
"price": 15.79,
"currency": "USD",
"exRates": {
"USD": 211.49
},
"invoiceTime": 1421719631301,
"expirationTime": 1421720531301,
"currentTime": 1422316288768,
"id": "TEST_INVOICE_ID",
"btcPaid": "0.074661",
"rate": 211.49,
"exceptionStatus": false,
"transactions": [
{
"amount": 7466100,
"confirmations": 6,
"time": "2015-01-20T02:07:46.000Z",
"receivedTime": "2015-01-20T02:07:45.881Z"
}
],
"flags": {
"refundable": true
},
"token": "MERCHANT_INVOICE_TOKEN",
"buyer": {
}
}
}

View File

@ -0,0 +1,17 @@
{
"facade": "merchant/supportRequest",
"data": [
{
"id": "TEST_REQUEST_ID",
"requestDate": "2015-01-27T00:36:12.360Z",
"status": "pending",
"token": "REFUND_REQUEST_TOKEN"
},
{
"id": "ANOTHER_ID",
"requestDate": "2015-01-27T00:36:12.360Z",
"status": "OTHER_STATUS",
"token": "ANOTHER_REFUND_REQUEST_TOKEN"
}
]
}

View File

@ -0,0 +1,9 @@
{
"facade": "merchant/supportRequest",
"data": {
"id": "Q6CuxYF83MfV1XgUBQBdbA",
"requestDate": "2015-01-27T00:36:12.360Z",
"status": "pending",
"token": "REFUND_REQUEST_TOKEN"
}
}

View File

@ -0,0 +1,11 @@
{
"facade": "merchant/supportRequest",
"data": [
{
"id": "TEST_REQUEST_ID",
"requestDate": "2015-01-27T00:36:12.360Z",
"status": "pending",
"token": "REFUND_REQUEST_TOKEN"
}
]
}

10
spec/fixtures/response-nodata.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
"facile": "is easy",
"diti": [
{
"requestDate": "2015-01-27T00:36:12.360Z",
"status": "pending"
}
]
}

View File

@ -1,81 +0,0 @@
require 'spec_helper'
describe BitPay::KeyUtils do
let(:key_utils) {BitPay::KeyUtils}
describe '.get_local_private_key' do
it "should get the key from the ENV['PRIV_KEY'] variable" do
stub_const('ENV', {'BITPAY_PEM' => PEM})
expect(key_utils.get_local_pem_file).to eq(PEM)
end
it 'should get the key from ~/.bitpay/bitpay.pem if env variable is not set' do
allow(File).to receive(:read).with(BitPay::PRIVATE_KEY_PATH) {PEM}
expect(key_utils.get_local_pem_file).to eq(PEM)
end
end
describe '.generate_pem' do
it 'should write a new key to ~/.bitpay/bitpay.pem' do
file = class_double("File").as_stubbed_const
double = double("Object").as_null_object
allow(file).to receive(:path).with(BitPay::BITPAY_CREDENTIALS_DIR).and_return(double)
expect(file).to receive(:open).with(BitPay::PRIVATE_KEY_PATH, 'w')
key_utils.generate_pem
end
end
describe '.retrieve_or_generate_pem' do
it 'should write a new key to ~/.bitpay/bitpay.pem if there is no existing file' do
file = class_double("File").as_stubbed_const
double = double("Object").as_null_object
allow(file).to receive(:read).with(BitPay::PRIVATE_KEY_PATH).and_throw(StandardError)
allow(file).to receive(:path).with(BitPay::BITPAY_CREDENTIALS_DIR).and_return(double)
expect(file).to receive(:open).with(BitPay::PRIVATE_KEY_PATH, 'w')
key_utils.retrieve_or_generate_pem
end
it 'should retrieve the pem if there is an existing file' do
file = class_double("File").as_stubbed_const
double = double("Object").as_null_object
allow(file).to receive(:path).with(BitPay::BITPAY_CREDENTIALS_DIR).and_return(double)
expect(file).to receive(:open).with(BitPay::PRIVATE_KEY_PATH, 'w')
key_utils.generate_pem
end
end
describe '.get_public_key_from_pem' do
it 'should generate the right public key' do
expect(key_utils.get_public_key_from_pem(PEM)).to eq(PUB_KEY)
end
it 'should get pem from the env if none is passed' do
expect(key_utils.get_public_key_from_pem(PEM)).to eq(PUB_KEY)
end
end
describe '.generate_sin_from_pem' do
let(:pem){PEM}
let(:sin){"TeyN4LPrXiG5t2yuSamKqP3ynVk3F52iHrX"}
it 'will return the right sin for the right pem' do
expect(key_utils.generate_sin_from_pem(pem)).to eq sin
end
end
context "errors when priv_key is not provided" do
before :each do
allow(File).to receive(:read).with(BitPay::PRIVATE_KEY_PATH) {nil}
end
it 'will not retrieve public key' do
expect{key_utils.get_public_key_from_pem(nil)}.to raise_error(BitPay::BitPayError)
end
end
end

View File

@ -1,10 +0,0 @@
#!/bin/bash
export RCROOTADDRESS=$1
echo $RCROOTADDRESS
export RCTESTUSER=$2
echo $RCTESTUSER
export RCTESTPASSWORD=$3
echo $RCTESTPASSWORD
export PRIV_KEY=$4
echo $PRIV_KEY

View File

@ -1,13 +1,10 @@
require 'webmock/rspec'
require 'pry'
require 'capybara/rspec'
require 'capybara/poltergeist'
require 'coveralls'
Coveralls.wear!
require File.join File.dirname(__FILE__), '..', 'lib', 'bitpay', 'client.rb'
require File.join File.dirname(__FILE__), '..', 'lib', 'bitpay', 'key_utils.rb'
require File.join File.dirname(__FILE__), '..', 'lib', 'bitpay.rb'
require File.join File.dirname(__FILE__), '..', 'lib', 'bitpay_sdk.rb'
require_relative '../config/constants.rb'
require_relative '../config/capybara.rb'
#
## Test Variables
@ -15,11 +12,23 @@ require_relative '../config/capybara.rb'
PEM = "-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEICg7E4NN53YkaWuAwpoqjfAofjzKI7Jq1f532dX+0O6QoAcGBSuBBAAK\noUQDQgAEjZcNa6Kdz6GQwXcUD9iJ+t1tJZCx7hpqBuJV2/IrQBfue8jh8H7Q/4vX\nfAArmNMaGotTpjdnymWlMfszzXJhlw==\n-----END EC PRIVATE KEY-----\n"
PUB_KEY = '038d970d6ba29dcfa190c177140fd889fadd6d2590b1ee1a6a06e255dbf22b4017'
CLIENT_ID = "TfFVQhy2hQvchv4VVG4c7j4XPa2viJ9HrR8"
CLIENT_ID = "TeyN4LPrXiG5t2yuSamKqP3ynVk3F52iHrX"
RSpec.configure do |config|
config.before :each do |example|
WebMock.allow_net_connect! if example.metadata[:type] == :feature
end
def generate_code(number)
legal_map = [*'A'..'Z'] + [*'a'..'z'] + [*0..9]
Array.new(number) { legal_map.sample }.join
end
def an_illegal_claim_code
short_code = generate_code(rand(6))
long_code = generate_code(rand(8..25))
[nil, short_code, long_code].sample
end
## Gets JSON responses from the fixtures directory
#
def get_fixture(name)
#JSON.parse(File.read(File.expand_path("../fixtures/#{name}", __FILE__)))
File.read(File.expand_path("../fixtures/#{name}", __FILE__))
end