Compare commits

...

28 Commits

Author SHA1 Message Date
Paul Daigle
cfc7cb9ccf add pair command to command line client
To support the pair command, cleaned up and extended client and
key_utils classes with more getters. Also added a param hash to the
invoice creation for extra items.

Updated Readme file
2014-10-07 15:27:33 -04:00
Paul Daigle
f64490125c remove broken cli functions, update key_utils 2014-10-07 01:06:31 -04:00
Paul Daigle
deca5e4e8b add license information 2014-10-07 00:52:27 -04:00
Paul Daigle
c8ff36fa13 Replace most ecdsa funtions with OpenSSL
This allows us to easily write a pem file to disk and recreate the key
from the pem file. We were unable to remove the ecdsa gem entirely, the
OpenSSL library does not handle EC signatures.
2014-10-06 23:38:28 -04:00
Paul Daigle
08f0a40aa2 Removing Compatibility methods
It doesn't make sense to be both cryptographically secure and insecure
in the same library.
2014-10-06 22:58:40 -04:00
Paul Daigle
2229f88db6 Use the public interface to retrieve an invoice
With this commit the following functionality is complete:
1. Using a token, pair a key
2. Create a new invoice
3. Retrieve the public invoice
2014-09-25 12:05:41 -04:00
Paul Daigle
1f0932385a Bump required ruby version for gem
Ruby 2.0 does not support required keyword arguments, but we would like
to use required keyword arguments.
2014-09-25 12:05:17 -04:00
Paul Daigle
c84c5cbbec 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-09-24 22:41:21 -04:00
Paul Daigle
27017c2a39 Rakefile to clear added api keys
Local tests are run many times a day, but the Travis.CI tests are run
against test server accounts. So the default task (for Travis.CI) does
not clean up all API keys from the local server, but there is a rake
task that can be run locally or remotely to do this for you.
2014-09-24 16:16:13 -04:00
Paul Daigle
98a71c3acf 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.
2014-09-24 16:15:54 -04:00
Paul Daigle
1276c59d9a capybara configurations 2014-09-23 17:00:09 -04:00
Paul Daigle
dd67d86b6a Refactoring key utils and tests 2014-09-19 16:18:04 -04:00
Hamish Eisler
a5fd1bcac2 reminder to validate label 2014-09-19 07:19:25 +00:00
Hamish Eisler
c653e10390 store private key to FS and update tests 2014-09-19 00:40:53 +00:00
Hamish Eisler
e91b95cf36 refactor SIN to CLIENT_ID 2014-09-12 22:31:22 +00:00
Hamish Eisler
3f4e5b4dbc more tests plus CLI enhancements 2014-09-10 23:54:07 +00:00
Hamish Eisler
89f6355394 Merge branch 'master' of github.com:heisler3030/ruby-client 2014-09-10 17:11:45 +00:00
Hamish Eisler
b5f559bf25 add more tests and cli 2014-09-10 17:08:48 +00:00
Hamish Eisler
d595427e05 remove old tests 2014-09-10 17:08:10 +00:00
Hamish Eisler
a0d3a4773d Change api docs link 2014-09-08 02:34:55 -07:00
Hamish Eisler
4f96d636c6 Merge branch 'master' of github.com:heisler3030/ruby-client 2014-09-08 09:29:58 +00:00
Hamish Eisler
473e67bce8 add tests and refactor 2014-09-08 09:28:59 +00:00
Hamish Eisler
c77cc3a26b add tests and refactor 2014-09-08 09:25:16 +00:00
Hamish Eisler
e1783b5c1c refactoring and base tests 2014-09-05 06:42:23 +00:00
Hamish Eisler
3e237970c5 working SIN derivation 2014-09-03 23:06:48 -07:00
Hamish Eisler
4e813a2b6c Merge branch 'master' of git://github.com/bitpay/ruby-client 2014-09-02 22:53:40 -07:00
Hamish Eisler
e526f84236 incomplete upgrade to API v2 2014-09-02 22:52:19 -07:00
Hamish Eisler
3eb56c025f begin bitauth updates 2014-08-16 00:10:52 -07:00
24 changed files with 766 additions and 117 deletions

3
.gitignore vendored
View File

@ -2,3 +2,6 @@
test.rb
Gemfile.lock
pkg
.c9
.ruby-version
.ruby-gemset

View File

@ -1,5 +1,2 @@
rvm:
- 1.9.2
- 1.9.3
- 2.0.0
- 2.1.0

View File

@ -1,4 +1,4 @@
Copyright (C) 2013 BitPay
Copyright (C) 2014 BitPay
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@ -13,12 +13,26 @@ 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
To create an invoice:
### Pairing with Bitpay.com
client = BitPay::Client.new 'YOUR_API_KEY'
invoice = client.post 'invoice', {:price => 10.00, :currency => 'USD'}
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.
@ -40,7 +54,7 @@ There are many options available when creating invoices, which are listed in the
To get updated information on this invoice, make a get call with the id returned:
invoice = client.get 'invoice/DGrAEmbsXe9bavBPMJ8kuk'
invoice = client.get_public_invoice(DGrAEmbsXe9bavBPMJ8kuk)'
## Testnet Usage
@ -48,11 +62,11 @@ During development and testing, take advantage of the [Bitcoin TestNet](https://
BitPay::Client.new("myAPIKey", {api_uri: "https://test.bitpay.com/api"})
Note that you will need a separate API key for `test.bitpay.com` which can be obtained by registering for a test account at https://test.bitpay.com/start
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/bitcoin-payment-gateway-api).
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).

View File

@ -1,11 +1,52 @@
require "bundler/gem_tasks"
require "rake/testtask"
require 'rspec/core/rake_task'
require 'capybara'
require 'capybara/poltergeist'
require 'mongo'
require_relative 'config/constants.rb'
require_relative 'config/capybara.rb'
RSpec::Core::RakeTask.new(:spec)
task :default => :spec
desc "Bitpay Tasks"
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
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
db = client['bitpay-dev']
coll = db['ratelimiters']
coll.remove()
puts "rate limiters cleared"
end
desc "Run specs and clear claim codes and rate_limiters."
task :spec_clear => ['spec', 'clear_claim_codes', 'clear_rate_limiters']
desc "Run all tests"
Rake::TestTask.new do |t|
t.libs << "spec"
t.test_files = FileList['test/*_test.rb','test/bitpay/*_test.rb']
t.verbose = true
end
task :default => :test

3
bin/bitpay Executable file
View File

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

View File

@ -2,6 +2,7 @@ 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'
@ -9,15 +10,26 @@ Gem::Specification.new do |s|
s.description = 'Powerful, flexible, lightweight, thread-safe interface to the BitPay developers API'
s.files = `git ls-files`.split("\n")
s.require_paths = %w[lib]
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 'minitest'
s.add_development_dependency 'webmock'
s.add_development_dependency 'yard'
end
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

5
config/capybara.rb Normal file
View File

@ -0,0 +1,5 @@
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

3
config/constants.rb Normal file
View File

@ -0,0 +1,3 @@
ROOT_ADDRESS = ENV['RCROOTADDRESS']
TEST_USER = ENV['RCTESTUSER']
TEST_PASS = ENV['RCTESTPASSWORD']

View File

@ -1,3 +1,7 @@
# 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
libdir = File.dirname(__FILE__)
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
require 'bitpay/client'
@ -10,8 +14,21 @@ module BitPay
CA_FILE = File.join File.dirname(__FILE__), 'bitpay','cacert.pem'
# Location of API
API_URI = 'https://bitpay.com/api'
API_URI = 'https://bitpay.com'
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'
class BitPayError < StandardError; end
end

63
lib/bitpay/cli.rb Normal file
View File

@ -0,0 +1,63 @@
# 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,64 +1,157 @@
# 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_relative 'key_utils'
module BitPay
# This class is used to instantiate a BitPay Client object. It is expected to be thread safe.
#
# @example
# # Create a client with your BitPay API key (obtained from the BitPay API access page at BitPay.com):
# client = BitPay::Client.new 'YOUR_API_KEY'
class Client
class BitPayError < StandardError; end
# Creates a BitPay Client object. The second parameter is a hash for overriding defaults.
#
# @return [Client]
# @example
# # Create a client with your BitPay API key (obtained from the BitPay API access page at BitPay.com):
# client = BitPay::Client.new 'YOUR_API_KEY'
def initialize(api_key, opts={})
@api_key = api_key
# # 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
# 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
# Load all the available tokens into @tokens
load_tokens
end
# Makes a GET call to the BitPay API.
# @return [Hash]
# @see get_invoice
# @example
# # Get an invoice:
# existing_invoice = client.get 'invoice/YOUR_INVOICE_ID'
def get(path)
request = Net::HTTP::Get.new @uri.path+'/'+path
request.basic_auth @api_key, ''
request['User-Agent'] = USER_AGENT
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)}"
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
(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}")
# Makes a POST call to the BitPay API.
# @return [Hash]
# @see create_invoice
# # Create an invoice:
# created_invoice = client.post 'invoice', {:price => 1.45, :currency => 'BTC'}
def post(path, params={})
# 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)
request = Net::HTTP::Post.new @uri.path+'/'+path
request.basic_auth @api_key, ''
request['User-Agent'] = USER_AGENT
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}")
end
# Build request headers and submit
request['User-Agent'] = @user_agent
request['Content-Type'] = 'application/json'
request['X-BitPay-Plugin-Info'] = 'Rubylib' + VERSION
request.body = params.to_json
request['X-Identity'] = @pub_key
response = @https.request request
JSON.parse response.body
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
end
@tokens = tokens
return tokens
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

143
lib/bitpay/key_utils.rb Normal file
View File

@ -0,0 +1,143 @@
# 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

@ -1,3 +1,7 @@
# 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
module BitPay
VERSION = '0.1.3'
VERSION = '2.0.0'
end

40
lib/harness.rb Normal file
View File

@ -0,0 +1,40 @@
# 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)

78
spec/client_spec.rb Normal file
View File

@ -0,0 +1,78 @@
require 'spec_helper'
def tokens
{"data" =>
[{"merchant" => "MERCHANTTOKEN"},
{"pos" =>"POSTOKEN"},
{"merchant/invoice" => "9kv7gGqZLoQ2fxbKEgfgndLoxwjp5na6VtGSH3sN7buX"}
]
}
end
describe BitPay::Client do
let(:bitpay_client) { BitPay::Client.new({api_uri: BitPay::TEST_API_URI}) }
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 => {})
end
describe "#initialize" do
it 'should be able to get pem file from the env' do
stub_const('ENV', {'BITPAY_PEM' => PEM})
expect {bitpay_client}.to_not raise_error
end
end
describe "#send_request" do
before do
stub_const('ENV', {'BITPAY_PEM' => PEM})
end
context "GET" 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")
end
end
context "POST" do
it 'should generate a post request' do
stub_request(:post, /#{BitPay::TEST_API_URI}.*/).to_return(:body => '{"awesome": "json"}')
bitpay_client.send_request("POST", "whatever", facade: "merchant")
expect(WebMock).to have_requested(:post, "#{BitPay::TEST_API_URI}/whatever")
end
end
end
describe "#pair_pos_client" do
it 'throws a BitPayError with the error message if the token setting fails' do
stub_const('ENV', {'BITPAY_PEM' => PEM})
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')
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"}')
end
end
describe "#create_invoice" do
subject { bitpay_client }
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"
end
end
end

View File

@ -0,0 +1,28 @@
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

37
spec/features/pos_spec.rb Normal file
View File

@ -0,0 +1,37 @@
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

@ -0,0 +1,13 @@
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

81
spec/key_utils_spec.rb Normal file
View File

@ -0,0 +1,81 @@
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

10
spec/set_constants.sh Executable file
View File

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

25
spec/spec_helper.rb Normal file
View File

@ -0,0 +1,25 @@
require 'webmock/rspec'
require 'pry'
require 'capybara/rspec'
require 'capybara/poltergeist'
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_relative '../config/constants.rb'
require_relative '../config/capybara.rb'
#
## Test Variables
#
PEM = "-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEICg7E4NN53YkaWuAwpoqjfAofjzKI7Jq1f532dX+0O6QoAcGBSuBBAAK\noUQDQgAEjZcNa6Kdz6GQwXcUD9iJ+t1tJZCx7hpqBuJV2/IrQBfue8jh8H7Q/4vX\nfAArmNMaGotTpjdnymWlMfszzXJhlw==\n-----END EC PRIVATE KEY-----\n"
PUB_KEY = '038d970d6ba29dcfa190c177140fd889fadd6d2590b1ee1a6a06e255dbf22b4017'
CLIENT_ID = "TfFVQhy2hQvchv4VVG4c7j4XPa2viJ9HrR8"
RSpec.configure do |config|
config.before :each do |example|
WebMock.allow_net_connect! if example.metadata[:type] == :feature
end
end

View File

@ -1,54 +0,0 @@
require File.join File.dirname(__FILE__), '..', 'env.rb'
USER_AGENT = 'ruby-bitpay-client '+BitPay::VERSION
def invoice_create_body
{:price => 1, :currency => 'USD'}
end
def invoice_response_body
{
"id" => "DGrAEmbsXe9bavBPMJ8kuk",
"url" => "https://bitpay.com/invoice?id=DGrAEmbsXe9bavBPMJ8kuk",
"status" => "new",
"btcPrice" => "0.0495",
"price" => 10,
"currency" => "USD",
"invoiceTime" => 1383265343674,
"expirationTime" => 1383266243674,
"currentTime" => 1383265957613
}
end
stub_request(:post, "https://KEY:@bitpay.com/api/invoice/create").
with(
:headers => {'User-Agent'=>USER_AGENT, 'Content-Type' => 'application/json'},
:body => invoice_create_body
).
to_return(:body => invoice_response_body.to_json)
stub_request(:get, "https://KEY:@bitpay.com/api/invoice/DGrAEmbsXe9bavBPMJ8kuk").
with(:headers => {'User-Agent'=>USER_AGENT}).
to_return(:body => invoice_response_body.to_json)
describe BitPay::Client do
before do
@client = BitPay::Client.new 'KEY'
end
describe 'post' do
it 'creates invoice' do
response = @client.post 'invoice/create', invoice_create_body
response.class.must_equal Hash
response['id'].must_equal 'DGrAEmbsXe9bavBPMJ8kuk'
end
end
describe 'get' do
it 'retreives invoice' do
response = @client.get 'invoice/DGrAEmbsXe9bavBPMJ8kuk'
response.class.must_equal Hash
response['id'].must_equal 'DGrAEmbsXe9bavBPMJ8kuk'
end
end
end

View File

@ -1,7 +0,0 @@
require 'addressable/uri'
require 'json'
require 'minitest/autorun'
require 'webmock'
require './lib/bitpay.rb'
include WebMock::API